311516dd0a
Der Snapshot enthält den vollständigen Zustand der Welt hinter einem Kopf mit Format und Version. Er dient als Speicherstand und als Grundlage der Prüfsumme. Das Konzept ist entsprechend korrigiert: Der Speicherstand ist der vollständige Zustand, nicht Startzahl plus Änderungen. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
122 lines
3.4 KiB
Rust
122 lines
3.4 KiB
Rust
use grovewright_core::codec::DecodeError;
|
|
use grovewright_core::command::Command;
|
|
use grovewright_core::map::Pos;
|
|
use grovewright_core::mapgen::outcrop_origin;
|
|
use grovewright_core::player::PlayerId;
|
|
use grovewright_core::snapshot::{SnapshotError, SNAPSHOT_VERSION};
|
|
use grovewright_core::world::World;
|
|
|
|
const ADA: PlayerId = PlayerId(0);
|
|
|
|
fn rock(world: &World) -> Pos {
|
|
outcrop_origin(world.map())
|
|
}
|
|
|
|
/// A world at tick 12 with one player who has hit a rock once.
|
|
fn played_world() -> World {
|
|
let mut world = World::new(7);
|
|
world.step(&[Command::Join {
|
|
key: [3; 16],
|
|
name: "Ada".to_owned(),
|
|
}]);
|
|
let pos = rock(&world);
|
|
world.step(&[Command::Dig { player: ADA, pos }]);
|
|
for _ in 0..10 {
|
|
world.step(&[]);
|
|
}
|
|
world
|
|
}
|
|
|
|
#[test]
|
|
fn a_snapshot_restores_an_identical_world() {
|
|
let world = played_world();
|
|
let restored = World::from_snapshot(&world.to_snapshot()).unwrap();
|
|
assert_eq!(restored, world);
|
|
assert_eq!(restored.state_hash(), world.state_hash());
|
|
}
|
|
|
|
#[test]
|
|
fn the_state_hash_covers_every_part_of_the_state() {
|
|
let base = played_world();
|
|
let pos = rock(&base);
|
|
|
|
let mut waited = base.clone();
|
|
waited.step(&[]);
|
|
assert_ne!(waited.state_hash(), base.state_hash(), "tick");
|
|
|
|
let mut dug = base.clone();
|
|
dug.step(&[Command::Dig { player: ADA, pos }]);
|
|
assert_ne!(dug.state_hash(), waited.state_hash(), "map and cooldown");
|
|
|
|
let mut left = base.clone();
|
|
left.step(&[Command::Leave { player: ADA }]);
|
|
assert_ne!(left.state_hash(), waited.state_hash(), "players");
|
|
|
|
assert_ne!(
|
|
World::new(1).state_hash(),
|
|
World::new(2).state_hash(),
|
|
"seed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn equal_worlds_have_equal_hashes() {
|
|
assert_eq!(played_world().state_hash(), played_world().state_hash());
|
|
}
|
|
|
|
#[test]
|
|
fn a_restored_world_continues_exactly_like_the_original() {
|
|
let mut original = played_world();
|
|
let mut restored = World::from_snapshot(&original.to_snapshot()).unwrap();
|
|
let pos = rock(&original);
|
|
for tick in 0..40u32 {
|
|
let commands = if tick.is_multiple_of(6) {
|
|
vec![Command::Dig { player: ADA, pos }]
|
|
} else {
|
|
vec![]
|
|
};
|
|
assert_eq!(original.step(&commands), restored.step(&commands));
|
|
}
|
|
assert_eq!(original.state_hash(), restored.state_hash());
|
|
assert_eq!(original, restored);
|
|
}
|
|
|
|
#[test]
|
|
fn bytes_that_are_not_a_snapshot_are_rejected() {
|
|
assert_eq!(World::from_snapshot(b""), Err(SnapshotError::NotASnapshot));
|
|
assert_eq!(
|
|
World::from_snapshot(b"GR"),
|
|
Err(SnapshotError::NotASnapshot)
|
|
);
|
|
assert_eq!(
|
|
World::from_snapshot(b"nope, not a save"),
|
|
Err(SnapshotError::NotASnapshot)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn snapshots_of_another_version_are_rejected() {
|
|
let mut bytes = played_world().to_snapshot();
|
|
bytes[4..8].copy_from_slice(&(SNAPSHOT_VERSION + 1).to_le_bytes());
|
|
assert_eq!(
|
|
World::from_snapshot(&bytes),
|
|
Err(SnapshotError::UnsupportedVersion(SNAPSHOT_VERSION + 1))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cut_off_and_padded_snapshots_are_rejected() {
|
|
let bytes = played_world().to_snapshot();
|
|
assert_eq!(
|
|
World::from_snapshot(&bytes[..bytes.len() - 1]),
|
|
Err(SnapshotError::Corrupt(DecodeError::UnexpectedEnd))
|
|
);
|
|
|
|
let mut padded = bytes.clone();
|
|
padded.push(0);
|
|
assert_eq!(
|
|
World::from_snapshot(&padded),
|
|
Err(SnapshotError::Corrupt(DecodeError::TrailingBytes))
|
|
);
|
|
}
|