replay: add determinism foundation and double-run verification (Phase 0)

Introduces the state-checksum machinery the replay feature rests on, and a
test that proves the simulation is deterministic for a given seed/binary.

- StateChecksum: FNV-1a Hasher (bit-pattern float hashing, -0 normalization,
  length-tagged strings) plus a portable mt19937 state fingerprint.
- BeltSystem/BuildingSystem: appendChecksum(Hasher&) folding transport and
  building/site/occupancy state in deterministic (sorted/insertion) order.
- Simulation: rngFingerprint() (cheap, for the future file checksum) and
  computeStateChecksum() (full state: RNG, scalars, wave/schematic/unlock
  state, subsystems, and ECS position/health/facing/body/scrap/identity).
- DeterminismTest: Hasher unit tests, RNG-fingerprint tests, and a double-run
  test asserting identical per-tick full-state checksums from one seed; plus a
  different-seed divergence guard.

Exit criteria met: double-run determinism test passes; full suite green
(334 cases / 3346 assertions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUsFgd2Ga6pmLz8giS8WUn
This commit is contained in:
2026-06-30 17:44:17 +02:00
parent a97687154e
commit a45df902aa
11 changed files with 596 additions and 0 deletions

View File

@@ -24,6 +24,7 @@
class AiSystem;
class BuildingSystem;
class Hasher;
class CombatSystem;
class DynamicBodySystem;
class MovementIntentSystem;
@@ -88,6 +89,16 @@ public:
bool isRecipeUnlocked(const std::string& recipeId) const;
bool isItemUnlocked(const std::string& itemId) const;
// -- Determinism (see docs/replay_design.md) -----------------------------
// 64-bit fingerprint of the RNG stream state. Cheap; written to the replay
// file periodically + after each command for desync detection.
unsigned long long rngFingerprint() const;
// 64-bit fingerprint of the full simulation state (RNG, scalars, buildings,
// belts, and ECS component state). Used by the double-run determinism test;
// a superset of rngFingerprint().
unsigned long long computeStateChecksum() const;
// Checks affordability, deducts building blocks, and places the building.
// Returns the new entity id, or kInvalidBuildingId if blocks are insufficient.
BuildingId tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
@@ -149,6 +160,11 @@ private:
std::map<std::string, SchematicState> m_schematicLevels;
std::map<std::string, SchematicState> m_moduleSchematicLevels;
// Determinism helpers — fold sub-state into the hasher in deterministic order.
static void appendSchematicMap(Hasher& hasher,
const std::map<std::string, SchematicState>& levels);
static void appendStringSet(Hasher& hasher, const std::set<std::string>& ids);
// Explicitly unlocked assembler recipe schematics (REQ-LOCK-EXPLICIT).
std::set<std::string> m_unlockedRecipeSchematicIds;