feat(course): audit fixes and twelve extensions (Modules 7b, 12, 13)
Audit pass: - Frame rule counts frames since the last move, so every step lasts exactly movesEvery(score) frames, also right after a speed-up - clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap - Pong Restart keeps held keys - Fact corrections (mulberry32 output coverage, Haskell IO, uniform) - New fixture cases from a mutation analysis Extensions (all three tracks, shared fixtures, course text): - Turn queue (Exercise 10.6), persisted high score (10.7), replay viewer (10.8), play() end-to-end helper (9.5) - Optional State (4.7) and optional Module 7b, stack-safe IO - Module 12, property-based testing with generators, shrinking, invariants and a model-based oracle - Module 13, Conway's Life with a Click event - JS type checking via jsconfig, JSDoc and types.d.ts - Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh, Playwright end-to-end suite (e2e/, scripts/e2e.sh) Docs: docs/contract.md is the canonical contract; specs and build log under docs/superpowers. Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
876f5aabb1
commit
edd57f0148
253 files changed
+21753
-503
No files matched your search
@@ -0,0 +1,69 @@
|
||||
// Each of the three techniques the other specs build on, proven on its own, on every track; and the oracle checked
|
||||
// against the course's own `play`.
|
||||
import { test, expect, TRACKS } from '../lib/app.mjs'
|
||||
import { seedCalls, SEED } from '../lib/seed.mjs'
|
||||
import { framesFrom, frameCount } from '../lib/drawing.mjs'
|
||||
import { FRAME } from '../lib/clock.mjs'
|
||||
import { Model, sessionView, newGame } from '../lib/oracle.mjs'
|
||||
|
||||
for (const track of TRACKS) {
|
||||
test.describe(`${track}: techniques`, () => {
|
||||
test('fixed seed: the page asks for one Uint32Array(1) and starts the game that seed makes', async ({ app }) => {
|
||||
const a = await app(track, 'snake')
|
||||
await a.start()
|
||||
expect(await seedCalls(a.page)).toEqual([{ type: 'Uint32Array', length: 1 }])
|
||||
// The apple of the first frame is where newGame(SEED) puts it; another seed would (almost surely) put it elsewhere.
|
||||
const apple = (await a.expectInSync()).find((s) => s.tag === 'Rect' && s.color === '#e53935')
|
||||
const want = newGame(SEED).apple
|
||||
expect(apple).toMatchObject({ x: want.x * 20 + 1, y: want.y * 20 + 1 })
|
||||
expect(newGame(SEED + 1).apple).not.toEqual(want)
|
||||
})
|
||||
|
||||
test('fake clock: frames come only when the test moves the clock, with the timestamps the clock arithmetic predicts', async ({ app, page }) => {
|
||||
const a = await app(track, 'snake')
|
||||
await a.start()
|
||||
// Paused: real time passing changes nothing.
|
||||
const before = await frameCount(page)
|
||||
await page.waitForTimeout(300)
|
||||
expect(await frameCount(page)).toBe(before)
|
||||
// 1000 ms of fake time: the page draws once per 25 ms game frame, and each drawing happens on the first
|
||||
// animation frame (a multiple of 16 ms after the first) at or after that game frame's time.
|
||||
const startTick = a.clock.now
|
||||
await a.advance(1000)
|
||||
expect(await a.clock.ticks()).toBe(startTick + 1000)
|
||||
const frames = await framesFrom(page, 0)
|
||||
const times = frames.map((f) => f.at - a.clock.t0)
|
||||
const due = Array.from({ length: 40 }, (_, i) => (i + 1) * 25).map((g) => Math.ceil(g / FRAME) * FRAME).filter((t) => t <= 1000)
|
||||
expect(times).toEqual([0, ...due])
|
||||
// And the model, fed the Time events of the same arithmetic, drew exactly the same frames.
|
||||
await a.expectInSync()
|
||||
// One long frame: fastForward fires one animation frame, 10 s later.
|
||||
await a.jump(10_000)
|
||||
const [last] = (await framesFrom(page, frames.length)).slice(-1)
|
||||
expect(last.at - a.clock.t0).toBe(1000 + 10_000)
|
||||
await a.expectInSync()
|
||||
})
|
||||
|
||||
test('recorded drawing: the first frame is the new session\'s scene, with colours, fonts and alignment as drawn', async ({ app, page }) => {
|
||||
const a = await app(track, 'snake')
|
||||
await a.start()
|
||||
const [first] = await framesFrom(page, 0)
|
||||
// Read straight from the recorder, before any normalisation: what the canvas reported at each call.
|
||||
const text = first.shapes.find((s) => s.tag === 'Text' && s.text.startsWith('Best'))
|
||||
expect(text).toMatchObject({ text: 'Best: 0', color: '#ffffff', align: 'right', x: 394, y: 18 })
|
||||
expect(text.font).toMatch(/16px monospace/)
|
||||
expect(first.shapes[0]).toEqual({ tag: 'Rect', x: 0, y: 0, w: 400, h: 400, color: '#111111' })
|
||||
expect(await a.expectInSync()).toEqual(await a.expected(sessionView({ game: newGame(SEED), best: 0, improved: false })))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test('oracle: fed input by input, the model emits exactly what the course\'s play() emits for the same inputs', () => {
|
||||
const m = new Model('snake', { seed: SEED, best: 4 })
|
||||
const times = Array.from({ length: 80 }, (_, i) => ({ tag: 'Time', t: i * 16 }))
|
||||
m.feed(times.slice(0, 30)).feed([{ tag: 'KeyDown', key: 'ArrowUp' }, { tag: 'KeyUp', key: 'ArrowUp' }]).feed(times.slice(30))
|
||||
m.feed([{ tag: 'Time', t: 80 * 16 + 10_000 }])
|
||||
const drawn = m.play().flat().filter((op) => op.tag === 'Draw').map((op) => op.shape)
|
||||
expect(m.frames.flatMap((f) => f.shapes)).toEqual(drawn)
|
||||
expect(m.startOps.map((op) => op.tag)).toEqual(['RandomSeed', 'LoadBest'])
|
||||
})
|
||||
Reference in new issue
Block a user