Files
Grovewright/core/tests/determinism.rs
T
Steven Wroblewski d84c338291 Abnahmetest prüft, dass die Startzahl die Karte formt
Der Vergleich der Prüfsummen allein konnte nicht fehlschlagen, weil die
Startzahl selbst in die Prüfsumme eingeht. Jetzt vergleicht der Test
auch die Karten beider Welten.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-30 21:48:42 +02:00

155 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.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());
}