feat(core): generic bounds-safe Grid<T>

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Parley Hatch 2026-05-28 21:04:44 -06:00
parent f707bd13be
commit dbfd44a014
2 changed files with 304 additions and 0 deletions

303
reikhelm-core/src/grid.rs Normal file
View file

@ -0,0 +1,303 @@
//! A generic, bounds-safe 2D grid (spec §4.2).
//!
//! [`Grid<T>`] is the workhorse storage type for the generator. It is the map
//! itself (`Grid<Tile>`) and also the scratch buffers used during generation
//! (`Grid<u32>` region-id maps, `Grid<f32>` distance fields / noise). Cells are
//! stored in a flat [`Vec<T>`] in row-major order (row 0 first, then row 1, …),
//! so a point `(x, y)` lives at index `y * width + x`.
//!
//! Every access is bounds-checked and total: out-of-bounds reads return
//! [`None`], out-of-bounds writes are silently ignored, and neighbor queries
//! yield only in-bounds points. No method panics on valid input (spec §8).
use serde::{Deserialize, Serialize};
use crate::geometry::{Point, Rect};
/// A row-major 2D grid of `T`, addressed by integer [`Point`]s.
///
/// The grid covers the half-open cell range `0..width` × `0..height`. Because
/// [`Point`] coordinates are signed, callers can compute candidate points that
/// fall outside the grid; the grid's accessors clip such points rather than
/// panicking.
///
/// `Serialize`/`Deserialize` are available whenever `T` itself supports them,
/// so a `Grid<Tile>` can round-trip through serde while a `Grid<SomeNonSerde>`
/// simply does not gain those impls.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Grid<T> {
width: u32,
height: u32,
cells: Vec<T>,
}
impl<T> Grid<T> {
/// Creates a `width` × `height` grid with every cell initialized to `fill`.
///
/// `fill` is cloned into each cell, so `T` must be [`Clone`]. A zero extent
/// in either dimension yields an empty grid (no cells).
pub fn new(width: u32, height: u32, fill: T) -> Self
where
T: Clone,
{
let len = width as usize * height as usize;
Grid {
width,
height,
cells: vec![fill; len],
}
}
/// Creates a `width` × `height` grid, computing each cell from its point.
///
/// `f` is called once per cell in row-major order with that cell's
/// coordinate. This is the natural constructor for derived grids (e.g. a
/// distance field or a checkerboard) where each value depends on position.
pub fn from_fn(width: u32, height: u32, f: impl Fn(Point) -> T) -> Self {
let mut cells = Vec::with_capacity(width as usize * height as usize);
for y in 0..height as i32 {
for x in 0..width as i32 {
cells.push(f(Point::new(x, y)));
}
}
Grid {
width,
height,
cells,
}
}
/// Returns the grid width in cells.
pub fn width(&self) -> u32 {
self.width
}
/// Returns the grid height in cells.
pub fn height(&self) -> u32 {
self.height
}
/// Returns `true` if `p` lies within the grid.
///
/// A point is in bounds when `0 <= x < width` and `0 <= y < height`.
pub fn in_bounds(&self, p: Point) -> bool {
p.x >= 0 && (p.x as u32) < self.width && p.y >= 0 && (p.y as u32) < self.height
}
/// Maps an in-bounds point to its flat `cells` index, or [`None`] if OOB.
///
/// Internal helper: every public accessor routes through this so the
/// row-major layout (`y * width + x`) lives in exactly one place.
fn index(&self, p: Point) -> Option<usize> {
if self.in_bounds(p) {
Some(p.y as usize * self.width as usize + p.x as usize)
} else {
None
}
}
/// Returns a shared reference to the cell at `p`, or [`None`] if OOB.
pub fn get(&self, p: Point) -> Option<&T> {
self.index(p).map(|i| &self.cells[i])
}
/// Returns a mutable reference to the cell at `p`, or [`None`] if OOB.
pub fn get_mut(&mut self, p: Point) -> Option<&mut T> {
match self.index(p) {
Some(i) => Some(&mut self.cells[i]),
None => None,
}
}
/// Writes `value` into the cell at `p`.
///
/// Out-of-bounds writes are a no-op (never a panic), so passes can carve
/// freely without first re-checking bounds (spec §8).
pub fn set(&mut self, p: Point, value: T) {
if let Some(i) = self.index(p) {
self.cells[i] = value;
}
}
/// Returns the in-bounds orthogonal neighbors (N, S, W, E) of `p`.
///
/// Out-of-bounds neighbors are filtered out, so a corner cell yields two
/// neighbors and an edge cell three. Order follows [`Point::OFFSETS4`].
pub fn neighbors4(&self, p: Point) -> impl Iterator<Item = Point> + '_ {
p.neighbors4().into_iter().filter(|&n| self.in_bounds(n))
}
/// Returns the in-bounds 8-connected neighbors of `p`.
///
/// Out-of-bounds neighbors are filtered out, so a corner cell yields three
/// neighbors. Order follows [`Point::OFFSETS8`] (orthogonal then diagonal).
pub fn neighbors8(&self, p: Point) -> impl Iterator<Item = Point> + '_ {
p.neighbors8().into_iter().filter(|&n| self.in_bounds(n))
}
/// Iterates over every cell as `(point, &value)` in row-major order.
///
/// Visits exactly `width * height` cells, each once.
pub fn iter(&self) -> impl Iterator<Item = (Point, &T)> + '_ {
let width = self.width;
self.cells.iter().enumerate().map(move |(i, value)| {
let x = (i % width as usize) as i32;
let y = (i / width as usize) as i32;
(Point::new(x, y), value)
})
}
/// Iterates over the cells inside `rect` as `(point, &value)`.
///
/// The rectangle is clipped to the grid, so points outside the grid are
/// never yielded even when `rect` extends past an edge. Iteration is
/// row-major within the (clipped) rectangle. An empty or fully-outside
/// rectangle yields nothing.
pub fn iter_rect(&self, rect: Rect) -> impl Iterator<Item = (Point, &T)> + '_ {
// Build the point range from `rect`'s (copied) fields rather than
// `rect.iter()`, whose returned iterator borrows the local `rect` and
// would dangle once this function returns.
let (left, right) = (rect.x, rect.right());
let (top, bottom) = (rect.y, rect.bottom());
(top..bottom)
.flat_map(move |y| (left..right).map(move |x| Point::new(x, y)))
.filter_map(move |p| self.get(p).map(|value| (p, value)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_set_round_trip_in_bounds() {
let mut g = Grid::new(4, 3, 0u32);
let p = Point::new(2, 1);
assert_eq!(g.get(p), Some(&0));
g.set(p, 42);
assert_eq!(g.get(p), Some(&42));
// get_mut mutates in place.
*g.get_mut(p).unwrap() = 7;
assert_eq!(g.get(p), Some(&7));
}
#[test]
fn set_out_of_bounds_is_a_no_op_and_get_returns_none() {
let mut g = Grid::new(4, 3, 0u32);
// Each side, including negative coords (signed points can go OOB).
let oob = [
Point::new(-1, 0),
Point::new(0, -1),
Point::new(4, 0), // x == width (exclusive)
Point::new(0, 3), // y == height (exclusive)
];
for &p in &oob {
assert!(!g.in_bounds(p));
assert_eq!(g.get(p), None);
assert!(g.get_mut(p).is_none());
g.set(p, 999); // must not panic and must not touch any cell
}
// No in-bounds cell was disturbed by the OOB writes.
assert!(g.iter().all(|(_, &v)| v == 0));
}
#[test]
fn neighbors_at_corner_are_in_bounds_only() {
let g = Grid::new(5, 5, 0u8);
let corner = Point::new(0, 0);
let n4: Vec<Point> = g.neighbors4(corner).collect();
// Only (1,0) and (0,1) are in bounds at the top-left corner.
assert_eq!(n4.len(), 2);
assert!(n4.contains(&Point::new(1, 0)));
assert!(n4.contains(&Point::new(0, 1)));
let n8: Vec<Point> = g.neighbors8(corner).collect();
// A corner has exactly 3 in-bounds 8-connected neighbors.
assert_eq!(n8.len(), 3);
assert!(n8.contains(&Point::new(1, 0)));
assert!(n8.contains(&Point::new(0, 1)));
assert!(n8.contains(&Point::new(1, 1)));
// All neighbors are genuinely in bounds.
assert!(n8.iter().all(|&p| g.in_bounds(p)));
}
#[test]
fn neighbors_in_interior_are_complete() {
let g = Grid::new(5, 5, 0u8);
let center = Point::new(2, 2);
assert_eq!(g.neighbors4(center).count(), 4);
assert_eq!(g.neighbors8(center).count(), 8);
}
#[test]
fn iter_visits_every_cell_exactly_once() {
let g = Grid::new(4, 3, 0u32);
let visited: Vec<Point> = g.iter().map(|(p, _)| p).collect();
// Exactly width * height cells.
assert_eq!(visited.len(), 4 * 3);
// Each in-bounds point appears exactly once.
for y in 0..3 {
for x in 0..4 {
let p = Point::new(x, y);
assert_eq!(visited.iter().filter(|&&q| q == p).count(), 1);
}
}
}
#[test]
fn iter_is_row_major() {
let g = Grid::new(3, 2, 0u32);
let points: Vec<Point> = g.iter().map(|(p, _)| p).collect();
assert_eq!(
points,
vec![
Point::new(0, 0),
Point::new(1, 0),
Point::new(2, 0),
Point::new(0, 1),
Point::new(1, 1),
Point::new(2, 1),
]
);
}
#[test]
fn iter_rect_is_clipped_to_grid_bounds() {
let g = Grid::new(4, 4, 0u32);
// Rect spills off the bottom-right edge: x:2..6, y:2..6.
let rect = Rect::new(2, 2, 4, 4);
let points: Vec<Point> = g.iter_rect(rect).map(|(p, _)| p).collect();
// Only the in-grid portion (x:2..4, y:2..4) = 4 cells survives.
assert_eq!(points.len(), 4);
assert!(points.iter().all(|&p| g.in_bounds(p)));
assert!(points.contains(&Point::new(2, 2)));
assert!(points.contains(&Point::new(3, 3)));
// Out-of-bounds cells are never yielded.
assert!(!points.contains(&Point::new(4, 4)));
}
#[test]
fn iter_rect_fully_outside_yields_nothing() {
let g = Grid::new(4, 4, 0u32);
let rect = Rect::new(10, 10, 3, 3);
assert_eq!(g.iter_rect(rect).count(), 0);
}
#[test]
fn from_fn_matches_closure_at_sampled_points() {
// Encode the coordinate so each cell has a distinct, checkable value.
let g = Grid::from_fn(5, 4, |p| (p.y * 100 + p.x) as u32);
assert_eq!(g.width(), 5);
assert_eq!(g.height(), 4);
for &p in &[Point::new(0, 0), Point::new(4, 3), Point::new(2, 1)] {
let expected = (p.y * 100 + p.x) as u32;
assert_eq!(g.get(p), Some(&expected));
}
}
}

View file

@ -4,3 +4,4 @@
//! dependencies. Modules are added by later tasks.
pub mod geometry;
pub mod grid;