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.git
npm install
npm run test
node 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?