use grovewright_core::codec::DecodeError; use grovewright_core::command::Command; use grovewright_core::inventory::ITEM_STONE; use grovewright_core::mapgen::outcrop_origin; use grovewright_core::player::PlayerId; use grovewright_core::replay::{Recording, RecordingError, RECORDING_VERSION}; use grovewright_core::world::World; const ADA: PlayerId = PlayerId(0); /// Commands of tick `tick` in a short scripted session. fn commands_for(world: &World, tick: u32) -> Vec { if tick == 0 { vec![Command::Join { key: [1; 16], name: "Ada".to_owned(), }] } else if tick.is_multiple_of(7) { vec![Command::Dig { player: ADA, pos: outcrop_origin(world.map()), }] } else { vec![] } } /// Plays 200 ticks from a fresh world and records them. fn recorded_session() -> (Recording, World) { let mut world = World::new(11); let mut recording = Recording::start(&world); for tick in 0..200 { let commands = commands_for(&world, tick); recording.push_tick(&commands); world.step(&commands); } (recording, world) } #[test] fn replaying_reaches_the_same_state() { let (recording, world) = recorded_session(); assert_eq!(recording.tick_count(), 200); assert_eq!(world.player(ADA).unwrap().inventory.count(ITEM_STONE), 1); assert_eq!(recording.replay(), Ok(world)); } #[test] fn a_recording_survives_encoding() { let (recording, world) = recorded_session(); let bytes = recording.to_bytes(&world); let (decoded, expected_hash) = Recording::from_bytes(&bytes).unwrap(); assert_eq!(decoded, recording); assert_eq!(expected_hash, world.state_hash()); } #[test] fn verify_accepts_a_faithful_recording() { let (recording, world) = recorded_session(); assert_eq!(Recording::verify(&recording.to_bytes(&world)), Ok(world)); } #[test] fn verify_reports_a_replay_that_ends_in_another_state() { let (recording, mut world) = recorded_session(); // One tick more than was recorded: the claimed end state is unreachable. world.step(&[]); let bytes = recording.to_bytes(&world); match Recording::verify(&bytes) { Err(RecordingError::Diverged { expected, actual }) => { assert_eq!(expected, world.state_hash()); assert_ne!(actual, expected); } other => panic!("expected a divergence, got {other:?}"), } } #[test] fn a_recording_can_start_in_the_middle_of_a_session() { let mut world = World::new(11); for tick in 0..50 { let commands = commands_for(&world, tick); world.step(&commands); } let mut recording = Recording::start(&world); for tick in 50..120 { let commands = commands_for(&world, tick); recording.push_tick(&commands); world.step(&commands); } assert_eq!(recording.tick_count(), 70); assert_eq!(recording.replay(), Ok(world)); } #[test] fn an_empty_recording_replays_to_its_starting_state() { let world = World::new(3); let recording = Recording::start(&world); assert_eq!(recording.tick_count(), 0); assert_eq!(Recording::verify(&recording.to_bytes(&world)), Ok(world)); } #[test] fn damaged_recordings_are_rejected() { let (recording, world) = recorded_session(); let bytes = recording.to_bytes(&world); assert_eq!( Recording::from_bytes(b""), Err(RecordingError::NotARecording) ); assert_eq!( Recording::from_bytes(&world.to_snapshot()), Err(RecordingError::NotARecording) ); let mut other_version = bytes.clone(); other_version[4..8].copy_from_slice(&(RECORDING_VERSION + 1).to_le_bytes()); assert_eq!( Recording::from_bytes(&other_version), Err(RecordingError::UnsupportedVersion(RECORDING_VERSION + 1)) ); assert_eq!( Recording::from_bytes(&bytes[..bytes.len() - 1]), Err(RecordingError::Corrupt(DecodeError::UnexpectedEnd)) ); let mut padded = bytes.clone(); padded.push(0); assert_eq!( Recording::from_bytes(&padded), Err(RecordingError::Corrupt(DecodeError::TrailingBytes)) ); }