Files
Grovewright/core/tests/replay.rs
T
Steven Wroblewski 40bef858f3 Aufzeichnung und Wiederholung von Sitzungen
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>
2026-09-30 21:27:36 +02:00

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))
);
}