// 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']) })