Skip to main content

terrain_forge/spatial/
pathfinding.rs

1//! Advanced pathfinding algorithms
2
3use crate::{Cell, Grid};
4use std::cmp::Ordering;
5use std::collections::{BinaryHeap, HashMap};
6
7/// Dijkstra map for multi-goal pathfinding
8#[derive(Debug, Clone)]
9pub struct DijkstraMap {
10    costs: Vec<f32>,
11    width: usize,
12    height: usize,
13}
14
15impl DijkstraMap {
16    pub fn new(width: usize, height: usize) -> Self {
17        Self {
18            costs: vec![f32::INFINITY; width * height],
19            width,
20            height,
21        }
22    }
23
24    pub fn get(&self, x: usize, y: usize) -> f32 {
25        self.costs[y * self.width + x]
26    }
27
28    pub fn set(&mut self, x: usize, y: usize, cost: f32) {
29        self.costs[y * self.width + x] = cost;
30    }
31
32    pub fn width(&self) -> usize {
33        self.width
34    }
35    pub fn height(&self) -> usize {
36        self.height
37    }
38}
39
40/// Flow field for AI movement
41#[derive(Debug, Clone)]
42pub struct FlowField {
43    directions: Vec<(i32, i32)>,
44    width: usize,
45    height: usize,
46}
47
48impl FlowField {
49    pub fn new(width: usize, height: usize) -> Self {
50        Self {
51            directions: vec![(0, 0); width * height],
52            width,
53            height,
54        }
55    }
56
57    pub fn get_direction(&self, x: usize, y: usize) -> (i32, i32) {
58        self.directions[y * self.width + x]
59    }
60
61    pub fn set_direction(&mut self, x: usize, y: usize, dir: (i32, i32)) {
62        self.directions[y * self.width + x] = dir;
63    }
64
65    pub fn width(&self) -> usize {
66        self.width
67    }
68    pub fn height(&self) -> usize {
69        self.height
70    }
71}
72
73/// Pathfinding constraints
74#[derive(Debug, Clone)]
75pub struct PathfindingConstraints {
76    pub movement_cost: HashMap<(i32, i32), f32>,
77    pub blocked_cells: Vec<(usize, usize)>,
78}
79
80impl Default for PathfindingConstraints {
81    fn default() -> Self {
82        let mut movement_cost = HashMap::new();
83        // Standard 8-directional movement
84        movement_cost.insert((-1, 0), 1.0);
85        movement_cost.insert((1, 0), 1.0);
86        movement_cost.insert((0, -1), 1.0);
87        movement_cost.insert((0, 1), 1.0);
88        movement_cost.insert((-1, -1), 1.414);
89        movement_cost.insert((-1, 1), 1.414);
90        movement_cost.insert((1, -1), 1.414);
91        movement_cost.insert((1, 1), 1.414);
92
93        Self {
94            movement_cost,
95            blocked_cells: Vec::new(),
96        }
97    }
98}
99
100#[derive(Debug, PartialEq)]
101struct Node {
102    cost: f32,
103    x: usize,
104    y: usize,
105}
106
107impl Eq for Node {}
108
109impl Ord for Node {
110    fn cmp(&self, other: &Self) -> Ordering {
111        other
112            .cost
113            .partial_cmp(&self.cost)
114            .unwrap_or(Ordering::Equal)
115    }
116}
117
118impl PartialOrd for Node {
119    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
120        Some(self.cmp(other))
121    }
122}
123
124/// Generate Dijkstra map from multiple goals
125pub fn dijkstra_map<C: Cell>(
126    grid: &Grid<C>,
127    goals: &[(usize, usize)],
128    constraints: &PathfindingConstraints,
129) -> DijkstraMap {
130    let mut map = DijkstraMap::new(grid.width(), grid.height());
131    let mut heap = BinaryHeap::new();
132
133    // Initialize goals with cost 0
134    for &(x, y) in goals {
135        map.set(x, y, 0.0);
136        heap.push(Node { cost: 0.0, x, y });
137    }
138
139    while let Some(Node { cost, x, y }) = heap.pop() {
140        if cost > map.get(x, y) {
141            continue;
142        }
143
144        for (&(dx, dy), &move_cost) in &constraints.movement_cost {
145            let nx = x as i32 + dx;
146            let ny = y as i32 + dy;
147
148            if nx >= 0 && ny >= 0 && (nx as usize) < grid.width() && (ny as usize) < grid.height() {
149                let nx = nx as usize;
150                let ny = ny as usize;
151
152                if constraints.blocked_cells.contains(&(nx, ny)) {
153                    continue;
154                }
155
156                if let Some(cell) = grid.get(nx as i32, ny as i32) {
157                    if !cell.is_passable() {
158                        continue;
159                    }
160                }
161
162                let new_cost = cost + move_cost;
163                if new_cost < map.get(nx, ny) {
164                    map.set(nx, ny, new_cost);
165                    heap.push(Node {
166                        cost: new_cost,
167                        x: nx,
168                        y: ny,
169                    });
170                }
171            }
172        }
173    }
174
175    map
176}
177
178/// Find a shortest path between two points using a Dijkstra cost map.
179pub fn shortest_path<C: Cell>(
180    grid: &Grid<C>,
181    start: (usize, usize),
182    end: (usize, usize),
183    constraints: &PathfindingConstraints,
184) -> Option<Vec<(usize, usize)>> {
185    if !grid.in_bounds(start.0 as i32, start.1 as i32)
186        || !grid.in_bounds(end.0 as i32, end.1 as i32)
187    {
188        return None;
189    }
190
191    if constraints.blocked_cells.contains(&start) || constraints.blocked_cells.contains(&end) {
192        return None;
193    }
194
195    if !grid
196        .get(start.0 as i32, start.1 as i32)
197        .is_some_and(|cell| cell.is_passable())
198    {
199        return None;
200    }
201
202    if !grid
203        .get(end.0 as i32, end.1 as i32)
204        .is_some_and(|cell| cell.is_passable())
205    {
206        return None;
207    }
208
209    let dijkstra = dijkstra_map(grid, &[end], constraints);
210    if dijkstra.get(start.0, start.1) == f32::INFINITY {
211        return None;
212    }
213
214    if start == end {
215        return Some(vec![start]);
216    }
217
218    let mut path = vec![start];
219    let mut current = start;
220    let mut remaining_steps = grid.width() * grid.height();
221
222    while current != end && remaining_steps > 0 {
223        let (x, y) = current;
224        let current_cost = dijkstra.get(x, y);
225        let mut best = None;
226        let mut best_cost = current_cost;
227
228        for &(dx, dy) in constraints.movement_cost.keys() {
229            let nx = x as i32 + dx;
230            let ny = y as i32 + dy;
231            if !grid.in_bounds(nx, ny) {
232                continue;
233            }
234
235            let npos = (nx as usize, ny as usize);
236            if constraints.blocked_cells.contains(&npos) {
237                continue;
238            }
239
240            if !grid.get(nx, ny).is_some_and(|cell| cell.is_passable()) {
241                continue;
242            }
243
244            let cost = dijkstra.get(npos.0, npos.1);
245            if cost < best_cost {
246                best_cost = cost;
247                best = Some(npos);
248            }
249        }
250
251        let next = best?;
252        path.push(next);
253        current = next;
254        remaining_steps = remaining_steps.saturating_sub(1);
255    }
256
257    if current == end {
258        Some(path)
259    } else {
260        None
261    }
262}
263
264/// Generate flow field from Dijkstra map
265pub fn flow_field_from_dijkstra(dijkstra: &DijkstraMap) -> FlowField {
266    let mut flow = FlowField::new(dijkstra.width(), dijkstra.height());
267
268    for y in 0..dijkstra.height() {
269        for x in 0..dijkstra.width() {
270            let current_cost = dijkstra.get(x, y);
271            if current_cost == f32::INFINITY {
272                continue;
273            }
274
275            let mut best_dir = (0, 0);
276            let mut best_cost = current_cost;
277
278            for dx in -1..=1 {
279                for dy in -1..=1 {
280                    if dx == 0 && dy == 0 {
281                        continue;
282                    }
283
284                    let nx = x as i32 + dx;
285                    let ny = y as i32 + dy;
286
287                    if nx >= 0
288                        && ny >= 0
289                        && (nx as usize) < dijkstra.width()
290                        && (ny as usize) < dijkstra.height()
291                    {
292                        let neighbor_cost = dijkstra.get(nx as usize, ny as usize);
293                        if neighbor_cost < best_cost {
294                            best_cost = neighbor_cost;
295                            best_dir = (dx, dy);
296                        }
297                    }
298                }
299            }
300
301            flow.set_direction(x, y, best_dir);
302        }
303    }
304
305    flow
306}