Skip to main content

terrain_forge/
grid.rs

1//! Core grid and cell types for terrain generation.
2//!
3//! # Coordinate conventions
4//!
5//! Two coordinate types are used throughout:
6//!
7//! - **`i32`** — Used by [`Grid::get`], [`Grid::set`], and [`Grid::in_bounds`].
8//!   Accepts negative values safely (returns `None` / `false`), which avoids
9//!   casts and overflow checks when doing arithmetic near grid edges.
10//!
11//! - **`usize`** — Used by [`Grid::flood_fill`], [`Grid::neighbors_4`],
12//!   [`Grid::neighbors_8`], indexing (`grid[(x, y)]`), and iterators.
13//!   These APIs assume coordinates are already validated or produced by the grid
14//!   itself.
15//!
16//! In both cases `(x, y)` means `(column, row)` with `(0, 0)` at the top-left.
17
18use serde::{Deserialize, Serialize};
19use std::fmt;
20use std::ops::{Index, IndexMut};
21
22/// Trait for grid cells.
23///
24/// Implement this for custom cell types to use with [`Grid`].
25/// The default implementation is [`Tile`].
26pub trait Cell: Clone + Default {
27    /// Returns `true` if this cell is passable (walkable).
28    fn is_passable(&self) -> bool;
29    /// Marks this cell as passable. Default implementation is a no-op.
30    fn set_passable(&mut self) {}
31}
32
33/// Basic tile type for dungeon/terrain generation.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
35pub enum Tile {
36    /// Impassable wall tile.
37    #[default]
38    Wall,
39    /// Passable floor tile.
40    Floor,
41}
42
43impl Tile {
44    /// Returns `true` if this tile is a wall.
45    pub fn is_wall(&self) -> bool {
46        matches!(self, Tile::Wall)
47    }
48    /// Returns `true` if this tile is a floor.
49    pub fn is_floor(&self) -> bool {
50        matches!(self, Tile::Floor)
51    }
52}
53
54impl Cell for Tile {
55    fn is_passable(&self) -> bool {
56        self.is_floor()
57    }
58    fn set_passable(&mut self) {
59        *self = Tile::Floor;
60    }
61}
62
63/// 2D grid of cells.
64///
65/// The primary data structure for terrain generation. Stores a flat `Vec` of
66/// cells indexed by `(x, y)` coordinates. See the [module docs](self) for
67/// coordinate conventions.
68///
69/// # Examples
70///
71/// ```
72/// use terrain_forge::{Grid, Tile};
73///
74/// let mut grid = Grid::new(10, 10);
75/// grid.set(5, 5, Tile::Floor);
76/// assert_eq!(grid.count(|t| t.is_floor()), 1);
77/// ```
78#[derive(Debug, Clone)]
79pub struct Grid<C: Cell = Tile> {
80    width: usize,
81    height: usize,
82    cells: Vec<C>,
83}
84
85impl<C: Cell> Grid<C> {
86    /// Creates a new grid filled with `C::default()`.
87    #[must_use]
88    pub fn new(width: usize, height: usize) -> Self {
89        Self {
90            width,
91            height,
92            cells: vec![C::default(); width * height],
93        }
94    }
95
96    /// Grid width in cells.
97    #[must_use]
98    #[inline]
99    pub fn width(&self) -> usize {
100        self.width
101    }
102    /// Grid height in cells.
103    #[must_use]
104    #[inline]
105    pub fn height(&self) -> usize {
106        self.height
107    }
108
109    /// Returns `true` if `(x, y)` is within bounds. Safely handles negative values.
110    #[must_use]
111    #[inline]
112    pub fn in_bounds(&self, x: i32, y: i32) -> bool {
113        x >= 0 && y >= 0 && (x as usize) < self.width && (y as usize) < self.height
114    }
115
116    /// Returns a reference to the cell at `(x, y)`, or `None` if out of bounds.
117    #[must_use]
118    #[inline]
119    pub fn get(&self, x: i32, y: i32) -> Option<&C> {
120        if self.in_bounds(x, y) {
121            Some(&self.cells[y as usize * self.width + x as usize])
122        } else {
123            None
124        }
125    }
126
127    /// Returns a mutable reference to the cell at `(x, y)`, or `None` if out of bounds.
128    #[inline]
129    pub fn get_mut(&mut self, x: i32, y: i32) -> Option<&mut C> {
130        if self.in_bounds(x, y) {
131            Some(&mut self.cells[y as usize * self.width + x as usize])
132        } else {
133            None
134        }
135    }
136
137    /// Sets the cell at `(x, y)`. Returns `true` if in bounds.
138    #[inline]
139    pub fn set(&mut self, x: i32, y: i32, cell: C) -> bool {
140        if self.in_bounds(x, y) {
141            self.cells[y as usize * self.width + x as usize] = cell;
142            true
143        } else {
144            false
145        }
146    }
147
148    /// Fills the entire grid with the given cell value.
149    pub fn fill(&mut self, cell: C) {
150        self.cells.fill(cell);
151    }
152
153    /// Fills a rectangular region with the given cell value.
154    pub fn fill_rect(&mut self, x: i32, y: i32, w: usize, h: usize, cell: C) {
155        for dy in 0..h {
156            for dx in 0..w {
157                self.set(x + dx as i32, y + dy as i32, cell.clone());
158            }
159        }
160    }
161
162    /// Counts cells matching the predicate.
163    #[must_use]
164    pub fn count<F: Fn(&C) -> bool>(&self, predicate: F) -> usize {
165        self.cells.iter().filter(|c| predicate(c)).count()
166    }
167
168    /// Iterates over all cells as `(x, y, &cell)`.
169    pub fn iter(&self) -> impl Iterator<Item = (usize, usize, &C)> {
170        self.cells
171            .iter()
172            .enumerate()
173            .map(move |(i, c)| (i % self.width, i / self.width, c))
174    }
175
176    /// BFS from `(sx, sy)`, returns all connected passable cells.
177    pub fn flood_fill(&self, sx: usize, sy: usize) -> Vec<(usize, usize)> {
178        let (w, h) = (self.width, self.height);
179        if sx >= w || sy >= h || !self[(sx, sy)].is_passable() {
180            return Vec::new();
181        }
182        let mut visited = vec![false; w * h];
183        let mut stack = vec![(sx, sy)];
184        let mut cells = Vec::new();
185        while let Some((x, y)) = stack.pop() {
186            let idx = y * w + x;
187            if visited[idx] {
188                continue;
189            }
190            visited[idx] = true;
191            cells.push((x, y));
192            if x > 0 && self[(x - 1, y)].is_passable() {
193                stack.push((x - 1, y));
194            }
195            if x + 1 < w && self[(x + 1, y)].is_passable() {
196                stack.push((x + 1, y));
197            }
198            if y > 0 && self[(x, y - 1)].is_passable() {
199                stack.push((x, y - 1));
200            }
201            if y + 1 < h && self[(x, y + 1)].is_passable() {
202                stack.push((x, y + 1));
203            }
204        }
205        cells
206    }
207
208    /// Returns all connected passable regions.
209    pub fn flood_regions(&self) -> Vec<Vec<(usize, usize)>> {
210        let (w, h) = (self.width, self.height);
211        let mut visited = vec![false; w * h];
212        let mut regions = Vec::new();
213        for y in 0..h {
214            for x in 0..w {
215                let idx = y * w + x;
216                if !visited[idx] && self[(x, y)].is_passable() {
217                    let mut stack = vec![(x, y)];
218                    let mut region = Vec::new();
219                    while let Some((cx, cy)) = stack.pop() {
220                        let ci = cy * w + cx;
221                        if visited[ci] {
222                            continue;
223                        }
224                        visited[ci] = true;
225                        region.push((cx, cy));
226                        if cx > 0 && self[(cx - 1, cy)].is_passable() {
227                            stack.push((cx - 1, cy));
228                        }
229                        if cx + 1 < w && self[(cx + 1, cy)].is_passable() {
230                            stack.push((cx + 1, cy));
231                        }
232                        if cy > 0 && self[(cx, cy - 1)].is_passable() {
233                            stack.push((cx, cy - 1));
234                        }
235                        if cy + 1 < h && self[(cx, cy + 1)].is_passable() {
236                            stack.push((cx, cy + 1));
237                        }
238                    }
239                    regions.push(region);
240                }
241            }
242        }
243        regions
244    }
245
246    /// 4-directional neighbors within bounds.
247    pub fn neighbors_4(&self, x: usize, y: usize) -> impl Iterator<Item = (usize, usize)> {
248        let (w, h) = (self.width, self.height);
249        let mut n = Vec::with_capacity(4);
250        if x > 0 {
251            n.push((x - 1, y));
252        }
253        if x + 1 < w {
254            n.push((x + 1, y));
255        }
256        if y > 0 {
257            n.push((x, y - 1));
258        }
259        if y + 1 < h {
260            n.push((x, y + 1));
261        }
262        n.into_iter()
263    }
264
265    /// 8-directional neighbors within bounds.
266    pub fn neighbors_8(&self, x: usize, y: usize) -> impl Iterator<Item = (usize, usize)> {
267        let (w, h) = (self.width, self.height);
268        let mut n = Vec::with_capacity(8);
269        for dy in -1i32..=1 {
270            for dx in -1i32..=1 {
271                if dx == 0 && dy == 0 {
272                    continue;
273                }
274                let nx = x as i32 + dx;
275                let ny = y as i32 + dy;
276                if nx >= 0 && ny >= 0 && (nx as usize) < w && (ny as usize) < h {
277                    n.push((nx as usize, ny as usize));
278                }
279            }
280        }
281        n.into_iter()
282    }
283}
284
285impl<C: Cell> Index<(usize, usize)> for Grid<C> {
286    type Output = C;
287    #[inline]
288    fn index(&self, (x, y): (usize, usize)) -> &Self::Output {
289        &self.cells[y * self.width + x]
290    }
291}
292
293impl<C: Cell> IndexMut<(usize, usize)> for Grid<C> {
294    #[inline]
295    fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Self::Output {
296        &mut self.cells[y * self.width + x]
297    }
298}
299
300impl<C: Cell + PartialEq> PartialEq for Grid<C> {
301    fn eq(&self, other: &Self) -> bool {
302        self.width == other.width && self.height == other.height && self.cells == other.cells
303    }
304}
305
306impl<C: Cell + Eq> Eq for Grid<C> {}
307
308impl fmt::Display for Tile {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        match self {
311            Tile::Wall => write!(f, "#"),
312            Tile::Floor => write!(f, "."),
313        }
314    }
315}
316
317impl fmt::Display for Grid<Tile> {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        for y in 0..self.height {
320            for x in 0..self.width {
321                write!(f, "{}", self[(x, y)])?;
322            }
323            if y + 1 < self.height {
324                writeln!(f)?;
325            }
326        }
327        Ok(())
328    }
329}
330
331/// Bresenham-style line from `start` to `end` (inclusive).
332pub fn line_points(start: (usize, usize), end: (usize, usize)) -> Vec<(usize, usize)> {
333    let (mut x, mut y) = (start.0 as i32, start.1 as i32);
334    let (tx, ty) = (end.0 as i32, end.1 as i32);
335    let mut points = Vec::new();
336    while x != tx || y != ty {
337        if x >= 0 && y >= 0 {
338            points.push((x as usize, y as usize));
339        }
340        if (x - tx).abs() > (y - ty).abs() {
341            x += if tx > x { 1 } else { -1 };
342        } else {
343            y += if ty > y { 1 } else { -1 };
344        }
345    }
346    if tx >= 0 && ty >= 0 {
347        points.push((tx as usize, ty as usize));
348    }
349    points
350}