condor_grid/algorithms/
bfs.rs1use std::collections::VecDeque;
9
10use crate::{
11 grid::Grid,
12 path::Path,
13 search::{BudgetWatch, Pathfinder, SearchRequest, SearchResult},
14};
15
16#[derive(Debug, Default, Clone, Copy)]
21pub struct Bfs;
22
23impl Pathfinder for Bfs {
24 fn name(&self) -> &'static str {
25 "bfs"
26 }
27
28 fn search(&self, grid: &Grid, request: SearchRequest) -> SearchResult {
29 crate::search::validate_request(grid, request)?;
30 let Some(start_index) = grid.index_of(request.start) else {
31 return crate::search::not_found(0);
32 };
33 let Some(goal_index) = grid.index_of(request.goal) else {
34 return crate::search::not_found(0);
35 };
36
37 if !grid.is_walkable(request.start) || !grid.is_walkable(request.goal) {
38 return crate::search::not_found(0);
39 }
40
41 if !grid.is_reachable(request.start, request.goal) {
42 return crate::search::not_found(0);
43 }
44
45 if request.start == request.goal {
46 return crate::search::found(
47 Path::from_steps(vec![request.start]).expect("path contains at least one point"),
48 1,
49 );
50 }
51
52 let mut frontier = VecDeque::from([start_index]);
53 let mut discovered = vec![false; grid.cell_count()];
54 let mut parents = vec![None; grid.cell_count()];
55 let mut visited_nodes = 0;
56 let watch = BudgetWatch::start(request.budget);
57
58 discovered[start_index] = true;
59
60 while let Some(current_index) = frontier.pop_front() {
61 visited_nodes += 1;
62
63 if current_index == goal_index {
64 break;
65 }
66
67 if let Err(reason) = watch.check(visited_nodes) {
68 return Err(crate::search::budget_error(reason));
69 }
70
71 let current = grid.point_from_index(current_index);
72 for neighbor in grid.neighbors4(current) {
73 let neighbor_index = grid
74 .index_of(neighbor)
75 .expect("walkable neighbors must exist inside the grid");
76
77 if discovered[neighbor_index] {
78 continue;
79 }
80
81 discovered[neighbor_index] = true;
82 parents[neighbor_index] = Some(current_index);
83 frontier.push_back(neighbor_index);
84 }
85 }
86
87 if !discovered[goal_index] {
88 return crate::search::not_found(visited_nodes);
89 }
90
91 crate::search::found(
92 reconstruct_path(grid, &parents, start_index, goal_index),
93 visited_nodes,
94 )
95 }
96}
97
98fn reconstruct_path(
99 grid: &Grid,
100 parents: &[Option<usize>],
101 start_index: usize,
102 goal_index: usize,
103) -> Path {
104 let mut current_index = goal_index;
105 let mut steps = vec![grid.point_from_index(goal_index)];
106
107 while current_index != start_index {
108 current_index =
109 parents[current_index].expect("a discovered goal must have a complete parent chain");
110 steps.push(grid.point_from_index(current_index));
111 }
112
113 steps.reverse();
114 Path::from_steps(steps).expect("path contains at least one point")
115}
116
117#[cfg(test)]
118mod tests {
119 use crate::{
120 algorithms::bfs::Bfs,
121 grid::{Cell, Grid},
122 point::Point,
123 search::{BudgetExhausted, GridSearchError, Pathfinder, SearchBudget, SearchRequest},
124 };
125
126 #[test]
127 fn expansion_budget_stops_before_goal() {
128 let grid = Grid::new(5, 1).expect("grid dimensions are valid");
129 let request = SearchRequest::new(Point::new(0, 0), Point::new(4, 0))
130 .with_budget(SearchBudget::max_expansions(2));
131 let error = Bfs
132 .search(&grid, request)
133 .expect_err("budget should exhaust on a long corridor");
134 assert_eq!(
135 error,
136 GridSearchError::BudgetExhausted(BudgetExhausted::Expansions {
137 limit: 2,
138 expansions: 2
139 })
140 );
141 }
142
143 #[test]
144 fn unlimited_budget_finds_path() {
145 let grid = Grid::new(5, 1).expect("grid dimensions are valid");
146 let request = SearchRequest::new(Point::new(0, 0), Point::new(4, 0));
147 let result = Bfs.search(&grid, request).expect("request is valid");
148 assert!(result.is_found());
149 }
150
151 #[test]
152 fn finds_a_shortest_path_through_the_only_gap() {
153 let mut grid = Grid::new(5, 5).expect("grid dimensions are valid");
154 for y in 0..5 {
155 if y != 2 {
156 grid.set_cell(Point::new(2, y), Cell::Blocked)
157 .expect("valid grid edit");
158 }
159 }
160
161 let bfs = Bfs;
162 let result = bfs.search(
163 &grid,
164 SearchRequest::new(Point::new(0, 0), Point::new(4, 4)),
165 );
166
167 assert!(result.as_ref().expect("valid search request").is_found());
168 assert_eq!(
169 result.as_ref().expect("valid search request").cost(),
170 Some(8)
171 );
172
173 let path = result
174 .as_ref()
175 .expect("valid search request")
176 .path()
177 .expect("path should exist");
178 assert_eq!(path.start(), Point::new(0, 0));
179 assert_eq!(path.goal(), Point::new(4, 4));
180 assert!(path.steps().contains(&Point::new(2, 2)));
181 }
182
183 #[test]
184 fn reports_when_no_path_exists() {
185 let mut grid = Grid::new(3, 3).expect("grid dimensions are valid");
186 for x in 0..3 {
187 grid.set_cell(Point::new(x, 1), Cell::Blocked)
188 .expect("valid grid edit");
189 }
190
191 let bfs = Bfs;
192 let result = bfs.search(
193 &grid,
194 SearchRequest::new(Point::new(0, 0), Point::new(2, 2)),
195 );
196
197 assert!(!result.as_ref().expect("valid search request").is_found());
198 assert_eq!(result.as_ref().expect("valid search request").cost(), None);
199 assert!(
200 result
201 .as_ref()
202 .expect("valid search request")
203 .stats()
204 .visited_nodes
205 > 0
206 );
207 }
208}