Skip to main content

proof_engine/ai/
pathfinding.rs

1//! A* pathfinding, Dijkstra maps, Jump Point Search, and hierarchical pathfinding.
2//!
3//! # Example
4//! ```rust
5//! use proof_engine::ai::pathfinding::{PathGrid, AStarPathfinder};
6//! use glam::Vec2;
7//!
8//! let mut grid = PathGrid::new(20, 20, 1.0);
9//! grid.set_walkable(5, 5, false); // obstacle
10//! let finder = AStarPathfinder::new();
11//! if let Some(path) = finder.find_path(&grid, Vec2::new(0.0, 0.0), Vec2::new(10.0, 10.0)) {
12//!     println!("Found path with {} waypoints", path.len());
13//! }
14//! ```
15
16use glam::Vec2;
17use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
18use std::cmp::Ordering;
19
20// ---------------------------------------------------------------------------
21// Core grid types
22// ---------------------------------------------------------------------------
23
24/// A single node used internally during A* search.
25#[derive(Debug, Clone)]
26pub struct PathNode {
27    pub pos: Vec2,
28    pub g_cost: f32,
29    pub h_cost: f32,
30    pub f_cost: f32,
31    pub parent: Option<usize>,
32}
33
34impl PathNode {
35    pub fn new(pos: Vec2, g_cost: f32, h_cost: f32, parent: Option<usize>) -> Self {
36        PathNode {
37            pos,
38            g_cost,
39            h_cost,
40            f_cost: g_cost + h_cost,
41            parent,
42        }
43    }
44}
45
46/// Wrapper so PathNode can be stored in a BinaryHeap (min-heap by f_cost).
47#[derive(Debug, Clone)]
48struct HeapNode {
49    f_cost: f32,
50    g_cost: f32,
51    index: usize,
52}
53
54impl PartialEq for HeapNode {
55    fn eq(&self, other: &Self) -> bool {
56        self.f_cost == other.f_cost
57    }
58}
59impl Eq for HeapNode {}
60impl PartialOrd for HeapNode {
61    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
62        Some(self.cmp(other))
63    }
64}
65impl Ord for HeapNode {
66    fn cmp(&self, other: &Self) -> Ordering {
67        // Reverse for min-heap
68        other.f_cost.partial_cmp(&self.f_cost)
69            .unwrap_or(Ordering::Equal)
70            .then_with(|| other.g_cost.partial_cmp(&self.g_cost).unwrap_or(Ordering::Equal))
71    }
72}
73
74/// A uniform grid used for all grid-based pathfinding algorithms.
75#[derive(Debug, Clone)]
76pub struct PathGrid {
77    pub width: usize,
78    pub height: usize,
79    pub cell_size: f32,
80    pub walkable: Vec<bool>,
81    pub weights: Vec<f32>,
82    pub origin: Vec2,
83}
84
85impl PathGrid {
86    /// Create a new fully-walkable grid.
87    pub fn new(width: usize, height: usize, cell_size: f32) -> Self {
88        let n = width * height;
89        PathGrid {
90            width,
91            height,
92            cell_size,
93            walkable: vec![true; n],
94            weights: vec![1.0; n],
95            origin: Vec2::ZERO,
96        }
97    }
98
99    /// Create grid with a world-space origin offset.
100    pub fn with_origin(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
101        let mut g = Self::new(width, height, cell_size);
102        g.origin = origin;
103        g
104    }
105
106    /// Mark a cell walkable or not.
107    pub fn set_walkable(&mut self, x: usize, y: usize, walkable: bool) {
108        if let Some(idx) = self.index(x, y) {
109            self.walkable[idx] = walkable;
110        }
111    }
112
113    /// Set the traversal weight of a cell (higher = more expensive).
114    pub fn set_weight(&mut self, x: usize, y: usize, weight: f32) {
115        if let Some(idx) = self.index(x, y) {
116            self.weights[idx] = weight.max(0.001);
117        }
118    }
119
120    /// Convert a world position to grid coordinates. Clamps to valid range.
121    pub fn world_to_grid(&self, pos: Vec2) -> (usize, usize) {
122        let local = pos - self.origin;
123        let x = ((local.x / self.cell_size).floor() as isize).clamp(0, self.width as isize - 1) as usize;
124        let y = ((local.y / self.cell_size).floor() as isize).clamp(0, self.height as isize - 1) as usize;
125        (x, y)
126    }
127
128    /// Convert grid coordinates to the world-space centre of that cell.
129    pub fn grid_to_world(&self, x: usize, y: usize) -> Vec2 {
130        self.origin + Vec2::new(
131            x as f32 * self.cell_size + self.cell_size * 0.5,
132            y as f32 * self.cell_size + self.cell_size * 0.5,
133        )
134    }
135
136    /// Check whether a cell is walkable.
137    pub fn is_walkable(&self, x: usize, y: usize) -> bool {
138        match self.index(x, y) {
139            Some(idx) => self.walkable[idx],
140            None => false,
141        }
142    }
143
144    /// Get the movement cost of a cell.
145    pub fn weight(&self, x: usize, y: usize) -> f32 {
146        match self.index(x, y) {
147            Some(idx) => self.weights[idx],
148            None => f32::INFINITY,
149        }
150    }
151
152    /// Returns walkable neighbours of (x, y) together with their movement cost.
153    /// Includes diagonals; diagonal cost is sqrt(2) * avg_weight.
154    pub fn neighbors(&self, x: usize, y: usize) -> Vec<(usize, usize, f32)> {
155        let mut result = Vec::with_capacity(8);
156        let dirs: &[(i32, i32, bool)] = &[
157            (-1,  0, false), (1,  0, false), (0, -1, false), (0, 1, false),
158            (-1, -1, true),  (1, -1, true),  (-1, 1, true),  (1, 1, true),
159        ];
160        for &(dx, dy, diagonal) in dirs {
161            let nx = x as i32 + dx;
162            let ny = y as i32 + dy;
163            if nx < 0 || ny < 0 || nx >= self.width as i32 || ny >= self.height as i32 {
164                continue;
165            }
166            let nx = nx as usize;
167            let ny = ny as usize;
168            if !self.is_walkable(nx, ny) {
169                continue;
170            }
171            // Diagonal movement: both cardinal neighbours must also be walkable
172            if diagonal {
173                if !self.is_walkable(x, ny) || !self.is_walkable(nx, y) {
174                    continue;
175                }
176                let cost = std::f32::consts::SQRT_2 * self.weight(nx, ny);
177                result.push((nx, ny, cost));
178            } else {
179                result.push((nx, ny, self.weight(nx, ny)));
180            }
181        }
182        result
183    }
184
185    /// Cardinal-only neighbours (no diagonals).
186    pub fn neighbors_cardinal(&self, x: usize, y: usize) -> Vec<(usize, usize, f32)> {
187        let mut result = Vec::with_capacity(4);
188        let dirs: &[(i32, i32)] = &[(-1, 0), (1, 0), (0, -1), (0, 1)];
189        for &(dx, dy) in dirs {
190            let nx = x as i32 + dx;
191            let ny = y as i32 + dy;
192            if nx < 0 || ny < 0 || nx >= self.width as i32 || ny >= self.height as i32 {
193                continue;
194            }
195            let nx = nx as usize;
196            let ny = ny as usize;
197            if self.is_walkable(nx, ny) {
198                result.push((nx, ny, self.weight(nx, ny)));
199            }
200        }
201        result
202    }
203
204    /// Line-of-sight check between two grid cells (Bresenham).
205    pub fn line_of_sight(&self, x0: usize, y0: usize, x1: usize, y1: usize) -> bool {
206        let mut x0 = x0 as i32;
207        let mut y0 = y0 as i32;
208        let x1 = x1 as i32;
209        let y1 = y1 as i32;
210        let dx = (x1 - x0).abs();
211        let dy = (y1 - y0).abs();
212        let sx = if x0 < x1 { 1 } else { -1 };
213        let sy = if y0 < y1 { 1 } else { -1 };
214        let mut err = dx - dy;
215        loop {
216            if x0 == x1 && y0 == y1 { break; }
217            if !self.is_walkable(x0 as usize, y0 as usize) {
218                return false;
219            }
220            let e2 = 2 * err;
221            if e2 > -dy { err -= dy; x0 += sx; }
222            if e2 < dx  { err += dx; y0 += sy; }
223        }
224        true
225    }
226
227    fn index(&self, x: usize, y: usize) -> Option<usize> {
228        if x < self.width && y < self.height {
229            Some(y * self.width + x)
230        } else {
231            None
232        }
233    }
234
235    /// Total number of cells.
236    pub fn len(&self) -> usize { self.width * self.height }
237}
238
239// ---------------------------------------------------------------------------
240// Heuristics
241// ---------------------------------------------------------------------------
242
243/// Available heuristic functions for A*.
244#[derive(Debug, Clone, Copy, PartialEq, Default)]
245pub enum Heuristic {
246    #[default]
247    Manhattan,
248    Euclidean,
249    Chebyshev,
250    Octile,
251}
252
253impl Heuristic {
254    pub fn compute(self, ax: usize, ay: usize, bx: usize, by: usize) -> f32 {
255        let dx = (ax as f32 - bx as f32).abs();
256        let dy = (ay as f32 - by as f32).abs();
257        match self {
258            Heuristic::Manhattan  => dx + dy,
259            Heuristic::Euclidean  => (dx * dx + dy * dy).sqrt(),
260            Heuristic::Chebyshev  => dx.max(dy),
261            Heuristic::Octile => {
262                let min = dx.min(dy);
263                let max = dx.max(dy);
264                std::f32::consts::SQRT_2 * min + (max - min)
265            }
266        }
267    }
268}
269
270// ---------------------------------------------------------------------------
271// Path types
272// ---------------------------------------------------------------------------
273
274/// A resolved path as a sequence of world positions.
275#[derive(Debug, Clone, Default)]
276pub struct Path {
277    pub waypoints: Vec<Vec2>,
278    pub total_cost: f32,
279}
280
281impl Path {
282    pub fn new(waypoints: Vec<Vec2>, total_cost: f32) -> Self {
283        Path { waypoints, total_cost }
284    }
285    pub fn is_empty(&self) -> bool { self.waypoints.is_empty() }
286    pub fn len(&self) -> usize { self.waypoints.len() }
287}
288
289/// A pathfinding request.
290#[derive(Debug, Clone)]
291pub struct PathRequest {
292    pub start: Vec2,
293    pub end: Vec2,
294    pub agent_radius: f32,
295    pub heuristic: Heuristic,
296    pub allow_partial: bool,
297}
298
299impl PathRequest {
300    pub fn new(start: Vec2, end: Vec2) -> Self {
301        PathRequest {
302            start,
303            end,
304            agent_radius: 0.0,
305            heuristic: Heuristic::Octile,
306            allow_partial: false,
307        }
308    }
309}
310
311/// The result of a pathfinding operation.
312#[derive(Debug, Clone)]
313pub enum PathResult {
314    Found(Path),
315    Partial(Path),
316    NoPath,
317    InvalidRequest(String),
318}
319
320/// Statistics collected during a pathfinding run.
321#[derive(Debug, Clone, Default)]
322pub struct PathfindingStats {
323    pub nodes_expanded: usize,
324    pub nodes_generated: usize,
325    pub path_length: usize,
326    pub duration_us: u64,
327}
328
329// ---------------------------------------------------------------------------
330// A* Pathfinder
331// ---------------------------------------------------------------------------
332
333/// Full A* pathfinder operating on a `PathGrid`.
334#[derive(Debug, Clone)]
335pub struct AStarPathfinder {
336    pub heuristic: Heuristic,
337    pub allow_diagonal: bool,
338    pub smooth_result: bool,
339}
340
341impl AStarPathfinder {
342    pub fn new() -> Self {
343        AStarPathfinder {
344            heuristic: Heuristic::Octile,
345            allow_diagonal: true,
346            smooth_result: true,
347        }
348    }
349
350    pub fn with_heuristic(mut self, h: Heuristic) -> Self { self.heuristic = h; self }
351    pub fn with_diagonal(mut self, v: bool) -> Self { self.allow_diagonal = v; self }
352    pub fn with_smoothing(mut self, v: bool) -> Self { self.smooth_result = v; self }
353
354    /// Find a path from `start` to `end` in world coordinates.
355    pub fn find_path(&self, grid: &PathGrid, start: Vec2, end: Vec2) -> Option<Vec<Vec2>> {
356        let (sx, sy) = grid.world_to_grid(start);
357        let (ex, ey) = grid.world_to_grid(end);
358
359        if !grid.is_walkable(sx, sy) || !grid.is_walkable(ex, ey) {
360            return None;
361        }
362        if sx == ex && sy == ey {
363            return Some(vec![start, end]);
364        }
365
366        let start_idx = sy * grid.width + sx;
367        let end_idx   = ey * grid.width + ex;
368
369        // g_costs[i] = best known cost to reach cell i
370        let mut g_costs: Vec<f32> = vec![f32::INFINITY; grid.len()];
371        let mut parents: Vec<Option<usize>> = vec![None; grid.len()];
372        let mut open: BinaryHeap<HeapNode> = BinaryHeap::new();
373        let mut closed: HashSet<usize> = HashSet::new();
374
375        g_costs[start_idx] = 0.0;
376        let h = self.heuristic.compute(sx, sy, ex, ey);
377        open.push(HeapNode { f_cost: h, g_cost: 0.0, index: start_idx });
378
379        while let Some(current) = open.pop() {
380            let idx = current.index;
381            if closed.contains(&idx) { continue; }
382            closed.insert(idx);
383
384            if idx == end_idx {
385                // Reconstruct path
386                let mut path_indices = Vec::new();
387                let mut cur = idx;
388                loop {
389                    path_indices.push(cur);
390                    match parents[cur] {
391                        Some(p) => cur = p,
392                        None => break,
393                    }
394                }
395                path_indices.reverse();
396                let mut waypoints: Vec<Vec2> = path_indices.iter().map(|&i| {
397                    let gx = i % grid.width;
398                    let gy = i / grid.width;
399                    grid.grid_to_world(gx, gy)
400                }).collect();
401                // Replace first/last with exact world positions
402                if !waypoints.is_empty() { waypoints[0] = start; }
403                if waypoints.len() > 1 { *waypoints.last_mut().unwrap() = end; }
404
405                if self.smooth_result {
406                    waypoints = smooth_path(waypoints, grid);
407                }
408                return Some(waypoints);
409            }
410
411            let cx = idx % grid.width;
412            let cy = idx / grid.width;
413            let neighbors = if self.allow_diagonal {
414                grid.neighbors(cx, cy)
415            } else {
416                grid.neighbors_cardinal(cx, cy)
417            };
418
419            for (nx, ny, move_cost) in neighbors {
420                let nidx = ny * grid.width + nx;
421                if closed.contains(&nidx) { continue; }
422                let tentative_g = g_costs[idx] + move_cost;
423                if tentative_g < g_costs[nidx] {
424                    g_costs[nidx] = tentative_g;
425                    parents[nidx] = Some(idx);
426                    let h = self.heuristic.compute(nx, ny, ex, ey);
427                    open.push(HeapNode {
428                        f_cost: tentative_g + h,
429                        g_cost: tentative_g,
430                        index: nidx,
431                    });
432                }
433            }
434        }
435        None
436    }
437
438    /// Find a path and return detailed stats.
439    pub fn find_path_detailed(
440        &self,
441        grid: &PathGrid,
442        request: &PathRequest,
443    ) -> (PathResult, PathfindingStats) {
444        let mut stats = PathfindingStats::default();
445        let (sx, sy) = grid.world_to_grid(request.start);
446        let (ex, ey) = grid.world_to_grid(request.end);
447
448        if !grid.is_walkable(sx, sy) || !grid.is_walkable(ex, ey) {
449            return (PathResult::NoPath, stats);
450        }
451
452        let start_idx = sy * grid.width + sx;
453        let end_idx   = ey * grid.width + ex;
454        let mut g_costs: Vec<f32> = vec![f32::INFINITY; grid.len()];
455        let mut parents: Vec<Option<usize>> = vec![None; grid.len()];
456        let mut open: BinaryHeap<HeapNode> = BinaryHeap::new();
457        let mut closed: HashSet<usize> = HashSet::new();
458        // Track best node for partial path
459        let mut best_idx = start_idx;
460        let mut best_h = request.heuristic.compute(sx, sy, ex, ey);
461
462        g_costs[start_idx] = 0.0;
463        open.push(HeapNode { f_cost: best_h, g_cost: 0.0, index: start_idx });
464        stats.nodes_generated += 1;
465
466        while let Some(current) = open.pop() {
467            let idx = current.index;
468            if closed.contains(&idx) { continue; }
469            closed.insert(idx);
470            stats.nodes_expanded += 1;
471
472            let cx = idx % grid.width;
473            let cy = idx / grid.width;
474            let h = request.heuristic.compute(cx, cy, ex, ey);
475            if h < best_h { best_h = h; best_idx = idx; }
476
477            if idx == end_idx {
478                let path = self.reconstruct(grid, &parents, idx, request.start, request.end);
479                stats.path_length = path.len();
480                return (PathResult::Found(Path::new(path, g_costs[idx])), stats);
481            }
482
483            let neighbors = if self.allow_diagonal {
484                grid.neighbors(cx, cy)
485            } else {
486                grid.neighbors_cardinal(cx, cy)
487            };
488            for (nx, ny, move_cost) in neighbors {
489                let nidx = ny * grid.width + nx;
490                if closed.contains(&nidx) { continue; }
491                let tentative_g = g_costs[idx] + move_cost;
492                if tentative_g < g_costs[nidx] {
493                    g_costs[nidx] = tentative_g;
494                    parents[nidx] = Some(idx);
495                    let h2 = request.heuristic.compute(nx, ny, ex, ey);
496                    open.push(HeapNode { f_cost: tentative_g + h2, g_cost: tentative_g, index: nidx });
497                    stats.nodes_generated += 1;
498                }
499            }
500        }
501
502        if request.allow_partial && best_idx != start_idx {
503            let bx = best_idx % grid.width;
504            let by = best_idx / grid.width;
505            let partial_end = grid.grid_to_world(bx, by);
506            let path = self.reconstruct(grid, &parents, best_idx, request.start, partial_end);
507            stats.path_length = path.len();
508            return (PathResult::Partial(Path::new(path, g_costs[best_idx])), stats);
509        }
510
511        (PathResult::NoPath, stats)
512    }
513
514    fn reconstruct(
515        &self,
516        grid: &PathGrid,
517        parents: &[Option<usize>],
518        end_idx: usize,
519        start_world: Vec2,
520        end_world: Vec2,
521    ) -> Vec<Vec2> {
522        let mut indices = Vec::new();
523        let mut cur = end_idx;
524        loop {
525            indices.push(cur);
526            match parents[cur] {
527                Some(p) => cur = p,
528                None => break,
529            }
530        }
531        indices.reverse();
532        let mut waypoints: Vec<Vec2> = indices.iter().map(|&i| {
533            grid.grid_to_world(i % grid.width, i / grid.width)
534        }).collect();
535        if !waypoints.is_empty() { waypoints[0] = start_world; }
536        if waypoints.len() > 1 { *waypoints.last_mut().unwrap() = end_world; }
537        if self.smooth_result {
538            waypoints = smooth_path(waypoints, grid);
539        }
540        waypoints
541    }
542}
543
544impl Default for AStarPathfinder {
545    fn default() -> Self { Self::new() }
546}
547
548// ---------------------------------------------------------------------------
549// Path smoothing
550// ---------------------------------------------------------------------------
551
552/// Remove redundant waypoints using line-of-sight checks (string-pulling).
553pub fn smooth_path(path: Vec<Vec2>, grid: &PathGrid) -> Vec<Vec2> {
554    if path.len() <= 2 { return path; }
555    let mut smoothed = Vec::with_capacity(path.len());
556    smoothed.push(path[0]);
557    let mut current = 0usize;
558    while current < path.len() - 1 {
559        let mut furthest = current + 1;
560        for next in (current + 2)..path.len() {
561            let (ax, ay) = grid.world_to_grid(path[current]);
562            let (bx, by) = grid.world_to_grid(path[next]);
563            if grid.line_of_sight(ax, ay, bx, by) {
564                furthest = next;
565            } else {
566                break;
567            }
568        }
569        smoothed.push(path[furthest]);
570        current = furthest;
571    }
572    smoothed
573}
574
575/// Catmull-Rom spline interpolation for smooth curved paths.
576pub fn spline_path(path: &[Vec2], samples_per_segment: usize) -> Vec<Vec2> {
577    if path.len() < 2 { return path.to_vec(); }
578    let mut result = Vec::new();
579    for i in 0..path.len().saturating_sub(1) {
580        let p0 = if i == 0 { path[0] } else { path[i - 1] };
581        let p1 = path[i];
582        let p2 = path[i + 1];
583        let p3 = if i + 2 < path.len() { path[i + 2] } else { path[i + 1] };
584        for s in 0..samples_per_segment {
585            let t = s as f32 / samples_per_segment as f32;
586            let t2 = t * t;
587            let t3 = t2 * t;
588            let v = 0.5 * (
589                (2.0 * p1)
590                + (-p0 + p2) * t
591                + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
592                + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
593            );
594            result.push(v);
595        }
596    }
597    result.push(*path.last().unwrap());
598    result
599}
600
601// ---------------------------------------------------------------------------
602// Dijkstra Map
603// ---------------------------------------------------------------------------
604
605/// A distance field computed simultaneously from multiple source cells.
606/// Useful for "all enemies chase the player" scenarios — compute once, use many.
607#[derive(Debug, Clone)]
608pub struct DijkstraMap {
609    pub width: usize,
610    pub height: usize,
611    pub distances: Vec<f32>,
612    pub flow: Vec<Option<(usize, usize)>>, // best next step toward nearest goal
613}
614
615impl DijkstraMap {
616    /// Create an empty map of the same dimensions as `grid`.
617    pub fn new(width: usize, height: usize) -> Self {
618        let n = width * height;
619        DijkstraMap {
620            width,
621            height,
622            distances: vec![f32::INFINITY; n],
623            flow: vec![None; n],
624        }
625    }
626
627    /// Build the distance field from a set of goal positions (world coords).
628    pub fn build(&mut self, grid: &PathGrid, goals: &[Vec2]) {
629        self.distances.fill(f32::INFINITY);
630        self.flow.fill(None);
631
632        let mut queue: VecDeque<usize> = VecDeque::new();
633        for &goal in goals {
634            let (gx, gy) = grid.world_to_grid(goal);
635            if grid.is_walkable(gx, gy) {
636                let idx = gy * grid.width + gx;
637                self.distances[idx] = 0.0;
638                queue.push_back(idx);
639            }
640        }
641
642        while let Some(idx) = queue.pop_front() {
643            let cx = idx % grid.width;
644            let cy = idx / grid.width;
645            for (nx, ny, cost) in grid.neighbors(cx, cy) {
646                let nidx = ny * grid.width + nx;
647                let new_dist = self.distances[idx] + cost;
648                if new_dist < self.distances[nidx] {
649                    self.distances[nidx] = new_dist;
650                    self.flow[nidx] = Some((cx, cy));
651                    queue.push_back(nidx);
652                }
653            }
654        }
655    }
656
657    /// Build from grid coordinates directly.
658    pub fn build_from_cells(&mut self, grid: &PathGrid, goals: &[(usize, usize)]) {
659        self.distances.fill(f32::INFINITY);
660        self.flow.fill(None);
661
662        let mut queue: VecDeque<usize> = VecDeque::new();
663        for &(gx, gy) in goals {
664            if grid.is_walkable(gx, gy) {
665                let idx = gy * grid.width + gx;
666                self.distances[idx] = 0.0;
667                queue.push_back(idx);
668            }
669        }
670
671        while let Some(idx) = queue.pop_front() {
672            let cx = idx % grid.width;
673            let cy = idx / grid.width;
674            for (nx, ny, cost) in grid.neighbors(cx, cy) {
675                let nidx = ny * grid.width + nx;
676                let new_dist = self.distances[idx] + cost;
677                if new_dist < self.distances[nidx] {
678                    self.distances[nidx] = new_dist;
679                    self.flow[nidx] = Some((cx, cy));
680                    queue.push_back(nidx);
681                }
682            }
683        }
684    }
685
686    /// Get the distance at a world position.
687    pub fn distance_at(&self, grid: &PathGrid, pos: Vec2) -> f32 {
688        let (x, y) = grid.world_to_grid(pos);
689        self.distances[y * self.width + x]
690    }
691
692    /// Get the best next step from a world position toward the nearest goal.
693    pub fn next_step(&self, grid: &PathGrid, pos: Vec2) -> Option<Vec2> {
694        let (x, y) = grid.world_to_grid(pos);
695        let idx = y * self.width + x;
696        self.flow[idx].map(|(nx, ny)| grid.grid_to_world(nx, ny))
697    }
698
699    /// Extract a full path by following the flow field from `start` to any goal.
700    pub fn extract_path(&self, grid: &PathGrid, start: Vec2) -> Vec<Vec2> {
701        let mut path = Vec::new();
702        let (mut cx, mut cy) = grid.world_to_grid(start);
703        let mut visited: HashSet<usize> = HashSet::new();
704        loop {
705            let idx = cy * self.width + cx;
706            if visited.contains(&idx) { break; }
707            visited.insert(idx);
708            path.push(grid.grid_to_world(cx, cy));
709            if self.distances[idx] == 0.0 { break; }
710            match self.flow[idx] {
711                Some((nx, ny)) => { cx = nx; cy = ny; }
712                None => break,
713            }
714        }
715        path
716    }
717}
718
719// ---------------------------------------------------------------------------
720// Jump Point Search
721// ---------------------------------------------------------------------------
722
723/// Jump Point Search — optimized A* for uniform-cost grids.
724/// Significantly reduces nodes expanded on open terrain.
725#[derive(Debug, Clone, Default)]
726pub struct JumpPointSearch {
727    pub heuristic: Heuristic,
728}
729
730impl JumpPointSearch {
731    pub fn new() -> Self { JumpPointSearch { heuristic: Heuristic::Octile } }
732
733    /// Find a path using JPS. Falls back to standard A* on weighted grids.
734    pub fn find_path(&self, grid: &PathGrid, start: Vec2, end: Vec2) -> Option<Vec<Vec2>> {
735        let (sx, sy) = grid.world_to_grid(start);
736        let (ex, ey) = grid.world_to_grid(end);
737        if !grid.is_walkable(sx, sy) || !grid.is_walkable(ex, ey) {
738            return None;
739        }
740        if sx == ex && sy == ey {
741            return Some(vec![start, end]);
742        }
743
744        let start_idx = sy * grid.width + sx;
745        let end_idx   = ey * grid.width + ex;
746        let mut g_costs: Vec<f32> = vec![f32::INFINITY; grid.len()];
747        let mut parents: Vec<Option<usize>> = vec![None; grid.len()];
748        let mut open: BinaryHeap<HeapNode> = BinaryHeap::new();
749        let mut closed: HashSet<usize> = HashSet::new();
750
751        g_costs[start_idx] = 0.0;
752        let h = self.heuristic.compute(sx, sy, ex, ey);
753        open.push(HeapNode { f_cost: h, g_cost: 0.0, index: start_idx });
754
755        while let Some(current) = open.pop() {
756            let idx = current.index;
757            if closed.contains(&idx) { continue; }
758            closed.insert(idx);
759
760            if idx == end_idx {
761                return Some(self.reconstruct(grid, &parents, idx, start, end));
762            }
763
764            let cx = idx % grid.width;
765            let cy = idx / grid.width;
766            let parent_idx = parents[idx];
767            let jump_points = self.identify_successors(grid, cx, cy, parent_idx, ex, ey);
768
769            for (jx, jy) in jump_points {
770                let jidx = jy * grid.width + jx;
771                if closed.contains(&jidx) { continue; }
772                let dist = ((jx as f32 - cx as f32).powi(2) + (jy as f32 - cy as f32).powi(2)).sqrt();
773                let tentative_g = g_costs[idx] + dist;
774                if tentative_g < g_costs[jidx] {
775                    g_costs[jidx] = tentative_g;
776                    parents[jidx] = Some(idx);
777                    let h2 = self.heuristic.compute(jx, jy, ex, ey);
778                    open.push(HeapNode { f_cost: tentative_g + h2, g_cost: tentative_g, index: jidx });
779                }
780            }
781        }
782        None
783    }
784
785    fn identify_successors(
786        &self, grid: &PathGrid,
787        x: usize, y: usize,
788        parent: Option<usize>,
789        ex: usize, ey: usize,
790    ) -> Vec<(usize, usize)> {
791        let mut successors = Vec::new();
792        let neighbors = self.prune_neighbors(grid, x, y, parent);
793        for (nx, ny) in neighbors {
794            let dx = (nx as i32 - x as i32).signum();
795            let dy = (ny as i32 - y as i32).signum();
796            if let Some((jx, jy)) = self.jump(grid, x as i32, y as i32, dx, dy, ex, ey) {
797                successors.push((jx as usize, jy as usize));
798            }
799        }
800        successors
801    }
802
803    fn prune_neighbors(
804        &self, grid: &PathGrid,
805        x: usize, y: usize,
806        parent: Option<usize>,
807    ) -> Vec<(usize, usize)> {
808        let Some(pidx) = parent else {
809            // No parent: return all walkable neighbors
810            return grid.neighbors(x, y).into_iter().map(|(nx, ny, _)| (nx, ny)).collect();
811        };
812        let px = pidx % grid.width;
813        let py = pidx / grid.width;
814        let dx = (x as i32 - px as i32).signum();
815        let dy = (y as i32 - py as i32).signum();
816        let mut result = Vec::new();
817
818        if dx != 0 && dy != 0 {
819            // Diagonal movement
820            if grid.is_walkable((x as i32 + dx) as usize, y) {
821                result.push(((x as i32 + dx) as usize, y));
822            }
823            if grid.is_walkable(x, (y as i32 + dy) as usize) {
824                result.push((x, (y as i32 + dy) as usize));
825            }
826            if grid.is_walkable((x as i32 + dx) as usize, (y as i32 + dy) as usize) {
827                result.push(((x as i32 + dx) as usize, (y as i32 + dy) as usize));
828            }
829            // Forced neighbors
830            if !grid.is_walkable((x as i32 - dx) as usize, y)
831                && grid.is_walkable((x as i32 - dx) as usize, (y as i32 + dy) as usize)
832            {
833                result.push(((x as i32 - dx) as usize, (y as i32 + dy) as usize));
834            }
835            if !grid.is_walkable(x, (y as i32 - dy) as usize)
836                && grid.is_walkable((x as i32 + dx) as usize, (y as i32 - dy) as usize)
837            {
838                result.push(((x as i32 + dx) as usize, (y as i32 - dy) as usize));
839            }
840        } else if dx != 0 {
841            // Horizontal
842            if grid.is_walkable((x as i32 + dx) as usize, y) {
843                result.push(((x as i32 + dx) as usize, y));
844            }
845            if !grid.is_walkable(x, y + 1) && grid.is_walkable((x as i32 + dx) as usize, y + 1) {
846                result.push(((x as i32 + dx) as usize, y + 1));
847            }
848            if y > 0 && !grid.is_walkable(x, y - 1) && grid.is_walkable((x as i32 + dx) as usize, y - 1) {
849                result.push(((x as i32 + dx) as usize, y - 1));
850            }
851        } else {
852            // Vertical
853            if grid.is_walkable(x, (y as i32 + dy) as usize) {
854                result.push((x, (y as i32 + dy) as usize));
855            }
856            if !grid.is_walkable(x + 1, y) && grid.is_walkable(x + 1, (y as i32 + dy) as usize) {
857                result.push((x + 1, (y as i32 + dy) as usize));
858            }
859            if x > 0 && !grid.is_walkable(x - 1, y) && grid.is_walkable(x - 1, (y as i32 + dy) as usize) {
860                result.push((x - 1, (y as i32 + dy) as usize));
861            }
862        }
863        result
864    }
865
866    fn jump(
867        &self, grid: &PathGrid,
868        x: i32, y: i32,
869        dx: i32, dy: i32,
870        ex: usize, ey: usize,
871    ) -> Option<(i32, i32)> {
872        let nx = x + dx;
873        let ny = y + dy;
874        if nx < 0 || ny < 0 || nx >= grid.width as i32 || ny >= grid.height as i32 {
875            return None;
876        }
877        if !grid.is_walkable(nx as usize, ny as usize) { return None; }
878        if nx as usize == ex && ny as usize == ey { return Some((nx, ny)); }
879
880        // Forced neighbours check
881        if dx != 0 && dy != 0 {
882            // Diagonal: check cardinal jumps
883            if self.jump(grid, nx, ny, dx, 0, ex, ey).is_some()
884                || self.jump(grid, nx, ny, 0, dy, ex, ey).is_some()
885            {
886                return Some((nx, ny));
887            }
888        } else if dx != 0 {
889            if (ny + 1 < grid.height as i32 && !grid.is_walkable(nx as usize, ny as usize + 1)
890                    && grid.is_walkable((nx + dx) as usize, ny as usize + 1))
891                || (ny - 1 >= 0 && !grid.is_walkable(nx as usize, (ny - 1) as usize)
892                    && grid.is_walkable((nx + dx) as usize, (ny - 1) as usize))
893            {
894                return Some((nx, ny));
895            }
896        } else {
897            if (nx + 1 < grid.width as i32 && !grid.is_walkable(nx as usize + 1, ny as usize)
898                    && grid.is_walkable(nx as usize + 1, (ny + dy) as usize))
899                || (nx - 1 >= 0 && !grid.is_walkable((nx - 1) as usize, ny as usize)
900                    && grid.is_walkable((nx - 1) as usize, (ny + dy) as usize))
901            {
902                return Some((nx, ny));
903            }
904        }
905
906        if dx != 0 && dy != 0 {
907            self.jump(grid, nx, ny, dx, dy, ex, ey)
908        } else {
909            self.jump(grid, nx, ny, dx, dy, ex, ey)
910        }
911    }
912
913    fn reconstruct(
914        &self, grid: &PathGrid,
915        parents: &[Option<usize>],
916        end_idx: usize,
917        start: Vec2, end: Vec2,
918    ) -> Vec<Vec2> {
919        let mut indices = Vec::new();
920        let mut cur = end_idx;
921        loop {
922            indices.push(cur);
923            match parents[cur] {
924                Some(p) => cur = p,
925                None => break,
926            }
927        }
928        indices.reverse();
929        // JPS stores jump points, so we need to fill in the intermediate cells
930        let mut waypoints = Vec::new();
931        for window in indices.windows(2) {
932            let ax = window[0] % grid.width;
933            let ay = window[0] / grid.width;
934            let bx = window[1] % grid.width;
935            let by = window[1] / grid.width;
936            waypoints.push(grid.grid_to_world(ax, ay));
937            // Interpolate
938            let mut cx = ax as i32;
939            let mut cy = ay as i32;
940            let dxs = (bx as i32 - ax as i32).signum();
941            let dys = (by as i32 - ay as i32).signum();
942            while cx as usize != bx || cy as usize != by {
943                cx += dxs;
944                cy += dys;
945                waypoints.push(grid.grid_to_world(cx as usize, cy as usize));
946            }
947        }
948        if indices.len() == 1 {
949            let ax = indices[0] % grid.width;
950            let ay = indices[0] / grid.width;
951            waypoints.push(grid.grid_to_world(ax, ay));
952        }
953        if !waypoints.is_empty() { waypoints[0] = start; }
954        if waypoints.len() > 1 { *waypoints.last_mut().unwrap() = end; }
955        waypoints
956    }
957}
958
959// ---------------------------------------------------------------------------
960// Hierarchical Pathfinder
961// ---------------------------------------------------------------------------
962
963/// A chunk in the hierarchical pathfinder.
964#[derive(Debug, Clone)]
965pub struct HierarchyChunk {
966    pub chunk_x: usize,
967    pub chunk_y: usize,
968    pub center: Vec2,
969    pub walkable: bool,
970    pub portal_cells: Vec<(usize, usize)>,
971}
972
973/// Two-level hierarchical pathfinder.
974/// Performs a coarse A* over chunks, then refines within each chunk.
975#[derive(Debug, Clone)]
976pub struct HierarchicalPathfinder {
977    pub chunk_size: usize,
978    fine_finder: AStarPathfinder,
979}
980
981impl HierarchicalPathfinder {
982    pub fn new(chunk_size: usize) -> Self {
983        HierarchicalPathfinder {
984            chunk_size,
985            fine_finder: AStarPathfinder::new(),
986        }
987    }
988
989    /// Build the chunk abstraction for the given grid.
990    pub fn build_chunks(&self, grid: &PathGrid) -> Vec<HierarchyChunk> {
991        let cw = (grid.width  + self.chunk_size - 1) / self.chunk_size;
992        let ch = (grid.height + self.chunk_size - 1) / self.chunk_size;
993        let mut chunks = Vec::with_capacity(cw * ch);
994        for cy in 0..ch {
995            for cx in 0..cw {
996                let x0 = cx * self.chunk_size;
997                let y0 = cy * self.chunk_size;
998                let x1 = (x0 + self.chunk_size).min(grid.width);
999                let y1 = (y0 + self.chunk_size).min(grid.height);
1000                let walkable = (x0..x1).any(|x| (y0..y1).any(|y| grid.is_walkable(x, y)));
1001                let center = Vec2::new(
1002                    (x0 + x1) as f32 * 0.5 * grid.cell_size,
1003                    (y0 + y1) as f32 * 0.5 * grid.cell_size,
1004                ) + grid.origin;
1005                chunks.push(HierarchyChunk {
1006                    chunk_x: cx,
1007                    chunk_y: cy,
1008                    center,
1009                    walkable,
1010                    portal_cells: Vec::new(),
1011                });
1012            }
1013        }
1014        chunks
1015    }
1016
1017    /// Find a path using hierarchical search.
1018    pub fn find_path(&self, grid: &PathGrid, start: Vec2, end: Vec2) -> Option<Vec<Vec2>> {
1019        // For simplicity, we use the fine finder for nearby queries and
1020        // coarse chunk-aware routing for longer distances.
1021        let (sx, sy) = grid.world_to_grid(start);
1022        let (ex, ey) = grid.world_to_grid(end);
1023        let chunk_dist = ((sx as i32 / self.chunk_size as i32 - ex as i32 / self.chunk_size as i32).abs()
1024            + (sy as i32 / self.chunk_size as i32 - ey as i32 / self.chunk_size as i32).abs()) as usize;
1025
1026        if chunk_dist <= 2 {
1027            // Close enough: use fine A* directly
1028            return self.fine_finder.find_path(grid, start, end);
1029        }
1030
1031        // Coarse pass: build a reduced grid of chunks
1032        let cw = (grid.width  + self.chunk_size - 1) / self.chunk_size;
1033        let ch = (grid.height + self.chunk_size - 1) / self.chunk_size;
1034        let mut chunk_grid = PathGrid::new(cw, ch, grid.cell_size * self.chunk_size as f32);
1035        chunk_grid.origin = grid.origin;
1036        // Mark non-walkable chunks
1037        for cy in 0..ch {
1038            for cx in 0..cw {
1039                let x0 = cx * self.chunk_size;
1040                let y0 = cy * self.chunk_size;
1041                let x1 = (x0 + self.chunk_size).min(grid.width);
1042                let y1 = (y0 + self.chunk_size).min(grid.height);
1043                let walkable = (x0..x1).any(|x| (y0..y1).any(|y| grid.is_walkable(x, y)));
1044                if !walkable {
1045                    chunk_grid.set_walkable(cx, cy, false);
1046                }
1047            }
1048        }
1049
1050        let chunk_start = chunk_grid.grid_to_world(sx / self.chunk_size, sy / self.chunk_size);
1051        let chunk_end   = chunk_grid.grid_to_world(ex / self.chunk_size, ey / self.chunk_size);
1052        let coarse = self.fine_finder.find_path(&chunk_grid, chunk_start, chunk_end)?;
1053
1054        // Fine pass: stitch through each coarse waypoint
1055        let mut full_path = Vec::new();
1056        let mut prev = start;
1057        for &waypoint in coarse.iter().skip(1) {
1058            // Clamp waypoint into fine grid
1059            let (wx, wy) = grid.world_to_grid(waypoint);
1060            let fine_wp = grid.grid_to_world(wx, wy);
1061            if let Some(mut seg) = self.fine_finder.find_path(grid, prev, fine_wp) {
1062                if !full_path.is_empty() { seg.remove(0); }
1063                full_path.extend(seg);
1064            }
1065            prev = fine_wp;
1066        }
1067
1068        if full_path.is_empty() {
1069            self.fine_finder.find_path(grid, start, end)
1070        } else {
1071            Some(full_path)
1072        }
1073    }
1074}
1075
1076// ---------------------------------------------------------------------------
1077// Unit Tests
1078// ---------------------------------------------------------------------------
1079
1080#[cfg(test)]
1081mod tests {
1082    use super::*;
1083    use glam::Vec2;
1084
1085    fn make_grid(w: usize, h: usize) -> PathGrid {
1086        PathGrid::new(w, h, 1.0)
1087    }
1088
1089    #[test]
1090    fn test_grid_index_round_trip() {
1091        let grid = make_grid(10, 10);
1092        let (gx, gy) = grid.world_to_grid(Vec2::new(3.7, 5.2));
1093        let world = grid.grid_to_world(gx, gy);
1094        // Centre of cell should be within 0.5
1095        assert!((world.x - 3.5).abs() < 0.5);
1096        assert!((world.y - 5.5).abs() < 0.5);
1097    }
1098
1099    #[test]
1100    fn test_astar_straight_line() {
1101        let grid = make_grid(10, 10);
1102        let finder = AStarPathfinder::new();
1103        let path = finder.find_path(&grid, Vec2::new(0.5, 0.5), Vec2::new(9.5, 0.5));
1104        assert!(path.is_some());
1105        let p = path.unwrap();
1106        assert!(p.len() >= 2);
1107    }
1108
1109    #[test]
1110    fn test_astar_blocked() {
1111        let mut grid = make_grid(5, 5);
1112        // Wall across the middle
1113        for y in 0..5 { grid.set_walkable(2, y, false); }
1114        let finder = AStarPathfinder::new().with_diagonal(false);
1115        let path = finder.find_path(&grid, Vec2::new(0.5, 2.5), Vec2::new(4.5, 2.5));
1116        assert!(path.is_none());
1117    }
1118
1119    #[test]
1120    fn test_astar_around_obstacle() {
1121        let mut grid = make_grid(10, 10);
1122        // Vertical wall with gap
1123        for y in 1..10 { grid.set_walkable(5, y, false); }
1124        let finder = AStarPathfinder::new();
1125        let path = finder.find_path(&grid, Vec2::new(0.5, 5.5), Vec2::new(9.5, 5.5));
1126        assert!(path.is_some());
1127    }
1128
1129    #[test]
1130    fn test_dijkstra_map() {
1131        let grid = make_grid(10, 10);
1132        let mut dmap = DijkstraMap::new(10, 10);
1133        dmap.build(&grid, &[Vec2::new(5.5, 5.5)]);
1134        // Distance at goal should be ~0
1135        assert!(dmap.distance_at(&grid, Vec2::new(5.5, 5.5)) < 1.0);
1136        // Distance at corner should be greater
1137        assert!(dmap.distance_at(&grid, Vec2::new(0.5, 0.5)) > 5.0);
1138    }
1139
1140    #[test]
1141    fn test_dijkstra_next_step() {
1142        let grid = make_grid(10, 10);
1143        let mut dmap = DijkstraMap::new(10, 10);
1144        dmap.build(&grid, &[Vec2::new(9.5, 9.5)]);
1145        let next = dmap.next_step(&grid, Vec2::new(0.5, 0.5));
1146        assert!(next.is_some());
1147    }
1148
1149    #[test]
1150    fn test_jps_finds_path() {
1151        let grid = make_grid(20, 20);
1152        let jps = JumpPointSearch::new();
1153        let path = jps.find_path(&grid, Vec2::new(0.5, 0.5), Vec2::new(19.5, 19.5));
1154        assert!(path.is_some());
1155    }
1156
1157    #[test]
1158    fn test_smooth_path_no_obstacles() {
1159        let grid = make_grid(10, 10);
1160        let path = vec![
1161            Vec2::new(0.5, 0.5),
1162            Vec2::new(1.5, 0.5),
1163            Vec2::new(2.5, 0.5),
1164            Vec2::new(3.5, 0.5),
1165        ];
1166        let smoothed = smooth_path(path.clone(), &grid);
1167        // Should reduce to just start and end since line-of-sight is clear
1168        assert!(smoothed.len() <= path.len());
1169    }
1170
1171    #[test]
1172    fn test_hierarchical_close_path() {
1173        let grid = make_grid(20, 20);
1174        let hf = HierarchicalPathfinder::new(4);
1175        let path = hf.find_path(&grid, Vec2::new(0.5, 0.5), Vec2::new(2.5, 2.5));
1176        assert!(path.is_some());
1177    }
1178
1179    #[test]
1180    fn test_hierarchical_far_path() {
1181        let grid = make_grid(40, 40);
1182        let hf = HierarchicalPathfinder::new(4);
1183        let path = hf.find_path(&grid, Vec2::new(0.5, 0.5), Vec2::new(38.5, 38.5));
1184        assert!(path.is_some());
1185    }
1186
1187    #[test]
1188    fn test_path_detailed_found() {
1189        let grid = make_grid(10, 10);
1190        let finder = AStarPathfinder::new();
1191        let req = PathRequest::new(Vec2::new(0.5, 0.5), Vec2::new(9.5, 9.5));
1192        let (result, stats) = finder.find_path_detailed(&grid, &req);
1193        assert!(matches!(result, PathResult::Found(_)));
1194        assert!(stats.nodes_expanded > 0);
1195    }
1196
1197    #[test]
1198    fn test_path_detailed_partial() {
1199        let mut grid = make_grid(10, 10);
1200        for x in 0..10 { grid.set_walkable(x, 5, false); }
1201        let finder = AStarPathfinder::new();
1202        let mut req = PathRequest::new(Vec2::new(0.5, 0.5), Vec2::new(9.5, 9.5));
1203        req.allow_partial = true;
1204        let (result, _) = finder.find_path_detailed(&grid, &req);
1205        // May be partial or no path, either is acceptable
1206        assert!(matches!(result, PathResult::Partial(_) | PathResult::NoPath));
1207    }
1208
1209    #[test]
1210    fn test_neighbors_cardinal_only() {
1211        let grid = make_grid(5, 5);
1212        let neighbors = grid.neighbors_cardinal(2, 2);
1213        assert_eq!(neighbors.len(), 4);
1214    }
1215
1216    #[test]
1217    fn test_line_of_sight() {
1218        let mut grid = make_grid(10, 10);
1219        assert!(grid.line_of_sight(0, 0, 9, 9));
1220        grid.set_walkable(5, 5, false);
1221        // After blocking, LOS may be false
1222        let _los = grid.line_of_sight(0, 0, 9, 9); // may or may not be blocked by Bresenham
1223    }
1224
1225    #[test]
1226    fn test_heuristics() {
1227        assert_eq!(Heuristic::Manhattan.compute(0, 0, 3, 4), 7.0);
1228        assert!((Heuristic::Euclidean.compute(0, 0, 3, 4) - 5.0).abs() < 0.001);
1229        assert_eq!(Heuristic::Chebyshev.compute(0, 0, 3, 4), 4.0);
1230    }
1231
1232    #[test]
1233    fn test_spline_path() {
1234        let path = vec![
1235            Vec2::new(0.0, 0.0),
1236            Vec2::new(5.0, 5.0),
1237            Vec2::new(10.0, 0.0),
1238        ];
1239        let splined = spline_path(&path, 4);
1240        assert!(splined.len() > path.len());
1241    }
1242
1243    #[test]
1244    fn test_grid_weight() {
1245        let mut grid = make_grid(5, 5);
1246        grid.set_weight(2, 2, 5.0);
1247        assert_eq!(grid.weight(2, 2), 5.0);
1248        assert_eq!(grid.weight(1, 1), 1.0);
1249    }
1250}