//! 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()); }