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,83 @@
|
||||
module Snake.Tests.M04State
|
||||
|
||||
open Expecto
|
||||
open Snake
|
||||
open Snake.State
|
||||
|
||||
// Small initial states for the "behaves like" checks: two States behave alike when they give the same pair from each.
|
||||
let private starts = [ 0; 1; 7; 42 ]
|
||||
|
||||
let private sameOnAll (st1: State<int, 'a>) (st2: State<int, 'a>) : unit =
|
||||
for s in starts do
|
||||
Expect.equal (State.run s st1) (State.run s st2) $"from state {s}"
|
||||
|
||||
// A counter step: gives the current number times 10 and adds 1 to the state. A function of unit, so that a stub
|
||||
// that throws fails each test on its own instead of the whole file at load time.
|
||||
let private step () : State<int, int> =
|
||||
State.get () |> State.bind (fun n -> State.put (n + 1) |> State.map (fun () -> n * 10))
|
||||
|
||||
let private f (a: int) : State<int, int> = State.modify (fun s -> s * 2) |> State.map (fun () -> a + 1)
|
||||
let private g (b: int) : State<int, int * int> = State.get () |> State.bind (fun s -> State.pure (b, s))
|
||||
|
||||
[<Tests>]
|
||||
let tests =
|
||||
testList "M04 State" [
|
||||
testCase "State pure gives its value and leaves the state alone" (fun () ->
|
||||
Expect.equal (State.run 99 (State.pure "x")) ("x", 99) "pure")
|
||||
|
||||
testCase "State map applies f to the value and keeps the state from the underlying step" (fun () ->
|
||||
Expect.equal (State.run 4 (State.map (fun v -> v + 1) (step ()))) (41, 5) "map")
|
||||
|
||||
testCase "State bind threads the state through both steps" (fun () ->
|
||||
let both = step () |> State.bind (fun a -> step () |> State.map (fun b -> (a, b)))
|
||||
Expect.equal (State.run 4 both) ((40, 50), 6) "bind")
|
||||
|
||||
testCase "State get gives the state as the value and leaves it alone" (fun () ->
|
||||
Expect.equal (State.run "board" (State.get ())) ("board", "board") "get")
|
||||
|
||||
testCase "State put replaces the state and gives unit" (fun () ->
|
||||
Expect.equal (State.run 99 (State.put 5)) ((), 5) "put")
|
||||
|
||||
testCase "State modify applies f to the state and gives unit" (fun () ->
|
||||
Expect.equal (State.run 21 (State.modify (fun s -> s * 2))) ((), 42) "modify")
|
||||
|
||||
testCase "State law: left identity, bind f (pure a) behaves like f a" (fun () ->
|
||||
sameOnAll (State.pure 3 |> State.bind f) (f 3))
|
||||
|
||||
testCase "State law: right identity, bind pure m behaves like m" (fun () ->
|
||||
sameOnAll (step () |> State.bind State.pure) (step ()))
|
||||
|
||||
testCase "State law: associativity, the grouping of bind does not matter" (fun () ->
|
||||
sameOnAll (step () |> State.bind f |> State.bind g) (step () |> State.bind (fun a -> f a |> State.bind g)))
|
||||
|
||||
testCase "State a counter hands out fresh labels in order" (fun () ->
|
||||
let fresh = State.get () |> State.bind (fun n -> State.put (n + 1) |> State.map (fun () -> $"L{n}"))
|
||||
|
||||
let three =
|
||||
fresh
|
||||
|> State.bind (fun a -> fresh |> State.bind (fun b -> fresh |> State.map (fun c -> [ a; b; c ])))
|
||||
|
||||
Expect.equal (State.run 0 three) ([ "L0"; "L1"; "L2" ], 3) "from 0"
|
||||
Expect.equal (State.run 7 three) ([ "L7"; "L8"; "L9" ], 10) "from 7")
|
||||
|
||||
testCase "State Random is State with the seed as S: the dice give the same values and seeds" (fun () ->
|
||||
// One draw written only with State's get and put: read the seed, compute, store the next seed.
|
||||
let draw: State<uint32, uint32> =
|
||||
State.get ()
|
||||
|> State.bind (fun seed ->
|
||||
let v, next = Random.run seed Random.nextUint32
|
||||
State.put next |> State.map (fun () -> v))
|
||||
|
||||
// The same draw, by turning a Random step into a State by hand.
|
||||
let wrapped: State<uint32, uint32> = State(fun seed -> Random.run seed Random.nextUint32)
|
||||
let d6State = draw |> State.map (fun v -> 1u + v % 6u)
|
||||
let twoDiceState = d6State |> State.bind (fun a -> d6State |> State.map (fun b -> a + b))
|
||||
let d6Random = Random.nextUint32 |> Random.map (fun v -> 1u + v % 6u)
|
||||
let twoDiceRandom = d6Random |> Random.bind (fun a -> d6Random |> Random.map (fun b -> a + b))
|
||||
|
||||
for seed in [ 0u; 1u; 42u; 4294967295u ] do
|
||||
Expect.equal (State.run seed draw) (Random.run seed Random.nextUint32) $"draw from {seed}"
|
||||
Expect.equal (State.run seed wrapped) (Random.run seed Random.nextUint32) $"wrapped from {seed}"
|
||||
Expect.equal (State.run seed d6State) (Random.run seed d6Random) $"d6 from {seed}"
|
||||
Expect.equal (State.run seed twoDiceState) (Random.run seed twoDiceRandom) $"twoDice from {seed}")
|
||||
]
|
||||
Reference in new issue
Block a user