1use std::collections::VecDeque;
10
11use crate::{
12 grid::Grid,
13 path::Path,
14 search::{BudgetWatch, Pathfinder, SearchRequest, SearchResult},
15};
16
17#[derive(Debug, Default, Clone, Copy)]
22pub struct BidirectionalBfs;
23
24impl Pathfinder for BidirectionalBfs {
25 fn name(&self) -> &'static str {
26 "bidirectional-bfs"
27 }
28
29 fn search(&self, grid: &Grid, request: SearchRequest) -> SearchResult {
30 crate::search::validate_request(grid, request)?;
31 let Some(start_index) = grid.index_of(request.start) else {
32 return crate::search::not_found(0);
33 };
34 let Some(goal_index) = grid.index_of(request.goal) else {
35 return crate::search::not_found(0);
36 };
37
38 if !grid.is_walkable(request.start) || !grid.is_walkable(request.goal) {
39 return crate::search::not_found(0);
40 }
41
42 if !grid.is_reachable(request.start, request.goal) {
43 return crate::search::not_found(0);
44 }
45
46 if request.start == request.goal {
47 return crate::search::found(
48 Path::from_steps(vec![request.start]).expect("path contains at least one point"),
49 1,
50 );
51 }
52
53 let mut start_frontier = VecDeque::from([start_index]);
54 let mut goal_frontier = VecDeque::from([goal_index]);
55 let mut distances_from_start = vec![None; grid.cell_count()];
56 let mut distances_from_goal = vec![None; grid.cell_count()];
57 let mut parents_from_start = vec![None; grid.cell_count()];
58 let mut parents_from_goal = vec![None; grid.cell_count()];
59 let mut settled_any = vec![false; grid.cell_count()];
60 let mut visited_nodes = 0;
61 let mut expand_from_start = true;
62 let watch = BudgetWatch::start(request.budget);
63
64 distances_from_start[start_index] = Some(0);
65 distances_from_goal[goal_index] = Some(0);
66
67 while !start_frontier.is_empty() && !goal_frontier.is_empty() {
68 let meeting = if expand_from_start {
69 expand_frontier(
70 grid,
71 &mut start_frontier,
72 &mut distances_from_start,
73 &distances_from_goal,
74 &mut parents_from_start,
75 &mut settled_any,
76 &mut visited_nodes,
77 )
78 } else {
79 expand_frontier(
80 grid,
81 &mut goal_frontier,
82 &mut distances_from_goal,
83 &distances_from_start,
84 &mut parents_from_goal,
85 &mut settled_any,
86 &mut visited_nodes,
87 )
88 };
89
90 if let Some(meeting_index) = meeting {
91 return crate::search::found(
92 reconstruct_path(
93 grid,
94 &parents_from_start,
95 &parents_from_goal,
96 start_index,
97 goal_index,
98 meeting_index,
99 ),
100 visited_nodes,
101 );
102 }
103
104 if let Err(reason) = watch.check(visited_nodes) {
105 return Err(crate::search::budget_error(reason));
106 }
107
108 expand_from_start = !expand_from_start;
109 }
110
111 crate::search::not_found(visited_nodes)
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116struct MeetingCandidate {
117 index: usize,
118 total_cost: usize,
119}
120
121fn expand_frontier(
122 grid: &Grid,
123 frontier: &mut VecDeque<usize>,
124 distances: &mut [Option<usize>],
125 other_distances: &[Option<usize>],
126 parents: &mut [Option<usize>],
127 settled_any: &mut [bool],
128 visited_nodes: &mut usize,
129) -> Option<usize> {
130 let layer_len = frontier.len();
131 let mut best_meeting = None;
132
133 for _ in 0..layer_len {
134 let current_index = frontier
135 .pop_front()
136 .expect("frontier layer length must match queued entries");
137
138 if !settled_any[current_index] {
139 settled_any[current_index] = true;
140 *visited_nodes += 1;
141 }
142
143 let current_distance =
144 distances[current_index].expect("queued nodes must have a known distance");
145 if let Some(other_distance) = other_distances[current_index] {
146 update_meeting_candidate(
147 &mut best_meeting,
148 current_index,
149 current_distance + other_distance,
150 );
151 }
152
153 let current = grid.point_from_index(current_index);
154 for neighbor in grid.neighbors4(current) {
155 let neighbor_index = grid
156 .index_of(neighbor)
157 .expect("walkable neighbors must exist inside the grid");
158
159 if distances[neighbor_index].is_some() {
160 continue;
161 }
162
163 let next_distance = current_distance + 1;
164 distances[neighbor_index] = Some(next_distance);
165 parents[neighbor_index] = Some(current_index);
166 frontier.push_back(neighbor_index);
167
168 if let Some(other_distance) = other_distances[neighbor_index] {
169 update_meeting_candidate(
170 &mut best_meeting,
171 neighbor_index,
172 next_distance + other_distance,
173 );
174 }
175 }
176 }
177
178 best_meeting.map(|candidate| candidate.index)
179}
180
181fn update_meeting_candidate(
182 best_meeting: &mut Option<MeetingCandidate>,
183 index: usize,
184 total_cost: usize,
185) {
186 let should_replace = best_meeting
187 .is_none_or(|current| (total_cost, index) < (current.total_cost, current.index));
188
189 if should_replace {
190 *best_meeting = Some(MeetingCandidate { index, total_cost });
191 }
192}
193
194fn reconstruct_path(
195 grid: &Grid,
196 parents_from_start: &[Option<usize>],
197 parents_from_goal: &[Option<usize>],
198 start_index: usize,
199 goal_index: usize,
200 meeting_index: usize,
201) -> Path {
202 let mut steps = vec![grid.point_from_index(meeting_index)];
203
204 let mut current_index = meeting_index;
205 while current_index != start_index {
206 current_index = parents_from_start[current_index]
207 .expect("meeting node must have a complete start-side parent chain");
208 steps.push(grid.point_from_index(current_index));
209 }
210 steps.reverse();
211
212 current_index = meeting_index;
213 while current_index != goal_index {
214 current_index = parents_from_goal[current_index]
215 .expect("meeting node must have a complete goal-side parent chain");
216 steps.push(grid.point_from_index(current_index));
217 }
218
219 Path::from_steps(steps).expect("path contains at least one point")
220}
221
222#[cfg(test)]
223mod tests {
224 use crate::{
225 algorithms::bidirectional_bfs::BidirectionalBfs,
226 grid::{Cell, Grid},
227 point::Point,
228 search::{Pathfinder, SearchRequest},
229 };
230
231 #[test]
232 fn finds_a_shortest_path_through_the_only_gap() {
233 let mut grid = Grid::new(5, 5).expect("grid dimensions are valid");
234 for y in 0..5 {
235 if y != 2 {
236 grid.set_cell(Point::new(2, y), Cell::Blocked)
237 .expect("valid grid edit");
238 }
239 }
240
241 let bidirectional_bfs = BidirectionalBfs;
242 let result = bidirectional_bfs.search(
243 &grid,
244 SearchRequest::new(Point::new(0, 0), Point::new(4, 4)),
245 );
246
247 assert!(result.as_ref().expect("valid search request").is_found());
248 assert_eq!(
249 result.as_ref().expect("valid search request").cost(),
250 Some(8)
251 );
252
253 let path = result
254 .as_ref()
255 .expect("valid search request")
256 .path()
257 .expect("path should exist");
258 assert_eq!(path.start(), Point::new(0, 0));
259 assert_eq!(path.goal(), Point::new(4, 4));
260 assert!(path.steps().contains(&Point::new(2, 2)));
261 }
262
263 #[test]
264 fn reports_when_no_path_exists() {
265 let mut grid = Grid::new(3, 3).expect("grid dimensions are valid");
266 for x in 0..3 {
267 grid.set_cell(Point::new(x, 1), Cell::Blocked)
268 .expect("valid grid edit");
269 }
270
271 let bidirectional_bfs = BidirectionalBfs;
272 let result = bidirectional_bfs.search(
273 &grid,
274 SearchRequest::new(Point::new(0, 0), Point::new(2, 2)),
275 );
276
277 assert!(!result.as_ref().expect("valid search request").is_found());
278 assert_eq!(result.as_ref().expect("valid search request").cost(), None);
279 assert!(
280 result
281 .as_ref()
282 .expect("valid search request")
283 .stats()
284 .visited_nodes
285 > 0
286 );
287 }
288}