// Conway's Game of Life — B3/S23 rule
//
// Two Uint8Array buffers double-buffer the grid. Each generation:
// - A live cell with 2 or 3 live neighbors survives.
// - A dead cell with exactly 3 live neighbors becomes alive.
// - All other cells die or remain dead.
// Cells that just died are rendered in the "dying" color for one frame.
export const WARMUP = { framesBeforeReady: 60 };
export const PARAMS = {
cellSize: { value: 4, options: ["1", "2", "4", "8"], label: "Cell Size (px)", folder: "Structure", rebuildOnChange: true },
preset: { value: "Random", options: ["Random", "Glider Gun", "R-Pentomino", "Acorn", "Pulsar"], label: "Initial Pattern", folder: "Structure", rebuildOnChange: true },
density: { value: 0.35, min: 0.05, max: 0.6, step: 0.01, label: "Initial Density", folder: "Structure", rebuildOnChange: true },
speed: { value: 1, options: ["1", "2", "4", "8"], label: "Steps/Frame", folder: "Behavior" },
living: { value: "#e8d8a8", type: "color", label: "Living", folder: "Appearance" },
dying: { value: "#5a3a2a", type: "color", label: "Dying", folder: "Appearance" },
bg: { value: "#0a0a0a", type: "color", label: "Background", folder: "Appearance" },
};
let state;
// Named Conway pattern coordinate tables (relative [col, row] offsets)
// Placed near grid center in sketchSetup.
const PRESET_PATTERNS = {
"Glider Gun": [
// Bill Gosper's Glider Gun (36×9 bounding box)
[0,4],[0,5],[1,4],[1,5],[10,4],[10,5],[10,6],[11,3],[11,7],
[12,2],[12,8],[13,2],[13,8],[14,5],[15,3],[15,7],[16,4],[16,5],[16,6],[17,5],
[20,2],[20,3],[20,4],[21,2],[21,3],[21,4],[22,1],[22,5],
[24,0],[24,1],[24,5],[24,6],[34,2],[34,3],[35,2],[35,3],
],
"R-Pentomino": [
[1,0],[2,0],[0,1],[1,1],[1,2],
],
"Acorn": [
[1,0],[3,1],[0,2],[1,2],[4,2],[5,2],[6,2],
],
"Pulsar": [
// Pulsar (period-3 oscillator) — 13×13 footprint
[2,0],[3,0],[4,0],[8,0],[9,0],[10,0],
[0,2],[5,2],[7,2],[12,2],[0,3],[5,3],[7,3],[12,3],[0,4],[5,4],[7,4],[12,4],
[2,5],[3,5],[4,5],[8,5],[9,5],[10,5],
[2,7],[3,7],[4,7],[8,7],[9,7],[10,7],
[0,8],[5,8],[7,8],[12,8],[0,9],[5,9],[7,9],[12,9],[0,10],[5,10],[7,10],[12,10],
[2,12],[3,12],[4,12],[8,12],[9,12],[10,12],
],
};
function hexToRgb(hex) {
const v = parseInt(hex.replace('#', ''), 16);
return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
}
export function sketchSetup(p, w, h, params) {
const cs = parseInt(params.cellSize);
const cols = Math.floor(w / cs);
const rows = Math.floor(h / cs);
const n = cols * rows;
const current = new Uint8Array(n);
const next = new Uint8Array(n);
const dying = new Uint8Array(n); // 1 = just died this step
const preset = params.preset;
if (preset === "Random") {
const density = params.density;
for (let i = 0; i < n; i++) {
current[i] = p.random() < density ? 1 : 0;
}
} else {
const pattern = PRESET_PATTERNS[preset];
if (pattern) {
// Find bounding box of pattern
const maxCol = Math.max(...pattern.map(([c]) => c));
const maxRow = Math.max(...pattern.map(([, r]) => r));
// Place near center
const startCol = Math.floor((cols - maxCol) / 2);
const startRow = Math.floor((rows - maxRow) / 2);
for (const [dc, dr] of pattern) {
const c = startCol + dc;
const r = startRow + dr;
if (c >= 0 && c < cols && r >= 0 && r < rows) {
current[r * cols + c] = 1;
}
}
}
}
p.pixelDensity(1);
p.background(10);
state = { cs, cols, rows, current, next, dying };
return state;
}
export function sketchDraw(p, w, h, params) {
const { cs, cols, rows, current, next, dying } = state;
const speed = parseInt(params.speed) || 1;
for (let s = 0; s < speed; s++) {
// Clear dying buffer
dying.fill(0);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const idx = row * cols + col;
let neighbors = 0;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const r = (row + dr + rows) % rows;
const c = (col + dc + cols) % cols;
neighbors += current[r * cols + c];
}
}
const alive = current[idx];
if (alive) {
next[idx] = (neighbors === 2 || neighbors === 3) ? 1 : 0;
if (next[idx] === 0) dying[idx] = 1;
} else {
next[idx] = neighbors === 3 ? 1 : 0;
}
}
}
// Swap buffers
current.set(next);
}
// Render using loadPixels for performance
const [lr, lg, lb] = hexToRgb(params.living);
const [dr, dg, db] = hexToRgb(params.dying);
const [br, bg2, bb] = hexToRgb(params.bg);
p.loadPixels();
const pd = p.pixels;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const idx = row * cols + col;
let r, g, b;
if (current[idx]) {
r = lr; g = lg; b = lb;
} else if (dying[idx]) {
r = dr; g = dg; b = db;
} else {
r = br; g = bg2; b = bb;
}
// Fill the cs×cs block of pixels
for (let py = 0; py < cs; py++) {
for (let px = 0; px < cs; px++) {
const pi = ((row * cs + py) * w + (col * cs + px)) * 4;
pd[pi] = r;
pd[pi + 1] = g;
pd[pi + 2] = b;
pd[pi + 3] = 255;
}
}
}
}
p.updatePixels();
}