Back to Conway's Game of Life
SOURCE / PINNED RELEASE

Made of little things.

Conway's Game of Life

Release
230955df1900…
Author-recorded commit
c08196c2b050…
License
LICENSE
Author’s source reference
nostr://npub16nzc4ql748r2kv9202rgzrt0fmtk4n3tzsjl86yf6ty9agqe9u0qjcc7hc/wss%3A%2F%2Fgit.napplet.soy%2F/n-644c45e22c7

Archive hash verified: 2627ce678935a213…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/engine.test.mjs
// Phase 1 engine pins (build plan §Phase 1): still life, oscillator period 2,
// glider translation (2,2)/4 steps proving torus wrap, dead-cell birth,
// stable() on empty and block, aliveCount bookkeeping. Constants asserted
// against the exported symbols (BIRTH/SURVIVE_MIN/SURVIVE_MAX), not literals.
import assert from 'node:assert/strict';
import test from 'node:test';
import {
  BIRTH,
  GameOfLife,
  MOORE_NEIGHBORS,
  SURVIVE_MAX,
  SURVIVE_MIN,
  decodeRle,
  encodeRle,
  registerPattern,
} from '../src/engine.ts';
import {
  GOSPER_GUN,
  GOSPER_GUN_RLE,
  GUN_SEED_X,
  GUN_SEED_Y,
  PATTERNS,
} from '../src/patterns.ts';

// Glider phase moving down-right; translates (+1,+1) every 4 generations
// (c/4 diagonal). Canonical phase, verified in-engine 2026-10-30: evolving
// this RLE 4 steps yields gen0 shifted by exactly (1,1).
const GLIDER_RLE = 'bo$2bo$3o';

// --- Phase 7: save-wire RLE encoder (spec §7) -------------------------------

test('encodeRle round-trips every cell through decodeRle (partial board)', () => {
  const g = new GameOfLife(7, 5);
  setBlock(g, 0, 0);
  g.set(6, 4, 1); // far corner + an isolated edge cell stress runs
  g.set(3, 2, 1);
  const d = decodeRle(encodeRle(g.grid, 7, 5));
  assert.equal(d.w, 7);
  assert.equal(d.h, 5);
  assert.deepEqual([...d.cells], [...g.grid], 'cell-for-cell round-trip');
});

test('encodeRle on the empty default board decodes back to 120x90 zeros', () => {
  const g = new GameOfLife(); // 120x90 per D2
  const d = decodeRle(encodeRle(g.grid, g.w, g.h));
  assert.equal(d.w, 120);
  assert.equal(d.h, 90, 'trailing blank rows survive the round-trip');
  assert.equal(d.cells.reduce((s, v) => s + v, 0), 0);
});

test('gun board survives the save round-trip with pop 36 in place', () => {
  const g = new GameOfLife();
  g.stampRle(GOSPER_GUN_RLE, GUN_SEED_X, GUN_SEED_Y);
  const d = decodeRle(encodeRle(g.grid, g.w, g.h));
  assert.deepEqual([...d.cells], [...g.grid]);
  assert.equal(d.cells.reduce((s, v) => s + v, 0), 36, 'gun pop pin mirrors save path');
});

test('encodeRle refuses a grid whose length does not match the board', () => {
  assert.throws(() => encodeRle(new Uint8Array(6), 4, 4), RangeError);
});

function setBlock(g, x, y) {
  for (let py = 0; py < 2; py++) for (let px = 0; px < 2; px++) g.set(x + px, y + py, 1);
}

function aliveCells(g) {
  const out = [];
  for (let y = 0; y < g.h; y++)
    for (let x = 0; x < g.w; x++) if (g.get(x, y) === 1) out.push(`${x},${y}`);
  return out.sort();
}

test('rule constants match Conway B3/S23', () => {
  assert.equal(BIRTH, 3);
  assert.equal(SURVIVE_MIN, 2);
  assert.equal(SURVIVE_MAX, 3);
  assert.equal(MOORE_NEIGHBORS, 8);
});

test('constructor guards against degenerate grids', () => {
  assert.throws(() => new GameOfLife(0, 5), RangeError);
  assert.throws(() => new GameOfLife(5, -1), RangeError);
  assert.throws(() => new GameOfLife(2.5, 5), RangeError);
  const g = new GameOfLife();
  assert.equal(g.w, 120);
  assert.equal(g.h, 90);
  assert.equal(g.aliveCount(), 0);
  assert.equal(g.generation, 0);
});

test('block is a still life and reads stable', () => {
  const g = new GameOfLife(10, 10);
  setBlock(g, 2, 2);
  assert.equal(g.aliveCount(), 4);
  assert.ok(g.stable(), 'block must report stable before any step');
  g.step();
  assert.deepEqual(aliveCells(g), ['2,2', '2,3', '3,2', '3,3']);
  assert.equal(g.generation, 1);
  assert.ok(g.stable());
});

test('empty board is stable', () => {
  assert.ok(new GameOfLife(8, 8).stable());
});

test('blinker is a period-2 oscillator and not stable', () => {
  const g = new GameOfLife(10, 10);
  for (let x = 3; x <= 5; x++) g.set(x, 4, 1);
  assert.ok(!g.stable());
  g.step();
  assert.deepEqual(aliveCells(g), ['4,3', '4,4', '4,5']);
  g.step();
  assert.deepEqual(aliveCells(g), ['3,4', '4,4', '5,4']);
  assert.equal(g.generation, 2);
  assert.equal(g.aliveCount(), 3);
});

test('dead cell with exactly BIRTH live neighbors is born', () => {
  const g = new GameOfLife(10, 10);
  // Three orthogonal neighbors of (5,5): above, left, right. (5,4) is
  // diagonal-adjacent to both side cells, so it has 2 neighbors and survives
  // one generation while the side cells die of underpopulation.
  g.set(5, 4, 1);
  g.set(4, 5, 1);
  g.set(6, 5, 1);
  assert.equal(g.countNeighbors(5, 5), BIRTH);
  g.step();
  assert.equal(g.get(5, 5), 1, 'birth at exactly 3 neighbors');
  assert.deepEqual(aliveCells(g), ['5,4', '5,5']);
  assert.equal(g.aliveCount(), 2);
  g.step();
  assert.equal(g.aliveCount(), 0, 'lonely birth dies of underpopulation');
});

test('survival band asserts against exported symbols', () => {
  const g = new GameOfLife(6, 6);
  // Live cell with SURVIVE_MIN neighbors survives.
  g.set(2, 2, 1);
  g.set(1, 2, 1);
  g.set(3, 2, 1); // (2,2) has exactly SURVIVE_MIN
  assert.equal(g.countNeighbors(2, 2), SURVIVE_MIN);
  g.step();
  assert.equal(g.get(2, 2), 1, `survives at ${SURVIVE_MIN}`);
});

test('live cell with more than SURVIVE_MAX neighbors dies', () => {
  const g = new GameOfLife(6, 6);
  setBlock(g, 1, 1);
  setBlock(g, 2, 2); // (2,2) ends up with 5 neighbors
  assert.ok(g.countNeighbors(2, 2) > SURVIVE_MAX);
  g.step();
  assert.equal(g.get(2, 2), 0, 'overpopulation death');
});

test('glider translates (1,1) per 4 steps (c/4 diagonal) across the torus edge', () => {
  // Build-plan wording said "(2,2) after 4 steps"; the true c/4 diagonal
  // kinematics (probed 2026-10-30, engine probe on an open 14x14 grid) are
  // (1,1) every 4 generations and (2,2) every 8. Pinned both ways.
  const g = new GameOfLife(20, 20);
  // Glider top-left near the bottom-right corner so its path wraps edges.
  const ox = g.w - 3;
  const oy = g.h - 3;
  const written = g.stampRle(GLIDER_RLE, ox, oy);
  assert.equal(written, 5);
  assert.equal(g.aliveCount(), 5);
  const cells0 = aliveCells(g).map((s) => s.split(',').map(Number));
  for (let i = 0; i < 4; i++) g.step();
  // Same phase, every cell translated by exactly (1,1) modulo the grid —
  // expected positions derived from the stamped cells, not hand-written.
  const expectShift = (dx, dy) =>
    cells0
      .map(([x, y]) => `${(x + dx) % g.w},${(y + dy) % g.h}`)
      .sort();
  assert.deepEqual(aliveCells(g), expectShift(1, 1));
  for (let i = 0; i < 4; i++) g.step();
  assert.deepEqual(aliveCells(g), expectShift(2, 2));
  assert.equal(g.aliveCount(), 5, 'glider wrapped edges and kept going');
});

test('torus wrap: corner neighbor counts see the far edges', () => {
  const g = new GameOfLife(10, 10);
  g.set(9, 9, 1);
  g.set(0, 9, 1);
  g.set(9, 0, 1);
  assert.equal(g.countNeighbors(0, 0), 3);
  g.step();
  assert.equal(g.get(0, 0), 1, 'birth wraps from the (9,9) corner');
});

test('get/set wrap coordinates (torus get/set)', () => {
  const g = new GameOfLife(10, 10);
  g.set(-1, -1, 1);
  assert.equal(g.get(9, 9), 1);
  assert.equal(g.get(-1, -1), 1);
  assert.equal(g.aliveCount(), 1, 'aliased writes must not double count');
  g.set(-1, -1, 1);
  assert.equal(g.aliveCount(), 1, 'idempotent re-set of same value');
  g.set(9, 9, 0);
  assert.equal(g.aliveCount(), 0);
});

test('aliveCount bookkeeping survives clear and random extremes', () => {
  const g = new GameOfLife(12, 12);
  setBlock(g, 1, 1);
  g.clear();
  assert.equal(g.aliveCount(), 0);
  assert.equal(g.generation, 0, 'clear resets generation counter');
  g.random(0);
  assert.equal(g.aliveCount(), 0);
  g.random(1);
  assert.equal(g.aliveCount(), 12 * 12);
});

test('RLE decoder round-trips and honors the subset grammar', () => {
  const d = decodeRle('bo$2bo$3o!');
  assert.equal(d.w, 3);
  assert.equal(d.h, 3);
  assert.equal(d.cells.reduce((a, b) => a + b, 0), 5);
  // Header form is accepted and ignored.
  const hd = decodeRle('x = 3, y = 3, bo$2bo$3o!');
  assert.deepEqual(Array.from(hd.cells), Array.from(d.cells));
  // Numeric runs expand.
  const runs = decodeRle('3o$3o$3o!');
  assert.equal(runs.cells.reduce((a, b) => a + b, 0), 9);
});

test('Gosper glider gun from the reference seed stamps 36 cells and keeps working', () => {
  // Seed arrays transcribed from upstream GameOfLife/game_of_life.py and
  // cross-verified byte-exact against the vault reference doc (the original
  // crawl had mangled row widths; doc fixed 2026-10-30).
  // Everything derived (population 36, RLE, bounding box) is computed.
  const seed = [
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0],
    [1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
  ];
  for (const row of seed) assert.equal(row.length, 39);
  // Trim dead margin columns to the pattern bounding box.
  const xs = [];
  for (let x = 0; x < 39; x++) if (seed.some((r) => r[x] === 1)) xs.push(x);
  const x0 = xs[0];
  const x1 = xs[xs.length - 1];
  let total = 0;
  let rle = '';
  for (const row of seed) {
    let run = 0;
    let out = '';
    for (let x = x0; x <= x1; x++) {
      if (row[x] === 0) {
        run++;
      } else {
        if (run > 0) out += `${run}b`;
        run = 0;
        out += 'o';
        total++;
      }
    }
    if (run > 0) out += `${run}b`;
    rle += out + '$';
  }
  const decoded = decodeRle(rle);
  assert.equal(total, 36, 'population computed from the seed data (plan pin)');
  assert.equal(decoded.w, x1 - x0 + 1);
  assert.equal(decoded.h, 9);
  const g = new GameOfLife(120, 90);
  // Reference seeds at pos (3,3): row offset 3, column offset 3 — x0 is the
  // left edge of the bounding box (column 0 of the data, the block pair).
  assert.equal(g.stampRle(rle, 3, 3), 36);
  assert.equal(g.aliveCount(), 36);
  // Gun period is 30: block-pair cell at data (row 4, col x0) -> (x=3, y=7)
  // is alive again after exactly one period, and the gun emitted gliders.
  assert.equal(x0, 0, 'bounding box left edge is the block pair column');
  for (let i = 0; i < 30; i++) g.step();
  assert.equal(g.get(3, 7), 1, 'gun structure intact after one period');
  assert.ok(g.aliveCount() > 36, 'gliders emitted: population grew');
});

test('patternAt stamps a registered pattern and wraps at the edge', () => {
  registerPattern('glider', GLIDER_RLE);
  const g = new GameOfLife(10, 10);
  assert.equal(g.patternAt('glider', 8, 8), 5);
  assert.equal(g.get(0, 0), 1, 'glider body wrapped past (9,9)');
  assert.equal(g.aliveCount(), 5);
  assert.throws(() => g.patternAt('nope', 0, 0), RangeError);
});

test('patterns.ts gun export equals the upstream seed and is registered', () => {
  // The Phase 2 seed must be the same gun the seed-array test above derives
  // from upstream: decode the shipped RLE and compare against the seed rows
  // (bounding box crop), and confirm patternAt(GOSPER_GUN, 3, 3) reproduces
  // the reference stamp (alive 36 at (3,3)).
  const seed = [
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0],
    [1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
  ];
  const d = decodeRle(GOSPER_GUN_RLE);
  assert.equal(d.w, 36);
  assert.equal(d.h, 9);
  assert.equal(d.cells.reduce((a, b) => a + b, 0), 36);
  for (let y = 0; y < 9; y++)
    for (let x = 0; x < 36; x++)
      assert.equal(d.cells[y * 36 + x], seed[y][x], `cell ${x},${y} matches upstream`);
  registerPattern(GOSPER_GUN, GOSPER_GUN_RLE);
  const g = new GameOfLife(120, 90);
  assert.equal(g.patternAt(GOSPER_GUN, GUN_SEED_X, GUN_SEED_Y), 36);
  assert.equal(g.aliveCount(), 36);
});

// --- Phase 5: pattern library (build plan §Phase 5 verify gate) -------------

function bboxOf(d) {
  let x0 = Infinity, y0 = Infinity, x1 = -1, y1 = -1;
  for (let y = 0; y < d.h; y++)
    for (let x = 0; x < d.w; x++)
      if (d.cells[y * d.w + x] === 1) {
        if (x < x0) x0 = x; if (x > x1) x1 = x;
        if (y < y0) y0 = y; if (y > y1) y1 = y;
      }
  return { w: x1 - x0 + 1, h: y1 - y0 + 1 };
}

test('library metadata matches the engine measurements', () => {
  const ids = new Set();
  for (const def of PATTERNS) {
    assert.ok(!ids.has(def.id), `duplicate pattern id: ${def.id}`);
    ids.add(def.id);
    const d = decodeRle(def.rle);
    const bb = bboxOf(d);
    assert.equal(bb.w, def.w, `${def.id} width`);
    assert.equal(bb.h, def.h, `${def.id} height`);
    assert.equal(d.cells.reduce((a, b) => a + b, 0), def.pop, `${def.id} population`);
  }
});

test('RLE round-trip: decode -> encode -> decode is lossless', () => {
  for (const def of PATTERNS) {
    const d1 = decodeRle(def.rle);
    const d2 = decodeRle(encodeRle(d1.cells, d1.w, d1.h));
    assert.deepEqual(Array.from(d2.cells), Array.from(d1.cells), `${def.id} round-trip`);
    assert.equal(d2.w, d1.w);
    assert.equal(d2.h, d1.h);
  }
});

test('RLE $N advances N rows (blank rows included)', () => {
  const d = decodeRle('2o$3$2o!'); // row0, advance 3 (rows 1-3 blank), row4
  assert.equal(d.h, 5);
  assert.equal(d.cells.reduce((a, b) => a + b, 0), 4);
  assert.equal(d.cells[0], 1);
  assert.equal(d.cells[4 * d.w], 1);
  assert.equal(d.cells[1 * d.w], 0);
  assert.equal(d.cells[3 * d.w], 0);
  // A digit before $ binds to the row advance, never to blank cells.
  const e = decodeRle('2o2$!');
  assert.equal(e.h, 2); // row + one blank row
  assert.equal(e.cells.reduce((a, b) => a + b, 0), 2);
});

test('gun stamps 36 from the library export (menu path, not just seed path)', () => {
  registerPattern(GOSPER_GUN, GOSPER_GUN_RLE);
  const g = new GameOfLife(120, 90);
  assert.equal(g.patternAt(GOSPER_GUN, 3, 3), 36);
  assert.equal(g.aliveCount(), 36);
});

test('pulsar is period 3 — full CP 48-56-72 phase pin (population alone is not enough: a malformed RLE can still stamp 48 cells)', () => {
  const g = new GameOfLife(40, 40);
  const def = PATTERNS.find((p) => p.id === 'pulsar');
  assert.equal(g.stampRle(def.rle, 13, 13), 48);
  const s0 = g.grid.join('|');
  g.step();
  assert.notEqual(g.grid.join('|'), s0, 'not fixed');
  // Conway's own name for the pulsar is "Cambridge Pulsar CP 48-56-72":
  // the per-phase populations 48 -> 56 -> 72 -> 48 are the fingerprint
  // that no look-alike oscillator with pop 48 can fake.
  assert.equal(g.aliveCount(), 56, 'phase 2 population is 56 (CP 48-56-72)');
  g.step();
  assert.notEqual(g.grid.join('|'), s0, 'not period 1 or 2');
  assert.equal(g.aliveCount(), 72, 'phase 3 population is 72 (CP 48-56-72)');
  g.step();
  assert.equal(g.grid.join('|'), s0, 'returns at exactly 3');
  assert.equal(g.aliveCount(), 48);
});

test('beacon oscillates at period 2 (LifeWiki classification pin)', () => {
  const g = new GameOfLife(20, 20);
  const def = PATTERNS.find((p) => p.id === 'beacon');
  assert.equal(g.stampRle(def.rle, 8, 8), 8);
  const s0 = g.grid.join('|');
  g.step();
  assert.notEqual(g.grid.join('|'), s0);
  g.step();
  assert.equal(g.grid.join('|'), s0, 'beacon returns at 2');
});

test('pentadecathlon returns at 15 and never earlier', () => {
  const g = new GameOfLife(40, 40);
  const def = PATTERNS.find((p) => p.id === 'pentadecathlon');
  assert.equal(g.stampRle(def.rle, 15, 18), 12);
  const s0 = g.grid.join('|');
  let first = null;
  for (let i = 1; i <= 15; i++) {
    g.step();
    if (first === null && g.grid.join('|') === s0) first = i;
  }
  assert.equal(first, def.period, 'first return equals declared period');
});

test('block and beehive are still lifes; r-pentomino churns', () => {
  for (const id of ['block', 'beehive']) {
    const g = new GameOfLife(20, 20);
    const def = PATTERNS.find((p) => p.id === id);
    g.stampRle(def.rle, 8, 8);
    assert.ok(g.stable(), `${id} stable`);
    g.step();
    assert.ok(g.stable(), `${id} stable after step`);
    assert.equal(g.aliveCount(), def.pop);
  }
  const g = new GameOfLife(40, 40);
  const def = PATTERNS.find((p) => p.id === 'r-pentomino');
  g.stampRle(def.rle, 18, 18);
  for (let i = 0; i < 50; i++) {
    assert.ok(!g.stable() || g.aliveCount() !== def.pop, `r-pentomino still active at gen ${i + 1}`);
    g.step();
  }
});

test('library glider and LWSS translate; near-edge stamp survives wrap', () => {
  // Library glider phase (LifeWiki glider.rle) on a small torus: after 8
  // steps it has crossed its own start by (2,2) modulo the grid.
  const g = new GameOfLife(14, 14);
  const glider = PATTERNS.find((p) => p.id === 'glider');
  const cells0 = [];
  assert.equal(g.stampRle(glider.rle, 11, 11), 5); // hugs the bottom-right corner
  for (let y = 0; y < 14; y++) for (let x = 0; x < 14; x++) if (g.get(x, y)) cells0.push([x, y]);
  for (let i = 0; i < 8; i++) g.step();
  const shifted = cells0.map(([x, y]) => `${(x + 2) % 14},${(y + 2) % 14}`).sort();
  const now = [];
  for (let y = 0; y < 14; y++) for (let x = 0; x < 14; x++) if (g.get(x, y)) now.push(`${x},${y}`);
  assert.deepEqual(now.sort(), shifted, 'glider wrapped and kept its shape');
  assert.equal(g.aliveCount(), 5);

  // LWSS: c/2 orthogonal (2 cells right per 4 gens), pinned via bbox delta.
  const h = new GameOfLife(40, 30);
  const lwss = PATTERNS.find((p) => p.id === 'lwss');
  assert.equal(h.stampRle(lwss.rle, 5, 13), 9);
  const before = bboxOf(decodeRle(lwss.rle));
  for (let i = 0; i < 4; i++) h.step();
  // Bounding-box width is unchanged after a full 4-step cycle phase...
  const live = [];
  for (let y = 0; y < 30; y++) for (let x = 0; x < 40; x++) if (h.get(x, y)) live.push([x, y]);
  assert.equal(live.length, 9, 'LWSS population intact after wrap-free travel');
  const bx0 = Math.min(...live.map(([x]) => x));
  assert.equal(bx0, 5 - 2, 'LifeWiki LWSS.rle phase travels LEFT at c/2: 2 columns in 4 generations');
  void before;
});

test('engine module has zero DOM/host surface', async () => {
  const { readFile } = await import('node:fs/promises');
  const src = await readFile(new URL('../src/engine.ts', import.meta.url), 'utf8');
  assert.doesNotMatch(src, /\bimport\b/, 'engine must be import-free (pure module)');
  assert.doesNotMatch(src, /document|window|canvas|localStorage/i);
});