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,68 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { pure, map, flatMap, get, put, modify, run } from '../src/state.js'
|
||||
import * as Random from '../src/random.js'
|
||||
|
||||
// Small initial states for the "behaves like" checks: two States behave alike when they give the same pair from each.
|
||||
const starts = [0, 1, 7, 42]
|
||||
const sameOnAll = (st1, st2) => starts.forEach((s) => assert.deepStrictEqual(run(st1, s), run(st2, s), `from state ${s}`))
|
||||
|
||||
// A counter step: gives the current number times 10 and adds 1 to the state. Built inside the tests, so that a stub
|
||||
// that throws fails each test on its own instead of the whole file at load time.
|
||||
const step = () => flatMap(get, (n) => map(put(n + 1), () => n * 10))
|
||||
|
||||
test('m04 State pure gives its value and leaves the state alone', () =>
|
||||
assert.deepStrictEqual(run(pure('x'), 99), ['x', 99]))
|
||||
|
||||
test('m04 State map applies f to the value and keeps the state from the underlying step', () =>
|
||||
assert.deepStrictEqual(run(map(step(), (v) => v + 1), 4), [41, 5]))
|
||||
|
||||
test('m04 State flatMap threads the state through both steps', () =>
|
||||
assert.deepStrictEqual(run(flatMap(step(), (a) => map(step(), (b) => [a, b])), 4), [[40, 50], 6]))
|
||||
|
||||
test('m04 State get gives the state as the value and leaves it alone', () =>
|
||||
assert.deepStrictEqual(run(get, 'board'), ['board', 'board']))
|
||||
|
||||
test('m04 State put replaces the state and gives undefined', () =>
|
||||
assert.deepStrictEqual(run(put(5), 99), [undefined, 5]))
|
||||
|
||||
test('m04 State modify applies f to the state and gives undefined', () =>
|
||||
assert.deepStrictEqual(run(modify((s) => s * 2), 21), [undefined, 42]))
|
||||
|
||||
test('m04 State law: left identity, flatMap(pure(a), f) behaves like f(a)', () => {
|
||||
const f = (a) => map(modify((s) => s * 2), () => a + 1)
|
||||
sameOnAll(flatMap(pure(3), f), f(3))
|
||||
})
|
||||
|
||||
test('m04 State law: right identity, flatMap(m, pure) behaves like m', () =>
|
||||
sameOnAll(flatMap(step(), pure), step()))
|
||||
|
||||
test('m04 State law: associativity, the grouping of flatMap does not matter', () => {
|
||||
const f = (a) => map(modify((s) => s * 2), () => a + 1)
|
||||
const g = (b) => flatMap(get, (s) => pure([b, s]))
|
||||
sameOnAll(flatMap(flatMap(step(), f), g), flatMap(step(), (a) => flatMap(f(a), g)))
|
||||
})
|
||||
|
||||
test('m04 State a counter hands out fresh labels in order', () => {
|
||||
const fresh = flatMap(get, (n) => map(put(n + 1), () => `L${n}`))
|
||||
const three = flatMap(fresh, (a) => flatMap(fresh, (b) => map(fresh, (c) => [a, b, c])))
|
||||
assert.deepStrictEqual(run(three, 0), [['L0', 'L1', 'L2'], 3])
|
||||
assert.deepStrictEqual(run(three, 7), [['L7', 'L8', 'L9'], 10])
|
||||
})
|
||||
|
||||
test('m04 State Random is State with the seed as S: the dice give the same values and seeds', () => {
|
||||
// One draw written only with State's get and put: read the seed, compute, store the next seed.
|
||||
const draw = flatMap(get, (seed) => {
|
||||
const [v, next] = Random.run(Random.nextUint32, seed)
|
||||
return map(put(next), () => v)
|
||||
})
|
||||
const d6State = map(draw, (v) => 1 + (v % 6))
|
||||
const twoDiceState = flatMap(d6State, (a) => map(d6State, (b) => a + b))
|
||||
const d6Random = Random.map(Random.nextUint32, (v) => 1 + (v % 6))
|
||||
const twoDiceRandom = Random.flatMap(d6Random, (a) => Random.map(d6Random, (b) => a + b))
|
||||
;[0, 1, 42, 4294967295].forEach((seed) => {
|
||||
assert.deepStrictEqual(run(draw, seed), Random.run(Random.nextUint32, seed), `draw from ${seed}`)
|
||||
assert.deepStrictEqual(run(d6State, seed), Random.run(d6Random, seed), `d6 from ${seed}`)
|
||||
assert.deepStrictEqual(run(twoDiceState, seed), Random.run(twoDiceRandom, seed), `twoDice from ${seed}`)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user