diff --git a/core/tests/determinism.rs b/core/tests/determinism.rs new file mode 100644 index 0000000..011a49a --- /dev/null +++ b/core/tests/determinism.rs @@ -0,0 +1,153 @@ +//! The central promise of the core: the same commands always produce the +//! same world. + +use grovewright_core::codec::{Reader, Writer}; +use grovewright_core::command::Command; +use grovewright_core::inventory::ITEM_STONE; +use grovewright_core::map::Pos; +use grovewright_core::mapgen::TEST_MAP_SIZE; +use grovewright_core::player::PlayerId; +use grovewright_core::replay::Recording; +use grovewright_core::rng::Rng; +use grovewright_core::world::World; + +/// Edge length of the square around the map centre in which the script digs. +const DIG_AREA: u32 = 24; + +/// Builds a busy, repeatable command stream: four players join, dig at +/// random places near the centre, and now and then leave and return. +fn script(seed: u64, ticks: usize) -> Vec> { + let mut rng = Rng::new(seed); + let first = (TEST_MAP_SIZE - DIG_AREA) / 2; + let mut out = Vec::with_capacity(ticks); + for tick in 0..ticks { + let mut commands = Vec::new(); + if tick < 4 { + commands.push(Command::Join { + key: [tick as u8 + 1; 16], + name: format!("P{tick}"), + }); + } + for player in 0..4 { + if rng.below(3) == 0 { + let x = first + rng.below(DIG_AREA); + let y = first + rng.below(DIG_AREA); + commands.push(Command::Dig { + player: PlayerId(player), + pos: Pos::new(x as i32, y as i32), + }); + } + } + if rng.below(200) == 0 { + commands.push(Command::Leave { + player: PlayerId(rng.below(4) as u8), + }); + } + if rng.below(200) == 0 { + let n = rng.below(4) as u8; + commands.push(Command::Join { + key: [n + 1; 16], + name: format!("P{n}"), + }); + } + out.push(commands); + } + out +} + +fn stones(world: &World) -> u32 { + world + .players() + .iter() + .map(|player| player.inventory.count(ITEM_STONE)) + .sum() +} + +#[test] +fn the_same_commands_give_the_same_world() { + let script = script(2026, 3_000); + let mut a = World::new(99); + let mut b = World::new(99); + for (tick, commands) in script.iter().enumerate() { + assert_eq!( + a.step(commands), + b.step(commands), + "events differ at tick {tick}" + ); + if tick.is_multiple_of(100) { + assert_eq!( + a.state_hash(), + b.state_hash(), + "state differs at tick {tick}" + ); + } + } + assert_eq!(a, b); + assert!( + stones(&a) > 50, + "the script should break plenty of rock, broke {}", + stones(&a) + ); +} + +#[test] +fn a_different_seed_gives_a_different_world() { + let script = script(2026, 200); + let mut a = World::new(1); + let mut b = World::new(2); + for commands in &script { + a.step(commands); + b.step(commands); + } + assert_ne!(a.state_hash(), b.state_hash()); +} + +#[test] +fn a_world_restored_in_the_middle_of_a_session_stays_in_step() { + let script = script(7, 2_000); + let mut original = World::new(5); + for commands in &script[..1_000] { + original.step(commands); + } + let mut restored = World::from_snapshot(&original.to_snapshot()).unwrap(); + for commands in &script[1_000..] { + assert_eq!(original.step(commands), restored.step(commands)); + } + assert_eq!(original.state_hash(), restored.state_hash()); +} + +#[test] +fn a_recorded_session_replays_to_the_same_hash() { + let script = script(31, 2_000); + let mut world = World::new(8); + let mut recording = Recording::start(&world); + for commands in &script { + recording.push_tick(commands); + world.step(commands); + } + let bytes = recording.to_bytes(&world); + let replayed = Recording::verify(&bytes).expect("the recording replays cleanly"); + assert_eq!(replayed.state_hash(), world.state_hash()); +} + +#[test] +fn commands_sent_as_bytes_give_the_same_world() { + let script = script(3, 1_500); + let mut local = World::new(21); + let mut remote = World::new(21); + for commands in &script { + let mut w = Writer::new(); + for command in commands { + command.encode(&mut w); + } + let bytes = w.into_bytes(); + let mut r = Reader::new(&bytes); + let mut received = Vec::new(); + while r.remaining() > 0 { + received.push(Command::decode(&mut r).expect("command decodes")); + } + local.step(commands); + remote.step(&received); + } + assert_eq!(local.state_hash(), remote.state_hash()); +}