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
5use super::*;
6
7/// Which creature is asking to move. Only one tile tells them apart (a
8/// walking gull cannot enter kelp and a crab slips through) but that one
9/// rule reaches every wall resolution, and `true` at a call site says
10/// nothing about which way round it goes.
11#[derive(Clone, Copy, PartialEq, Eq, Debug)]
12pub(crate) enum Walker {
13    Crab,
14    Gull,
15}
16
17impl Board {
18    /// Wall resolution for any creature: forward, else preferred side by
19    /// handedness, else other side, else reverse.
20    pub(super) fn resolve_walls_for(
21        &self,
22        tile: u16,
23        dir: &mut Direction,
24        handed: Handedness,
25        who: Walker,
26    ) {
27        let d = *dir;
28        if self.passable_for(tile, d, who) {
29            return;
30        }
31        let (first, second) = match handed {
32            Handedness::Left => (d.left(), d.right()),
33            Handedness::Right => (d.right(), d.left()),
34        };
35        if self.passable_for(tile, first, who) {
36            *dir = first;
37        } else if self.passable_for(tile, second, who) {
38            *dir = second;
39        } else {
40            *dir = d.reverse();
41        }
42    }
43
44    /// Spec ยง4.1 step 3: forward, else preferred side (the crab's big claw),
45    /// else other side, else reverse.
46    pub(super) fn resolve_walls(&self, crab: &mut Crab) {
47        self.resolve_walls_for(crab.tile, &mut crab.dir, crab.handed, Walker::Crab);
48    }
49
50    /// A turnstile physically deflects whoever crosses it, alternating
51    /// sides; it overrides lures and signposts. `true` if the tile was one,
52    /// in which case the walker's exit is already wall-resolved and its
53    /// arrival is settled. One body for crabs and gulls, so the deflection rule cannot
54    /// drift between them.
55    pub(super) fn turnstile_deflect(
56        &mut self,
57        tile: u16,
58        dir: &mut Direction,
59        handed: Handedness,
60        who: Walker,
61    ) -> bool {
62        let t = tile as usize;
63        let TileKind::Turnstile { next_right } = self.tiles[t] else {
64            return false;
65        };
66        *dir = if next_right { dir.right() } else { dir.left() };
67        self.tiles[t] = TileKind::Turnstile {
68            next_right: !next_right,
69        };
70        self.resolve_walls_for(tile, dir, handed, who);
71        true
72    }
73
74    /// Can a creature step from `tile` in `dir`: no wall on that edge, the
75    /// neighbour exists, and the neighbour is not a rock.
76    pub(super) fn passable(&self, tile: u16, dir: Direction) -> bool {
77        self.passable_for(tile, dir, Walker::Crab)
78    }
79
80    /// The slot in the wall bitmaps for the edge leaving `(x, y)` toward
81    /// `dir`: `true` picks `h_walls`. The one statement of the edge
82    /// arithmetic, so a read and a write can never disagree about which
83    /// edge a wall stands on.
84    fn wall_edge(&self, x: usize, y: usize, dir: Direction) -> (bool, usize) {
85        let w = self.width as usize;
86        match dir {
87            Direction::Up => (true, y * w + x),
88            Direction::Down => (true, (y + 1) * w + x),
89            Direction::Left => (false, y * (w + 1) + x),
90            Direction::Right => (false, y * (w + 1) + x + 1),
91        }
92    }
93
94    /// Whether a wall stands on the edge leaving `(x, y)` toward `dir`.
95    pub(super) fn edge_blocked(&self, x: usize, y: usize, dir: Direction) -> bool {
96        let (horizontal, i) = self.wall_edge(x, y, dir);
97        if horizontal {
98            self.h_walls[i]
99        } else {
100            self.v_walls[i]
101        }
102    }
103
104    /// Put a wall on that edge, or take it away.
105    pub(super) fn set_edge(&mut self, x: usize, y: usize, dir: Direction, present: bool) {
106        let (horizontal, i) = self.wall_edge(x, y, dir);
107        if horizontal {
108            self.h_walls[i] = present;
109        } else {
110            self.v_walls[i] = present;
111        }
112    }
113
114    /// Whether a creature at `tile` may exit toward `dir`. Kelp lets crabs
115    /// slip through but blocks walking gulls.
116    pub(super) fn passable_for(&self, tile: u16, dir: Direction, who: Walker) -> bool {
117        let (x, y) = self.coords(tile);
118        if self.edge_blocked(x as usize, y as usize, dir) {
119            return false;
120        }
121        let (dx, dy) = dir.offset();
122        let (nx, ny) = (x + dx, y + dy);
123        let dest = if !self.in_bounds(nx, ny) {
124            if !self.wrap {
125                return false;
126            }
127            let (wx, wy) = self.wrap_coords(nx, ny);
128            self.tiles[self.index(wx, wy) as usize]
129        } else {
130            self.tiles[self.index(nx, ny) as usize]
131        };
132        match dest {
133            TileKind::Rock => false,
134            TileKind::Kelp => who == Walker::Crab,
135            TileKind::Empty
136            | TileKind::Castle(_)
137            | TileKind::Spawner(_)
138            | TileKind::Turnstile { .. }
139            | TileKind::Pool => true,
140        }
141    }
142
143    /// The open spots, in bounds and on empty sand, along a ring of offsets
144    /// around `(cx, cy)`, in ring order, each with the offset it sits at.
145    /// The shared half of every castle-ring walk.
146    pub(super) fn ring_openings(
147        &self,
148        cx: i32,
149        cy: i32,
150        ring: &[(i32, i32)],
151    ) -> Vec<(i32, i32, i32, i32)> {
152        ring.iter()
153            .map(|&(ox, oy)| (cx + ox, cy + oy, ox, oy))
154            .filter(|&(nx, ny, _, _)| {
155                self.in_bounds(nx, ny) && self.tiles[self.index(nx, ny) as usize] == TileKind::Empty
156            })
157            .collect()
158    }
159
160    pub(super) fn wrap_coords(&self, x: i32, y: i32) -> (i32, i32) {
161        let w = i32::from(self.width);
162        let h = i32::from(self.height);
163        ((x % w + w) % w, (y % h + h) % h)
164    }
165
166    pub(super) fn neighbor(&self, tile: u16, dir: Direction) -> u16 {
167        let (x, y) = self.coords(tile);
168        let (dx, dy) = dir.offset();
169        if self.wrap {
170            let (wx, wy) = self.wrap_coords(x + dx, y + dy);
171            return self.index(wx, wy);
172        }
173        self.index(x + dx, y + dy)
174    }
175
176    /// The tile one step away in `dir`, if there is one: across the seam on
177    /// a wrapping arena, `None` off the edge otherwise. The public face of
178    /// [`Board::neighbor`] for callers outside the board: the bot plans on
179    /// the same beach the crabs walk, seam included.
180    pub fn step(&self, tile: u16, dir: Direction) -> Option<u16> {
181        let (x, y) = self.coords(tile);
182        let (dx, dy) = dir.offset();
183        if self.wrap {
184            let (wx, wy) = self.wrap_coords(x + dx, y + dy);
185            return Some(self.index(wx, wy));
186        }
187        self.in_bounds(x + dx, y + dy)
188            .then(|| self.index(x + dx, y + dy))
189    }
190
191    /// Tile index to `(x, y)`, in the `i32` the movement arithmetic speaks;
192    /// [`Board::coords_u8`] is the byte-sized form. The board's arithmetic
193    /// is the only copy: the bot, the solver, and the renderers all ask
194    /// rather than re-derive.
195    pub fn coords(&self, tile: u16) -> (i32, i32) {
196        (
197            i32::from(tile % u16::from(self.width)),
198            i32::from(tile / u16::from(self.width)),
199        )
200    }
201
202    /// `(x, y)` back to the tile index, the inverse of [`Board::coords_u8`].
203    pub fn index_of(&self, x: u8, y: u8) -> u16 {
204        self.index(i32::from(x), i32::from(y))
205    }
206
207    pub(super) fn index(&self, x: i32, y: i32) -> u16 {
208        debug_assert!(self.in_bounds(x, y));
209        (y * i32::from(self.width) + x) as u16
210    }
211
212    pub(super) fn in_bounds(&self, x: i32, y: i32) -> bool {
213        x >= 0 && y >= 0 && x < i32::from(self.width) && y < i32::from(self.height)
214    }
215}