fix: final review findings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-29 01:43:48 +01:00
co-authored by Claude Opus 5.5
parent 82f47fb3ee
commit b46a3611af
7 changed files with 23 additions and 11 deletions
+4
View File
@@ -5,6 +5,10 @@ obj/
.scala-build/ .scala-build/
.bsp/ .bsp/
*/web/dist/ */web/dist/
.bloop/
.metals/
.idea/
.vscode/
# build outputs # build outputs
.fable/ .fable/
+3 -3
View File
@@ -44,9 +44,9 @@ To play the game in any track you also need a modern browser. [Module 0](course/
Run the tests (from the repository root): Run the tests (from the repository root):
```sh ```sh
cd js && node --test "test/m03-*.test.js" # JavaScript, one module (all: ./test.sh) (cd js && node --test "test/m03-*.test.js") # JavaScript, one module (all: ./test.sh)
cd scala && ./test.sh M03 # Scala, one module (all: ./test.sh) (cd scala && ./test.sh M03) # Scala, one module (all: ./test.sh)
cd fsharp && ./test.sh M03 # F#, one module (all: ./test.sh) (cd fsharp && ./test.sh M03) # F#, one module (all: ./test.sh)
``` ```
In a fresh checkout, Module 0's tests pass and every other module's tests fail: the files in `src/` are stubs for you to fill in. In a fresh checkout, Module 0's tests pass and every other module's tests fail: the files in `src/` are stubs for you to fill in.
+2
View File
@@ -189,6 +189,8 @@ cd fsharp
./test.sh # everything ./test.sh # everything
``` ```
If `scala-cli` prints a hint that the munit dependency is outdated, ignore it: the course pins munit on purpose, for reproducibility.
Try it now with Module 0, which has a few sanity tests that already pass. They check that your toolchain works and that the fixtures folder can be read. If you see three green tests (Scala and F#: a short summary with no failures), you are set up. Try it now with Module 0, which has a few sanity tests that already pass. They check that your toolchain works and that the fixtures folder can be read. If you see three green tests (Scala and F#: a short summary with no failures), you are set up.
### What a red test means ### What a red test means
+1 -1
View File
@@ -195,7 +195,7 @@ Our `view : GameState → Scene` is exactly this, for a canvas instead of HTML.
## Exercises ## 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/M06View.test.scala` or `fsharp/tests/M06View.fs`. Fixture-based tests have the same case names in every track. > **About test names.** The test titles quoted under "Proved by" are the JavaScript ones, which carry an `m06` prefix (e.g. `m06 CELL is 20 pixels`). The Scala and F# suites test the same things one-to-one, without that prefix: `CELL is 20 pixels` is identical, and the fixture-driven cases appear as `view: <case name>` with the same case names in every track. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M06View.test.scala` or `fsharp/tests/M06View.fs`.
### Exercise 6.1 — `view` ### Exercise 6.1 — `view`
+5 -5
View File
@@ -229,11 +229,11 @@ Remember section 12 of Module 7: the loop only helps if each continuation does a
That's the whole runtime. Go back to the stateful code at the start of Module 8, with its `let state` and logic in callbacks, and compare. There, the mutable variable held the *game state*, and game logic lived in the callbacks. Here, the variable holds the *current stream* — the runtime doesn't even know there's a game. It has no idea what a snake, a tick or a score is. All the game logic is in pure values; the runtime is a few lines of plumbing. That is what "pushing effects to the edge" means in practice. That's the whole runtime. Go back to the stateful code at the start of Module 8, with its `let state` and logic in callbacks, and compare. There, the mutable variable held the *game state*, and game logic lived in the callbacks. Here, the variable holds the *current stream* — the runtime doesn't even know there's a game. It has no idea what a snake, a tick or a score is. All the game logic is in pure values; the runtime is a few lines of plumbing. That is what "pushing effects to the edge" means in practice.
``` ```
┌──────────────────────── runtime (impure, ~20 lines) ─────────────────────────┐ ┌────────────────────────────── runtime (impure, ~20 lines) ─────────────────────────────────┐
browser ──key──▶│ feed(KeyDown key): [ios, next] = stream.step(ev); stream = next; ios.forEach(runIO) │──▶ canvas browser ──key──▶│ feed(KeyDown key): [ios, next] = stream.step(ev); stream = next; ios.forEach(runIO) │
──time─▶│ feed(Time t) │ ──time─▶│ feed(Time t) │
└───────────────────────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────────────────────────────────────┘
▲ the one `let` / `var` / ref cell: stream ▲ the one `let` / `var` / ref cell: stream
``` ```
The tests describe the behaviour precisely; read them before writing the code: The tests describe the behaviour precisely; read them before writing the code:
+7 -1
View File
@@ -29,7 +29,13 @@ let private violations (code: string) =
let private fsFiles () = let private fsFiles () =
if Directory.Exists srcDir then if Directory.Exists srcDir then
Directory.GetFiles(srcDir, "*.fs", SearchOption.AllDirectories) |> Array.sort |> List.ofArray Directory.GetFiles(srcDir, "*.fs", SearchOption.AllDirectories)
|> Array.filter (fun f ->
// skip generated output: any path component named bin or obj
let rel = Path.GetRelativePath(srcDir, f)
not (rel.Split([| '/'; '\\' |]) |> Array.exists (fun d -> d = "bin" || d = "obj")))
|> Array.sort
|> List.ofArray
else else
[] []
+1 -1
View File
@@ -27,4 +27,4 @@ createServer(async (req, res) => {
res.writeHead(e.code === 'EACCES' ? 403 : 404, { 'content-type': 'text/plain' }) res.writeHead(e.code === 'EACCES' ? 403 : 404, { 'content-type': 'text/plain' })
res.end(e.code === 'EACCES' ? 'forbidden' : 'not found') res.end(e.code === 'EACCES' ? 'forbidden' : 'not found')
} }
}).listen(port, () => console.log(`Serving ${root} on http://localhost:${port}`)) }).listen(port, '127.0.0.1', () => console.log(`Serving ${root} on http://localhost:${port}`))