Skip to main content

proof_engine/worldgen/
genetics.rs

1//! DNA/genetics system — heritable traits for procedural creatures.
2//!
3//! Models a simplified genome with dominant/recessive alleles, crossover,
4//! mutation, and phenotype expression.
5
6use super::Rng;
7
8/// A single gene with two alleles.
9#[derive(Debug, Clone, Copy)]
10pub struct Gene { pub allele_a: u8, pub allele_b: u8 }
11
12impl Gene {
13    pub fn new(a: u8, b: u8) -> Self { Self { allele_a: a, allele_b: b } }
14    pub fn homozygous(&self) -> bool { self.allele_a == self.allele_b }
15    /// Dominant expression (higher allele value dominates).
16    pub fn express(&self) -> u8 { self.allele_a.max(self.allele_b) }
17    /// Codominant expression (average).
18    pub fn express_codominant(&self) -> f32 { (self.allele_a as f32 + self.allele_b as f32) * 0.5 }
19}
20
21/// Trait categories encoded in the genome.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum TraitType {
24    Size, Strength, Speed, Intelligence, Aggression, Coloration,
25    Pattern, HornSize, TailLength, WingSpan, Armor, Venom,
26    Bioluminescence, Regeneration, Camouflage, SenseAcuity,
27}
28
29/// A complete genome.
30#[derive(Debug, Clone)]
31pub struct Genome {
32    pub genes: Vec<(TraitType, Gene)>,
33    pub mutation_rate: f32,
34}
35
36impl Genome {
37    pub fn random(rng: &mut Rng) -> Self {
38        let traits = [
39            TraitType::Size, TraitType::Strength, TraitType::Speed,
40            TraitType::Intelligence, TraitType::Aggression, TraitType::Coloration,
41            TraitType::Pattern, TraitType::HornSize, TraitType::TailLength,
42            TraitType::WingSpan, TraitType::Armor, TraitType::Venom,
43            TraitType::Bioluminescence, TraitType::Regeneration,
44            TraitType::Camouflage, TraitType::SenseAcuity,
45        ];
46        let genes = traits.iter().map(|&t| {
47            (t, Gene::new(rng.range_u32(0, 256) as u8, rng.range_u32(0, 256) as u8))
48        }).collect();
49        Self { genes, mutation_rate: 0.02 }
50    }
51
52    /// Express phenotype for a trait.
53    pub fn phenotype(&self, trait_type: TraitType) -> f32 {
54        self.genes.iter()
55            .find(|(t, _)| *t == trait_type)
56            .map(|(_, g)| g.express_codominant() / 255.0)
57            .unwrap_or(0.5)
58    }
59
60    /// Crossover: combine two genomes (sexual reproduction).
61    pub fn crossover(&self, other: &Genome, rng: &mut Rng) -> Genome {
62        let mut child_genes = Vec::with_capacity(self.genes.len());
63        for (i, (trait_type, gene_a)) in self.genes.iter().enumerate() {
64            let gene_b = &other.genes[i].1;
65            // Pick one allele from each parent
66            let allele_a = if rng.coin(0.5) { gene_a.allele_a } else { gene_a.allele_b };
67            let allele_b = if rng.coin(0.5) { gene_b.allele_a } else { gene_b.allele_b };
68            child_genes.push((*trait_type, Gene::new(allele_a, allele_b)));
69        }
70        Genome { genes: child_genes, mutation_rate: (self.mutation_rate + other.mutation_rate) * 0.5 }
71    }
72
73    /// Apply random mutations.
74    pub fn mutate(&mut self, rng: &mut Rng) {
75        for (_, gene) in &mut self.genes {
76            if rng.coin(self.mutation_rate) {
77                let delta = (rng.gaussian() * 10.0) as i16;
78                gene.allele_a = (gene.allele_a as i16 + delta).clamp(0, 255) as u8;
79            }
80            if rng.coin(self.mutation_rate) {
81                let delta = (rng.gaussian() * 10.0) as i16;
82                gene.allele_b = (gene.allele_b as i16 + delta).clamp(0, 255) as u8;
83            }
84        }
85    }
86
87    /// Fitness score for a given environment (higher = better adapted).
88    pub fn fitness(&self, env: &Environment) -> f32 {
89        let mut score = 0.0_f32;
90        score += (self.phenotype(TraitType::Size) - env.ideal_size).abs() * -1.0;
91        score += self.phenotype(TraitType::Speed) * env.predation_pressure;
92        score += self.phenotype(TraitType::Camouflage) * env.predation_pressure * 0.5;
93        score += self.phenotype(TraitType::Armor) * env.predation_pressure * 0.3;
94        score += self.phenotype(TraitType::Intelligence) * 0.2;
95        score += 1.0; // base fitness
96        score.max(0.0)
97    }
98}
99
100/// Environmental pressures that affect fitness.
101#[derive(Debug, Clone)]
102pub struct Environment {
103    pub ideal_size: f32,
104    pub predation_pressure: f32,
105    pub food_availability: f32,
106    pub temperature: f32,
107}
108
109impl Default for Environment {
110    fn default() -> Self {
111        Self { ideal_size: 0.5, predation_pressure: 0.5, food_availability: 0.5, temperature: 0.5 }
112    }
113}
114
115/// A population of creatures.
116#[derive(Debug, Clone)]
117pub struct Population {
118    pub genomes: Vec<Genome>,
119    pub generation: u32,
120}
121
122impl Population {
123    pub fn random(size: usize, rng: &mut Rng) -> Self {
124        let genomes = (0..size).map(|_| Genome::random(rng)).collect();
125        Self { genomes, generation: 0 }
126    }
127
128    /// Run one generation of evolution (selection + reproduction + mutation).
129    pub fn evolve(&mut self, env: &Environment, rng: &mut Rng) {
130        let n = self.genomes.len();
131        if n < 2 { return; }
132
133        // Fitness-proportionate selection
134        let fitnesses: Vec<f32> = self.genomes.iter().map(|g| g.fitness(env)).collect();
135        let total_fitness: f32 = fitnesses.iter().sum();
136        if total_fitness < 0.01 { return; }
137
138        let mut next_gen = Vec::with_capacity(n);
139        for _ in 0..n {
140            let parent_a = roulette_select(&fitnesses, total_fitness, rng);
141            let parent_b = roulette_select(&fitnesses, total_fitness, rng);
142            let mut child = self.genomes[parent_a].crossover(&self.genomes[parent_b], rng);
143            child.mutate(rng);
144            next_gen.push(child);
145        }
146
147        self.genomes = next_gen;
148        self.generation += 1;
149    }
150
151    /// Average phenotype for a trait across the population.
152    pub fn avg_phenotype(&self, trait_type: TraitType) -> f32 {
153        if self.genomes.is_empty() { return 0.5; }
154        let sum: f32 = self.genomes.iter().map(|g| g.phenotype(trait_type)).sum();
155        sum / self.genomes.len() as f32
156    }
157}
158
159fn roulette_select(fitnesses: &[f32], total: f32, rng: &mut Rng) -> usize {
160    let mut target = rng.next_f32() * total;
161    for (i, &f) in fitnesses.iter().enumerate() {
162        target -= f;
163        if target <= 0.0 { return i; }
164    }
165    fitnesses.len() - 1
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_genome_random() {
174        let mut rng = Rng::new(42);
175        let g = Genome::random(&mut rng);
176        assert_eq!(g.genes.len(), 16);
177    }
178
179    #[test]
180    fn test_crossover() {
181        let mut rng = Rng::new(42);
182        let a = Genome::random(&mut rng);
183        let b = Genome::random(&mut rng);
184        let child = a.crossover(&b, &mut rng);
185        assert_eq!(child.genes.len(), a.genes.len());
186    }
187
188    #[test]
189    fn test_evolution_changes_population() {
190        let mut rng = Rng::new(42);
191        let mut pop = Population::random(50, &mut rng);
192        let env = Environment { ideal_size: 0.8, predation_pressure: 0.7, ..Default::default() };
193        let initial_speed = pop.avg_phenotype(TraitType::Speed);
194        for _ in 0..100 { pop.evolve(&env, &mut rng); }
195        // With high predation, speed should trend upward
196        let final_speed = pop.avg_phenotype(TraitType::Speed);
197        // Not guaranteed but likely:
198        assert!(pop.generation == 100);
199    }
200
201    #[test]
202    fn test_phenotype_range() {
203        let mut rng = Rng::new(42);
204        let g = Genome::random(&mut rng);
205        let p = g.phenotype(TraitType::Size);
206        assert!(p >= 0.0 && p <= 1.0);
207    }
208}