From 82f47fb3eeb69c9eba0555b61f6a95831545dcaf Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 29 Oct 2026 01:17:35 +0100 Subject: [PATCH] docs(course): consistency pass - m04: F# mulberry32 hints and skeleton route * and + through uint64 helpers; new 'F# in the browser' note on the Fable uint32 trap - m01-m05: 'About test names' notes like m06-m11; m00 explains JS vs Scala/F# titles - m00/m09: first build-web.sh run restores Fable; m09 JS file list (main.js is given) Co-Authored-By: Claude Opus 5.5 --- course/00-setup.md | 6 +++--- course/01-purity-and-immutability.md | 2 ++ course/02-the-game-as-data.md | 2 ++ course/03-pure-game-rules.md | 2 ++ course/04-randomness-without-side-effects.md | 20 ++++++++++++++------ course/05-folding-time.md | 2 ++ course/09-wiring-it-together.md | 2 +- 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/course/00-setup.md b/course/00-setup.md index 367250a..fbaed0f 100644 --- a/course/00-setup.md +++ b/course/00-setup.md @@ -160,7 +160,7 @@ Each module tells you, in its header, which files you will edit. The map is the | 06 | `view.js` | `View.scala` | `View.fs` | | 07 | `io.js` | `IO.scala` | `IO.fs` | | 08 | `stream.js` | `Stream.scala` | `Stream.fs` | -| 09 | `input.js`, `app.js`, `runtime.js` (+ `browser.js`, `main.js`) | `Input.scala`, `App.scala`, `Runtime.scala` | `Input.fs`, `App.fs`, `Runtime.fs` | +| 09 | `input.js`, `app.js`, `runtime.js`, `browser.js` (`main.js` is given) | `Input.scala`, `App.scala`, `Runtime.scala` | `Input.fs`, `App.fs`, `Runtime.fs` | | 10 | edits `game`, `input`, `app` | same | same | | 11 | `pong/` | `pong/` | `Pong/` | @@ -212,7 +212,7 @@ This is expected, and it is the starting point of every module. In Scala you see A red test is information, not a verdict. Read it in this order: -1. **The test name.** Test names are sentences written for you: `dropLast leaves its input untouched`. Fixture tests are named after the function and the fixture case: `m03 advance: advance-into-vacating-tail`. +1. **The test name.** Test names are sentences written for you: `dropLast leaves its input untouched`. Fixture tests are named after the function and the fixture case: `m03 advance: advance-into-vacating-tail`. (That is the JavaScript title; JS puts the module in front. Scala and F# print `advance: advance-into-vacating-tail`, and their other titles sometimes use their own track's vocabulary, for example `bind` instead of `flatMap`. The modules quote the JavaScript titles; each module's "About test names" note says how the other tracks differ.) 2. **The message.** `TODO: implement in m03` means you have not written that function yet. An assertion failure shows *expected* versus *actual*: compare them field by field. With game states, the difference is often in one field (`dir`, `seed`, `status`). 3. **The fixture.** For a fixture test, open `fixtures/-.json` and search for the case name. You will see the exact input. Draw it on paper; the boards in fixtures are deliberately tiny (3 x 2, 5 x 5) so that you can. @@ -280,7 +280,7 @@ You can play the game once Module 9 is done. For reference, this is how: - **JS:** `node scripts/serve.mjs` from the repository root, then open http://localhost:8000/js/ - **Scala:** `cd scala && ./build-web.sh` (the first run downloads the Scala.js linker), then serve as above and open http://localhost:8000/scala/ -- **F#:** `cd fsharp && ./build-web.sh`, serve, and open http://localhost:8000/fsharp/ +- **F#:** `cd fsharp && ./build-web.sh` (the first run restores the Fable compiler), serve, and open http://localhost:8000/fsharp/ ## Exercises diff --git a/course/01-purity-and-immutability.md b/course/01-purity-and-immutability.md index 179b3e6..1e6199f 100644 --- a/course/01-purity-and-immutability.md +++ b/course/01-purity-and-immutability.md @@ -271,6 +271,8 @@ This is why the Scala and F# tracks have a smaller Module 1: `pipe`, `compose` a All functions here must be pure: no mutation (inputs are deep-frozen in the JS tests, so mutation throws), no reassignment. +> **About test names.** The test titles quoted under "Tests" are the JavaScript ones; the JS runner prints them with the module in front (`m01 range(0) is empty`). The Scala suite uses the same titles. The F# titles use F# call syntax (`range 0 is empty`). Scala and F# have no tests for `pipe`, `compose` and `deepFreeze`, which are JS-only exercises. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M01Util.test.scala` or `fsharp/tests/M01Util.fs`. (Module 1 has no fixture-based tests.) + ### Exercise 1.1 — `range` `range(n)` returns the numbers `0, 1, ..., n-1` in order; `range(0)` is empty. You will use it in Module 4 to list every cell of the board. diff --git a/course/02-the-game-as-data.md b/course/02-the-game-as-data.md index 761bd33..662ebfd 100644 --- a/course/02-the-game-as-data.md +++ b/course/02-the-game-as-data.md @@ -249,6 +249,8 @@ Cases with no data are constants; a case with data (`Turn`) is a function that b ## Exercises +> **About test names.** The test titles quoted under "Test"/"Tests" are the JavaScript ones; the JS runner prints them with the module in front (`m02 opposite is its own inverse`). The Scala and F# suites test the same things, but where JS checks a function you write (`point`, `Action`) they check the declarations you were given instead: `Point holds its coordinates`, `Action.Turn carries the direction` (F#: `Action Turn carries the direction`), `the other Actions are distinct constants`. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M02Model.test.scala` or `fsharp/tests/M02Model.fs`. Fixture-based tests have the same case names in every track (JS `m02 delta: up`, Scala/F# `delta: up`; the `pointEquals` cases are labelled `pointEquals (case-class ==)` in Scala and `pointEquals (record =)` in F#). + ### Exercise 2.0 — Your own design (paper) Complete the design questions at the top of "Concepts" before reading the canonical model. Afterwards, write three sentences: one thing your design did better or differently, one thing the canonical model does that you had not thought of, and one trade-off you would make differently in a bigger game. Keep the sheet; Module 11 asks you to do this again for Pong. diff --git a/course/03-pure-game-rules.md b/course/03-pure-game-rules.md index 5f640fa..ed760e5 100644 --- a/course/03-pure-game-rules.md +++ b/course/03-pure-game-rules.md @@ -205,6 +205,8 @@ In JS, import them at the top of `rules.js`, for example `import { delta, opposi ## Exercises +> **About test names.** This module's tests are all fixture-based: one test per fixture case, named after the function and the case. The JS runner prints them with the module in front (`m03 advance: advance-into-vacating-tail`); Scala and F# print `advance: advance-into-vacating-tail`. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M03Rules.test.scala` or `fsharp/tests/M03Rules.fs`. + ### Exercise 3.1 — `move` **JavaScript** diff --git a/course/04-randomness-without-side-effects.md b/course/04-randomness-without-side-effects.md index c1891da..d948778 100644 --- a/course/04-randomness-without-side-effects.md +++ b/course/04-randomness-without-side-effects.md @@ -82,13 +82,15 @@ The three tracks must produce **bit-identical** numbers, otherwise the shared fi | | JavaScript | Scala | F# | |---|---|---|---| | Seed type | `number` (a 64-bit float) | `Long` (holds 0 until 2^32) | `uint32` | -| 32-bit multiply | `Math.imul(a, b)` | `Int` `*` wraps around | `uint32` `*` wraps around | +| 32-bit multiply | `Math.imul(a, b)` | `Int` `*` wraps around | `uint32` `*` wraps on .NET, but not in the browser (see below) | | Unsigned shift | `>>>` | `>>>` on `Int` | `>>>` on `uint32` (unsigned type, so logical) | | XOR / OR | `^` / `\|` | `^` / `\|` | `^^^` / `\|\|\|` | | Back to unsigned | `x >>> 0` | `x.toLong & 0xFFFFFFFFL` | already unsigned | The usual bugs: in JS, forgetting that `+` and `*` on numbers do not wrap at 2^32 (so you get huge or fractional values), or that `^` and `|` produce *signed* 32-bit results (so you get negative numbers). In Scala, doing the arithmetic in `Long` (which does not wrap at 32 bits) or forgetting to convert the final `Int` back to unsigned. The fixture `m04-random.json` checks ten consecutive draws from seeds `0`, `1` and `4294967295` (the largest seed), which catches every one of these mistakes. +> **F# in the browser.** The F# tests run on .NET, where `uint32` `+` and `*` wrap around at 2^32 exactly as the table says. The browser page, however, is compiled to JavaScript by Fable (Module 9), and Fable turns `uint32` `+` and `*` into plain JavaScript `+` and `*`. Those do not wrap, and a product of two large 32-bit numbers is not even exact in a JavaScript `number`. So a `nextUint32` written with plain `*` and `+` passes every test on .NET and then places the apples differently (or on cells that do not exist) in the browser. The tests cannot see this, because they never run the Fable output. The fix is to do each `*` and `+` through two tiny helpers that go up to `uint64` and back: `let mul (a: uint32) (b: uint32) = uint32 (uint64 a * uint64 b)`, and `add` the same way. The product of two `uint32` values always fits in a `uint64`, and converting back to `uint32` keeps the low 32 bits, so the helpers are exact on .NET and in the browser. (`^^^`, `|||` and `>>>` behave the same on both and need no helper.) The doc comment in `fsharp/src/Random.fs` says the same. + ### Threading seeds by hand Suppose you have `nextUint32 : seed → [value, seed]` and you want a die roll, then the sum of two dice. Written directly (JavaScript, as an illustration): @@ -310,6 +312,8 @@ And now the payoff: `newGame(42)` is the same game every time you call it. Any s ## Exercises +> **About test names.** The test titles quoted under "Test"/"Tests" are the JavaScript ones; the JS runner prints them with the module in front (`m04 intBelow always lands in [0, n)`). The Scala and F# suites test the same things one-to-one with the same titles, except that F# says `bind` where JS and Scala say `flatMap` (`bind threads the seed through both steps`). Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M04Random.test.scala` and `M04Spawn.test.scala`, or `fsharp/tests/M04Random.fs` and `M04Spawn.fs`. Fixture-based tests have the same case names in every track (JS `m04 nextUint32: seed-0-step-1`, Scala/F# `nextUint32: seed-0-step-1`). + ### Exercise 4.1 — `nextUint32` Implement mulberry32 as described above (the formula is also in the doc comment of your stub). @@ -329,7 +333,7 @@ let nextUint32 : Random // Random.run 0u Random.nextUint3 Fixture: `fixtures/m04-random.json`, cases `seed-0-step-1` ... `seed-4294967295-step-10`. Each step starts from the previous step's new seed. -Watch out: see the 32-bit table above. Both the value **and** the new seed must be unsigned and below 2^32. In Scala, do the arithmetic in `Int` (where `*` wraps) and convert at the end. +Watch out: see the 32-bit table above. Both the value **and** the new seed must be unsigned and below 2^32. In Scala, do the arithmetic in `Int` (where `*` wraps) and convert at the end. In F#, route every `*` and `+` through the `uint64` helpers (see "F# in the browser"), or the game will pass the tests and still misbehave in the browser. ### Exercise 4.2 — `pure`, `map`, `flatMap`, `run` @@ -457,7 +461,7 @@ Translate the pseudo-code line by line, then fix the integer behaviour. Start wi - **JS:** `(seed + 0x6D2B79F5) >>> 0` wraps the sum into an unsigned 32-bit number. `Math.imul(a, b)` multiplies like a 32-bit machine. `^`, `|`, `>>>` work on 32-bit integers; `^` and `|` give *signed* results, so finish the value with `>>> 0`. Use `const` for each intermediate (you cannot reassign `t`; call them `t1`, `t2`). - **Scala:** `val s = seed.toInt + 0x6D2B79F5` (Int addition wraps). Then the `Int` formula with `*`, `^`, `|`, `>>>`. Return `(value.toLong & 0xFFFFFFFFL, s.toLong & 0xFFFFFFFFL)`. Remember to wrap the function: `Random(seed => ...)`. -- **F#:** `let s = seed + 0x6D2B79F5u` (uint32 addition wraps). Then `^^^`, `|||`, `>>>`, `*` on `uint32`; literals need the `u` suffix (`1u`, `61u`). No conversion needed at the end. Wrap the function: `Random(fun seed -> ...)`. +- **F#:** first write the two helpers `mul` and `add` from "F# in the browser" above, and use them for every `*` and `+` (on .NET plain `uint32` arithmetic would wrap too, but the browser build would break). Then `let s = add seed 0x6D2B79F5u`, and `^^^`, `|||`, `>>>` on `uint32`; literals need the `u` suffix (`1u`, `61u`). No conversion needed at the end. Wrap the function: `Random(fun seed -> ...)`. If seed 0 passes but seed 4294967295 fails, your addition is not wrapping. @@ -483,11 +487,15 @@ lazy val nextUint32: Random[Long] = Random { seed => } ``` ```fsharp +// wrap-around * and + that also work in the browser (Fable): via uint64 and back +let private mul (a: uint32) (b: uint32) : uint32 = uint32 (uint64 a * uint64 b) +let private add (a: uint32) (b: uint32) : uint32 = ___ + let nextUint32 : Random = Random(fun seed -> - let s = seed + 0x6D2B79F5u - let t1 = (s ^^^ (s >>> 15)) * ___ - let t2 = t1 ^^^ (t1 + ___ * ___) + let s = add seed 0x6D2B79F5u + let t1 = mul (s ^^^ (s >>> 15)) ___ + let t2 = t1 ^^^ add t1 (mul ___ ___) (t2 ^^^ (t2 >>> 14), ___)) ``` diff --git a/course/05-folding-time.md b/course/05-folding-time.md index 35e4c40..ebef97f 100644 --- a/course/05-folding-time.md +++ b/course/05-folding-time.md @@ -166,6 +166,8 @@ The game loop from the top of this module becomes: **the states of the game are ## Exercises +> **About test names.** The test titles quoted under "Tests" are the JavaScript ones; the JS runner prints them with the module in front (`m05 history has one more state than there are actions`). The Scala and F# suites use the same titles. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M05Game.test.scala` or `fsharp/tests/M05Game.fs`. Fixture-based tests have the same case names in every track (JS `m05 replay: twelve-actions-eat-one-apple`, Scala/F# `replay: twelve-actions-eat-one-apple`). + ### Exercise 5.1 — `update` `Tick` runs a game step (`tick` from `spawn`), `Turn(dir)` asks to turn (`turn` from `rules`), every other action returns the state unchanged. diff --git a/course/09-wiring-it-together.md b/course/09-wiring-it-together.md index ca4d23a..407a4b5 100644 --- a/course/09-wiring-it-together.md +++ b/course/09-wiring-it-together.md @@ -272,7 +272,7 @@ The first run downloads the Scala.js linker and takes a while. It writes `scala/ cd fsharp && ./build-web.sh ``` -then serve the repository root (`node scripts/serve.mjs`, run from the repository root) and open . +The first run restores Fable (a local .NET tool listed in `fsharp/.config/dotnet-tools.json`) and takes a while. It writes `fsharp/web/dist/`. Then serve the repository root (`node scripts/serve.mjs`, run from the repository root) and open . Use the arrow keys or WASD. **You built a game in which every line of logic is pure.** Play a round. Then, before moving on, do the trace exercise in "Check your understanding": follow one key press from the keyboard to the pixels, naming each function it passes through.