Skip to main content

waymark/
hierarchical_pathfinding.rs

1//! Hierarchical pathfinding for large worlds
2//!
3//! This module provides multi-level pathfinding that enables efficient navigation
4//! across large game worlds by using hierarchical abstraction of the navigation mesh.
5//!
6//! The hierarchical approach works by:
7//! 1. Dividing the world into tiles or clusters
8//! 2. Creating abstract graphs at multiple levels
9//! 3. Planning high-level paths first, then refining down to detailed paths
10//! 4. Caching results at different levels for performance
11
12use super::nav_mesh::NavMesh;
13use super::status::Status;
14use super::{PolyRef, QueryFilter};
15use crate::error::DetourError;
16use glam::Vec3;
17
18use std::cmp::Reverse;
19use std::collections::{BinaryHeap, HashMap};
20
21/// Maximum number of hierarchical levels
22#[allow(dead_code)]
23const MAX_HIERARCHY_LEVELS: usize = 4;
24
25/// Default cluster size for level 0 (in navigation mesh units)
26const DEFAULT_CLUSTER_SIZE: f32 = 64.0;
27
28/// Hierarchical pathfinding system
29#[derive(Debug)]
30pub struct HierarchicalPathfinder {
31    /// The base navigation mesh
32    /// TODO: Currently stored but not used - would be used for actual pathfinding implementation
33    #[allow(dead_code)]
34    nav_mesh: NavMesh,
35    /// Hierarchical levels (level 0 = finest detail)
36    pub levels: Vec<HierarchyLevel>,
37    /// Cluster size at level 0
38    pub base_cluster_size: f32,
39}
40
41/// A single level in the hierarchy
42#[derive(Debug, Clone)]
43pub struct HierarchyLevel {
44    /// Level index (0 = base level)
45    pub level: usize,
46    /// Size of clusters at this level
47    pub cluster_size: f32,
48    /// Clusters at this level
49    pub clusters: Vec<Cluster>,
50    /// Abstract graph connections between clusters
51    pub connections: Vec<ClusterConnection>,
52    /// Spatial index for fast cluster lookup
53    pub spatial_index: HashMap<(i32, i32), Vec<usize>>,
54}
55
56/// A cluster of navigation polygons
57#[derive(Debug, Clone)]
58pub struct Cluster {
59    /// Unique cluster ID
60    pub id: usize,
61    /// Bounding box of the cluster
62    pub bounds: ClusterBounds,
63    /// Polygons contained in this cluster
64    pub polygons: Vec<PolyRef>,
65    /// Entry/exit points to other clusters
66    pub portals: Vec<Portal>,
67    /// Center point of the cluster
68    pub center: Vec3,
69    /// Whether this cluster is walkable
70    pub walkable: bool,
71}
72
73/// Connection between two clusters
74#[derive(Debug, Clone)]
75pub struct ClusterConnection {
76    /// Source cluster ID
77    pub from_cluster: usize,
78    /// Destination cluster ID
79    pub to_cluster: usize,
80    /// Portal connecting the clusters
81    pub portal: Portal,
82    /// Cost to traverse this connection
83    pub cost: f32,
84    /// Distance of the connection
85    pub distance: f32,
86}
87
88/// Portal between clusters
89#[derive(Debug, Clone)]
90pub struct Portal {
91    /// Position of the portal
92    pub position: Vec3,
93    /// Width of the portal
94    pub width: f32,
95    /// Direction vector of the portal
96    pub direction: Vec3,
97    /// Polygons on either side of the portal
98    pub poly_refs: [PolyRef; 2],
99}
100
101/// Bounding box for a cluster
102#[derive(Debug, Clone)]
103pub struct ClusterBounds {
104    pub min: Vec3,
105    pub max: Vec3,
106}
107
108/// Configuration for hierarchical pathfinding
109#[derive(Debug, Clone)]
110pub struct HierarchicalConfig {
111    /// Number of hierarchy levels
112    pub max_levels: usize,
113    /// Base cluster size
114    pub base_cluster_size: f32,
115    /// Scaling factor between levels
116    pub level_scale_factor: f32,
117    /// Maximum distance to search at each level
118    pub max_search_distance: f32,
119    /// Whether to cache paths
120    pub enable_caching: bool,
121}
122
123impl Default for HierarchicalConfig {
124    fn default() -> Self {
125        Self {
126            max_levels: 3,
127            base_cluster_size: DEFAULT_CLUSTER_SIZE,
128            level_scale_factor: 4.0,
129            max_search_distance: 1000.0,
130            enable_caching: true,
131        }
132    }
133}
134
135/// Result of hierarchical pathfinding
136#[derive(Debug)]
137pub struct HierarchicalPath {
138    /// Complete path from start to end
139    pub path: Vec<Vec3>,
140    /// Clusters traversed at each level
141    pub cluster_path: Vec<Vec<usize>>,
142    /// Cost of the path
143    pub cost: f32,
144    /// Status of the pathfinding operation
145    pub status: Status,
146}
147
148/// Cache entry for hierarchical paths
149/// TODO: Implement path caching for improved performance
150#[allow(dead_code)]
151#[derive(Debug, Clone)]
152struct CacheEntry {
153    path: Vec<usize>,
154    cost: f32,
155    timestamp: u64,
156}
157
158/// Path cache for hierarchical pathfinding
159/// TODO: Implement path caching for improved performance
160#[allow(dead_code)]
161#[derive(Debug)]
162struct PathCache {
163    entries: HashMap<(usize, usize, usize), CacheEntry>, // (level, from, to) -> path
164    max_entries: usize,
165    current_time: u64,
166}
167
168impl HierarchicalPathfinder {
169    /// Creates a new hierarchical pathfinder from a navigation mesh
170    pub fn new(nav_mesh: NavMesh, config: HierarchicalConfig) -> Result<Self, DetourError> {
171        let mut pathfinder = Self {
172            nav_mesh,
173            levels: Vec::new(),
174            base_cluster_size: config.base_cluster_size,
175        };
176
177        // Build the hierarchical levels
178        pathfinder.build_hierarchy(&config)?;
179
180        Ok(pathfinder)
181    }
182
183    /// Finds a path using hierarchical pathfinding
184    pub fn find_path(
185        &self,
186        start: Vec3,
187        end: Vec3,
188        filter: &QueryFilter,
189    ) -> Result<HierarchicalPath, DetourError> {
190        // Find starting and ending clusters at each level
191        let start_clusters = self.find_clusters_at_position(start)?;
192        let end_clusters = self.find_clusters_at_position(end)?;
193
194        if start_clusters.is_empty() || end_clusters.is_empty() {
195            return Ok(HierarchicalPath {
196                path: Vec::new(),
197                cluster_path: Vec::new(),
198                cost: f32::INFINITY,
199                status: Status::Failure,
200            });
201        }
202
203        // Plan from highest level down to lowest level
204        let mut cluster_paths = Vec::new();
205        let mut current_start = start_clusters[self.levels.len() - 1];
206        let mut current_end = end_clusters[self.levels.len() - 1];
207
208        // Start from the highest level and work down
209        for level in (0..self.levels.len()).rev() {
210            let level_path = self.find_path_at_level(level, current_start, current_end, filter)?;
211
212            if level_path.is_empty() {
213                return Ok(HierarchicalPath {
214                    path: Vec::new(),
215                    cluster_path: Vec::new(),
216                    cost: f32::INFINITY,
217                    status: Status::Failure,
218                });
219            }
220
221            cluster_paths.push(level_path.clone());
222
223            // Refine for the next level down
224            if level > 0 {
225                current_start = start_clusters[level - 1];
226                current_end = end_clusters[level - 1];
227            }
228        }
229
230        // Convert cluster path to actual world positions
231        let world_path = self.convert_to_world_path(&cluster_paths, start, end)?;
232        let total_cost = self.calculate_path_cost(&world_path);
233
234        Ok(HierarchicalPath {
235            path: world_path,
236            cluster_path: cluster_paths,
237            cost: total_cost,
238            status: Status::Success,
239        })
240    }
241
242    /// Builds the hierarchical levels
243    fn build_hierarchy(&mut self, config: &HierarchicalConfig) -> Result<(), DetourError> {
244        // Build each level of the hierarchy
245        for level in 0..config.max_levels {
246            let cluster_size =
247                config.base_cluster_size * config.level_scale_factor.powi(level as i32);
248            let hierarchy_level = self.build_level(level, cluster_size)?;
249            self.levels.push(hierarchy_level);
250        }
251
252        Ok(())
253    }
254
255    /// Builds a single level of the hierarchy
256    fn build_level(&self, level: usize, cluster_size: f32) -> Result<HierarchyLevel, DetourError> {
257        let mut clusters = Vec::new();
258        let mut spatial_index = HashMap::new();
259
260        // Get navigation mesh bounds
261        let mesh_bounds = self.get_mesh_bounds();
262
263        // Create grid of clusters
264        let grid_width = ((mesh_bounds.max.x - mesh_bounds.min.x) / cluster_size).ceil() as i32;
265        let grid_height = ((mesh_bounds.max.z - mesh_bounds.min.z) / cluster_size).ceil() as i32;
266
267        let mut cluster_id = 0;
268
269        for grid_y in 0..grid_height {
270            for grid_x in 0..grid_width {
271                let min_x = mesh_bounds.min.x + grid_x as f32 * cluster_size;
272                let min_z = mesh_bounds.min.z + grid_y as f32 * cluster_size;
273                let max_x = min_x + cluster_size;
274                let max_z = min_z + cluster_size;
275
276                let bounds = ClusterBounds {
277                    min: Vec3::new(min_x, mesh_bounds.min.y, min_z),
278                    max: Vec3::new(max_x, mesh_bounds.max.y, max_z),
279                };
280
281                // Find polygons in this cluster
282                let polygons = self.find_polygons_in_bounds(&bounds)?;
283
284                if !polygons.is_empty() {
285                    let center = Vec3::new(
286                        (min_x + max_x) * 0.5,
287                        (mesh_bounds.min.y + mesh_bounds.max.y) * 0.5,
288                        (min_z + max_z) * 0.5,
289                    );
290
291                    let cluster = Cluster {
292                        id: cluster_id,
293                        bounds,
294                        polygons,
295                        portals: Vec::new(),
296                        center,
297                        walkable: true,
298                    };
299
300                    // Add to spatial index
301                    spatial_index
302                        .entry((grid_x, grid_y))
303                        .or_insert_with(Vec::new)
304                        .push(clusters.len());
305
306                    clusters.push(cluster);
307                    cluster_id += 1;
308                }
309            }
310        }
311
312        // Build connections between clusters
313        let connections = self.build_cluster_connections(&clusters)?;
314
315        Ok(HierarchyLevel {
316            level,
317            cluster_size,
318            clusters,
319            connections,
320            spatial_index,
321        })
322    }
323
324    /// Finds clusters that contain the given position
325    pub fn find_clusters_at_position(&self, position: Vec3) -> Result<Vec<usize>, DetourError> {
326        let mut cluster_ids = Vec::new();
327
328        for level in &self.levels {
329            // Calculate grid coordinates
330            let grid_x =
331                ((position.x - self.get_mesh_bounds().min.x) / level.cluster_size).floor() as i32;
332            let grid_z =
333                ((position.z - self.get_mesh_bounds().min.z) / level.cluster_size).floor() as i32;
334
335            if let Some(cluster_indices) = level.spatial_index.get(&(grid_x, grid_z)) {
336                // Find the specific cluster containing this position
337                for &cluster_idx in cluster_indices {
338                    let cluster = &level.clusters[cluster_idx];
339                    if self.point_in_bounds(position, &cluster.bounds) {
340                        cluster_ids.push(cluster_idx);
341                        break;
342                    }
343                }
344            }
345
346            // If no cluster found at this level, use the nearest one
347            if cluster_ids.len() <= level.level {
348                let nearest = self.find_nearest_cluster(level, position)?;
349                cluster_ids.push(nearest);
350            }
351        }
352
353        Ok(cluster_ids)
354    }
355
356    /// Finds a path at a specific hierarchy level
357    fn find_path_at_level(
358        &self,
359        level: usize,
360        start_cluster: usize,
361        end_cluster: usize,
362        _filter: &QueryFilter,
363    ) -> Result<Vec<usize>, DetourError> {
364        if level >= self.levels.len() {
365            return Err(DetourError::Failure);
366        }
367
368        let hierarchy_level = &self.levels[level];
369
370        // Use A* to find path between clusters
371        let mut open_set = BinaryHeap::new();
372        let mut came_from = HashMap::new();
373        let mut g_score = HashMap::new();
374        let mut f_score = HashMap::new();
375
376        // Initialize with start cluster
377        g_score.insert(start_cluster, 0.0);
378        let h_start = self.heuristic_cost(hierarchy_level, start_cluster, end_cluster);
379        f_score.insert(start_cluster, h_start);
380        open_set.push(Reverse((
381            ordered_float::NotNan::new(h_start).map_err(|_| DetourError::Failure)?,
382            start_cluster,
383        )));
384
385        while let Some(Reverse((_, current))) = open_set.pop() {
386            if current == end_cluster {
387                // Reconstruct path
388                let mut path = vec![current];
389                let mut node = current;
390                while let Some(&parent) = came_from.get(&node) {
391                    path.push(parent);
392                    node = parent;
393                }
394                path.reverse();
395                return Ok(path);
396            }
397
398            // Explore neighbors
399            for connection in &hierarchy_level.connections {
400                let neighbor = if connection.from_cluster == current {
401                    connection.to_cluster
402                } else if connection.to_cluster == current {
403                    connection.from_cluster
404                } else {
405                    continue;
406                };
407
408                let tentative_g_score =
409                    g_score.get(&current).unwrap_or(&f32::INFINITY) + connection.cost;
410
411                if tentative_g_score < *g_score.get(&neighbor).unwrap_or(&f32::INFINITY) {
412                    came_from.insert(neighbor, current);
413                    g_score.insert(neighbor, tentative_g_score);
414                    let h_neighbor = self.heuristic_cost(hierarchy_level, neighbor, end_cluster);
415                    let f_neighbor = tentative_g_score + h_neighbor;
416                    f_score.insert(neighbor, f_neighbor);
417
418                    open_set.push(Reverse((
419                        ordered_float::NotNan::new(f_neighbor).map_err(|_| DetourError::Failure)?,
420                        neighbor,
421                    )));
422                }
423            }
424        }
425
426        // No path found
427        Ok(Vec::new())
428    }
429
430    /// Calculates heuristic cost between clusters
431    fn heuristic_cost(&self, level: &HierarchyLevel, from: usize, to: usize) -> f32 {
432        if from >= level.clusters.len() || to >= level.clusters.len() {
433            return f32::INFINITY;
434        }
435
436        let from_cluster = &level.clusters[from];
437        let to_cluster = &level.clusters[to];
438
439        // Euclidean distance between cluster centers
440        (to_cluster.center - from_cluster.center).length()
441    }
442
443    /// Converts cluster path to world coordinates
444    fn convert_to_world_path(
445        &self,
446        cluster_paths: &[Vec<usize>],
447        start: Vec3,
448        end: Vec3,
449    ) -> Result<Vec<Vec3>, DetourError> {
450        if cluster_paths.is_empty() {
451            return Ok(vec![start, end]);
452        }
453
454        let mut world_path = vec![start];
455
456        // Use the most detailed level (level 0) for the actual path
457        if let Some(detailed_path) = cluster_paths.first() {
458            for &cluster_id in detailed_path.iter().skip(1) {
459                if cluster_id < self.levels[0].clusters.len() {
460                    let cluster = &self.levels[0].clusters[cluster_id];
461                    world_path.push(cluster.center);
462                }
463            }
464        }
465
466        world_path.push(end);
467        Ok(world_path)
468    }
469
470    /// Calculates the total cost of a path
471    pub fn calculate_path_cost(&self, path: &[Vec3]) -> f32 {
472        let mut total_cost = 0.0;
473        for i in 1..path.len() {
474            total_cost += (path[i] - path[i - 1]).length();
475        }
476        total_cost
477    }
478
479    /// Helper methods
480    fn get_mesh_bounds(&self) -> ClusterBounds {
481        // For now, return a default bounds - in a real implementation,
482        // this would calculate the actual bounds of the navigation mesh
483        ClusterBounds {
484            min: Vec3::new(-500.0, -10.0, -500.0),
485            max: Vec3::new(500.0, 10.0, 500.0),
486        }
487    }
488
489    fn find_polygons_in_bounds(&self, bounds: &ClusterBounds) -> Result<Vec<PolyRef>, DetourError> {
490        // For now, return dummy polygon references
491        // In a real implementation, this would query the navigation mesh
492        // for polygons that intersect with the given bounds
493
494        // Return some polygons for any bounds within reasonable range
495        // This ensures clusters have content and can form connections
496        if bounds.max.x >= -600.0
497            && bounds.min.x <= 600.0
498            && bounds.max.z >= -600.0
499            && bounds.min.z <= 600.0
500        {
501            // Generate a few dummy polygon references based on bounds position
502            let base_id = ((bounds.min.x + 600.0) / 100.0) as u32 * 100
503                + ((bounds.min.z + 600.0) / 100.0) as u32;
504            Ok(vec![
505                PolyRef::new(base_id + 1),
506                PolyRef::new(base_id + 2),
507                PolyRef::new(base_id + 3),
508            ])
509        } else {
510            Ok(vec![]) // No polygons in this area
511        }
512    }
513
514    fn point_in_bounds(&self, point: Vec3, bounds: &ClusterBounds) -> bool {
515        point.x >= bounds.min.x
516            && point.x <= bounds.max.x
517            && point.y >= bounds.min.y
518            && point.y <= bounds.max.y
519            && point.z >= bounds.min.z
520            && point.z <= bounds.max.z
521    }
522
523    fn find_nearest_cluster(
524        &self,
525        level: &HierarchyLevel,
526        position: Vec3,
527    ) -> Result<usize, DetourError> {
528        let mut nearest_idx = 0;
529        let mut nearest_distance = f32::INFINITY;
530
531        for (idx, cluster) in level.clusters.iter().enumerate() {
532            let distance = (cluster.center - position).length();
533            if distance < nearest_distance {
534                nearest_distance = distance;
535                nearest_idx = idx;
536            }
537        }
538
539        Ok(nearest_idx)
540    }
541
542    fn build_cluster_connections(
543        &self,
544        clusters: &[Cluster],
545    ) -> Result<Vec<ClusterConnection>, DetourError> {
546        let mut connections = Vec::new();
547
548        // For each pair of clusters, check if they are adjacent and create connections
549        for i in 0..clusters.len() {
550            for j in (i + 1)..clusters.len() {
551                if self.clusters_are_adjacent(&clusters[i], &clusters[j]) {
552                    let distance = (clusters[j].center - clusters[i].center).length();
553                    let cost = distance * 1.0; // Base cost multiplier
554
555                    // Create a simple portal at the midpoint
556                    let portal = Portal {
557                        position: (clusters[i].center + clusters[j].center) * 0.5,
558                        width: 2.0,
559                        direction: (clusters[j].center - clusters[i].center).normalize(),
560                        poly_refs: [PolyRef::new(1), PolyRef::new(2)], // Dummy polygon references
561                    };
562
563                    connections.push(ClusterConnection {
564                        from_cluster: i,
565                        to_cluster: j,
566                        portal,
567                        cost,
568                        distance,
569                    });
570                }
571            }
572        }
573
574        Ok(connections)
575    }
576
577    fn clusters_are_adjacent(&self, cluster1: &Cluster, cluster2: &Cluster) -> bool {
578        // Simple adjacency check based on bounding box overlap or proximity
579        let distance = (cluster1.center - cluster2.center).length();
580        distance <= cluster1.bounds.max.x - cluster1.bounds.min.x + 1.0 // Allow some tolerance
581    }
582}
583
584#[allow(dead_code)]
585impl PathCache {
586    fn new(max_entries: usize) -> Self {
587        Self {
588            entries: HashMap::new(),
589            max_entries,
590            current_time: 0,
591        }
592    }
593
594    fn get(&mut self, level: usize, from: usize, to: usize) -> Option<Vec<usize>> {
595        self.current_time += 1;
596
597        if let Some(entry) = self.entries.get_mut(&(level, from, to)) {
598            entry.timestamp = self.current_time;
599            Some(entry.path.clone())
600        } else {
601            None
602        }
603    }
604
605    fn insert(&mut self, level: usize, from: usize, to: usize, path: Vec<usize>, cost: f32) {
606        self.current_time += 1;
607
608        // Remove oldest entries if cache is full
609        if self.entries.len() >= self.max_entries {
610            let oldest_key = self
611                .entries
612                .iter()
613                .min_by_key(|(_, entry)| entry.timestamp)
614                .map(|(key, _)| *key);
615
616            if let Some(key) = oldest_key {
617                self.entries.remove(&key);
618            }
619        }
620
621        self.entries.insert(
622            (level, from, to),
623            CacheEntry {
624                path,
625                cost,
626                timestamp: self.current_time,
627            },
628        );
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635    use crate::nav_mesh::NavMesh;
636
637    fn create_test_nav_mesh() -> NavMesh {
638        use super::super::NavMeshParams;
639        let params = NavMeshParams {
640            origin: [0.0, 0.0, 0.0],
641            tile_width: 100.0,
642            tile_height: 100.0,
643            max_tiles: 256, // Must be < 1024
644            max_polys_per_tile: 2048,
645        };
646        NavMesh::new(params).expect("Failed to create test NavMesh")
647    }
648
649    #[test]
650    fn test_hierarchical_pathfinder_creation() {
651        let nav_mesh = create_test_nav_mesh();
652        let config = HierarchicalConfig::default();
653
654        let pathfinder = HierarchicalPathfinder::new(nav_mesh, config);
655        assert!(pathfinder.is_ok());
656
657        let pathfinder = pathfinder.unwrap();
658        assert_eq!(pathfinder.levels.len(), 3); // Default max_levels
659    }
660
661    #[test]
662    fn test_cluster_bounds() {
663        let bounds = ClusterBounds {
664            min: Vec3::new(0.0, 0.0, 0.0),
665            max: Vec3::new(10.0, 10.0, 10.0),
666        };
667
668        let nav_mesh = create_test_nav_mesh();
669        let config = HierarchicalConfig::default();
670        let pathfinder = HierarchicalPathfinder::new(nav_mesh, config).unwrap();
671
672        // Test point inside bounds
673        assert!(pathfinder.point_in_bounds(Vec3::new(5.0, 5.0, 5.0), &bounds));
674
675        // Test point outside bounds
676        assert!(!pathfinder.point_in_bounds(Vec3::new(15.0, 5.0, 5.0), &bounds));
677    }
678
679    #[test]
680    fn test_cluster_adjacency() {
681        let nav_mesh = create_test_nav_mesh();
682        let config = HierarchicalConfig::default();
683        let pathfinder = HierarchicalPathfinder::new(nav_mesh, config).unwrap();
684
685        let cluster1 = Cluster {
686            id: 0,
687            bounds: ClusterBounds {
688                min: Vec3::new(0.0, 0.0, 0.0),
689                max: Vec3::new(10.0, 10.0, 10.0),
690            },
691            polygons: vec![PolyRef::new(1), PolyRef::new(2)],
692            portals: Vec::new(),
693            center: Vec3::new(5.0, 5.0, 5.0),
694            walkable: true,
695        };
696
697        let cluster2 = Cluster {
698            id: 1,
699            bounds: ClusterBounds {
700                min: Vec3::new(10.0, 0.0, 0.0),
701                max: Vec3::new(20.0, 10.0, 10.0),
702            },
703            polygons: vec![PolyRef::new(3), PolyRef::new(4)],
704            portals: Vec::new(),
705            center: Vec3::new(15.0, 5.0, 5.0),
706            walkable: true,
707        };
708
709        // Adjacent clusters should be detected
710        assert!(pathfinder.clusters_are_adjacent(&cluster1, &cluster2));
711
712        let cluster3 = Cluster {
713            id: 2,
714            bounds: ClusterBounds {
715                min: Vec3::new(50.0, 0.0, 0.0),
716                max: Vec3::new(60.0, 10.0, 10.0),
717            },
718            polygons: vec![PolyRef::new(5), PolyRef::new(6)],
719            portals: Vec::new(),
720            center: Vec3::new(55.0, 5.0, 5.0),
721            walkable: true,
722        };
723
724        // Non-adjacent clusters should not be detected as adjacent
725        assert!(!pathfinder.clusters_are_adjacent(&cluster1, &cluster3));
726    }
727
728    #[test]
729    fn test_path_cache() {
730        let mut cache = PathCache::new(3);
731
732        // Insert paths
733        cache.insert(0, 1, 2, vec![1, 5, 2], 10.0);
734        cache.insert(0, 2, 3, vec![2, 6, 3], 15.0);
735
736        // Retrieve paths
737        assert_eq!(cache.get(0, 1, 2), Some(vec![1, 5, 2]));
738        assert_eq!(cache.get(0, 2, 3), Some(vec![2, 6, 3]));
739        assert_eq!(cache.get(0, 3, 4), None);
740
741        // Test cache eviction
742        cache.insert(0, 3, 4, vec![3, 7, 4], 20.0);
743        cache.insert(0, 4, 5, vec![4, 8, 5], 25.0);
744
745        // Oldest entry should be evicted
746        assert!(cache.entries.len() <= 3);
747    }
748
749    #[test]
750    fn test_heuristic_cost_calculation() {
751        let nav_mesh = create_test_nav_mesh();
752        let config = HierarchicalConfig::default();
753        let pathfinder = HierarchicalPathfinder::new(nav_mesh, config).unwrap();
754
755        if !pathfinder.levels.is_empty() && pathfinder.levels[0].clusters.len() >= 2 {
756            let cost = pathfinder.heuristic_cost(&pathfinder.levels[0], 0, 1);
757            assert!(cost >= 0.0);
758            assert!(cost.is_finite());
759        }
760    }
761}