M1a: deterministischer Rust-Kern #3
@@ -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