use core::fmt;
use hashbrown::HashMap;
use hashbrown::hash_map::Entry;
use crate::coord::{Coord, Dir6, Dir8, Hex, Idx, Metric, Sq};
use crate::grid::sealed::Sealed;
use crate::grid::{Grid, same_grid, slot};
use crate::layout::Offset;
use crate::tag::Tag;
use alloc::vec::Vec;
pub const MAX_CELLS: u64 = 1 << 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridError {
InvalidDimensions {
w: i32,
h: i32,
},
InvalidRadius {
radius: i32,
},
TooManyCells {
cells: u64,
},
StepNotInvertible,
MetricDisagrees {
span: u32,
},
}
impl fmt::Display for GridError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::InvalidDimensions { w, h } => write!(f, "a grid cannot be {w} x {h}"),
Self::InvalidRadius { radius } => write!(f, "radius must be >= 0, not {radius}"),
Self::TooManyCells { cells } => write!(
f,
"the requested board needs {cells} cells; a grid may hold at most {MAX_CELLS}",
),
Self::StepNotInvertible => write!(
f,
"a step cannot be undone: it is not the same offset everywhere on the board, so \
the grid cannot find who steps into a cell. Two cells that step onto one cell, \
and a portal, both do this",
),
Self::MetricDisagrees { span } => write!(
f,
"the metric disagrees with the directions: a step covers {span} units, but a \
step must cover at most one",
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Adjacency {
Four,
Eight,
}
#[derive(Debug, Clone)]
pub struct FullGrid<C: Coord> {
cells: Vec<C>,
index: HashMap<C, u32>,
dirs: Vec<C::Dir>,
metric: Metric<C>,
tag: Tag,
}
impl<C: Coord> FullGrid<C> {
#[must_use]
pub fn new(cells: impl IntoIterator<Item = C>, dirs: &[C::Dir], metric: Metric<C>) -> Self {
Self::try_new(cells, dirs, metric).unwrap_or_else(|error| panic!("{error}"))
}
pub fn try_new(
cells: impl IntoIterator<Item = C>,
dirs: &[C::Dir],
metric: Metric<C>,
) -> Result<Self, GridError> {
let mut ordered = Vec::new();
let mut index = HashMap::new();
for c in cells {
if let Entry::Vacant(slot) = index.entry(c) {
slot.insert(ordered.len() as u32);
ordered.push(c);
}
if ordered.len() as u64 > MAX_CELLS {
return Err(GridError::TooManyCells {
cells: ordered.len() as u64,
});
}
}
for &c in &ordered {
for &d in dirs {
let to = c.step(d);
if to == c || !index.contains_key(&to) {
continue;
}
let span = metric.distance(c, to);
if span > 1 {
return Err(GridError::MetricDisagrees { span });
}
if Self::source(to, d) != c {
return Err(GridError::StepNotInvertible);
}
}
}
Ok(Self {
tag: Tag::of(ordered.iter()),
cells: ordered,
index,
dirs: dirs.to_vec(),
metric,
})
}
const fn idx(&self, i: u32) -> Idx {
Idx::new(self.tag, i)
}
fn landing(&self, c: C, d: C::Dir) -> Option<Idx> {
let to = c.step(d);
if to == c {
return None;
}
self.index.get(&to).map(|&j| self.idx(j))
}
fn source(c: C, d: C::Dir) -> C {
#[allow(clippy::eq_op)]
let origin = c - c;
c - (origin.step(d) - origin)
}
#[must_use]
pub fn filtered(&self, keep: impl Fn(C) -> bool) -> Self {
Self::new(
self.cells.iter().copied().filter(|&c| keep(c)),
&self.dirs,
self.metric,
)
}
}
impl<C: Coord> Sealed for FullGrid<C> {
fn tag(&self) -> Tag {
self.tag
}
}
impl<C: Coord> Grid for FullGrid<C> {
type Cell = C;
fn len(&self) -> usize {
self.cells.len()
}
fn coord(&self, i: Idx) -> C {
self.cells[slot(self.len(), self.tag, i)]
}
fn index_of(&self, c: C) -> Option<Idx> {
self.index.get(&c).map(|&i| self.idx(i))
}
fn dirs(&self) -> &[C::Dir] {
&self.dirs
}
fn step(&self, i: Idx, d: C::Dir) -> Option<Idx> {
let c = self.cells[slot(self.len(), self.tag, i)];
if !self.dirs.contains(&d) {
return None;
}
self.landing(c, d)
}
fn neighbors(&self, i: Idx) -> impl Iterator<Item = (C::Dir, Idx)> {
let c = self.cells[slot(self.len(), self.tag, i)];
self.dirs
.iter()
.filter_map(move |&d| self.landing(c, d).map(|j| (d, j)))
}
fn in_neighbors(&self, j: Idx) -> impl Iterator<Item = (C::Dir, Idx)> {
let c = self.cells[slot(self.len(), self.tag, j)];
self.dirs.iter().filter_map(move |&d| {
let from = Self::source(c, d);
if from == c || from.step(d) != c {
return None;
}
self.index.get(&from).map(|&i| (d, self.idx(i)))
})
}
fn metric(&self) -> Metric<C> {
self.metric
}
fn root(&self) -> &Self {
self
}
fn to_root(&self, i: Idx) -> Idx {
let _ = slot(self.len(), self.tag, i);
i
}
fn of_root(&self, i: Idx) -> Option<Idx> {
same_grid(self.tag, i);
((i.raw() as usize) < self.len()).then_some(i)
}
}
impl FullGrid<Sq> {
#[must_use]
pub fn square(w: i32, h: i32, adj: Adjacency) -> Self {
Self::try_square(w, h, adj).unwrap_or_else(|error| panic!("{error}"))
}
pub fn try_square(w: i32, h: i32, adj: Adjacency) -> Result<Self, GridError> {
if w < 0 || h < 0 {
return Err(GridError::InvalidDimensions { w, h });
}
let cells_count = u64::from(w.unsigned_abs()) * u64::from(h.unsigned_abs());
if cells_count > MAX_CELLS {
return Err(GridError::TooManyCells { cells: cells_count });
}
let cells = (0..h).flat_map(move |y| (0..w).map(move |x| Sq::new(x, y)));
match adj {
Adjacency::Four => Self::try_new(cells, &Dir8::ORTHO, Metric::MANHATTAN),
Adjacency::Eight => Self::try_new(cells, &Dir8::ALL, Metric::CHEBYSHEV),
}
}
#[must_use]
pub fn disc(radius: i32, adj: Adjacency) -> Self {
Self::try_disc(radius, adj).unwrap_or_else(|error| panic!("{error}"))
}
pub fn try_disc(radius: i32, adj: Adjacency) -> Result<Self, GridError> {
if radius < 0 {
return Err(GridError::InvalidRadius { radius });
}
let side = 2 * u64::from(radius.unsigned_abs()) + 1;
let cells_count = side * side;
if cells_count > MAX_CELLS {
return Err(GridError::TooManyCells { cells: cells_count });
}
let r2 = i64::from(radius) * i64::from(radius);
let cells = (-radius..=radius).flat_map(move |y| {
let yy = i64::from(y) * i64::from(y);
(-radius..=radius)
.filter(move |&x| i64::from(x) * i64::from(x) + yy <= r2)
.map(move |x| Sq::new(x, y))
});
match adj {
Adjacency::Four => Self::try_new(cells, &Dir8::ORTHO, Metric::MANHATTAN),
Adjacency::Eight => Self::try_new(cells, &Dir8::ALL, Metric::CHEBYSHEV),
}
}
}
impl FullGrid<Hex> {
#[must_use]
pub fn hexagon(radius: i32) -> Self {
Self::try_hexagon(radius).unwrap_or_else(|error| panic!("{error}"))
}
pub fn try_hexagon(radius: i32) -> Result<Self, GridError> {
if radius < 0 {
return Err(GridError::InvalidRadius { radius });
}
let r = u64::from(radius.unsigned_abs());
let cells_count = 3 * r * (r + 1) + 1;
if cells_count > MAX_CELLS {
return Err(GridError::TooManyCells { cells: cells_count });
}
let cells = (-radius..=radius).flat_map(move |q| {
((-radius).max(-q - radius)..=radius.min(-q + radius)).map(move |r| Hex::new(q, r))
});
Self::try_new(cells, &Dir6::ALL, Metric::HEX)
}
#[must_use]
pub fn hex_rect(w: i32, h: i32, offset: Offset) -> Self {
Self::try_hex_rect(w, h, offset).unwrap_or_else(|error| panic!("{error}"))
}
pub fn try_hex_rect(w: i32, h: i32, offset: Offset) -> Result<Self, GridError> {
if w < 0 || h < 0 {
return Err(GridError::InvalidDimensions { w, h });
}
let cells_count = u64::from(w.unsigned_abs()) * u64::from(h.unsigned_abs());
if cells_count > MAX_CELLS {
return Err(GridError::TooManyCells { cells: cells_count });
}
let cells = (0..h).flat_map(move |row| (0..w).map(move |col| offset.to_hex(col, row)));
Self::try_new(cells, &Dir6::ALL, Metric::HEX)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
#[test]
fn every_error_reads_as_one_sentence() {
let errors = [
GridError::InvalidDimensions { w: -1, h: 2 },
GridError::InvalidRadius { radius: -1 },
GridError::TooManyCells { cells: 1 << 25 },
GridError::StepNotInvertible,
GridError::MetricDisagrees { span: 2 },
];
for error in errors {
let message = error.to_string();
assert!(!message.contains(" "), "{message:?}");
assert!(!message.contains('+'), "{message:?}");
}
}
#[test]
fn a_rectangle_has_width_times_height_cells() {
let g = FullGrid::square(5, 3, Adjacency::Four);
assert_eq!(g.len(), 15);
let mut cells = g.cells();
assert_eq!(
cells.next(),
Some(Sq::new(0, 0)),
"row-major: index 0 is the top-left"
);
assert_eq!(
cells.nth(3),
Some(Sq::new(4, 0)),
"then along the first row"
);
assert_eq!(cells.next(), Some(Sq::new(0, 1)), "then down to the second");
}
#[test]
fn indices_round_trip_through_coordinates() {
let g = FullGrid::square(4, 4, Adjacency::Eight);
for i in g.indices() {
assert_eq!(g.index_of(g.coord(i)), Some(i));
}
}
#[test]
fn four_way_grids_have_no_diagonal_steps_at_all() {
let g = FullGrid::square(3, 3, Adjacency::Four);
let mid = g.at(Sq::new(1, 1));
assert_eq!(g.neighbors(mid).count(), 4);
assert!(g.step(mid, Dir8::Ne).is_none());
}
#[test]
fn eight_way_grids_have_eight_neighbours_in_the_middle_and_three_in_a_corner() {
let g = FullGrid::square(3, 3, Adjacency::Eight);
assert_eq!(g.neighbors(g.at(Sq::new(1, 1))).count(), 8);
assert_eq!(g.neighbors(g.at(Sq::new(0, 0))).count(), 3);
}
#[test]
fn steps_off_the_board_are_none() {
let g = FullGrid::square(3, 3, Adjacency::Eight);
let corner = g.at(Sq::new(0, 0));
assert!(g.step(corner, Dir8::N).is_none());
assert!(g.step(corner, Dir8::W).is_none());
assert!(g.step(corner, Dir8::Se).is_some());
}
#[test]
fn a_disc_keeps_the_cell_exactly_on_the_rim_and_drops_the_corner_of_the_box() {
let counts: Vec<usize> = (0..5)
.map(|r| FullGrid::disc(r, Adjacency::Four).len())
.collect();
assert_eq!(counts, vec![1, 5, 13, 29, 49]);
let g = FullGrid::disc(5, Adjacency::Four);
assert!(
g.contains(Sq::new(3, 4)),
"3-4-5: on the rim, and the rim is in"
);
assert!(
!g.contains(Sq::new(4, 4)),
"inside the box, outside the circle"
);
}
#[test]
fn a_disc_measures_the_diagonal_the_way_its_adjacency_moves() {
for (adj, dirs, diagonal) in [(Adjacency::Four, 4, 2), (Adjacency::Eight, 8, 1)] {
let g = FullGrid::disc(3, adj);
let centre = g.at(Sq::new(0, 0));
let corner = g.at(Sq::new(1, 1));
assert_eq!(g.neighbors(centre).count(), dirs, "{adj:?}");
assert_eq!(g.distance(centre, corner), diagonal, "{adj:?}");
}
}
#[test]
fn a_hexagon_of_radius_r_has_the_centred_hexagonal_number_of_cells() {
for (r, cells) in [(0, 1), (1, 7), (2, 19), (3, 37)] {
assert_eq!(FullGrid::hexagon(r).len(), cells, "radius {r}");
}
}
#[test]
fn a_hex_cell_has_six_neighbours_unless_it_is_on_the_rim() {
let g = FullGrid::hexagon(2);
let centre = g.at(Hex::new(0, 0));
assert_eq!(g.neighbors(centre).count(), 6);
let rim = g.at(Hex::new(2, 0));
assert_eq!(g.neighbors(rim).count(), 3);
}
const OFFSETS: [Offset; 4] = [Offset::OddR, Offset::EvenR, Offset::OddQ, Offset::EvenQ];
#[test]
fn a_hex_rectangle_holds_exactly_width_times_height_cells() {
for o in OFFSETS {
for (w, h) in [(1, 1), (1, 7), (7, 1), (5, 4), (9, 8)] {
assert_eq!(
FullGrid::hex_rect(w, h, o).len(),
usize::try_from(w * h).unwrap(),
"{o:?} {w}x{h}"
);
}
}
}
#[test]
fn every_cell_of_a_hex_rectangle_is_the_tilemap_cell_it_was_built_from() {
for o in OFFSETS {
let g = FullGrid::hex_rect(6, 5, o);
for row in 0..5 {
for col in 0..6 {
let i = g.index_of(o.to_hex(col, row));
let i = i.unwrap_or_else(|| panic!("{o:?} has no ({col}, {row})"));
assert_eq!(o.from_hex(g.coord(i)), (col, row), "{o:?}");
}
}
}
}
#[test]
fn filtered_drops_cells_and_the_steps_into_them() {
let full = FullGrid::square(3, 3, Adjacency::Four);
let holed = full.filtered(|c| c != Sq::new(1, 1));
assert_eq!(holed.len(), 8);
assert!(!holed.contains(Sq::new(1, 1)));
let above = holed.at(Sq::new(1, 0));
assert!(holed.step(above, Dir8::S).is_none());
}
#[test]
fn duplicate_cells_are_dropped_keeping_the_first() {
let g = FullGrid::new(
[Sq::new(0, 0), Sq::new(1, 0), Sq::new(0, 0)],
&Dir8::ORTHO,
Metric::MANHATTAN,
);
assert_eq!(g.len(), 2);
assert_eq!(
g.index_of(Sq::new(0, 0)),
g.indices().next(),
"the first occurrence kept its place, so it is still cell zero"
);
}
#[test]
fn fallible_constructors_report_invalid_requests() {
assert!(matches!(
FullGrid::<Sq>::try_square(-1, 2, Adjacency::Four),
Err(GridError::InvalidDimensions { w: -1, h: 2 })
));
assert!(matches!(
FullGrid::<Sq>::try_disc(-1, Adjacency::Four),
Err(GridError::InvalidRadius { radius: -1 })
));
assert!(matches!(
FullGrid::<Hex>::try_hexagon(-1),
Err(GridError::InvalidRadius { radius: -1 })
));
assert!(matches!(
FullGrid::<Sq>::try_new(
[Sq::new(0, 0), Sq::new(1, 1)],
&Dir8::ALL,
Metric::MANHATTAN,
),
Err(GridError::MetricDisagrees { span: 2 })
));
assert!(matches!(
FullGrid::<Sq>::try_square(4097, 4097, Adjacency::Four),
Err(GridError::TooManyCells { cells: 16_785_409 })
));
}
}