import { readFileSync, writeFileSync } from 'node:fs' // 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 { update } = await import(SOL + 'game.js') const { tick } = await import(SOL + 'spawn.js') const { pongUpdate } = await import(SOL + 'pong/rules.js') const load = (n) => JSON.parse(readFileSync(`${FIX}${n}.json`, 'utf8')) const save = (n, fx) => writeFileSync(`${FIX}${n}.json`, JSON.stringify(fx, null, 2) + '\n') const clone = (x) => JSON.parse(JSON.stringify(x)) // ---- m10-update { const fx = load('m10-update') const base = fx.cases.find((c) => c.name === 'frame-below-threshold-only-counts-the-frame').args[0] const mk = (name, patch) => { const s = { ...clone(base), ...patch }; return { name, args: [s, { tag: 'Frame' }], expected: update(s, { tag: 'Frame' }) } } const frames = [ mk('frame-below-threshold-only-counts-the-frame', { frame: 0 }), mk('frame-just-before-threshold-only-counts-the-frame', { frame: 6 }), mk('frame-at-threshold-ticks-the-game-and-restarts-the-count', { frame: 7 }), mk('frame-after-a-move-counts-from-zero-again', { snake: [{ x: 1, y: 0 }], frame: 0 }), mk('frame-tick-that-eats-restarts-the-count', { frame: 7, score: 2, apple: { x: 1, y: 0 } }), mk('frame-just-before-the-shrunk-threshold-only-counts-the-frame', { frame: 5, score: 3 }), mk('frame-threshold-shrinks-as-score-grows', { frame: 6, score: 3 }), mk('frame-at-top-speed-first-frame-does-not-tick', { frame: 0, score: 18 }), mk('frame-at-top-speed-ticks-every-second-frame', { frame: 1, score: 18 }), mk('frame-count-past-the-threshold-still-ticks', { frame: 9 }), mk('frame-tick-can-end-the-game', { snake: [{ x: 2, y: 0 }], frame: 7 }), ] const keep = fx.cases.filter((c) => !c.name.startsWith('frame-') || c.name.startsWith('frame-when-')) const i = keep.findIndex((c) => c.name.startsWith('frame-when-')) fx.cases = [...keep.slice(0, i), ...frames, ...keep.slice(i)] save('m10-update', fx) } // ---- m04-tick { const fx = load('m04-tick') const s = { width: 2, height: 2, snake: [{ x: 0, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }], dir: 'Up', nextDir: 'Right', apple: { x: 1, y: 0 }, seed: 7, score: 2, status: 'Playing', frame: 0 } fx.cases = [...fx.cases.filter((c) => c.name !== 'eating-the-last-free-cell-wins'), { name: 'eating-the-last-free-cell-wins', args: [s], expected: tick(s) }] save('m04-tick', fx) } // ---- m11-pong-update { const fx = load('m11-pong-update') const base = fx.cases.find((c) => c.name === 'ball-bounces-off-the-player-paddle').args[0] const mk = (name, patch, action = { tag: 'Frame' }) => { const s = { ...clone(base), ...patch }; return { name, args: [s, action], expected: pongUpdate(s, action) } } fx.cases.forEach((c) => { if (c.name.startsWith('restart-')) c.expected = pongUpdate(c.args[0], c.args[1]) }) const extra = [ mk('player-paddle-catches-the-ball-at-its-first-row', { ball: { x: 3, y: 19 } }), mk('ball-may-enter-the-top-row', { ball: { x: 40, y: 1 }, vel: { dx: 1, dy: -1 } }), mk('ball-may-enter-the-bottom-row', { ball: { x: 40, y: 48 }, vel: { dx: 1, dy: 1 } }), mk('ball-in-column-zero-is-still-in-play', { ball: { x: 1, y: 40 }, vel: { dx: -1, dy: 1 } }), mk('ball-in-the-last-column-is-still-in-play', { ball: { x: 78, y: 40 }, vel: { dx: 1, dy: 1 } }), mk('restart-keeps-the-held-keys', { held: { up: true, down: false }, score: { player: 1, cpu: 4 }, frame: 250, player: 3 }, { tag: 'Restart' }), ] fx.cases = [...fx.cases.filter((c) => !extra.some((e) => e.name === c.name)), ...extra] save('m11-pong-update', fx) }