5dacbf2222
Recording::step führt einen Takt aus und zeichnet ihn auf. Andere Befehle als die ausgeführten können so nicht in eine Aufzeichnung geraten. Aufzeichnung und Welt prüfen gegenseitig ihren Takt, und ein beschädigter Startzustand fällt schon beim Einlesen auf. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
238 lines
7.2 KiB
Rust
238 lines
7.2 KiB
Rust
//! Pinned values for everything that must not change unnoticed.
|
|
//!
|
|
//! Most other tests compare two computations inside the same build, so a
|
|
//! change that affects both sides alike stays green. This file compares
|
|
//! against values recorded once. If one of them fails, rules or encodings
|
|
//! changed, on purpose or by accident. If saves or recordings are affected,
|
|
//! raise the version constant named in the failure message, then update the
|
|
//! value here deliberately.
|
|
|
|
use grovewright_core::codec::Writer;
|
|
use grovewright_core::command::Command;
|
|
use grovewright_core::hash::fnv1a64;
|
|
use grovewright_core::map::{Map, Pos, Terrain, Tile};
|
|
use grovewright_core::mapgen::outcrop_origin;
|
|
use grovewright_core::player::PlayerId;
|
|
use grovewright_core::replay::Recording;
|
|
use grovewright_core::world::World;
|
|
|
|
const SEED: u64 = 2026;
|
|
|
|
/// The commands of a session of 60 ticks. It is built from literal values
|
|
/// and the outcrop origin of the map, not from constants such as
|
|
/// `DIG_COOLDOWN_TICKS`, so that changing a rule changes the result and not
|
|
/// the script.
|
|
///
|
|
/// Never change or extend this script. To pin more, add a new script and a
|
|
/// new pinned value instead.
|
|
fn frozen_script(world: &World) -> Vec<Vec<Command>> {
|
|
let origin = outcrop_origin(world.map());
|
|
let right_of_origin = Pos::new(origin.x + 1, origin.y);
|
|
// Pairs of (tick, command). Commands of the same tick are applied in the
|
|
// order listed here.
|
|
let scripted = [
|
|
// Two players join in the same tick.
|
|
(
|
|
0,
|
|
Command::Join {
|
|
key: [1; 16],
|
|
name: "Ada".to_owned(),
|
|
},
|
|
),
|
|
(
|
|
0,
|
|
Command::Join {
|
|
key: [2; 16],
|
|
name: "Grace".to_owned(),
|
|
},
|
|
),
|
|
// Player 0 hits the outcrop origin three times, five ticks apart.
|
|
// The third hit breaks the rock and gives a stone.
|
|
(
|
|
2,
|
|
Command::Dig {
|
|
player: PlayerId(0),
|
|
pos: origin,
|
|
},
|
|
),
|
|
(
|
|
7,
|
|
Command::Dig {
|
|
player: PlayerId(0),
|
|
pos: origin,
|
|
},
|
|
),
|
|
(
|
|
12,
|
|
Command::Dig {
|
|
player: PlayerId(0),
|
|
pos: origin,
|
|
},
|
|
),
|
|
// Player 1 hits the tile to the right of the origin once.
|
|
(
|
|
20,
|
|
Command::Dig {
|
|
player: PlayerId(1),
|
|
pos: right_of_origin,
|
|
},
|
|
),
|
|
// Player 1 leaves and returns with a name of 40 bytes, which the
|
|
// world shortens to 32.
|
|
(
|
|
30,
|
|
Command::Leave {
|
|
player: PlayerId(1),
|
|
},
|
|
),
|
|
(
|
|
40,
|
|
Command::Join {
|
|
key: [2; 16],
|
|
name: "ä".repeat(20),
|
|
},
|
|
),
|
|
];
|
|
let mut ticks = vec![Vec::new(); 60];
|
|
for (tick, command) in scripted {
|
|
ticks[tick].push(command);
|
|
}
|
|
ticks
|
|
}
|
|
|
|
/// The commands of a second session of 30 ticks that ends with one player
|
|
/// offline, so that the encoding of offline players is pinned as well.
|
|
///
|
|
/// Never change or extend this script either.
|
|
fn frozen_offline_script(world: &World) -> Vec<Vec<Command>> {
|
|
let origin = outcrop_origin(world.map());
|
|
let scripted = [
|
|
(
|
|
0,
|
|
Command::Join {
|
|
key: [7; 16],
|
|
name: "Linus".to_owned(),
|
|
},
|
|
),
|
|
(
|
|
1,
|
|
Command::Join {
|
|
key: [8; 16],
|
|
name: "Margaret".to_owned(),
|
|
},
|
|
),
|
|
(
|
|
3,
|
|
Command::Dig {
|
|
player: PlayerId(1),
|
|
pos: origin,
|
|
},
|
|
),
|
|
(
|
|
10,
|
|
Command::Leave {
|
|
player: PlayerId(0),
|
|
},
|
|
),
|
|
];
|
|
let mut ticks = vec![Vec::new(); 30];
|
|
for (tick, command) in scripted {
|
|
ticks[tick].push(command);
|
|
}
|
|
ticks
|
|
}
|
|
|
|
/// Covers the map generator, the map encoding and the snapshot header and
|
|
/// checksum.
|
|
#[test]
|
|
fn the_starting_snapshot_is_unchanged() {
|
|
let actual = fnv1a64(&World::new(SEED).to_snapshot());
|
|
assert_eq!(
|
|
actual, 0x6812_937b_f2b2_19ae,
|
|
"the starting world or the snapshot format changed; if saves are \
|
|
affected, raise SNAPSHOT_VERSION in core/src/snapshot.rs; then \
|
|
update this value"
|
|
);
|
|
}
|
|
|
|
/// Covers the rules of joining, leaving and digging, the shortening of names
|
|
/// and the encoding of players.
|
|
#[test]
|
|
fn the_frozen_session_ends_in_the_same_state() {
|
|
let mut world = World::new(SEED);
|
|
for commands in frozen_script(&world) {
|
|
world.step(&commands);
|
|
}
|
|
assert_eq!(
|
|
world.state_hash(),
|
|
0xc304_43df_fc74_dcfc,
|
|
"the rules or the snapshot format changed; old recordings no longer \
|
|
verify; if saves are affected, raise SNAPSHOT_VERSION in \
|
|
core/src/snapshot.rs; then update this value"
|
|
);
|
|
}
|
|
|
|
/// Covers the encoding of a player who is offline.
|
|
#[test]
|
|
fn the_frozen_offline_session_ends_in_the_same_state() {
|
|
let mut world = World::new(SEED);
|
|
for commands in frozen_offline_script(&world) {
|
|
world.step(&commands);
|
|
}
|
|
assert_eq!(
|
|
world.state_hash(),
|
|
0x904f_10e1_7c52_8be4,
|
|
"the rules or the snapshot format changed; old recordings no longer \
|
|
verify; if saves are affected, raise SNAPSHOT_VERSION in \
|
|
core/src/snapshot.rs; then update this value"
|
|
);
|
|
}
|
|
|
|
/// Covers the recording header and layout and the wire format of commands.
|
|
#[test]
|
|
fn the_recording_format_is_unchanged() {
|
|
let mut world = World::new(SEED);
|
|
let mut recording = Recording::start(&world);
|
|
for commands in frozen_script(&world) {
|
|
recording.step(&mut world, &commands);
|
|
}
|
|
assert_eq!(
|
|
fnv1a64(&recording.to_bytes(&world)),
|
|
0x1c24_8cc7_6bc8_846a,
|
|
"the recording or command format changed; if other tests in this \
|
|
file fail as well, deal with those first, because a recording \
|
|
contains a snapshot and an end state; otherwise raise \
|
|
RECORDING_VERSION in core/src/replay.rs; then update this value"
|
|
);
|
|
}
|
|
|
|
/// Covers the byte layout of a map: width before height, little endian,
|
|
/// terrain numbers and damage. The map is not square, so a swap of width
|
|
/// and height shows.
|
|
#[test]
|
|
fn the_map_encoding_is_unchanged() {
|
|
let mut map = Map::filled(3, 2, Terrain::Ground);
|
|
map.set(Pos::new(1, 0), Tile::new(Terrain::Bedrock));
|
|
map.set(Pos::new(0, 1), Tile::new(Terrain::HardRock));
|
|
map.set(
|
|
Pos::new(2, 1),
|
|
Tile {
|
|
terrain: Terrain::SoftRock,
|
|
damage: 1,
|
|
},
|
|
);
|
|
let mut w = Writer::new();
|
|
map.encode(&mut w);
|
|
assert_eq!(
|
|
w.into_bytes(),
|
|
vec![
|
|
3, 0, 0, 0, // width
|
|
2, 0, 0, 0, // height
|
|
0, 0, 3, 0, 0, 0, // row 0: ground, bedrock, ground
|
|
2, 0, 0, 0, 1, 1, // row 1: hard rock, ground, soft rock hit once
|
|
],
|
|
"the map encoding changed; raise SNAPSHOT_VERSION in \
|
|
core/src/snapshot.rs; then update these bytes"
|
|
);
|
|
}
|