import test from 'node:test' import assert from 'node:assert/strict' import { runIO, runApp } from '../src/runtime.js' import { pure, draw, randomSeed, map, sequence } from '../src/io.js' import { view } from '../src/view.js' import { newGame, tick } from '../src/spawn.js' import { classicMain } from './m09-helpers.js' // A fake platform: records every call and lets the test fire events by hand. const fakePlatform = (seed = 42) => { const calls = [] const handlers = {} return { calls, fillRect: (...args) => calls.push(['fillRect', ...args]), fillText: (...args) => calls.push(['fillText', ...args]), randomSeed: () => { calls.push(['randomSeed']); return seed }, startClock: (cb) => { handlers.time = cb }, onKeyDown: (cb) => { handlers.down = cb }, onKeyUp: (cb) => { handlers.up = cb }, fireTime: (t) => handlers.time(t), fireKey: (k) => handlers.down(k), fireKeyUp: (k) => handlers.up(k), } } // The m09 program (classicMain) ticks every 150 ms; the shipped `main` uses speedActions after m10. const main = classicMain // What the platform should have been asked to draw for a scene. const expectedCalls = (scene) => scene.map((s) => s.tag === 'Rect' ? ['fillRect', s.x, s.y, s.w, s.h, s.color] : ['fillText', s.x, s.y, s.text, s.color, s.size, s.align]) const rect = { tag: 'Rect', x: 1, y: 2, w: 3, h: 4, color: '#abcdef' } const text = { tag: 'Text', x: 5, y: 6, text: 'hi', color: '#fff', size: 16, align: 'center' } test('m09 runIO of a pure value returns it without touching the platform', () => { const p = fakePlatform() assert.equal(runIO(pure(7), p), 7) assert.deepStrictEqual(p.calls, []) }) test('m09 runIO maps Draw Rect to fillRect(x, y, w, h, color)', () => { const p = fakePlatform() runIO(draw(rect), p) assert.deepStrictEqual(p.calls, [['fillRect', 1, 2, 3, 4, '#abcdef']]) }) test('m09 runIO maps Draw Text to fillText(x, y, text, color, size, align)', () => { const p = fakePlatform() runIO(draw(text), p) assert.deepStrictEqual(p.calls, [['fillText', 5, 6, 'hi', '#fff', 16, 'center']]) }) test('m09 runIO answers RandomSeed with platform.randomSeed() and continues with it', () => { const p = fakePlatform(99) assert.equal(runIO(map(randomSeed, (s) => s + 1), p), 100) assert.deepStrictEqual(p.calls, [['randomSeed']]) }) test('m09 runIO performs effects in order', () => { const p = fakePlatform() runIO(sequence([draw(rect), draw(text), draw(rect)]), p) assert.deepStrictEqual(p.calls.map((c) => c[0]), ['fillRect', 'fillText', 'fillRect']) }) test('m09 runIO handles 100000 effects without overflowing the stack', () => { const p = fakePlatform() const n = 100_000 runIO(sequence(Array.from({ length: n }, () => draw(rect))), p) assert.equal(p.calls.length, n) }) test('m09 runApp asks the platform for a seed once, at start, and draws nothing before the first input', () => { const p = fakePlatform() runApp(main, p) assert.deepStrictEqual(p.calls, [['randomSeed']]) }) test('m09 runApp draws the initial scene on the first clock tick, then the ticked scene after 150 ms', () => { const p = fakePlatform(42) runApp(main, p) p.calls.length = 0 p.fireTime(150) assert.deepStrictEqual(p.calls, [ ...expectedCalls(view(newGame(42))), ...expectedCalls(view(tick(newGame(42)))), ]) }) test('m09 runApp feeds key presses to the game', () => { const p = fakePlatform(42) runApp(main, p) p.fireTime(0) p.calls.length = 0 p.fireKey('ArrowUp') p.fireTime(150) const turned = { ...newGame(42), nextDir: 'Up' } assert.deepStrictEqual(p.calls, [ ...expectedCalls(view(turned)), ...expectedCalls(view(tick(turned))), ]) }) test('m09 runApp ignores key releases and unmapped keys (no redraw)', () => { const p = fakePlatform(42) runApp(main, p) p.fireTime(0) p.calls.length = 0 p.fireKeyUp('ArrowUp') p.fireKey('x') assert.deepStrictEqual(p.calls, []) })