//! The `pos` module contains structures for representation an //! entity or objects position and facing direction inside the //! dungeon grid. use crate::{MAP_SIZE_USIZE, map::MAP_SIZE}; macro_rules! downcast { ($usize:expr, $type:ty) => { if $usize > <$type>::MAX as usize { None } else { #[expect(clippy::cast_possible_truncation)] Some($usize as $type) } }; } #[macro_export] macro_rules! const_pos { ($name:ident, $x:expr, $y:expr) => { const $name: Pos = Pos::new_unchecked($x, $y); }; } /// The `Direction` type represents a direction an entity /// or any position object is facing inside the dungeon map. /// Since the dungeon lives on a grid, there are only four /// possible directions. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Direction { North, South, East, West, } /// The `Pos` type represents a 2D position inside the dungeon grid. /// /// The max size for the dungeon map is set by the `MAP_SIZE` constant /// and therefore the x and y positions can be between 0 and `MAP_SIZE - 1`. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Pos(u16, u16); impl Pos { /// Creates a new position from a given x and y position. /// /// Returns `None` if the position goes out of the map. /// /// # Examples /// /// ``` /// use dungeon::Pos; /// /// let pos = Pos::new(0,0); /// assert!(pos.is_some()); /// ``` /// /// ``` /// use dungeon::{Pos, MAP_SIZE}; /// /// let pos = Pos::new(MAP_SIZE, MAP_SIZE); /// assert!(pos.is_none()) /// ``` #[must_use] pub const fn new(x: u16, y: u16) -> Option { if x >= MAP_SIZE || y >= MAP_SIZE { None } else { Some(Self(x, y)) } } /// Creates a new position from a given x and y position. /// /// Bounds checks are asserted at runtime and will panic if out of bounds. /// /// # Examples /// /// ``` /// use dungeon::Pos; /// /// let pos = Pos::new_unchecked(1, 1); /// assert_eq!(pos.xy(), (1,1)); /// ``` #[must_use] pub const fn new_unchecked(x: u16, y: u16) -> Self { assert!(x < MAP_SIZE, "Positions must be smaller then MAP_SIZE"); assert!(y < MAP_SIZE, "Positions must be smaller then MAP_SIZE"); Self(x, y) } /// Returns the x and y positions of `Pos`. /// /// # Examples /// /// ``` /// use dungeon::Pos; /// /// let pos = Pos::new(5,7).unwrap(); /// let (x,y) = pos.xy(); /// assert_eq!(x, 5); /// assert_eq!(y, 7); /// ``` #[must_use] pub const fn xy(self) -> (u16, u16) { (self.0, self.1) } /// Converts the x and y positions into an index of a continous list. /// /// # Examples /// /// ``` /// use dungeon::{Pos, MAP_SIZE_USIZE}; /// /// let pos = Pos::new(1,2).unwrap(); /// let idx = pos.idx(); /// assert_eq!(idx, 1 + 2 * MAP_SIZE_USIZE); /// ``` #[must_use] pub const fn idx(self) -> usize { let (x, y) = self.xy(); let idx = x + y * MAP_SIZE; idx as usize } /// Converse an index into a possible x and y position /// /// # Examples /// /// ``` /// use dungeon::{Pos}; /// /// let idx_pos = Pos::from_idx(17); /// let pos = Pos::new(17, 0); /// /// assert_eq!(idx_pos, pos); /// ``` /// /// ``` /// use dungeon::{Pos}; /// /// let idx_pos = Pos::from_idx(170); /// let pos = Pos::new(70, 1); /// /// assert_eq!(idx_pos, pos); /// ``` #[must_use] pub const fn from_idx(idx: usize) -> Option { let x = downcast!(idx % MAP_SIZE_USIZE, u16); let y = downcast!(idx / MAP_SIZE_USIZE, u16); match (x, y) { (Some(a), Some(b)) => Self::new(a, b), _ => None, } } /// Steps `Pos` one space in the `Direction` `dir`. /// /// Returns `None` if the position goes out of the map. /// /// # Examples /// /// ``` /// use dungeon::{Direction, Pos}; /// /// let pos = Pos::new(0, 1).unwrap(); /// let new_pos = pos.step(Direction::North); /// assert_eq!(new_pos, Pos::new(0, 0)); /// ``` /// /// ``` /// use dungeon::{Direction, Pos}; /// /// let pos = Pos::new(0, 1).unwrap(); /// let new_pos = pos.step(Direction::West); /// assert!(new_pos.is_none()); /// ``` #[must_use] pub const fn step(self, dir: Direction) -> Option { use Direction as D; let (x, y) = self.xy(); match dir { D::North if y > 0 => Self::new(x, y - 1), D::South => Self::new(x, y + 1), D::East => Self::new(x + 1, y), D::West if x > 0 => Self::new(x - 1, y), _ => None, } } /// Computes the absolute difference between to positions /// /// Both values are gurenteed to be less than MAP_SIZE /// /// # Examples /// /// ``` /// use dungeon::Pos; /// /// let pos1 = Pos::new(2,7).unwrap(); /// let pos2 = Pos::new(1,17).unwrap(); /// let diff = pos1.abs_diff(pos2); /// assert_eq!(diff.xy(), (1, 10)); /// ``` /// #[must_use] pub const fn abs_diff(self, other: Self) -> Self { let x = self.0.abs_diff(other.0); let y = self.1.abs_diff(other.1); Self(x, y) } /// Returns of the given position is on the border of the map /// /// ``` /// use dungeon::{Pos, MAP_SIZE}; /// /// let pos1 = Pos::new(0, 17).unwrap(); /// let pos2 = Pos::new(1, 17).unwrap(); /// let pos3 = Pos::new(MAP_SIZE - 1, 17).unwrap(); /// let pos4 = Pos::new(55, MAP_SIZE - 1).unwrap(); /// let pos5 = Pos::new(55, 0).unwrap(); /// /// assert!(pos1.is_border()); /// assert!(!pos2.is_border()); /// assert!(pos3.is_border()); /// assert!(pos4.is_border()); /// assert!(pos5.is_border()); /// ``` #[must_use] pub const fn is_border(&self) -> bool { self.0 == 0 || self.0 == MAP_SIZE - 1 || self.1 == 0 || self.1 == MAP_SIZE - 1 } /// Returns an iterator over all possible `Pos` pub fn values() -> impl Iterator { (0..MAP_SIZE).flat_map(|y| (0..MAP_SIZE).filter_map(move |x| Self::new(x, y))) } } impl Default for Pos { /// Returns a default postion at the origin (0,0) /// /// ``` /// use dungeon::Pos; /// /// let pos = Pos::default(); /// /// assert_eq!(pos.xy(), (0, 0)); /// ``` /// fn default() -> Self { const_pos!(DEFAULT, 0, 0); DEFAULT } }