use std::{cmp::Ordering, collections::BinaryHeap};
use crate::{
grid::Grid,
path::Path,
search::{BudgetWatch, GridSearchError, SearchOutcome, SearchRequest, SearchStats},
};
pub type AllOptimalPathsResult = Result<SearchOutcome<Vec<Path>, SearchStats>, GridSearchError>;
#[derive(Debug, Clone, Copy)]
pub struct AllOptimalPaths {
pub max_paths: usize,
}
impl Default for AllOptimalPaths {
fn default() -> Self {
Self { max_paths: 64 }
}
}
impl AllOptimalPaths {
pub const NAME: &str = "astar-bag";
pub fn enumerate(&self, grid: &Grid, request: SearchRequest) -> AllOptimalPathsResult {
crate::search::validate_request(grid, request)?;
let Some(start_index) = grid.index_of(request.start) else {
return Ok(SearchOutcome::no_path(SearchStats { visited_nodes: 0 }));
};
let Some(goal_index) = grid.index_of(request.goal) else {
return Ok(SearchOutcome::no_path(SearchStats { visited_nodes: 0 }));
};
if request.start == request.goal {
let path =
Path::from_steps(vec![request.start]).expect("path contains at least one point");
return Ok(SearchOutcome::found(
vec![path],
SearchStats { visited_nodes: 1 },
));
}
let watch = BudgetWatch::start(request.budget);
let (best_costs, visited_nodes) = exact_costs_from_start(grid, start_index, &watch)?;
let Some(goal_cost) = best_costs[goal_index] else {
return Ok(SearchOutcome::no_path(SearchStats { visited_nodes }));
};
let paths = enumerate_backward(
grid,
&best_costs,
start_index,
goal_index,
goal_cost,
self.max_paths,
);
Ok(SearchOutcome::found(paths, SearchStats { visited_nodes }))
}
}
fn exact_costs_from_start(
grid: &Grid,
start_index: usize,
watch: &BudgetWatch,
) -> Result<(Vec<Option<usize>>, usize), GridSearchError> {
let mut frontier = BinaryHeap::from([FrontierEntry {
cost_so_far: 0,
index: start_index,
}]);
let mut best_costs: Vec<Option<usize>> = vec![None; grid.cell_count()];
let mut visited_nodes = 0usize;
best_costs[start_index] = Some(0);
while let Some(entry) = frontier.pop() {
if best_costs[entry.index] != Some(entry.cost_so_far) {
continue;
}
visited_nodes += 1;
if let Err(reason) = watch.check(visited_nodes) {
return Err(crate::search::budget_error(reason));
}
let current = grid.point_from_index(entry.index);
for neighbor in grid.neighbors4(current) {
let neighbor_index = grid
.index_of(neighbor)
.expect("walkable neighbors must exist inside the grid");
let edge_cost = grid
.traversal_cost(neighbor)
.expect("walkable neighbors must have a traversal cost");
let Some(next_cost) = entry.cost_so_far.checked_add(edge_cost) else {
continue;
};
if best_costs[neighbor_index].is_some_and(|best| next_cost >= best) {
continue;
}
best_costs[neighbor_index] = Some(next_cost);
frontier.push(FrontierEntry {
cost_so_far: next_cost,
index: neighbor_index,
});
}
}
Ok((best_costs, visited_nodes))
}
fn enumerate_backward(
grid: &Grid,
best_costs: &[Option<usize>],
start_index: usize,
goal_index: usize,
goal_cost: usize,
max_paths: usize,
) -> Vec<Path> {
let mut paths = Vec::new();
let mut suffix = vec![goal_index];
let mut choice_stack: Vec<Vec<usize>> =
vec![optimal_predecessors(grid, best_costs, goal_index)];
while let Some(alternatives) = choice_stack.last_mut() {
if paths.len() >= max_paths {
break;
}
let Some(predecessor) = alternatives.pop() else {
choice_stack.pop();
suffix.pop();
continue;
};
suffix.push(predecessor);
if predecessor == start_index {
let steps: Vec<_> = suffix
.iter()
.rev()
.map(|&index| grid.point_from_index(index))
.collect();
paths.push(
Path::from_steps_with_cost(steps, goal_cost)
.expect("path contains at least one point"),
);
suffix.pop();
} else {
choice_stack.push(optimal_predecessors(grid, best_costs, predecessor));
}
}
paths
}
fn optimal_predecessors(grid: &Grid, best_costs: &[Option<usize>], index: usize) -> Vec<usize> {
let Some(cost) = best_costs[index] else {
return Vec::new();
};
let enter_cost = grid
.traversal_cost(grid.point_from_index(index))
.expect("cells with a best cost are walkable");
grid.neighbors4(grid.point_from_index(index))
.into_iter()
.filter_map(|neighbor| grid.index_of(neighbor))
.filter(|&neighbor_index| {
best_costs[neighbor_index]
.and_then(|neighbor_cost| neighbor_cost.checked_add(enter_cost))
== Some(cost)
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FrontierEntry {
cost_so_far: usize,
index: usize,
}
impl Ord for FrontierEntry {
fn cmp(&self, other: &Self) -> Ordering {
other
.cost_so_far
.cmp(&self.cost_so_far)
.then_with(|| other.index.cmp(&self.index))
}
}
impl PartialOrd for FrontierEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use crate::{
algorithms::astar_all_optimal::AllOptimalPaths,
grid::{Cell, Grid},
point::Point,
search::SearchRequest,
};
#[test]
fn open_rectangle_yields_all_lattice_optimal_paths() {
let grid = Grid::new(3, 3).expect("grid dimensions are valid");
let outcome = AllOptimalPaths::default()
.enumerate(
&grid,
SearchRequest::new(Point::new(0, 0), Point::new(2, 2)),
)
.expect("endpoints are walkable");
let paths = outcome.path().expect("open grid has optimal paths");
assert_eq!(paths.len(), 6);
for path in paths {
assert_eq!(path.cost(), 4);
assert_eq!(path.start(), Point::new(0, 0));
assert_eq!(path.goal(), Point::new(2, 2));
}
}
#[test]
fn cap_limits_enumeration() {
let grid = Grid::new(4, 4).expect("grid dimensions are valid");
let outcome = AllOptimalPaths { max_paths: 3 }
.enumerate(
&grid,
SearchRequest::new(Point::new(0, 0), Point::new(3, 3)),
)
.expect("endpoints are walkable");
assert_eq!(outcome.path().map(Vec::len), Some(3));
}
#[test]
fn corridor_has_exactly_one_optimal_path() {
let mut grid = Grid::new(5, 5).expect("grid dimensions are valid");
for y in 0..5 {
if y != 2 {
grid.set_cell(Point::new(2, y), Cell::Blocked)
.expect("valid grid edit");
}
}
let outcome = AllOptimalPaths::default()
.enumerate(
&grid,
SearchRequest::new(Point::new(0, 2), Point::new(4, 2)),
)
.expect("endpoints are walkable");
assert_eq!(outcome.path().map(Vec::len), Some(1));
}
#[test]
fn reports_when_no_path_exists() {
let mut grid = Grid::new(3, 3).expect("grid dimensions are valid");
for x in 0..3 {
grid.set_cell(Point::new(x, 1), Cell::Blocked)
.expect("valid grid edit");
}
let outcome = AllOptimalPaths::default()
.enumerate(
&grid,
SearchRequest::new(Point::new(0, 0), Point::new(2, 2)),
)
.expect("endpoints are walkable");
assert!(!outcome.is_found());
}
}