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
@@ -0,0 +1,71 @@
|
||||
/// m13 life rules: the next generation is a function of the whole old board, and nothing else.
|
||||
module Snake.Life.Rules
|
||||
|
||||
open Snake
|
||||
open Snake.Model
|
||||
open Snake.Life.Model
|
||||
|
||||
// Every cell of the board, row-major (y, then x). Filtering this list keeps any result in canonical order for free.
|
||||
let private board: Point list =
|
||||
[ for y in 0 .. LH - 1 do
|
||||
for x in 0 .. LW - 1 -> { X = x; Y = y } ]
|
||||
|
||||
let private onBoard (p: Point) : bool = p.X >= 0 && p.X < LW && p.Y >= 0 && p.Y < LH
|
||||
|
||||
// A cell's position in row-major order: the order the canonical list is sorted by.
|
||||
let private index (p: Point) : int = p.Y * LW + p.X
|
||||
|
||||
let private offsets = [ (-1, -1); (0, -1); (1, -1); (-1, 0); (1, 0); (-1, 1); (0, 1); (1, 1) ]
|
||||
|
||||
// The up to eight neighbours of `p` that are on the board (the edges are dead: nothing wraps around).
|
||||
let private neighbours (p: Point) : Point list =
|
||||
offsets |> List.map (fun (dx, dy) -> { X = p.X + dx; Y = p.Y + dy }) |> List.filter onBoard
|
||||
|
||||
// B3/S23, every cell judged on the *old* list only. A Set of the old cells makes "is this neighbour alive?" one
|
||||
// lookup, so a generation is 1200 x 8 lookups however many cells live. The set is built once and only read.
|
||||
let lifeStep (cells: Point list) : Point list =
|
||||
let alive = Set.ofList cells
|
||||
let count p = neighbours p |> List.filter alive.Contains |> List.length
|
||||
|
||||
board
|
||||
|> List.filter (fun p ->
|
||||
let n = count p
|
||||
n = 3 || (n = 2 && alive.Contains p))
|
||||
|
||||
// Add a dead cell or remove a live one, keeping the list sorted row-major without duplicates. Off the board: unchanged.
|
||||
let private toggle (cells: Point list) (p: Point) : Point list =
|
||||
if not (onBoard p) then
|
||||
cells
|
||||
elif List.contains p cells then
|
||||
List.filter (fun c -> c <> p) cells
|
||||
else
|
||||
List.filter (fun c -> index c < index p) cells
|
||||
@ [ p ]
|
||||
@ List.filter (fun c -> index c > index p) cells
|
||||
|
||||
// A random board: one `intBelow 4` per cell in row-major order, the cell alive when the draw is 0 (about one cell in
|
||||
// four). mapFold threads the seed through the 1200 draws; the result is the cells and the seed after the last draw.
|
||||
let private randomCells (seed: uint32) : Point list * uint32 =
|
||||
let draws, next = List.mapFold (fun s _ -> Random.run s (Random.intBelow 4)) seed board
|
||||
List.zip board draws |> List.filter (fun (_, d) -> d = 0) |> List.map fst, next
|
||||
|
||||
let private nextGeneration (state: LifeState) : LifeState =
|
||||
{ state with
|
||||
Cells = lifeStep state.Cells
|
||||
Generation = state.Generation + 1 }
|
||||
|
||||
let lifeUpdate (state: LifeState) (action: LifeAction) : LifeState =
|
||||
match action with
|
||||
| LifeAction.Frame -> if state.Running then nextGeneration state else state
|
||||
| LifeAction.Step -> if state.Running then state else nextGeneration state
|
||||
| LifeAction.TogglePause -> { state with Running = not state.Running }
|
||||
// Clicking edits the world whether it runs or not: the click is one action between two generations.
|
||||
| LifeAction.Toggle p -> { state with Cells = toggle state.Cells p }
|
||||
| LifeAction.Clear -> { state with Cells = []; Generation = 0 }
|
||||
| LifeAction.Randomize ->
|
||||
let cells, seed = randomCells state.Seed
|
||||
|
||||
{ state with
|
||||
Cells = cells
|
||||
Seed = seed
|
||||
Generation = 0 }
|
||||
Reference in new issue
Block a user