//! Phase H gate: replay determinism. The web CI minimum is "1,000 simulated //! matches" with "0 replay hash mismatches". A match is a pure function of its //! seed, roster, and ordered inputs, so re-running the recorded inputs must //! reproduce the final hash exactly. use game_runtime::{replay, run_scripted, solo_roster, duel_roster}; use protocol::Action; use world_model::Rng; /// Build a varied but deterministic script for a given seed. fn script(seed: u64) -> Vec> { let mut rng = Rng::derive(seed, "script"); let mut turns = Vec::new(); for _ in 0..6 { let mut subs = Vec::new(); for pid in 1..=2u32 { let a = match rng.below(5) { 0 => Action::Move { dx: rng.range_i64(-1, 1) as i32, dy: rng.range_i64(-1, 1) as i32 }, 1 => Action::Cast, 2 => Action::Attack { target: if pid == 1 { 2 } else { 1 } }, 3 => Action::Inspect { target: if pid == 1 { 2 } else { 1 } }, _ => Action::Wait, }; subs.push((pid, a)); } turns.push(subs); } turns } #[test] fn one_thousand_matches_replay_with_zero_drift() { let mut mismatches = 0u32; for seed in 0..1000u64 { let roster = if seed % 2 == 0 { solo_roster("p") } else { duel_roster("a", "b") }; let scripts = script(seed); let (m, log) = run_scripted(seed, &roster, &scripts); let again = replay(seed, &roster, &log.turns); if m.replay.final_hash != again.final_hash { mismatches += 1; } // Per-turn hashes must also agree. for (a, b) in m.replay.turns.iter().zip(again.turns.iter()) { if a.turn_hash != b.turn_hash { mismatches += 1; } } } assert_eq!(mismatches, 0, "replay hash mismatches across 1000 matches"); } #[test] fn re_executing_same_seed_twice_is_identical() { for seed in [1u64, 7, 99, 12345, 0xdead_beef] { let roster = solo_roster("p"); let (a, _) = run_scripted(seed, &roster, &script(seed)); let (b, _) = run_scripted(seed, &roster, &script(seed)); assert_eq!(a.replay.final_hash, b.replay.final_hash, "seed {seed}"); } }