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); }