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:
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user