94 lines
3.4 KiB
FSharp
94 lines
3.4 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
|
|
| PongAction.Restart -> initialPong state.Seed
|
|
| PongAction.Frame -> frame state
|