Skip to main content

pinch_points/sim/board/
geometry.rs

1//! The board's geometry: walls, passability, wrapping, and the arithmetic
2//! that turns a tile index into coordinates and back. Shared by every
3//! creature pass.
4//!
5//! [`Grid`] holds the beach and answers what needs only the beach: sizes,
6//! indices, which edges carry a wall. The rest stays on [`Board`], because
7//! passability and neighbours also depend on the wrap switch, and a
8//! turnstile crossing mutates a tile.
9
10use super::*;
11
12/// The beach: its size, its walls, and what stands on each tile. Set by
13/// level authoring, mutated in play only by turnstile flips and castle
14/// swaps.
15#[derive(Clone, Debug)]
16pub struct Grid {
17    pub(super) width: u8,
18    pub(super) height: u8,
19    /// Horizontal wall segments, `(height + 1)` rows × `width` columns.
20    /// `h_walls[y * width + x]` is the edge *above* tile `(x, y)`.
21    pub(super) h_walls: Vec<bool>,
22    /// Vertical wall segments, `height` rows × `(width + 1)` columns.
23    /// `v_walls[y * (width + 1) + x]` is the edge *left of* tile `(x, y)`.
24    pub(super) v_walls: Vec<bool>,
25    pub(super) tiles: Vec<TileKind>,
26}
27
28/// Which creature is asking to move. Only one tile tells them apart (a
29/// walking gull cannot enter kelp and a crab slips through) but that one
30/// rule reaches every wall resolution, and `true` at a call site says
31/// nothing about which way round it goes.
32#[derive(Clone, Copy, PartialEq, Eq, Debug)]
33pub(crate) enum Walker {
34    Crab,
35    Gull,
36}
37
38impl Grid {
39    pub fn width(&self) -> u8 {
40        self.width
41    }
42
43    pub fn height(&self) -> u8 {
44        self.height
45    }
46
47    /// The slot in the wall bitmaps for the edge leaving `(x, y)` toward
48    /// `dir`: `true` picks `h_walls`. The one statement of the edge
49    /// arithmetic, so a read and a write can never disagree about which
50    /// edge a wall stands on.
51    fn wall_edge(&self, x: usize, y: usize, dir: Direction) -> (bool, usize) {
52        let w = self.width as usize;
53        match dir {
54            Direction::Up => (true, y * w + x),
55            Direction::Down => (true, (y + 1) * w + x),
56            Direction::Left => (false, y * (w + 1) + x),
57            Direction::Right => (false, y * (w + 1) + x + 1),
58        }
59    }
60
61    /// Whether a wall stands on the edge leaving `(x, y)` toward `dir`.
62    pub(super) fn edge_blocked(&self, x: usize, y: usize, dir: Direction) -> bool {
63        let (horizontal, i) = self.wall_edge(x, y, dir);
64        if horizontal {
65            self.h_walls[i]
66        } else {
67            self.v_walls[i]
68        }
69    }
70
71    /// Put a wall on that edge, or take it away.
72    pub(super) fn set_edge(&mut self, x: usize, y: usize, dir: Direction, present: bool) {
73        let (horizontal, i) = self.wall_edge(x, y, dir);
74        if horizontal {
75            self.h_walls[i] = present;
76        } else {
77            self.v_walls[i] = present;
78        }
79    }
80
81    /// Coordinates folded back onto the beach, for a wrapping arena.
82    pub(super) fn wrap_coords(&self, x: i32, y: i32) -> (i32, i32) {
83        let w = i32::from(self.width);
84        let h = i32::from(self.height);
85        ((x % w + w) % w, (y % h + h) % h)
86    }
87
88    /// Tile index to `(x, y)`, in the `i32` the movement arithmetic speaks.
89    /// This is the only copy of the arithmetic: the bot, the solver, and
90    /// the renderers all ask rather than re-derive.
91    pub fn coords(&self, tile: u16) -> (i32, i32) {
92        (
93            i32::from(tile % u16::from(self.width)),
94            i32::from(tile / u16::from(self.width)),
95        )
96    }
97
98    /// `(x, y)` back to the tile index, the inverse of [`Board::coords_u8`].
99    pub fn index_of(&self, x: u8, y: u8) -> u16 {
100        self.index(i32::from(x), i32::from(y))
101    }
102
103    pub(super) fn index(&self, x: i32, y: i32) -> u16 {
104        debug_assert!(self.in_bounds(x, y));
105        (y * i32::from(self.width) + x) as u16
106    }
107
108    pub(super) fn in_bounds(&self, x: i32, y: i32) -> bool {
109        x >= 0 && y >= 0 && x < i32::from(self.width) && y < i32::from(self.height)
110    }
111}
112
113impl Board {
114    /// Wall resolution for any creature: forward, else preferred side by
115    /// handedness, else other side, else reverse.
116    pub(super) fn resolve_walls_for(
117        &self,
118        tile: u16,
119        dir: &mut Direction,
120        handed: Handedness,
121        who: Walker,
122    ) {
123        let d = *dir;
124        if self.passable_for(tile, d, who) {
125            return;
126        }
127        let (first, second) = match handed {
128            Handedness::Left => (d.left(), d.right()),
129            Handedness::Right => (d.right(), d.left()),
130        };
131        if self.passable_for(tile, first, who) {
132            *dir = first;
133        } else if self.passable_for(tile, second, who) {
134            *dir = second;
135        } else {
136            *dir = d.reverse();
137        }
138    }
139
140    /// Spec §4.1 step 3: forward, else preferred side (the crab's big claw),
141    /// else other side, else reverse.
142    pub(super) fn resolve_walls(&self, crab: &mut Crab) {
143        self.resolve_walls_for(crab.tile, &mut crab.dir, crab.handed, Walker::Crab);
144    }
145
146    /// A turnstile physically deflects whoever crosses it, alternating
147    /// sides; it overrides lures and signposts. `true` if the tile was one,
148    /// in which case the walker's exit is already wall-resolved and its
149    /// arrival is settled. One body for crabs and gulls, so the deflection rule cannot
150    /// drift between them.
151    pub(super) fn turnstile_deflect(
152        &mut self,
153        tile: u16,
154        dir: &mut Direction,
155        handed: Handedness,
156        who: Walker,
157    ) -> bool {
158        let t = tile as usize;
159        let TileKind::Turnstile { next_right } = self.grid.tiles[t] else {
160            return false;
161        };
162        *dir = if next_right { dir.right() } else { dir.left() };
163        self.grid.tiles[t] = TileKind::Turnstile {
164            next_right: !next_right,
165        };
166        self.resolve_walls_for(tile, dir, handed, who);
167        true
168    }
169
170    /// Can a creature step from `tile` in `dir`: no wall on that edge, the
171    /// neighbour exists, and the neighbour is not a rock.
172    pub(super) fn passable(&self, tile: u16, dir: Direction) -> bool {
173        self.passable_for(tile, dir, Walker::Crab)
174    }
175
176    pub(super) fn edge_blocked(&self, x: usize, y: usize, dir: Direction) -> bool {
177        self.grid.edge_blocked(x, y, dir)
178    }
179
180    pub(super) fn set_edge(&mut self, x: usize, y: usize, dir: Direction, present: bool) {
181        self.grid.set_edge(x, y, dir, present);
182    }
183
184    /// Whether a creature at `tile` may exit toward `dir`. Kelp lets crabs
185    /// slip through but blocks walking gulls.
186    pub(super) fn passable_for(&self, tile: u16, dir: Direction, who: Walker) -> bool {
187        let (x, y) = self.coords(tile);
188        if self.edge_blocked(x as usize, y as usize, dir) {
189            return false;
190        }
191        let (dx, dy) = dir.offset();
192        let (nx, ny) = (x + dx, y + dy);
193        let dest = if !self.in_bounds(nx, ny) {
194            if !self.wrap {
195                return false;
196            }
197            let (wx, wy) = self.wrap_coords(nx, ny);
198            self.grid.tiles[self.index(wx, wy) as usize]
199        } else {
200            self.grid.tiles[self.index(nx, ny) as usize]
201        };
202        match dest {
203            TileKind::Rock => false,
204            TileKind::Kelp => who == Walker::Crab,
205            TileKind::Empty
206            | TileKind::Castle(_)
207            | TileKind::Spawner(_)
208            | TileKind::Turnstile { .. }
209            | TileKind::Pool => true,
210        }
211    }
212
213    /// The open spots, in bounds and on empty sand, along a ring of offsets
214    /// around `(cx, cy)`, in ring order, each with the offset it sits at.
215    /// The shared half of every castle-ring walk.
216    pub(super) fn ring_openings(
217        &self,
218        cx: i32,
219        cy: i32,
220        ring: &[(i32, i32)],
221    ) -> Vec<(i32, i32, i32, i32)> {
222        ring.iter()
223            .map(|&(ox, oy)| (cx + ox, cy + oy, ox, oy))
224            .filter(|&(nx, ny, _, _)| {
225                self.in_bounds(nx, ny)
226                    && self.grid.tiles[self.index(nx, ny) as usize] == TileKind::Empty
227            })
228            .collect()
229    }
230
231    pub(super) fn wrap_coords(&self, x: i32, y: i32) -> (i32, i32) {
232        self.grid.wrap_coords(x, y)
233    }
234
235    pub(super) fn neighbor(&self, tile: u16, dir: Direction) -> u16 {
236        let (x, y) = self.coords(tile);
237        let (dx, dy) = dir.offset();
238        if self.wrap {
239            let (wx, wy) = self.wrap_coords(x + dx, y + dy);
240            return self.index(wx, wy);
241        }
242        self.index(x + dx, y + dy)
243    }
244
245    /// The tile one step away in `dir`, if there is one: across the seam on
246    /// a wrapping arena, `None` off the edge otherwise. The public face of
247    /// [`Board::neighbor`] for callers outside the board: the bot plans on
248    /// the same beach the crabs walk, seam included.
249    pub fn step(&self, tile: u16, dir: Direction) -> Option<u16> {
250        let (x, y) = self.coords(tile);
251        let (dx, dy) = dir.offset();
252        if self.wrap {
253            let (wx, wy) = self.wrap_coords(x + dx, y + dy);
254            return Some(self.index(wx, wy));
255        }
256        self.in_bounds(x + dx, y + dy)
257            .then(|| self.index(x + dx, y + dy))
258    }
259
260    /// Tile index to `(x, y)`, in the `i32` the movement arithmetic speaks;
261    /// [`Board::coords_u8`] is the byte-sized form. Forwards to the grid,
262    /// which owns the arithmetic.
263    pub fn coords(&self, tile: u16) -> (i32, i32) {
264        self.grid.coords(tile)
265    }
266
267    /// `(x, y)` back to the tile index, the inverse of [`Board::coords_u8`].
268    pub fn index_of(&self, x: u8, y: u8) -> u16 {
269        self.grid.index_of(x, y)
270    }
271
272    pub(super) fn index(&self, x: i32, y: i32) -> u16 {
273        self.grid.index(x, y)
274    }
275
276    pub(super) fn in_bounds(&self, x: i32, y: i32) -> bool {
277        self.grid.in_bounds(x, y)
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    /// A tile index and a pair of coordinates are the same thing said two
286    /// ways, and everything the sim does walks between them: a crab's
287    /// position, a signpost's tile, the wire's board, the render layer's
288    /// every sprite. An off-by-one in either direction would put the whole
289    /// board a tile out of step with itself.
290    #[test]
291    fn an_index_and_a_pair_of_coordinates_are_the_same_place() {
292        for (w, h) in [(1u8, 1u8), (2, 3), (9, 7), (21, 13)] {
293            let board = Board::new(w, h, 1);
294            for y in 0..h {
295                for x in 0..w {
296                    let tile = board.index_of(x, y);
297                    assert_eq!(
298                        board.coords(tile),
299                        (i32::from(x), i32::from(y)),
300                        "{w}x{h}: ({x},{y}) did not survive the trip through {tile}"
301                    );
302                    assert!(
303                        (tile as usize) < usize::from(w) * usize::from(h),
304                        "{w}x{h}: ({x},{y}) indexed off the end at {tile}"
305                    );
306                }
307            }
308        }
309    }
310}