docs(course): fix review findings in modules 06–11

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-29 01:06:09 +01:00
1 parent a7a4cb5b2a
commit 673be0067e
6 files changed
+93 -51

No files matched your search

+36 -10
View File
@@ -135,13 +135,28 @@ final case class PongState(ball: Point, vel: Velocity, player: Int, cpu: Int, he
type Velocity = { Dx: int; Dy: int }
type Held = { Up: bool; Down: bool }
type Score = { Player: int; Cpu: int }
type PongStatus = Playing | Over
[<RequireQualifiedAccess>]
type PongStatus =
| Playing
| Over
type PongState =
{ Ball: Point; Vel: Velocity; Player: int; Cpu: int; Held: Held
Score: Score; Status: PongStatus; Seed: uint32; Frame: int }
{ Ball: Point
Vel: Velocity
Player: int
Cpu: int
Held: Held
Score: Score
Status: PongStatus
Seed: uint32
Frame: int }
// The court, in cells: W = 80, H = 50, PCELL = 8, PADDLE = 8, PLAYER_X = 2, CPU_X = 77, WIN = 5
```
> **F# note:** `[<RequireQualifiedAccess>]` means you must write `PongStatus.Playing`, never plain `Playing`. Snake's `Status` also has a `Playing` case (and, in step 2, Snake's `Action` has `Frame` and `Direction` has `Up`/`Down`). Without the attribute, whichever type was opened last would silently win, and `Playing` in a Pong file could mean Snake's status. Qualified access makes every use unambiguous.
The most interesting field is **`held`**. In Snake, a key press was an *instantaneous* event: pressing `ArrowUp` meant "turn up" once. In Pong, what matters is whether a key **is being held** at the moment a frame happens. The browser doesn't give us "is held"; it gives us two events, keydown and keyup. So "held" is *state* that we derive from those events, and it must live in `PongState` — not in a variable in the runtime, and certainly not by asking the browser during `update`. That's the stateful sketch's `upHeld`/`downHeld`, turned into data.
### Step 2 — Identify the events; make them data
@@ -173,7 +188,12 @@ enum PongAction:
**F#** (given)
```fsharp
type Paddle = PaddleUp | PaddleDown (* check the names in your Model.fs *)
[<RequireQualifiedAccess>]
type Paddle =
| Up
| Down
[<RequireQualifiedAccess>]
type PongAction =
| Press of Paddle
| Release of Paddle
@@ -181,6 +201,8 @@ type PongAction =
| Restart
```
In F# you write these as `Paddle.Up`, `PongAction.Press Paddle.Up`, `PongAction.Frame` — qualified, for the reason given in step 1.
And the translation from browser keys is a pure function, now with a second argument because the same key means different things going down and going up:
```
@@ -223,10 +245,12 @@ Like Snake's `app`:
### Step 7 — One runtime at the edge
Nothing to write. `js/src/pong/main.js` is one line: `runApp(pongMain, browserPlatform(document.getElementById('game'), window))`. In Scala, `scala/web/Main.scala` has a `PongMain` object; `build-web.sh` already compiles it to `web/dist/pong.js`. F# has the equivalent in `fsharp/web/`. Your m09 runtime runs a game it has never heard of. That's the reward for keeping it ignorant.
Nothing to write. `js/src/pong/main.js` is one line: `runApp(pongMain, browserPlatform(document.getElementById('game'), window))`. In Scala, `scala/web/Main.scala` has a `PongMain` object; `build-web.sh` already compiles it to `web/dist/pong.js`. F# has the equivalent in `fsharp/web/PongMain.fs`: `Runtime.runApp (BrowserPlatform.create (BrowserPlatform.gameCanvas ())) (Pong.App.pongMain ())`. Your m09 runtime runs a game it has never heard of. That's the reward for keeping it ignorant.
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones. The Scala and F# suites test the same things one-to-one, but their titles use each track's vocabulary: "events" instead of "inputs", `None` instead of `null`, `()` instead of `undefined`, `bind`/`choose` instead of `flatMap`/`filterMap`, `IO.map throwingF io` instead of `map(io, throwingF)`, and so on. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M11Pong.test.scala` or `fsharp/tests/M11Pong.fs`. Fixture-based tests have the same case names in every track.
### Exercise 11.1 — model
**JavaScript** (`js/src/pong/model.js`; the constants `W`, `H`, `PCELL`, `PADDLE`, `PLAYER_X`, `CPU_X`, `WIN` are given)
@@ -249,7 +273,7 @@ object PongModel:
let initialPong (seed: uint32) : PongState = failwith "TODO m11"
```
**Proved by:** `the court is 80 by 50 cells of 8 pixels, with 8-cell paddles`, `initialPong puts the ball in the middle, both paddles centred, and nobody ahead`, `PongAction builds Press and Release with a key, and has Frame and Restart as plain values`.
**Proved by:** `the court is 80 by 50 cells of 8 pixels, with 8-cell paddles`, `initialPong puts the ball in the middle, both paddles centred, and nobody ahead`, `PongAction builds Press and Release with a key, and has Frame and Restart as plain values` (JS only; in Scala and F# the types are given, so there is nothing to test).
### Exercise 11.2 — rules
@@ -332,15 +356,17 @@ let FRAME_MS_PONG = 30L
let pongKey (isDown: bool) (key: string) : PongAction option = failwith "TODO m11"
let pongActions : Stream<PongAction> = (* ... *)
let pongStates (seed: uint32) (actions: Stream<PongAction>) : Stream<PongState> = failwith "TODO m11"
let pongMain : IO<Stream<IO<unit>>> = (* ... *)
let pongMain () : IO<Stream<IO<unit>>> = failwith "TODO m11"
```
In F#, `main` and `pongMain` are **functions of unit** (`pongMain ()`), not values. F# evaluates a module's top-level values when the module is first used, so a value whose definition fails (like a stub) would break every test that touches the module; a function of unit is only evaluated when called. The value it returns is still the same pure description.
**Proved by:** `fixtures/m11-pong-key.json` (21 cases, including `r-up-does-nothing` and the `constructor`/`toString` traps from Module 9), `the clock runs every 30 ms`, `pongActions turns the clock into Frames and keys into Press, Release and Restart`, `pongStates starts with the new match, then yields one state per action`, `pongMain asks for a seed, draws the new match first, and then the updated one`.
**Run and play:**
- JS: `cd js && node --test "test/m11-*.test.js"`; then `node scripts/serve.mjs` and open <http://localhost:8000/js/pong.html>
- Scala: `cd scala && ./test.sh M11`; then `./build-web.sh`, serve, open <http://localhost:8000/scala/pong.html>
- F#: `cd fsharp && ./test.sh M11`; then `./build-web.sh`, serve, open <http://localhost:8000/fsharp/pong.html>
- JS: `cd js && node --test "test/m11-*.test.js"`; then, **from the repository root**, `node scripts/serve.mjs` and open <http://localhost:8000/js/pong.html>
- Scala: `cd scala && ./test.sh M11`; then `./build-web.sh`; serve from the repository root (`node scripts/serve.mjs`) and open <http://localhost:8000/scala/pong.html>
- F#: `cd fsharp && ./test.sh M11`; then `./build-web.sh`; serve from the repository root (`node scripts/serve.mjs`) and open <http://localhost:8000/fsharp/pong.html>
Finally run the **whole** suite of your track (`./test.sh`). The purity audit now scans your Pong files too.