I have a 2D cellular automaton open source project here, where the automaton is basically this:
export interface CA { i: Uint8ClampedArray; load: Uint8ClampedArray; max: number; move: Array<RuleType> | Array<(n: Uint8ClampedArray) => number>; n: Uint8ClampedArray; save: Uint8ClampedArray; size: number;}export type RuleType = { make: number; test: Array<(r: number) => boolean>;};type CA2DPropsType = { h: number; max: number; move: Array<RuleType> | Array<(n: Uint8ClampedArray) => number>; w: number;};export class CA2D implements CA { load: Uint8ClampedArray; save: Uint8ClampedArray; i: Uint8ClampedArray; n: Uint8ClampedArray; w: number; h: number; size: number; // 0 means it doesn't care move: Array<RuleType> | Array<(n: Uint8ClampedArray) => number>; max: number; constructor({ w, h, move, max }: CA2DPropsType) { this.w = w; this.h = h; this.max = max; this.size = w * h; this.i = new Uint8ClampedArray(9); this.n = new Uint8ClampedArray(9); this.load = new Uint8ClampedArray(this.size); this.save = new Uint8ClampedArray(this.size); this.move = move; } update() { update2d(this); } seed() { seed(this); }}function loadNeighborhood2d(ca: CA2D, x: number, y: number) { let x1; let x2; let x3; if (x === 0) { x1 = ca.w - 1; x2 = x; x3 = x + 1; } else if (x === ca.w - 1) { x1 = x - 1; x2 = x; x3 = 0; } else { x1 = x - 1; x2 = x; x3 = x + 1; } let y1; let y2; let y3; if (y === 0) { y1 = ca.h - 1; y2 = y; y3 = y + 1; } else if (y === ca.h - 1) { y1 = y - 1; y2 = y; y3 = 0; } else { y1 = y - 1; y2 = y; y3 = y + 1; } let i00 = y1 * ca.h + x1; let i01 = y1 * ca.h + x2; let i02 = y1 * ca.h + x3; let i10 = y2 * ca.h + x1; let i11 = y2 * ca.h + x2; let i12 = y2 * ca.h + x3; let i20 = y3 * ca.h + x1; let i21 = y3 * ca.h + x2; let i22 = y3 * ca.h + x3; // indexes ca.i[0] = i00; // upper left ca.i[1] = i01; ca.i[2] = i02; ca.i[3] = i10; ca.i[4] = i11; // middle ca.i[5] = i12; ca.i[6] = i20; ca.i[7] = i21; ca.i[8] = i22; // lower right // neighborhoods ca.n[0] = ca.save[i00]; // upper left ca.n[1] = ca.save[i01]; ca.n[2] = ca.save[i02]; ca.n[3] = ca.save[i10]; ca.n[4] = ca.save[i11]; // middle ca.n[5] = ca.save[i12]; ca.n[6] = ca.save[i20]; ca.n[7] = ca.save[i21]; ca.n[8] = ca.save[i22]; // lower right}export function update2d(ca: CA2D) { let y = 0; while (y < ca.h) { let x = 0; while (x < ca.w) { loadNeighborhood2d(ca, x, y); loadUpdate(y * ca.h + x, ca); x++; } y++; } saveUpdates(ca);}export function loadUpdate(p: number, ca: CA): void { let k = 0; ruleLoop: while (k < ca.move.length) { let rule = ca.move[k++]; if (typeof rule === "function") { const make = rule(ca.n); if (make >= 0) { ca.load[p] = make; break ruleLoop; } } else { let i = 0; const { test, make } = rule; while (i < ca.n.length) { const v = ca.n[i]; const t = test[i++]; if (!t(v)) { continue ruleLoop; } } ca.load[p] = make; break ruleLoop; } }}export function randomIntBetween(min: number, max: number) { // min and max included return Math.floor(Math.random() * (max - min + 1) + min);}export function saveUpdates(ca: CA) { let i = 0; while (i < ca.load.length) { ca.save[i] = ca.load[i]; i++; }}export function seed(ca: CA) { let i = 0; while (i < ca.save.length) { const rand = randomIntBetween(0, ca.max); ca.save[i++] = rand; }}
I am generating a Gif from it in the terminal like this:
import { any, CA2D, CA2DRenderer, eq, wait } from './src/index.js'import CanvasGifEncoder from '@pencil.js/canvas-gif-encoder'import GIFEncoder from 'gif-encoder-2'import fs from 'fs'import { createCanvas } from 'canvas'import { writeFile } from 'fs'import _ from 'lodash'const COLOR = { black: 'rgba(40, 40, 40, 0)', blue: 'rgba(56, 201, 247)', green: 'hsl(165, 92%, 44%)', greenLight: 'hsl(165, 92%, 79%)', purple: 'rgba(121, 85, 243, 0.8)', purpleLight: 'hsl(254, 87%, 70%)', red: 'rgba(238, 56, 96)', white: 'rgba(255, 255, 255)', white2: 'rgba(244, 244, 244)', white3: 'rgba(222, 222, 222)', yellow: 'rgba(246, 223, 104)',}export const caProps = { h: 60, max: 1, move: [ (n: Uint8ClampedArray) => { const sum = _.sum(n) const isLive = n[4] === 1 if (isLive && sum >= 3 && sum <= 4) { return 1 } else { return -1 } }, (n: Uint8ClampedArray) => { const sum = _.sum(n) const isDead = n[4] === 0 if (isDead && sum === 3) { return 0 } else { return -1 } }, (n: Uint8ClampedArray) => { const isLive = n[4] === 1 return isLive ? 0 : -1 }, ], w: 60,}const ca = new CA2D(caProps)ca.seed()// const saveByteArray = (function () {// var a = document.createElement('a')// document.body.appendChild(a)// a.style.display = 'none'// return function (data: Array<BlobPart>, name: string) {// var blob = new Blob(data, { type: 'octet/stream' }),// url = window.URL.createObjectURL(blob)// a.href = url// a.download = name// a.click()// window.URL.revokeObjectURL(url)// }// })()start()async function start() { const canvas = createCanvas(500, 500) const renderer = new CA2DRenderer(canvas) renderer.setDimensions({ gap: 1, width: 500, rowSize: ca.h, columnSize: ca.w, }) const encoder = new GIFEncoder(500, 500) encoder.setDelay(100) encoder.start() // document.body.appendChild(renderer.canvas) const colors = [COLOR.white2, COLOR.green, COLOR.purple, COLOR.red] renderer.draw(ca, colors) if (renderer.context) encoder.addFrame(renderer.context) let i = 0 while (i < 5) { console.log(i) await wait(20) ca.update() renderer.draw(ca, colors) if (renderer.context) encoder.addFrame(renderer.context) i++ } encoder.finish() const buffer = encoder.out.getData() fs.writeFileSync('example.gif', buffer) // saveByteArray([buffer], 'example.gif')}
You can run the project like this:
git clone git@github.com:lancejpollard/ca.js.gitnpm installnpm run testnode dist.test/test
Notice the rules in the second code snippet:
[ (n: Uint8ClampedArray) => { const sum = _.sum(n) const isLive = n[4] === 1 if (isLive && sum >= 3 && sum <= 4) { return 1 } else { return -1 } }, (n: Uint8ClampedArray) => { const sum = _.sum(n) const isDead = n[4] === 0 if (isDead && sum === 3) { return 0 } else { return -1 } }, (n: Uint8ClampedArray) => { const isLive = n[4] === 1 return isLive ? 0 : -1 },]
It gets a neighborhood array (including self), and then implements the 3 rules on the Wiki page:
- Any live cell with two or three live neighbours survives.
- Any dead cell with three live neighbours becomes a live cell.
- All other live cells die in the next generation. Similarly, all other dead cells stay dead.
I am getting it stabilizing after 5 generations though, what did I do wrong?
What do I need to change in the rules or the CA2D implementation to get it working? I don't think the CA2D implementation needs to change, I think I got that right, but maybe I misinterpreted the rules?