condor_grid/grid/
reachability.rs1use std::collections::VecDeque;
9
10use super::Grid;
11use crate::Point;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct GridReachabilityIndex {
16 width: usize,
17 height: usize,
18 components: Vec<Option<u32>>,
19 component_count: u32,
20}
21
22impl GridReachabilityIndex {
23 #[must_use]
28 pub fn from_grid(grid: &Grid) -> Self {
29 let width = grid.width();
30 let height = grid.height();
31 let mut components = vec![None; width * height];
32 let mut component_count = 0;
33
34 for y in 0..height {
35 for x in 0..width {
36 let point = Point::new(x, y);
37 let index = grid.index_of(point).unwrap();
38
39 if grid.is_walkable(point) && components[index].is_none() {
40 let component_id = component_count;
41 component_count += 1;
42
43 let mut queue = VecDeque::new();
44 queue.push_back(point);
45 components[index] = Some(component_id);
46
47 while let Some(current) = queue.pop_front() {
48 for neighbor in grid.neighbors4(current) {
49 let neighbor_index = grid.index_of(neighbor).unwrap();
50 if components[neighbor_index].is_none() {
51 components[neighbor_index] = Some(component_id);
52 queue.push_back(neighbor);
53 }
54 }
55 }
56 }
57 }
58 }
59
60 Self {
61 width,
62 height,
63 components,
64 component_count,
65 }
66 }
67
68 #[must_use]
70 pub fn component_id(&self, point: Point) -> Option<u32> {
71 if point.x >= self.width || point.y >= self.height {
72 return None;
73 }
74 let index = (point.y * self.width) + point.x;
75 self.components[index]
76 }
77
78 #[must_use]
83 pub fn is_reachable(&self, start: Point, goal: Point) -> bool {
84 let start_comp = self.component_id(start);
85 let goal_comp = self.component_id(goal);
86
87 match (start_comp, goal_comp) {
88 (Some(s), Some(g)) => s == g,
89 _ => false,
90 }
91 }
92
93 #[must_use]
95 pub fn component_count(&self) -> u32 {
96 self.component_count
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::{
104 AStar, Bfs, BidirectionalBfs, Cell, Dijkstra, JumpPointSearch, Pathfinder,
105 RectangularSymmetryReduction, SearchRequest,
106 };
107
108 #[test]
109 fn open_field_has_one_component() {
110 let grid = Grid::new(10, 10).expect("grid dimensions are valid");
111 let index = GridReachabilityIndex::from_grid(&grid);
112 assert_eq!(index.component_count(), 1);
113 assert!(index.is_reachable(Point::new(0, 0), Point::new(9, 9)));
114 }
115
116 #[test]
117 fn blocked_wall_splits_components() {
118 let mut grid = Grid::new(10, 10).expect("grid dimensions are valid");
119 for y in 0..10 {
120 grid.set_cell(Point::new(5, y), Cell::Blocked)
121 .expect("valid grid edit");
122 }
123 let index = GridReachabilityIndex::from_grid(&grid);
124 assert_eq!(index.component_count(), 2);
125 assert!(!index.is_reachable(Point::new(0, 0), Point::new(9, 9)));
126 assert!(index.is_reachable(Point::new(0, 0), Point::new(4, 9)));
127 assert!(index.is_reachable(Point::new(6, 0), Point::new(9, 9)));
128 }
129
130 #[test]
131 fn blocked_cells_have_no_component() {
132 let mut grid = Grid::new(10, 10).expect("grid dimensions are valid");
133 grid.set_cell(Point::new(5, 5), Cell::Blocked)
134 .expect("valid grid edit");
135 let index = GridReachabilityIndex::from_grid(&grid);
136 assert_eq!(index.component_id(Point::new(5, 5)), None);
137 }
138
139 #[test]
140 fn out_of_bounds_points_have_no_component() {
141 let grid = Grid::new(4, 4).expect("grid dimensions are valid");
142 let index = GridReachabilityIndex::from_grid(&grid);
143 assert_eq!(index.component_id(Point::new(4, 0)), None);
144 assert_eq!(index.component_id(Point::new(0, 4)), None);
145 }
146
147 #[test]
148 fn pathfinders_short_circuit_on_disconnected_islands() {
149 let mut grid = Grid::new(10, 10).expect("grid dimensions are valid");
150 for y in 0..10 {
151 grid.set_cell(Point::new(5, y), Cell::Blocked)
152 .expect("wall cell should be valid");
153 }
154 grid.index_reachability();
155
156 let request = SearchRequest::new(Point::new(0, 0), Point::new(9, 9));
157 let algorithms: Vec<Box<dyn Pathfinder>> = vec![
158 Box::new(Bfs),
159 Box::new(AStar),
160 Box::new(BidirectionalBfs),
161 Box::new(Dijkstra),
162 Box::new(JumpPointSearch),
163 Box::new(RectangularSymmetryReduction),
164 ];
165
166 for algorithm in algorithms {
167 let result = algorithm
168 .search(&grid, request)
169 .expect("test request should be valid");
170 assert!(
171 !result.is_found(),
172 "Algorithm {} should not find a path",
173 algorithm.name()
174 );
175 assert_eq!(
176 result.stats().visited_nodes,
177 0,
178 "Algorithm {} should have 0 visited nodes due to short-circuit",
179 algorithm.name()
180 );
181 }
182 }
183
184 #[test]
185 fn pathfinders_work_normally_when_no_index_present() {
186 let mut grid = Grid::new(3, 3).expect("grid dimensions are valid");
187 for x in 0..3 {
188 grid.set_cell(Point::new(x, 1), Cell::Blocked)
189 .expect("wall cell should be valid");
190 }
191
192 let request = SearchRequest::new(Point::new(0, 0), Point::new(2, 2));
193 let result = AStar
194 .search(&grid, request)
195 .expect("test request should be valid");
196
197 assert!(!result.is_found());
198 assert!(result.stats().visited_nodes > 0);
199 }
200}