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