# Module 10 — Extensions, all pure > **Time:** ~2–3 h · **You'll edit:** JS `js/src/game.js`, `js/src/input.js`, `js/src/app.js` · Scala `scala/src/Game.scala`, `scala/src/Input.scala`, `scala/src/App.scala` · F# `fsharp/src/Game.fs`, `fsharp/src/Input.fs`, `fsharp/src/App.fs` > **Tests:** JS `cd js && node --test "test/m10-*.test.js"` · Scala `cd scala && ./test.sh M10` · F# `cd fsharp && ./test.sh M10` — and afterwards **all** tests: JS `./test.sh` · Scala `./test.sh` · F# `./test.sh` > **Prerequisites:** Modules 0–9 (a playable game) ## Why this module You have a playable Snake. Now come the features every player asks for: **pause**, **restart**, and a game that **speeds up** as you score. This module is about a question you should ask of any architecture: *when I add a feature, how much do I have to touch, and how much can break?* Think about how these features usually get added to a stateful game: ```js // The usual, stateful way (do NOT write this in the course) let paused = false let timer = setInterval(step, 150) window.addEventListener('keydown', (e) => { if (e.key === ' ') paused = !paused if (e.key === 'r') { snake = [...]; apple = ...; score = 0; paused = false } // did we reset everything? }) function step() { if (paused) return // ... move, eat ... if (ateApple && score % 3 === 0) { clearInterval(timer) timer = setInterval(step, Math.max(50, 150 - score * 5)) // change the clock itself } } ``` Every feature adds another mutable variable, another `if` in a callback, and another way to forget something (does restart reset the timer speed? does pressing an arrow while paused queue a turn?). Speeding up means *reaching out and changing the clock*, which is an effect hidden in the middle of the game logic. In our design, the same three features are **new cases in the `Action` data type, new cases in `update`, two more keys in `keyToAction`, and one different input stream**. The runtime doesn't change at all. The m09 tests keep passing. And every new behaviour is pinned down by fixtures that run without a browser. ## Learning objectives By the end of this module you can: - **implement** `TogglePause`, `Restart` and `Frame` as pure cases of `update`; - **explain** why speed-up is done with a frame counter and `movesEvery` instead of changing the clock, and **compute** the move interval for a given score; - **predict**, using the fixtures, exactly which `Frame` actions move the snake; - **explain** why features added this way cannot break earlier modules' tests, and which tests *would* catch a mistake; - **design** (on paper) a persisted high score as a new effect, without writing impure code in the game logic. ## Concepts ### 1. Pause is a status, not a flag in a callback `GameState` has had a `status` field since Module 2, with the value `Paused` waiting for this moment. And since Module 3, `turn` and `advance` do nothing unless the status is `Playing`. So most of "pause" is already done: a paused game *ignores* ticks and turns, by construction. (This is Review Focus 4 of the course: keys pressed while paused or after game over must do nothing. The fixtures `turn-while-paused-is-ignored` and `tick-while-paused-is-ignored` check it.) What's left is the action that switches it: - `TogglePause`: `Playing` → `Paused`, `Paused` → `Playing`, and **any other status is unchanged** (you can't pause a game that is over, nor "unpause" it into `Playing`). Compare the two worlds. In the stateful version, "paused" is a separate boolean that every callback must remember to check. In ours, it's one value of a field that the rules already respect — there is no way to forget it, because the rules never run in the wrong status. ### 2. Restart is "a new game", which is a value you already have - `Restart`: `newGame(state.seed)`, **from any status**. That's it, and it's the shortest bug-free restart you'll ever write. The stateful version had to *reset* every variable it had created, and forgetting one is a classic bug. Here there is nothing to reset, because there is nothing that persists except the value you return: the new game is a complete state built from scratch by the same function that built the first one. Notice which seed is used: `state.seed`, the *current* seed, which has advanced every time an apple was placed. So a restart is deterministic (the same state always restarts the same way) but it's a different game from the first one. Think about that choice in "Check your understanding". ### 3. Speed: change the counting, not the clock How do you make the snake move faster as the score grows? The stateful answer is to change the timer: `clearInterval`, `setInterval` with a shorter delay. In our design that would mean the *clock stream* depends on the *game state* — the input would depend on the output — and the runtime would have to be told to restart its clock. That's an effect, right in the middle of the game. Here's the alternative that keeps everything pure: **run one fast clock that never changes, and let the game decide on which beats to move.** - A new action, `Frame`, arrives every `FRAME_MS = 25` ms. - `GameState` has a `frame` counter (since Module 2, waiting for this moment). - On `Frame`: if the game isn't `Playing`, nothing happens. Otherwise, `frame` goes up by one, and **if the new frame number is a multiple of `movesEvery(score)`, the game also does a `tick`** (on the state with the incremented frame). - `movesEvery(score) = max(2, 8 - floor(score / 3))`. So the speed is a pure function of the score: | score | `movesEvery` | time per move (× 25 ms) | |---|---|---| | 0–2 | 8 | 200 ms | | 3–5 | 7 | 175 ms | | 6–8 | 6 | 150 ms | | 9–11 | 5 | 125 ms | | 12–14 | 4 | 100 ms | | 15–17 | 3 | 75 ms | | 18 and up | 2 | 50 ms | This is a general and very useful trick: **instead of changing the rate of an external effect, observe a fixed-rate effect and decide in pure code.** The clock stays dumb; the decision is in `update`, where it can be tested with fixtures. It's also how many real games work: a fixed simulation step, with the game deciding what happens on each one ([Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html), [Glenn Fiedler, "Fix Your Timestep!"](https://gafferongames.com/post/fix_your_timestep/)). Predict with the fixtures before you code. Each line is "frame before → score → does this `Frame` tick?": ``` frame 0, score 0 → new frame 1: 1 % 8 ≠ 0 → no tick (frame-below-threshold-only-counts-the-frame) frame 6, score 0 → new frame 7: 7 % 8 ≠ 0 → no tick (frame-just-before-threshold-…) frame 7, score 0 → new frame 8: 8 % 8 = 0 → tick (frame-at-threshold-ticks-the-game) frame 15, score 0 → new frame 16: 16 % 8 = 0 → tick (frame-at-second-multiple-…) frame 6, score 3 → new frame 7: 7 % 7 = 0 → tick (frame-threshold-shrinks-as-score-grows) frame 7, score 3 → new frame 8: 8 % 7 ≠ 0 → no tick (frame-after-threshold-shrinks-does-not-tick) ``` The last two lines show that the multiple is checked against the *current* score's `movesEvery`. When the score crosses a threshold, the next move may come a little earlier or later than a "clean" schedule would suggest; for a game this is invisible, and the rule is simple and testable. ### 4. The clock stream changes, the runtime doesn't The input side needs one change. Instead of `Tick` every 150 ms, the clock now produces `Frame` every 25 ms; key presses go through `keyToAction` as before, which now also knows two more keys: - `' '` (Space) → `TogglePause`; `'r'` / `'R'` → `Restart`. ``` speedActions = merge(map(interval(FRAME_MS), _ => Frame), filterMap(keyDowns, keyToAction)) ``` and `main` switches from `classicActions` to `speedActions`. Compare it with `classicActions`: the same shape, a different clock and action. The runtime still holds one stream, steps it, runs the IOs. It has no idea that the game can now pause or speed up. ### 5. Why the earlier tests keep passing Run all the tests of your track after this module. They should all be green, including Modules 5 and 9. Why? - `update` still handles `Tick` and `Turn` exactly as before; you only added cases. The m05 fixtures don't contain the new actions. - `classicActions` is unchanged. The m09 tests that check "a tick every 150 ms" use `classicMain`, a copy of the program built from `classicActions`, defined in the test helpers. The one m09 test that uses the real `main` only checks that "time passing produces more drawings", which is true for both versions. - The runtime is unchanged, so the runtime tests can't be affected. This is the "open for extension" property you get from data + a fold: a new feature is a new case, and old cases don't move. When something does break, it breaks in a fixture that names the exact situation (`restart-from-paused`, `frame-tick-can-end-the-game`), not in a browser session. > **From Scala:** in Scala, write `update` as a `match` with one case per `Action` and **no `case _`**. Then if you add a case to `Action` and forget it in `update`, the compiler warns you ("match may not be exhaustive"). The stub's doc comment asks for this. F# gives the same warning for incomplete matches (`FS0025`). JS has no such check; the fixtures are your safety net. ### 6. A thought exercise: a high score that survives a reload Suppose you want to keep the best score in the browser's `localStorage` ([MDN: `localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)) so it survives reloading the page. Reading and writing storage are effects. Where would they go? Work through these questions on paper; no code is required (and none is tested). 1. **New operations.** Which `Op`s do you need? Perhaps `{tag:'LoadHigh'}` (answer: a number) and `{tag:'SaveHigh', score}` (answer: unit). In Scala/F#, which new case does `OpResult` need? 2. **New interpreter cases.** What does `trace` answer to `LoadHigh` — a fixed number passed in, like the seed? What does `runIO` do — and which new `Platform` methods does that need? 3. **Loading.** The high score must be known before the first frame is drawn. Where in `main` does it go? (Hint: `main` already starts with one effect, `randomSeed`. How do you do two effects, the second after the first? Which function from Module 7 combines them?) 4. **State.** Where does the high score live during the game? Is it a field of `GameState`? What does `Restart` do with it? 5. **Saving.** When should it be saved — on every frame, or only when a game ends with a new record? Our stream turns *each state* into an `IO`; to notice "the game just ended", you need to know the previous state too. Which combinator from Module 8 could carry the previous state along? (Hint: `scan` can accumulate pairs.) 6. **The test.** Write down, in words, a test that checks "after a game over with score 7 and a previous best of 5, a `SaveHigh 7` op is performed". Which interpreter would that test use? Notice what you did *not* have to do: touch the rules of the game, or let `update` call `localStorage`. The effect enters the program as one more `Op`; the decision of *when* to save is pure; and only the interpreter knows about the browser. That's the method of the whole course, applied to a feature you designed yourself. ## 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/M10Extensions.test.scala` or `fsharp/tests/M10Extensions.fs`. Fixture-based tests have the same case names in every track. ### Exercise 10.1 — `movesEvery` **JavaScript** (`js/src/game.js`) ```js export const movesEvery = (score) => /* max(2, 8 - floor(score / 3)) */ ``` **Scala** (`scala/src/Game.scala`) ```scala def movesEvery(score: Int): Int = ??? ``` **F#** (`fsharp/src/Game.fs`) ```fsharp let movesEvery (score: int) : int = failwith "TODO m10" ``` **Proved by:** `fixtures/m10-moves-every.json` (`score-0`, `score-2`, `score-3`, `score-17`, `score-18`, `score-100`). ### Exercise 10.2 — `update`: `TogglePause`, `Restart`, `Frame` Extend `update(state, action)` (same file as 10.1) with the three new actions, as specified in sections 1–3. Keep the old `Tick` and `Turn` cases as they are. **JavaScript** ```js export const update = (state, action) => /* GameState */ ``` **Scala** (one `case` per `Action`, no wildcard) ```scala def update(state: GameState, action: Action): GameState = ??? ``` **F#** ```fsharp let update (state: GameState) (action: Action) : GameState = failwith "TODO m05/m10" ``` **Proved by:** `fixtures/m10-update.json`, 22 cases: `toggle-pause-pauses-a-playing-game`, `toggle-pause-resumes-a-paused-game`, `toggle-pause-ignored-when-game-over`, `toggle-pause-ignored-when-won`, `turn-while-paused-is-ignored`, `tick-while-paused-is-ignored`, the four `restart-from-…` cases, the `frame-…` cases from section 3, `frame-at-top-speed-ticks-every-second-frame`, `frame-at-top-speed-odd-frame-does-not-tick`, `frame-tick-can-end-the-game`, and `frame-when-paused-is-unchanged` / `…-game-over-…` / `…-won-…`. **Watch out for:** in `Frame`, the tick happens on the state **with the incremented frame**, so the result has the new `frame` *and* the moved snake. And `movesEvery` uses the score *before* the tick (a tick that eats an apple raises the score for the *next* frame, not this one). ### Exercise 10.3 — two more keys Extend `keyToAction` (`js/src/input.js`, `scala/src/Input.scala`, `fsharp/src/Input.fs`): `' '` → `TogglePause`; `'r'`, `'R'` → `Restart`. Signature unchanged. **Proved by:** `fixtures/m10-input.json` (`space-toggles-pause`, `r-restarts`, `R-restarts`, `ArrowUp-still-turns-up`, `d-still-turns-right`, `Enter-is-not-bound`, `empty-string-is-not-bound`), plus all of `m09-input.json` still passing. ### Exercise 10.4 — the frame clock **JavaScript** (`js/src/app.js`) ```js export const FRAME_MS = 25 export const speedActions = /* Stream */ // and change `main` to use speedActions ``` **Scala** (`scala/src/App.scala`) ```scala val FRAME_MS: Long = 25 lazy val speedActions: Stream[Action] = ??? // and change `main` to use speedActions ``` **F#** (`fsharp/src/App.fs`) ```fsharp let FRAME_MS = 25L let speedActions : Stream = (* ... *) // and change `main` to use speedActions ``` **Proved by:** `the animation clock runs every 25 ms`, `speedActions turns the clock into Frame actions every 25 ms`, `speedActions still turns keys into actions, including pause and restart`, `main (seed 42) draws a moved snake after eight 25 ms frames`. **Run:** - JS: `cd js && node --test "test/m10-*.test.js"`, then `./test.sh` for everything - Scala: `cd scala && ./test.sh M10`, then `./test.sh` - F#: `cd fsharp && ./test.sh M10`, then `./test.sh` Then play: JS — reload ; Scala — `./build-web.sh` again and reload ; F# — `./build-web.sh` again and reload . Press Space, press R, and eat apples until it gets hard. ## Hints
Exercise 10.2 — nudge Write each new case as its own small function (`togglePause(state)`, `frame(state)`) and keep `update` a flat dispatch on `action.tag`. For `Frame`, do it in two steps: first build the state with `frame + 1`; then decide whether to `tick` it.
Exercise 10.2 — stronger hint For `TogglePause`, match on the *status*: two statuses change, the other two don't. For `Frame`, the guard "not Playing → unchanged" comes first. Then: `counted = { ...state, frame: state.frame + 1 }`, and the result is `counted.frame % movesEvery(state.score) === 0 ? tick(counted) : counted`.
Exercise 10.2 — near-solution (try the others first) **JavaScript** ```js const togglePause = (state) => state.status === 'Playing' ? { ...state, status: 'Paused' } : state.status === 'Paused' ? /* ? */ : state const frame = (state) => { if (state.status !== 'Playing') return state const counted = /* ? */ return /* tick(counted) or counted? */ } export const update = (state, action) => { switch (action.tag) { case 'Tick': return tick(state) case 'Turn': return turn(state, action.dir) case 'TogglePause': return /* ? */ case 'Restart': return /* ? */ case 'Frame': return /* ? */ } } ``` **Scala** ```scala def update(state: GameState, action: Action): GameState = action match case Action.Tick => Spawn.tick(state) case Action.Turn(d) => Rules.turn(state, d) case Action.TogglePause => /* ? */ case Action.Restart => /* ? */ case Action.Frame => /* ? */ ``` **F#** ```fsharp let update (state: GameState) (action: Action) : GameState = match action with | Tick -> Spawn.tick state | Turn d -> Rules.turn state d | TogglePause -> (* ? *) | Restart -> (* ? *) | Frame -> (* ? *) ```
Exercise 10.4 — nudge `speedActions` is `classicActions` with a different interval and a different constant action. Then `main` changes one word.
## Check your understanding 1. List every file you changed in this module. Which important file did you *not* change, and why didn't you need to? 2. In the stateful version, speed-up called `clearInterval`/`setInterval`. Why would doing that in our design require the input stream to depend on the game state, and why is that a problem? 3. Predict: from a fresh game (frame 0, score 0, Playing), how many `Frame` actions until the snake first moves? With score 18? Check against the table. 4. A player presses Space, then ArrowUp, then Space again, all within one move interval. Which way is the snake heading afterwards? Use the rules from Modules 3 and 10 to justify your answer. 5. `Restart` uses `newGame(state.seed)`. What would change if it used the seed of the very first game instead? Which would you prefer for a speedrun leaderboard, and which for casual play? 6. The m09 test "main draws again once time has passed" passes both before and after this module, but "the program built from classicActions draws the new game first, then the ticked game after 150 ms" uses a copy of `main`. Why was the test suite designed this way? 7. For the high-score thought exercise: which parts of your design are pure, which are interpreter code, and how would you test that a new record gets saved without a browser? ## In production - **Elm** programs add features exactly like this: a new `Msg` variant, a new branch in `update`; the compiler's exhaustiveness check lists every place that must handle it ([The Elm Architecture](https://guide.elm-lang.org/architecture/)). - **Redux** reducers grow by new action types; the Redux docs recommend keeping reducers pure and putting effects in middleware, the same split as our `update` vs. interpreter ([Redux fundamentals: state, actions and reducers](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers)). - **Fixed-timestep game loops** separate the rate at which the platform calls you from the rate at which the simulation advances; our `Frame` + `movesEvery` is a minimal version ([Glenn Fiedler, "Fix Your Timestep!"](https://gafferongames.com/post/fix_your_timestep/)). - **Event sourcing** stores the list of events and derives state by folding them — the same "state = fold(update, events)" idea, used for databases and audit logs; it is also why pause/restart cost nothing extra to replay ([Martin Fowler, "Event Sourcing"](https://martinfowler.com/eaaDev/EventSourcing.html)). ## Further reading - [Robert Nystrom, Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html) — fixed and variable time steps, and why games separate them. - [Glenn Fiedler, "Fix Your Timestep!"](https://gafferongames.com/post/fix_your_timestep/) — the classic article on decoupling simulation from the clock. - [Martin Fowler, "Event Sourcing"](https://martinfowler.com/eaaDev/EventSourcing.html) — what happens when you take "a program is a fold over events" seriously in business software. - [MDN: `Window.localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) — the API you would wrap in a new `Op` for the high-score exercise.