sokoban-elements 0.2.1

Parsing and writing Sokoban, Hexoban, Trioban and Octoban file formats.
Documentation
use std::fmt;
use std::fmt::Display;
use std::num::TryFromIntError;

use crate::utilities::{index_x, index_y, saturating_index_1d};
use crate::{Direction, DirectionsSet, DirectionsSetIter, Size2D, Tessellation, TileShape};

/// 1D / 2D coordinate for [`GameBoard`](crate::GameBoard) positions.
///
/// It can calculate it's neighboring positions in context of underlying [`Tessellation`] and
/// [`Size2D`]:
///
/// ```rust
/// # use sokoban_elements::*;
/// #
/// # fn main() {
/// #
/// let size = Size2D::new(19, 11);
///
/// let position = Point2D::new(4, 2);
/// let neighbor: Option<Point2D> = position.neighbor(Tessellation::Hexoban, Direction::Left, size);
///
/// let position = Point2D::new(11, 8);
/// let neighbor: Option<Point2D> = position.neighbor(Tessellation::Sokoban, Direction::Up, size);
/// assert!(neighbor.unwrap() == Point2D::new(11, 7));
///
/// let neighbor_directions: Option<DirectionsSet> =
///     position.neighbor_directions(Tessellation::Sokoban, size);
/// assert!(
///     neighbor_directions.unwrap()
///         == [
///             Direction::Up,
///             Direction::Right,
///             Direction::Down,
///             Direction::Left
///         ]
/// );
///
/// let neighbors = Vec::from_iter(position.neighbors(Tessellation::Sokoban, size).unwrap());
/// assert!(
///     neighbors
///         == vec![
///             (Direction::Up, Point2D::new(11, 7)),
///             (Direction::Right, Point2D::new(12, 8)),
///             (Direction::Down, Point2D::new(11, 9)),
///             (Direction::Left, Point2D::new(10, 8))
///         ]
/// );
/// # }
/// ```
///
/// In situations where game board is better represented as 1D array, `Point2D` can be still used to
/// reference individual game board tiles. It implements conversion between 2D `(x, y)` coordinate
/// representation and equivalent 1D array index.
///
/// ```rust
/// # use sokoban_elements::*;
/// #
/// # fn main() {
/// #
/// let size = Size2D::new(5, 6);
/// let pos_2d = Point2D::new(4, 2);
/// let pos_1d: usize = pos_2d.idx(size);
/// #
/// # }
/// ```
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Point2D {
    pub x: usize,
    pub y: usize,
}

impl Point2D {
    pub fn new(x: usize, y: usize) -> Self { Self { x, y } }

    /// Creates `Point2D` from 1D array index and board size.
    pub fn new_from_idx(idx: usize, size: &Size2D) -> Self {
        Self {
            x: index_x(idx, size.width),
            y: index_y(idx, size.width),
        }
    }

    /// Returns `Point2D` at origin `(0, 0)`.
    pub fn origin() -> Self { Self { x: 0, y: 0 } }

    /// Returns `true` if `self` is on board, given the board size.
    pub fn is_on_board(&self, size: Size2D) -> bool { self.x < size.width && self.y < size.height }

    /// Converts `Point2D` into 1D array position.
    ///
    /// 1D index can be used to reference board positions for 2D boards stored as 1D array of tiles.
    pub fn idx(&self, size: Size2D) -> usize {
        // Chosen for convenience. In real world there are no such large Sokoban boards
        // that coordinates don't fit into `usize`.
        saturating_index_1d(self.x, self.y, size.width)
    }

    /// See [`Tessellation::neighbor()`].
    pub fn neighbor(
        &self, tessellation: Tessellation, to: Direction, size: Size2D,
    ) -> Option<Self> {
        tessellation
            .neighbor(self.x, self.y, to, size.width, size.height)
            .map(|(new_x, new_y)| Self { x: new_x, y: new_y })
    }

    /// Returns iterator over neighboring `(Direction, Point2D)`.
    ///
    /// Returns `None` if `self` is off-board coordinate.
    ///
    /// See [`Tessellation::neighbor()`].
    pub fn neighbors(&self, tessellation: Tessellation, size: Size2D) -> Option<IterNeighbors> {
        IterNeighbors::new(tessellation, *self, size)
    }

    /// See [`Tessellation::neighbor_directions()`].
    pub fn neighbor_directions(
        &self, tessellation: Tessellation, size: Size2D,
    ) -> Option<DirectionsSet> {
        tessellation.neighbor_directions(self.x, self.y, size.width, size.height)
    }

    /// See [`Tessellation::tile_shape_from_pos()`].
    pub fn tile_shape(&self, tessellation: Tessellation) -> TileShape {
        tessellation.tile_shape_from_pos(self.x, self.y)
    }
}

impl Display for Point2D {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

// ----------------------------------------
// Conversions from tuple of values
// ----------------------------------------

macro_rules! impl_point2d_from_2D {
    (usize) => {
        /// `value` is `(x, y)`
        impl From<(usize, usize)> for Point2D {
            fn from(value: (usize, usize)) -> Self {
                Self {
                    x: value.0,
                    y: value.1,
                }
            }
        }

        /// `value` is `(x, y)`
        impl From<[usize; 2]> for Point2D {
            fn from(value: [usize; 2]) -> Self { Self::from((value[0], value[1])) }
        }
    };

    (u8) => {
        impl_point2d_from_2D!(@smaller u8);
    };
    (u16) => {
        impl_point2d_from_2D!(@smaller u16);
    };

    (@smaller $t:ty) => {
        /// `value` is `(x, y)`
        impl From<($t, $t)> for Point2D {
            fn from(value: ($t, $t)) -> Self {
                Self {
                    x: value.0.into(),
                    y: value.1.into(),
                }
            }
        }

        /// `value` is `(x, y)`
        impl From<[$t; 2]> for Point2D {
            fn from(value: [$t; 2]) -> Self { Self::from((value[0], value[1])) }
        }
    };

    (u32) => {
        impl_point2d_from_2D!(@others u32);
    };
    (u64) => {
        impl_point2d_from_2D!(@others u64);
    };
    (isize) => {
        impl_point2d_from_2D!(@others isize);
    };
    (i64) => {
        impl_point2d_from_2D!(@others i64);
    };
    (i32) => {
        impl_point2d_from_2D!(@others i32);
    };
    (i16) => {
        impl_point2d_from_2D!(@others i16);
    };
    (i8) => {
        impl_point2d_from_2D!(@others i8);
    };


    (@others $t:ty) => {
        impl TryFrom<($t, $t)> for Point2D {
            type Error = TryFromIntError;

            /// `value` is `(x, y)`
            fn try_from(value: ($t, $t)) -> Result<Self, Self::Error> {
                Ok(Point2D {
                    x: value.0.try_into()?,
                    y: value.1.try_into()?,
                })
            }
        }

        impl TryFrom<[$t; 2]> for Point2D {
            type Error = TryFromIntError;

            /// `value` is `[x, y]`
            fn try_from(value: [$t; 2]) -> Result<Self, Self::Error> {
                Self::try_from((value[0], value[1]))
            }
        }
    };
}

impl_point2d_from_2D!(usize);
impl_point2d_from_2D!(u64);
impl_point2d_from_2D!(u32);
impl_point2d_from_2D!(u16);
impl_point2d_from_2D!(u8);
impl_point2d_from_2D!(isize);
impl_point2d_from_2D!(i64);
impl_point2d_from_2D!(i32);
impl_point2d_from_2D!(i16);
impl_point2d_from_2D!(i8);

// ----------------------------------------
// Neighbors iterator
// ----------------------------------------

/// Iterator over [`Point2D`] neighboring tuples of `(Direction, Point2D)`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IterNeighbors {
    tessellation:             Tessellation,
    src:                      Point2D,
    size:                     Size2D,
    only_neighbor_directions: DirectionsSetIter,
}

impl IterNeighbors {
    pub(self) fn new(tessellation: Tessellation, at: Point2D, size: Size2D) -> Option<Self> {
        tessellation
            .neighbor_directions(at.x, at.y, size.width, size.height)
            .map(|ndd| {
                Self {
                    tessellation,
                    src: at,
                    size,
                    only_neighbor_directions: ndd.iter(),
                }
            })
    }
}

impl Iterator for IterNeighbors {
    type Item = (Direction, Point2D);

    fn next(&mut self) -> Option<(Direction, Point2D)> {
        self.only_neighbor_directions.next().map(|d| {
            self.src
                .neighbor(self.tessellation, d, self.size)
                .map(|p| (d, p))
        })?
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const DATA: [(usize, usize, usize); 45] = [
        (0, 0, 0),
        (1, 0, 1),
        (2, 0, 2),
        (3, 0, 3),
        (4, 0, 4),
        (0, 1, 5),
        (1, 1, 6),
        (2, 1, 7),
        (3, 1, 8),
        (4, 1, 9),
        (0, 2, 10),
        (1, 2, 11),
        (2, 2, 12),
        (3, 2, 13),
        (4, 2, 14),
        (0, 3, 15),
        (1, 3, 16),
        (2, 3, 17),
        (3, 3, 18),
        (4, 3, 19),
        (0, 4, 20),
        (1, 4, 21),
        (2, 4, 22),
        (3, 4, 23),
        (4, 4, 24),
        (0, 5, 25),
        (1, 5, 26),
        (2, 5, 27),
        (3, 5, 28),
        (4, 5, 29),
        (0, 6, 30),
        (1, 6, 31),
        (2, 6, 32),
        (3, 6, 33),
        (4, 6, 34),
        (0, 7, 35),
        (1, 7, 36),
        (2, 7, 37),
        (3, 7, 38),
        (4, 7, 39),
        (0, 8, 40),
        (1, 8, 41),
        (2, 8, 42),
        (3, 8, 43),
        (4, 8, 44),
    ];

    #[test]
    fn it_can_be_constructed_from_2d_coordinates() {
        let size: Size2D = (5_usize, 9_usize).into();

        for (x, y, idx) in DATA {
            let pos = Point2D::new(x, y);
            assert_eq!(pos.x, x);
            assert_eq!(pos.y, y);
            assert_eq!(pos.idx(size), idx);
        }
    }

    #[test]
    fn it_can_be_constructed_from_1d_index() {
        let size: Size2D = (5_usize, 9_usize).into();

        for (x, y, idx) in DATA {
            let pos = Point2D::new_from_idx(idx, &size);
            assert_eq!(pos.x, x);
            assert_eq!(pos.y, y);
            assert_eq!(pos.idx(size), idx);
        }
    }
}