ef728cd100
Snapshots tragen jetzt eine Prüfsumme (Format 2). Beim Wiederherstellen werden Zustände abgelehnt, die die Regeln nie erzeugen: zu große Takte, mehr als vier Spieler online, doppelte Schlüssel, unmögliche Wartezeiten. World::load ersetzt den öffentlichen Aufruf von set_all_offline. Die festgeschriebenen Werte sind bewusst angepasst und um einen Offline-Spieler und das Kartenformat ergänzt. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
239 lines
7.2 KiB
Rust
239 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.push_tick(&commands);
|
|
world.step(&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"
|
|
);
|
|
}
|