Welt und ihre Regeln
World::step wendet die Befehle eines Takts der Reihe nach an: Spieler treten bei und verlassen die Welt, drei Schläge brechen weichen Fels und bringen einen Stein. Abgelehnte Befehle ändern nichts. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
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_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 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);
|
||||
}
|
||||
Reference in New Issue
Block a user