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>
93 lines
3.8 KiB
TypeScript
93 lines
3.8 KiB
TypeScript
// Given, not an exercise: the course's data types, written down for an editor that understands types (optional,
|
|
// see Module 0, "If your editor understands types"). Nothing imports this file at run time; the JS files refer to it
|
|
// from their doc comments (`@import`, `@type`). Every sum type is a union of plain objects with a `tag`, so a
|
|
// `switch (x.tag)` narrows `x` to one case inside each `case`.
|
|
|
|
// ---- m02 model, m10 replay viewer, m11 Pong, m13 Life
|
|
// The model types are written where the model is: as `@typedef` comments in model.js (Module 2 shows them there),
|
|
// pong/model.js, life/model.js and replay.js. Each action union sits next to the object of the same name that
|
|
// builds its values, so `import { Action } from './model.js'` gives both. They are re-exported here so that every
|
|
// type can be found in one place.
|
|
|
|
import type { Point, Direction, Status, Action, GameState } from './model.js'
|
|
import type { Viewer, ViewerAction } from './replay.js'
|
|
import type { PongState, PongAction } from './pong/model.js'
|
|
import type { LifeState, LifeAction } from './life/model.js'
|
|
export type { Point, Direction, Status, Action, GameState, Viewer, ViewerAction, PongState, PongAction, LifeState, LifeAction }
|
|
|
|
// ---- m04 random and state
|
|
|
|
/** A random value: a function from a seed to the value and the next seed. */
|
|
export type Random<A> = (seed: number) => [A, number]
|
|
/** A value that reads and replaces a state of type S. `Random<A>` is `State<number, A>`. */
|
|
export type State<S, A> = (s: S) => [A, S]
|
|
|
|
// ---- m06 view
|
|
|
|
export type Align = 'left' | 'center' | 'right'
|
|
export type Shape =
|
|
| { tag: 'Rect'; x: number; y: number; w: number; h: number; color: string }
|
|
| { tag: 'Text'; x: number; y: number; text: string; color: string; size: number; align: Align }
|
|
export type Scene = Shape[]
|
|
|
|
// ---- m07 IO
|
|
|
|
export type Op =
|
|
| { tag: 'Draw'; shape: Shape }
|
|
| { tag: 'RandomSeed' }
|
|
| { tag: 'LoadBest' }
|
|
| { tag: 'SaveBest'; score: number }
|
|
|
|
/** A program that produces an A. The answer given to `k` depends on the op (a number, or undefined), hence `any`. */
|
|
export type IO<A> =
|
|
| { tag: 'Pure'; value: A }
|
|
| { tag: 'Effect'; op: Op; k: (answer: any) => IO<A> }
|
|
|
|
/** What the pure interpreters return: the program's result and the ops it performed, in order. */
|
|
export type Trace<A> = { result: A; ops: Op[] }
|
|
|
|
// ---- m07b stack-safe IO
|
|
|
|
export type SafeIO<A> =
|
|
| { tag: 'Pure'; value: A }
|
|
| { tag: 'Effect'; op: Op; k: (answer: any) => SafeIO<A> }
|
|
| { tag: 'FlatMap'; io: SafeIO<any>; f: (result: any) => SafeIO<A> }
|
|
|
|
// ---- m08 streams
|
|
|
|
/** What the world feeds a stream. `Click` arrives in Module 13. */
|
|
export type Input =
|
|
| { tag: 'Time'; t: number }
|
|
| { tag: 'KeyDown'; key: string }
|
|
| { tag: 'KeyUp'; key: string }
|
|
| { tag: 'Click'; x: number; y: number }
|
|
|
|
export type Stream<A> = { step: (input: Input) => [A[], Stream<A>] }
|
|
|
|
/** The whole program: ask the world for what it needs, then a stream of drawings. */
|
|
export type Program = IO<Stream<IO<unknown>>>
|
|
|
|
// ---- m09 runtime
|
|
|
|
export type Platform = {
|
|
fillRect: (x: number, y: number, w: number, h: number, color: string) => void
|
|
fillText: (x: number, y: number, text: string, color: string, size: number, align: Align) => void
|
|
randomSeed: () => number
|
|
loadBest: () => number
|
|
saveBest: (score: number) => void
|
|
startClock: (onTime: (t: number) => void) => void
|
|
onKeyDown: (cb: (key: string) => void) => void
|
|
onKeyUp: (cb: (key: string) => void) => void
|
|
onClick: (cb: (x: number, y: number) => void) => void
|
|
}
|
|
|
|
// ---- m10 high score and replay viewer
|
|
|
|
export type Session = { game: GameState; best: number; improved: boolean }
|
|
/** A recorded game (`fixtures/replay-sample.json`). */
|
|
export type Recording = { seed: number; actions: Action[] }
|
|
|
|
// ---- m12 property-based testing
|
|
|
|
export type CheckResult<A> = { ok: true; runs: number } | { ok: false; run: number; seed: number; value: A }
|