//! The `map` module contains structures of the dungeon game map //! including the current `Floor`, and map `Tile`. use rand::Rng; use strum::IntoEnumIterator; use strum_macros::EnumIter; use std::{ cell::RefCell, fmt::{Display, Write}, hash::{DefaultHasher, Hash, Hasher}, }; use crate::{pos::Pos, rng::DungeonRng}; /// `MAP_SIZE` is the size of the size of the dungeon grid. pub const MAP_SIZE: u16 = 48; /// `MAP_SIZE` as a usize pub const MAP_SIZE_USIZE: usize = MAP_SIZE as usize; /// The number of tiles in the dungeon grid pub const TILE_COUNT: usize = MAP_SIZE_USIZE * MAP_SIZE_USIZE; /// The `Tile` enum represents what is (or is not) at /// any given spot in the dungeon grid. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, EnumIter)] pub enum Tile { /// `Wall` represents an impassible wall Wall, /// `Room` represents empty walkable space for a rectangular room Room, /// `Hallway` represents empty walkable space for a hallway Hallway, /// `Stairs` represents stairs to another floor Stairs, } impl Tile { /// Returns a list of all possible tiles pub fn values() -> impl Iterator { Self::iter() } /// Returns if the tile is a wall #[must_use] pub const fn is_wall(self) -> bool { matches!(self, Self::Wall) } /// Returns if the tile is walkable #[must_use] pub const fn is_walkable(self) -> bool { matches!(self, Self::Room | Self::Hallway | Self::Stairs) } /// Returns if the tile is blast resistant #[must_use] pub const fn blast_resistant(self) -> bool { matches!(self, Self::Stairs) } } impl Display for Tile { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let char = match self { Self::Wall => '#', Self::Room => '.', Self::Hallway => ',', Self::Stairs => '>', }; f.write_char(char) } } /// The `Floor` type represents the current playing /// grid of the dungeon. It contains the tiles of the /// grid, and the starting position of the player. #[derive(Debug, Clone)] pub struct Floor { /// The dungeon grid tiles: Box<[Tile; TILE_COUNT]>, /// The position the player starts at player_start: Pos, /// The computed hash of the tile map hash: RefCell, /// If the tiles are dirty (hash needs to be recomputed) dirty: RefCell, } impl Floor { /// Construct a floor from its components pub const fn new(tiles: Box<[Tile; TILE_COUNT]>, player_start: Pos) -> Self { Self { tiles, player_start, hash: RefCell::new(0), dirty: RefCell::new(true), } } /// Returns the start position of the player #[must_use] pub const fn player_start(&self) -> Pos { self.player_start } /// Returns a `Tile` on the dungeon grid at `Pos`. #[must_use] pub const fn get(&self, pos: Pos) -> Tile { let idx = pos.idx(); self.tiles[idx] } /// Returns a multable reference to a `Tile` on the dungeon grid at `Pos`. #[must_use] pub fn get_mut(&mut self, pos: Pos) -> &mut Tile { *self.dirty.get_mut() = true; let idx = pos.idx(); &mut self.tiles[idx] } /// Returns a reference to all tiles in the `Floor`. /// The size of this lise will always be `TILE_COUNT` long. #[must_use] pub const fn tiles(&self) -> &[Tile] { &*self.tiles } /// Returns a mutable reference to all tiles in the `Floor`. /// The size of this lise will always be `TILE_COUNT` long. #[must_use] pub fn tiles_mut(&mut self) -> &mut [Tile] { *self.dirty.get_mut() = true; &mut *self.tiles } /// Returns the neighbors of a tile inside the floor, checking /// that the neighbor positions are the same tile type as in `pos`. pub fn neighbors(&self, pos: &Pos) -> impl Iterator { pos.neighbors().filter(|p| self.get(*p).is_walkable()) } /// Computes the hash of the tile map #[must_use] pub fn hash(&self) -> u64 { // initial (immutable) dirty check if !*self.dirty.borrow() { return *self.hash.borrow(); } // recompute hash let mut dirty = self.dirty.borrow_mut(); let mut hash = self.hash.borrow_mut(); let mut s = DefaultHasher::new(); self.tiles.hash(&mut s); *hash = s.finish(); *dirty = false; *hash } /// Returns a random open (no wall) position #[must_use] pub fn random_walkable_pos(&self, rng: &mut DungeonRng) -> Pos { loop { let pos = rng.random(); if !self.get(pos).is_walkable() { continue; } break pos; } } /// Blows up a set number of tiles with a given radius pub fn explode(&mut self, center_pos: Pos, radius: i16) { let tiles_mut = self.tiles_mut(); for x_off in -radius..=radius { for y_off in -radius..=radius { let Some(x) = center_pos.x().checked_add_signed(x_off) else { continue; }; let Some(y) = center_pos.y().checked_add_signed(y_off) else { continue; }; let Some(pos) = Pos::new(x, y) else { continue }; if pos.is_border() || tiles_mut[pos.idx()].blast_resistant() { continue; } tiles_mut[pos.idx()] = Tile::Room; } } } } impl Display for Floor { /// Display the floor as a string for debugging /// /// # Examples /// ```no_run /// use dungeon::Dungeon; /// let dungeon = Dungeon::random(); /// let floor = &dungeon.floor; /// println!("{floor}"); /// ``` fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for pos in Pos::values() { // If it's the player start, show 'P' if self.player_start == pos { f.write_char('P')?; continue; } // Otherwise, show the tile character let tile = self.get(pos); tile.fmt(f)?; // Newline at the end of each row if pos.xy().0 == MAP_SIZE - 1 { f.write_char('\n')?; } } Ok(()) } } /// Tests #[cfg(test)] mod tests { use crate::Dungeon; // Test floor printing #[test] fn test_floor_display() { let dungeon = Dungeon::random(); let floor = &dungeon.floor; // Print the display for visual inspection println!("{floor}"); } }