use core::fmt;
use hashbrown::HashMap;
use hashbrown::hash_map::Entry;
use crate::coord::{Coord, Dir6, Dir8, Hex, Idx, Metric, Sq, Tag};
use crate::grid::{Grid, same_grid, slot};
use crate::layout::Offset;
use alloc::vec;
use alloc::vec::Vec;
const NONE: u32 = u32::MAX;
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,
},
TooManyEdges {
cells: u64,
directions: u64,
},
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::TooManyEdges { cells, directions } => write!(
f,
"a {cells}-cell grid with {directions} directions has too many edge entries",
),
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>,
steps: Vec<u32>,
back: Back,
metric: Metric<C>,
tag: Tag,
}
#[derive(Debug, Clone, Default)]
struct Back {
start: Vec<u32>,
from: Vec<u32>,
dir: Vec<u32>,
}
impl Back {
fn of(steps: &[u32], cells: usize, dirs: usize) -> Self {
let mut start = vec![0u32; cells + 1];
for &j in steps {
if j != NONE {
start[j as usize + 1] += 1;
}
}
for k in 1..start.len() {
start[k] += start[k - 1];
}
let edges = *start.last().unwrap_or(&0) as usize;
let (mut from, mut dir) = (vec![0; edges], vec![0u32; edges]);
let mut at = start.clone();
for (slot, &j) in steps.iter().enumerate() {
if j == NONE {
continue;
}
let put = at[j as usize] as usize;
from[put] = (slot / dirs) as u32;
dir[put] = (slot % dirs) as u32;
at[j as usize] += 1;
}
Self { start, from, dir }
}
}
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,
});
}
}
let edge_count = ordered
.len()
.checked_mul(dirs.len())
.ok_or(GridError::TooManyEdges {
cells: ordered.len() as u64,
directions: dirs.len() as u64,
})?;
if edge_count > u32::MAX as usize {
return Err(GridError::TooManyEdges {
cells: ordered.len() as u64,
directions: dirs.len() as u64,
});
}
let mut steps = Vec::with_capacity(edge_count);
for (i, &c) in ordered.iter().enumerate() {
for &d in dirs {
let j = index.get(&c.step(d)).copied().unwrap_or(NONE);
if j as usize == i {
steps.push(NONE);
continue;
}
if j != NONE {
let span = metric.distance(c, ordered[j as usize]);
if span > 1 {
return Err(GridError::MetricDisagrees { span });
}
}
steps.push(j);
}
}
let back = Back::of(&steps, ordered.len(), dirs.len());
Ok(Self {
tag: Tag::of(ordered.iter()),
cells: ordered,
index,
dirs: dirs.to_vec(),
steps,
back,
metric,
})
}
const fn idx(&self, i: u32) -> Idx {
Idx::new(self.tag, i)
}
fn reached(&self, j: u32) -> Option<Idx> {
(j != NONE).then(|| self.idx(j))
}
#[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> Grid for FullGrid<C> {
type Cell = C;
type Root = Self;
fn tag(&self) -> Tag {
self.tag
}
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 cell = slot(self.len(), self.tag, i);
let at = self.dirs.iter().position(|&x| x == d)?;
self.reached(self.steps[cell * self.dirs.len() + at])
}
fn neighbors(&self, i: Idx) -> impl Iterator<Item = (C::Dir, Idx)> {
let base = slot(self.len(), self.tag, i) * self.dirs.len();
self.dirs
.iter()
.enumerate()
.filter_map(move |(at, &d)| self.reached(self.steps[base + at]).map(|j| (d, j)))
}
fn in_neighbors(&self, j: Idx) -> impl Iterator<Item = (C::Dir, Idx)> {
let cell = slot(self.len(), self.tag, j);
let (lo, hi) = (
self.back.start[cell] as usize,
self.back.start[cell + 1] as usize,
);
(lo..hi).map(move |k| {
(
self.dirs[self.back.dir[k] as usize],
self.idx(self.back.from[k]),
)
})
}
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 = w as u64 * h as u64;
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 * radius as u64 + 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 = radius as u64;
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 = w as u64 * h as u64;
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::TooManyEdges {
cells: 1,
directions: 1,
},
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 in 0..4 {
assert_eq!(
FullGrid::hexagon(r).len() as i32,
3 * r * (r + 1) + 1,
"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() as i32,
w * h,
"{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 })
));
}
}