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        if avg_vel.length_squared() > 0.0 {
557            avg_vel = avg_vel.normalize() * boid.max_speed;
558        }
559        let force = avg_vel - boid.velocity;
560        boid.limit_force(force)
561    }
562
563    /// Cohesion: steer toward the average position of local flockmates.
564    pub fn cohesion_force(&self, idx: usize) -> Vec2 {
565        let boid = &self.boids[idx];
566        let mut center = Vec2::ZERO;
567        let mut count = 0;
568        for (i, other) in self.boids.iter().enumerate() {
569            if i == idx { continue; }
570            if boid.position.distance(other.position) < self.neighbor_radius {
571                center += other.position;
572                count += 1;
573            }
574        }
575        if count == 0 { return Vec2::ZERO; }
576        center /= count as f32;
577        let desired = center - boid.position;
578        if desired.length_squared() == 0.0 { return Vec2::ZERO; }
579        let desired = desired.normalize() * boid.max_speed;
580        let force = desired - boid.velocity;
581        boid.limit_force(force)
582    }
583
584    /// Add a seek force toward a target for all boids.
585    pub fn seek_target(&mut self, target: Vec2, weight: f32, dt: f32) {
586        for boid in self.boids.iter_mut() {
587            let diff = target - boid.position;
588            if diff.length_squared() < 0.0001 { continue; }
589            let desired = diff.normalize() * boid.max_speed;
590            let steer = boid.limit_force(desired - boid.velocity) * weight;
591            boid.velocity = boid.limit_speed(boid.velocity + steer * dt);
592        }
593    }
594
595    /// Returns centroid of the flock.
596    pub fn centroid(&self) -> Vec2 {
597        if self.boids.is_empty() { return Vec2::ZERO; }
598        let sum: Vec2 = self.boids.iter().map(|b| b.position).sum();
599        sum / self.boids.len() as f32
600    }
601
602    /// Returns average speed of the flock.
603    pub fn average_speed(&self) -> f32 {
604        if self.boids.is_empty() { return 0.0; }
605        self.boids.iter().map(|b| b.velocity.length()).sum::<f32>() / self.boids.len() as f32
606    }
607}
608
609impl Default for Flock {
610    fn default() -> Self { Self::new() }
611}
612
613// ---------------------------------------------------------------------------
614// DynamicObstacleField — runtime cost modification
615// ---------------------------------------------------------------------------
616
617/// Applies temporary cost increases to a flow field (e.g., for dynamic obstacles).
618#[derive(Debug, Clone)]
619pub struct DynamicObstacleField {
620    /// (world_pos, radius, cost_multiplier)
621    obstacles: Vec<(Vec2, f32, f32)>,
622}
623
624impl DynamicObstacleField {
625    pub fn new() -> Self { DynamicObstacleField { obstacles: Vec::new() } }
626
627    pub fn add_obstacle(&mut self, center: Vec2, radius: f32, cost_multiplier: f32) {
628        self.obstacles.push((center, radius, cost_multiplier));
629    }
630
631    pub fn clear(&mut self) { self.obstacles.clear(); }
632
633    /// Apply obstacles to a `FlowField`'s cost array and rebuild.
634    pub fn apply_and_rebuild(&self, field: &mut FlowField, goals: &[(usize, usize)]) {
635        for &(center, radius, mult) in &self.obstacles {
636            let (cx, cy) = field.world_to_grid(center);
637            let cell_r = (radius / field.cell_size).ceil() as i32 + 1;
638            for dy in -cell_r..=cell_r {
639                for dx in -cell_r..=cell_r {
640                    let nx = cx as i32 + dx;
641                    let ny = cy as i32 + dy;
642                    if nx < 0 || ny < 0 || nx >= field.width as i32 || ny >= field.height as i32 {
643                        continue;
644                    }
645                    let world_pos = field.grid_to_world(nx as usize, ny as usize);
646                    if world_pos.distance(center) <= radius {
647                        let i = ny as usize * field.width + nx as usize;
648                        field.costs[i] = (field.costs[i] * mult).min(f32::INFINITY);
649                    }
650                }
651            }
652        }
653        field.build_integration_field(goals);
654        field.build_direction_field();
655    }
656}
657
658impl Default for DynamicObstacleField {
659    fn default() -> Self { Self::new() }
660}
661
662// ---------------------------------------------------------------------------
663// Unit tests
664// ---------------------------------------------------------------------------
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use glam::Vec2;
670
671    fn make_field(w: usize, h: usize) -> FlowField {
672        FlowField::new(w, h, 1.0)
673    }
674
675    #[test]
676    fn test_build_integration_field() {
677        let mut field = make_field(10, 10);
678        field.build_integration_field(&[(9, 9)]);
679        // Goal cell should have cost 0
680        assert_eq!(field.integration[9 * 10 + 9], 0.0);
681        // Origin should have a finite positive cost
682        assert!(field.integration[0].is_finite());
683        assert!(field.integration[0] > 0.0);
684    }
685
686    #[test]
687    fn test_build_direction_field() {
688        let mut field = make_field(10, 10);
689        field.build_integration_field(&[(9, 9)]);
690        field.build_direction_field();
691        // Direction at (0,0) should point roughly toward (9,9)
692        let dir = field.directions[0];
693        assert!(dir.x > 0.0 || dir.y > 0.0);
694    }
695
696    #[test]
697    fn test_get_direction_normalized() {
698        let mut field = make_field(10, 10);
699        field.build_integration_field(&[(9, 9)]);
700        field.build_direction_field();
701        let dir = field.get_direction(Vec2::new(0.5, 0.5));
702        // Should be approximately unit length (or zero)
703        let len = dir.length();
704        assert!(len <= 1.01);
705    }
706
707    #[test]
708    fn test_impassable_cell() {
709        let mut field = make_field(10, 10);
710        field.set_cost(5, 5, f32::INFINITY);
711        field.build_integration_field(&[(9, 9)]);
712        field.build_direction_field();
713        // Impassable cell should have zero direction
714        assert_eq!(field.directions[5 * 10 + 5], Vec2::ZERO);
715    }
716
717    #[test]
718    fn test_multi_goal() {
719        let mut field = make_field(10, 10);
720        field.build_integration_field(&[(0, 0), (9, 9)]);
721        field.build_direction_field();
722        // Cell (5,5) integration should be lower than with single goal
723        let val = field.integration[5 * 10 + 5];
724        assert!(val.is_finite());
725    }
726
727    #[test]
728    fn test_flow_field_cache_lru() {
729        let mut cache = FlowFieldCache::new(2);
730        let costs = vec![1.0f32; 100];
731        let _ = cache.get_or_build((9, 9), 10, 10, 1.0, &costs);
732        let _ = cache.get_or_build((0, 0), 10, 10, 1.0, &costs);
733        assert_eq!(cache.len(), 2);
734        // Adding a third should evict oldest
735        let _ = cache.get_or_build((5, 5), 10, 10, 1.0, &costs);
736        assert_eq!(cache.len(), 2);
737    }
738
739    #[test]
740    fn test_flock_separation() {
741        let mut flock = Flock::new();
742        // Two boids very close together
743        flock.add_agent(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0));
744        flock.add_agent(Vec2::new(0.1, 0.0), Vec2::new(-1.0, 0.0));
745        let sep0 = flock.separation_force(0);
746        assert!(sep0.length() > 0.0);
747    }
748
749    #[test]
750    fn test_flock_alignment() {
751        let mut flock = Flock::new();
752        flock.add_agent(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0));
753        flock.add_agent(Vec2::new(1.0, 0.0), Vec2::new(1.0, 0.0));
754        let ali = flock.alignment_force(0);
755        // Both moving same direction — alignment force should be near zero
756        assert!(ali.length() < 0.1);
757    }
758
759    #[test]
760    fn test_flock_cohesion() {
761        let mut flock = Flock::new();
762        flock.add_agent(Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0));
763        flock.add_agent(Vec2::new(3.0, 0.0), Vec2::new(0.0, 0.0));
764        let coh = flock.cohesion_force(0);
765        // Should point in +x direction
766        assert!(coh.x > 0.0);
767    }
768
769    #[test]
770    fn test_flock_update() {
771        let mut flock = Flock::new();
772        for i in 0..10 {
773            flock.add_agent(Vec2::new(i as f32, 0.0), Vec2::new(1.0, 0.0));
774        }
775        let centroid_before = flock.centroid();
776        flock.update(0.1);
777        let centroid_after = flock.centroid();
778        // Centroid should have moved
779        assert!((centroid_after - centroid_before).length() > 0.0);
780    }
781
782    #[test]
783    fn test_flow_field_group_update() {
784        let mut field = make_field(20, 20);
785        field.build_integration_field(&[(19, 19)]);
786        field.build_direction_field();
787
788        let mut group = FlowFieldGroup::new();
789        for i in 0..5 {
790            group.add_agent(FlowFieldAgent::new(i as u64, Vec2::new(i as f32, 0.0), 2.0, 0.3));
791        }
792        group.set_destination(Vec2::new(19.5, 19.5), field);
793        group.update(0.1);
794        // Agents should have moved
795        for agent in &group.agents {
796            assert!(agent.position.x >= 0.0);
797        }
798    }
799
800    #[test]
801    fn test_dynamic_obstacle_field() {
802        let mut field = make_field(10, 10);
803        let obs = DynamicObstacleField::new();
804        obs.apply_and_rebuild(&mut field, &[(9, 9)]);
805        assert!(field.integration[0].is_finite());
806    }
807
808    #[test]
809    fn test_world_to_grid_round_trip() {
810        let field = make_field(10, 10);
811        let pos = Vec2::new(3.7, 6.2);
812        let (gx, gy) = field.world_to_grid(pos);
813        let world = field.grid_to_world(gx, gy);
814        assert!((world.x - 3.5).abs() < 0.5);
815        assert!((world.y - 6.5).abs() < 0.5);
816    }
817
818    #[test]
819    fn test_flock_seek_target() {
820        let mut flock = Flock::new();
821        flock.add_agent(Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0));
822        flock.seek_target(Vec2::new(10.0, 0.0), 1.0, 0.1);
823        let speed = flock.boids[0].velocity.length();
824        assert!(speed > 0.0);
825    }
826}