1use std::{cmp::Ordering, collections::BinaryHeap};
20
21use crate::{
22 grid::Grid,
23 path::Path,
24 search::{BudgetWatch, Pathfinder, SearchRequest, SearchResult},
25};
26
27#[derive(Debug, Default, Clone, Copy)]
33pub struct Dijkstra;
34
35impl Pathfinder for Dijkstra {
36 fn name(&self) -> &'static str {
37 "dijkstra"
38 }
39
40 fn search(&self, grid: &Grid, request: SearchRequest) -> SearchResult {
41 crate::search::validate_request(grid, request)?;
42 let Some(start_index) = grid.index_of(request.start) else {
43 return crate::search::not_found(0);
44 };
45 let Some(goal_index) = grid.index_of(request.goal) else {
46 return crate::search::not_found(0);
47 };
48
49 if !grid.is_walkable(request.start) || !grid.is_walkable(request.goal) {
50 return crate::search::not_found(0);
51 }
52
53 if !grid.is_reachable(request.start, request.goal) {
54 return crate::search::not_found(0);
55 }
56
57 if request.start == request.goal {
58 return crate::search::found(
59 Path::from_steps(vec![request.start]).expect("path contains at least one point"),
60 1,
61 );
62 }
63
64 let mut frontier = BinaryHeap::from([FrontierEntry {
65 cost_so_far: 0,
66 index: start_index,
67 }]);
68 let mut best_costs = vec![None; grid.cell_count()];
69 let mut parents = vec![None; grid.cell_count()];
70 let mut visited_nodes = 0usize;
71 let watch = BudgetWatch::start(request.budget);
72
73 best_costs[start_index] = Some(0);
74
75 while let Some(entry) = frontier.pop() {
76 if best_costs[entry.index] != Some(entry.cost_so_far) {
77 continue;
78 }
79
80 visited_nodes += 1;
81 if entry.index == goal_index {
82 break;
83 }
84
85 if let Err(reason) = watch.check(visited_nodes) {
86 return Err(crate::search::budget_error(reason));
87 }
88
89 let current = grid.point_from_index(entry.index);
90 for neighbor in grid.neighbors4(current) {
91 let neighbor_index = grid
92 .index_of(neighbor)
93 .expect("walkable neighbors must exist inside the grid");
94 let edge_cost = grid
95 .traversal_cost(neighbor)
96 .expect("walkable neighbors must have a traversal cost");
97 let Some(next_cost) = entry.cost_so_far.checked_add(edge_cost) else {
98 continue;
99 };
100
101 if best_costs[neighbor_index].is_some_and(|best_cost| next_cost >= best_cost) {
102 continue;
103 }
104
105 best_costs[neighbor_index] = Some(next_cost);
106 parents[neighbor_index] = Some(entry.index);
107 frontier.push(FrontierEntry {
108 cost_so_far: next_cost,
109 index: neighbor_index,
110 });
111 }
112 }
113
114 let Some(goal_cost) = best_costs[goal_index] else {
115 return crate::search::not_found(visited_nodes);
116 };
117
118 crate::search::found(
119 reconstruct_path(grid, &parents, start_index, goal_index, goal_cost),
120 visited_nodes,
121 )
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126struct FrontierEntry {
127 cost_so_far: usize,
128 index: usize,
129}
130
131impl Ord for FrontierEntry {
132 fn cmp(&self, other: &Self) -> Ordering {
133 other
134 .cost_so_far
135 .cmp(&self.cost_so_far)
136 .then_with(|| other.index.cmp(&self.index))
137 }
138}
139
140impl PartialOrd for FrontierEntry {
141 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
142 Some(self.cmp(other))
143 }
144}
145
146fn reconstruct_path(
147 grid: &Grid,
148 parents: &[Option<usize>],
149 start_index: usize,
150 goal_index: usize,
151 total_cost: usize,
152) -> Path {
153 let mut current_index = goal_index;
154 let mut steps = vec![grid.point_from_index(goal_index)];
155
156 while let Some(parent_index) = parents[current_index] {
157 steps.push(grid.point_from_index(parent_index));
158 current_index = parent_index;
159 }
160
161 steps.reverse();
162 debug_assert_eq!(current_index, start_index);
163 Path::from_steps_with_cost(steps, total_cost).expect("path contains at least one point")
164}
165
166#[cfg(test)]
167mod tests {
168 use crate::{
169 algorithms::dijkstra::Dijkstra,
170 grid::{Cell, Grid},
171 point::Point,
172 search::{Pathfinder, SearchRequest},
173 };
174
175 #[test]
176 fn finds_a_shortest_path_through_the_only_gap() {
177 let mut grid = Grid::new(5, 5).expect("grid dimensions are valid");
178 for y in 0..5 {
179 if y != 2 {
180 grid.set_cell(Point::new(2, y), Cell::Blocked)
181 .expect("valid grid edit");
182 }
183 }
184
185 let dijkstra = Dijkstra;
186 let result = dijkstra.search(
187 &grid,
188 SearchRequest::new(Point::new(0, 0), Point::new(4, 4)),
189 );
190
191 assert!(result.as_ref().expect("valid search request").is_found());
192 assert_eq!(
193 result.as_ref().expect("valid search request").cost(),
194 Some(8)
195 );
196 let path = result
197 .as_ref()
198 .expect("valid search request")
199 .path()
200 .expect("path should exist");
201 assert!(path.steps().contains(&Point::new(2, 2)));
202 }
203
204 #[test]
205 fn reports_when_no_path_exists() {
206 let mut grid = Grid::new(3, 3).expect("grid dimensions are valid");
207 for x in 0..3 {
208 grid.set_cell(Point::new(x, 1), Cell::Blocked)
209 .expect("valid grid edit");
210 }
211
212 let dijkstra = Dijkstra;
213 let result = dijkstra.search(
214 &grid,
215 SearchRequest::new(Point::new(0, 0), Point::new(2, 2)),
216 );
217
218 assert!(!result.as_ref().expect("valid search request").is_found());
219 assert_eq!(result.as_ref().expect("valid search request").cost(), None);
220 assert!(
221 result
222 .as_ref()
223 .expect("valid search request")
224 .stats()
225 .visited_nodes
226 > 0
227 );
228 }
229
230 #[test]
231 fn prefers_a_cheaper_weighted_detour() {
232 let mut grid = Grid::new(5, 3).expect("grid dimensions are valid");
233 assert_eq!(grid.set_traversal_cost(Point::new(1, 1), 5), Ok(()));
234 assert_eq!(grid.set_traversal_cost(Point::new(2, 1), 5), Ok(()));
235 assert_eq!(grid.set_traversal_cost(Point::new(3, 1), 5), Ok(()));
236
237 let dijkstra = Dijkstra;
238 let result = dijkstra.search(
239 &grid,
240 SearchRequest::new(Point::new(0, 1), Point::new(4, 1)),
241 );
242
243 assert!(result.as_ref().expect("valid search request").is_found());
244 assert_eq!(
245 result.as_ref().expect("valid search request").cost(),
246 Some(6)
247 );
248 let path = result
249 .as_ref()
250 .expect("valid search request")
251 .path()
252 .expect("path should exist");
253 assert!(
254 path.steps().contains(&Point::new(0, 0)) || path.steps().contains(&Point::new(0, 2))
255 );
256 assert_eq!(path.cost(), 6);
257 }
258
259 #[test]
260 fn supports_maximum_single_edge_cost() {
261 let mut grid = Grid::new(2, 1).expect("grid dimensions are valid");
262 assert_eq!(
263 grid.set_traversal_cost(Point::new(1, 0), usize::MAX),
264 Ok(())
265 );
266
267 let result = Dijkstra.search(
268 &grid,
269 SearchRequest::new(Point::new(0, 0), Point::new(1, 0)),
270 );
271
272 assert!(result.as_ref().expect("valid search request").is_found());
273 assert_eq!(
274 result.as_ref().expect("valid search request").cost(),
275 Some(usize::MAX)
276 );
277 assert_eq!(
278 result
279 .as_ref()
280 .expect("valid search request")
281 .path()
282 .expect("path should exist")
283 .cost(),
284 usize::MAX
285 );
286 }
287}