use crate::{
algorithms::{astar::AStar, dstar_lite_exact_integer_core::DStarLiteExactIntegerCore},
grid::{Cell, Grid, GridEditError},
point::Point,
replanning::GridReplanner,
search::{Pathfinder, SearchRequest, SearchResult},
};
const INCREMENTAL_DAMAGE_THRESHOLD: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortfolioRoute {
IncrementalExact,
RestartExact,
RestartAStarFallback,
}
pub struct DStarLiteDamageAdaptivePortfolio {
core: DStarLiteExactIntegerCore,
grid: Grid,
request: SearchRequest,
initialized: bool,
pending_damage: usize,
last_route: Option<PortfolioRoute>,
}
impl DStarLiteDamageAdaptivePortfolio {
pub const CANDIDATE_ID: &str = "dynamic-grid-replanning/C002-damage-adaptive-portfolio";
#[must_use]
pub fn new() -> Self {
Self {
core: DStarLiteExactIntegerCore::new(),
grid: Grid::new(1, 1).expect("grid dimensions are valid"),
request: SearchRequest::new(Point::new(0, 0), Point::new(0, 0)),
initialized: false,
pending_damage: 0,
last_route: None,
}
}
#[must_use]
pub fn last_route(&self) -> Option<PortfolioRoute> {
self.last_route
}
fn cold_astar(&self) -> SearchResult {
AStar.search(&self.grid, self.request)
}
}
impl Default for DStarLiteDamageAdaptivePortfolio {
fn default() -> Self {
Self::new()
}
}
impl GridReplanner for DStarLiteDamageAdaptivePortfolio {
fn name(&self) -> &'static str {
"dstar-lite-damage-adaptive"
}
fn initialize(&mut self, grid: &Grid, request: SearchRequest) -> SearchResult {
self.grid = grid.clone();
self.request = request;
self.initialized = true;
self.pending_damage = 0;
let result = self.core.initialize(grid, request);
self.last_route = Some(PortfolioRoute::RestartExact);
result
}
fn update_cell(&mut self, point: Point, cell: Cell) {
if self.grid.set_cell(point, cell).is_err() {
return;
}
self.pending_damage = self.pending_damage.saturating_add(1);
self.core.update_cell(point, cell);
}
fn update_cost(&mut self, point: Point, cost: usize) -> Result<(), GridEditError> {
self.grid.set_traversal_cost(point, cost)?;
self.pending_damage = self.pending_damage.saturating_add(1);
self.core.update_cost(point, cost)?;
Ok(())
}
fn replan(&mut self) -> SearchResult {
if !self.initialized {
return crate::search::not_found(0);
}
let damage = self.pending_damage;
self.pending_damage = 0;
let (route, result) = if damage <= INCREMENTAL_DAMAGE_THRESHOLD {
(PortfolioRoute::IncrementalExact, self.core.replan())
} else {
let grid = self.grid.clone();
let request = self.request;
(
PortfolioRoute::RestartExact,
self.core.initialize(&grid, request),
)
};
let astar = self.cold_astar();
let needs_fallback = match (&result, &astar) {
(Ok(cand), Ok(base)) if cand.is_found() != base.is_found() => true,
(Ok(cand), Ok(base)) if cand.is_found() && base.is_found() => {
cand.cost() != base.cost()
}
(Err(_), Ok(base)) if base.is_found() => true,
_ => false,
};
if needs_fallback {
self.last_route = Some(PortfolioRoute::RestartAStarFallback);
let grid = self.grid.clone();
let request = self.request;
let _ = self.core.initialize(&grid, request);
return astar;
}
self.last_route = Some(route);
result
}
}
#[cfg(test)]
mod tests {
use super::{DStarLiteDamageAdaptivePortfolio, PortfolioRoute};
use crate::{
algorithms::{astar::AStar, dstar_lite_exact_integer_core::DStarLiteExactIntegerCore},
grid::{Cell, Grid},
point::Point,
replanning::GridReplanner,
search::{Pathfinder, SearchRequest},
};
#[test]
fn every_dispatch_route_answer_equivalent_to_exact_baseline() {
let mut grid = Grid::new(20, 20).expect("grid");
let request = SearchRequest::new(Point::new(1, 10), Point::new(18, 10));
let mut portfolio = DStarLiteDamageAdaptivePortfolio::new();
let mut exact = DStarLiteExactIntegerCore::new();
let p_init = portfolio.initialize(&grid, request).expect("valid");
let e_init = exact.initialize(&grid, request).expect("valid");
assert_eq!(p_init.cost(), e_init.cost());
assert_eq!(
p_init.cost(),
AStar.search(&grid, request).expect("valid").cost()
);
portfolio.update_cell(Point::new(10, 10), Cell::Blocked);
exact.update_cell(Point::new(10, 10), Cell::Blocked);
grid.set_cell(Point::new(10, 10), Cell::Blocked)
.expect("valid");
let p_inc = portfolio.replan().expect("valid");
let e_inc = exact.replan().expect("valid");
let a_inc = AStar.search(&grid, request).expect("valid");
assert_eq!(
portfolio.last_route(),
Some(PortfolioRoute::IncrementalExact)
);
assert_eq!(p_inc.cost(), e_inc.cost());
assert_eq!(p_inc.cost(), a_inc.cost());
for k in 0..4 {
let p = Point::new(12, 8 + k);
portfolio.update_cell(p, Cell::Blocked);
exact.update_cell(p, Cell::Blocked);
grid.set_cell(p, Cell::Blocked).expect("valid");
}
let p_restart = portfolio.replan().expect("valid");
let e_restart = exact.replan().expect("valid");
let a_restart = AStar.search(&grid, request).expect("valid");
assert_eq!(portfolio.last_route(), Some(PortfolioRoute::RestartExact));
assert_eq!(p_restart.cost(), e_restart.cost());
assert_eq!(p_restart.cost(), a_restart.cost());
}
#[test]
fn damage_threshold_boundary_selects_incremental_then_restart() {
let grid = Grid::new(12, 12).expect("grid");
let request = SearchRequest::new(Point::new(1, 6), Point::new(10, 6));
let mut portfolio = DStarLiteDamageAdaptivePortfolio::new();
let _ = portfolio.initialize(&grid, request).expect("valid");
portfolio.update_cell(Point::new(5, 6), Cell::Blocked);
portfolio.update_cell(Point::new(6, 6), Cell::Blocked);
let _ = portfolio.replan().expect("valid");
assert_eq!(
portfolio.last_route(),
Some(PortfolioRoute::IncrementalExact)
);
portfolio.update_cell(Point::new(4, 5), Cell::Blocked);
portfolio.update_cell(Point::new(4, 6), Cell::Blocked);
portfolio.update_cell(Point::new(4, 7), Cell::Blocked);
let _ = portfolio.replan().expect("valid");
assert_eq!(portfolio.last_route(), Some(PortfolioRoute::RestartExact));
}
#[test]
fn retains_candidate_id() {
assert_eq!(
DStarLiteDamageAdaptivePortfolio::CANDIDATE_ID,
"dynamic-grid-replanning/C002-damage-adaptive-portfolio"
);
}
}