Files
functional-programming-snake/fsharp/tests/M04State.fs
T
Lucas WintherandClaude Fable 5.2 edd57f0148 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>
2026-11-11 12:05:29 +01:00

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}")
]