src/pathfinding.rs 10.1 K · 336 lines · raw · history

1 use std::{
2 cmp::Reverse,
3 collections::{BinaryHeap, HashSet, VecDeque},
4 };
5
6 use bevy::prelude::*;
7
8 use crate::world::{CHEAPEST_MOVE_COST, WorldMap};
9
10 const STRAIGHT: u32 = 10;
11 const DIAGONAL: u32 = 14;
12 const MIN_TERRAIN_COST: u32 = CHEAPEST_MOVE_COST;
13
14 const NEIGHBORS: [IVec2; 8] = [
15 IVec2::new(1, 0),
16 IVec2::new(-1, 0),
17 IVec2::new(0, 1),
18 IVec2::new(0, -1),
19 IVec2::new(1, 1),
20 IVec2::new(1, -1),
21 IVec2::new(-1, 1),
22 IVec2::new(-1, -1),
23 ];
24
25 pub fn find_path(map: &WorldMap, start: UVec2, goal: UVec2) -> Option<Vec<UVec2>> {
26 map.move_cost(goal.as_ivec2())?;
27 let goal = goal.as_ivec2();
28 astar(map, start, |p| p == goal, |p| octile(p, goal))
29 }
30
31 pub fn find_path_adjacent(map: &WorldMap, start: UVec2, target: UVec2) -> Option<Vec<UVec2>> {
32 let target = target.as_ivec2();
33 astar(
34 map,
35 start,
36 |p| is_adjacent(p, target),
37 |p| octile(p, target).saturating_sub(DIAGONAL * MIN_TERRAIN_COST),
38 )
39 }
40
41 pub fn is_adjacent(a: IVec2, b: IVec2) -> bool {
42 a != b && (a - b).abs().max_element() == 1
43 }
44
45 pub fn nearest_tile(
46 map: &WorldMap,
47 start: UVec2,
48 max_tiles: usize,
49 mut is_target: impl FnMut(UVec2) -> bool,
50 ) -> Option<UVec2> {
51 let mut seen = HashSet::from([start]);
52 let mut queue = VecDeque::from([start]);
53 while let Some(pos) = queue.pop_front() {
54 if is_target(pos) {
55 return Some(pos);
56 }
57 if seen.len() >= max_tiles {
58 continue;
59 }
60 for step in NEIGHBORS {
61 let next = pos.as_ivec2() + step;
62 if map.move_cost(next).is_some() && seen.insert(next.as_uvec2()) {
63 queue.push_back(next.as_uvec2());
64 }
65 }
66 }
67 None
68 }
69
70 fn astar(
71 map: &WorldMap,
72 start: UVec2,
73 is_goal: impl Fn(IVec2) -> bool,
74 heuristic: impl Fn(IVec2) -> u32,
75 ) -> Option<Vec<UVec2>> {
76 let start = start.as_ivec2();
77 map.move_cost(start)?;
78 if is_goal(start) {
79 return Some(vec![]);
80 }
81
82 let width = map.size.x as usize;
83 let index = |p: IVec2| p.y as usize * width + p.x as usize;
84 let tiles = map.size.element_product() as usize;
85 let mut cost_so_far = vec![u32::MAX; tiles];
86 let mut came_from = vec![None::<IVec2>; tiles];
87 let mut open = BinaryHeap::new();
88
89 cost_so_far[index(start)] = 0;
90 open.push(Reverse((heuristic(start), start.x, start.y)));
91
92 while let Some(Reverse((_, x, y))) = open.pop() {
93 let current = IVec2::new(x, y);
94 if is_goal(current) {
95 return Some(reconstruct(&came_from, index, start, current));
96 }
97 let current_cost = cost_so_far[index(current)];
98 for step in NEIGHBORS {
99 let next = current + step;
100 let Some(terrain_cost) = map.step_cost(current, next) else {
101 continue;
102 };
103 let diagonal = step.x != 0 && step.y != 0;
104 if diagonal
105 && (map.move_cost(current + IVec2::new(step.x, 0)).is_none()
106 || map.move_cost(current + IVec2::new(0, step.y)).is_none())
107 {
108 continue;
109 }
110 let base = if diagonal { DIAGONAL } else { STRAIGHT };
111 let new_cost = current_cost + base * terrain_cost;
112 if new_cost < cost_so_far[index(next)] {
113 cost_so_far[index(next)] = new_cost;
114 came_from[index(next)] = Some(current);
115 open.push(Reverse((new_cost + heuristic(next), next.x, next.y)));
116 }
117 }
118 }
119 None
120 }
121
122 fn octile(a: IVec2, b: IVec2) -> u32 {
123 let d = (a - b).abs();
124 let (min, max) = (d.x.min(d.y) as u32, d.x.max(d.y) as u32);
125 (DIAGONAL * min + STRAIGHT * (max - min)) * MIN_TERRAIN_COST
126 }
127
128 fn reconstruct(
129 came_from: &[Option<IVec2>],
130 index: impl Fn(IVec2) -> usize,
131 start: IVec2,
132 goal: IVec2,
133 ) -> Vec<UVec2> {
134 let mut path = vec![goal.as_uvec2()];
135 let mut current = goal;
136 while let Some(prev) = came_from[index(current)] {
137 if prev == start {
138 break;
139 }
140 path.push(prev.as_uvec2());
141 current = prev;
142 }
143 path.reverse();
144 path
145 }
146
147 #[cfg(test)]
148 mod tests {
149 use super::*;
150 use crate::world::Terrain;
151
152 fn map_from(rows: &[&str]) -> WorldMap {
153 let size = UVec2::new(rows[0].len() as u32, rows.len() as u32);
154 let mut map = WorldMap::filled(size, Terrain::Grass);
155 for (row, line) in rows.iter().enumerate() {
156 let y = size.y - 1 - row as u32;
157 for (x, c) in line.chars().enumerate() {
158 let terrain = match c {
159 '#' => Terrain::Stone,
160 '~' => Terrain::ShallowWater,
161 _ => Terrain::Grass,
162 };
163 map.set(UVec2::new(x as u32, y), terrain);
164 }
165 }
166 map
167 }
168
169 fn is_connected(start: UVec2, path: &[UVec2]) -> bool {
170 std::iter::once(start)
171 .chain(path.iter().copied())
172 .collect::<Vec<_>>()
173 .windows(2)
174 .all(|w| (w[0].as_ivec2() - w[1].as_ivec2()).abs().max_element() == 1)
175 }
176
177 #[test]
178 fn path_excludes_start_and_ends_at_goal() {
179 let map = map_from(&["....."]);
180 let path = find_path(&map, UVec2::new(0, 0), UVec2::new(4, 0)).unwrap();
181 assert_eq!(
182 path,
183 vec![
184 UVec2::new(1, 0),
185 UVec2::new(2, 0),
186 UVec2::new(3, 0),
187 UVec2::new(4, 0)
188 ]
189 );
190 }
191
192 #[test]
193 fn path_to_self_is_empty() {
194 let map = map_from(&["..."]);
195 assert_eq!(find_path(&map, UVec2::ZERO, UVec2::ZERO), Some(vec![]));
196 }
197
198 #[test]
199 fn path_goes_around_walls() {
200 let map = map_from(&[
201 ".....", //
202 ".###.", ".#.#.", "...#.",
203 ]);
204 let start = UVec2::new(2, 1);
205 let goal = UVec2::new(4, 0);
206 let path = find_path(&map, start, goal).unwrap();
207 assert!(is_connected(start, &path));
208 assert_eq!(path.last(), Some(&goal));
209 assert!(path.iter().all(|p| map.move_cost(p.as_ivec2()).is_some()));
210 }
211
212 #[test]
213 fn unreachable_or_impassable_goal_has_no_path() {
214 let map = map_from(&[
215 "..#..", //
216 "..#..",
217 ]);
218 assert_eq!(find_path(&map, UVec2::new(0, 0), UVec2::new(4, 0)), None);
219 assert_eq!(find_path(&map, UVec2::new(0, 0), UVec2::new(2, 0)), None);
220 }
221
222 // Colonists move in straight lines between tile centers, so a diagonal step between two
223 // blocked orthogonal neighbours would visually clip through the corners of walls.
224 #[test]
225 fn diagonal_steps_never_cut_wall_corners() {
226 let map = map_from(&[
227 "#.", //
228 ".#",
229 ]);
230 assert_eq!(find_path(&map, UVec2::new(0, 0), UVec2::new(1, 1)), None);
231 let map = map_from(&[
232 "..", //
233 "#.",
234 ]);
235 let path = find_path(&map, UVec2::new(0, 1), UVec2::new(1, 0)).unwrap();
236 assert_eq!(path, vec![UVec2::new(1, 1), UVec2::new(1, 0)]);
237 }
238
239 #[test]
240 fn prefers_detour_over_slow_terrain() {
241 let map = map_from(&[
242 ".....", //
243 "..~..",
244 ]);
245 let path = find_path(&map, UVec2::new(0, 0), UVec2::new(4, 0)).unwrap();
246 assert!(!path.contains(&UVec2::new(2, 0)));
247 }
248
249 #[test]
250 fn walks_around_a_hill_rather_than_over_it() {
251 let mut map = map_from(&[
252 ".....", //
253 ".....",
254 ]);
255 map.set_height(UVec2::new(2, 0), 10.0);
256 let path = find_path(&map, UVec2::new(0, 0), UVec2::new(4, 0)).unwrap();
257 assert!(!path.contains(&UVec2::new(2, 0)));
258 }
259
260 #[test]
261 fn detours_along_paths_when_they_are_faster() {
262 let mut map = map_from(&[
263 "..........", //
264 "..........",
265 ]);
266 for x in 0..10 {
267 map.set(UVec2::new(x, 1), Terrain::Paved);
268 }
269 let path = find_path(&map, UVec2::new(0, 0), UVec2::new(9, 0)).unwrap();
270 assert!(path.iter().filter(|p| p.y == 1).count() >= 6);
271 }
272
273 #[test]
274 fn crosses_slow_terrain_when_it_is_the_only_way() {
275 let map = map_from(&["..~.."]);
276 let path = find_path(&map, UVec2::new(0, 0), UVec2::new(4, 0)).unwrap();
277 assert!(path.contains(&UVec2::new(2, 0)));
278 }
279
280 #[test]
281 fn path_adjacent_stops_next_to_target() {
282 let map = map_from(&["......"]);
283 let path = find_path_adjacent(&map, UVec2::new(0, 0), UVec2::new(5, 0)).unwrap();
284 assert_eq!(path.last(), Some(&UVec2::new(4, 0)));
285 }
286
287 #[test]
288 fn path_adjacent_works_for_impassable_targets() {
289 let map = map_from(&["...#"]);
290 let path = find_path_adjacent(&map, UVec2::new(0, 0), UVec2::new(3, 0)).unwrap();
291 assert_eq!(path.last(), Some(&UVec2::new(2, 0)));
292 }
293
294 #[test]
295 fn path_adjacent_from_neighbor_is_empty() {
296 let map = map_from(&["..."]);
297 let path = find_path_adjacent(&map, UVec2::new(1, 0), UVec2::new(2, 0));
298 assert_eq!(path, Some(vec![]));
299 }
300
301 #[test]
302 fn path_adjacent_to_enclosed_target_is_none() {
303 let walled = map_from(&[
304 "#####", //
305 "#...#", "#####", ".....",
306 ]);
307 assert_eq!(
308 find_path_adjacent(&walled, UVec2::new(0, 0), UVec2::new(2, 2)),
309 None
310 );
311 }
312
313 #[test]
314 fn nearest_tile_finds_closest_match_by_walking_distance() {
315 let map = map_from(&[
316 ".#...", //
317 ".#.#.", "...#.",
318 ]);
319 let found = nearest_tile(&map, UVec2::new(0, 0), 100, |p| p.x == 4);
320 assert_eq!(found, Some(UVec2::new(4, 2)));
321 assert_eq!(
322 nearest_tile(&map, UVec2::new(0, 0), 100, |p| p.x == 9),
323 None
324 );
325 }
326
327 #[test]
328 fn finds_paths_across_a_generated_world() {
329 let map = crate::worldgen::generate(crate::worldgen::WORLD_SIZE, 1);
330 let region = crate::worldgen::largest_region(&map);
331 let (start, goal) = (region[0], region[region.len() - 1]);
332 let path = find_path(&map, start, goal).unwrap();
333 assert!(is_connected(start, &path));
334 assert_eq!(path.last(), Some(&goal));
335 }
336 }