feat(js): m11 pong capstone (model, rules, view, app, page) and polling smoke test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd7c5f2acf
commit
1d1d918805
16 files changed
+3090
-3
No files matched your search
@@ -0,0 +1,67 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
export const pongUpdate = (state, action) => {
|
||||
switch (action.tag) {
|
||||
case 'Press': return { ...state, held: { ...state.held, [action.key.toLowerCase()]: true } }
|
||||
case 'Release': return { ...state, held: { ...state.held, [action.key.toLowerCase()]: false } }
|
||||
case 'Restart': return initialPong(state.seed)
|
||||
case 'Frame': return frame(state)
|
||||
default: return state
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user