use alloc::collections::BinaryHeap;
use crate::coord::Idx;
use crate::grid::{Grid, slot};
use crate::path::{Cost, Movement, Path, Step};
use crate::tag::Tag;
use alloc::vec;
use alloc::vec::Vec;
const NO_PARENT: u32 = u32::MAX;
const CEILING: u64 = Cost::MAX as u64;
fn add(a: u64, b: u64) -> u64 {
a.saturating_add(b).min(CEILING)
}
fn as_cost(total: u64) -> Cost {
Cost::try_from(total).unwrap_or(Cost::MAX)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Visit {
est: u64,
cost: u64,
at: u32,
}
impl Ord for Visit {
fn cmp(&self, o: &Self) -> core::cmp::Ordering {
o.est.cmp(&self.est).then_with(|| o.at.cmp(&self.at))
}
}
impl PartialOrd for Visit {
fn partial_cmp(&self, o: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(o))
}
}
pub(crate) struct Route {
pub(crate) nodes: Vec<u32>,
pub(crate) cost: Cost,
}
pub(crate) struct Frontier {
cost: Vec<u64>,
parent: Vec<u32>,
queue: BinaryHeap<Visit>,
}
impl Frontier {
fn new(nodes: usize, start: u32) -> Self {
let mut it = Self {
cost: vec![u64::MAX; nodes],
parent: vec![NO_PARENT; nodes],
queue: BinaryHeap::new(),
};
it.cost[start as usize] = 0;
it.queue.push(Visit {
est: 0,
cost: 0,
at: start,
});
it
}
fn is_stale(&self, v: &Visit) -> bool {
self.cost[v.at as usize] < v.cost
}
fn relax(&mut self, from: &Visit, to: u32, step: Cost, h: impl FnOnce() -> Cost) {
let total = add(from.cost, u64::from(step));
let at = to as usize;
if total >= self.cost[at] {
return;
}
self.cost[at] = total;
self.parent[at] = from.at;
self.queue.push(Visit {
est: add(total, u64::from(h())),
cost: total,
at: to,
});
}
pub(crate) fn walk_home(&self, goal: u32) -> Route {
let mut nodes = vec![goal];
let mut at = goal;
loop {
let up = self.parent[at as usize];
if up == NO_PARENT {
break;
}
assert!(
nodes.len() <= self.cost.len(),
"the search's predecessors are cyclic — this is a bug in spacewalk",
);
at = up;
nodes.push(at);
}
nodes.reverse();
Route {
nodes,
cost: as_cost(self.cost[goal as usize]),
}
}
}
pub(crate) fn astar<E, I>(
nodes: usize,
start: u32,
goal: u32,
mut edges: E,
estimate: impl Fn(u32) -> Cost,
) -> Option<Route>
where
E: FnMut(u32) -> I,
I: IntoIterator<Item = (u32, Cost)>,
{
let mut frontier = Frontier::new(nodes, start);
while let Some(v) = frontier.queue.pop() {
if frontier.is_stale(&v) {
continue;
}
if v.at == goal {
return Some(frontier.walk_home(goal));
}
for (to, step) in edges(v.at) {
frontier.relax(&v, to, step, || estimate(to));
}
}
None
}
pub(crate) fn explore<E, I>(
nodes: usize,
start: u32,
budget: Cost,
mut edges: E,
) -> (Vec<(u32, Cost)>, Frontier)
where
E: FnMut(u32) -> I,
I: IntoIterator<Item = (u32, Cost)>,
{
let cap = u64::from(budget);
let mut frontier = Frontier::new(nodes, start);
let mut reached = Vec::new();
while let Some(v) = frontier.queue.pop() {
if v.cost > cap {
break;
}
if frontier.is_stale(&v) {
continue;
}
reached.push((v.at, as_cost(v.cost)));
for (to, step) in edges(v.at) {
frontier.relax(&v, to, step, || 0);
}
}
(reached, frontier)
}
fn edge(cost: Option<Cost>, node: Idx, floor: Cost) -> Option<(u32, Cost)> {
let cost = cost?;
debug_assert!(
cost >= floor,
"a step costs {cost}, below the promised minimum of {floor}: the A* heuristic \
will overestimate and paths will not be optimal. Use Movement::scan."
);
Some((node.raw(), cost))
}
fn succ<'a, B, F>(b: &'a B, i: Idx, m: &'a Movement<F>) -> impl Iterator<Item = (u32, Cost)> + 'a
where
B: Grid + ?Sized,
F: Fn(Step<B::Cell>) -> Option<Cost>,
{
b.neighbors(i)
.filter_map(move |(dir, to)| edge(m.enter(Step { from: i, to, dir }), to, m.min_step()))
}
fn pred<'a, B, F>(b: &'a B, j: Idx, m: &'a Movement<F>) -> impl Iterator<Item = (u32, Cost)> + 'a
where
B: Grid + ?Sized,
F: Fn(Step<B::Cell>) -> Option<Cost>,
{
b.in_neighbors(j)
.filter_map(move |(dir, from)| edge(m.enter(Step { from, to: j, dir }), from, m.min_step()))
}
fn minted(tag: Tag, found: Vec<(u32, Cost)>) -> Vec<(Idx, Cost)> {
found
.into_iter()
.map(|(n, cost)| (Idx::new(tag, n), cost))
.collect()
}
fn as_path(tag: Tag, route: Route) -> Path {
let steps = route.nodes.into_iter().map(|n| Idx::new(tag, n)).collect();
Path::of(steps, route.cost)
}
pub(crate) fn find<B, F>(b: &B, start: Idx, goal: Idx, m: &Movement<F>) -> Option<Path>
where
B: Grid + ?Sized,
F: Fn(Step<B::Cell>) -> Option<Cost>,
{
let tag = b.tag();
let _ = slot(b.len(), tag, goal);
let _ = slot(b.len(), tag, start);
let estimate = |n| {
b.distance(Idx::new(tag, n), goal)
.saturating_mul(m.min_step())
};
let steps = |n| succ(b, Idx::new(tag, n), m);
astar(b.len(), start.raw(), goal.raw(), steps, estimate).map(|route| as_path(tag, route))
}
pub(crate) fn reachable<B, F>(b: &B, start: Idx, budget: Cost, m: &Movement<F>) -> Vec<(Idx, Cost)>
where
B: Grid + ?Sized,
F: Fn(Step<B::Cell>) -> Option<Cost>,
{
let tag = b.tag();
let _ = slot(b.len(), tag, start);
let (found, _) = explore(b.len(), start.raw(), budget, |n| {
succ(b, Idx::new(tag, n), m)
});
minted(tag, found)
}
pub(crate) fn reaching<B, F>(b: &B, goal: Idx, budget: Cost, m: &Movement<F>) -> Vec<(Idx, Cost)>
where
B: Grid + ?Sized,
F: Fn(Step<B::Cell>) -> Option<Cost>,
{
let tag = b.tag();
let _ = slot(b.len(), tag, goal);
let (found, _) = explore(b.len(), goal.raw(), budget, |n| {
pred(b, Idx::new(tag, n), m)
});
minted(tag, found)
}
pub(crate) fn toward<B, F>(
b: &B,
start: Idx,
target: Idx,
budget: Cost,
m: &Movement<F>,
) -> Option<Path>
where
B: Grid + ?Sized,
F: Fn(Step<B::Cell>) -> Option<Cost>,
{
let tag = b.tag();
let _ = slot(b.len(), tag, target);
let _ = slot(b.len(), tag, start);
let (seen, frontier) = explore(b.len(), start.raw(), budget, |n| {
succ(b, Idx::new(tag, n), m)
});
let &(goal, _) = seen
.iter()
.min_by_key(|&&(n, cost)| (b.distance(Idx::new(tag, n), target), cost, n))?;
Some(as_path(tag, frontier.walk_home(goal)))
}