/// 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