Files
Ange-Marie MAURIN - A2M Systèmes ab219ad611 feat(core): simulation déterministe, enregistrement et rejeu
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).
2026-11-06 19:50:56 +01:00

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);
}