Snapshot und Zustandsprüfsumme
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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user