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?