use crate::{
grid::Grid,
path::Path,
point::Point,
search::{BudgetWatch, Pathfinder, SearchRequest, SearchResult},
};
#[derive(Debug, Default, Clone, Copy)]
pub struct IterativeDeepeningAStar;
impl Pathfinder for IterativeDeepeningAStar {
fn name(&self) -> &'static str {
"idastar"
}
fn search(&self, grid: &Grid, request: SearchRequest) -> SearchResult {
crate::search::validate_request(grid, request)?;
if !grid.is_reachable(request.start, request.goal) {
return crate::search::not_found(0);
}
if request.start == request.goal {
return crate::search::found(
Path::from_steps(vec![request.start]).expect("path contains at least one point"),
1,
);
}
let watch = BudgetWatch::start(request.budget);
let mut visited_nodes = 0usize;
let mut threshold = manhattan_distance(request.start, request.goal);
loop {
let mut on_path = vec![request.start];
let outcome = bounded_search(
grid,
request.goal,
0,
threshold,
&mut on_path,
&mut visited_nodes,
&watch,
)?;
match outcome {
BoundedOutcome::Found { cost } => {
return crate::search::found(
Path::from_steps_with_cost(on_path, cost)
.expect("path contains at least one point"),
visited_nodes,
);
}
BoundedOutcome::NextThreshold(next) => threshold = next,
BoundedOutcome::Exhausted => return crate::search::not_found(visited_nodes),
}
}
}
}
enum BoundedOutcome {
Found { cost: usize },
NextThreshold(usize),
Exhausted,
}
fn bounded_search(
grid: &Grid,
goal: Point,
cost_so_far: usize,
threshold: usize,
on_path: &mut Vec<Point>,
visited_nodes: &mut usize,
watch: &BudgetWatch,
) -> Result<BoundedOutcome, crate::search::GridSearchError> {
let current = *on_path.last().expect("path always holds the start");
*visited_nodes += 1;
if let Err(reason) = watch.check(*visited_nodes) {
return Err(crate::search::budget_error(reason));
}
let lower_bound = cost_so_far.saturating_add(manhattan_distance(current, goal));
if lower_bound > threshold {
return Ok(BoundedOutcome::NextThreshold(lower_bound));
}
if current == goal {
return Ok(BoundedOutcome::Found { cost: cost_so_far });
}
let mut next_threshold: Option<usize> = None;
for neighbor in grid.neighbors4(current) {
if on_path.contains(&neighbor) {
continue;
}
let edge_cost = grid
.traversal_cost(neighbor)
.expect("walkable neighbors must have a traversal cost");
let Some(next_cost) = cost_so_far.checked_add(edge_cost) else {
continue;
};
on_path.push(neighbor);
let outcome = bounded_search(
grid,
goal,
next_cost,
threshold,
on_path,
visited_nodes,
watch,
)?;
match outcome {
BoundedOutcome::Found { cost } => return Ok(BoundedOutcome::Found { cost }),
BoundedOutcome::NextThreshold(candidate) => {
on_path.pop();
next_threshold = Some(next_threshold.map_or(candidate, |best: usize| {
if candidate < best { candidate } else { best }
}));
}
BoundedOutcome::Exhausted => {
on_path.pop();
}
}
}
Ok(next_threshold.map_or(BoundedOutcome::Exhausted, BoundedOutcome::NextThreshold))
}
fn manhattan_distance(from: Point, to: Point) -> usize {
from.x.abs_diff(to.x) + from.y.abs_diff(to.y)
}
#[cfg(test)]
mod tests {
use crate::{
algorithms::{astar::AStar, iterative_deepening_astar::IterativeDeepeningAStar},
grid::{Cell, Grid},
point::Point,
search::{Pathfinder, SearchRequest},
};
#[test]
fn matches_astar_cost_on_a_weighted_detour() {
let mut grid = Grid::new(4, 3).expect("grid dimensions are valid");
grid.set_traversal_cost(Point::new(1, 1), 10)
.expect("valid cost edit");
grid.set_traversal_cost(Point::new(2, 1), 10)
.expect("valid cost edit");
let request = SearchRequest::new(Point::new(0, 1), Point::new(3, 1));
let idastar = IterativeDeepeningAStar
.search(&grid, request)
.expect("endpoints are walkable");
let astar = AStar
.search(&grid, request)
.expect("endpoints are walkable");
assert!(idastar.is_found());
assert_eq!(idastar.cost(), astar.cost());
}
#[test]
fn matches_astar_cost_through_the_only_gap() {
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 request = SearchRequest::new(Point::new(0, 0), Point::new(4, 4));
let idastar = IterativeDeepeningAStar
.search(&grid, request)
.expect("endpoints are walkable");
let astar = AStar
.search(&grid, request)
.expect("endpoints are walkable");
assert!(idastar.is_found());
assert_eq!(idastar.cost(), astar.cost());
}
#[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 result = IterativeDeepeningAStar
.search(
&grid,
SearchRequest::new(Point::new(0, 0), Point::new(2, 2)),
)
.expect("endpoints are walkable");
assert!(!result.is_found());
}
}