Files
Grovewright/core/tests/determinism.rs
Steven Wroblewski 5dacbf2222 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>
2026-10-01 10:34:30 +02:00

154 lines
4.6 KiB
Rust

//! The central promise of the core: the same commands always produce the
//! same world.
use grovewright_core::codec::{Reader, Writer};
use grovewright_core::command::Command;
use grovewright_core::inventory::ITEM_STONE;
use grovewright_core::map::Pos;
use grovewright_core::mapgen::TEST_MAP_SIZE;
use grovewright_core::player::PlayerId;
use grovewright_core::replay::Recording;
use grovewright_core::rng::Rng;
use grovewright_core::world::World;
/// Edge length of the square around the map centre in which the script digs.
const DIG_AREA: u32 = 24;
/// Builds a busy, repeatable command stream: four players join, dig at
/// random places near the centre, and now and then leave and return.
fn script(seed: u64, ticks: usize) -> Vec<Vec<Command>> {
let mut rng = Rng::new(seed);
let first = (TEST_MAP_SIZE - DIG_AREA) / 2;
let mut out = Vec::with_capacity(ticks);
for tick in 0..ticks {
let mut commands = Vec::new();
if tick < 4 {
commands.push(Command::Join {
key: [tick as u8 + 1; 16],
name: format!("P{tick}"),
});
}
for player in 0..4 {
if rng.below(3) == 0 {
let x = first + rng.below(DIG_AREA);
let y = first + rng.below(DIG_AREA);
commands.push(Command::Dig {
player: PlayerId(player),
pos: Pos::new(x as i32, y as i32),
});
}
}
if rng.below(200) == 0 {
commands.push(Command::Leave {
player: PlayerId(rng.below(4) as u8),
});
}
if rng.below(200) == 0 {
let n = rng.below(4) as u8;
commands.push(Command::Join {
key: [n + 1; 16],
name: format!("P{n}"),
});
}
out.push(commands);
}
out
}
fn stones(world: &World) -> u32 {
world
.players()
.iter()
.map(|player| player.inventory.count(ITEM_STONE))
.sum()
}
#[test]
fn the_same_commands_give_the_same_world() {
let script = script(2026, 3_000);
let mut a = World::new(99);
let mut b = World::new(99);
for (tick, commands) in script.iter().enumerate() {
assert_eq!(
a.step(commands),
b.step(commands),
"events differ at tick {tick}"
);
if tick.is_multiple_of(100) {
assert_eq!(
a.state_hash(),
b.state_hash(),
"state differs at tick {tick}"
);
}
}
assert_eq!(a, b);
assert!(
stones(&a) > 50,
"the script should break plenty of rock, broke {}",
stones(&a)
);
}
#[test]
fn a_different_seed_gives_a_different_world() {
let script = script(2026, 200);
let mut a = World::new(1);
let mut b = World::new(2);
assert_ne!(a.map(), b.map(), "the seed must shape the map");
for commands in &script {
a.step(commands);
b.step(commands);
}
assert_ne!(a.state_hash(), b.state_hash());
}
#[test]
fn a_world_restored_in_the_middle_of_a_session_stays_in_step() {
let script = script(7, 2_000);
let mut original = World::new(5);
for commands in &script[..1_000] {
original.step(commands);
}
let mut restored = World::from_snapshot(&original.to_snapshot()).unwrap();
for commands in &script[1_000..] {
assert_eq!(original.step(commands), restored.step(commands));
}
assert_eq!(original.state_hash(), restored.state_hash());
}
#[test]
fn a_recorded_session_replays_to_the_same_hash() {
let script = script(31, 2_000);
let mut world = World::new(8);
let mut recording = Recording::start(&world);
for commands in &script {
recording.step(&mut world, commands);
}
let bytes = recording.to_bytes(&world);
let replayed = Recording::verify(&bytes).expect("the recording replays cleanly");
assert_eq!(replayed.state_hash(), world.state_hash());
}
#[test]
fn commands_sent_as_bytes_give_the_same_world() {
let script = script(3, 1_500);
let mut local = World::new(21);
let mut remote = World::new(21);
for commands in &script {
let mut w = Writer::new();
for command in commands {
command.encode(&mut w);
}
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
let mut received = Vec::new();
while r.remaining() > 0 {
received.push(Command::decode(&mut r).expect("command decodes"));
}
local.step(commands);
remote.step(&received);
}
assert_eq!(local.state_hash(), remote.state_hash());
}