// The oracle: the pure JS solutions, run in Node, say what a page must draw for a given seed, stored best and list of // inputs. Nothing here touches a browser. // // A program (`main`, `pongMain`, `replayMain(recording)`, `lifeMain`) is an IO that yields a stream; `trace` answers // its RandomSeed and LoadBest ops with the test's seed and best, exactly as the course's `play(main, seed, events, // best)` does (js/solutions/app.js). The Model then steps that stream one input at a time and traces each IO it emits, // so it knows, per input, which shapes are drawn and which scores are saved. Flattened, its output equals `play`'s // (tests/techniques.spec.mjs checks that); grouped per input, it lines up with the page's recorded frames. // // Next to the program, the Model steps a stream of the program's *states*, built from the same pieces as the program // (e.g. `clampTime(sessions(seed, best, speedActions), MAX_GAP_MS)` for Snake). It is used only to plan inputs and to // state assertions in game terms ("the head moved one cell"); every judgement of the page is made on its drawing. import { readFileSync } from 'node:fs' import { trace } from '../../js/solutions/io.js' import { clampTime } from '../../js/solutions/stream.js' import { main, play, sessions, sessionView, speedActions, MAX_GAP_MS, FRAME_MS } from '../../js/solutions/app.js' import { movesEvery } from '../../js/solutions/game.js' import { newGame } from '../../js/solutions/spawn.js' import { pongMain, pongStates, pongActions, FRAME_MS_PONG } from '../../js/solutions/pong/app.js' import { pongView } from '../../js/solutions/pong/view.js' import { replayMain, viewerStates, replayActions, viewerView, frames as replayFrames, REPLAY_MS } from '../../js/solutions/replay.js' import { lifeMain, lifeStates, lifeActions, GEN_MS } from '../../js/solutions/life/app.js' import { lifeView } from '../../js/solutions/life/view.js' import { LCELL, LW, LH } from '../../js/solutions/life/model.js' export const recording = JSON.parse(readFileSync(new URL('../../fixtures/replay-sample.json', import.meta.url), 'utf8')) /** The four pages of a track, the program each runs, and the matching stream of states. */ export const programs = { snake: { page: 'index.html', main: () => main, states: (seed, best) => clampTime(sessions(seed, best, speedActions), MAX_GAP_MS), }, pong: { page: 'pong.html', main: () => pongMain, states: (seed) => clampTime(pongStates(seed, pongActions), MAX_GAP_MS), }, replay: { page: 'replay.html', main: () => replayMain(recording), states: () => clampTime(viewerStates(replayFrames(recording).length - 1, replayActions), MAX_GAP_MS), }, life: { page: 'life.html', main: () => lifeMain, states: (seed) => clampTime(lifeStates(seed, lifeActions), MAX_GAP_MS), }, } export class Model { constructor(name, { seed, best = 0 }) { const program = programs[name] this.name = name this.seed = seed this.best = best const traced = trace(program.main(), seed, best) this.startOps = traced.ops // RandomSeed, LoadBest: what the program asks before it starts this.stream = traced.result this.stateStream = program.states(seed, best) this.state = null // latest state the program emitted this.inputs = [] // every input fed so far this.frames = [] // per input that drew anything: {input, shapes} this.saves = [] // scores passed to SaveBest, in order } /** Feed inputs, in order, as the page received them. */ feed(inputs) { for (const input of inputs) { const [ios, next] = this.stream.step(input) this.stream = next const ops = ios.flatMap((io) => trace(io, this.seed, this.best).ops) const shapes = ops.filter((op) => op.tag === 'Draw').map((op) => op.shape) this.saves.push(...ops.filter((op) => op.tag === 'SaveBest').map((op) => op.score)) if (shapes.length > 0) this.frames.push({ input, shapes }) const [states, nextStates] = this.stateStream.step(input) this.stateStream = nextStates if (states.length > 0) this.state = states[states.length - 1] this.inputs.push(input) } return this } /** What the state would be after these inputs, without feeding them. Streams are values, so this is free. */ peek(inputs) { const [states] = inputs.reduce(([acc, s], input) => { const [out, next] = s.step(input) return [out.length > 0 ? out[out.length - 1] : acc, next] }, [this.state, this.stateStream]) return states } /** The course's own one-equality run of the program over every input fed so far. */ play() { return play(programs[this.name].main(), this.seed, this.inputs, this.best) } lastFrame() { return this.frames.length === 0 ? null : this.frames[this.frames.length - 1] } } // Re-exported for tests that state expectations directly in model terms. export { sessionView, newGame, movesEvery, pongView, viewerView, replayFrames, lifeView } export { FRAME_MS, MAX_GAP_MS, FRAME_MS_PONG, REPLAY_MS, GEN_MS, LCELL, LW, LH }