Files
Grovewright/core/tests/replay.rs
T
Steven Wroblewski f2e4c91082 Regel-Kennung und Aufzeichnung Version 3
Die eingefrorenen Skripte ziehen von den festgeschriebenen Tests in den
Kern. Aus ihren Endzuständen leitet sich die Regel-Kennung ab, die sich
mit jeder Regel ändert, die ein Skript berührt. Aufzeichnungen tragen
sie und werden mit anderen Regeln abgelehnt, statt eine scheinbare
Abweichung zu melden. Fremde Aufzeichnungen dürfen höchstens 30 Tage
lang sein. Ein Test hält fest, dass zu lange Namen beim Dekodieren
abgelehnt werden.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-10-01 18:00:00 +02:00

239 lines
7.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, MAX_RECORDING_TICKS, RECORDING_VERSION};
use grovewright_core::rules::rules_id;
use grovewright_core::snapshot::SnapshotError;
use grovewright_core::world::World;
const ADA: PlayerId = PlayerId(0);
/// Offset of the starting snapshot in an encoded recording: marker, version,
/// rules identity, expected hash and the snapshot's length come first.
const START: usize = 4 + 4 + 8 + 8 + 4;
/// 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, the version and the rules
// identity.
bytes[16] ^= 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);
// Decoding derives the start tick from the stored start world. The
// decoded recording must end at the same tick and equal the original.
let (decoded, _) = Recording::from_bytes(&recording.to_bytes(&world)).unwrap();
assert_eq!(decoded.end_tick(), 120);
assert_eq!(decoded, recording);
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, rules identity, expected
// hash and its own length: 4 + 4 + 8 + 8 + 4 bytes.
bytes[START + 10] ^= 0x01;
assert_eq!(
Recording::from_bytes(&bytes),
Err(RecordingError::BadStart(SnapshotError::ChecksumMismatch))
);
}
#[test]
fn a_recording_of_other_rules_is_refused_before_replaying() {
let (recording, world) = recorded_session();
let mut bytes = recording.to_bytes(&world);
// The rules identity follows the marker and the version.
bytes[8] ^= 0x01;
assert_eq!(
Recording::from_bytes(&bytes),
Err(RecordingError::OtherRules {
recorded: rules_id() ^ 0x01,
current: rules_id()
})
);
}
/// The encoding of `recording` with its tick count replaced by `ticks`. The
/// tick count follows the starting snapshot.
fn with_tick_count(recording: &Recording, world: &World, ticks: u32) -> Vec<u8> {
let mut bytes = recording.to_bytes(world);
let length = u32::from_le_bytes(bytes[START - 4..START].try_into().unwrap()) as usize;
let at = START + length;
bytes[at..at + 4].copy_from_slice(&ticks.to_le_bytes());
bytes
}
#[test]
fn a_recording_longer_than_thirty_days_is_refused() {
let world = World::new(3);
let recording = Recording::start(&world);
let (longest, _) =
Recording::from_bytes(&with_tick_count(&recording, &world, MAX_RECORDING_TICKS)).unwrap();
assert_eq!(longest.tick_count(), MAX_RECORDING_TICKS);
assert_eq!(
Recording::from_bytes(&with_tick_count(
&recording,
&world,
MAX_RECORDING_TICKS + 1
)),
Err(RecordingError::Corrupt(DecodeError::InvalidValue(
"tick count"
)))
);
}