//! 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> { 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> { 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" ); }