Abnahmetests für den Gleichschritt

Fünf Tests prüfen über tausende Takte mit vier Spielern, dass gleiche
Befehle dieselbe Welt ergeben: im direkten Vergleich, nach einem
Snapshot mitten in der Sitzung, beim Wiederholen einer Aufzeichnung
und nach dem Weg über die Kodierung.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-09-30 21:35:44 +02:00
parent 40bef858f3
commit 7aae093f82
+153
View File
@@ -0,0 +1,153 @@
//! 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);
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.push_tick(commands);
world.step(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());
}