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)