Skip to main content

proof_engine/ai/
flowfield.rs

1//! Flow field pathfinding and Reynolds flocking for large groups of agents.
2//!
3//! Flow fields compute a single vector-field over a grid from one or more
4//! goal cells.  Every agent in the group samples the field at its position
5//! and follows the resulting direction — O(1) per agent after the O(N) build.
6//!
7//! # Example
8//! ```rust
9//! use proof_engine::ai::flowfield::{FlowField, FlowFieldGroup};
10//! use glam::Vec2;
11//!
12//! let mut field = FlowField::new(20, 20, 1.0);
13//! field.set_cost(10, 10, f32::INFINITY); // obstacle
14//! field.build_integration_field(&[(19, 19)]);
15//! field.build_direction_field();
16//!
17//! let dir = field.get_direction(Vec2::new(0.5, 0.5));
18//! println!("flow direction: {:?}", dir);
19//! ```
20
21use glam::Vec2;
22use std::collections::{HashMap, VecDeque};
23
24// ---------------------------------------------------------------------------
25// FlowField
26// ---------------------------------------------------------------------------
27
28/// A grid-based flow field.
29///
30/// Build order:
31/// 1. `set_cost` for any impassable/expensive cells.
32/// 2. `build_integration_field` — BFS/Dijkstra from goals.
33/// 3. `build_direction_field`   — gradient from integration field.
34/// 4. `get_direction`           — per-agent query.
35#[derive(Debug, Clone)]
36pub struct FlowField {
37    pub width: usize,
38    pub height: usize,
39    pub cell_size: f32,
40    /// Per-cell traversal cost (>= 1.0; `INFINITY` = impassable).
41    pub costs: Vec<f32>,
42    /// Integration (distance) field — lowest value near goals.
43    pub integration: Vec<f32>,
44    /// Direction field — normalised Vec2 pointing toward nearest goal.
45    pub directions: Vec<Vec2>,
46    pub origin: Vec2,
47}
48
49impl FlowField {
50    /// Create a new uniform-cost flow field.
51    pub fn new(width: usize, height: usize, cell_size: f32) -> Self {
52        let n = width * height;
53        FlowField {
54            width,
55            height,
56            cell_size,
57            costs: vec![1.0; n],
58            integration: vec![f32::INFINITY; n],
59            directions: vec![Vec2::ZERO; n],
60            origin: Vec2::ZERO,
61        }
62    }
63
64    pub fn with_origin(mut self, origin: Vec2) -> Self {
65        self.origin = origin;
66        self
67    }
68
69    /// Set the traversal cost of a cell.  Use `f32::INFINITY` for impassable.
70    pub fn set_cost(&mut self, x: usize, y: usize, cost: f32) {
71        if let Some(i) = self.idx(x, y) {
72            self.costs[i] = cost;
73        }
74    }
75
76    /// Reset all costs to `1.0`.
77    pub fn reset_costs(&mut self) {
78        self.costs.fill(1.0);
79    }
80
81    /// World position → grid cell.
82    pub fn world_to_grid(&self, pos: Vec2) -> (usize, usize) {
83        let local = pos - self.origin;
84        let x = ((local.x / self.cell_size).floor() as isize)
85            .clamp(0, self.width as isize - 1) as usize;
86        let y = ((local.y / self.cell_size).floor() as isize)
87            .clamp(0, self.height as isize - 1) as usize;
88        (x, y)
89    }
90
91    /// Grid cell → world-space centre.
92    pub fn grid_to_world(&self, x: usize, y: usize) -> Vec2 {
93        self.origin
94            + Vec2::new(
95                x as f32 * self.cell_size + self.cell_size * 0.5,
96                y as f32 * self.cell_size + self.cell_size * 0.5,
97            )
98    }
99
100    /// Build the integration (distance-weighted) field from a set of goal cells
101    /// using a priority-queue Dijkstra.
102    pub fn build_integration_field(&mut self, goals: &[(usize, usize)]) {
103        self.integration.fill(f32::INFINITY);
104        // Min-heap via BinaryHeap with reversed ordering
105        use std::collections::BinaryHeap;
106        use std::cmp::Ordering;
107
108        #[derive(Clone)]
109        struct Node(f32, usize); // (cost, idx)
110        impl PartialEq for Node { fn eq(&self, o: &Self) -> bool { self.0 == o.0 } }
111        impl Eq for Node {}
112        impl PartialOrd for Node {
113            fn partial_cmp(&self, o: &Self) -> Option<Ordering> { Some(self.cmp(o)) }
114        }
115        impl Ord for Node {
116            fn cmp(&self, o: &Self) -> Ordering {
117                o.0.partial_cmp(&self.0).unwrap_or(Ordering::Equal)
118            }
119        }
120
121        let mut heap: BinaryHeap<Node> = BinaryHeap::new();
122        for &(gx, gy) in goals {
123            if let Some(i) = self.idx(gx, gy) {
124                if self.costs[i].is_finite() {
125                    self.integration[i] = 0.0;
126                    heap.push(Node(0.0, i));
127                }
128            }
129        }
130
131        while let Some(Node(cost, idx)) = heap.pop() {
132            if cost > self.integration[idx] { continue; }
133            let cx = idx % self.width;
134            let cy = idx / self.width;
135            for (nx, ny, dc) in self.cardinal_neighbors(cx, cy) {
136                let ni = ny * self.width + nx;
137                let new_cost = cost + self.costs[ni] * dc;
138                if new_cost < self.integration[ni] {
139                    self.integration[ni] = new_cost;
140                    heap.push(Node(new_cost, ni));
141                }
142            }
143        }
144    }
145
146    /// Build the direction field by gradient-descent from the integration field.
147    pub fn build_direction_field(&mut self) {
148        for y in 0..self.height {
149            for x in 0..self.width {
150                let idx = y * self.width + x;
151                if !self.costs[idx].is_finite() {
152                    self.directions[idx] = Vec2::ZERO;
153                    continue;
154                }
155                // Find the neighbor with the lowest integration value
156                let mut best_val = self.integration[idx];
157                let mut best_dir = Vec2::ZERO;
158                for (nx, ny, _) in self.all_neighbors(x, y) {
159                    let ni = ny * self.width + nx;
160                    if self.integration[ni] < best_val {
161                        best_val = self.integration[ni];
162                        let dx = nx as f32 - x as f32;
163                        let dy = ny as f32 - y as f32;
164                        let d = Vec2::new(dx, dy);
165                        best_dir = if d.length() > 0.0 { d.normalize() } else { Vec2::ZERO };
166                    }
167                }
168                self.directions[idx] = best_dir;
169            }
170        }
171    }
172
173    /// Sample the flow field at a world position using bilinear interpolation.
174    pub fn get_direction(&self, world_pos: Vec2) -> Vec2 {
175        let local = world_pos - self.origin;
176        let fx = (local.x / self.cell_size - 0.5).max(0.0);
177        let fy = (local.y / self.cell_size - 0.5).max(0.0);
178        let x0 = (fx.floor() as usize).min(self.width  - 1);
179        let y0 = (fy.floor() as usize).min(self.height - 1);
180        let x1 = (x0 + 1).min(self.width  - 1);
181        let y1 = (y0 + 1).min(self.height - 1);
182        let tx = fx.fract();
183        let ty = fy.fract();
184
185        let d00 = self.directions[y0 * self.width + x0];
186        let d10 = self.directions[y0 * self.width + x1];
187        let d01 = self.directions[y1 * self.width + x0];
188        let d11 = self.directions[y1 * self.width + x1];
189
190        let top    = d00.lerp(d10, tx);
191        let bottom = d01.lerp(d11, tx);
192        let result = top.lerp(bottom, ty);
193        if result.length_squared() > 0.0 { result.normalize() } else { Vec2::ZERO }
194    }
195
196    /// Get the integration value at a world position.
197    pub fn get_integration(&self, world_pos: Vec2) -> f32 {
198        let (x, y) = self.world_to_grid(world_pos);
199        self.integration[y * self.width + x]
200    }
201
202    /// Check if a cell is passable.
203    pub fn is_passable(&self, x: usize, y: usize) -> bool {
204        self.idx(x, y).map(|i| self.costs[i].is_finite()).unwrap_or(false)
205    }
206
207    fn idx(&self, x: usize, y: usize) -> Option<usize> {
208        if x < self.width && y < self.height { Some(y * self.width + x) } else { None }
209    }
210
211    fn cardinal_neighbors(&self, x: usize, y: usize) -> Vec<(usize, usize, f32)> {
212        let mut out = Vec::with_capacity(4);
213        let dirs = [(-1i32, 0i32, 1.0f32), (1, 0, 1.0), (0, -1, 1.0), (0, 1, 1.0)];
214        for (dx, dy, dc) in dirs {
215            let nx = x as i32 + dx;
216            let ny = y as i32 + dy;
217            if nx >= 0 && ny >= 0 && nx < self.width as i32 && ny < self.height as i32 {
218                let nx = nx as usize; let ny = ny as usize;
219                if self.costs[ny * self.width + nx].is_finite() {
220                    out.push((nx, ny, dc));
221                }
222            }
223        }
224        out
225    }
226
227    fn all_neighbors(&self, x: usize, y: usize) -> Vec<(usize, usize, f32)> {
228        let mut out = Vec::with_capacity(8);
229        for dx in -1i32..=1 {
230            for dy in -1i32..=1 {
231                if dx == 0 && dy == 0 { continue; }
232                let nx = x as i32 + dx;
233                let ny = y as i32 + dy;
234                if nx >= 0 && ny >= 0 && nx < self.width as i32 && ny < self.height as i32 {
235                    let nx = nx as usize; let ny = ny as usize;
236                    let dc = if dx.abs() + dy.abs() == 2 {
237                        std::f32::consts::SQRT_2
238                    } else {
239                        1.0
240                    };
241                    if self.costs[ny * self.width + nx].is_finite() {
242                        out.push((nx, ny, dc));
243                    }
244                }
245            }
246        }
247        out
248    }
249
250    /// Total cell count.
251    pub fn len(&self) -> usize { self.width * self.height }
252}
253
254// ---------------------------------------------------------------------------
255// FlowFieldCache  (LRU-evicting cache per target cell)
256// ---------------------------------------------------------------------------
257
258/// Caches pre-built flow fields keyed by goal cell.  Evicts least-recently-used
259/// entries when the capacity limit is reached.
260#[derive(Debug)]
261pub struct FlowFieldCache {
262    pub capacity: usize,
263    fields: HashMap<(usize, usize), FlowField>,
264    /// Access order — front = least recently used, back = most recently used.
265    access_order: VecDeque<(usize, usize)>,
266}
267
268impl FlowFieldCache {
269    pub fn new(capacity: usize) -> Self {
270        FlowFieldCache {
271            capacity,
272            fields: HashMap::new(),
273            access_order: VecDeque::new(),
274        }
275    }
276
277    /// Get or build a flow field for `goal` cell coordinates.
278    pub fn get_or_build(
279        &mut self,
280        goal: (usize, usize),
281        width: usize,
282        height: usize,
283        cell_size: f32,
284        costs: &[f32],
285    ) -> &FlowField {
286        if !self.fields.contains_key(&goal) {
287            // Evict LRU if over capacity
288            if self.fields.len() >= self.capacity {
289                if let Some(evict_key) = self.access_order.pop_front() {
290                    self.fields.remove(&evict_key);
291                }
292            }
293            let mut field = FlowField::new(width, height, cell_size);
294            field.costs.copy_from_slice(costs);
295            field.build_integration_field(&[goal]);
296            field.build_direction_field();
297            self.fields.insert(goal, field);
298            self.access_order.push_back(goal);
299        } else {
300            // Move to back (most recently used)
301            self.access_order.retain(|k| k != &goal);
302            self.access_order.push_back(goal);
303        }
304        &self.fields[&goal]
305    }
306
307    /// Remove a cached field.
308    pub fn invalidate(&mut self, goal: (usize, usize)) {
309        self.fields.remove(&goal);
310        self.access_order.retain(|k| k != &goal);
311    }
312
313    /// Clear all cached fields.
314    pub fn clear(&mut self) {
315        self.fields.clear();
316        self.access_order.clear();
317    }
318
319    pub fn len(&self) -> usize { self.fields.len() }
320    pub fn is_empty(&self) -> bool { self.fields.is_empty() }
321}
322
323// ---------------------------------------------------------------------------
324// FlowFieldGroup
325// ---------------------------------------------------------------------------
326
327/// A group of agents that share a single flow field to a common destination.
328#[derive(Debug, Clone)]
329pub struct FlowFieldAgent {
330    pub id: u64,
331    pub position: Vec2,
332    pub velocity: Vec2,
333    pub speed: f32,
334    pub radius: f32,
335}
336
337impl FlowFieldAgent {
338    pub fn new(id: u64, position: Vec2, speed: f32, radius: f32) -> Self {
339        FlowFieldAgent { id, position, velocity: Vec2::ZERO, speed, radius }
340    }
341}
342
343/// Manages a group of agents all moving toward the same destination via a shared flow field.
344#[derive(Debug, Clone)]
345pub struct FlowFieldGroup {
346    pub agents: Vec<FlowFieldAgent>,
347    pub field: Option<FlowField>,
348    pub destination: Option<Vec2>,
349    pub separation_weight: f32,
350}
351
352impl FlowFieldGroup {
353    pub fn new() -> Self {
354        FlowFieldGroup {
355            agents: Vec::new(),
356            field: None,
357            destination: None,
358            separation_weight: 1.5,
359        }
360    }
361
362    pub fn add_agent(&mut self, agent: FlowFieldAgent) {
363        self.agents.push(agent);
364    }
365
366    /// Set the shared destination and (re)build the flow field.
367    pub fn set_destination(&mut self, dest: Vec2, field: FlowField) {
368        self.destination = Some(dest);
369        self.field = Some(field);
370    }
371
372    /// Update all agents: apply flow field direction + separation.
373    pub fn update(&mut self, dt: f32) {
374        let Some(ref field) = self.field else { return; };
375        let n = self.agents.len();
376
377        // Gather desired velocities
378        let mut desired: Vec<Vec2> = self.agents.iter().map(|a| {
379            let dir = field.get_direction(a.position);
380            dir * a.speed
381        }).collect();
382
383        // Add separation from nearby agents
384        for i in 0..n {
385            let mut sep = Vec2::ZERO;
386            for j in 0..n {
387                if i == j { continue; }
388                let diff = self.agents[i].position - self.agents[j].position;
389                let dist = diff.length();
390                let min_dist = self.agents[i].radius + self.agents[j].radius + 0.5;
391                if dist < min_dist && dist > 0.0 {
392                    sep += (diff / dist) * (min_dist - dist);
393                }
394            }
395            desired[i] += sep * self.separation_weight;
396        }
397
398        // Apply
399        for (agent, vel) in self.agents.iter_mut().zip(desired.iter()) {
400            agent.velocity = *vel;
401            agent.position += agent.velocity * dt;
402        }
403    }
404
405    /// Check how many agents have reached the destination.
406    pub fn arrived_count(&self, threshold: f32) -> usize {
407        let Some(dest) = self.destination else { return 0; };
408        self.agents.iter().filter(|a| a.position.distance(dest) <= threshold).count()
409    }
410
411    pub fn all_arrived(&self, threshold: f32) -> bool {
412        self.arrived_count(threshold) == self.agents.len()
413    }
414}
415
416impl Default for FlowFieldGroup {
417    fn default() -> Self { Self::new() }
418}
419
420// ---------------------------------------------------------------------------
421// Reynolds Flocking
422// ---------------------------------------------------------------------------
423
424/// An individual boid in the flock.
425#[derive(Debug, Clone)]
426pub struct Boid {
427    pub position: Vec2,
428    pub velocity: Vec2,
429    pub max_speed: f32,
430    pub max_force: f32,
431}
432
433impl Boid {
434    pub fn new(position: Vec2, velocity: Vec2, max_speed: f32, max_force: f32) -> Self {
435        Boid { position, velocity, max_speed, max_force }
436    }
437
438    fn limit_force(&self, force: Vec2) -> Vec2 {
439        let len = force.length();
440        if len > self.max_force { force / len * self.max_force } else { force }
441    }
442
443    fn limit_speed(&self, vel: Vec2) -> Vec2 {
444        let len = vel.length();
445        if len > self.max_speed { vel / len * self.max_speed } else { vel }
446    }
447}
448
449/// Reynolds flocking simulation (separation, alignment, cohesion).
450#[derive(Debug, Clone)]
451pub struct Flock {
452    pub boids: Vec<Boid>,
453    pub separation_weight: f32,
454    pub alignment_weight: f32,
455    pub cohesion_weight: f32,
456    /// Radius within which a boid considers others its neighbors.
457    pub neighbor_radius: f32,
458    /// Minimum distance before separation kicks in.
459    pub separation_radius: f32,
460    /// Optional world bounds (min, max).
461    pub bounds: Option<(Vec2, Vec2)>,
462}
463
464impl Flock {
465    pub fn new() -> Self {
466        Flock {
467            boids: Vec::new(),
468            separation_weight: 1.5,
469            alignment_weight: 1.0,
470            cohesion_weight: 1.0,
471            neighbor_radius: 5.0,
472            separation_radius: 2.0,
473            bounds: None,
474        }
475    }
476
477    /// Add a boid with a given position and initial velocity.
478    pub fn add_agent(&mut self, pos: Vec2, vel: Vec2) {
479        self.boids.push(Boid::new(pos, vel, 5.0, 0.5));
480    }
481
482    pub fn add_boid(&mut self, boid: Boid) {
483        self.boids.push(boid);
484    }
485
486    /// Advance the simulation by `dt` seconds.
487    pub fn update(&mut self, dt: f32) {
488        let n = self.boids.len();
489        let mut steering = vec![Vec2::ZERO; n];
490
491        for i in 0..n {
492            let sep = self.separation_force(i);
493            let ali = self.alignment_force(i);
494            let coh = self.cohesion_force(i);
495            steering[i] = sep * self.separation_weight
496                        + ali * self.alignment_weight
497                        + coh * self.cohesion_weight;
498            steering[i] = self.boids[i].limit_force(steering[i]);
499        }
500
501        for (boid, steer) in self.boids.iter_mut().zip(steering.iter()) {
502            boid.velocity = boid.limit_speed(boid.velocity + *steer * dt);
503            boid.position += boid.velocity * dt;
504        }
505
506        // Wrap or clamp to bounds
507        if let Some((bmin, bmax)) = self.bounds {
508            for boid in self.boids.iter_mut() {
509                // Wrap around
510                if boid.position.x < bmin.x { boid.position.x = bmax.x; }
511                if boid.position.x > bmax.x { boid.position.x = bmin.x; }
512                if boid.position.y < bmin.y { boid.position.y = bmax.y; }
513                if boid.position.y > bmax.y { boid.position.y = bmin.y; }
514            }
515        }
516    }
517
518    /// Separation: steer to avoid crowding local flockmates.
519    pub fn separation_force(&self, idx: usize) -> Vec2 {
520        let boid = &self.boids[idx];
521        let mut force = Vec2::ZERO;
522        let mut count = 0;
523        for (i, other) in self.boids.iter().enumerate() {
524            if i == idx { continue; }
525            let diff = boid.position - other.position;
526            let dist = diff.length();
527            if dist < self.separation_radius && dist > 0.0 {
528                force += diff.normalize() / dist; // weighted by inverse distance
529                count += 1;
530            }
531        }
532        if count > 0 {
533            force /= count as f32;
534            if force.length_squared() > 0.0 {
535                force = force.normalize() * boid.max_speed - boid.velocity;
536                force = boid.limit_force(force);
537            }
538        }
539        force
540    }
541
542    /// Alignment: steer toward the average heading of local flockmates.
543    pub fn alignment_force(&self, idx: usize) -> Vec2 {
544        let boid = &self.boids[idx];
545        let mut avg_vel = Vec2::ZERO;
546        let mut count = 0;
547        for (i, other) in self.boids.iter().enumerate() {
548            if i == idx { continue; }
549            if boid.position.distance(other.position) < self.neighbor_radius {
550                avg_vel += other.velocity;
551                count += 1;
552            }
553        }
554        if count == 0 { return Vec2::ZERO; }
555        avg_vel /= count as f32;
556        let force = avg_vel - boid.velocity;
557        boid.limit_force(force)
558    }
559
560    /// Cohesion: steer toward the average position of local flockmates.
561    pub fn cohesion_force(&self, idx: usize) -> Vec2 {
562        let boid = &self.boids[idx];
563        let mut center = Vec2::ZERO;
564        let mut count = 0;
565        for (i, other) in self.boids.iter().enumerate() {
566            if i == idx { continue; }
567            if boid.position.distance(other.position) < self.neighbor_radius {
568                center += other.position;
569                count += 1;
570            }
571        }
572        if count == 0 { return Vec2::ZERO; }
573        center /= count as f32;
574        let desired = center - boid.position;
575        if desired.length_squared() == 0.0 { return Vec2::ZERO; }
576        let desired = desired.normalize() * boid.max_speed;
577        let force = desired - boid.velocity;
578        boid.limit_force(force)
579    }
580
581    /// Add a seek force toward a target for all boids.
582    pub fn seek_target(&mut self, target: Vec2, weight: f32, dt: f32) {
583        for boid in self.boids.iter_mut() {
584            let diff = target - boid.position;
585            if diff.length_squared() < 0.0001 { continue; }
586            let desired = diff.normalize() * boid.max_speed;
587            let steer = boid.limit_force(desired - boid.velocity) * weight;
588            boid.velocity = boid.limit_speed(boid.velocity + steer * dt);
589        }
590    }
591
592    /// Returns centroid of the flock.
593    pub fn centroid(&self) -> Vec2 {
594        if self.boids.is_empty() { return Vec2::ZERO; }
595        let sum: Vec2 = self.boids.iter().map(|b| b.position).sum();
596        sum / self.boids.len() as f32
597    }
598
599    /// Returns average speed of the flock.
600    pub fn average_speed(&self) -> f32 {
601        if self.boids.is_empty() { return 0.0; }
602        self.boids.iter().map(|b| b.velocity.length()).sum::<f32>() / self.boids.len() as f32
603    }
604}
605
606impl Default for Flock {
607    fn default() -> Self { Self::new() }
608}
609
610// ---------------------------------------------------------------------------
611// DynamicObstacleField — runtime cost modification
612// ---------------------------------------------------------------------------
613
614/// Applies temporary cost increases to a flow field (e.g., for dynamic obstacles).
615#[derive(Debug, Clone)]
616pub struct DynamicObstacleField {
617    /// (world_pos, radius, cost_multiplier)
618    obstacles: Vec<(Vec2, f32, f32)>,
619}
620
621impl DynamicObstacleField {
622    pub fn new() -> Self { DynamicObstacleField { obstacles: Vec::new() } }
623
624    pub fn add_obstacle(&mut self, center: Vec2, radius: f32, cost_multiplier: f32) {
625        self.obstacles.push((center, radius, cost_multiplier));
626    }
627
628    pub fn clear(&mut self) { self.obstacles.clear(); }
629
630    /// Apply obstacles to a `FlowField`'s cost array and rebuild.
631    pub fn apply_and_rebuild(&self, field: &mut FlowField, goals: &[(usize, usize)]) {
632        for &(center, radius, mult) in &self.obstacles {
633            let (cx, cy) = field.world_to_grid(center);
634            let cell_r = (radius / field.cell_size).ceil() as i32 + 1;
635            for dy in -cell_r..=cell_r {
636                for dx in -cell_r..=cell_r {
637                    let nx = cx as i32 + dx;
638                    let ny = cy as i32 + dy;
639                    if nx < 0 || ny < 0 || nx >= field.width as i32 || ny >= field.height as i32 {
640                        continue;
641                    }
642                    let world_pos = field.grid_to_world(nx as usize, ny as usize);
643                    if world_pos.distance(center) <= radius {
644                        let i = ny as usize * field.width + nx as usize;
645                        field.costs[i] = (field.costs[i] * mult).min(f32::INFINITY);
646                    }
647                }
648            }
649        }
650        field.build_integration_field(goals);
651        field.build_direction_field();
652    }
653}
654
655impl Default for DynamicObstacleField {
656    fn default() -> Self { Self::new() }
657}
658
659// ---------------------------------------------------------------------------
660// Unit tests
661// ---------------------------------------------------------------------------
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666    use glam::Vec2;
667
668    fn make_field(w: usize, h: usize) -> FlowField {
669        FlowField::new(w, h, 1.0)
670    }
671
672    #[test]
673    fn test_build_integration_field() {
674        let mut field = make_field(10, 10);
675        field.build_integration_field(&[(9, 9)]);
676        // Goal cell should have cost 0
677        assert_eq!(field.integration[9 * 10 + 9], 0.0);
678        // Origin should have a finite positive cost
679        assert!(field.integration[0].is_finite());
680        assert!(field.integration[0] > 0.0);
681    }
682
683    #[test]
684    fn test_build_direction_field() {
685        let mut field = make_field(10, 10);
686        field.build_integration_field(&[(9, 9)]);
687        field.build_direction_field();
688        // Direction at (0,0) should point roughly toward (9,9)
689        let dir = field.directions[0];
690        assert!(dir.x > 0.0 || dir.y > 0.0);
691    }
692
693    #[test]
694    fn test_get_direction_normalized() {
695        let mut field = make_field(10, 10);
696        field.build_integration_field(&[(9, 9)]);
697        field.build_direction_field();
698        let dir = field.get_direction(Vec2::new(0.5, 0.5));
699        // Should be approximately unit length (or zero)
700        let len = dir.length();
701        assert!(len <= 1.01);
702    }
703
704    #[test]
705    fn test_impassable_cell() {
706        let mut field = make_field(10, 10);
707        field.set_cost(5, 5, f32::INFINITY);
708        field.build_integration_field(&[(9, 9)]);
709        field.build_direction_field();
710        // Impassable cell should have zero direction
711        assert_eq!(field.directions[5 * 10 + 5], Vec2::ZERO);
712    }
713
714    #[test]
715    fn test_multi_goal() {
716        let mut field = make_field(10, 10);
717        field.build_integration_field(&[(0, 0), (9, 9)]);
718        field.build_direction_field();
719        // Cell (5,5) integration should be lower than with single goal
720        let val = field.integration[5 * 10 + 5];
721        assert!(val.is_finite());
722    }
723
724    #[test]
725    fn test_flow_field_cache_lru() {
726        let mut cache = FlowFieldCache::new(2);
727        let costs = vec![1.0f32; 100];
728        let _ = cache.get_or_build((9, 9), 10, 10, 1.0, &costs);
729        let _ = cache.get_or_build((0, 0), 10, 10, 1.0, &costs);
730        assert_eq!(cache.len(), 2);
731        // Adding a third should evict oldest
732        let _ = cache.get_or_build((5, 5), 10, 10, 1.0, &costs);
733        assert_eq!(cache.len(), 2);
734    }
735
736    #[test]
737    fn test_flock_separation() {
738        let mut flock = Flock::new();
739        // Two boids very close together
740        flock.add_agent(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0));
741        flock.add_agent(Vec2::new(0.1, 0.0), Vec2::new(-1.0, 0.0));
742        let sep0 = flock.separation_force(0);
743        assert!(sep0.length() > 0.0);
744    }
745
746    #[test]
747    fn test_flock_alignment() {
748        let mut flock = Flock::new();
749        flock.add_agent(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0));
750        flock.add_agent(Vec2::new(1.0, 0.0), Vec2::new(1.0, 0.0));
751        let ali = flock.alignment_force(0);
752        // Both moving same direction — alignment force should be near zero
753        assert!(ali.length() < 0.1);
754    }
755
756    #[test]
757    fn test_flock_cohesion() {
758        let mut flock = Flock::new();
759        flock.add_agent(Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0));
760        flock.add_agent(Vec2::new(3.0, 0.0), Vec2::new(0.0, 0.0));
761        let coh = flock.cohesion_force(0);
762        // Should point in +x direction
763        assert!(coh.x > 0.0);
764    }
765
766    #[test]
767    fn test_flock_update() {
768        let mut flock = Flock::new();
769        for i in 0..10 {
770            flock.add_agent(Vec2::new(i as f32, 0.0), Vec2::new(1.0, 0.0));
771        }
772        let centroid_before = flock.centroid();
773        flock.update(0.1);
774        let centroid_after = flock.centroid();
775        // Centroid should have moved
776        assert!((centroid_after - centroid_before).length() > 0.0);
777    }
778
779    #[test]
780    fn test_flow_field_group_update() {
781        let mut field = make_field(20, 20);
782        field.build_integration_field(&[(19, 19)]);
783        field.build_direction_field();
784
785        let mut group = FlowFieldGroup::new();
786        for i in 0..5 {
787            group.add_agent(FlowFieldAgent::new(i as u64, Vec2::new(i as f32, 0.0), 2.0, 0.3));
788        }
789        group.set_destination(Vec2::new(19.5, 19.5), field);
790        group.update(0.1);
791        // Agents should have moved
792        for agent in &group.agents {
793            assert!(agent.position.x >= 0.0);
794        }
795    }
796
797    #[test]
798    fn test_dynamic_obstacle_field() {
799        let mut field = make_field(10, 10);
800        let obs = DynamicObstacleField::new();
801        obs.apply_and_rebuild(&mut field, &[(9, 9)]);
802        assert!(field.integration[0].is_finite());
803    }
804
805    #[test]
806    fn test_world_to_grid_round_trip() {
807        let field = make_field(10, 10);
808        let pos = Vec2::new(3.7, 6.2);
809        let (gx, gy) = field.world_to_grid(pos);
810        let world = field.grid_to_world(gx, gy);
811        assert!((world.x - 3.5).abs() < 0.5);
812        assert!((world.y - 6.5).abs() < 0.5);
813    }
814
815    #[test]
816    fn test_flock_seek_target() {
817        let mut flock = Flock::new();
818        flock.add_agent(Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0));
819        flock.seek_target(Vec2::new(10.0, 0.0), 1.0, 0.1);
820        let speed = flock.boids[0].velocity.length();
821        assert!(speed > 0.0);
822    }
823}