feat(js): m10 extensions (pause, restart, speed-up via Frame) with tests

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-28 23:56:49 +01:00
co-authored by Claude Sonnet 5.5
parent 25141ce86d
commit fd7c5f2acf
13 changed files with 1347 additions and 17 deletions
+6 -4
View File
@@ -1,8 +1,8 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { states, classicActions, render, main, TICK_MS } from '../src/app.js'
import { run } from '../src/stream.js'
import { trace } from '../src/io.js'
import { run, map as mapStream } from '../src/stream.js'
import { trace, map as mapIO, randomSeed } from '../src/io.js'
import { view } from '../src/view.js'
import { newGame } from '../src/spawn.js'
import { Action } from '../src/model.js'
@@ -40,8 +40,10 @@ test('m09 main asks for a random seed, then builds the stream from it', () => {
assert.deepStrictEqual(trace(outs[0], 0).ops, trace(render(newGame(42)), 0).ops)
})
test('m09 main with seed 42 draws the new game first, then the ticked game after 150 ms', () => {
const { result: stream } = trace(main, 42)
// `main` itself switches to speedActions in m10, so the 150 ms behaviour is checked on the same program built from classicActions.
test('m09 the program built from classicActions draws the new game first, then the ticked game after 150 ms', () => {
const classicMain = mapIO(randomSeed, (seed) => mapStream(states(seed, classicActions), render))
const { result: stream } = trace(classicMain, 42)
const [first, next] = stream.step(time(0))
const [second] = next.step(time(150))
assert.deepStrictEqual(trace(first[0], 0).ops.map((o) => o.shape), view(newGame(42)))
+5 -1
View File
@@ -2,7 +2,8 @@ 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 { main } from '../src/app.js'
import { states, classicActions, render } from '../src/app.js'
import { map as mapStream } from '../src/stream.js'
import { view } from '../src/view.js'
import { newGame, tick } from '../src/spawn.js'
@@ -24,6 +25,9 @@ const fakePlatform = (seed = 42) => {
}
}
// The m09 program: ticks every 150 ms. (The shipped `main` uses speedActions after m10, so these tests build their own.)
const main = map(randomSeed, (seed) => mapStream(states(seed, classicActions), render))
// What the platform should have been asked to draw for a scene.
const expectedCalls = (scene) =>
scene.map((s) =>
+51
View File
@@ -0,0 +1,51 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { update, movesEvery } from '../src/game.js'
import { keyToAction } from '../src/input.js'
import { speedActions, main, FRAME_MS } from '../src/app.js'
import { run } from '../src/stream.js'
import { trace } from '../src/io.js'
import { view } from '../src/view.js'
import { newGame } from '../src/spawn.js'
import { Action } from '../src/model.js'
import { loadFixture, freeze } from './helpers.js'
const fixtureTests = (name, f) => {
const fx = loadFixture(name)
fx.cases.forEach((c) =>
test(`m10 ${fx.function}: ${c.name}`, () => assert.deepStrictEqual(f(...freeze(c.args)), c.expected)))
}
fixtureTests('m10-update', update)
fixtureTests('m10-moves-every', movesEvery)
fixtureTests('m10-input', keyToAction)
const time = (t) => ({ tag: 'Time', t })
test('m10 the animation clock runs every 25 ms', () => assert.equal(FRAME_MS, 25))
test('m10 speedActions turns the clock into Frame actions every 25 ms', () =>
assert.deepStrictEqual(
run(speedActions, freeze([25, 50, 75, 100, 125, 150, 175, 200].map(time))),
Array.from({ length: 8 }, () => Action.Frame)))
test('m10 speedActions still turns keys into actions, including pause and restart', () =>
assert.deepStrictEqual(
run(speedActions, freeze([{ tag: 'KeyDown', key: ' ' }, { tag: 'KeyDown', key: 'r' }, { tag: 'KeyDown', key: 'ArrowUp' }])),
[Action.TogglePause, Action.Restart, Action.Turn('Up')]))
test('m10 main (seed 42) draws a moved snake after eight 25 ms frames', () => {
const { ops, result: stream } = trace(main, 42)
assert.deepStrictEqual(ops, [{ tag: 'RandomSeed' }])
const [first, afterFirst] = stream.step(time(0))
assert.deepStrictEqual(trace(first[0], 0).ops.map((o) => o.shape), view(newGame(42)))
const [drawings] = [25, 50, 75, 100, 125, 150, 175, 200].reduce(
([outs, s], t) => {
const [out, next] = s.step(time(t))
return [[...outs, ...out], next]
},
[[], afterFirst])
assert.equal(drawings.length, 8)
const moved = { ...newGame(42), snake: newGame(42).snake.map((p) => ({ x: p.x + 1, y: p.y })), frame: 8 }
assert.deepStrictEqual(trace(drawings[7], 0).ops.map((o) => o.shape), view(moved))
})