Skip to main content

shuyi_core/
grid.rs

1//! Grid geometry: positions, the four directions, a rectangular grid and
2//! sets of its tiles, and the terrain a tile is.
3
4use alloc::vec;
5use alloc::vec::Vec;
6
7use core::fmt;
8
9use serde::{Deserialize, Serialize};
10
11/// A tile position. Signed so neighbour maths can go negative before bounds
12/// checks; `(0, 0)` is the top-left tile, `x` grows right, `y` grows down.
13#[derive(
14    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
15)]
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17pub struct Pos {
18    /// Column.
19    pub x: i32,
20    /// Row.
21    pub y: i32,
22}
23
24/// A terrain: its index in the game's table of terrains. The engine knows
25/// no terrain of its own; a game's map is a [`Grid`] of these, and its
26/// skins look each one up in the game's table.
27#[derive(
28    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
29)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31pub struct TerrainId(pub u16);
32
33impl Pos {
34    /// Creates a position.
35    pub const fn new(x: i32, y: i32) -> Self {
36        Self { x, y }
37    }
38
39    /// Manhattan (4-way) distance between `a` and `b`.
40    pub fn manhattan(a: Pos, b: Pos) -> u32 {
41        a.x.abs_diff(b.x) + a.y.abs_diff(b.y)
42    }
43
44    /// The adjacent position in direction `dir` (may be out of bounds).
45    #[must_use]
46    pub fn step(self, dir: Dir) -> Pos {
47        let (dx, dy) = dir.delta();
48        Pos::new(self.x + dx, self.y + dy)
49    }
50}
51
52/// One of the four grid directions.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55pub enum Dir {
56    /// `+x`.
57    Right,
58    /// `+y`.
59    Down,
60    /// `-x`.
61    Left,
62    /// `-y`.
63    Up,
64}
65
66impl Dir {
67    /// Every direction in the fixed order used for deterministic iteration.
68    pub const ALL: [Dir; 4] = [Dir::Right, Dir::Down, Dir::Left, Dir::Up];
69
70    /// The `(dx, dy)` offset of one step in this direction.
71    pub const fn delta(self) -> (i32, i32) {
72        match self {
73            Dir::Right => (1, 0),
74            Dir::Down => (0, 1),
75            Dir::Left => (-1, 0),
76            Dir::Up => (0, -1),
77        }
78    }
79}
80
81/// A `width × height` rectangle of cells, stored row-major. The cell count
82/// always equals `width * height`; deserialising checks it.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
85#[serde(try_from = "RawGrid<T>")]
86pub struct Grid<T> {
87    width: u16,
88    height: u16,
89    cells: Vec<T>,
90}
91
92impl<T> Grid<T> {
93    /// Builds a grid from row-major `cells`; `None` if the count is not
94    /// `width * height`.
95    pub fn from_cells(width: u16, height: u16, cells: Vec<T>) -> Option<Self> {
96        (cells.len() == usize::from(width) * usize::from(height)).then_some(Self {
97            width,
98            height,
99            cells,
100        })
101    }
102
103    /// A `width × height` grid with every cell set to `value`.
104    pub fn filled(width: u16, height: u16, value: T) -> Self
105    where
106        T: Clone,
107    {
108        Self {
109            width,
110            height,
111            cells: vec![value; usize::from(width) * usize::from(height)],
112        }
113    }
114
115    /// Number of columns.
116    pub fn width(&self) -> u16 {
117        self.width
118    }
119
120    /// Number of rows.
121    pub fn height(&self) -> u16 {
122        self.height
123    }
124
125    /// Every cell, row-major.
126    pub fn cells(&self) -> &[T] {
127        &self.cells
128    }
129
130    /// Whether `pos` is inside the grid.
131    pub fn in_bounds(&self, pos: Pos) -> bool {
132        pos.x >= 0 && pos.y >= 0 && pos.x < i32::from(self.width) && pos.y < i32::from(self.height)
133    }
134
135    /// The cell at `pos`, or `None` if out of bounds.
136    pub fn get(&self, pos: Pos) -> Option<&T> {
137        self.cells.get(self.index(pos)?)
138    }
139
140    /// The cell at `pos` for writing, or `None` if out of bounds.
141    pub fn get_mut(&mut self, pos: Pos) -> Option<&mut T> {
142        let i = self.index(pos)?;
143        self.cells.get_mut(i)
144    }
145
146    /// The row-major index of `pos`, or `None` if out of bounds.
147    fn index(&self, pos: Pos) -> Option<usize> {
148        if !self.in_bounds(pos) {
149            return None;
150        }
151        // In bounds, so both coordinates are non-negative and small.
152        let x = usize::try_from(pos.x).ok()?;
153        let y = usize::try_from(pos.y).ok()?;
154        Some(y * usize::from(self.width) + x)
155    }
156
157    /// The in-bounds neighbours of `pos`, in [`Dir::ALL`] order.
158    pub fn neighbors4(&self, pos: Pos) -> impl Iterator<Item = Pos> + '_ {
159        Dir::ALL
160            .into_iter()
161            .map(move |d| pos.step(d))
162            .filter(|&p| self.in_bounds(p))
163    }
164
165    /// Every position, row-major (the order of [`Grid::cells`]).
166    pub fn positions(&self) -> impl Iterator<Item = Pos> + use<T> {
167        let (w, h) = (i32::from(self.width), i32::from(self.height));
168        (0..h).flat_map(move |y| (0..w).map(move |x| Pos::new(x, y)))
169    }
170}
171
172/// A [`Grid`] as written in a save, before its cell count is checked.
173#[derive(Deserialize)]
174#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
175struct RawGrid<T> {
176    width: u16,
177    height: u16,
178    cells: Vec<T>,
179}
180
181/// A saved grid whose cell count is not `width * height`.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub struct GridSizeError;
184
185impl fmt::Display for GridSizeError {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        f.write_str("the grid's cell count is not width × height")
188    }
189}
190
191impl<T> TryFrom<RawGrid<T>> for Grid<T> {
192    type Error = GridSizeError;
193
194    fn try_from(raw: RawGrid<T>) -> Result<Self, GridSizeError> {
195        Grid::from_cells(raw.width, raw.height, raw.cells).ok_or(GridSizeError)
196    }
197}
198
199/// A set of tiles of a `width × height` map, stored as a bitset. Iterates in
200/// row-major order. Positions outside the map are never members.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct TileSet {
203    width: u16,
204    height: u16,
205    words: Vec<u64>,
206}
207
208impl TileSet {
209    /// An empty set for a `width × height` map.
210    pub fn new(width: u16, height: u16) -> Self {
211        let bits = usize::from(width) * usize::from(height);
212        Self {
213            width,
214            height,
215            words: vec![0; bits.div_ceil(64)],
216        }
217    }
218
219    /// Map width.
220    pub fn width(&self) -> u16 {
221        self.width
222    }
223
224    /// Map height.
225    pub fn height(&self) -> u16 {
226        self.height
227    }
228
229    /// Whether `pos` is in the set.
230    pub fn contains(&self, pos: Pos) -> bool {
231        self.bit(pos)
232            .is_some_and(|(w, mask)| self.words.get(w).is_some_and(|word| word & mask != 0))
233    }
234
235    /// Adds `pos`; returns whether it was newly added (`false` if already a
236    /// member or outside the map).
237    pub fn insert(&mut self, pos: Pos) -> bool {
238        let Some((w, mask)) = self.bit(pos) else {
239            return false;
240        };
241        let Some(word) = self.words.get_mut(w) else {
242            return false;
243        };
244        let added = *word & mask == 0;
245        *word |= mask;
246        added
247    }
248
249    /// Adds every member of `other` that lies inside this set's map.
250    pub fn union_with(&mut self, other: &TileSet) {
251        for pos in other.iter() {
252            self.insert(pos);
253        }
254    }
255
256    /// Number of members.
257    pub fn len(&self) -> usize {
258        self.words.iter().map(|w| w.count_ones() as usize).sum()
259    }
260
261    /// Whether the set has no members.
262    pub fn is_empty(&self) -> bool {
263        self.words.iter().all(|&w| w == 0)
264    }
265
266    /// Every member, row-major.
267    pub fn iter(&self) -> impl Iterator<Item = Pos> + '_ {
268        let width = usize::from(self.width);
269        self.words
270            .iter()
271            .enumerate()
272            .filter(|&(_, &word)| word != 0)
273            .flat_map(move |(w, &word)| {
274                (0..64)
275                    .filter(move |bit| word >> bit & 1 == 1)
276                    .filter_map(move |bit| {
277                        let index = w * 64 + bit;
278                        // Both fit: index < width * height ≤ u16::MAX².
279                        let x = i32::try_from(index % width).ok()?;
280                        let y = i32::try_from(index / width).ok()?;
281                        Some(Pos::new(x, y))
282                    })
283            })
284    }
285
286    /// The word index and bit mask of `pos`, or `None` outside the map.
287    fn bit(&self, pos: Pos) -> Option<(usize, u64)> {
288        let x = u16::try_from(pos.x).ok().filter(|&x| x < self.width)?;
289        let y = u16::try_from(pos.y).ok().filter(|&y| y < self.height)?;
290        let index = usize::from(y) * usize::from(self.width) + usize::from(x);
291        Some((index / 64, 1 << (index % 64)))
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use alloc::collections::BTreeSet;
298    use alloc::string::ToString;
299
300    use proptest::prelude::*;
301
302    use super::*;
303
304    fn grid(w: u16, h: u16) -> Grid<u32> {
305        let n = u32::from(w) * u32::from(h);
306        Grid::from_cells(w, h, (0..n).collect()).unwrap_or_else(|| unreachable!())
307    }
308
309    fn p(x: i32, y: i32) -> Pos {
310        Pos::new(x, y)
311    }
312
313    #[test]
314    fn manhattan_distance() {
315        assert_eq!(Pos::manhattan(Pos::new(0, 0), Pos::new(3, 4)), 7);
316        assert_eq!(Pos::manhattan(Pos::new(3, 4), Pos::new(0, 0)), 7);
317        assert_eq!(Pos::manhattan(Pos::new(-2, 5), Pos::new(1, 1)), 7);
318        assert_eq!(Pos::manhattan(Pos::new(2, 2), Pos::new(2, 2)), 0);
319    }
320
321    #[test]
322    fn step_follows_dir_delta() {
323        let p = Pos::new(5, 5);
324        assert_eq!(p.step(Dir::Right), Pos::new(6, 5));
325        assert_eq!(p.step(Dir::Down), Pos::new(5, 6));
326        assert_eq!(p.step(Dir::Left), Pos::new(4, 5));
327        assert_eq!(p.step(Dir::Up), Pos::new(5, 4));
328    }
329
330    #[test]
331    fn from_cells_checks_count() {
332        assert!(Grid::from_cells(2, 3, vec![0; 6]).is_some());
333        assert!(Grid::from_cells(2, 3, vec![0; 5]).is_none());
334        assert!(Grid::from_cells(2, 3, vec![0; 7]).is_none());
335        let g = Grid::from_cells(0, 0, Vec::<u8>::new());
336        assert!(g.is_some_and(|g| !g.in_bounds(Pos::new(0, 0))));
337    }
338
339    #[test]
340    fn dimensions_and_cells() {
341        let g = grid(3, 2);
342        assert_eq!((g.width(), g.height()), (3, 2));
343        assert_eq!(g.cells(), &[0, 1, 2, 3, 4, 5]);
344    }
345
346    #[test]
347    fn bounds_and_get() {
348        let g = grid(3, 2);
349        assert!(g.in_bounds(Pos::new(0, 0)));
350        assert!(g.in_bounds(Pos::new(2, 1)));
351        assert!(!g.in_bounds(Pos::new(3, 0)));
352        assert!(!g.in_bounds(Pos::new(0, 2)));
353        assert!(!g.in_bounds(Pos::new(-1, 0)));
354        assert!(!g.in_bounds(Pos::new(0, -1)));
355        assert_eq!(g.get(Pos::new(0, 0)), Some(&0));
356        assert_eq!(g.get(Pos::new(2, 0)), Some(&2));
357        assert_eq!(g.get(Pos::new(1, 1)), Some(&4));
358        assert_eq!(g.get(Pos::new(2, 1)), Some(&5));
359        assert_eq!(g.get(Pos::new(3, 0)), None);
360        assert_eq!(g.get(Pos::new(-1, 1)), None);
361    }
362
363    #[test]
364    fn filled_and_get_mut() {
365        let mut g = Grid::filled(3, 2, 7u8);
366        assert_eq!((g.width(), g.height()), (3, 2));
367        assert_eq!(g.cells(), &[7; 6]);
368        if let Some(c) = g.get_mut(Pos::new(2, 1)) {
369            *c = 9;
370        }
371        if let Some(c) = g.get_mut(Pos::new(1, 0)) {
372            *c = 5;
373        }
374        assert_eq!(g.cells(), &[7, 5, 7, 7, 7, 9]);
375        assert!(g.get_mut(Pos::new(3, 0)).is_none());
376        assert!(g.get_mut(Pos::new(0, -1)).is_none());
377    }
378
379    #[test]
380    fn neighbors_in_fixed_order() {
381        let g = grid(3, 3);
382        let n: Vec<Pos> = g.neighbors4(Pos::new(1, 1)).collect();
383        assert_eq!(
384            n,
385            [
386                Pos::new(2, 1),
387                Pos::new(1, 2),
388                Pos::new(0, 1),
389                Pos::new(1, 0)
390            ]
391        );
392        let corner: Vec<Pos> = g.neighbors4(Pos::new(0, 0)).collect();
393        assert_eq!(corner, [Pos::new(1, 0), Pos::new(0, 1)]);
394        let far: Vec<Pos> = g.neighbors4(Pos::new(2, 2)).collect();
395        assert_eq!(far, [Pos::new(1, 2), Pos::new(2, 1)]);
396    }
397
398    #[test]
399    fn positions_are_row_major() {
400        let g = grid(2, 2);
401        let p: Vec<Pos> = g.positions().collect();
402        assert_eq!(
403            p,
404            [
405                Pos::new(0, 0),
406                Pos::new(1, 0),
407                Pos::new(0, 1),
408                Pos::new(1, 1)
409            ]
410        );
411        for (pos, cell) in g.positions().zip(g.cells()) {
412            assert_eq!(g.get(pos), Some(cell));
413        }
414    }
415
416    #[test]
417    fn grid_serde_round_trips_and_checks_the_cell_count() {
418        let g = grid(3, 2);
419        let text = ron::to_string(&g).unwrap();
420        assert_eq!(text, "(width:3,height:2,cells:[0,1,2,3,4,5])");
421        assert_eq!(ron::from_str::<Grid<u32>>(&text).unwrap(), g);
422        let bad = ron::from_str::<Grid<u32>>("(width:3,height:2,cells:[0,1])");
423        assert!(bad.unwrap_err().to_string().contains("not width × height"));
424        assert_eq!(ron::from_str::<Pos>("(x:-1,y:4)").unwrap(), Pos::new(-1, 4));
425    }
426
427    #[test]
428    fn tile_set_basics() {
429        let mut set = TileSet::new(10, 10);
430        assert_eq!((set.width(), set.height()), (10, 10));
431        assert!(set.is_empty(), "{set:?}");
432        assert_eq!(set.len(), 0);
433        assert!(set.insert(p(4, 6)));
434        assert!(!set.insert(p(4, 6)));
435        assert!(set.insert(p(3, 6)));
436        assert!(set.insert(p(9, 9)));
437        assert!(set.insert(p(0, 0)));
438        for outside in [p(-1, 0), p(10, 0), p(0, 10), p(0, -1), p(10, 10)] {
439            assert!(!set.insert(outside));
440            assert!(!set.contains(outside));
441        }
442        assert!(!set.is_empty(), "{set:?}");
443        assert_eq!(set.len(), 4);
444        assert!(set.contains(p(4, 6)));
445        assert!(set.contains(p(3, 6)));
446        assert!(!set.contains(p(5, 6)));
447        assert!(!set.contains(p(6, 4)));
448        // Row-major, across the 64-bit word boundary (index 63 | 64).
449        let members: Vec<Pos> = set.iter().collect();
450        assert_eq!(members, [p(0, 0), p(3, 6), p(4, 6), p(9, 9)]);
451    }
452
453    #[test]
454    fn tile_set_word_boundaries() {
455        // 3 × 43 = 129 tiles: three words, the last with one bit.
456        let mut set = TileSet::new(3, 43);
457        let all: Vec<Pos> = (0..43).flat_map(|y| (0..3).map(move |x| p(x, y))).collect();
458        for &pos in &all {
459            assert!(set.insert(pos));
460        }
461        assert_eq!(set.len(), 129);
462        assert_eq!(set.iter().collect::<Vec<_>>(), all);
463        assert!(!set.contains(p(0, 43)));
464        let found = TileSet::new(0, 0);
465        assert!(found.is_empty(), "{found:?}");
466        assert_eq!(TileSet::new(0, 5).iter().count(), 0);
467    }
468
469    #[test]
470    fn tile_set_union() {
471        let mut a = TileSet::new(3, 2);
472        a.insert(p(0, 0));
473        let mut b = TileSet::new(4, 4);
474        b.insert(p(2, 1));
475        b.insert(p(3, 3)); // Outside `a`'s map: dropped.
476        b.insert(p(0, 0));
477        a.union_with(&b);
478        assert_eq!(a.iter().collect::<Vec<_>>(), [p(0, 0), p(2, 1)]);
479        assert_eq!(a.len(), 2);
480    }
481
482    proptest! {
483        #[test]
484        fn neighbors_stay_in_bounds(w in 0u16..20, h in 0u16..20, x in -3i32..25, y in -3i32..25) {
485            let g = grid(w, h);
486            let pos = Pos::new(x, y);
487            for n in g.neighbors4(pos) {
488                prop_assert!(g.in_bounds(n));
489                prop_assert_eq!(Pos::manhattan(pos, n), 1);
490            }
491            if g.in_bounds(pos) {
492                let expected = Dir::ALL.iter().filter(|&&d| g.in_bounds(pos.step(d))).count();
493                prop_assert_eq!(g.neighbors4(pos).count(), expected);
494            }
495        }
496
497        #[test]
498        fn tile_set_matches_a_btree_set(
499            w in 0u16..20,
500            h in 0u16..20,
501            ops in prop::collection::vec((-2i32..22, -2i32..22), 0..60),
502        ) {
503            let mut set = TileSet::new(w, h);
504            let mut model = BTreeSet::new();
505            for (x, y) in ops {
506                let pos = p(x, y);
507                let inside = x >= 0 && y >= 0 && x < i32::from(w) && y < i32::from(h);
508                let added = inside && model.insert((y, x));
509                prop_assert_eq!(set.insert(pos), added);
510            }
511            prop_assert_eq!(set.len(), model.len());
512            prop_assert_eq!(set.is_empty(), model.is_empty());
513            let members: Vec<(i32, i32)> = set.iter().map(|q| (q.y, q.x)).collect();
514            prop_assert_eq!(members, model.into_iter().collect::<Vec<_>>());
515        }
516    }
517}