use crate::{
AStar, Grid, HierarchicalGridBuildError, Pathfinder, Point,
search::{SearchRequest, SearchResult},
};
pub trait PreprocessedGridBuilder {
type Map: PreparedGridSearch;
fn name(&self) -> &'static str;
fn preprocess(&self, grid: &Grid) -> Result<Self::Map, PreprocessedGridBuildError>;
}
pub trait PreparedGridSearch {
fn name(&self) -> &'static str;
fn grid(&self) -> &Grid;
fn metadata(&self) -> &PreprocessedGridMetadata;
fn search(&self, request: SearchRequest) -> SearchResult;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PreprocessedGridMetadata {
pub builder_name: &'static str,
pub query_algorithm: &'static str,
pub width: usize,
pub height: usize,
pub cell_count: usize,
pub walkable_cell_count: usize,
pub blocked_cell_count: usize,
pub movement_model: &'static str,
pub cost_model: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum PreprocessedGridBuildError {
#[error(transparent)]
Hierarchical(#[from] HierarchicalGridBuildError),
#[error("{algorithm} requires uniform traversal cost 1; cell {point:?} has cost {cost}")]
NonUniformCost {
algorithm: &'static str,
point: Point,
cost: usize,
},
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StaticPreparedGridBuilder;
impl StaticPreparedGridBuilder {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl PreprocessedGridBuilder for StaticPreparedGridBuilder {
type Map = StaticPreparedGrid;
fn name(&self) -> &'static str {
"static-prepared-grid"
}
fn preprocess(&self, grid: &Grid) -> Result<Self::Map, PreprocessedGridBuildError> {
Ok(StaticPreparedGrid {
grid: grid.clone(),
metadata: metadata_for_grid(grid, self.name(), AStar.name()),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticPreparedGrid {
grid: Grid,
metadata: PreprocessedGridMetadata,
}
impl StaticPreparedGrid {
#[must_use]
pub fn builder() -> StaticPreparedGridBuilder {
StaticPreparedGridBuilder
}
}
impl PreparedGridSearch for StaticPreparedGrid {
fn name(&self) -> &'static str {
self.metadata.builder_name
}
fn grid(&self) -> &Grid {
&self.grid
}
fn metadata(&self) -> &PreprocessedGridMetadata {
&self.metadata
}
fn search(&self, request: SearchRequest) -> SearchResult {
AStar.search(&self.grid, request)
}
}
pub(crate) fn metadata_for_grid(
grid: &Grid,
builder_name: &'static str,
query_algorithm: &'static str,
) -> PreprocessedGridMetadata {
let mut walkable_cell_count = 0usize;
let mut weighted = false;
for y in 0..grid.height() {
for x in 0..grid.width() {
let point = Point::new(x, y);
if let Some(cost) = grid.traversal_cost(point) {
walkable_cell_count += 1;
weighted |= cost != 1;
}
}
}
let cell_count = grid.cell_count();
PreprocessedGridMetadata {
builder_name,
query_algorithm,
width: grid.width(),
height: grid.height(),
cell_count,
walkable_cell_count,
blocked_cell_count: cell_count - walkable_cell_count,
movement_model: "4-way",
cost_model: if weighted { "weighted" } else { "uniform" },
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Cell, Pathfinder};
#[test]
fn static_prepared_grid_records_build_metadata_separately_from_query_stats() {
let mut grid = Grid::new(4, 3).expect("grid dimensions are valid");
grid.set_cell(Point::new(1, 1), Cell::Blocked)
.expect("fixture point is in bounds");
grid.set_traversal_cost(Point::new(2, 0), 3)
.expect("traversal cost is positive");
let prepared = StaticPreparedGrid::builder()
.preprocess(&grid)
.expect("static grid should preprocess");
let metadata = *prepared.metadata();
assert_eq!(metadata.builder_name, "static-prepared-grid");
assert_eq!(metadata.query_algorithm, "astar");
assert_eq!(metadata.width, 4);
assert_eq!(metadata.height, 3);
assert_eq!(metadata.cell_count, 12);
assert_eq!(metadata.walkable_cell_count, 11);
assert_eq!(metadata.blocked_cell_count, 1);
assert_eq!(metadata.movement_model, "4-way");
assert_eq!(metadata.cost_model, "weighted");
let result = prepared
.search(SearchRequest::new(Point::new(0, 0), Point::new(3, 2)))
.expect("request is valid");
assert!(result.is_found());
assert_eq!(*prepared.metadata(), metadata);
assert!(result.visited_nodes() > 0);
assert!(result.cost().is_some());
}
#[test]
fn static_prepared_grid_queries_an_immutable_snapshot() {
let mut grid = Grid::new(5, 1).expect("grid dimensions are valid");
let request = SearchRequest::new(Point::new(0, 0), Point::new(4, 0));
let prepared = StaticPreparedGrid::builder()
.preprocess(&grid)
.expect("static grid should preprocess");
grid.set_cell(Point::new(2, 0), Cell::Blocked)
.expect("fixture point is in bounds");
let prepared_result = prepared.search(request).expect("request is valid");
let changed_online_result = AStar.search(&grid, request).expect("request is valid");
assert!(prepared_result.is_found());
assert_eq!(prepared_result.cost(), Some(4));
assert!(!changed_online_result.is_found());
}
#[test]
fn static_prepared_grid_matches_astar_on_a_uniform_obstacle_course() {
let mut grid = Grid::new(5, 3).expect("grid dimensions are valid");
grid.set_cell(Point::new(2, 0), Cell::Blocked)
.expect("fixture point is in bounds");
grid.set_cell(Point::new(2, 2), Cell::Blocked)
.expect("fixture point is in bounds");
let request = SearchRequest::new(Point::new(0, 1), Point::new(4, 1));
let prepared = StaticPreparedGrid::builder()
.preprocess(&grid)
.expect("static grid should preprocess");
let prepared_result = prepared.search(request).expect("request is valid");
let astar_result = AStar.search(&grid, request).expect("request is valid");
assert_eq!(prepared_result, astar_result);
assert!(
prepared_result
.path()
.is_some_and(|path| prepared.grid().path_is_walkable(path.steps()))
);
}
}