422 lines
25 KiB
Markdown
422 lines
25 KiB
Markdown
# Module 11 — Capstone: Pong, by the method
|
||
|
||
> **Time:** ~6–10 h (it's a whole game; spread it over several sessions) · **You'll edit:** JS `js/src/pong/model.js`, `rules.js`, `view.js`, `app.js` · Scala `scala/src/pong/Model.scala`, `Rules.scala`, `View.scala`, `App.scala` · F# `fsharp/src/Pong/Model.fs`, `Rules.fs`, `View.fs`, `App.fs`
|
||
> **Tests:** JS `cd js && node --test "test/m11-*.test.js"` · Scala `cd scala && ./test.sh M11` · F# `cd fsharp && ./test.sh M11`
|
||
> **Prerequisites:** Modules 0–10, and **[the method](method.md)** — read it before starting; this module is you applying it.
|
||
|
||
## Why this module
|
||
|
||
Everything so far was guided: each module told you which function to write and why. That's how you learn the pieces. But the goal of this course is not "can build Snake when told how". It is: **given a game written the usual way — with variables that change, timers and listeners — can you turn it into a purely functional one, and explain every step?**
|
||
|
||
So in this module you get a different game, **Pong**, described the way you'd find it in the wild: a set of rules and a sketch of a stateful implementation. You convert it, following the recipe in [method.md](method.md). The scaffolding is deliberately lighter. The specification and the tests are complete (so you know when you're done), but the hints mostly ask questions instead of giving shapes. Where Snake modules gave you a near-solution, this one asks you to look back at your own Snake code — you've written every pattern you need already.
|
||
|
||
You reuse, unchanged: `IO` (m07), streams (m08), the runtime and platform (m09), `nextUint32` (m04). Notice, as you go, how much of the course turns out to be **reusable infrastructure** and how little is Snake-specific.
|
||
|
||
## Learning objectives
|
||
|
||
By the end of this module you can:
|
||
|
||
- **apply** the conversion method to an unfamiliar game: identify state, events, the update fold, the view, the effects and the wiring;
|
||
- **design** a state that turns a *held key* (keydown + keyup) into data, and **explain** why Snake didn't need that;
|
||
- **implement** a multi-step frame update as a pipeline of small pure functions, in a specified order;
|
||
- **reuse** the IO, stream and runtime modules for a second program without modifying them;
|
||
- **justify** your design choices against the course's canonical ones.
|
||
|
||
## The game, as you'd find it
|
||
|
||
Pong is a two-player tennis game: each side has a vertical paddle and must stop the ball from leaving the court on their side ([Pong on Wikipedia](https://en.wikipedia.org/wiki/Pong)). Our version: you play the left paddle with the keyboard against a simple computer player on the right; first to 5 points wins.
|
||
|
||
Here is a typical stateful implementation sketch. Read it the way you'd read a colleague's code that you've been asked to convert. Don't run it; it isn't in the repository.
|
||
|
||
```js
|
||
// The usual, stateful way (a sketch to convert; do NOT write this in the course)
|
||
let ball = { x: 40, y: 25 }, dx = 1, dy = 1
|
||
let player = 21, cpu = 21, frame = 0
|
||
let scorePlayer = 0, scoreCpu = 0, over = false
|
||
let upHeld = false, downHeld = false
|
||
|
||
window.addEventListener('keydown', (e) => {
|
||
if (e.key === 'ArrowUp') upHeld = true
|
||
if (e.key === 'ArrowDown') downHeld = true
|
||
if (e.key === 'r') { /* reset everything... hopefully */ }
|
||
})
|
||
window.addEventListener('keyup', (e) => {
|
||
if (e.key === 'ArrowUp') upHeld = false
|
||
if (e.key === 'ArrowDown') downHeld = false
|
||
})
|
||
|
||
setInterval(() => {
|
||
if (over) return
|
||
frame++
|
||
if (downHeld) player++
|
||
if (upHeld) player--
|
||
player = Math.max(0, Math.min(42, player))
|
||
if (frame % 2 === 0) cpu += Math.sign(ball.y - 4 - cpu) // cpu follows the ball, at half speed
|
||
// ... move the ball, bounce on walls and paddles, score, serve with Math.random() ...
|
||
ctx.fillRect(/* paddles, ball, score ... */)
|
||
}, 30)
|
||
```
|
||
|
||
Count the hidden things before reading on: how many mutable variables? Where is time? Where is randomness? Where are effects mixed into logic? (You'll answer these in step 1 below.)
|
||
|
||
## The rules, precisely
|
||
|
||
The tests are shared fixtures, so the rules must be exact. Here they are in prose. The court is measured in **cells**: `W = 80` wide, `H = 50` high; each cell is `PCELL = 8` pixels. Paddles are `PADDLE = 8` cells tall. The player's paddle is in column `PLAYER_X = 2`, the computer's in column `CPU_X = 77`. A paddle's position is the `y` of its **top** cell. `WIN = 5` points end the match.
|
||
|
||
**A new match** (`initialPong(seed)`): ball at `(40, 25)` moving `dx = 1, dy = 1`; both paddles at `y = 21`; no keys held; score 0–0; status `Playing`; the given seed; frame 0.
|
||
|
||
**Keys.** `ArrowUp`/`w`/`W` and `ArrowDown`/`s`/`S` are held down and released; while held, the paddle keeps moving. `r`/`R` pressed restarts. Everything else is ignored.
|
||
|
||
**Each frame** (every 30 ms), if the match is being played, in this order:
|
||
|
||
1. The frame counter goes up by one.
|
||
2. **The player's paddle** moves one cell down if "down" is held and one cell up if "up" is held (both held: they cancel out), and is kept within `[0, H - PADDLE]`.
|
||
3. **The computer's paddle** moves only when the *new* frame number is even. It moves one cell toward `ball.y - PADDLE / 2` (it aims to centre itself on the ball) and stays put if it's already there; also kept within `[0, H - PADDLE]`. It looks at the ball *before* the ball moves this frame.
|
||
4. **The ball moves vertically:** `ny = y + dy`. If `ny < 0` or `ny >= H`, it bounces off the wall: `dy` flips and `ny = y + dy` (with the new `dy`).
|
||
5. **The ball moves horizontally:** `nx = x + dx`. It bounces off a paddle — `dx` flips and `nx = x` (the ball stays in its column this frame) — when
|
||
- `nx == PLAYER_X`, the ball is moving left (`dx < 0`), and `player <= ny < player + PADDLE` (the player's **new** position, after step 2); or
|
||
- `nx == CPU_X`, the ball is moving right (`dx > 0`), and `cpu <= ny < cpu + PADDLE`.
|
||
6. **Scoring:** if `nx < 0`, the computer scores; if `nx >= W`, the player scores. A point **serves** a new ball: position `(40, 25)`, `dx` toward the side that conceded (computer scored → `-1`, player scored → `+1`), and `dy = 1` if `nextUint32(seed)` is even, `-1` otherwise — and the seed advances to the one `nextUint32` returned.
|
||
7. **End:** when a score reaches `WIN`, the status becomes `Over`.
|
||
|
||
When the match is `Over`, frames change nothing. Key presses and releases are still recorded (so that a key held across a restart behaves sensibly). **Restart** gives `initialPong(state.seed)` at any time.
|
||
|
||
**The picture** (`pongView`), drawn in order, all text `'#ffffff'` and `'center'`-aligned:
|
||
|
||
- background `Rect(0, 0, 640, 400, '#111111')`;
|
||
- player paddle `Rect(PLAYER_X*8, player*8, 8, 64, '#ffffff')`, then the cpu paddle the same way at `CPU_X`;
|
||
- ball `Rect(x*8, y*8, 8, 8, '#ffeb3b')`;
|
||
- score `Text(320, 30, '<player> <cpu>', …, 24, …)` — three spaces between the numbers;
|
||
- when `Over`: `Text(320, 200, 'You Win!' or 'CPU Wins', …, 32, …)` (`'You Win!'` if the player has more points) and `Text(320, 230, 'Press R to restart', …, 16, …)`.
|
||
|
||
Note that, unlike the Snake view, there's no 1-pixel inset: paddles and ball fill their cells.
|
||
|
||
## Applying the method
|
||
|
||
Open [method.md](method.md) next to this page. Each step below names the step of the method, asks you the questions you should answer *before* looking at the course's choice, then gives the canonical choice (fixed, because the fixtures depend on it). As in Module 2, **write your own answer first, then compare**. Where your design differs, ask whether yours is better, worse, or only different — and why.
|
||
|
||
### Step 1 — Identify the state; make it one value
|
||
|
||
Go through the stateful sketch and list every `let`. Then ask:
|
||
|
||
- Which of them change during a frame? Which change only between frames because of keys?
|
||
- Is `frame` really state, or could it be computed? (Could the cpu's "every second frame" rule work without it?)
|
||
- Where is the randomness hiding? What must be in the state so that serving is a pure function?
|
||
- Is anything in the sketch *derived* (computable from other state) and therefore not needed in the state?
|
||
|
||
**Canonical choice:**
|
||
|
||
**JavaScript** (`js/src/pong/model.js`)
|
||
|
||
```js
|
||
// PongState = {
|
||
// ball: {x, y}, vel: {dx, dy}, player, cpu,
|
||
// held: {up, down}, score: {player, cpu},
|
||
// status: 'Playing' | 'Over', seed, frame
|
||
// }
|
||
```
|
||
|
||
**Scala** (`scala/src/pong/Model.scala`, given)
|
||
|
||
```scala
|
||
final case class Velocity(dx: Int, dy: Int)
|
||
final case class Held(up: Boolean, down: Boolean)
|
||
final case class Score(player: Int, cpu: Int)
|
||
enum PongStatus:
|
||
case Playing, Over
|
||
|
||
final case class PongState(ball: Point, vel: Velocity, player: Int, cpu: Int, held: Held,
|
||
score: Score, status: PongStatus, seed: Long, frame: Int)
|
||
```
|
||
|
||
**F#** (`fsharp/src/Pong/Model.fs`, given)
|
||
|
||
```fsharp
|
||
type Velocity = { Dx: int; Dy: int }
|
||
type Held = { Up: bool; Down: bool }
|
||
type Score = { Player: int; Cpu: int }
|
||
|
||
[<RequireQualifiedAccess>]
|
||
type PongStatus =
|
||
| Playing
|
||
| Over
|
||
|
||
type PongState =
|
||
{ Ball: Point
|
||
Vel: Velocity
|
||
Player: int
|
||
Cpu: int
|
||
Held: Held
|
||
Score: Score
|
||
Status: PongStatus
|
||
Seed: uint32
|
||
Frame: int }
|
||
|
||
// The court, in cells: W = 80, H = 50, PCELL = 8, PADDLE = 8, PLAYER_X = 2, CPU_X = 77, WIN = 5
|
||
```
|
||
|
||
> **F# note:** `[<RequireQualifiedAccess>]` means you must write `PongStatus.Playing`, never plain `Playing`. Snake's `Status` also has a `Playing` case (and, in step 2, Snake's `Action` has `Frame` and `Direction` has `Up`/`Down`). Without the attribute, whichever type was opened last would silently win, and `Playing` in a Pong file could mean Snake's status. Qualified access makes every use unambiguous.
|
||
|
||
The most interesting field is **`held`**. In Snake, a key press was an *instantaneous* event: pressing `ArrowUp` meant "turn up" once. In Pong, what matters is whether a key **is being held** at the moment a frame happens. The browser doesn't give us "is held"; it gives us two events, keydown and keyup. So "held" is *state* that we derive from those events, and it must live in `PongState` — not in a variable in the runtime, and certainly not by asking the browser during `update`. That's the stateful sketch's `upHeld`/`downHeld`, turned into data.
|
||
|
||
### Step 2 — Identify the events; make them data
|
||
|
||
What can happen to a match? List the events in the sketch's callbacks and timer. Then decide: what's the *smallest* set of actions that the update needs to know about?
|
||
|
||
**Canonical choice:** four actions.
|
||
|
||
**JavaScript**
|
||
|
||
```js
|
||
// PongAction = {tag:'Press', key:'Up'|'Down'} | {tag:'Release', key:'Up'|'Down'} | {tag:'Frame'} | {tag:'Restart'}
|
||
// constructors: PongAction.Press(key), PongAction.Release(key), PongAction.Frame, PongAction.Restart
|
||
```
|
||
|
||
**Scala** (given)
|
||
|
||
```scala
|
||
enum Paddle:
|
||
case Up, Down
|
||
|
||
enum PongAction:
|
||
case Press(key: Paddle)
|
||
case Release(key: Paddle)
|
||
case Frame
|
||
case Restart
|
||
```
|
||
|
||
**F#** (given)
|
||
|
||
```fsharp
|
||
[<RequireQualifiedAccess>]
|
||
type Paddle =
|
||
| Up
|
||
| Down
|
||
|
||
[<RequireQualifiedAccess>]
|
||
type PongAction =
|
||
| Press of Paddle
|
||
| Release of Paddle
|
||
| Frame
|
||
| Restart
|
||
```
|
||
|
||
In F# you write these as `Paddle.Up`, `PongAction.Press Paddle.Up`, `PongAction.Frame` — qualified, for the reason given in step 1.
|
||
|
||
And the translation from browser keys is a pure function, now with a second argument because the same key means different things going down and going up:
|
||
|
||
```
|
||
pongKey(isDown, key):
|
||
ArrowUp / w / W → Press Up (isDown) | Release Up (not isDown)
|
||
ArrowDown / s / S → Press Down (isDown) | Release Down (not isDown)
|
||
r / R → Restart (only when isDown; releasing r does nothing)
|
||
anything else → nothing (null / None)
|
||
```
|
||
|
||
Question: why are `Press` and `Release` separate actions rather than one `SetHeld(key, bool)`? (There's no single right answer; think about what reads better in `update` and in fixtures.)
|
||
|
||
### Step 3 — Write `update` as a fold
|
||
|
||
`pongUpdate(state, action)` is your Pong `update`. `Press`/`Release` set a field of `held`; `Restart` is one call; `Frame` is the seven-step pipeline from [the rules](#the-rules-precisely).
|
||
|
||
The `Frame` case is the biggest function you'll write in the course. The way to keep it readable is the way you kept Snake readable: **split it into small, pure, separately-understandable steps**, each taking a state and returning a state, applied in the order of the rules. You'll want a `clamp(lo, hi, x)` helper (it's tested on its own).
|
||
|
||
Questions to answer before writing code:
|
||
|
||
- The cpu "looks at the ball before the ball moves", and the player paddle bounce uses "the player's new position". What does that tell you about the order of your steps?
|
||
- Where does the serve's randomness come from? Which Module 4 function do you call, and what must you do with the seed it returns?
|
||
- Steps 4–6 compute `ny`, then `nx`, then maybe a score. Will you do the ball in one function or several? If several, how do they share `ny`?
|
||
|
||
### Step 4 — Write the view as data
|
||
|
||
`pongView(state)` returns a `Scene` — the same `Shape`s as Snake. You already know how to do this; follow [the picture spec](#the-rules-precisely) exactly.
|
||
|
||
### Step 5 — Effects as values
|
||
|
||
Which effects does Pong need? Drawing, and… is there anything else? The serve's randomness is *pure* (it's in the seed). The only impure randomness is the seed at the start, which `randomSeed` already describes. So: **no new `Op`, no change to `IO`.** That is worth pausing on. A whole second game with randomness, a clock, held keys and a computer opponent needs exactly the effects you already have.
|
||
|
||
### Step 6 — Wire the streams
|
||
|
||
Like Snake's `app`:
|
||
|
||
- `pongActions`: `Frame` every `FRAME_MS_PONG = 30` ms (from `interval`), merged with `pongKey(true, key)` of every key press (`keyDowns`) and `pongKey(false, key)` of every key release (`keyUps`). That's three streams merged — in which order? (The test `pongActions turns the clock into Frames and keys into Press, Release and Restart` tells you what it expects; read it.)
|
||
- `pongStates(seed, actions)`: start from `initialPong(seed)`, fold with `pongUpdate`, emit the initial state first.
|
||
- `pongMain`: ask for a seed; turn the stream of states into a stream of drawings of `pongView`.
|
||
|
||
### Step 7 — One runtime at the edge
|
||
|
||
Nothing to write. `js/src/pong/main.js` is one line: `runApp(pongMain, browserPlatform(document.getElementById('game'), window))`. In Scala, `scala/web/Main.scala` has a `PongMain` object; `build-web.sh` already compiles it to `web/dist/pong.js`. F# has the equivalent in `fsharp/web/PongMain.fs`: `Runtime.runApp (BrowserPlatform.create (BrowserPlatform.gameCanvas ())) (Pong.App.pongMain ())`. Your m09 runtime runs a game it has never heard of. That's the reward for keeping it ignorant.
|
||
|
||
## 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/M11Pong.test.scala` or `fsharp/tests/M11Pong.fs`. Fixture-based tests have the same case names in every track.
|
||
|
||
### Exercise 11.1 — model
|
||
|
||
**JavaScript** (`js/src/pong/model.js`; the constants `W`, `H`, `PCELL`, `PADDLE`, `PLAYER_X`, `CPU_X`, `WIN` are given)
|
||
|
||
```js
|
||
export const initialPong = (seed) => /* PongState */
|
||
export const PongAction = { Press: (key) => ..., Release: (key) => ..., Frame: ..., Restart: ... }
|
||
```
|
||
|
||
**Scala** (`scala/src/pong/Model.scala`)
|
||
|
||
```scala
|
||
object PongModel:
|
||
def initialPong(seed: Long): PongState = ???
|
||
```
|
||
|
||
**F#** (`fsharp/src/Pong/Model.fs`)
|
||
|
||
```fsharp
|
||
let initialPong (seed: uint32) : PongState = failwith "TODO m11"
|
||
```
|
||
|
||
**Proved by:** `the court is 80 by 50 cells of 8 pixels, with 8-cell paddles`, `initialPong puts the ball in the middle, both paddles centred, and nobody ahead`, `PongAction builds Press and Release with a key, and has Frame and Restart as plain values` (JS only; in Scala and F# the types are given, so there is nothing to test).
|
||
|
||
### Exercise 11.2 — rules
|
||
|
||
**JavaScript** (`js/src/pong/rules.js`)
|
||
|
||
```js
|
||
export const clamp = (lo, hi, x) => /* number */
|
||
export const pongUpdate = (state, action) => /* PongState */
|
||
```
|
||
|
||
**Scala** (`scala/src/pong/Rules.scala`)
|
||
|
||
```scala
|
||
object PongRules:
|
||
def clamp(lo: Int, hi: Int, x: Int): Int = ???
|
||
def pongUpdate(state: PongState, action: PongAction): PongState = ???
|
||
```
|
||
|
||
**F#** (`fsharp/src/Pong/Rules.fs`)
|
||
|
||
```fsharp
|
||
let clamp (lo: int) (hi: int) (x: int) : int = failwith "TODO m11"
|
||
let pongUpdate (state: PongState) (action: PongAction) : PongState = failwith "TODO m11"
|
||
```
|
||
|
||
**Proved by:** `clamp keeps a number inside [lo, hi]`, `pongUpdate does not modify the state it is given`, `an idle match stays on the court and ends once someone reaches five`, and `fixtures/m11-pong-update.json` — 43 named cases. Read the case names: they're the specification broken into single facts (`player-stops-at-the-top-edge`, `cpu-does-not-move-on-an-odd-frame`, `paddle-bounce-uses-the-player-position-after-it-moved`, `serve-direction-comes-from-the-seed-odd-value-serves-upward`, `cpu-reaching-five-ends-the-match`, …). When one fails, open the fixture, find the case, and work it out by hand.
|
||
|
||
**Strategy:** don't write all seven steps and then run the tests. Write `Press`/`Release`/`Restart` and run; then step 1 and 2 of `Frame` and run (watch the paddle cases go green); then the cpu; then the ball. Let the case names guide you.
|
||
|
||
### Exercise 11.3 — view
|
||
|
||
**JavaScript** (`js/src/pong/view.js`)
|
||
|
||
```js
|
||
export const pongView = (state) => /* Scene */
|
||
```
|
||
|
||
**Scala** (`scala/src/pong/View.scala`)
|
||
|
||
```scala
|
||
object PongView:
|
||
def pongView(state: PongState): Scene = ???
|
||
```
|
||
|
||
**F#** (`fsharp/src/Pong/View.fs`)
|
||
|
||
```fsharp
|
||
let pongView (state: PongState) : Scene = failwith "TODO m11"
|
||
```
|
||
|
||
**Proved by:** `fixtures/m11-pong-view.json` (`new-match`, `paddles-and-ball-move-with-the-state`, `player-wins`, `cpu-wins`).
|
||
|
||
### Exercise 11.4 — keys and wiring
|
||
|
||
**JavaScript** (`js/src/pong/app.js`)
|
||
|
||
```js
|
||
export const FRAME_MS_PONG = 30
|
||
export const pongKey = (isDown, key) => /* PongAction or null */
|
||
export const pongActions = /* Stream<PongAction> */
|
||
export const pongStates = (seed, actions) => /* Stream<PongState> */
|
||
export const pongMain = /* IO<Stream<IO<undefined>>> */
|
||
```
|
||
|
||
**Scala** (`scala/src/pong/App.scala`)
|
||
|
||
```scala
|
||
object PongApp:
|
||
val FRAME_MS_PONG: Long = 30
|
||
def pongKey(isDown: Boolean, key: String): Option[PongAction] = ???
|
||
lazy val pongActions: Stream[PongAction] = ???
|
||
def pongStates(seed: Long, actions: Stream[PongAction]): Stream[PongState] = ???
|
||
lazy val pongMain: IO[Stream[IO[Unit]]] = ???
|
||
```
|
||
|
||
**F#** (`fsharp/src/Pong/App.fs`)
|
||
|
||
```fsharp
|
||
let FRAME_MS_PONG = 30L
|
||
let pongKey (isDown: bool) (key: string) : PongAction option = failwith "TODO m11"
|
||
let pongActions : Stream<PongAction> = (* ... *)
|
||
let pongStates (seed: uint32) (actions: Stream<PongAction>) : Stream<PongState> = failwith "TODO m11"
|
||
let pongMain () : IO<Stream<IO<unit>>> = failwith "TODO m11"
|
||
```
|
||
|
||
In F#, `main` and `pongMain` are **functions of unit** (`pongMain ()`), not values. F# evaluates a module's top-level values when the module is first used, so a value whose definition fails (like a stub) would break every test that touches the module; a function of unit is only evaluated when called. The value it returns is still the same pure description.
|
||
|
||
**Proved by:** `fixtures/m11-pong-key.json` (21 cases, including `r-up-does-nothing` and the `constructor`/`toString` traps from Module 9), `the clock runs every 30 ms`, `pongActions turns the clock into Frames and keys into Press, Release and Restart`, `pongStates starts with the new match, then yields one state per action`, `pongMain asks for a seed, draws the new match first, and then the updated one`.
|
||
|
||
**Run and play:**
|
||
- JS: `cd js && node --test "test/m11-*.test.js"`; then, **from the repository root**, `node scripts/serve.mjs` and open <http://localhost:8000/js/pong.html>
|
||
- Scala: `cd scala && ./test.sh M11`; then `./build-web.sh`; serve from the repository root (`node scripts/serve.mjs`) and open <http://localhost:8000/scala/pong.html>
|
||
- F#: `cd fsharp && ./test.sh M11`; then `./build-web.sh`; serve from the repository root (`node scripts/serve.mjs`) and open <http://localhost:8000/fsharp/pong.html>
|
||
|
||
Finally run the **whole** suite of your track (`./test.sh`). The purity audit now scans your Pong files too.
|
||
|
||
## Hints
|
||
|
||
The hints for this module are deliberately lighter. Before opening one, look at the equivalent piece of your Snake code; that's the real hint.
|
||
|
||
<details><summary>Exercise 11.2 — nudge</summary>
|
||
|
||
Write `frame(state)` as a composition of steps, each `PongState → PongState`: `count`, `movePlayer`, `moveCpu`, `moveBall` (which may itself call `serve`), `checkOver`. In JS, `pipe` from Module 1 (or a chain of calls) reads top to bottom in rule order; in Scala `andThen`; in F# `|>`. Which of these steps need the guard "only when Playing"? (Only the whole `Frame`.)
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 11.2 — stronger hint</summary>
|
||
|
||
- `moveCpu` compares `cpu` with `target = ball.y - PADDLE / 2`: below target → +1, above → -1, equal → 0 (a "sign" function); clamp; and only on even `frame` — which, after `count`, is the *new* frame.
|
||
- In `moveBall`, compute `dy`/`ny` first, then `dx`/`nx` using the already-moved `player`/`cpu` from the state. Then: if `nx < 0` or `nx >= W`, return `serve(...)` with the right scorer; otherwise, the state with the new ball and velocity.
|
||
- `serve`: `[v, seed'] = nextUint32(state.seed)`; `dy = v % 2 === 0 ? 1 : -1`. In Scala the seed is a `Long`, in F# a `uint32`; the fixtures are the same in all tracks, so watch for sign problems.
|
||
- Did your serve test fail on the score but pass on the ball? Check that you add the point *and* serve, in the same step.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 11.4 — nudge</summary>
|
||
|
||
Three sources, one `merge` chain: `merge(merge(frames, presses), releases)`. `presses` is `keyDowns` through `filterMap` with `pongKey(true, …)`; `releases` similarly. The rest is your Snake `app` with different names.
|
||
|
||
</details>
|
||
|
||
## Check your understanding
|
||
|
||
1. In your own words, apply the first two steps of the method to the stateful sketch: list its hidden state (including hidden time and randomness) and its events.
|
||
2. Why is `held` in `PongState` and not in the runtime? What would break in testing if the runtime tracked held keys and passed "is up held?" to `update`?
|
||
3. Predict: the player holds `ArrowUp` and, while still holding it, presses and releases `ArrowDown`. What is `held` after each of the three events? Which way does the paddle move on frames in between?
|
||
4. The cpu moves on even frames and looks at the ball before it moves. What would change in the fixtures if it looked *after* the ball moved? Find the case that pins this down (hint: its name mentions "before").
|
||
5. You reused `IO`, `Stream`, `runIO`, `runApp` and `nextUint32` without changing a line. List what was Snake-specific in the course, and what was general infrastructure. What does that suggest about where effort should go in a functional codebase?
|
||
6. Pong's serve direction is random. Explain why `pongUpdate` is still a pure function, and how a test can check a "random" serve exactly.
|
||
7. Imagine a two-player mode: W/S for the left paddle, arrows for the right. Which steps of the method change, and which files? Is there any change to the runtime?
|
||
8. Compare your own design from steps 1–2 with the canonical one. Name one place where yours was different and argue for or against it.
|
||
|
||
## In production
|
||
|
||
- **The Elm Architecture** and **Elmish** are this method frozen into a framework: model (step 1), messages (step 2), update (step 3), view (step 4), commands (step 5), subscriptions (step 6), and a runtime you don't write (step 7) ([The Elm Architecture](https://guide.elm-lang.org/architecture/), [Elmish](https://elmish.github.io/elmish/)).
|
||
- **Redux** applies steps 1–3 to web apps; its "Redux Style Guide" rules (state is serializable, reducers are pure, no side effects in reducers) are the pitfalls list of the method ([Redux](https://redux.js.org/)).
|
||
- **Deterministic lockstep** multiplayer games (many real-time strategy games) send only *inputs* between machines and run the same pure update on each one; they depend on exactly the property you just built — same seed and same inputs give the same game ([Glenn Fiedler, "Deterministic Lockstep"](https://gafferongames.com/post/deterministic_lockstep/)).
|
||
|
||
## Further reading
|
||
|
||
- [The method](method.md) — the recipe you applied, with a worked example and a checklist.
|
||
- [Pong (Wikipedia)](https://en.wikipedia.org/wiki/Pong) — history and the original rules.
|
||
- [The Elm Architecture](https://guide.elm-lang.org/architecture/) — read it again now; every part should look familiar.
|
||
- [Glenn Fiedler, "Deterministic Lockstep"](https://gafferongames.com/post/deterministic_lockstep/) — what pure, deterministic game updates make possible in networked games.
|
||
- [John Hughes, "Why Functional Programming Matters"](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) — reread the conclusion with the whole course behind you.
|