A cellular automaton simulator that I wrote in a hurry (so it's pretty messy).
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

414 lines
14 KiB

mod grid;
use crate::grid::{Grid, State, TILESIZE, TILEMASKI, TILEBITS};
use std::thread::sleep;
use std::time::Duration;
use std::num::NonZeroUsize;
use rand::Rng;
struct Disp<'a>(&'a Grid, (isize, isize), (isize, isize));
impl<'a> std::fmt::Display for Disp<'a>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
for y in ((self.2).0 << TILEBITS .. (self.2).1 << TILEBITS).rev()
{
for x in (self.1).0 << TILEBITS .. (self.1).1 << TILEBITS
{
match self.0.get(x, y)
{
State::Dead if x & TILEMASKI == 0 || y & TILEMASKI == 0 => { write!(f, "▒")?; },
State::Dead if self.0.backed_at(x, y) => { write!(f, "░")?; },
State::Dead => { write!(f, " ")?; },
State::Alive => { write!(f, "▓")?; },
}
}
writeln!(f, "")?;
}
writeln!(f, "")
}
}
#[derive(Clone, Debug, PartialEq)]
enum Op {
FromFile {
filename: String,
init_grid: Option<Grid>,
wrapx: usize,
wrapy: usize,
maxsteps: usize,
rules: ([bool; 9], [bool; 9]),
delay_ms: u64,
},
Random {
prob: f64,
wrapx: usize,
wrapy: usize,
maxsteps: usize,
rules: ([bool; 9], [bool; 9]),
delay_ms: u64,
},
RandomHist {
samples: usize,
threads: usize,
wrapx: usize,
wrapy: usize,
maxsteps: usize,
rules: ([bool; 9], [bool; 9]),
}
}
const USAGE_STRING: &str =
"
This tool has three subcommands, which can be passed in as arguments.
The first is the `file` subcommand, which reads a pattern from a file and steps the automaton for a number of steps:
file <filename> <x size> <y size> <max steps> <ruleset> <frame delay>
`filename` indicates the file to load the initial pattern from. The file is parsed as if it were ASCII art.
The second is the `random` subcommand, which creates a random initial configuration and steps the automaton for a number of steps:
random <probability> <x size> <y size> <max steps> <ruleset> <frame delay>
The `probability` argument specifies the probability of a cell being alive in the initial state.
The third command in the `randhist` subcommand, which runs many simulations starting in a random state and outputs statistics about them:
randhist <number of samples> <number of threads> <x size> <y size> <max steps> <ruleset>
Some subcommands have have the following arguments:
`x size` and `y size` specify the size of the world, after which it wraps around. (BUG: this cannot be 1.) If it is zero, then the world will be unbounded, although this is disallowed in random trials.
`max steps` is the largest number of steps allowed for cycle detection or simulation.
`ruleset` describes the rules of the ceullular automaton, given in conditions required for birth or survival. For example, Conway's gave of life is specified as B3/S23 -- a cell becomes alive with 3 living neighbors, and stays alive with 2 or 3 living neighbors.
`frame delay` is the number of milliseconds between the printing of frames. If this is zero, then no display will happen at all.
";
fn main()
{
use std::sync::{mpsc, atomic::AtomicUsize, Arc};
let mut args = std::env::args().skip(1);
let mut ops = vec![];
loop {
match args.next().as_deref() {
Some("file") => {
ops.push(Op::FromFile {
filename: args.next().expect("incorrect usage"),
init_grid: None,
wrapx: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
wrapy: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
maxsteps: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
rules: parse_rules(&args.next().expect("incorrect usage")).expect("could not parse rules"),
delay_ms: args.next().expect("incorrect usage").parse::<u64>().expect("not an integer"),
});
}
Some("random") => {
ops.push(Op::Random {
prob: args.next().expect("incorrect usage").parse::<f64>().expect("not a probability"),
wrapx: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
wrapy: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
maxsteps: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
rules: parse_rules(&args.next().expect("incorrect usage")).expect("could not parse rules"),
delay_ms: args.next().expect("incorrect usage").parse::<u64>().expect("not an integer"),
});
}
Some("randhist") => {
ops.push(Op::RandomHist {
samples: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
threads: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
wrapx: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
wrapy: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
maxsteps: args.next().expect("incorrect usage").parse::<usize>().expect("not an integer"),
rules: parse_rules(&args.next().expect("incorrect usage")).expect("could not parse rules"),
});
}
Some("--help") | Some("-h") | Some("-help") | Some("/?") => {
println!("{USAGE_STRING}");
return
}
Some(_) => {
eprintln!("{USAGE_STRING}");
return
}
None => break
}
}
for op in ops.iter_mut() {
match op {
Op::FromFile { filename, init_grid, wrapx, wrapy, .. } => {
let fstr = format!("could not open file {}", &filename);
let data = std::fs::read_to_string(filename).expect(fstr.as_str());
let mut grid = Grid::new_blank_wrapped(*wrapx, *wrapy);
for (y, line) in data.lines().enumerate() {
for (x, c) in line.chars().enumerate() {
if !c.is_whitespace() {
grid.set(x as isize, -(y as isize), State::Alive);
}
}
}
*init_grid = Some(grid);
}
Op::Random { prob, wrapx, wrapy, .. } => {
if prob.is_nan() || *prob > 1.0 || *prob < 0.0 {
eprintln!("probability must be between 0 and 1");
return;
}
if *wrapx == 0 || *wrapy == 0 {
eprintln!("random configurations must have a nonzero wrap size");
return;
}
}
Op::RandomHist { wrapx, wrapy, .. } => {
if *wrapx == 0 || *wrapy == 0 {
eprintln!("random configurations must have a nonzero wrap size");
return;
}
}
}
}
for op in ops {
match op {
Op::FromFile {
filename: _,
init_grid: Some(mut init_grid),
wrapx: _,
wrapy: _,
maxsteps,
rules: (on, off),
delay_ms,
} => {
let mut grid = init_grid.clone();
let fo = find_first_oscillation(&mut init_grid, &on, &off, maxsteps);
let num_steps = if let Some((stability_time, period)) = fo {
let mut count_off = 0;
let mut count_on = 0;
init_grid.per_tile(|_, t|
{
for x in 0 .. TILESIZE
{
for y in 0 .. TILESIZE
{
match t.get_at(x, y)
{
State::Dead => { count_off += 1; },
State::Alive => { count_on += 1; },
}
}
}
});
println!("took {stability_time} steps to converge to a period of {period}");
println!("cells off : {count_off}");
println!("cells on : {count_on}");
if count_off + count_on != 0 {
println!("ratio on : {}", (count_on as f64) / (count_off + count_on) as f64);
}
stability_time + 3 * period
} else {
println!("could not find oscillation within maximum steps");
maxsteps
};
if delay_ms > 0 {
let mut disp = format!("{}", Disp(&grid, (0, 1), (0, 1)));
for i in 0..num_steps {
println!("step {i}");
print!("{disp}");
let extents = grid.step(&on, &off, i % TILESIZE == 0);
disp = format!("{}", Disp(&grid, extents.0, extents.1));
sleep(Duration::from_millis(delay_ms));
}
}
}
Op::FromFile { init_grid: None, .. } => unreachable!(),
Op::Random {
prob,
wrapx,
wrapy,
maxsteps,
rules: (on, off),
delay_ms,
} => {
let mut init_grid = Grid::new_random(NonZeroUsize::new(wrapx).unwrap(), NonZeroUsize::new(wrapy).unwrap(), prob);
let mut grid = init_grid.clone();
let fo = find_first_oscillation(&mut init_grid, &on, &off, maxsteps);
let num_steps = if let Some((stability_time, period)) = fo {
let mut count_off = 0;
let mut count_on = 0;
init_grid.per_tile(|_, t|
{
for x in 0 .. TILESIZE
{
for y in 0 .. TILESIZE
{
match t.get_at(x, y)
{
State::Dead => { count_off += 1; },
State::Alive => { count_on += 1; },
}
}
}
});
println!("took {stability_time} steps to converge to a period of {period}");
println!("cells off : {count_off}");
println!("cells on : {count_on}");
if count_off + count_on != 0 {
println!("ratio on : {}", (count_on as f64) / (count_off + count_on) as f64);
}
stability_time + 3 * period
} else {
println!("could not find oscillation within maximum steps");
maxsteps
};
if delay_ms > 0 {
let mut disp = format!("{}", Disp(&grid, (0, 1), (0, 1)));
for i in 0..num_steps {
println!("step {i}");
print!("{disp}");
let extents = grid.step(&on, &off, i % TILESIZE == 0);
disp = format!("{}", Disp(&grid, extents.0, extents.1));
sleep(Duration::from_millis(delay_ms));
}
}
},
Op::RandomHist {
samples,
threads,
wrapx,
wrapy,
maxsteps,
rules: (on, off),
} => {
let wrapx = NonZeroUsize::new(wrapx).unwrap();
let wrapy = NonZeroUsize::new(wrapy).unwrap();
let (tx, rx) = mpsc::channel();
let i = Arc::new(AtomicUsize::new(0));
for _ in 0..threads {
let tx = tx.clone();
let i = i.clone();
let f = move || {
let mut rng = rand::thread_rng();
while i.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < samples {
let prob = rng.gen::<f64>();
let mut init_grid = Grid::new_random(wrapx, wrapy, prob);
let fo = find_first_oscillation(&mut init_grid, &on, &off, maxsteps);
if let Some((stability_time, period)) = fo {
let mut count_off = 0;
let mut count_on = 0;
init_grid.per_tile(|_, t|
{
for x in 0 .. TILESIZE
{
for y in 0 .. TILESIZE
{
match t.get_at(x, y)
{
State::Dead => { count_off += 1; },
State::Alive => { count_on += 1; },
}
}
}
});
let ratio = if count_off + count_on != 0 {
(count_on as f64) / (count_off + count_on) as f64
} else {
0.0
};
tx.send((prob, Some((stability_time, period, count_off, count_on, ratio)))).expect("main thread ended before child");
} else {
tx.send((prob, None)).expect("main thread ended before child");
};
}
};
std::thread::spawn(f);
}
std::mem::drop(tx);
while let Ok((prob, stats)) = rx.recv() {
if let Some((stability_time, period, count_off, count_on, ratio)) = stats {
println!("{wrapx} {wrapy} {prob} {stability_time} {period} {count_off} {count_on} {ratio}");
} else {
println!("{wrapx} {wrapy} {prob} Inf NaN NaN NaN NaN");
}
}
}
}
}
}
fn parse_rules(s: &String) -> Option<([bool; 9], [bool; 9])> {
let mut on = [false; 9];
let mut off = [true; 9];
let mut state = None;
for c in s.chars() {
match (state, c) {
(_, '/') => (),
(Some(true), '0'..='9') => {
off[c.to_digit(10).unwrap() as usize] = false;
}
(Some(false), '0'..='9') => {
on[c.to_digit(10).unwrap() as usize] = true;
}
(_, 'B') | (_, 'b') => {
state = Some(false);
}
(_, 'S') | (_, 's') => {
state = Some(true);
}
_ => return None
}
}
Some((on, off))
}
fn find_first_oscillation(g: &mut Grid, on: &[bool; 9], off: &[bool; 9], max_steps: usize) -> Option<(usize, usize)> {
let mut tortoise = g.clone();
let mut hare = g.clone();
let mut i = 0;
tortoise.step(on, off, i % TILESIZE == 0);
hare.step(on, off, false);
hare.step(on, off, i % TILESIZE == 0);
while hare != tortoise {
if i > max_steps {
return None;
}
tortoise.step(on, off, i % TILESIZE == 0);
hare.step(on, off, false);
hare.step(on, off, i % TILESIZE == 0);
i += 1;
}
tortoise.step(on, off, true);
let mut next_occurrence = tortoise.clone();
let mut period = 1;
next_occurrence.step(on, off, true);
while tortoise != next_occurrence {
next_occurrence.step(on, off, true);
period += 1;
}
let mut leading = g.clone();
for _ in 0..period {
leading.step(on, off, true);
}
let mut first_index = 0;
while *g != leading {
g.step(on, off, true);
leading.step(on, off, true);
first_index += 1;
}
Some((first_index, period))
}