//! The `pos` module contains structures for representation an //! entity or objects position and facing direction inside the //! dungeon grid. use strum::IntoEnumIterator; use strum_macros::EnumIter; use rand::{ Rng, distr::{Distribution, StandardUniform}, }; use std::{ fmt::Display, ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}, }; 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 { ($x:expr, $y:expr) => {{ assert!( $x < $crate::map::MAP_SIZE, "Positions must be smaller then MAP_SIZE" ); assert!( $y < $crate::map::MAP_SIZE, "Positions must be smaller then MAP_SIZE" ); const CONST_POS: Pos = unsafe { Pos::new_unchecked($x, $y) }; CONST_POS }}; } /// 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, EnumIter)] pub enum Direction { North, South, East, West, } impl Direction { /// Returns an iterator over all possible directions pub fn values() -> impl Iterator { Self::iter() } /// Returns a random direction provided with a rng #[must_use] pub fn random(rng: &mut R) -> Self { match rng.random_range(0..4) { 0 => Self::North, 1 => Self::South, 2 => Self::East, _ => Self::West, } } } impl Display for Direction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::North => f.write_str("NORTH"), Self::South => f.write_str("SOUTH"), Self::East => f.write_str("EAST"), Self::West => f.write_str("WEST"), } } } impl Distribution for StandardUniform { fn sample(&self, rng: &mut R) -> Direction { Direction::random(rng) } } /// 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 = unsafe { Pos::new_unchecked(1, 1) }; /// assert_eq!(pos.xy(), (1,1)); /// ``` /// /// # Safety /// /// Library code and crates that use it expect the `Pos` x and y positions /// to be within a gurenteed bound. When they are not this can cause /// undefined behaviour, or crashes. #[must_use] pub const unsafe fn new_unchecked(x: u16, y: u16) -> Self { 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) } /// Returns the x component of `Pos` /// /// # Examples /// /// ``` /// use dungeon::Pos; /// /// let pos = Pos::new(5,7).unwrap(); /// assert_eq!(pos.x(), 5); /// ``` #[must_use] pub const fn x(&self) -> u16 { self.0 } /// Returns the y component of `Pos` /// /// # Examples /// /// ``` /// use dungeon::Pos; /// /// let pos = Pos::new(5,7).unwrap(); /// assert_eq!(pos.y(), 7); /// ``` #[must_use] pub const fn y(&self) -> u16 { 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.x() as usize, self.y() as usize); x + y * MAP_SIZE_USIZE } /// Converse an index into a possible x and y position /// /// # Examples /// /// ``` /// use dungeon::{Pos, MAP_SIZE_USIZE}; /// /// let idx_pos = Pos::from_idx(MAP_SIZE_USIZE); /// let pos = Pos::new(0, 1); /// /// assert_eq!(idx_pos, pos); /// ``` /// /// ``` /// use dungeon::{Pos, MAP_SIZE_USIZE}; /// /// let idx_pos = Pos::from_idx(MAP_SIZE_USIZE * 70 + 1); /// 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 { self.step_by(dir, 1) } /// Steps `Pos` a given ammount 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_by(Direction::South, 3); /// assert_eq!(new_pos, Pos::new(0, 4)); /// ``` /// /// ``` /// use dungeon::{Direction, Pos}; /// /// let pos = Pos::new(1, 1).unwrap(); /// let new_pos = pos.step_by(Direction::West, 2); /// assert!(new_pos.is_none()); /// ``` #[must_use] pub const fn step_by(self, dir: Direction, amt: u16) -> Option { let (x, y) = self.xy(); if amt > MAP_SIZE { return None; } match dir { Direction::North if y >= amt => Self::new(x, y - amt), Direction::South => Self::new(x, y + amt), Direction::East => Self::new(x + amt, y), Direction::West if x >= amt => Self::new(x - amt, 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 the manhattan distance between `self` and `other` /// /// # Examples /// /// ``` /// use dungeon::Pos; /// /// let pos1 = Pos::new(3, 6).unwrap(); /// let pos2 = Pos::new(5, 1).unwrap(); /// assert_eq!(pos1.manhattan(pos2), 7); /// ``` #[must_use] pub const fn manhattan(self, other: Self) -> u16 { let abs_diff = Self::abs_diff(self, other); abs_diff.0 + abs_diff.1 } /// Returns of the given position is on the border of the map /// /// ``` /// use dungeon::{Pos, MAP_SIZE}; /// /// // Assuming MAP_SIZE is at least 2 /// /// let pos1 = Pos::new(0, MAP_SIZE - 1).unwrap(); /// let pos2 = Pos::new(1, MAP_SIZE - 2).unwrap(); /// let pos3 = Pos::new(MAP_SIZE - 1, MAP_SIZE - 1).unwrap(); /// let pos4 = Pos::new(MAP_SIZE - 1, MAP_SIZE - 1).unwrap(); /// let pos5 = Pos::new(MAP_SIZE - 1, 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 the cardinal neighbors of this positions pub fn neighbors(&self) -> impl Iterator { Direction::values().filter_map(|dir| self.step(dir)) } /// 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))) } /// Returns a random position provided with a rng #[must_use] pub fn random(rng: &mut R) -> Self { let x = rng.random_range(0..MAP_SIZE); let y = rng.random_range(0..MAP_SIZE); Self(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!(0, 0) } } impl TryFrom for Pos { type Error = (); fn try_from(value: usize) -> Result { Self::from_idx(value).ok_or(()) } } impl Distribution for StandardUniform { fn sample(&self, rng: &mut R) -> Pos { Pos::random(rng) } } /// The `FPos` type represents a floating 2D (temp) position. /// /// This position is not gurenteed to be inside the dungeon grid. FPos exists to /// tween between two different `Pos`. All bounds checks should be done in `Pos` before updating /// the underlying Floating Position. #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub struct FPos(f32, f32); impl FPos { /// Creates a new position from a given floating x and y position. /// /// Returns `None` if the position goes out of the map. /// /// # Examples /// /// ``` /// use dungeon::FPos; /// /// let fpos = FPos::new(5.4,6.7); /// ``` #[must_use] pub const fn new(x: f32, y: f32) -> Self { Self(x, y) } /// Creates a new `FPos` using the checked/fixed `Pos`. /// /// # Examples /// /// ``` /// use dungeon::{Pos, FPos}; /// /// let pos = Pos::new(1, 1).unwrap(); /// let fpos = FPos::from_pos(pos); /// ``` #[must_use] pub const fn from_pos(pos: Pos) -> Self { let (x, y) = pos.xy(); Self(x as f32, y as f32) } /// Returns the x and y positions of `FPos`. /// /// # Examples /// /// ``` /// use dungeon::FPos; /// /// let fpos = FPos::new(5.0,7.2); /// let (x,y) = fpos.xy(); /// assert_eq!(x, 5.0); /// assert_eq!(y, 7.2); /// ``` #[must_use] pub const fn xy(self) -> (f32, f32) { (self.0, self.1) } /// Returns the x component of `FPos` /// /// # Examples /// /// ``` /// use dungeon::FPos; /// /// let fpos = FPos::new(5.0,7.2); /// assert_eq!(fpos.x(), 5.0); /// ``` #[must_use] pub const fn x(&self) -> f32 { self.0 } /// Returns the y component of `FPos` /// /// # Examples /// /// ``` /// use dungeon::FPos; /// /// let fpos = FPos::new(5.0,7.2); /// assert_eq!(fpos.y(), 7.2); /// ``` #[must_use] pub const fn y(&self) -> f32 { self.1 } /// Returns the component-wise absolute value of `FPos`. #[must_use] pub const fn abs(&self) -> Self { Self(self.0.abs(), self.1.abs()) } /// Returns the euclidean magnitude of `FPos`. #[must_use] pub fn magnitude(&self) -> f32 { (self.0 * self.0 + self.1 * self.1).sqrt() } /// Steps `FPos` a given floating amount in the `Direction` `dir`. /// /// Returns `None` if the floating position wraps. /// /// # Examples /// /// ``` /// use dungeon::{Direction, FPos}; /// /// let fpos = FPos::new(0.0, 1.0); /// let new_fpos = fpos.step_by(Direction::North, 0.3); /// assert!(new_fpos.is_some()); /// ``` /// /// ``` /// use dungeon::{Direction, FPos}; /// /// let fpos = FPos::new(0.0, 0.0); /// let new_fpos = fpos.step_by(Direction::North, 5.0); /// assert!(new_fpos.is_none()); /// ``` #[must_use] pub fn step_by(self, dir: Direction, amt: f32) -> Option { use Direction as D; let (mut x, mut y) = self.xy(); match dir { D::North if y >= amt => y.sub_assign(amt), D::South => y.add_assign(amt), D::East => x.add_assign(amt), D::West if x >= amt => x.sub_assign(amt), _ => return None, }; Some(Self(x, y)) } /// Moves from one position towards another by at most some euclidean distance. /// Returns the distance moved. /// /// # Examples /// Note: some assertions may be commented out, as they would fail due to floating-point error. /// /// ``` /// use dungeon::FPos; /// /// let mut fpos1 = FPos::new(0.0,0.0); /// let fpos2 = FPos::new(0.0,1.0); /// let moved = fpos1.move_towards(fpos2, 0.6); /// assert_eq!(moved, 0.6); /// assert_eq!(fpos1, FPos::new(0.0, 0.6)); /// let moved = fpos1.move_towards(fpos2, 0.6); /// // assert_eq!(moved, 0.4); /// assert_eq!(fpos1, FPos::new(0.0, 1.0)); /// ``` /// pub fn move_towards(&mut self, goal: Self, by: f32) -> f32 { let diff = goal - *self; let dist = diff.magnitude(); if by >= dist { *self = goal; dist } else { *self += diff * by / dist; by } } /// Equivalent to `move_towards` but returns a copy, discarding the distance moved. /// /// # Examples /// /// ``` /// use dungeon::FPos; /// /// let fpos1 = FPos::new(0.0,0.0); /// let fpos2 = FPos::new(0.0,1.0); /// let fpos1 = fpos1.moved_towards(fpos2, 0.6); /// assert_eq!(fpos1, FPos::new(0.0, 0.6)); /// let fpos1 = fpos1.moved_towards(fpos2, 0.6); /// assert_eq!(fpos1, FPos::new(0.0, 1.0)); /// ``` #[must_use] pub fn moved_towards(mut self, goal: Self, by: f32) -> Self { self.move_towards(goal, by); self } /// Moves from one position towards another by at most some manhattan distance. /// Prefers vertical movement over horizontal movement. /// Returns the distance moved. /// /// # Examples /// Note: some assertions may be commented out, as they would fail due to floating-point error. /// /// ``` /// use dungeon::FPos; /// /// let mut fpos1 = FPos::new(0.0,0.0); /// let fpos2 = FPos::new(1.0,1.0); /// let moved = fpos1.move_towards_manhattan(fpos2, 0.6); /// assert_eq!(moved, 0.6); /// assert_eq!(fpos1, FPos::new(0.0, 0.6)); /// let moved = fpos1.move_towards_manhattan(fpos2, 0.6); /// assert_eq!(moved, 0.6); /// // assert_eq!(fpos1, FPos::new(0.2, 1.0)); /// let moved = fpos1.move_towards_manhattan(fpos2, 0.6); /// assert_eq!(moved, 0.6); /// // assert_eq!(fpos1, FPos::new(0.8, 1.0)); /// let moved = fpos1.move_towards_manhattan(fpos2, 0.6); /// // assert_eq!(moved, 0.2); /// assert_eq!(fpos1, FPos::new(1.0, 1.0)); /// ``` /// pub fn move_towards_manhattan(&mut self, goal: Self, by: f32) -> f32 { let vert_checkpoint = Self(self.0, goal.1); let mut rem = by; rem -= self.move_towards(vert_checkpoint, rem); rem -= self.move_towards(goal, rem); by - rem } /// Computes the absolute difference between to positions /// /// # Examples /// /// ``` /// use dungeon::FPos; /// /// let fpos1 = FPos::new(1.0,0.0); /// let fpos2 = FPos::new(0.0,1.0); /// let diff = fpos1.abs_diff(fpos2); /// assert_eq!(diff.xy(), (1.0, 1.0)); /// ``` /// #[must_use] pub fn abs_diff(self, other: Self) -> Self { (self - other).abs() } /// Returns the manhattan distance between `self` and `other` /// /// # Examples /// /// ``` /// use dungeon::FPos; /// /// let fpos1 = FPos::new(1.0, 0.0); /// let fpos2 = FPos::new(1.2, 1.3); /// assert_eq!(fpos1.manhattan(fpos2), 1.5); /// ``` #[must_use] pub fn manhattan(self, other: Self) -> f32 { let abs_diff = Self::abs_diff(self, other); abs_diff.0 + abs_diff.1 } } impl AddAssign for FPos { fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0; self.1 += rhs.1; } } impl Add for FPos { type Output = Self; fn add(mut self, rhs: Self) -> Self { self += rhs; self } } impl SubAssign for FPos { fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0; self.1 -= rhs.1; } } impl Sub for FPos { type Output = Self; fn sub(mut self, rhs: Self) -> Self { self -= rhs; self } } impl MulAssign for FPos { fn mul_assign(&mut self, rhs: f32) { self.0 *= rhs; self.1 *= rhs; } } impl Mul for FPos { type Output = Self; fn mul(mut self, rhs: f32) -> Self { self *= rhs; self } } impl DivAssign for FPos { fn div_assign(&mut self, rhs: f32) { self.0 /= rhs; self.1 /= rhs; } } impl Div for FPos { type Output = Self; fn div(mut self, rhs: f32) -> Self { self /= rhs; self } } impl Default for FPos { /// Returns a default postion at the origin (0,0) /// /// ``` /// use dungeon::FPos; /// /// let fpos = FPos::default(); /// /// assert_eq!(fpos.xy(), (0.0, 0.0)); /// ``` /// fn default() -> Self { Self::new(0.0, 0.0) } } impl From for FPos { fn from(value: Pos) -> Self { Self::from_pos(value) } }