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:
Lucas WintherandClaude Fable 5.2 committed 2026-11-11 12:05:29 +01:00
1 parent 876f5aabb1
commit edd57f0148
253 files changed
+21753 -503

No files matched your search

+7 -6
View File
@@ -1,6 +1,6 @@
# The method: from a stateful game to a functional one
> **When to read this:** after Module 9 (you need `IO`, streams and the runtime), and before Module 11, which asks you to apply it to Pong on your own. Come back to it whenever you convert something.
> **When to read this:** after Module 9 (you need `IO`, streams and the runtime), and before Module 11, which asks you to apply it to Pong on your own (Module 13 applies it once more, to Conway's Life). Come back to it whenever you convert something.
> **What it is:** the course, compressed into a recipe you can carry to other programs — with a small worked example, a checklist and a list of the mistakes people make most often.
## The recipe in one page
@@ -338,12 +338,16 @@ Use this when you convert anything. Each line is a yes/no question about your re
**Mutation in loops.** `for (…) result.push(…)`, `board[y][x] = …`, `list.sort()` (which sorts in place). It's tempting when the rule is naturally "go through all the bricks and remove the hit ones". *Fix:* `map`/`filter`/`reduce`, building new arrays; `[...xs].sort()` or a non-mutating `toSorted()` if you must sort; for grids, a list of rows rebuilt with `map`.
**Effects in update.** A `console.log` for debugging that stays, a `localStorage.setItem` "just when the game ends", a sound played on eating. Each one makes `update` untestable and order-dependent. *Fix:* return data. For a sound or a save, add an `Op` and produce the `IO` from the state stream (see Module 10's high-score thought exercise). For debugging, use `history` and `trace` instead of logging.
**Effects in update.** A `console.log` for debugging that stays, a `localStorage.setItem` "just when the game ends", a sound played on eating. Each one makes `update` untestable and order-dependent. *Fix:* return data. For a sound or a save, add an `Op` and produce the `IO` from the state stream (Module 10 builds one: the persisted high score of section 8, Exercise 10.7). For debugging, use `history` and `trace` instead of logging.
**State in the runtime.** "I'll just keep `upHeld` in the runtime and pass it in." Then the runtime knows about the game and half the behaviour can't be replayed. *Fix:* the runtime holds only the current stream; held keys are state derived from `Press`/`Release` actions (Module 11).
**Changing the clock instead of counting.** `clearInterval`/`setInterval` to speed up. *Fix:* one fixed clock; decide in `update` which frames matter (Module 10).
**Counting the wrong thing.** "Step when the total frame count is a multiple of the interval" works until the interval changes; then one step is cut short (Module 10, Puzzle 3). *Fix:* store what the rule talks about, here the frames since the last step, and state the requirement as a property you can test (Module 12).
**Trusting the clock to be regular.** A hidden browser tab, a laptop that sleeps or a debugger breakpoint makes the next `Time` event arrive seconds late, and a stream that faithfully catches up plays those seconds in one go. *Fix:* decide how much time the program accepts at once and clamp in pure code (`clampTime`, Module 10).
**Forgetting to wrap the next stream.** A combinator that returns the inner `next` instead of `combinator(next, …)` works for exactly one event. *Fix:* every combinator's next stream is the same combinator around the inner next stream(s) (Module 8).
**Left-nested `flatMap` chains.** Building a long sequence with a left fold overflows the stack in the interpreter's continuations. *Fix:* fold right (Module 7, section 12).
@@ -362,10 +366,7 @@ Each of these is a good exercise in the method. Start from the inventory (step 0
- *Events:* `Press`/`Release` (left/right), `Frame`, `Launch`, `Restart`.
- *Hints:* reuse Pong's held-key pattern and its step-by-step frame pipeline. Represent bricks as a list (or a set) of positions; a collision step returns the remaining bricks and the bounced velocity together. Decide, and write a fixture for, what happens when the ball hits two bricks in one frame.
**Conway's Game of Life** ([Wikipedia](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life)).
- *State:* the set of live cells (a list of points, or a set of `"x,y"` strings in JS), generation, paused flag, speed.
- *Events:* `Frame`, `TogglePause`, `Step` (one generation while paused), `Clear`, `Randomize` (needs a seed), and — for editing — clicking a cell.
- *Hints:* `step(cells)` is the purest function you'll ever write: count neighbours with `map`/`filter`, keep cells with 2–3 neighbours, birth cells with exactly 3. Clicking needs a new input type and a new `Platform` method (`onClick(cb: (x, y) => void)`): that's a change to the stream inputs, the runtime and the browser platform, but *not* to the game logic. Doing it will teach you exactly where the edge is.
**Conway's Game of Life** is now built: it is the second capstone, [Module 13](13-capstone-life.md). It adds the one thing the other games did not need, a new kind of input (mouse clicks), and with it a change to the stream inputs, the runtime and the browser platform, but not to any game. If you have done Module 13, try this extension of it: let the player drag to draw several cells in one gesture (`MouseDown`, `MouseMove`, `MouseUp` inputs, and a "drawing" flag in the state, like Pong's held keys).
## Further reading