Skip to main content

evolution_ternary/
population.rs

1//! Core population types — agents with ternary strategy vectors.
2
3use core::fmt;
4
5/// A single value in the ternary alphabet {0, 1, 2}.
6pub type TernaryValue = u8;
7
8/// Validate that a value is in {0, 1, 2}.
9fn valid_ternary(v: TernaryValue) -> bool {
10    matches!(v, 0 | 1 | 2)
11}
12
13/// An agent whose strategy is a vector of ternary values.
14#[derive(Clone, PartialEq)]
15pub struct TernaryAgent {
16    /// The strategy vector — each element is 0, 1, or 2.
17    pub strategy: Vec<TernaryValue>,
18    /// Cached fitness (set by the engine after evaluation).
19    pub fitness: f64,
20}
21
22impl TernaryAgent {
23    /// Create a new agent, validating all values are ternary.
24    pub fn new(strategy: Vec<TernaryValue>) -> Option<Self> {
25        if strategy.iter().all(|&v| valid_ternary(v)) {
26            Some(Self { strategy, fitness: 0.0 })
27        } else {
28            None
29        }
30    }
31
32    /// Create an agent without validation (for internal use where values are known good).
33    pub fn new_unchecked(strategy: Vec<TernaryValue>) -> Self {
34        Self { strategy, fitness: 0.0 }
35    }
36
37    /// Strategy length.
38    pub fn len(&self) -> usize {
39        self.strategy.len()
40    }
41
42    /// Whether the strategy is empty.
43    pub fn is_empty(&self) -> bool {
44        self.strategy.is_empty()
45    }
46}
47
48impl fmt::Debug for TernaryAgent {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "Agent({:?}, fit={:.3})", self.strategy, self.fitness)
51    }
52}
53
54/// A population of ternary agents.
55#[derive(Clone)]
56pub struct TernaryPopulation {
57    pub agents: Vec<TernaryAgent>,
58    /// Strategy vector length (constant across the population).
59    pub strategy_len: usize,
60}
61
62impl TernaryPopulation {
63    /// Create a new population from a list of agents.
64    ///
65    /// Returns `None` if agents have mismatched strategy lengths or the list is empty.
66    pub fn new(agents: Vec<TernaryAgent>) -> Option<Self> {
67        if agents.is_empty() {
68            return None;
69        }
70        let len = agents[0].len();
71        if !agents.iter().all(|a| a.len() == len) {
72            return None;
73        }
74        Some(Self { agents, strategy_len: len })
75    }
76
77    /// Create a random population of `n` agents, each with `strategy_len` ternary values.
78    pub fn random(n: usize, strategy_len: usize) -> Self {
79        let mut rng = SimpleRng::from_seed(42);
80        let agents = (0..n)
81            .map(|_| {
82                let strategy: Vec<TernaryValue> = (0..strategy_len)
83                    .map(|_| (rng.next_u32() % 3) as TernaryValue)
84                    .collect();
85                TernaryAgent::new_unchecked(strategy)
86            })
87            .collect();
88        Self { agents, strategy_len }
89    }
90
91    /// Create a random population with a given seed for reproducibility.
92    pub fn random_seeded(n: usize, strategy_len: usize, seed: u64) -> Self {
93        let mut rng = SimpleRng::from_seed(seed);
94        let agents = (0..n)
95            .map(|_| {
96                let strategy: Vec<TernaryValue> = (0..strategy_len)
97                    .map(|_| (rng.next_u32() % 3) as TernaryValue)
98                    .collect();
99                TernaryAgent::new_unchecked(strategy)
100            })
101            .collect();
102        Self { agents, strategy_len }
103    }
104
105    /// Population size.
106    pub fn size(&self) -> usize {
107        self.agents.len()
108    }
109
110    /// Sort agents by fitness descending (natural selection = sort).
111    pub fn sort_by_fitness(&mut self) {
112        self.agents.sort_by(|a, b| b.fitness.partial_cmp(&a.fitness).unwrap_or(std::cmp::Ordering::Equal));
113    }
114
115    /// Get the best agent.
116    pub fn best(&self) -> &TernaryAgent {
117        self.agents.iter().max_by(|a, b| a.fitness.partial_cmp(&b.fitness).unwrap_or(std::cmp::Ordering::Equal)).unwrap()
118    }
119
120    /// Get the average fitness.
121    pub fn avg_fitness(&self) -> f64 {
122        if self.agents.is_empty() { return 0.0; }
123        self.agents.iter().map(|a| a.fitness).sum::<f64>() / self.agents.len() as f64
124    }
125
126    /// Get the worst agent.
127    pub fn worst(&self) -> &TernaryAgent {
128        self.agents.iter().min_by(|a, b| a.fitness.partial_cmp(&b.fitness).unwrap_or(std::cmp::Ordering::Equal)).unwrap()
129    }
130}
131
132/// Minimal xorshift PRNG — no external deps needed.
133#[derive(Clone)]
134pub struct SimpleRng {
135    state: u64,
136}
137
138impl SimpleRng {
139    pub fn from_seed(seed: u64) -> Self {
140        // Ensure non-zero state
141        Self { state: if seed == 0 { 0xDEAD_BEEF_CAFE_BABE } else { seed } }
142    }
143
144    pub fn next_u32(&mut self) -> u32 {
145        // xorshift64
146        self.state ^= self.state << 13;
147        self.state ^= self.state >> 7;
148        self.state ^= self.state << 17;
149        self.state as u32
150    }
151
152    pub fn next_u64(&mut self) -> u64 {
153        let hi = self.next_u32() as u64;
154        let lo = self.next_u32() as u64;
155        (hi << 32) | lo
156    }
157
158    /// Uniform f64 in [0, 1).
159    pub fn next_f64(&mut self) -> f64 {
160        (self.next_u32() as f64) / (u32::MAX as f64)
161    }
162}