use core::fmt;
use crate::coord::{Coord, Idx};
use crate::grid::{Dir, Grid, cost_ceiling};
use alloc::vec::Vec;
pub type Cost = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MovementError {
CostTooHigh {
cost: Cost,
ceiling: Cost,
cells: usize,
},
}
impl fmt::Display for MovementError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::CostTooHigh {
cost,
ceiling,
cells,
} => write!(
f,
"a step costs {cost}, but on a board of {cells} cells no step may cost more than \
{ceiling} without overflowing Cost ({})",
Cost::MAX,
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Step<C: Coord> {
pub from: Idx,
pub to: Idx,
pub dir: C::Dir,
}
pub struct Movement<F> {
enter: F,
min_step: Cost,
}
impl<F> fmt::Debug for Movement<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Movement")
.field("min_step", &self.min_step)
.finish_non_exhaustive()
}
}
impl<F> Movement<F> {
pub fn scan<B: Grid + ?Sized>(g: &B, enter: F) -> Self
where
F: Fn(Step<B::Cell>) -> Option<Cost>,
{
Self::try_scan(g, enter).unwrap_or_else(|error| panic!("{error}"))
}
pub fn try_scan<B: Grid + ?Sized>(g: &B, enter: F) -> Result<Self, MovementError>
where
F: Fn(Step<B::Cell>) -> Option<Cost>,
{
let ceiling = cost_ceiling(g.len());
let mut min_step = Cost::MAX;
let mut max_step = 0;
let mut has_step = false;
for from in g.indices() {
for (dir, to) in g.neighbors(from) {
if let Some(cost) = enter(Step { from, to, dir }) {
has_step = true;
min_step = min_step.min(cost);
max_step = max_step.max(cost);
}
}
}
if has_step && max_step > ceiling {
return Err(MovementError::CostTooHigh {
cost: max_step,
ceiling,
cells: g.len(),
});
}
Ok(Self {
enter,
min_step: if has_step { min_step } else { 0 },
})
}
#[must_use]
pub fn new(enter: F, min_step: Cost) -> Self {
Self { enter, min_step }
}
#[must_use]
pub fn min_step(&self) -> Cost {
self.min_step
}
pub(crate) fn enter<C: Coord>(&self, s: Step<C>) -> Option<Cost>
where
F: Fn(Step<C>) -> Option<Cost>,
{
(self.enter)(s)
}
}
impl Movement<()> {
#[must_use]
pub fn cell_cost<'a, B, G>(
g: &'a B,
cost: G,
) -> Movement<impl Fn(Step<B::Cell>) -> Option<Cost> + 'a>
where
B: Grid + ?Sized + 'a,
G: Fn(B::Cell) -> Option<Cost> + 'a,
{
Movement::scan(g, move |s| cost(g.coord(s.to)))
}
#[must_use]
pub fn edge_cost<'a, B, G>(
g: &'a B,
cost: G,
) -> Movement<impl Fn(Step<B::Cell>) -> Option<Cost> + 'a>
where
B: Grid + ?Sized + 'a,
G: Fn(B::Cell, B::Cell, Dir<B>) -> Option<Cost> + 'a,
{
Movement::scan(g, move |s| cost(g.coord(s.from), g.coord(s.to), s.dir))
}
#[must_use]
pub fn uniform<B: Grid + ?Sized>(
g: &B,
cost: Cost,
) -> Movement<impl Fn(Step<B::Cell>) -> Option<Cost>> {
Self::try_uniform(g, cost).unwrap_or_else(|error| panic!("{error}"))
}
#[allow(clippy::type_complexity)]
pub fn try_uniform<B: Grid + ?Sized>(
g: &B,
cost: Cost,
) -> Result<Movement<impl Fn(Step<B::Cell>) -> Option<Cost>>, MovementError> {
let ceiling = cost_ceiling(g.len());
if cost > ceiling {
return Err(MovementError::CostTooHigh {
cost,
ceiling,
cells: g.len(),
});
}
Ok(Movement::new(move |_| Some(cost), cost))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Path {
steps: Vec<Idx>,
cost: Cost,
}
impl Path {
pub(crate) fn of(steps: Vec<Idx>, cost: Cost) -> Self {
debug_assert!(!steps.is_empty(), "a search always sets out from somewhere");
Self { steps, cost }
}
#[must_use]
pub fn steps(&self) -> &[Idx] {
&self.steps
}
#[must_use = "iterate the path's coordinates"]
pub fn cells<'a, B: Grid + ?Sized>(&'a self, g: &'a B) -> impl Iterator<Item = B::Cell> + 'a {
self.steps.iter().copied().map(move |i| g.coord(i))
}
#[must_use]
pub fn cost(&self) -> Cost {
self.cost
}
#[must_use]
pub fn destination(&self) -> Idx {
*self
.steps
.last()
.expect("a Path always has a first cell, so it always has a last")
}
#[must_use]
pub fn start(&self) -> Idx {
self.steps[0]
}
#[must_use]
pub fn len(&self) -> usize {
self.steps.len() - 1
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coord::{Dir8, Sq};
use crate::full::{Adjacency, FullGrid};
use alloc::vec;
fn open(g: &FullGrid<Sq>) -> Movement<impl Fn(Step<Sq>) -> Option<Cost>> {
Movement::scan(g, |_| Some(10))
}
#[test]
fn a_path_includes_its_start_and_is_never_charged_for_it() {
let g = FullGrid::square(5, 5, Adjacency::Four);
let a = g.at(Sq::new(0, 0));
let b = g.at(Sq::new(2, 0));
let p = g.path(a, b, &open(&g)).unwrap();
assert_eq!(p.steps().len(), 3, "start, middle, end");
assert_eq!(p.steps()[0], a);
assert_eq!(p.destination(), b);
assert_eq!(p.len(), 2, "two cells moved through");
assert_eq!(
p.cost(),
20,
"two steps at 10, and nothing for standing still"
);
}
#[test]
fn a_path_to_where_you_already_are_costs_nothing() {
let g = FullGrid::square(3, 3, Adjacency::Four);
let a = g.at(Sq::new(1, 1));
let p = g.path(a, a, &open(&g)).unwrap();
assert_eq!(p.cost(), 0);
assert_eq!(p.steps(), vec![a]);
assert!(p.is_empty());
}
#[test]
fn a_walled_off_cell_has_no_path_to_it() {
let g = FullGrid::square(3, 3, Adjacency::Four);
let target = g.at(Sq::new(2, 2));
let walls = [Sq::new(1, 2), Sq::new(2, 1)];
let m = Movement::scan(&g, |s| (!walls.contains(&g.coord(s.to))).then_some(10));
assert!(g.path(g.at(Sq::new(0, 0)), target, &m).is_none());
}
#[test]
fn a_cheap_road_is_worth_a_detour() {
let g = FullGrid::square(5, 3, Adjacency::Four);
let m = Movement::scan(&g, |s| Some(if g.coord(s.to).y == 0 { 10 } else { 30 }));
let a = g.at(Sq::new(0, 1));
let b = g.at(Sq::new(4, 1));
let p = g.path(a, b, &m).unwrap();
assert_eq!(p.cost(), 80);
assert!(
p.steps().iter().any(|&i| g.coord(i).y == 0),
"it used the road"
);
}
#[test]
fn min_step_is_the_cheapest_step_anywhere_and_a_road_drags_it_down() {
let g = FullGrid::square(4, 4, Adjacency::Four);
let m = Movement::scan(&g, |s| {
Some(if g.coord(s.to) == Sq::new(0, 0) {
5
} else {
10
})
});
assert_eq!(m.min_step(), 5);
}
#[test]
fn an_impassable_board_yields_a_zero_minimum_and_still_answers() {
let g = FullGrid::square(3, 3, Adjacency::Four);
let m = Movement::scan(&g, |_| None);
let (from, to) = (g.at(Sq::new(0, 0)), g.at(Sq::new(2, 2)));
assert_eq!(m.min_step(), 0, "no steps at all: promise nothing");
assert!(g.path(from, to, &m).is_none());
assert_eq!(
g.reachable(from, 100, &m),
vec![(from, 0)],
"you can still stand still"
);
}
#[test]
fn reach_is_bounded_by_the_budget() {
let g = FullGrid::square(9, 9, Adjacency::Four);
let centre = g.at(Sq::new(4, 4));
let m = open(&g);
for n in 0..4u32 {
let want = 2 * n * (n + 1) + 1;
assert_eq!(
g.reachable(centre, n * 10, &m).len() as u32,
want,
"budget {n}"
);
}
}
#[test]
fn reach_comes_back_cheapest_first() {
let g = FullGrid::square(5, 5, Adjacency::Four);
let costs: Vec<Cost> = g
.reachable(g.at(Sq::new(2, 2)), 40, &open(&g))
.iter()
.map(|&(_, c)| c)
.collect();
assert!(costs.windows(2).all(|w| w[0] <= w[1]), "{costs:?}");
assert_eq!(costs[0], 0, "you are the first thing you can reach");
}
#[test]
fn path_toward_closes_the_distance_when_it_cannot_arrive() {
let g = FullGrid::square(10, 1, Adjacency::Four);
let start = g.at(Sq::new(0, 0));
let far = g.at(Sq::new(9, 0));
let p = g.path_toward(start, far, 20, &open(&g)).unwrap();
assert_eq!(
g.coord(p.destination()),
Sq::new(2, 0),
"as near as it can get"
);
assert_eq!(p.cost(), 20);
}
#[test]
fn path_toward_arrives_when_the_target_is_in_reach() {
let g = FullGrid::square(10, 1, Adjacency::Four);
let start = g.at(Sq::new(0, 0));
let near = g.at(Sq::new(2, 0));
let p = g.path_toward(start, near, 100, &open(&g)).unwrap();
assert_eq!(p.destination(), near);
}
#[test]
fn path_toward_stays_put_when_it_is_already_as_close_as_it_can_be() {
let g = FullGrid::square(3, 1, Adjacency::Four);
let a = g.at(Sq::new(0, 0));
let m = Movement::new(|_| Some(10), 10);
let p = g.path_toward(a, g.at(Sq::new(2, 0)), 0, &m).unwrap();
assert_eq!(p.destination(), a);
assert_eq!(p.cost(), 0);
}
#[test]
fn a_diagonal_costs_more_than_an_orthogonal_if_you_say_so() {
let g = FullGrid::square(5, 5, Adjacency::Eight);
let m = Movement::scan(&g, |s| Some(if s.dir.is_diagonal() { 14 } else { 10 }));
let a = g.at(Sq::new(0, 0));
let b = g.at(Sq::new(2, 2));
assert_eq!(g.path(a, b, &m).unwrap().cost(), 28);
assert_eq!(m.min_step(), 10);
}
#[test]
fn a_one_way_ledge_can_be_dropped_off_but_not_climbed() {
let g = FullGrid::square(1, 3, Adjacency::Four);
let top = g.at(Sq::new(0, 0));
let bottom = g.at(Sq::new(0, 2));
let m = Movement::scan(&g, |s| (s.dir == Dir8::S).then_some(10));
assert_eq!(g.path(top, bottom, &m).unwrap().cost(), 20, "down is fine");
assert!(g.path(bottom, top, &m).is_none(), "up is not");
}
#[test]
fn fallible_movement_constructors_report_overflowing_costs() {
let g = FullGrid::square(3, 1, Adjacency::Four);
let cost = Cost::MAX / 2 + 1;
let want = MovementError::CostTooHigh {
cost,
ceiling: Cost::MAX / 2,
cells: 3,
};
assert!(matches!(
Movement::try_scan(&g, |_| Some(cost)),
Err(error) if error == want
));
assert!(matches!(
Movement::try_uniform(&g, cost),
Err(error) if error == want
));
}
#[test]
fn coordinate_facing_movement_constructors_price_their_coordinates() {
let g = FullGrid::square(3, 1, Adjacency::Four);
let by_cell = Movement::cell_cost(&g, |cell| (cell != Sq::new(1, 0)).then_some(10));
assert!(
g.path_between(Sq::new(0, 0), Sq::new(2, 0), &by_cell)
.is_none()
);
let by_edge = Movement::edge_cost(&g, |from, to, dir| {
Some(if from.x == 0 && to.x == 1 && dir == Dir8::E {
1
} else {
10
})
});
assert_eq!(
g.path_between(Sq::new(0, 0), Sq::new(2, 0), &by_edge)
.unwrap()
.cost(),
11
);
}
}