Virgule fixe Q16.16, générateur PCG32 à graine explicite, horloge à pas fixe exacte, simulation d'un micro-mode de démonstration (marqué SIMULÉ), empreinte FNV-1a de l'état, enregistrement binaire versionné des entrées, rejeu avec d'autres réglages de ressenti et détection du premier tick divergent. Empreinte de référence multiplateforme figée. 22 tests (Debug, Release, ASan).
109 lines
3.8 KiB
C++
109 lines
3.8 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
#include "sequa/core/sim/replay.hpp"
|
|
|
|
#include "scripted_session.hpp"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace sequa::core::sim;
|
|
|
|
namespace {
|
|
|
|
constexpr Tick session_ticks = 60 * 60; // une minute a 60 Hz
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Replay : le rejeu est identique bit a bit a la session enregistree")
|
|
{
|
|
SimConfig config;
|
|
config.seed = 2026;
|
|
SessionRecorder recorder(config);
|
|
std::vector<std::uint64_t> live_hashes;
|
|
for (Tick tick = 0; tick < session_ticks; ++tick) {
|
|
recorder.step(testing::scripted_input(tick));
|
|
live_hashes.push_back(recorder.simulation().state_hash());
|
|
}
|
|
|
|
const Recording& recording = recorder.recording();
|
|
REQUIRE(recording.is_valid());
|
|
CHECK(recording.tick_count == session_ticks);
|
|
CHECK(recording.inputs.size() < session_ticks); // les ticks neutres ne sont pas stockes
|
|
|
|
const ReplayResult result = replay(recording, config.feel);
|
|
CHECK(result.hashes == live_hashes);
|
|
CHECK(result.final_hash == recorder.simulation().state_hash());
|
|
}
|
|
|
|
TEST_CASE("Replay : meme session, autre reglage de ressenti, divergence localisee")
|
|
{
|
|
const Recording recording = testing::record_scripted_session(7, session_ticks);
|
|
const ReplayResult reference = replay(recording, FeelParams{});
|
|
|
|
FeelParams floaty;
|
|
floaty.jump_velocity = Fixed::from_int(14);
|
|
const ReplayResult variant = replay(recording, floaty);
|
|
|
|
const auto divergence = first_divergence(reference, variant);
|
|
REQUIRE(divergence.has_value());
|
|
// Le premier saut a lieu au tick 0 : les deux versions divergent des ce tick.
|
|
CHECK(*divergence == 0);
|
|
CHECK(first_divergence(reference, replay(recording, FeelParams{})) == std::nullopt);
|
|
}
|
|
|
|
TEST_CASE("Replay : serialisation aller-retour exacte")
|
|
{
|
|
const Recording recording = testing::record_scripted_session(11, session_ticks);
|
|
const std::vector<std::byte> bytes = serialize(recording);
|
|
const auto loaded = deserialize_recording(bytes);
|
|
REQUIRE(loaded.has_value());
|
|
CHECK(*loaded == recording);
|
|
CHECK(replay(*loaded, FeelParams{}).final_hash == replay(recording, FeelParams{}).final_hash);
|
|
}
|
|
|
|
TEST_CASE("Replay : un enregistrement corrompu est refuse")
|
|
{
|
|
const Recording recording = testing::record_scripted_session(11, 600);
|
|
const std::vector<std::byte> bytes = serialize(recording);
|
|
|
|
SUBCASE("tronque")
|
|
{
|
|
const std::vector<std::byte> truncated(bytes.begin(), bytes.end() - 1);
|
|
CHECK_FALSE(deserialize_recording(truncated).has_value());
|
|
}
|
|
SUBCASE("mauvais magique")
|
|
{
|
|
std::vector<std::byte> bad = bytes;
|
|
bad[0] = std::byte{0};
|
|
CHECK_FALSE(deserialize_recording(bad).has_value());
|
|
}
|
|
SUBCASE("ticks desordonnes")
|
|
{
|
|
Recording unordered = recording;
|
|
REQUIRE(unordered.inputs.size() >= 2);
|
|
std::swap(unordered.inputs[0], unordered.inputs[1]);
|
|
CHECK_FALSE(deserialize_recording(serialize(unordered)).has_value());
|
|
}
|
|
SUBCASE("vide")
|
|
{
|
|
CHECK_FALSE(deserialize_recording({}).has_value());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Replay : empreinte de reference multiplateforme")
|
|
{
|
|
// Valeur figee : toute plateforme ou tout compilateur doit retrouver exactement cette
|
|
// empreinte (docs, §10.1 et §16). Si elle change, c'est soit une modification voulue
|
|
// de la simulation (mettre a jour la valeur et le format d'etat), soit une perte de
|
|
// determinisme a corriger.
|
|
const Recording recording = testing::record_scripted_session(2026, session_ticks);
|
|
const ReplayResult result = replay(recording, FeelParams{});
|
|
MESSAGE("empreinte finale : " << result.final_hash << ", score " << result.final_world.score);
|
|
CHECK(result.final_hash == 15306857814008723692ull);
|
|
CHECK(result.final_world.score == 6);
|
|
}
|