Files
Grovewright/core/tests/world.rs
T
Steven Wroblewski 3857fe99a6 Festgeschriebene Werte und Test für überlange Namen
Die neuen Tests in golden.rs vergleichen gegen einmal aufgezeichnete
Werte statt gegen eine zweite Berechnung im selben Build. So fällt
über Builds hinweg auf, wenn sich Regeln oder Formate unbeabsichtigt
ändern: der Startzustand der Welt, das Ende einer festen Befehlsfolge
und die Prüfsumme der Aufzeichnung. Die Fehlermeldung nennt die
Versionskonstante, die dann erhöht werden muss.

Der neue Test in world.rs deckt ab, dass überlange Namen auf beiden
Beitrittswegen gekürzt gespeichert werden: beim ersten Beitritt und
bei der Rückkehr.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 22:18:35 +02:00

317 lines
8.7 KiB
Rust

use grovewright_core::command::Command;
use grovewright_core::inventory::ITEM_STONE;
use grovewright_core::map::{Pos, Terrain};
use grovewright_core::mapgen::{outcrop_origin, spawn_pos};
use grovewright_core::player::{
PlayerId, PlayerKey, MAX_KNOWN_PLAYERS, MAX_NAME_BYTES, MAX_ONLINE_PLAYERS,
};
use grovewright_core::world::{Event, World, DIG_COOLDOWN_TICKS, TICKS_PER_SECOND};
fn key(n: u8) -> PlayerKey {
[n; 16]
}
fn join(n: u8) -> Command {
Command::Join {
key: key(n),
name: format!("Spieler {n}"),
}
}
/// A world with one online player, and a soft rock tile to dig at.
fn world_with_player() -> (World, PlayerId, Pos) {
let mut world = World::new(42);
world.step(&[join(1)]);
let rock = outcrop_origin(world.map());
(world, PlayerId(0), rock)
}
/// Steps without commands until a player who just hit may hit again.
fn wait_for_cooldown(world: &mut World) {
for _ in 0..DIG_COOLDOWN_TICKS {
world.step(&[]);
}
}
#[test]
fn a_new_world_starts_at_tick_zero_without_players() {
let world = World::new(42);
assert_eq!(world.seed(), 42);
assert_eq!(world.tick(), 0);
assert!(world.players().is_empty());
assert_eq!(TICKS_PER_SECOND, 20);
}
#[test]
fn step_advances_the_tick() {
let mut world = World::new(1);
assert!(world.step(&[]).is_empty());
assert_eq!(world.tick(), 1);
}
#[test]
fn joining_adds_an_online_player() {
let mut world = World::new(1);
let events = world.step(&[join(7)]);
assert_eq!(
events,
vec![Event::PlayerJoined {
player: PlayerId(0)
}]
);
let player = world.player(PlayerId(0)).unwrap();
assert!(player.online);
assert_eq!(player.name, "Spieler 7");
assert_eq!(world.find_player(&key(7)), Some(PlayerId(0)));
assert_eq!(world.find_player(&key(8)), None);
assert!(world.player(PlayerId(1)).is_none());
}
#[test]
fn joining_twice_changes_nothing() {
let mut world = World::new(1);
world.step(&[join(7)]);
assert!(world.step(&[join(7)]).is_empty());
assert_eq!(world.players().len(), 1);
}
#[test]
fn leaving_and_returning_keeps_the_inventory() {
let (mut world, id, rock) = world_with_player();
for _ in 0..3 {
world.step(&[Command::Dig {
player: id,
pos: rock,
}]);
wait_for_cooldown(&mut world);
}
assert_eq!(world.player(id).unwrap().inventory.count(ITEM_STONE), 1);
assert_eq!(
world.step(&[Command::Leave { player: id }]),
vec![Event::PlayerLeft { player: id }]
);
assert!(!world.player(id).unwrap().online);
assert!(world.step(&[Command::Leave { player: id }]).is_empty());
assert_eq!(
world.step(&[join(1)]),
vec![Event::PlayerJoined { player: id }]
);
assert_eq!(world.players().len(), 1);
assert!(world.player(id).unwrap().online);
assert_eq!(world.player(id).unwrap().inventory.count(ITEM_STONE), 1);
}
#[test]
fn a_returning_player_can_change_their_name() {
let (mut world, id, _) = world_with_player();
world.step(&[Command::Leave { player: id }]);
world.step(&[Command::Join {
key: key(1),
name: "Neuer Name".to_owned(),
}]);
assert_eq!(world.player(id).unwrap().name, "Neuer Name");
}
#[test]
fn overlong_names_are_stored_shortened_on_both_join_paths() {
let mut world = World::new(1);
world.step(&[Command::Join {
key: key(1),
name: "ä".repeat(20),
}]);
assert_eq!(world.player(PlayerId(0)).unwrap().name, "ä".repeat(16));
world.step(&[Command::Leave {
player: PlayerId(0),
}]);
world.step(&[Command::Join {
key: key(1),
name: "x".repeat(MAX_NAME_BYTES + 10),
}]);
assert_eq!(
world.player(PlayerId(0)).unwrap().name,
"x".repeat(MAX_NAME_BYTES)
);
}
#[test]
fn a_fifth_online_player_is_refused() {
let mut world = World::new(1);
for n in 0..MAX_ONLINE_PLAYERS as u8 {
world.step(&[join(n)]);
}
assert_eq!(
world.step(&[join(99)]),
vec![Event::JoinRefused { key: key(99) }]
);
assert_eq!(world.players().len(), MAX_ONLINE_PLAYERS);
// A known player who is offline is refused as well while the world is full.
world.step(&[Command::Leave {
player: PlayerId(0),
}]);
world.step(&[join(50)]);
assert_eq!(
world.step(&[join(0)]),
vec![Event::JoinRefused { key: key(0) }]
);
}
#[test]
fn the_world_remembers_a_limited_number_of_players() {
let mut world = World::new(1);
for n in 0..MAX_KNOWN_PLAYERS as u8 {
world.step(&[join(n)]);
world.step(&[Command::Leave {
player: PlayerId(n),
}]);
}
assert_eq!(world.players().len(), MAX_KNOWN_PLAYERS);
assert_eq!(
world.step(&[join(200)]),
vec![Event::JoinRefused { key: key(200) }]
);
assert_eq!(world.players().len(), MAX_KNOWN_PLAYERS);
}
#[test]
fn set_all_offline_marks_every_player_offline() {
let mut world = World::new(1);
world.step(&[join(1), join(2)]);
world.set_all_offline();
assert!(world.players().iter().all(|player| !player.online));
}
#[test]
fn three_hits_break_soft_rock_and_give_one_stone() {
let (mut world, id, rock) = world_with_player();
let dig = [Command::Dig {
player: id,
pos: rock,
}];
let hit = vec![Event::TileHit {
player: id,
pos: rock,
}];
assert_eq!(world.step(&dig), hit);
assert_eq!(world.map().get(rock).unwrap().damage, 1);
wait_for_cooldown(&mut world);
assert_eq!(world.step(&dig), hit);
assert_eq!(world.map().get(rock).unwrap().damage, 2);
wait_for_cooldown(&mut world);
assert_eq!(
world.step(&dig),
vec![
Event::TileChanged { pos: rock },
Event::ItemGained {
player: id,
item: ITEM_STONE,
count: 1
}
]
);
let tile = world.map().get(rock).unwrap();
assert_eq!((tile.terrain, tile.damage), (Terrain::Ground, 0));
assert_eq!(world.player(id).unwrap().inventory.count(ITEM_STONE), 1);
}
#[test]
fn hits_during_the_cooldown_are_ignored() {
let (mut world, id, rock) = world_with_player();
let dig = [Command::Dig {
player: id,
pos: rock,
}];
world.step(&dig);
for _ in 1..DIG_COOLDOWN_TICKS {
assert!(world.step(&dig).is_empty());
}
assert_eq!(world.map().get(rock).unwrap().damage, 1);
assert_eq!(world.step(&dig).len(), 1);
assert_eq!(world.map().get(rock).unwrap().damage, 2);
}
#[test]
fn two_hits_in_one_tick_count_once() {
let (mut world, id, rock) = world_with_player();
let dig = Command::Dig {
player: id,
pos: rock,
};
let twice = [dig.clone(), dig];
assert_eq!(world.step(&twice).len(), 1);
assert_eq!(world.map().get(rock).unwrap().damage, 1);
}
#[test]
fn ground_bedrock_and_positions_off_the_map_cannot_be_dug() {
let (mut world, id, _) = world_with_player();
let spawn = spawn_pos(world.map());
for pos in [
spawn,
Pos::new(0, 0),
Pos::new(-5, 3),
Pos::new(10_000, 10_000),
] {
let before = world.map().clone();
let events = world.step(&[Command::Dig { player: id, pos }]);
assert!(events.is_empty(), "{pos:?}");
assert_eq!(world.map(), &before, "{pos:?}");
}
}
#[test]
fn hard_rock_resists_the_stone_pickaxe() {
let (mut world, id, _) = world_with_player();
let width = world.map().width() as usize;
let index = world
.map()
.tiles()
.iter()
.position(|tile| tile.terrain == Terrain::HardRock)
.expect("the test map has hard rock");
let pos = Pos::new((index % width) as i32, (index / width) as i32);
assert!(world.step(&[Command::Dig { player: id, pos }]).is_empty());
assert_eq!(world.map().get(pos).unwrap().damage, 0);
}
#[test]
fn unknown_and_offline_players_cannot_dig() {
let (mut world, id, rock) = world_with_player();
assert!(world
.step(&[Command::Dig {
player: PlayerId(9),
pos: rock
}])
.is_empty());
world.step(&[Command::Leave { player: id }]);
assert!(world
.step(&[Command::Dig {
player: id,
pos: rock
}])
.is_empty());
assert_eq!(world.map().get(rock).unwrap().damage, 0);
}
#[test]
fn commands_of_one_tick_are_applied_in_order() {
let (mut world, id, rock) = world_with_player();
// Leaving first makes the hit in the same tick invalid.
let events = world.step(&[
Command::Leave { player: id },
Command::Dig {
player: id,
pos: rock,
},
]);
assert_eq!(events, vec![Event::PlayerLeft { player: id }]);
assert_eq!(world.map().get(rock).unwrap().damage, 0);
}