Skip to main content

phago_agents/
spawn.rs

1//! Spawn policies for evolutionary agent creation.
2//!
3//! When an agent dies, the colony can spawn a replacement using a policy.
4//! The FitnessSpawnPolicy creates a mutated offspring of the fittest
5//! living agent, implementing biological selection.
6
7use crate::genome::AgentGenome;
8use phago_core::types::{AgentId, Position};
9
10/// Trait for spawn policies.
11pub trait SpawnPolicy {
12    /// Decide whether to spawn a new agent after a death.
13    ///
14    /// Returns the genome and position for the new agent, or None if
15    /// no spawn should occur (e.g., population cap reached).
16    fn on_death(
17        &mut self,
18        dead_id: AgentId,
19        alive_count: usize,
20        fittest_genome: Option<&AgentGenome>,
21        fittest_position: Option<Position>,
22    ) -> Option<(AgentGenome, Position)>;
23}
24
25/// Fitness-based spawn: create mutated offspring of the fittest agent.
26pub struct FitnessSpawnPolicy {
27    /// Maximum population size.
28    pub max_population: usize,
29    /// Mutation rate for offspring genomes.
30    pub mutation_rate: f64,
31    /// Counter for seeding mutations.
32    spawn_counter: u64,
33}
34
35impl FitnessSpawnPolicy {
36    pub fn new(max_population: usize, mutation_rate: f64) -> Self {
37        Self {
38            max_population,
39            mutation_rate,
40            spawn_counter: 0,
41        }
42    }
43}
44
45impl SpawnPolicy for FitnessSpawnPolicy {
46    fn on_death(
47        &mut self,
48        _dead_id: AgentId,
49        alive_count: usize,
50        fittest_genome: Option<&AgentGenome>,
51        fittest_position: Option<Position>,
52    ) -> Option<(AgentGenome, Position)> {
53        // Don't spawn if at or above population cap
54        if alive_count >= self.max_population {
55            return None;
56        }
57
58        // Need a fittest agent to inherit from
59        let parent_genome = fittest_genome?;
60        let parent_pos = fittest_position.unwrap_or(Position::new(0.0, 0.0));
61
62        self.spawn_counter += 1;
63        let offspring_genome = parent_genome.mutate(self.mutation_rate, self.spawn_counter);
64
65        // Spawn near parent with slight offset
66        let offset_x = ((self.spawn_counter as f64 * 2.7).sin()) * 3.0;
67        let offset_y = ((self.spawn_counter as f64 * 1.3).cos()) * 3.0;
68        let position = Position::new(parent_pos.x + offset_x, parent_pos.y + offset_y);
69
70        Some((offspring_genome, position))
71    }
72}
73
74/// No-spawn policy: never create new agents (static population).
75pub struct NoSpawnPolicy;
76
77impl SpawnPolicy for NoSpawnPolicy {
78    fn on_death(
79        &mut self,
80        _dead_id: AgentId,
81        _alive_count: usize,
82        _fittest_genome: Option<&AgentGenome>,
83        _fittest_position: Option<Position>,
84    ) -> Option<(AgentGenome, Position)> {
85        None
86    }
87}
88
89/// Random spawn policy: create agents with random genomes (control group).
90pub struct RandomSpawnPolicy {
91    pub max_population: usize,
92    spawn_counter: u64,
93}
94
95impl RandomSpawnPolicy {
96    pub fn new(max_population: usize) -> Self {
97        Self {
98            max_population,
99            spawn_counter: 0,
100        }
101    }
102}
103
104impl SpawnPolicy for RandomSpawnPolicy {
105    fn on_death(
106        &mut self,
107        _dead_id: AgentId,
108        alive_count: usize,
109        _fittest_genome: Option<&AgentGenome>,
110        _fittest_position: Option<Position>,
111    ) -> Option<(AgentGenome, Position)> {
112        if alive_count >= self.max_population {
113            return None;
114        }
115
116        self.spawn_counter += 1;
117        // Large mutation from default = effectively random
118        let genome = AgentGenome::default_genome().mutate(0.8, self.spawn_counter);
119        let x = ((self.spawn_counter as f64 * 3.7).sin()) * 10.0;
120        let y = ((self.spawn_counter as f64 * 2.1).cos()) * 10.0;
121
122        Some((genome, Position::new(x, y)))
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn fitness_spawn_creates_offspring() {
132        let mut policy = FitnessSpawnPolicy::new(10, 0.1);
133        let genome = AgentGenome::default_genome();
134        let pos = Position::new(5.0, 5.0);
135
136        let result = policy.on_death(AgentId::new(), 3, Some(&genome), Some(pos));
137        assert!(result.is_some());
138    }
139
140    #[test]
141    fn fitness_spawn_respects_cap() {
142        let mut policy = FitnessSpawnPolicy::new(5, 0.1);
143        let genome = AgentGenome::default_genome();
144
145        let result = policy.on_death(AgentId::new(), 5, Some(&genome), Some(Position::new(0.0, 0.0)));
146        assert!(result.is_none());
147    }
148
149    #[test]
150    fn no_spawn_never_spawns() {
151        let mut policy = NoSpawnPolicy;
152        let genome = AgentGenome::default_genome();
153        let result = policy.on_death(AgentId::new(), 1, Some(&genome), Some(Position::new(0.0, 0.0)));
154        assert!(result.is_none());
155    }
156}