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,180 @@
|
||||
module Snake.Tests.M10Replay
|
||||
|
||||
open System.Text.Json
|
||||
open Expecto
|
||||
open Snake
|
||||
open Snake.Model
|
||||
open Snake.View
|
||||
open Snake.IO
|
||||
open Snake.Stream
|
||||
open Snake.Replay
|
||||
open Snake.Tests.Fixtures
|
||||
open Snake.Tests.M09Helpers
|
||||
|
||||
// Viewers in the fixtures use the keys index, last and playing.
|
||||
let private viewer (v: JsonElement) : Viewer =
|
||||
{ Index = num (prop "index" v); Last = num (prop "last" v); Playing = bool (prop "playing" v) }
|
||||
|
||||
let private viewerAction (v: JsonElement) : ViewerAction =
|
||||
match str (prop "tag" v) with
|
||||
| "Next" -> ViewerAction.Next
|
||||
| "Prev" -> ViewerAction.Prev
|
||||
| "First" -> ViewerAction.First
|
||||
| "Last" -> ViewerAction.Last
|
||||
| "TogglePlay" -> ViewerAction.TogglePlay
|
||||
| "Beat" -> ViewerAction.Beat
|
||||
| other -> failwith $"unknown viewer action tag: {other}"
|
||||
|
||||
// The sample recording, as the page loads it. Not a fixture of cases: a recording {seed, actions}.
|
||||
let private sample: Recording =
|
||||
let v = load "replay-sample"
|
||||
{ Seed = u32 (prop "seed" v); Actions = list action (prop "actions" v) }
|
||||
|
||||
[<Tests>]
|
||||
let tests =
|
||||
testList "M10 Replay" [
|
||||
yield! fixtureTests "m10-viewer-update" "viewerUpdate" (fun c ->
|
||||
Expect.equal (viewerUpdate (viewer (arg 0 c)) (viewerAction (arg 1 c))) (viewer (expected c)) "viewerUpdate")
|
||||
|
||||
yield! fixtureTests "m10-viewer-key" "viewerKey" (fun c ->
|
||||
Expect.equal (viewerKey (str (arg 0 c))) (opt viewerAction (expected c)) "viewerKey")
|
||||
|
||||
yield! fixtureTests "m10-viewer-view" "viewerView" (fun c ->
|
||||
Expect.equal (viewerView (list gameState (arg 0 c)) (viewer (arg 1 c))) (list shape (expected c)) "viewerView")
|
||||
|
||||
yield!
|
||||
[ testCase
|
||||
"the sample recording is worth watching: Tick and Turn only, under 400 actions, at least five apples, then game over"
|
||||
(fun () ->
|
||||
Expect.all
|
||||
sample.Actions
|
||||
(fun a ->
|
||||
match a with
|
||||
| Tick
|
||||
| Turn _ -> true
|
||||
| _ -> false)
|
||||
"Tick and Turn only"
|
||||
|
||||
let n = List.length sample.Actions
|
||||
Expect.isTrue (n >= 150 && n <= 400) $"{n} actions"
|
||||
let fs = Game.history (Spawn.newGame sample.Seed) sample.Actions
|
||||
let last = List.last fs
|
||||
Expect.isGreaterThanOrEqual last.Score 5 "score"
|
||||
Expect.equal last.Status GameOver "last frame"
|
||||
Expect.equal (List.item (List.length fs - 2) fs).Status Playing "frame before the last")
|
||||
|
||||
testCase "frames replays the recording with history, from the new game of its seed" (fun () ->
|
||||
let recording = { Seed = 42u; Actions = [ Tick; Turn Up; Tick ] }
|
||||
Expect.equal (frames recording) (Game.history (Spawn.newGame 42u) recording.Actions) "frames"
|
||||
Expect.equal (List.length (frames sample)) (List.length sample.Actions + 1) "length")
|
||||
|
||||
testCase "viewerStart is the first frame, not playing" (fun () ->
|
||||
Expect.equal (viewerStart 12) { Index = 0; Last = 12; Playing = false } "viewerStart")
|
||||
|
||||
testCase "viewerUpdate keeps the index in [0, last] whatever the actions" (fun () ->
|
||||
let actions =
|
||||
[ ViewerAction.Prev
|
||||
ViewerAction.Next
|
||||
ViewerAction.Last
|
||||
ViewerAction.Next
|
||||
ViewerAction.TogglePlay
|
||||
ViewerAction.Beat
|
||||
ViewerAction.Beat
|
||||
ViewerAction.First
|
||||
ViewerAction.Prev
|
||||
ViewerAction.Beat ]
|
||||
|
||||
let viewers = List.scan viewerUpdate (viewerStart 3) actions
|
||||
|
||||
Expect.all
|
||||
viewers
|
||||
(fun v -> v.Index >= 0 && v.Index <= 3 && not (v.Playing && v.Index = 3))
|
||||
"in range, never playing at the end")
|
||||
|
||||
testCase "the autoplay clock beats every 100 ms" (fun () -> Expect.equal REPLAY_MS 100L "REPLAY_MS")
|
||||
|
||||
testCase "replayActions turns the clock into Beats and keys into viewer actions" (fun () ->
|
||||
Expect.equal
|
||||
(run [ Time 100L; KeyDown "ArrowRight"; KeyDown "x"; KeyDown " "; Time 300L ] replayActions)
|
||||
[ ViewerAction.Beat
|
||||
ViewerAction.Next
|
||||
ViewerAction.TogglePlay
|
||||
ViewerAction.Beat
|
||||
ViewerAction.Beat ]
|
||||
"actions")
|
||||
|
||||
testCase "autoplay: Space, then the clock steps one frame per 100 ms and stops at the end" (fun () ->
|
||||
let out =
|
||||
viewerStates 3 replayActions
|
||||
|> run [ Time 0L; KeyDown " "; Time 100L; Time 200L; Time 300L; Time 400L; Time 500L ]
|
||||
|
||||
Expect.equal (out |> List.map (fun v -> v.Index)) [ 0; 0; 1; 2; 3; 3; 3 ] "indices"
|
||||
|
||||
Expect.equal
|
||||
(out |> List.map (fun v -> v.Playing))
|
||||
[ false; true; true; true; false; false; false ]
|
||||
"playing")
|
||||
|
||||
testCase "without Space nothing plays: the clock alone never moves the viewer" (fun () ->
|
||||
Expect.all
|
||||
(viewerStates 3 replayActions |> run [ Time 0L; Time 1000L; Time 2000L ])
|
||||
(fun v -> v.Index = 0)
|
||||
"index 0")
|
||||
|
||||
testCase "replayMain asks for nothing and draws the first frame of the recording first" (fun () ->
|
||||
let traced = IO.trace 0u (replayMain sample)
|
||||
Expect.equal traced.Ops [] "ops"
|
||||
let drawings, _ = Stream.step (Time 0L) traced.Result
|
||||
Expect.equal (List.length drawings) 1 "one drawing"
|
||||
|
||||
Expect.equal
|
||||
(drawn (List.head drawings))
|
||||
(viewerView (frames sample) (viewerStart (List.length sample.Actions)))
|
||||
"first scene"
|
||||
|
||||
let start = View.view (Spawn.newGame sample.Seed)
|
||||
|
||||
Expect.equal
|
||||
(drawn (List.head drawings) |> List.truncate (List.length start))
|
||||
start
|
||||
"the game's own view first")
|
||||
|
||||
testCase "replayMain: End shows the last frame of the game, the game over" (fun () ->
|
||||
let out = (IO.trace 0u (replayMain sample)).Result |> run [ Time 0L; KeyDown "End" ]
|
||||
let fs = frames sample
|
||||
let last = List.length fs - 1
|
||||
let scene = drawn (List.last out)
|
||||
Expect.equal scene (viewerView fs { Index = last; Last = last; Playing = false }) "last scene"
|
||||
|
||||
Expect.exists
|
||||
scene
|
||||
(fun s ->
|
||||
match s with
|
||||
| Text(_, _, "Game Over", _, _, _) -> true
|
||||
| _ -> false)
|
||||
"Game Over")
|
||||
|
||||
testCase "stepping back shows exactly the earlier scene" (fun () ->
|
||||
let out =
|
||||
(IO.trace 0u (replayMain sample)).Result
|
||||
|> run
|
||||
[ Time 0L
|
||||
KeyDown "ArrowRight"
|
||||
KeyDown "ArrowRight"
|
||||
KeyDown "ArrowRight"
|
||||
KeyDown "ArrowLeft"
|
||||
KeyDown "d"
|
||||
KeyDown "a"
|
||||
KeyDown "a" ]
|
||||
|
||||
let scenes = List.map drawn out
|
||||
Expect.equal scenes[4] scenes[2] "back one"
|
||||
Expect.equal scenes[6] scenes[4] "forward and back"
|
||||
Expect.equal scenes[7] scenes[1] "back to frame 1"
|
||||
Expect.notEqual scenes[3] scenes[2] "frames differ")
|
||||
|
||||
testCase "replayMain: after ten seconds in a background tab autoplay moves 2 frames (250 ms), not 100" (fun () ->
|
||||
let _, afterPlay = Stream.step (KeyDown " ") (IO.trace 0u (replayMain sample)).Result
|
||||
let _, afterFirst = Stream.step (Time 0L) afterPlay
|
||||
Expect.equal (List.length (fst (Stream.step (Time 10000L) afterFirst))) 2 "two drawings") ]
|
||||
]
|
||||
Reference in new issue
Block a user