use std::ops::{Index,IndexMut}; use rand::prelude::random; // it's premature abstraction to make this a trait, but whatever! trait Pixel: Copy + Clone { // if we make an image with these as pixels, this'll be the // default background fn empty() -> Self; // how many color stops do we want to report in the PPM file? fn depth() -> usize; // print this. (hooboy, this ain't efficient, but eh: fast enough // for the one-off script this is! if I turn this into a library, // I'd make this take a formatter instead) fn show(&self) -> String; } // I left this in after testing this for a simple grayscale version impl Pixel for bool { // defaults to white fn empty() -> bool { false } fn depth() -> usize { 1 } fn show(&self) -> String { if *self { "1 1 1" } else { "0 0 0" }.to_string() } } // The three kinds of pixels: Black, White, and Red #[derive(Copy, Clone)] enum Px { Black, White, Red } impl Px { // we use this in the CA impl, later on fn idx(&self) -> usize { match self { Px::Black => 0, Px::White => 1, Px::Red => 2, } } } impl Pixel for Px { fn empty() -> Px { Px::White } fn depth() -> usize { 1 } fn show(&self) -> String { match self { Px::Black => "0 0 0", Px::White => "1 1 1", Px::Red => "1 0 0", }.to_string() } } // Our simple image abstraction struct Image { width: usize, height: usize, // we maintain the invariant that the length of `data` here is // `width * height`. (Or at least, if we don't, things // crash. Fun!) data: Vec, } impl Image { fn new(width: usize, height: usize) -> Image { let data = vec![T::empty(); width*height]; Image { width, height, data, } } // This prints the PPM file: fn show(&self) -> String { let mut str = String::new(); str.push_str("P3\n"); str.push_str(&format!("{} {}\n", self.width, self.height)); str.push_str(&format!("{}\n", T::depth())); for px in self.data.iter() { str.push_str(&format!("{} ", px.show())); } str } // This looks up the pixel, but returns an 'empty' pixel if we // can't find it. fn get(&self, (x, y): (usize, usize)) -> T { // ...I only just realized while commenting this file that // this is wrong, but I'm too lazy to fix it now. *self.data.get(x + y * self.height).unwrap_or(&T::empty()) } } // This lets us index into our image using a tuple as coordinate! impl Index<(usize, usize)> for Image { type Output = T; fn index(&self, (x, y): (usize, usize)) -> &T { &self.data[x + y * self.height] } } // This lets us modify our image too! impl IndexMut<(usize, usize)> for Image { fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut T { &mut self.data[x + y * self.height] } } // Okay, here's where the CA stuff comes in. So: a given 'generation' // in this system is a vector of cells, where each cell is either // black, white, or red. Each subsequent generation, a cell turns into // a new cell based on a rule which applies to the previous // generation, looking at the same cell and its immediate // neighbors. With `n` possible states, that gives us `n**3` possible // 'neighborhoods': in this case, 27. So we can describe an automaton // of this form by simply enumerating the resut for each of the // neighborhoods, which is why this has 27 different `Px` values: one // for each possible neighborhood. struct Rule { result: [Px;27], } impl Rule { // We can describe a given automaton by using a string of 27 // characters. We use this to create the filenames, so we could in // theory reproduce the automaton again later on fn descr(&self) -> String { let mut str = String::new(); for r in self.result.iter() { str.push(match r { Px::White => 'w', Px::Black => 'b', Px::Red => 'r', }) } str } // This implements the logic (which is really just the lookup) for // 'how do we know the cell at generation n given the neighboor at // generation n-1?' fn step(&self, (l, c, r): (Px, Px, Px)) -> Px { let index = l.idx() + c.idx() * 3 + r.idx() * 9; self.result[index] } // This generates a random automaton. fn random() -> Rule { let mut result = [Px::White; 27]; for i in 0..27 { let r: f32 = random(); result[i] = if r < 0.33 { Px::White } else if r > 0.66 { Px::Black } else { Px::Red } } Rule { result } } } fn main() { // I choose something odd so our initial condition can be all // white cells with a single black cell in the middle to make // something interesting happen let w = 99; let mut img: Image = Image::new(w, w); img[(w/2, 0)] = Px::Black; // choose a random rule and find out what it is let rule = Rule::random(); eprintln!("Using {}", rule.descr()); // for each generation (except the last) for y in 0..(w-1) { // this is the logic on the left-hand side, where we hard-code // that the stuff off the side is the 'empty' value. img[(0, y+1)] = rule.step((Px::empty(), img[(0, y)], img[(1, y)])); // for everything in the middle, we calculate the neighborhood and then step the rule for x in 1..(w-1) { let env = (img.get((x, y)), img.get((x+1, y)), img.get((x+2, y))); img[(x+1, y+1)] = rule.step(env); } // ditto for the right-hand side img[(w-1, y+1)] = rule.step((img[(w-2, y)], img[(w-1, y)], Px::empty())); } // print this out { std::fs::write( &format!("output/{}.ppm", rule.descr()), img.show().as_bytes(), ).unwrap(); } }