40bef858f3
Eine Aufzeichnung hält den Startzustand und die Befehle jedes Takts fest. Wiederholen ergibt exakt dieselbe Welt; verify meldet, wenn das Ende von der aufgezeichneten Prüfsumme abweicht. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
136 lines
4.1 KiB
Rust
136 lines
4.1 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::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.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))
|
|
);
|
|
}
|