1pub mod heap;
2pub mod nodemap;
3
4use std::{
5 fmt::{self, Debug},
6 hash::Hash,
7 time::{Duration, Instant},
8};
9
10use num_format::ToFormattedString;
11use tracing::{debug, trace, warn};
12
13use crate::pathfinder::astar::{
14 heap::{PathfinderHeap, WeightedNode},
15 nodemap::NodeMap,
16};
17
18pub struct Path<P, M>
19where
20 P: Eq + Hash + Copy + Debug,
21{
22 pub movements: Vec<Movement<P, M>>,
23 pub is_partial: bool,
24 pub cost: f32,
29}
30
31const COEFFICIENTS: [f32; 7] = [1.5, 2., 2.5, 3., 4., 5., 10.];
34
35const MIN_IMPROVEMENT: f32 = 0.01;
36
37pub fn a_star<P, M, HeuristicFn, SuccessorsFn, SuccessFn>(
42 start: P,
43 heuristic: HeuristicFn,
44 mut successors: SuccessorsFn,
45 success: SuccessFn,
46 min_timeout: PathfinderTimeout,
47 max_timeout: PathfinderTimeout,
48) -> Path<P, M>
49where
50 P: Eq + Hash + Copy + Debug,
51 HeuristicFn: Fn(P) -> f32,
52 SuccessorsFn: FnMut(P) -> Vec<Edge<P, M>>,
53 SuccessFn: Fn(P) -> bool,
54{
55 let start_time = Instant::now();
56
57 let mut open_set = PathfinderHeap::new();
58 open_set.push(WeightedNode {
59 g_score: 0.,
60 f_score: 0.,
61 index: 0,
62 });
63 let mut nodes: NodeMap<P> = NodeMap::default();
64 nodes.insert(
65 start,
66 Node {
67 came_from: u32::MAX,
68 g_score: 0.,
69 },
70 );
71
72 let mut best_paths: [u32; 7] = [0; 7];
73 let mut best_path_scores: [f32; 7] = [heuristic(start); 7];
74
75 let mut num_nodes = 0_usize;
76 let mut num_movements = 0;
77
78 while let Some(WeightedNode { index, g_score, .. }) = open_set.pop() {
79 let (&node, node_data) = nodes.get_index(index).unwrap();
80 if g_score > node_data.g_score {
81 continue;
82 }
83
84 num_nodes += 1;
85
86 if success(node) {
87 let best_path = index;
88 log_perf_info(start_time, num_nodes, num_movements);
89
90 return Path {
91 movements: reconstruct_path(nodes, best_path, successors),
92 is_partial: false,
93 cost: g_score,
94 };
95 }
96
97 for neighbor in successors(node) {
98 let tentative_g_score = g_score + neighbor.cost;
99 let neighbor_heuristic;
101 let neighbor_index;
102
103 num_movements += 1;
104
105 match nodes.entry(neighbor.movement.target) {
106 indexmap::map::Entry::Occupied(mut e) => {
107 if e.get().g_score > tentative_g_score {
108 neighbor_heuristic = heuristic(*e.key());
109 neighbor_index = e.index() as u32;
110 e.insert(Node {
111 came_from: index,
112 g_score: tentative_g_score,
113 });
114 } else {
115 continue;
116 }
117 }
118 indexmap::map::Entry::Vacant(e) => {
119 neighbor_heuristic = heuristic(*e.key());
120 neighbor_index = e.index() as u32;
121 e.insert(Node {
122 came_from: index,
123 g_score: tentative_g_score,
124 });
125 }
126 }
127
128 open_set.push(WeightedNode {
132 index: neighbor_index,
133 g_score: tentative_g_score,
134 f_score: tentative_g_score + neighbor_heuristic,
135 });
136
137 for (coefficient_i, &coefficient) in COEFFICIENTS.iter().enumerate() {
138 let node_score = neighbor_heuristic + tentative_g_score / coefficient;
139 if best_path_scores[coefficient_i] - node_score > MIN_IMPROVEMENT {
140 best_paths[coefficient_i] = neighbor_index;
141 best_path_scores[coefficient_i] = node_score;
142 }
143 }
144 }
145
146 if num_nodes.is_multiple_of(10_000) {
148 let min_timeout_reached = match min_timeout {
149 PathfinderTimeout::Time(max_duration) => start_time.elapsed() >= max_duration,
150 PathfinderTimeout::Nodes(max_nodes) => num_nodes >= max_nodes,
151 };
152
153 if min_timeout_reached {
154 if best_paths[6] != 0 {
156 break;
157 }
158
159 if min_timeout_reached {
160 let max_timeout_reached = match max_timeout {
161 PathfinderTimeout::Time(max_duration) => {
162 start_time.elapsed() >= max_duration
163 }
164 PathfinderTimeout::Nodes(max_nodes) => num_nodes >= max_nodes,
165 };
166
167 if max_timeout_reached {
168 trace!("A* couldn't find a path in time, returning best path");
170 break;
171 }
172 }
173 }
174 }
175 }
176
177 let best_path_idx = determine_best_path_idx(best_paths, 0);
178 log_perf_info(start_time, num_nodes, num_movements);
179 Path {
180 movements: reconstruct_path(nodes, best_paths[best_path_idx], successors),
181 is_partial: true,
182 cost: best_path_scores[best_path_idx],
183 }
184}
185
186fn log_perf_info(start_time: Instant, num_nodes: usize, num_movements: usize) {
187 let elapsed = start_time.elapsed();
188 let elapsed_seconds = elapsed.as_secs_f64();
189 let nodes_per_second = (num_nodes as f64 / elapsed_seconds) as u64;
190 let num_movements_per_second = (num_movements as f64 / elapsed_seconds) as u64;
191 debug!(
192 "Considered {} nodes in {elapsed:?}",
193 num_nodes.to_formatted_string(&num_format::Locale::en)
194 );
195 debug!(
196 "A* ran at {} nodes per second and {} movements per second",
197 nodes_per_second.to_formatted_string(&num_format::Locale::en),
198 num_movements_per_second.to_formatted_string(&num_format::Locale::en),
199 );
200}
201
202fn determine_best_path_idx(best_paths: [u32; 7], start: u32) -> usize {
203 for (i, &node) in best_paths.iter().enumerate() {
206 if node != start {
207 return i;
208 }
209 }
210 warn!("No best node found, returning first node");
211 0
212}
213
214fn reconstruct_path<P, M, SuccessorsFn>(
215 nodes: NodeMap<P>,
216 mut current_index: u32,
217 mut successors: SuccessorsFn,
218) -> Vec<Movement<P, M>>
219where
220 P: Eq + Hash + Copy + Debug,
221 SuccessorsFn: FnMut(P) -> Vec<Edge<P, M>>,
222{
223 let mut path = Vec::new();
224 while let Some((&node_position, node)) = nodes.get_index(current_index) {
225 if node.came_from == u32::MAX {
226 break;
227 }
228 let came_from_position = *nodes.get_index(node.came_from).unwrap().0;
229
230 let mut best_successor = None;
233 let mut best_successor_cost = f32::INFINITY;
234 for successor in successors(came_from_position) {
235 if successor.movement.target == node_position && successor.cost < best_successor_cost {
236 best_successor_cost = successor.cost;
237 best_successor = Some(successor);
238 }
239 }
240 let Some(found_successor) = best_successor else {
241 warn!(
242 "a successor stopped being possible while reconstructing the path, returning empty path"
243 );
244 return vec![];
245 };
246
247 path.push(Movement {
248 target: node_position,
249 data: found_successor.movement.data,
250 });
251
252 current_index = node.came_from;
253 }
254 path.reverse();
255 path
256}
257
258pub struct Node {
259 pub came_from: u32,
260 pub g_score: f32,
261}
262
263#[derive(Clone, Debug)]
264pub struct Edge<P: Hash + Copy, M> {
265 pub movement: Movement<P, M>,
266 pub cost: f32,
267}
268
269pub struct Movement<P: Hash + Copy, M> {
270 pub target: P,
271 pub data: M,
272}
273
274impl<P: Hash + Copy + Debug, M: Debug> Debug for Movement<P, M> {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 f.debug_struct("Movement")
277 .field("target", &self.target)
278 .field("data", &self.data)
279 .finish()
280 }
281}
282impl<P: Hash + Copy + Clone, M: Clone> Clone for Movement<P, M> {
283 fn clone(&self) -> Self {
284 Self {
285 target: self.target,
286 data: self.data.clone(),
287 }
288 }
289}
290
291#[derive(Clone, Copy, Debug, PartialEq)]
299pub enum PathfinderTimeout {
300 Time(Duration),
305 Nodes(usize),
310}
311impl Default for PathfinderTimeout {
312 fn default() -> Self {
313 Self::Time(Duration::from_secs(1))
314 }
315}