M1a: deterministischer Rust-Kern #3
@@ -14,3 +14,4 @@ pub mod map;
|
|||||||
pub mod mapgen;
|
pub mod mapgen;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod rng;
|
pub mod rng;
|
||||||
|
pub mod world;
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
//! The shared world and the rules that change it.
|
||||||
|
|
||||||
|
use crate::command::Command;
|
||||||
|
use crate::inventory::{ItemId, ITEM_STONE};
|
||||||
|
use crate::map::{Map, Pos, Terrain, Tile};
|
||||||
|
use crate::mapgen::generate_test_map;
|
||||||
|
use crate::player::{
|
||||||
|
clamp_name, Player, PlayerId, PlayerKey, MAX_KNOWN_PLAYERS, MAX_ONLINE_PLAYERS,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Simulation steps per second of play.
|
||||||
|
pub const TICKS_PER_SECOND: u32 = 20;
|
||||||
|
/// Ticks a player has to wait between two pickaxe hits.
|
||||||
|
pub const DIG_COOLDOWN_TICKS: u64 = 5;
|
||||||
|
|
||||||
|
/// Something that happened during a tick. Events tell the presentation what
|
||||||
|
/// to show. They are not part of the world state.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Event {
|
||||||
|
PlayerJoined {
|
||||||
|
player: PlayerId,
|
||||||
|
},
|
||||||
|
PlayerLeft {
|
||||||
|
player: PlayerId,
|
||||||
|
},
|
||||||
|
/// A player could not join because the world is full.
|
||||||
|
JoinRefused {
|
||||||
|
key: PlayerKey,
|
||||||
|
},
|
||||||
|
/// A pickaxe hit landed without breaking the tile.
|
||||||
|
TileHit {
|
||||||
|
player: PlayerId,
|
||||||
|
pos: Pos,
|
||||||
|
},
|
||||||
|
/// A tile changed its terrain.
|
||||||
|
TileChanged {
|
||||||
|
pos: Pos,
|
||||||
|
},
|
||||||
|
ItemGained {
|
||||||
|
player: PlayerId,
|
||||||
|
item: ItemId,
|
||||||
|
count: u32,
|
||||||
|
},
|
||||||
|
/// A hit was refused because the inventory has no room for the result.
|
||||||
|
InventoryFull {
|
||||||
|
player: PlayerId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete state of the shared world.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct World {
|
||||||
|
pub(crate) seed: u64,
|
||||||
|
pub(crate) tick: u64,
|
||||||
|
pub(crate) map: Map,
|
||||||
|
pub(crate) players: Vec<Player>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World {
|
||||||
|
/// Creates a fresh world with the test map for `seed` and no players.
|
||||||
|
pub fn new(seed: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
seed,
|
||||||
|
tick: 0,
|
||||||
|
map: generate_test_map(seed),
|
||||||
|
players: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seed(&self) -> u64 {
|
||||||
|
self.seed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of ticks simulated so far.
|
||||||
|
pub fn tick(&self) -> u64 {
|
||||||
|
self.tick
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map(&self) -> &Map {
|
||||||
|
&self.map
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn players(&self) -> &[Player] {
|
||||||
|
&self.players
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn player(&self, id: PlayerId) -> Option<&Player> {
|
||||||
|
self.players.get(id.0 as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_player(&self, key: &PlayerKey) -> Option<PlayerId> {
|
||||||
|
self.players
|
||||||
|
.iter()
|
||||||
|
.position(|player| &player.key == key)
|
||||||
|
.map(|index| PlayerId(index as u8))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks every player as offline. Used after loading a save from disk,
|
||||||
|
/// when nobody has joined yet.
|
||||||
|
pub fn set_all_offline(&mut self) {
|
||||||
|
for player in &mut self.players {
|
||||||
|
player.online = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies `commands` in order and advances the world by one tick.
|
||||||
|
pub fn step(&mut self, commands: &[Command]) -> Vec<Event> {
|
||||||
|
let mut events = Vec::new();
|
||||||
|
for command in commands {
|
||||||
|
match command {
|
||||||
|
Command::Join { key, name } => self.join(key, name, &mut events),
|
||||||
|
Command::Leave { player } => self.leave(*player, &mut events),
|
||||||
|
Command::Dig { player, pos } => self.dig(*player, *pos, &mut events),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tick += 1;
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join(&mut self, key: &PlayerKey, name: &str, events: &mut Vec<Event>) {
|
||||||
|
let online = self.players.iter().filter(|player| player.online).count();
|
||||||
|
match self.find_player(key) {
|
||||||
|
Some(id) => {
|
||||||
|
let player = &mut self.players[id.0 as usize];
|
||||||
|
if player.online {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if online >= MAX_ONLINE_PLAYERS {
|
||||||
|
events.push(Event::JoinRefused { key: *key });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.online = true;
|
||||||
|
player.name = clamp_name(name);
|
||||||
|
events.push(Event::PlayerJoined { player: id });
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if online >= MAX_ONLINE_PLAYERS || self.players.len() >= MAX_KNOWN_PLAYERS {
|
||||||
|
events.push(Event::JoinRefused { key: *key });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.players.push(Player::new(*key, name));
|
||||||
|
let id = PlayerId((self.players.len() - 1) as u8);
|
||||||
|
events.push(Event::PlayerJoined { player: id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leave(&mut self, id: PlayerId, events: &mut Vec<Event>) {
|
||||||
|
let Some(player) = self.players.get_mut(id.0 as usize) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !player.online {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.online = false;
|
||||||
|
events.push(Event::PlayerLeft { player: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dig(&mut self, id: PlayerId, pos: Pos, events: &mut Vec<Event>) {
|
||||||
|
let tick = self.tick;
|
||||||
|
let Some(player) = self.players.get_mut(id.0 as usize) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !player.online || tick < player.next_action_tick {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(tile) = self.map.get(pos) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (Some(hits), Some(tier)) = (
|
||||||
|
tile.terrain.hits_to_break(),
|
||||||
|
tile.terrain.required_tool_tier(),
|
||||||
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if player.tool_tier < tier {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let damage = tile.damage.saturating_add(1);
|
||||||
|
let breaks = damage >= hits;
|
||||||
|
if breaks && player.inventory.free_space(ITEM_STONE) == 0 {
|
||||||
|
events.push(Event::InventoryFull { player: id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.next_action_tick = tick + DIG_COOLDOWN_TICKS;
|
||||||
|
if breaks {
|
||||||
|
self.map.set(pos, Tile::new(Terrain::Ground));
|
||||||
|
player.inventory.add(ITEM_STONE, 1);
|
||||||
|
events.push(Event::TileChanged { pos });
|
||||||
|
events.push(Event::ItemGained {
|
||||||
|
player: id,
|
||||||
|
item: ITEM_STONE,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.map.set(
|
||||||
|
pos,
|
||||||
|
Tile {
|
||||||
|
terrain: tile.terrain,
|
||||||
|
damage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
events.push(Event::TileHit { player: id, pos });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::inventory::{INVENTORY_SLOTS, STACK_LIMIT};
|
||||||
|
use crate::mapgen::outcrop_origin;
|
||||||
|
|
||||||
|
// Lives here instead of in `tests/world.rs` because filling an inventory
|
||||||
|
// needs direct access to the player, which the public API does not give.
|
||||||
|
#[test]
|
||||||
|
fn a_full_inventory_refuses_the_breaking_hit() {
|
||||||
|
let mut world = World::new(42);
|
||||||
|
world.step(&[Command::Join {
|
||||||
|
key: [1; 16],
|
||||||
|
name: "Ada".to_owned(),
|
||||||
|
}]);
|
||||||
|
let id = PlayerId(0);
|
||||||
|
let rock = outcrop_origin(world.map());
|
||||||
|
let dig = [Command::Dig {
|
||||||
|
player: id,
|
||||||
|
pos: rock,
|
||||||
|
}];
|
||||||
|
let capacity = STACK_LIMIT * INVENTORY_SLOTS as u32;
|
||||||
|
assert_eq!(world.players[0].inventory.add(ITEM_STONE, capacity), 0);
|
||||||
|
|
||||||
|
for _ in 0..2 {
|
||||||
|
assert_eq!(
|
||||||
|
world.step(&dig),
|
||||||
|
vec![Event::TileHit {
|
||||||
|
player: id,
|
||||||
|
pos: rock
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
for _ in 0..DIG_COOLDOWN_TICKS {
|
||||||
|
world.step(&[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(world.step(&dig), vec![Event::InventoryFull { player: id }]);
|
||||||
|
let tile = world.map().get(rock).unwrap();
|
||||||
|
assert_eq!((tile.terrain, tile.damage), (Terrain::SoftRock, 2));
|
||||||
|
|
||||||
|
// The refused hit starts no cooldown: once there is room, the very
|
||||||
|
// next tick can break the tile.
|
||||||
|
assert!(world.players[0].inventory.remove(ITEM_STONE, 1));
|
||||||
|
assert_eq!(
|
||||||
|
world.step(&dig),
|
||||||
|
vec![
|
||||||
|
Event::TileChanged { pos: rock },
|
||||||
|
Event::ItemGained {
|
||||||
|
player: id,
|
||||||
|
item: ITEM_STONE,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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