5dacbf2222
Recording::step führt einen Takt aus und zeichnet ihn auf. Andere Befehle als die ausgeführten können so nicht in eine Aufzeichnung geraten. Aufzeichnung und Welt prüfen gegenseitig ihren Takt, und ein beschädigter Startzustand fällt schon beim Einlesen auf. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
183 lines
5.6 KiB
Rust
183 lines
5.6 KiB
Rust
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::snapshot::SnapshotError;
|
|
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<Command> {
|
|
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.step(&mut world, &commands);
|
|
}
|
|
(recording, world)
|
|
}
|
|
|
|
#[test]
|
|
fn stepping_through_a_recording_gives_the_events_of_the_world() {
|
|
let mut plain = World::new(11);
|
|
let mut recorded = World::new(11);
|
|
let mut recording = Recording::start(&recorded);
|
|
for tick in 0..30 {
|
|
let commands = commands_for(&plain, tick);
|
|
assert_eq!(
|
|
recording.step(&mut recorded, &commands),
|
|
plain.step(&commands),
|
|
"tick {tick}"
|
|
);
|
|
}
|
|
assert_eq!(recorded, plain);
|
|
assert_eq!(recording.end_tick(), 30);
|
|
}
|
|
|
|
#[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, world) = recorded_session();
|
|
let mut bytes = recording.to_bytes(&world);
|
|
// The expected state hash follows the marker and the version.
|
|
bytes[8] ^= 0x01;
|
|
match Recording::verify(&bytes) {
|
|
Err(RecordingError::Diverged { expected, actual }) => {
|
|
assert_eq!(actual, world.state_hash());
|
|
assert_ne!(expected, actual);
|
|
}
|
|
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.step(&mut world, &commands);
|
|
}
|
|
assert_eq!(recording.tick_count(), 70);
|
|
assert_eq!(recording.end_tick(), 120);
|
|
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]
|
|
#[should_panic(expected = "the world is not at the tick this recording has reached")]
|
|
fn a_recording_refuses_a_world_it_does_not_follow() {
|
|
let mut world = World::new(3);
|
|
let mut recording = Recording::start(&world);
|
|
world.step(&[]);
|
|
recording.step(&mut world, &[]);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "the end state is not at the tick this recording has reached")]
|
|
fn a_recording_refuses_an_end_state_at_another_tick() {
|
|
let (recording, mut world) = recorded_session();
|
|
world.step(&[]);
|
|
recording.to_bytes(&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))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_recording_with_a_damaged_start_is_rejected() {
|
|
let (recording, world) = recorded_session();
|
|
let mut bytes = recording.to_bytes(&world);
|
|
// The snapshot starts after marker, version, expected hash and its own
|
|
// length: 4 + 4 + 8 + 4 bytes.
|
|
bytes[20 + 10] ^= 0x01;
|
|
assert_eq!(
|
|
Recording::from_bytes(&bytes),
|
|
Err(RecordingError::BadStart(SnapshotError::ChecksumMismatch))
|
|
);
|
|
}
|