72 lines
3.2 KiB
Scala
72 lines
3.2 KiB
Scala
package snake
|
|
package pong
|
|
|
|
import PongModel.*
|
|
|
|
/** m11 pong rules: one pure `pongUpdate`, built from small steps that each return a new piece of state. */
|
|
object PongRules:
|
|
|
|
def clamp(lo: Int, hi: Int, x: Int): Int = math.min(hi, math.max(lo, x))
|
|
|
|
private def clampPaddle(y: Int): Int = clamp(0, H - PADDLE, y)
|
|
|
|
private def 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.
|
|
private def moveCpu(state: PongState, frame: Int): Int =
|
|
if frame % 2 == 0 then clampPaddle(state.cpu + (state.ball.y - PADDLE / 2 - state.cpu).sign)
|
|
else state.cpu
|
|
|
|
private def hits(paddle: Int, y: Int): Boolean = paddle <= y && y < paddle + PADDLE
|
|
|
|
private enum Side:
|
|
case Player, Cpu
|
|
|
|
// Where the ball goes next, given the paddles' new positions, and who scored, if anyone.
|
|
private def moveBall(state: PongState, player: Int, cpu: Int): (Point, Velocity, Option[Side]) =
|
|
val (ball, vel) = (state.ball, state.vel)
|
|
val wall = ball.y + vel.dy < 0 || ball.y + vel.dy >= H
|
|
val dy = if wall then -vel.dy else vel.dy
|
|
val ny = ball.y + dy
|
|
val nx = ball.x + vel.dx
|
|
val bounce =
|
|
(nx == PLAYER_X && vel.dx < 0 && hits(player, ny)) ||
|
|
(nx == CPU_X && vel.dx > 0 && hits(cpu, ny))
|
|
val scorer = if nx < 0 then Some(Side.Cpu) else if nx >= W then Some(Side.Player) else None
|
|
(Point(if bounce then ball.x else nx, ny), Velocity(if bounce then -vel.dx else vel.dx, dy), scorer)
|
|
|
|
// After a point the ball restarts in the middle, heading toward whoever conceded; the seed picks up or down.
|
|
private def serve(state: PongState, scorer: Side): PongState =
|
|
val (r, seed) = Random.nextUint32.run(state.seed)
|
|
val score = scorer match
|
|
case Side.Player => state.score.copy(player = state.score.player + 1)
|
|
case Side.Cpu => state.score.copy(cpu = state.score.cpu + 1)
|
|
state.copy(
|
|
ball = Point(40, 25),
|
|
vel = Velocity(if scorer == Side.Cpu then -1 else 1, if r % 2 == 0 then 1 else -1),
|
|
score = score,
|
|
seed = seed,
|
|
status = if math.max(score.player, score.cpu) >= WIN then PongStatus.Over else PongStatus.Playing
|
|
)
|
|
|
|
private def frame(state: PongState): PongState =
|
|
if state.status != PongStatus.Playing then state
|
|
else
|
|
val n = state.frame + 1
|
|
val player = movePlayer(state)
|
|
val cpu = moveCpu(state, n)
|
|
val (ball, vel, scorer) = moveBall(state, player, cpu)
|
|
val next = state.copy(ball = ball, vel = vel, player = player, cpu = cpu, frame = n)
|
|
scorer.fold(next)(serve(next, _))
|
|
|
|
private def setHeld(state: PongState, key: Paddle, value: Boolean): PongState = key match
|
|
case Paddle.Up => state.copy(held = state.held.copy(up = value))
|
|
case Paddle.Down => state.copy(held = state.held.copy(down = value))
|
|
|
|
def pongUpdate(state: PongState, action: PongAction): PongState = action match
|
|
case PongAction.Press(key) => setHeld(state, key, true)
|
|
case PongAction.Release(key) => setHeld(state, key, false)
|
|
case PongAction.Restart => initialPong(state.seed)
|
|
case PongAction.Frame => frame(state)
|