Skip to main content

rust_synth/math/
life.rs

1//! Conway's Game of Life — deterministic, toroidal (wrapping) grid.
2//!
3//! Stepped from the TUI on every beat boundary (BPM-synchronous).
4
5use super::harmony::rand_u32;
6
7pub struct Life {
8    pub rows: usize,
9    pub cols: usize,
10    grid: Vec<bool>,
11    scratch: Vec<bool>,
12    pub generation: u64,
13}
14
15impl Life {
16    pub fn random(rows: usize, cols: usize, seed: u64, fill: f32) -> Self {
17        let mut s = seed;
18        let n = rows * cols;
19        let mut grid = vec![false; n];
20        let threshold = (fill.clamp(0.0, 1.0) * 1000.0) as u32;
21        for cell in grid.iter_mut() {
22            *cell = rand_u32(&mut s, 1000) < threshold;
23        }
24        Self {
25            rows,
26            cols,
27            grid,
28            scratch: vec![false; n],
29            generation: 0,
30        }
31    }
32
33    #[inline]
34    pub fn alive(&self, r: usize, c: usize) -> bool {
35        self.grid[r * self.cols + c]
36    }
37
38    /// Apply one step of B3/S23. Writes scratch, swaps.
39    pub fn step(&mut self) {
40        let rows = self.rows as isize;
41        let cols = self.cols as isize;
42        let cols_us = self.cols;
43        for r in 0..rows {
44            for c in 0..cols {
45                let mut n = 0u8;
46                for dr in -1..=1 {
47                    for dc in -1..=1 {
48                        if dr == 0 && dc == 0 {
49                            continue;
50                        }
51                        let rr = (r + dr).rem_euclid(rows) as usize;
52                        let cc = (c + dc).rem_euclid(cols) as usize;
53                        if self.grid[rr * cols_us + cc] {
54                            n += 1;
55                        }
56                    }
57                }
58                let idx = r as usize * cols_us + c as usize;
59                let was = self.grid[idx];
60                let now = matches!((was, n), (true, 2) | (true, 3) | (false, 3));
61                self.scratch[idx] = now;
62            }
63        }
64        std::mem::swap(&mut self.grid, &mut self.scratch);
65        self.generation += 1;
66    }
67
68    pub fn alive_count(&self) -> usize {
69        self.grid.iter().filter(|&&b| b).count()
70    }
71
72    pub fn density(&self) -> f32 {
73        self.alive_count() as f32 / (self.rows * self.cols).max(1) as f32
74    }
75
76    /// Cells alive in a single row — used as "fitness" per track.
77    pub fn row_alive_count(&self, r: usize) -> usize {
78        if r >= self.rows {
79            return 0;
80        }
81        let start = r * self.cols;
82        self.grid[start..start + self.cols]
83            .iter()
84            .filter(|&&b| b)
85            .count()
86    }
87
88    /// Cells alive in a single column — used as "present-moment energy".
89    pub fn col_alive_count(&self, c: usize) -> usize {
90        if c >= self.cols {
91            return 0;
92        }
93        let mut n = 0;
94        for r in 0..self.rows {
95            if self.grid[r * self.cols + c] {
96                n += 1;
97            }
98        }
99        n
100    }
101
102    /// Set a single cell (bounds-safe, wraps).
103    pub fn set(&mut self, r: usize, c: usize, alive: bool) {
104        let rr = r % self.rows;
105        let cc = c % self.cols;
106        self.grid[rr * self.cols + cc] = alive;
107    }
108
109    /// Seed a glider near the top-left corner. Useful for non-empty grids
110    /// that would otherwise die quickly.
111    pub fn inject_glider(&mut self, r0: usize, c0: usize) {
112        let cells = [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)];
113        for (dr, dc) in cells {
114            let r = (r0 + dr) % self.rows;
115            let c = (c0 + dc) % self.cols;
116            self.grid[r * self.cols + c] = true;
117        }
118    }
119
120    /// Random splash of live cells to jolt a stagnant grid.
121    pub fn sprinkle(&mut self, seed: &mut u64, count: usize) {
122        for _ in 0..count {
123            let r = rand_u32(seed, self.rows as u32) as usize;
124            let c = rand_u32(seed, self.cols as u32) as usize;
125            self.grid[r * self.cols + c] = true;
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn blinker_oscillates() {
136        let mut life = Life::random(5, 5, 0, 0.0);
137        // Place a horizontal blinker.
138        life.grid[2 * 5 + 1] = true;
139        life.grid[2 * 5 + 2] = true;
140        life.grid[2 * 5 + 3] = true;
141        let before = life.alive_count();
142        life.step();
143        let after = life.alive_count();
144        life.step();
145        let back = life.alive_count();
146        assert_eq!(before, 3);
147        assert_eq!(after, 3);
148        assert_eq!(back, 3);
149    }
150
151    #[test]
152    fn empty_stays_empty() {
153        let mut life = Life::random(6, 6, 99, 0.0);
154        life.step();
155        assert_eq!(life.alive_count(), 0);
156    }
157}