Aufzeichnung an den Weltschritt koppeln

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>
This commit is contained in:
Steven Wroblewski
2026-10-01 10:34:30 +02:00
parent ef728cd100
commit 5dacbf2222
4 changed files with 97 additions and 19 deletions
+57 -10
View File
@@ -4,6 +4,7 @@ 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);
@@ -31,12 +32,28 @@ fn recorded_session() -> (Recording, World) {
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.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();
@@ -62,14 +79,14 @@ fn verify_accepts_a_faithful_recording() {
#[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);
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!(expected, world.state_hash());
assert_ne!(actual, expected);
assert_eq!(actual, world.state_hash());
assert_ne!(expected, actual);
}
other => panic!("expected a divergence, got {other:?}"),
}
@@ -85,10 +102,10 @@ fn a_recording_can_start_in_the_middle_of_a_session() {
let mut recording = Recording::start(&world);
for tick in 50..120 {
let commands = commands_for(&world, tick);
recording.push_tick(&commands);
world.step(&commands);
recording.step(&mut world, &commands);
}
assert_eq!(recording.tick_count(), 70);
assert_eq!(recording.end_tick(), 120);
assert_eq!(recording.replay(), Ok(world));
}
@@ -100,6 +117,23 @@ fn an_empty_recording_replays_to_its_starting_state() {
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();
@@ -133,3 +167,16 @@ fn damaged_recordings_are_rejected() {
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))
);
}