Files
functional-programming-snake/scala/solutions/pong/Rules.scala
T

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)