1use crate::{
11 AStar, Grid, HierarchicalGridBuildError, Pathfinder, Point,
12 search::{SearchRequest, SearchResult},
13};
14
15pub trait PreprocessedGridBuilder {
20 type Map: PreparedGridSearch;
22
23 fn name(&self) -> &'static str;
25
26 fn preprocess(&self, grid: &Grid) -> Result<Self::Map, PreprocessedGridBuildError>;
33}
34
35pub trait PreparedGridSearch {
41 fn name(&self) -> &'static str;
43
44 fn grid(&self) -> &Grid;
46
47 fn metadata(&self) -> &PreprocessedGridMetadata;
49
50 fn search(&self, request: SearchRequest) -> SearchResult;
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct PreprocessedGridMetadata {
60 pub builder_name: &'static str,
62 pub query_algorithm: &'static str,
64 pub width: usize,
65 pub height: usize,
66 pub cell_count: usize,
67 pub walkable_cell_count: usize,
68 pub blocked_cell_count: usize,
69 pub movement_model: &'static str,
71 pub cost_model: &'static str,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
77#[non_exhaustive]
78pub enum PreprocessedGridBuildError {
79 #[error(transparent)]
81 Hierarchical(#[from] HierarchicalGridBuildError),
82 #[error("{algorithm} requires uniform traversal cost 1; cell {point:?} has cost {cost}")]
84 NonUniformCost {
85 algorithm: &'static str,
86 point: Point,
87 cost: usize,
88 },
89}
90
91#[derive(Debug, Clone, Copy, Default)]
96pub struct StaticPreparedGridBuilder;
97
98impl StaticPreparedGridBuilder {
99 #[must_use]
101 pub const fn new() -> Self {
102 Self
103 }
104}
105
106impl PreprocessedGridBuilder for StaticPreparedGridBuilder {
107 type Map = StaticPreparedGrid;
108
109 fn name(&self) -> &'static str {
110 "static-prepared-grid"
111 }
112
113 fn preprocess(&self, grid: &Grid) -> Result<Self::Map, PreprocessedGridBuildError> {
114 Ok(StaticPreparedGrid {
115 grid: grid.clone(),
116 metadata: metadata_for_grid(grid, self.name(), AStar.name()),
117 })
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct StaticPreparedGrid {
127 grid: Grid,
128 metadata: PreprocessedGridMetadata,
129}
130
131impl StaticPreparedGrid {
132 #[must_use]
134 pub fn builder() -> StaticPreparedGridBuilder {
135 StaticPreparedGridBuilder
136 }
137}
138
139impl PreparedGridSearch for StaticPreparedGrid {
140 fn name(&self) -> &'static str {
141 self.metadata.builder_name
142 }
143
144 fn grid(&self) -> &Grid {
145 &self.grid
146 }
147
148 fn metadata(&self) -> &PreprocessedGridMetadata {
149 &self.metadata
150 }
151
152 fn search(&self, request: SearchRequest) -> SearchResult {
153 AStar.search(&self.grid, request)
154 }
155}
156
157pub(crate) fn metadata_for_grid(
163 grid: &Grid,
164 builder_name: &'static str,
165 query_algorithm: &'static str,
166) -> PreprocessedGridMetadata {
167 let mut walkable_cell_count = 0usize;
168 let mut weighted = false;
169
170 for y in 0..grid.height() {
171 for x in 0..grid.width() {
172 let point = Point::new(x, y);
173 if let Some(cost) = grid.traversal_cost(point) {
174 walkable_cell_count += 1;
175 weighted |= cost != 1;
176 }
177 }
178 }
179
180 let cell_count = grid.cell_count();
181 PreprocessedGridMetadata {
182 builder_name,
183 query_algorithm,
184 width: grid.width(),
185 height: grid.height(),
186 cell_count,
187 walkable_cell_count,
188 blocked_cell_count: cell_count - walkable_cell_count,
189 movement_model: "4-way",
190 cost_model: if weighted { "weighted" } else { "uniform" },
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::{Cell, Pathfinder};
198
199 #[test]
200 fn static_prepared_grid_records_build_metadata_separately_from_query_stats() {
201 let mut grid = Grid::new(4, 3).expect("grid dimensions are valid");
202 grid.set_cell(Point::new(1, 1), Cell::Blocked)
203 .expect("fixture point is in bounds");
204 grid.set_traversal_cost(Point::new(2, 0), 3)
205 .expect("traversal cost is positive");
206
207 let prepared = StaticPreparedGrid::builder()
208 .preprocess(&grid)
209 .expect("static grid should preprocess");
210 let metadata = *prepared.metadata();
211
212 assert_eq!(metadata.builder_name, "static-prepared-grid");
213 assert_eq!(metadata.query_algorithm, "astar");
214 assert_eq!(metadata.width, 4);
215 assert_eq!(metadata.height, 3);
216 assert_eq!(metadata.cell_count, 12);
217 assert_eq!(metadata.walkable_cell_count, 11);
218 assert_eq!(metadata.blocked_cell_count, 1);
219 assert_eq!(metadata.movement_model, "4-way");
220 assert_eq!(metadata.cost_model, "weighted");
221
222 let result = prepared
223 .search(SearchRequest::new(Point::new(0, 0), Point::new(3, 2)))
224 .expect("request is valid");
225 assert!(result.is_found());
226 assert_eq!(*prepared.metadata(), metadata);
227 assert!(result.visited_nodes() > 0);
228 assert!(result.cost().is_some());
229 }
230
231 #[test]
232 fn static_prepared_grid_queries_an_immutable_snapshot() {
233 let mut grid = Grid::new(5, 1).expect("grid dimensions are valid");
234 let request = SearchRequest::new(Point::new(0, 0), Point::new(4, 0));
235 let prepared = StaticPreparedGrid::builder()
236 .preprocess(&grid)
237 .expect("static grid should preprocess");
238
239 grid.set_cell(Point::new(2, 0), Cell::Blocked)
240 .expect("fixture point is in bounds");
241
242 let prepared_result = prepared.search(request).expect("request is valid");
243 let changed_online_result = AStar.search(&grid, request).expect("request is valid");
244
245 assert!(prepared_result.is_found());
246 assert_eq!(prepared_result.cost(), Some(4));
247 assert!(!changed_online_result.is_found());
248 }
249
250 #[test]
251 fn static_prepared_grid_matches_astar_on_a_uniform_obstacle_course() {
252 let mut grid = Grid::new(5, 3).expect("grid dimensions are valid");
253 grid.set_cell(Point::new(2, 0), Cell::Blocked)
254 .expect("fixture point is in bounds");
255 grid.set_cell(Point::new(2, 2), Cell::Blocked)
256 .expect("fixture point is in bounds");
257 let request = SearchRequest::new(Point::new(0, 1), Point::new(4, 1));
258
259 let prepared = StaticPreparedGrid::builder()
260 .preprocess(&grid)
261 .expect("static grid should preprocess");
262 let prepared_result = prepared.search(request).expect("request is valid");
263 let astar_result = AStar.search(&grid, request).expect("request is valid");
264
265 assert_eq!(prepared_result, astar_result);
266 assert!(
267 prepared_result
268 .path()
269 .is_some_and(|path| prepared.grid().path_is_walkable(path.steps()))
270 );
271 }
272}