use crate::coord::{Coord, Idx, Metric, Tag};
#[allow(unused_imports)]
use crate::full::FullGrid;
use crate::path::{Cost, Movement, Path, Step};
use crate::sub::SubGrid;
use alloc::vec;
use alloc::vec::Vec;
pub const MAX_SIGHT: u32 = 64;
pub type Dir<B> = <<B as Grid>::Cell as Coord>::Dir;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sight {
pub eye: Idx,
pub at: Idx,
pub target: Idx,
}
#[track_caller]
pub(crate) fn slot(len: usize, tag: Tag, i: Idx) -> usize {
debug_assert!(
i.tag().agrees(tag),
"cell {i} was issued by a different grid than the one being asked \
(indices are per-grid, and this one is in range for both, so nothing else can catch it). \
Look the cell up again with `Grid::index_of` or `Grid::at` on the grid you mean.",
);
assert!(
(i.raw() as usize) < len,
"cell {i} is not on this grid, which has {len} cells (indices are per-grid; \
a stale one from before `filtered` or a subset renumbered will not do)",
);
i.raw() as usize
}
#[track_caller]
pub(crate) fn same_grid(tag: Tag, i: Idx) {
debug_assert!(
i.tag().agrees(tag),
"cell {i} was issued by a different grid than the one being asked \
(indices are per-grid, and this one may well be in range for both). \
Look the cell up again with `Grid::index_of` or `Grid::at` on the grid you mean.",
);
}
pub(crate) fn cost_ceiling(len: usize) -> Cost {
Cost::MAX / (len.saturating_sub(1).max(1) as Cost)
}
pub trait Grid {
type Cell: Coord;
type Root: Grid<Cell = Self::Cell>;
fn tag(&self) -> Tag;
fn len(&self) -> usize;
fn coord(&self, i: Idx) -> Self::Cell;
fn index_of(&self, c: Self::Cell) -> Option<Idx>;
fn dirs(&self) -> &[Dir<Self>];
fn step(&self, i: Idx, d: Dir<Self>) -> Option<Idx>;
fn neighbors(&self, i: Idx) -> impl Iterator<Item = (Dir<Self>, Idx)>;
fn in_neighbors(&self, j: Idx) -> impl Iterator<Item = (Dir<Self>, Idx)>;
fn metric(&self) -> Metric<Self::Cell>;
fn root(&self) -> &Self::Root;
fn to_root(&self, i: Idx) -> Idx;
fn of_root(&self, i: Idx) -> Option<Idx>;
fn is_empty(&self) -> bool {
self.len() == 0
}
#[track_caller]
fn at(&self, c: Self::Cell) -> Idx {
match self.index_of(c) {
Some(i) => i,
None => panic!(
"{c:?} is not a cell of this grid, which has {} (use `index_of` if being off the \
board is an answer rather than a mistake)",
self.len(),
),
}
}
#[must_use]
fn step_from(&self, c: Self::Cell, d: Dir<Self>) -> Option<Self::Cell> {
let i = self.index_of(c)?;
self.step(i, d).map(|j| self.coord(j))
}
fn indices(&self) -> impl Iterator<Item = Idx> {
let tag = self.tag();
#[allow(clippy::cast_possible_truncation)]
(0..self.len() as u32).map(move |i| Idx::new(tag, i))
}
fn cells(&self) -> impl Iterator<Item = Self::Cell> {
self.indices().map(|i| self.coord(i))
}
fn coords_of(&self, of: impl IntoIterator<Item = Idx>) -> impl Iterator<Item = Self::Cell> {
of.into_iter().map(|i| self.coord(i))
}
fn contains(&self, c: Self::Cell) -> bool {
self.index_of(c).is_some()
}
fn subset(&self, cells: impl IntoIterator<Item = Idx>) -> SubGrid<'_, Self::Root> {
SubGrid::of(self.root(), cells.into_iter().map(|i| self.to_root(i)))
}
#[must_use]
fn within_cell(&self, c: Self::Cell, min: u32, max: u32) -> Option<SubGrid<'_, Self::Root>> {
self.index_of(c).map(|i| self.within(i, min, max))
}
fn ray(&self, i: Idx, d: Dir<Self>) -> impl Iterator<Item = Idx> {
let _ = slot(self.len(), self.tag(), i);
core::iter::successors(self.step(i, d), move |&j| self.step(j, d)).take(self.len())
}
#[must_use]
fn run(&self, i: Idx, d: Dir<Self>, same: impl Fn(Idx) -> bool) -> Vec<Idx> {
let _ = slot(self.len(), self.tag(), i);
let behind = |j: Idx| {
self.in_neighbors(j)
.find(|&(dir, _)| dir == d)
.map(|(_, f)| f)
};
let mut line: Vec<Idx> = core::iter::successors(behind(i), |&j| behind(j))
.take(self.len())
.take_while(|&j| same(j))
.collect();
line.reverse();
line.push(i);
line.extend(self.ray(i, d).take_while(|&j| same(j)));
line
}
#[must_use]
fn offset(&self, i: Idx, delta: Self::Cell) -> Option<Idx> {
let _ = slot(self.len(), self.tag(), i);
self.index_of(self.coord(i) + delta)
}
#[must_use]
fn distance(&self, a: Idx, b: Idx) -> u32 {
let _ = slot(self.len(), self.tag(), a);
let _ = slot(self.len(), self.tag(), b);
self.metric().distance(self.coord(a), self.coord(b))
}
#[must_use]
fn within(&self, i: Idx, min: u32, max: u32) -> SubGrid<'_, Self::Root> {
let _ = slot(self.len(), self.tag(), i);
if min > max {
return self.subset([]);
}
let (c, metric) = (self.coord(i), self.metric());
if metric.count(max) <= self.len() as u64 {
let hit: Vec<Idx> = metric
.deltas(max)
.into_iter()
.filter(|&(_, d)| d >= min)
.filter_map(|(delta, _)| self.index_of(c + delta))
.collect();
self.subset(hit)
} else {
self.subset(
self.indices()
.filter(|&j| (min..=max).contains(&metric.distance(c, self.coord(j)))),
)
}
}
#[must_use]
fn ring(&self, i: Idx, r: u32) -> SubGrid<'_, Self::Root> {
self.within(i, r, r)
}
#[must_use]
fn line(&self, a: Idx, b: Idx) -> Vec<Idx> {
let _ = slot(self.len(), self.tag(), a);
let _ = slot(self.len(), self.tag(), b);
let metric = self.metric();
if !metric.has_lerp() {
return Vec::new();
}
if a == b {
return vec![a]; }
let ordered = self.coord(a) < self.coord(b);
let (lo, hi) = if ordered { (a, b) } else { (b, a) };
let (ca, cb) = (self.coord(lo), self.coord(hi));
let distance = self.distance(lo, hi);
if distance > self.len() as u32 {
let mut cells: Vec<(u32, Idx)> = self
.indices()
.filter_map(|j| {
let at = self.distance(lo, j);
(j == lo
|| j == hi
|| (at <= distance
&& metric.lerp(ca, cb, at, distance) == Some(self.coord(j))))
.then_some((at, j))
})
.collect();
cells.sort_unstable_by_key(|&(at, j)| (at, j));
if lo != a {
cells.reverse();
}
return cells.into_iter().map(|(_, j)| j).collect();
}
let n = distance.max(1);
let mut cells: Vec<Idx> = (0..=n)
.filter_map(|t| self.index_of(metric.lerp(ca, cb, t, n)?))
.collect();
cells.dedup();
if cells.first() != Some(&lo) {
cells.insert(0, lo);
}
if cells.last() != Some(&hi) {
cells.push(hi);
}
if lo != a {
cells.reverse();
}
cells
}
fn los(&self, a: Idx, b: Idx, blocks: impl Fn(Idx) -> bool) -> bool {
self.los_by(a, b, |s| blocks(s.at))
}
fn los_by(&self, a: Idx, b: Idx, blocks: impl Fn(Sight) -> bool) -> bool {
self.line(a, b).into_iter().all(|at| {
at == a
|| at == b
|| !blocks(Sight {
eye: a,
at,
target: b,
})
})
}
#[must_use]
fn visible_from(
&self,
i: Idx,
r: u32,
blocks: impl Fn(Idx) -> bool,
) -> SubGrid<'_, Self::Root> {
self.visible_from_by(i, r, |s| blocks(s.at))
}
#[must_use]
fn visible_from_by(
&self,
i: Idx,
r: u32,
blocks: impl Fn(Sight) -> bool,
) -> SubGrid<'_, Self::Root> {
assert!(
r <= MAX_SIGHT,
"a sight radius of {r} is beyond MAX_SIGHT ({MAX_SIGHT}); raycasting is O(r^3) and \
this would take minutes. Use shadowcasting for a radius this large.",
);
let seen: Vec<Idx> = self
.within(i, 0, r)
.cells()
.filter_map(|c| self.index_of(c))
.filter(|&j| self.los_by(i, j, &blocks))
.collect();
self.subset(seen)
}
#[must_use]
fn visible_from_cell(
&self,
c: Self::Cell,
r: u32,
blocks: impl Fn(Idx) -> bool,
) -> Option<SubGrid<'_, Self::Root>> {
self.index_of(c).map(|i| self.visible_from(i, r, blocks))
}
#[must_use]
fn component(&self, i: Idx, passable: impl Fn(Idx) -> bool) -> SubGrid<'_, Self::Root> {
let _ = slot(self.len(), self.tag(), i);
if !passable(i) {
return self.subset([]);
}
let mut seen = vec![false; self.len()];
seen[i.raw() as usize] = true;
let mut frontier = vec![i];
while let Some(at) = frontier.pop() {
for (_, j) in self.neighbors(at) {
if !seen[j.raw() as usize] && passable(j) {
seen[j.raw() as usize] = true;
frontier.push(j);
}
}
}
self.subset(self.indices().filter(|&j| seen[j.raw() as usize]))
}
#[must_use]
fn component_from(
&self,
c: Self::Cell,
passable: impl Fn(Idx) -> bool,
) -> Option<SubGrid<'_, Self::Root>> {
self.index_of(c).map(|i| self.component(i, passable))
}
#[must_use]
fn is_connected(&self, passable: impl Fn(Idx) -> bool) -> bool {
let Some(first) = self.indices().find(|&i| passable(i)) else {
return true;
};
let total = self.indices().filter(|&i| passable(i)).count();
self.component(first, &passable).len() == total
}
fn path<F>(&self, start: Idx, goal: Idx, m: &Movement<F>) -> Option<Path>
where
F: Fn(Step<Self::Cell>) -> Option<Cost>,
{
crate::search::find(self, start, goal, m)
}
#[must_use]
fn path_between<F>(&self, start: Self::Cell, goal: Self::Cell, m: &Movement<F>) -> Option<Path>
where
F: Fn(Step<Self::Cell>) -> Option<Cost>,
{
let (Some(start), Some(goal)) = (self.index_of(start), self.index_of(goal)) else {
return None;
};
self.path(start, goal, m)
}
fn reachable<F>(&self, start: Idx, budget: Cost, m: &Movement<F>) -> Vec<(Idx, Cost)>
where
F: Fn(Step<Self::Cell>) -> Option<Cost>,
{
crate::search::reachable(self, start, budget, m)
}
#[must_use]
fn reachable_from<F>(
&self,
start: Self::Cell,
budget: Cost,
m: &Movement<F>,
) -> Option<Vec<(Self::Cell, Cost)>>
where
F: Fn(Step<Self::Cell>) -> Option<Cost>,
{
let start = self.index_of(start)?;
Some(
self.reachable(start, budget, m)
.into_iter()
.map(|(i, cost)| (self.coord(i), cost))
.collect(),
)
}
fn path_toward<F>(&self, start: Idx, target: Idx, budget: Cost, m: &Movement<F>) -> Option<Path>
where
F: Fn(Step<Self::Cell>) -> Option<Cost>,
{
crate::search::toward(self, start, target, budget, m)
}
fn reaching<F>(&self, goal: Idx, budget: Cost, m: &Movement<F>) -> Vec<(Idx, Cost)>
where
F: Fn(Step<Self::Cell>) -> Option<Cost>,
{
crate::search::reaching(self, goal, budget, m)
}
#[must_use]
fn reaching_cell<F>(
&self,
goal: Self::Cell,
budget: Cost,
m: &Movement<F>,
) -> Option<Vec<(Self::Cell, Cost)>>
where
F: Fn(Step<Self::Cell>) -> Option<Cost>,
{
let goal = self.index_of(goal)?;
Some(
self.reaching(goal, budget, m)
.into_iter()
.map(|(i, cost)| (self.coord(i), cost))
.collect(),
)
}
}
#[cfg(test)]
mod tests {
use crate::coord::{Dir8, Hex, Idx, Metric, Sq};
use crate::full::{Adjacency, FullGrid};
use crate::grid::{Grid, Sight};
use crate::path::Movement;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn a_ray_stops_at_the_edge() {
let g = FullGrid::square(8, 8, Adjacency::Eight);
let corner = g.at(Sq::new(0, 0));
assert_eq!(g.ray(corner, Dir8::E).count(), 7);
assert_eq!(g.ray(corner, Dir8::W).count(), 0);
}
#[test]
fn a_ray_stops_at_a_hole_but_an_offset_leaps_it() {
let g = FullGrid::square(5, 1, Adjacency::Four).filtered(|c| c.x != 2);
let start = g.at(Sq::new(0, 0));
assert_eq!(g.ray(start, Dir8::E).count(), 1);
assert_eq!(g.offset(start, Sq::new(3, 0)), g.index_of(Sq::new(3, 0)));
}
#[test]
fn within_measures_coordinates_so_a_hole_does_not_shorten_it() {
let g = FullGrid::hexagon(2).filtered(|c| c != Hex::new(1, 0));
let centre = g.at(Hex::new(0, 0));
let ring2 = g.ring(centre, 2);
assert!(ring2.contains(Hex::new(2, 0)));
assert!(!ring2.contains(Hex::new(0, 0)));
}
#[test]
fn within_zero_includes_the_origin_and_within_one_does_not() {
let g = FullGrid::square(5, 5, Adjacency::Four);
let mid = g.at(Sq::new(2, 2));
assert!(g.within(mid, 0, 1).contains(Sq::new(2, 2)));
assert!(!g.within(mid, 1, 1).contains(Sq::new(2, 2)));
assert_eq!(g.within(mid, 1, 1).len(), 4);
}
#[test]
fn a_run_walks_both_ways_and_reads_along_the_direction_it_was_asked_for() {
let g = FullGrid::square(7, 1, Adjacency::Four);
let at = |x| g.at(Sq::new(x, 0));
let wall = |i| (1..=5).contains(&g.coord(i).x);
let east = g.run(at(3), Dir8::E, wall);
assert_eq!(east, vec![at(1), at(2), at(3), at(4), at(5)]);
let west: Vec<_> = g.run(at(3), Dir8::W, wall).into_iter().rev().collect();
assert_eq!(west, east, "the same line, read the other way");
}
#[test]
fn a_run_always_holds_its_anchor_and_never_asks_about_it() {
let g = FullGrid::square(5, 1, Adjacency::Four);
let mid = g.at(Sq::new(2, 0));
assert_eq!(g.run(mid, Dir8::E, |_| false), vec![mid]);
}
#[test]
fn a_run_reaches_back_up_a_one_way_ledge() {
let g = ledges((0..4).map(|y| Sq::new(0, y)));
let mid = g.at(Sq::new(0, 2));
assert_eq!(g.run(mid, Dir8::S, |_| true).len(), 4);
}
#[test]
fn a_wall_across_a_room_splits_it_into_two_components() {
let g = FullGrid::square(5, 3, Adjacency::Four);
let open = |i| g.coord(i).x != 2;
let west = g.component(g.at(Sq::new(0, 0)), open);
let east = g.component(g.at(Sq::new(4, 0)), open);
assert_eq!(west.len(), 6);
assert_eq!(east.len(), 6);
assert!(west.cells().all(|c| !east.contains(c)), "disjoint");
assert!(!g.is_connected(open));
let door = |i| g.coord(i) != Sq::new(2, 0) && g.coord(i) != Sq::new(2, 2);
assert!(g.is_connected(door));
}
#[test]
fn a_component_excludes_a_start_that_is_itself_impassable() {
let g = FullGrid::square(3, 3, Adjacency::Four);
let wall = g.at(Sq::new(1, 1));
assert!(g.component(wall, |i| i != wall).is_empty());
}
fn ledges(cells: impl IntoIterator<Item = Sq>) -> FullGrid<Sq> {
FullGrid::new(cells, &[Dir8::S], Metric::MANHATTAN)
}
#[test]
fn a_component_follows_forward_edges_only() {
let g = ledges((0..4).map(|y| Sq::new(0, y)));
let top = g.at(Sq::new(0, 0));
let bottom = g.at(Sq::new(0, 3));
assert_eq!(g.component(top, |_| true).len(), 4);
assert_eq!(g.component(bottom, |_| true).len(), 1);
}
#[test]
fn is_connected_asks_from_the_lowest_index_and_the_answer_can_turn_on_it() {
assert!(
ledges((0..4).map(|y| Sq::new(0, y))).is_connected(|_| true),
"index 0 is the top of the drop, and everything is below it"
);
assert!(
!ledges((0..4).rev().map(|y| Sq::new(0, y))).is_connected(|_| true),
"index 0 is the bottom, and nothing can be reached from there"
);
}
#[test]
fn an_eight_way_grid_gives_an_archer_a_square_and_a_four_way_grid_a_diamond() {
let mid = Sq::new(3, 3);
let eight = FullGrid::square(7, 7, Adjacency::Eight);
let i = eight.at(mid);
assert_eq!(
eight.within(i, 1, 2).len(),
24,
"a 5x5 square, less the centre"
);
let four = FullGrid::square(7, 7, Adjacency::Four);
let j = four.at(mid);
assert_eq!(four.within(j, 1, 2).len(), 12, "a diamond");
}
#[test]
fn coordinate_facing_helpers_cover_the_common_queries() {
let g = FullGrid::square(5, 5, Adjacency::Four);
let movement = Movement::uniform(&g, 10);
assert_eq!(g.step_from(Sq::new(1, 1), Dir8::E), Some(Sq::new(2, 1)));
assert_eq!(g.step_from(Sq::new(-1, 1), Dir8::E), None);
let path = g
.path_between(Sq::new(0, 0), Sq::new(2, 0), &movement)
.unwrap();
assert_eq!(
path.cells(&g).collect::<Vec<_>>(),
vec![Sq::new(0, 0), Sq::new(1, 0), Sq::new(2, 0)]
);
assert_eq!(
g.reachable_from(Sq::new(2, 2), 10, &movement)
.unwrap()
.len(),
5
);
assert_eq!(
g.reaching_cell(Sq::new(2, 2), 0, &movement).unwrap().len(),
1
);
assert!(
g.within_cell(Sq::new(2, 2), 0, 1)
.unwrap()
.contains(Sq::new(2, 2))
);
assert!(
g.component_from(Sq::new(2, 2), |_| true)
.unwrap()
.contains(Sq::new(2, 2))
);
assert!(
g.visible_from_cell(Sq::new(2, 2), 1, |_| false)
.unwrap()
.contains(Sq::new(2, 2))
);
}
#[test]
fn a_target_blind_gate_answers_exactly_as_the_plain_predicate_does() {
let g = FullGrid::square(7, 7, Adjacency::Eight);
let wall = |i: Idx| g.coord(i).x == 3 && g.coord(i).y != 3;
for a in g.indices() {
for b in g.indices() {
assert_eq!(
g.los(a, b, wall),
g.los_by(a, b, |s| wall(s.at)),
"{:?} -> {:?}",
g.coord(a),
g.coord(b)
);
}
}
}
#[test]
fn a_gate_that_reads_the_target_says_what_no_bare_predicate_could() {
let g = FullGrid::square(7, 1, Adjacency::Eight);
let window = g.at(Sq::new(3, 0));
let west = g.at(Sq::new(0, 0));
let east = g.at(Sq::new(6, 0));
let one_way = |s: Sight| s.at == window && g.coord(s.target).x > 3;
assert!(!g.los_by(west, east, one_way), "you cannot see in");
assert!(g.los_by(east, west, one_way), "but you can see out");
}
#[test]
fn a_field_of_view_gate_is_told_which_cell_is_being_looked_at() {
let g = FullGrid::square(5, 5, Adjacency::Eight);
let eye = g.at(Sq::new(0, 2));
let far = g.at(Sq::new(4, 2));
let seen = g.visible_from_by(eye, 4, |s| s.target == far);
assert!(!seen.contains(Sq::new(4, 2)), "singled out by target");
assert!(
seen.contains(Sq::new(3, 2)),
"its neighbour is reached through the very same cells"
);
}
#[test]
fn the_eye_and_the_target_are_never_themselves_tested() {
let g = FullGrid::square(5, 1, Adjacency::Eight);
let (a, b) = (g.at(Sq::new(0, 0)), g.at(Sq::new(4, 0)));
assert!(g.los_by(a, b, |s| {
assert_ne!(s.at, s.eye, "the cell you stand in");
assert_ne!(s.at, s.target, "the wall you are looking at");
false
}),);
}
}