feat(fsharp): m11 pong capstone (model, rules, view, app) with tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
038a375027
commit
1d0b8c3a5c
@@ -0,0 +1,93 @@
|
||||
/// 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
|
||||
| PongAction.Restart -> initialPong state.Seed
|
||||
| PongAction.Frame -> frame state
|
||||
Reference in New Issue
Block a user