Elementary Cellular Automata
Elementary cellular automata (ECA), extensively studied by Stephen Wolfram in the 1980s, are the simplest class of cellular automata—discrete computational systems where cells update their state based on local neighborhood rules. Unlike two-dimensional automata such as the Game of Life, ECA operate on a single row of binary cells (on or off), with each cell's next state determined by its current state and that of its two immediate neighbors. This yields exactly 256 possible rules, numbered 0–255 using Wolfram's naming convention.
In this visualization, time flows downward: each horizontal row represents the automaton's state at one moment, with subsequent rows showing its evolution. Despite their simplicity, ECA exhibit a surprising range of behaviors. Rule 0 produces uniform death; Rule 184 (the "traffic rule") conserves the number of active cells while propagating waves through the lattice. Most remarkably, Rules 30, 90, and 110 generate complex, apparently chaotic patterns from minimal initial conditions—Rule 110 has even been proven Turing complete, capable in principle of universal computation.
import { Environment, Terrain, CanvasRenderer, utils } from "flocc"; import { Button, Input, Panel, Radio } from "flocc-ui"; /* ----- PARAMETERS ----- */ const RULE = 30; const PROBABILITY_OF_MUTATION = 0.0; const RANDOMIZE = false; const NEIGHBORHOOD = 1; const CLASSES = 2; /* ---------------------- */ const [width, height] = [500, 500]; /* ----- SETUP ----- */ const environment = new Environment({ width, height }); const renderer = new CanvasRenderer(environment, { width, height }); renderer.mount("#container"); const terrain = new Terrain(width, height, { grayscale: true }); environment.use(terrain); environment.set("rule", RULE); function p(n) { return (255 * (1 - n)) / (CLASSES - 1); } function ip(n) { return Math.round(1 - (n * (CLASSES - 1)) / 255); } function tick(x, y) { if (y !== environment.time + 1) return; const power = CLASSES ** (2 * NEIGHBORHOOD + 1); if (power > 100000) throw new Error("Can't handle a power that big!"); const ruleString = environment.memo(() => { return utils .zfill(environment.get("rule").toString(CLASSES), power) .split("") .reverse() .join(""); }); const neighbors = []; for (let dx = -NEIGHBORHOOD; dx <= NEIGHBORHOOD; dx++) { neighbors.push(ip(terrain.sample(x + dx, y - 1))); } let state = neighbors.join(""); state = parseInt(state, CLASSES); const value = utils.uniform() > PROBABILITY_OF_MUTATION ? +ruleString.charAt(state) : utils.random(0, CLASSES - 1); return p(value); } function setup() { terrain.init((x, y) => { if (y > 0) return 255; const value = RANDOMIZE ? utils.random(0, CLASSES - 1) : x === Math.round(width / 2) ? CLASSES - 1 : 0; return p(value); }); terrain.addRule(tick); } function UI() { new Panel(environment, [ new Radio({ choices: [30, 90, 110, 184], name: "rule", value: RULE }), new Input({ name: "rule", value: 30 }), new Button({ label: "Reset", onClick() { cancelAnimationFrame(animationFrame); environment.time = 0; setup(); run(); } }) ]); } let animationFrame = 0; function run() { environment.tick(); if (environment.time < height) animationFrame = requestAnimationFrame(run); } setup(); UI(); run();