use super::USER;
use crate::kit::{CYAN, PINK, Rng, SUN};
use crate::paint::{Ink, Palette, Rgb};
use crate::recipe::{Glyphs, Recipe};
use crate::stage::{Canvas, Shader, Stage, TICK_SECS};
pub(super) const RECIPE: Recipe = Recipe {
name: "Cambrian",
blurb: "rival automata fight for the caption",
credit: USER,
needs: Glyphs::BASE,
summon: |seed| Box::new(Cambrian::new(seed)),
};
const FARE: f64 = 20.0;
const RUSH: f64 = 4.0;
const WINDOW: f64 = 0.5;
const STRIKE: f64 = 0.3;
const GENESIS: f64 = 0.6;
const BARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
const DEAD: f64 = 0.42;
struct Genome {
rule: u8,
color: Rgb,
misfire: f64,
}
const GENOMES: [Genome; 3] = [
Genome {
rule: 110,
color: CYAN,
misfire: 0.0112,
},
Genome {
rule: 126,
color: PINK,
misfire: 0.0241,
},
Genome {
rule: 150,
color: SUN,
misfire: 0.0010,
},
];
const _: () = {
let mut index = 0;
while index < GENOMES.len() {
assert!(
GENOMES[index].rule & 0b01 == 0,
"a rule lights an empty window"
);
assert!(
GENOMES[index].rule & 0b10 != 0,
"a rule cannot grow from the corner"
);
index += 1;
}
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Species(usize);
impl Species {
fn random(rng: &mut Rng) -> Self {
Self(rng.below(GENOMES.len()))
}
fn mutant(self, rng: &mut Rng) -> Self {
let other = rng.below(GENOMES.len() - 1);
Self(if other >= self.0 { other + 1 } else { other })
}
fn genome(self) -> &'static Genome {
&GENOMES[self.0]
}
fn ink(self, stage: &Stage, palette: Palette) -> Rgb {
let lit = self.genome().color.blend(palette.fg, 0.65);
if stage.busy() {
lit
} else {
lit.blend(palette.grey(DEAD), 0.5)
}
}
}
#[derive(Default)]
struct World {
cells: Vec<Option<Species>>,
}
struct Generation {
mutants: Vec<(usize, Species)>,
sample: Option<Species>,
genesis: Option<Species>,
}
impl World {
fn fit(&mut self, width: usize, rng: &mut Rng) -> Option<Species> {
let kept = self.cells.len().min(width);
let mut cells = vec![None; width - kept];
cells.extend_from_slice(&self.cells[self.cells.len() - kept..]);
self.cells = cells;
self.reseed(rng)
}
fn reseed(&mut self, rng: &mut Rng) -> Option<Species> {
if self.cells.iter().any(Option::is_some) {
return None;
}
let corner = self.cells.last_mut()?;
let seed = Species::random(rng);
*corner = Some(seed);
Some(seed)
}
fn generate(&mut self, rng: &mut Rng) -> Generation {
let cells = &self.cells;
let mut mutants = Vec::new();
let mut sample = None;
let mut births = 0;
let next = (0..cells.len())
.map(|at| {
let window = [
at.checked_sub(1).and_then(|left| cells[left]),
cells[at],
cells.get(at + 1).copied().flatten(),
];
let live = window.iter().flatten().count();
if live == 0 {
return None;
}
let species = *window.iter().flatten().nth(rng.below(live))?;
let pattern = window
.iter()
.fold(0, |pattern, cell| pattern << 1 | u8::from(cell.is_some()));
let genome = species.genome();
let born = if (genome.rule >> pattern) & 1 == 1 {
species
} else if rng.chance(genome.misfire) {
let mutant = species.mutant(rng);
mutants.push((at, mutant));
mutant
} else {
return None;
};
births += 1;
if rng.below(births) == 0 {
sample = Some(born);
}
Some(born)
})
.collect();
self.cells = next;
Generation {
mutants,
sample,
genesis: self.reseed(rng),
}
}
}
struct Cambrian {
rng: Rng,
world: World,
captions: i64,
phase: f64,
sample: Option<Species>,
strikes: Vec<(usize, Species)>,
struck: f64,
genesis: f64,
}
impl Cambrian {
fn new(seed: u64) -> Self {
Self {
rng: Rng::new(seed),
world: World::default(),
captions: 0,
phase: 0.0,
sample: None,
strikes: Vec::new(),
struck: f64::NEG_INFINITY,
genesis: f64::NEG_INFINITY,
}
}
fn sow(&mut self, seed: Species, t: f64) {
self.sample = Some(seed);
self.genesis = t;
}
}
impl Shader for Cambrian {
fn tick(&mut self, stage: &Stage) {
if stage.captions != self.captions {
self.captions = stage.captions;
if let Some(seed) = self.world.fit(stage.caption.len(), &mut self.rng) {
self.sow(seed, stage.t);
}
}
if !stage.busy() {
return;
}
self.phase = (stage.flow(WINDOW) / FARE)
.min(RUSH)
.mul_add(TICK_SECS, self.phase);
if self.phase < 1.0 {
return;
}
self.phase -= 1.0;
let Generation {
mutants,
sample,
genesis,
} = self.world.generate(&mut self.rng);
if !mutants.is_empty() {
self.strikes = mutants;
self.struck = stage.t;
}
match genesis {
Some(seed) => self.sow(seed, stage.t),
None => self.sample = sample.or(self.sample),
}
}
fn status(&self, stage: &Stage, palette: Palette, canvas: &mut Canvas) {
let t = stage.t;
let dead = palette.grey(DEAD);
let label = |species: Species| format!("{:03}", species.genome().rule);
let strike = self
.strikes
.last()
.filter(|_| t - self.struck < STRIKE)
.map(|(_, mutant)| *mutant);
let flare = strike.or(self.sample.filter(|_| t - self.genesis < GENESIS));
canvas.mark = match (flare, self.sample) {
(Some(species), _) => label(species)
.chars()
.map(|glyph| {
Ink::of(glyph)
.fg(palette.bg)
.bg(species.ink(stage, palette))
.bold(true)
})
.collect(),
(None, Some(species)) => label(species)
.chars()
.map(|glyph| Ink::of(glyph).fg(species.ink(stage, palette)).bold(true))
.collect(),
(None, None) => vec![Ink::of('·').fg(dead); 3],
};
let corner = self.world.cells.len().checked_sub(1);
canvas.letters = stage.letters(|cell, _| {
let Some(Some(species)) = self.world.cells.get(cell).copied() else {
return Ink::default().fg(dead);
};
let mut paint = Ink::default().fg(species.ink(stage, palette));
let struck = self
.strikes
.iter()
.any(|(at, _)| *at == cell && t - self.struck < STRIKE);
if struck {
let fade = 1.0 - (t - self.struck) / STRIKE;
paint = paint
.bg(species.ink(stage, palette).blend(palette.bg, 0.55 * fade))
.bold(true);
} else if corner == Some(cell) && t - self.genesis < GENESIS {
let fade = 1.0 - (t - self.genesis) / GENESIS;
paint = paint
.bg(species.ink(stage, palette).blend(palette.bg, 0.6 * fade))
.bold(true);
}
paint
});
let mut census = [0; GENOMES.len()];
for Species(index) in self.world.cells.iter().flatten() {
census[*index] += 1;
}
let living: usize = census.iter().sum();
canvas.tail = std::iter::once(Ink::of(' '))
.chain(census.iter().enumerate().map(|(index, count)| {
if *count == 0 {
return Ink::of('·').fg(dead);
}
let height = (count * BARS.len()).div_ceil(living);
Ink::of(BARS[height - 1]).fg(Species(index).ink(stage, palette))
}))
.collect();
}
fn bullet(&self, stage: &Stage, palette: Palette) -> Ink {
Ink::of('•').fg(self.sample.map_or_else(
|| palette.grey( 0.6),
|species| species.ink(stage, palette),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_species_takes_the_crown_at_every_length() {
let mut rng = Rng::new( 0xca3b);
for width in [7, 16, 27, 45] {
let mut crowned = [0_u32; GENOMES.len()];
for _ in 0..4 {
let mut world = World::default();
world.fit(width, &mut rng);
let mut crown = 0;
for _ in 0..12_000 {
world.generate(&mut rng);
let mut census = [0; GENOMES.len()];
for Species(index) in world.cells.iter().flatten() {
census[*index] += 1;
}
for (index, count) in census.iter().enumerate() {
if *count > census[crown] {
crown = index;
}
}
crowned[crown] += 1;
}
}
let total: u32 = crowned.iter().sum();
for (genome, reigns) in GENOMES.iter().zip(crowned) {
assert!(
reigns * 8 >= total,
"rule {} starves at {width} cells: {crowned:?}",
genome.rule
);
}
}
}
}