feat(core): generic bounds-safe Grid<T>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f707bd13be
commit
dbfd44a014
2 changed files with 304 additions and 0 deletions
303
reikhelm-core/src/grid.rs
Normal file
303
reikhelm-core/src/grid.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,3 +4,4 @@
|
|||
//! dependencies. Modules are added by later tasks.
|
||||
|
||||
pub mod geometry;
|
||||
pub mod grid;
|
||||
|
|
|
|||
Loading…
Reference in a new issue