// m11 pong rules: one pure `pongUpdate`, built from small steps that each return a new piece of state. import { H, W, PADDLE, PLAYER_X, CPU_X, WIN, initialPong } from './model.js' import { nextUint32 } from '../random.js' export const clamp = (lo, hi, x) => Math.min(hi, Math.max(lo, x)) const clampPaddle = (y) => clamp(0, H - PADDLE, y) const movePlayer = (state) => clampPaddle(state.player + (state.held.down ? 1 : 0) - (state.held.up ? 1 : 0)) // The cpu chases the middle of its paddle toward the ball, one cell every other frame. const moveCpu = (state, frame) => frame % 2 === 0 ? clampPaddle(state.cpu + Math.sign(state.ball.y - PADDLE / 2 - state.cpu)) : state.cpu const hits = (paddle, y) => paddle <= y && y < paddle + PADDLE // Where the ball goes next, given the paddles' new positions. `scorer` says who got the point, if anyone. const moveBall = (state, player, cpu) => { const { ball, vel } = state const wall = ball.y + vel.dy < 0 || ball.y + vel.dy >= H const dy = wall ? -vel.dy : vel.dy const ny = ball.y + dy const nx = ball.x + vel.dx const bounce = (nx === PLAYER_X && vel.dx < 0 && hits(player, ny)) || (nx === CPU_X && vel.dx > 0 && hits(cpu, ny)) return { ball: { x: bounce ? ball.x : nx, y: ny }, vel: { dx: bounce ? -vel.dx : vel.dx, dy }, scorer: nx < 0 ? 'cpu' : nx >= W ? 'player' : null, } } // After a point the ball restarts in the middle, heading toward whoever conceded; the seed picks up or down. const serve = (state, scorer) => { const [r, seed] = nextUint32(state.seed) const score = { ...state.score, [scorer]: state.score[scorer] + 1 } return { ...state, ball: { x: 40, y: 25 }, vel: { dx: scorer === 'cpu' ? -1 : 1, dy: r % 2 === 0 ? 1 : -1 }, score, seed, status: score[scorer] >= WIN ? 'Over' : 'Playing', } } const frame = (state) => { if (state.status !== 'Playing') return state const n = state.frame + 1 const player = movePlayer(state) const cpu = moveCpu(state, n) const { scorer, ...moved } = moveBall(state, player, cpu) const next = { ...state, ...moved, player, cpu, frame: n } return scorer === null ? next : serve(next, scorer) } // Only the keys the model knows ('Up', 'Down') are held; anything else leaves the state alone. const heldFlag = { Up: 'up', Down: 'down' } const setHeld = (state, key, value) => Object.hasOwn(heldFlag, key) ? { ...state, held: { ...state.held, [heldFlag[key]]: value } } : state export const pongUpdate = (state, action) => { switch (action.tag) { case 'Press': return setHeld(state, action.key, true) case 'Release': return setHeld(state, action.key, false) case 'Restart': return initialPong(state.seed) case 'Frame': return frame(state) default: return state } }