Files
functional-programming-snake/fsharp/solutions/Pong/Rules.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

95 lines
3.5 KiB
FSharp

/// m11 pong rules: one pure `pongUpdate`, built from small steps that each return a new piece of state.
module Snake.Pong.Rules
open Snake
open Snake.Model
open Snake.Pong.Model
let clamp (lo: int) (hi: int) (x: int) : int = min hi (max lo x)
let private clampPaddle y = clamp 0 (H - PADDLE) y
let private movePlayer (state: PongState) : int =
clampPaddle (state.Player + (if state.Held.Down then 1 else 0) - (if state.Held.Up then 1 else 0))
// The cpu chases the middle of its paddle toward the ball, one cell every other frame.
let private moveCpu (state: PongState) (frame: int) : int =
if frame % 2 = 0 then
clampPaddle (state.Cpu + sign (state.Ball.Y - PADDLE / 2 - state.Cpu))
else
state.Cpu
let private hits (paddle: int) (y: int) = paddle <= y && y < paddle + PADDLE
type private Scorer =
| PlayerScored
| CpuScored
// Where the ball goes next, given the paddles' new positions, and who scored, if anyone.
let private moveBall (state: PongState) (player: int) (cpu: int) : Point * Velocity * Scorer option =
let ball, vel = state.Ball, state.Vel
let wall = ball.Y + vel.Dy < 0 || ball.Y + vel.Dy >= H
let dy = if wall then -vel.Dy else vel.Dy
let ny = ball.Y + dy
let nx = ball.X + vel.Dx
let bounce =
(nx = PLAYER_X && vel.Dx < 0 && hits player ny) || (nx = CPU_X && vel.Dx > 0 && hits cpu ny)
let scorer =
if nx < 0 then Some CpuScored
elif nx >= W then Some PlayerScored
else None
({ X = (if bounce then ball.X else nx); Y = ny }, { Dx = (if bounce then -vel.Dx else vel.Dx); Dy = dy }, scorer)
// After a point the ball restarts in the middle, heading toward whoever conceded; the seed picks up or down.
let private serve (state: PongState) (scorer: Scorer) : PongState =
let r, seed = Random.run state.Seed Random.nextUint32
let score =
match scorer with
| PlayerScored -> { state.Score with Player = state.Score.Player + 1 }
| CpuScored -> { state.Score with Cpu = state.Score.Cpu + 1 }
{ state with
Ball = { X = 40; Y = 25 }
Vel = { Dx = (if scorer = CpuScored then -1 else 1); Dy = (if r % 2u = 0u then 1 else -1) }
Score = score
Seed = seed
Status = if max score.Player score.Cpu >= WIN then PongStatus.Over else PongStatus.Playing }
let private frame (state: PongState) : PongState =
if state.Status <> PongStatus.Playing then
state
else
let n = state.Frame + 1
let player = movePlayer state
let cpu = moveCpu state n
let ball, vel, scorer = moveBall state player cpu
let next =
{ state with
Ball = ball
Vel = vel
Player = player
Cpu = cpu
Frame = n }
match scorer with
| Some side -> serve next side
| None -> next
let private setHeld (state: PongState) (key: Paddle) (value: bool) : PongState =
match key with
| Paddle.Up -> { state with Held = { state.Held with Up = value } }
| Paddle.Down -> { state with Held = { state.Held with Down = value } }
let pongUpdate (state: PongState) (action: PongAction) : PongState =
match action with
| PongAction.Press key -> setHeld state key true
| PongAction.Release key -> setHeld state key false
// A new match, but the keys are still where the player's fingers are.
| PongAction.Restart -> { initialPong state.Seed with Held = state.Held }
| PongAction.Frame -> frame state