feat(course): audit fixes and twelve extensions (Modules 7b, 12, 13)
Audit pass: - Frame rule counts frames since the last move, so every step lasts exactly movesEvery(score) frames, also right after a speed-up - clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap - Pong Restart keeps held keys - Fact corrections (mulberry32 output coverage, Haskell IO, uniform) - New fixture cases from a mutation analysis Extensions (all three tracks, shared fixtures, course text): - Turn queue (Exercise 10.6), persisted high score (10.7), replay viewer (10.8), play() end-to-end helper (9.5) - Optional State (4.7) and optional Module 7b, stack-safe IO - Module 12, property-based testing with generators, shrinking, invariants and a model-based oracle - Module 13, Conway's Life with a Click event - JS type checking via jsconfig, JSDoc and types.d.ts - Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh, Playwright end-to-end suite (e2e/, scripts/e2e.sh) Docs: docs/contract.md is the canonical contract; specs and build log under docs/superpowers. Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
876f5aabb1
commit
edd57f0148
253 files changed
+21753
-503
No files matched your search
@@ -77,6 +77,8 @@ type Event =
|
||||
|
||||
**Naming note.** The JS doc comments call this type `Input`; Scala and F# call it `Event`, because their m09 module is already named `Input` and the two names would clash. Same thing.
|
||||
|
||||
(A fourth kind, a mouse click on the canvas, arrives in [Module 13](13-capstone-life.md), the second capstone. It is one more case of this type and one more source next to `keyDowns`; nothing in this module changes for it.)
|
||||
|
||||
Notice that time arrives as an event too. The program never *asks* for the time (`Date.now()` is banned by the purity audit); the outside world *tells* it what time it is, the same way it tells it about keys. Hidden time becomes an explicit input. Keep that in mind; it's the most important design decision in this module.
|
||||
|
||||
### 2. A stream is a machine you feed one event at a time
|
||||
@@ -169,7 +171,7 @@ Take a minute and work out by hand why the second list is `[10, 30, 130, 160, 20
|
||||
|
||||
**`interval(ms)`** is the interesting one. It turns the clock into regular beats: on `Time t`, it emits every multiple of `ms` that has been *crossed* since the last time event — every `k*ms` with `last < k*ms ≤ t` — in ascending order. It starts with `last = 0` and afterwards `last` becomes `t`. The values it emits are those multiples themselves.
|
||||
|
||||
Why "every multiple crossed" rather than "one beat per time event"? Because the outside world does not call us on a perfect schedule. The browser reports time about 60 times per second, but if the tab was in the background for two seconds, the next time event might jump from `t = 1000` to `t = 3000`. The game should then catch up on all the ticks it missed, not silently skip them. (This idea, separating game time from the rate at which the outside world calls you, is the classic "fixed timestep" pattern; see [Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html).)
|
||||
Why "every multiple crossed" rather than "one beat per time event"? Because the outside world does not call us on a perfect schedule. The browser reports time about 60 times per second, but one slow frame can make the next time event jump from `t = 1000` to `t = 1070`. The game should then catch up on the ticks it missed, not silently skip them, so that game time does not drift away from real time. (This idea, separating game time from the rate at which the outside world calls you, is the classic "fixed timestep" pattern; see [Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html).) Catching up has a limit, though: if the tab was in the background for a whole minute, replaying that minute in one go is not what anybody wants. `interval` stays simple and always catches up; Module 10 adds a separate combinator that limits how far the clock may jump.
|
||||
|
||||
Here is how `last` threads through a sequence of events for `interval(10)`:
|
||||
|
||||
@@ -492,8 +494,10 @@ let rec map (f: 'a -> 'b) (Stream step) : Stream<'b> =
|
||||
4. Why does `map(s, f)` have to return `map(next, f)` and not just `next`? Write down what `run` would produce with the wrong version on two key presses.
|
||||
5. The test "running the same stream value twice gives the same outputs" would fail for a typical RxJS pipeline with a `scan`. Explain why in terms of where the accumulator lives.
|
||||
6. `merge(a, b)` steps *both* streams on *every* event. What would go wrong with an `interval` inside `b` if `merge` only stepped `b` on events that `a` ignored?
|
||||
7. The browser reports time about 60 times per second, and the snake ticks every 150 ms. Using `interval`, explain what happens if the browser stops calling us for 1 second (say, the tab was hidden) and then resumes. Is that what you want for this game?
|
||||
7. The browser reports time about 60 times per second, and the snake ticks every 150 ms. Using `interval`, explain what happens if the browser stops calling us for 1 second (say, the tab was hidden) and then resumes. Is that what you want for this game? And after a whole minute? (Module 10 comes back to this.)
|
||||
8. Our streams never ask what time it is. Why is that important for testing? Where *will* the time come from?
|
||||
9. **Challenge: laws for streams.** Two streams "behave the same" when `run` gives the same outputs for every list of events. Argue from the definitions: (a) `map(map(s, f), g)` behaves like `map(s, x => g(f(x)))`; (b) `merge(merge(a, b), c)` behaves like `merge(a, merge(b, c))`; (c) `merge(a, b)` does *not* in general behave like `merge(b, a)`: give two streams and one event that show the difference; (d) is `filter(map(s, f), p)` the same as `map(filter(s, p), f)`? When?
|
||||
10. Every combinator in this module changes a stream's *outputs*. Sketch one that changes its *inputs*: `mapEvents(s, g)`, which behaves like `s` but shows it `g(event)` instead of each event. What could you build with it? (Module 10 needs a stateful version of this idea.)
|
||||
|
||||
## In production
|
||||
|
||||
@@ -510,3 +514,5 @@ let rec map (f: 'a -> 'b) (Stream step) : Stream<'b> =
|
||||
- [Functional reactive programming (Wikipedia)](https://en.wikipedia.org/wiki/Functional_reactive_programming) — an overview of the family of ideas and implementations.
|
||||
- [Robert Nystrom, Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html) — why games decouple game time from the rate at which the outside world calls them; the reason `interval` catches up.
|
||||
- [RxJS: Overview](https://rxjs.dev/guide/overview) — the push-based alternative, to compare with what you built.
|
||||
- [Evan Czaplicki, "Elm: Concurrent FRP for Functional GUIs"](https://elm-lang.org/assets/papers/concurrent-frp.pdf) — the thesis behind Elm; its survey of earlier FRP designs explains the trade-offs between signals, events and automata.
|
||||
- [Conal Elliott, "Push-Pull Functional Reactive Programming"](http://conal.net/papers/push-pull-frp/) — how the two strategies of section 7 can be combined.
|
||||
Reference in new issue
Block a user