import { writeFileSync } from 'node:fs' import { lifeStep, lifeUpdate } from '../../js/solutions/life/rules.js' import { lifeView } from '../../js/solutions/life/view.js' import { lifeKey, lifeClick } from '../../js/solutions/life/app.js' import { LifeAction as A, initialLife } from '../../js/solutions/life/model.js' // Fixture regeneration aid (see scripts/fixtures/README.md). Written during the 2026-11-11 build, kept for reference. // Writes into fixtures/ unless FIXTURES_OUT names another directory (with a trailing slash). const FIX = process.env.FIXTURES_OUT ?? new URL('../../fixtures/', import.meta.url).pathname const SOL = new URL('../../js/solutions/', import.meta.url).href const HERE = new URL('./', import.meta.url).pathname const F = FIX const fmt = (fn, cases) => '{\n "function": "' + fn + '",\n "cases": [\n' + cases.map((c) => ' ' + JSON.stringify(c)).join(',\n') + '\n ]\n}\n' const P = (...xy) => { const out = []; for (let i = 0; i < xy.length; i += 2) out.push({ x: xy[i], y: xy[i + 1] }); return out.sort((a, b) => a.y - b.y || a.x - b.x) } const step = [ ['an-empty-board-stays-empty', []], ['a-lonely-cell-dies', P(5, 5)], ['two-cells-die-of-loneliness', P(5, 5, 6, 5)], ['a-block-is-stable', P(5, 5, 6, 5, 5, 6, 6, 6)], ['a-horizontal-blinker-turns-vertical', P(4, 5, 5, 5, 6, 5)], ['a-vertical-blinker-turns-horizontal', P(5, 4, 5, 5, 5, 6)], ['three-cells-in-an-l-give-birth-to-a-block', P(5, 5, 6, 5, 5, 6)], ['a-cell-with-four-neighbours-dies-of-overcrowding', P(5, 4, 4, 5, 5, 5, 6, 5, 5, 6)], ['a-glider-moves-and-the-result-is-sorted-row-major', P(1, 0, 2, 1, 0, 2, 1, 2, 2, 2)], ['a-block-in-the-corner-is-stable', P(0, 0, 1, 0, 0, 1, 1, 1)], ['a-block-in-the-far-corner-is-stable', P(38, 28, 39, 28, 38, 29, 39, 29)], ['a-blinker-on-the-top-edge-cannot-grow-beyond-it', P(4, 0, 5, 0, 6, 0)], ['a-blinker-on-the-right-edge-does-not-wrap-to-the-left-edge', P(39, 9, 39, 10, 39, 11)], ['cells-on-opposite-edges-are-not-neighbours', P(0, 10, 39, 9, 39, 10, 39, 11)], ['a-blinker-on-the-bottom-edge-cannot-grow-beyond-it', P(10, 29, 11, 29, 12, 29)], ] writeFileSync(F + 'm13-life-step.json', fmt('lifeStep', step.map(([name, c]) => ({ name, args: [c], expected: lifeStep(c) })))) const st = (cells, running, seed, generation) => ({ cells, running, seed, generation }) const blinker = P(4, 5, 5, 5, 6, 5) const upd = [ ['frame-while-running-computes-the-next-generation', st(blinker, true, 7, 3), A.Frame], ['frame-while-paused-is-unchanged', st(blinker, false, 7, 3), A.Frame], ['frame-on-an-empty-running-board-still-counts-the-generation', st([], true, 7, 3), A.Frame], ['step-while-paused-computes-one-generation', st(blinker, false, 7, 3), A.Step], ['step-while-running-is-ignored', st(blinker, true, 7, 3), A.Step], ['toggle-pause-starts-a-paused-world', st(blinker, false, 7, 3), A.TogglePause], ['toggle-pause-pauses-a-running-world', st(blinker, true, 7, 3), A.TogglePause], ['toggle-on-an-empty-board-adds-the-cell', st([], false, 7, 0), A.Toggle({ x: 3, y: 2 })], ['toggle-adds-a-dead-cell-in-row-major-order', st(blinker, false, 7, 3), A.Toggle({ x: 9, y: 4 })], ['toggle-adds-a-cell-between-two-on-the-same-row', st(P(4, 5, 6, 5), false, 7, 3), A.Toggle({ x: 5, y: 5 })], ['toggle-adds-a-cell-before-all-others', st(blinker, false, 7, 3), A.Toggle({ x: 0, y: 0 })], ['toggle-adds-a-cell-after-all-others', st(blinker, false, 7, 3), A.Toggle({ x: 39, y: 29 })], ['toggle-removes-a-live-cell', st(blinker, false, 7, 3), A.Toggle({ x: 5, y: 5 })], ['toggle-adds-a-cell-at-the-start-of-a-row', st(blinker, false, 7, 3), A.Toggle({ x: 4, y: 5 })], ['toggle-works-while-running-too', st(blinker, true, 7, 3), A.Toggle({ x: 5, y: 6 })], ['toggle-off-the-board-is-ignored', st(blinker, false, 7, 3), A.Toggle({ x: 40, y: 3 })], ['toggle-above-the-board-is-ignored', st(blinker, false, 7, 3), A.Toggle({ x: 3, y: -1 })], ['toggle-just-below-the-board-is-ignored', st(blinker, false, 7, 3), A.Toggle({ x: 3, y: 30 })], ['clear-empties-the-board-and-restarts-the-count', st(blinker, true, 7, 3), A.Clear], ['randomize-fills-the-board-from-the-seed-and-advances-it', st([], false, 42, 0), A.Randomize], ['randomize-replaces-the-cells-restarts-the-count-and-keeps-running', st(blinker, true, 7, 12), A.Randomize], ] // toggle-twice: apply on the result of removing (4,5): start state has blinker minus (4,5) upd[13][1] = st(P(5, 5, 6, 5), false, 7, 3) writeFileSync(F + 'm13-life-update.json', fmt('lifeUpdate', upd.map(([name, s, a]) => ({ name, args: [s, a], expected: lifeUpdate(s, a) })))) const vw = [ ['a-new-world-is-an-empty-paused-board', initialLife(42)], ['live-cells-in-row-major-order-while-running', st(P(0, 0, 5, 5, 39, 29), true, 1, 17)], ['paused-after-some-generations', st(P(4, 5, 5, 5, 6, 5), false, 1, 4)], ] writeFileSync(F + 'm13-life-view.json', fmt('lifeView', vw.map(([name, s]) => ({ name, args: [s], expected: lifeView(s) })))) const keys = [['space-toggles-pause', ' '], ['n-steps', 'n'], ['N-steps', 'N'], ['c-clears', 'c'], ['C-clears', 'C'], ['r-randomizes', 'r'], ['R-randomizes', 'R'], ['ArrowUp-is-not-bound', 'ArrowUp'], ['Enter-is-not-bound', 'Enter'], ['constructor-is-not-bound', 'constructor'], ['toString-is-not-bound', 'toString'], ['empty-string-is-not-bound', '']] writeFileSync(F + 'm13-life-key.json', fmt('lifeKey', keys.map(([name, k]) => ({ name, args: [k], expected: lifeKey(k) })))) const clicks = [['the-top-left-pixel-is-cell-0-0', 0, 0], ['the-last-pixel-of-the-first-cell', 15, 15], ['the-next-pixel-is-the-next-cell', 16, 0], ['the-row-below', 3, 16], ['a-pixel-in-the-middle', 100, 250], ['the-bottom-right-pixel-of-the-board', 639, 479], ['right-of-the-board-is-nothing', 640, 10], ['the-status-strip-under-the-board-is-nothing', 10, 480], ['left-of-the-board-is-nothing', -1, 10], ['above-the-board-is-nothing', 10, -1]] writeFileSync(F + 'm13-life-click.json', fmt('lifeClick', clicks.map(([name, x, y]) => ({ name, args: [x, y], expected: lifeClick(x, y) }))))