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>
87 lines
3.9 KiB
JavaScript
87 lines
3.9 KiB
JavaScript
import { todo } from './todo.js'
|
|
import { pure } from './io.js'
|
|
/** @import { Action, GameState, Session, Scene, IO, Stream, Input, Op, Program } from './types' */
|
|
|
|
/** Milliseconds between game steps. */
|
|
export const TICK_MS = 150
|
|
|
|
/** Milliseconds between animation frames (m10). */
|
|
export const FRAME_MS = 25
|
|
|
|
/** (m10) The longest step of time the game accepts at once, in milliseconds (see `clampTime` in stream.js). */
|
|
export const MAX_GAP_MS = 250
|
|
|
|
/**
|
|
* Stream of everything that can happen to the game: `Action.Tick` every TICK_MS (from `interval`),
|
|
* merged with the `Action` for each key press that `keyToAction` recognises (from `keyDowns`).
|
|
* @type {Stream<Action>}
|
|
*/
|
|
export const classicActions = { step: () => todo('m09') }
|
|
|
|
/**
|
|
* (m10) Like `classicActions`, but the clock emits `Action.Frame` every FRAME_MS instead of `Tick` every TICK_MS;
|
|
* key presses are translated by `keyToAction` as before (now including pause and restart).
|
|
* @type {Stream<Action>}
|
|
*/
|
|
export const speedActions = { step: () => todo('m10') }
|
|
|
|
/**
|
|
* Fold a stream of actions into a stream of game states: start from `newGame(seed)`, apply `update` to each action
|
|
* (scan), and emit the initial state first (startWith) so there is something to draw before anything happens.
|
|
* @type {(seed: number, actions: Stream<Action>) => Stream<GameState>}
|
|
*/
|
|
export const states = (seed, actions) => todo('m09')
|
|
|
|
/** The drawing of one state, as an IO: `drawScene(view(state))`.
|
|
* @type {(state: GameState) => IO<undefined>} */
|
|
export const render = (state) => todo('m09')
|
|
|
|
/**
|
|
* (m10) Session = `{game, best, improved}`: the game, the best score so far, and whether the action that produced
|
|
* this session raised the best. One action: `game' = update(game, action)`, `best' = max(best, game'.score)`,
|
|
* `improved = best' > best` (strictly greater: equalling the record is not a new record).
|
|
* @type {(session: Session, action: Action) => Session}
|
|
*/
|
|
export const sessionUpdate = (session, action) => todo('m10')
|
|
|
|
/**
|
|
* (m10) Like `states`, one level up: `startWith(scan(actions, sessionUpdate, s0), s0)`
|
|
* with `s0 = {game: newGame(seed), best, improved: false}`.
|
|
* @type {(seed: number, best: number, actions: Stream<Action>) => Stream<Session>}
|
|
*/
|
|
export const sessions = (seed, best, actions) => todo('m10')
|
|
|
|
/**
|
|
* (m10) `view(session.game)` followed by one more shape: the text `Best: <best>` at the top right,
|
|
* `{tag:'Text', x: game.width * CELL - 6, y: 18, text: 'Best: 3', color: '#ffffff', size: 16, align: 'right'}`.
|
|
* @type {(session: Session) => Scene}
|
|
*/
|
|
export const sessionView = (session) => todo('m10')
|
|
|
|
/**
|
|
* (m10) `drawScene(sessionView(session))`, followed by `saveBest(session.best)` when `session.improved`
|
|
* (and only then).
|
|
* @type {(session: Session) => IO<undefined>}
|
|
*/
|
|
export const renderSession = (session) => todo('m10')
|
|
|
|
/**
|
|
* The whole program as a value, `IO<Stream<IO<unit>>>`: ask for a random seed, then turn the stream of
|
|
* states (built from `classicActions` in m09, from `speedActions` after m10) into a stream of drawings (`render`).
|
|
* (m10) The whole stream is wrapped in `clampTime(..., MAX_GAP_MS)`.
|
|
* (m10, Exercise 10.7) After the seed, ask for the stored best (`loadBest`), and draw the stream of
|
|
* `sessions(seed, best, speedActions)` with `renderSession` instead of the states with `render`.
|
|
* Nothing runs here.
|
|
* @type {Program}
|
|
*/
|
|
export const main = pure({ step: () => todo('m09') })
|
|
|
|
/**
|
|
* (m09) The whole program in one equality: for each drawing that `main` emits while it is fed `events`
|
|
* (a list of inputs), the list of ops that drawing performs. Interpret `main` with `trace(main, seed)`, `run` the
|
|
* stream it produces on `events`, and `trace` each emitted IO, keeping its `ops`.
|
|
* (m10) Pass `best` to both traces as well, so that `LoadBest` is answered with it.
|
|
* @type {(main: Program, seed: number, events: Input[], best?: number) => Op[][]}
|
|
*/
|
|
export const play = (main, seed, events, best = 0) => todo('m09')
|