Files
functional-programming-snake/js/solutions/pong/rules.js
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

78 lines
3.0 KiB
JavaScript

// m11 pong rules: one pure `pongUpdate`, built from small steps that each return a new piece of state.
import { H, W, PADDLE, PLAYER_X, CPU_X, WIN, initialPong } from './model.js'
import { nextUint32 } from '../random.js'
/** @import { PongState, PongAction } from '../types' */
/** @type {(lo: number, hi: number, x: number) => number} */
export const clamp = (lo, hi, x) => Math.min(hi, Math.max(lo, x))
const clampPaddle = (y) => clamp(0, H - PADDLE, y)
const movePlayer = (state) =>
clampPaddle(state.player + (state.held.down ? 1 : 0) - (state.held.up ? 1 : 0))
// The cpu chases the middle of its paddle toward the ball, one cell every other frame.
const moveCpu = (state, frame) =>
frame % 2 === 0 ? clampPaddle(state.cpu + Math.sign(state.ball.y - PADDLE / 2 - state.cpu)) : state.cpu
const hits = (paddle, y) => paddle <= y && y < paddle + PADDLE
// Where the ball goes next, given the paddles' new positions. `scorer` says who got the point, if anyone.
const moveBall = (state, player, cpu) => {
const { ball, vel } = state
const wall = ball.y + vel.dy < 0 || ball.y + vel.dy >= H
const dy = wall ? -vel.dy : vel.dy
const ny = ball.y + dy
const nx = ball.x + vel.dx
const bounce =
(nx === PLAYER_X && vel.dx < 0 && hits(player, ny)) ||
(nx === CPU_X && vel.dx > 0 && hits(cpu, ny))
return {
ball: { x: bounce ? ball.x : nx, y: ny },
vel: { dx: bounce ? -vel.dx : vel.dx, dy },
scorer: nx < 0 ? 'cpu' : nx >= W ? 'player' : null,
}
}
// After a point the ball restarts in the middle, heading toward whoever conceded; the seed picks up or down.
const serve = (state, scorer) => {
const [r, seed] = nextUint32(state.seed)
const score = { ...state.score, [scorer]: state.score[scorer] + 1 }
return {
...state,
ball: { x: 40, y: 25 },
vel: { dx: scorer === 'cpu' ? -1 : 1, dy: r % 2 === 0 ? 1 : -1 },
score,
seed,
status: score[scorer] >= WIN ? 'Over' : 'Playing',
}
}
const frame = (state) => {
if (state.status !== 'Playing') return state
const n = state.frame + 1
const player = movePlayer(state)
const cpu = moveCpu(state, n)
const { scorer, ...moved } = moveBall(state, player, cpu)
const next = { ...state, ...moved, player, cpu, frame: n }
return scorer === null ? next : serve(next, scorer)
}
// Only the keys the model knows ('Up', 'Down') are held; anything else leaves the state alone.
const heldFlag = { Up: 'up', Down: 'down' }
const setHeld = (state, key, value) =>
Object.hasOwn(heldFlag, key) ? { ...state, held: { ...state.held, [heldFlag[key]]: value } } : state
/** @type {(state: PongState, action: PongAction) => PongState} */
export const pongUpdate = (state, action) => {
switch (action.tag) {
case 'Press': return setHeld(state, action.key, true)
case 'Release': return setHeld(state, action.key, false)
// A new match, but the keys are still where the player's fingers are.
case 'Restart': return { ...initialPong(state.seed), held: state.held }
case 'Frame': return frame(state)
default: return state
}
}