From ab8abaaa7734355797dfb7368c9bd4e600e0d9b7 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 29 Oct 2026 01:04:03 +0100 Subject: [PATCH] =?UTF-8?q?docs(course):=20fix=20review=20findings=20in=20?= =?UTF-8?q?modules=2000=E2=80=9305?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- course/00-setup.md | 6 ++-- course/02-the-game-as-data.md | 22 +++++++++++--- course/03-pure-game-rules.md | 14 +++++++++ course/04-randomness-without-side-effects.md | 31 ++++++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/course/00-setup.md b/course/00-setup.md index 17bb215..367250a 100644 --- a/course/00-setup.md +++ b/course/00-setup.md @@ -220,7 +220,7 @@ A green test does not prove your code is right, and a red test never means you a ### The purity rules -> **The purity audit.** A test in every track (`purity.test.js`, `PurityTest`, `Purity`) scans your `src/` files (comments are ignored) and fails if it finds any of these: +> **The purity audit.** A test in every track (JS `test/purity.test.js`, Scala `PuritySuite` in `test/Purity.test.scala`, F# `tests/Purity.fs`) scans your `src/` files (comments are ignored) and fails if it finds any of these: > > | Track | Banned outside the runtime files | > |---|---| @@ -246,7 +246,7 @@ Each module builds on the previous ones. If you get stuck, work through this lis 1. Re-read the doc comment in the stub and the "Concepts" section of the module. 2. Open the failing fixture case and work it out on paper. -3. Open the module's **Hints**. Every exercise has three tiers: a *nudge*, a *stronger hint*, and a *near-solution* skeleton with holes. Open them one at a time. +3. Open the module's **Hints**. From Module 1 on, every coding exercise has three tiers: a *nudge*, a *stronger hint*, and a *near-solution* skeleton with holes. Open them one at a time. (Module 0's setup exercises have only the hints they need.) 4. Ask your mentor (human or AI), explaining what you tried. 5. Only then: **take the checkpoint.** Copy that module's files from `solutions/` into `src/` (for example `cp js/solutions/rules.js js/src/rules.js`) and continue with the next module. Because tests are organised per module, all earlier tests stay green. @@ -269,7 +269,7 @@ Every module has the same parts, in this order: 2. **Learning objectives**: what you should be able to do afterwards. 3. **Concepts**: the ideas, with Scala bridges (blockquotes starting with **From Scala:**) and code in all three languages. 4. **Exercises**: the functions to write, with signatures in three languages, the tests and fixtures that check them, and pitfalls. -5. **Hints**: folded away; open them one tier at a time. +5. **Hints**: folded away; from Module 1 on, every coding exercise has a nudge, a stronger hint and a near-solution. Open them one tier at a time. 6. **Check your understanding**: questions you should be able to answer before moving on. Several ask you to predict an output or to say what would break if something changed. 7. **In production**: how real libraries and frameworks solve the same problem. 8. **Further reading**: links to the sources of the ideas. diff --git a/course/02-the-game-as-data.md b/course/02-the-game-as-data.md index 04ca366..761bd33 100644 --- a/course/02-the-game-as-data.md +++ b/course/02-the-game-as-data.md @@ -26,7 +26,7 @@ After this module you can: ### Design it yourself first -Take a sheet of paper and fifteen minutes. **Do not read past the "Stop here" line until you have written something down.** It does not matter if your answer differs from the course's; what matters is that you have thought about the questions, so that the canonical model answers questions you have actually asked. +Take a sheet of paper and fifteen minutes. **Do not read past the "Stop here" line until you have written something down, and do not open this module's stub file yet** (`js/src/model.js`, `scala/src/Model.scala`, `fsharp/src/Model.fs`): its type declarations already contain the course's answer. You will open it for the exercises, after the reveal. It does not matter if your answer differs from the course's; what matters is that you have thought about the questions, so that the canonical model answers questions you have actually asked. Your task: write down the type of "the state of a game of Snake", in the language of your track or in pseudo-code. Use these questions to guide you: @@ -47,9 +47,9 @@ Your task: write down the type of "the state of a game of Snake", in the languag ### The canonical model -Here is the model the course uses. All three tracks use the same fields with the same meaning, which is what lets one JSON fixture test all three. +Here is the model the course uses. (Now you may open your stub file: in Scala and F# these declarations are already written there for you, and in JS they are the JSDoc comments at the top of `model.js`.) All three tracks use the same fields with the same meaning, which is what lets one JSON fixture test all three. -**JavaScript** (JS has no types, so the shape is documented with JSDoc in `model.js`) +**JavaScript** (JS has no types, so the shape is documented with JSDoc comments) ```js /** @typedef {{x: number, y: number}} Point */ /** @typedef {'Up'|'Down'|'Left'|'Right'} Direction */ @@ -59,7 +59,7 @@ Here is the model the course uses. All three tracks use the same fields with the // Action: {tag:'Tick'} | {tag:'Turn', dir} | {tag:'TogglePause'} | {tag:'Restart'} | {tag:'Frame'} ``` -**Scala** (already in `Model.scala`) +**Scala** ```scala final case class Point(x: Int, y: Int) enum Direction: @@ -403,6 +403,20 @@ Replace `get Tick() { return todo('m02') }` by `Tick: { tag: 'Tick' }`, and so o +
Exercise 2.4 (Action) — near-solution (try the others first) + +```js +export const Action = { + Tick: { tag: ___ }, + Turn: (dir) => ({ tag: ___, ___ }), + TogglePause: ___, + Restart: ___, + Frame: ___, +} +``` + +
+
Exercise 2.5 (initialState) — nudge This is a single record literal. Read the fixture `m02-initial-state.json`: the `expected` value is exactly the record you must build (with the seed from `args`). diff --git a/course/03-pure-game-rules.md b/course/03-pure-game-rules.md index 4fdf36c..5f640fa 100644 --- a/course/03-pure-game-rules.md +++ b/course/03-pure-game-rules.md @@ -335,6 +335,20 @@ Valid `x` values on a board of width 5 are 0, 1, 2, 3, 4. So `x >= 0` and `x < w
+
Exercise 3.2 (inBounds) — near-solution (try the others first) + +```js +export const inBounds = (state, p) => p.x >= 0 && p.x < ___ && ___ && ___ +``` +```scala +def inBounds(state: GameState, p: Point): Boolean = p.x >= 0 && p.x < ___ && ___ && ___ +``` +```fsharp +let inBounds (state: GameState) (p: Point) : bool = p.X >= 0 && p.X < ___ && ___ && ___ +``` + +
+
Exercise 3.3 (turn) — nudge Write the guard first: in which two situations does the function return `state` as it is? Then the one-field copy. diff --git a/course/04-randomness-without-side-effects.md b/course/04-randomness-without-side-effects.md index 2ae25ac..c1891da 100644 --- a/course/04-randomness-without-side-effects.md +++ b/course/04-randomness-without-side-effects.md @@ -541,6 +541,20 @@ It is "the value of `nextUint32`, transformed". Which of your new functions tran
+
Exercise 4.3 (intBelow) — near-solution (try the others first) + +```js +export const intBelow = (n) => map(___, (v) => ___) +``` +```scala +def intBelow(n: Int): Random[Int] = nextUint32.map(v => ___) +``` +```fsharp +let intBelow (n: int) : Random = nextUint32 |> map (fun v -> ___) +``` + +
+
Exercise 4.4 (freeCells) — nudge First build *all* cells in row-major order, then keep those not in the snake. @@ -615,6 +629,23 @@ Each is one line: a function applied to the result of another.
+
Exercise 4.6 (tick, newGame) — near-solution (try the others first) + +```js +export const tick = (state) => spawnApple(___(state)) +export const newGame = (seed) => ___(initialState(___)) +``` +```scala +def tick(state: GameState): GameState = spawnApple(___) +def newGame(seed: Long): GameState = ___(Model.initialState(___)) +``` +```fsharp +let tick (state: GameState) : GameState = state |> Rules.advance |> ___ +let newGame (seed: uint32) : GameState = seed |> ___ |> ___ +``` + +
+ ## Check your understanding 1. Why is `const roll = () => Math.floor(Math.random() * 6)` impure, while `d6 = map(nextUint32, v => 1 + v % 6)` is pure? Where did the "randomness" go?