Skip to main content

sokoban_elements/
point2d.rs

1use std::fmt;
2use std::fmt::Display;
3use std::num::TryFromIntError;
4
5use crate::utilities::{index_x, index_y, saturating_index_1d};
6use crate::{Direction, DirectionsSet, DirectionsSetIter, Size2D, Tessellation, TileShape};
7
8/// 1D / 2D coordinate for [`GameBoard`](crate::GameBoard) positions.
9///
10/// It can calculate it's neighboring positions in context of underlying [`Tessellation`] and
11/// [`Size2D`]:
12///
13/// ```rust
14/// # use sokoban_elements::*;
15/// #
16/// # fn main() {
17/// #
18/// let size = Size2D::new(19, 11);
19///
20/// let position = Point2D::new(4, 2);
21/// let neighbor: Option<Point2D> = position.neighbor(Tessellation::Hexoban, Direction::Left, size);
22///
23/// let position = Point2D::new(11, 8);
24/// let neighbor: Option<Point2D> = position.neighbor(Tessellation::Sokoban, Direction::Up, size);
25/// assert!(neighbor.unwrap() == Point2D::new(11, 7));
26///
27/// let neighbor_directions: Option<DirectionsSet> =
28///     position.neighbor_directions(Tessellation::Sokoban, size);
29/// assert!(
30///     neighbor_directions.unwrap()
31///         == [
32///             Direction::Up,
33///             Direction::Right,
34///             Direction::Down,
35///             Direction::Left
36///         ]
37/// );
38///
39/// let neighbors = Vec::from_iter(position.neighbors(Tessellation::Sokoban, size).unwrap());
40/// assert!(
41///     neighbors
42///         == vec![
43///             (Direction::Up, Point2D::new(11, 7)),
44///             (Direction::Right, Point2D::new(12, 8)),
45///             (Direction::Down, Point2D::new(11, 9)),
46///             (Direction::Left, Point2D::new(10, 8))
47///         ]
48/// );
49/// # }
50/// ```
51///
52/// In situations where game board is better represented as 1D array, `Point2D` can be still used to
53/// reference individual game board tiles. It implements conversion between 2D `(x, y)` coordinate
54/// representation and equivalent 1D array index.
55///
56/// ```rust
57/// # use sokoban_elements::*;
58/// #
59/// # fn main() {
60/// #
61/// let size = Size2D::new(5, 6);
62/// let pos_2d = Point2D::new(4, 2);
63/// let pos_1d: usize = pos_2d.idx(size);
64/// #
65/// # }
66/// ```
67#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
68pub struct Point2D {
69    pub x: usize,
70    pub y: usize,
71}
72
73impl Point2D {
74    pub fn new(x: usize, y: usize) -> Self { Self { x, y } }
75
76    /// Creates `Point2D` from 1D array index and board size.
77    pub fn new_from_idx(idx: usize, size: &Size2D) -> Self {
78        Self {
79            x: index_x(idx, size.width),
80            y: index_y(idx, size.width),
81        }
82    }
83
84    /// Returns `Point2D` at origin `(0, 0)`.
85    pub fn origin() -> Self { Self { x: 0, y: 0 } }
86
87    /// Returns `true` if `self` is on board, given the board size.
88    pub fn is_on_board(&self, size: Size2D) -> bool { self.x < size.width && self.y < size.height }
89
90    /// Converts `Point2D` into 1D array position.
91    ///
92    /// 1D index can be used to reference board positions for 2D boards stored as 1D array of tiles.
93    pub fn idx(&self, size: Size2D) -> usize {
94        // Chosen for convenience. In real world there are no such large Sokoban boards
95        // that coordinates don't fit into `usize`.
96        saturating_index_1d(self.x, self.y, size.width)
97    }
98
99    /// See [`Tessellation::neighbor()`].
100    pub fn neighbor(
101        &self, tessellation: Tessellation, to: Direction, size: Size2D,
102    ) -> Option<Self> {
103        tessellation
104            .neighbor(self.x, self.y, to, size.width, size.height)
105            .map(|(new_x, new_y)| Self { x: new_x, y: new_y })
106    }
107
108    /// Returns iterator over neighboring `(Direction, Point2D)`.
109    ///
110    /// Returns `None` if `self` is off-board coordinate.
111    ///
112    /// See [`Tessellation::neighbor()`].
113    pub fn neighbors(&self, tessellation: Tessellation, size: Size2D) -> Option<IterNeighbors> {
114        IterNeighbors::new(tessellation, *self, size)
115    }
116
117    /// See [`Tessellation::neighbor_directions()`].
118    pub fn neighbor_directions(
119        &self, tessellation: Tessellation, size: Size2D,
120    ) -> Option<DirectionsSet> {
121        tessellation.neighbor_directions(self.x, self.y, size.width, size.height)
122    }
123
124    /// See [`Tessellation::tile_shape_from_pos()`].
125    pub fn tile_shape(&self, tessellation: Tessellation) -> TileShape {
126        tessellation.tile_shape_from_pos(self.x, self.y)
127    }
128}
129
130impl Display for Point2D {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "({}, {})", self.x, self.y)
133    }
134}
135
136// ----------------------------------------
137// Conversions from tuple of values
138// ----------------------------------------
139
140macro_rules! impl_point2d_from_2D {
141    (usize) => {
142        /// `value` is `(x, y)`
143        impl From<(usize, usize)> for Point2D {
144            fn from(value: (usize, usize)) -> Self {
145                Self {
146                    x: value.0,
147                    y: value.1,
148                }
149            }
150        }
151
152        /// `value` is `(x, y)`
153        impl From<[usize; 2]> for Point2D {
154            fn from(value: [usize; 2]) -> Self { Self::from((value[0], value[1])) }
155        }
156    };
157
158    (u8) => {
159        impl_point2d_from_2D!(@smaller u8);
160    };
161    (u16) => {
162        impl_point2d_from_2D!(@smaller u16);
163    };
164
165    (@smaller $t:ty) => {
166        /// `value` is `(x, y)`
167        impl From<($t, $t)> for Point2D {
168            fn from(value: ($t, $t)) -> Self {
169                Self {
170                    x: value.0.into(),
171                    y: value.1.into(),
172                }
173            }
174        }
175
176        /// `value` is `(x, y)`
177        impl From<[$t; 2]> for Point2D {
178            fn from(value: [$t; 2]) -> Self { Self::from((value[0], value[1])) }
179        }
180    };
181
182    (u32) => {
183        impl_point2d_from_2D!(@others u32);
184    };
185    (u64) => {
186        impl_point2d_from_2D!(@others u64);
187    };
188    (isize) => {
189        impl_point2d_from_2D!(@others isize);
190    };
191    (i64) => {
192        impl_point2d_from_2D!(@others i64);
193    };
194    (i32) => {
195        impl_point2d_from_2D!(@others i32);
196    };
197    (i16) => {
198        impl_point2d_from_2D!(@others i16);
199    };
200    (i8) => {
201        impl_point2d_from_2D!(@others i8);
202    };
203
204
205    (@others $t:ty) => {
206        impl TryFrom<($t, $t)> for Point2D {
207            type Error = TryFromIntError;
208
209            /// `value` is `(x, y)`
210            fn try_from(value: ($t, $t)) -> Result<Self, Self::Error> {
211                Ok(Point2D {
212                    x: value.0.try_into()?,
213                    y: value.1.try_into()?,
214                })
215            }
216        }
217
218        impl TryFrom<[$t; 2]> for Point2D {
219            type Error = TryFromIntError;
220
221            /// `value` is `[x, y]`
222            fn try_from(value: [$t; 2]) -> Result<Self, Self::Error> {
223                Self::try_from((value[0], value[1]))
224            }
225        }
226    };
227}
228
229impl_point2d_from_2D!(usize);
230impl_point2d_from_2D!(u64);
231impl_point2d_from_2D!(u32);
232impl_point2d_from_2D!(u16);
233impl_point2d_from_2D!(u8);
234impl_point2d_from_2D!(isize);
235impl_point2d_from_2D!(i64);
236impl_point2d_from_2D!(i32);
237impl_point2d_from_2D!(i16);
238impl_point2d_from_2D!(i8);
239
240// ----------------------------------------
241// Neighbors iterator
242// ----------------------------------------
243
244/// Iterator over [`Point2D`] neighboring tuples of `(Direction, Point2D)`.
245#[derive(Debug, Clone, Copy, PartialEq)]
246pub struct IterNeighbors {
247    tessellation:             Tessellation,
248    src:                      Point2D,
249    size:                     Size2D,
250    only_neighbor_directions: DirectionsSetIter,
251}
252
253impl IterNeighbors {
254    pub(self) fn new(tessellation: Tessellation, at: Point2D, size: Size2D) -> Option<Self> {
255        tessellation
256            .neighbor_directions(at.x, at.y, size.width, size.height)
257            .map(|ndd| {
258                Self {
259                    tessellation,
260                    src: at,
261                    size,
262                    only_neighbor_directions: ndd.iter(),
263                }
264            })
265    }
266}
267
268impl Iterator for IterNeighbors {
269    type Item = (Direction, Point2D);
270
271    fn next(&mut self) -> Option<(Direction, Point2D)> {
272        self.only_neighbor_directions.next().map(|d| {
273            self.src
274                .neighbor(self.tessellation, d, self.size)
275                .map(|p| (d, p))
276        })?
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    const DATA: [(usize, usize, usize); 45] = [
285        (0, 0, 0),
286        (1, 0, 1),
287        (2, 0, 2),
288        (3, 0, 3),
289        (4, 0, 4),
290        (0, 1, 5),
291        (1, 1, 6),
292        (2, 1, 7),
293        (3, 1, 8),
294        (4, 1, 9),
295        (0, 2, 10),
296        (1, 2, 11),
297        (2, 2, 12),
298        (3, 2, 13),
299        (4, 2, 14),
300        (0, 3, 15),
301        (1, 3, 16),
302        (2, 3, 17),
303        (3, 3, 18),
304        (4, 3, 19),
305        (0, 4, 20),
306        (1, 4, 21),
307        (2, 4, 22),
308        (3, 4, 23),
309        (4, 4, 24),
310        (0, 5, 25),
311        (1, 5, 26),
312        (2, 5, 27),
313        (3, 5, 28),
314        (4, 5, 29),
315        (0, 6, 30),
316        (1, 6, 31),
317        (2, 6, 32),
318        (3, 6, 33),
319        (4, 6, 34),
320        (0, 7, 35),
321        (1, 7, 36),
322        (2, 7, 37),
323        (3, 7, 38),
324        (4, 7, 39),
325        (0, 8, 40),
326        (1, 8, 41),
327        (2, 8, 42),
328        (3, 8, 43),
329        (4, 8, 44),
330    ];
331
332    #[test]
333    fn it_can_be_constructed_from_2d_coordinates() {
334        let size: Size2D = (5_usize, 9_usize).into();
335
336        for (x, y, idx) in DATA {
337            let pos = Point2D::new(x, y);
338            assert_eq!(pos.x, x);
339            assert_eq!(pos.y, y);
340            assert_eq!(pos.idx(size), idx);
341        }
342    }
343
344    #[test]
345    fn it_can_be_constructed_from_1d_index() {
346        let size: Size2D = (5_usize, 9_usize).into();
347
348        for (x, y, idx) in DATA {
349            let pos = Point2D::new_from_idx(idx, &size);
350            assert_eq!(pos.x, x);
351            assert_eq!(pos.y, y);
352            assert_eq!(pos.idx(size), idx);
353        }
354    }
355}