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>
84 lines
4.5 KiB
FSharp
84 lines
4.5 KiB
FSharp
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}")
|
|
]
|