1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/examples/pong/ball.ne

289 lines
9.6 KiB
Text
Raw Normal View History

examples/pong: production-quality Pong game with powerups and multi-ball A complete, playable Pong game split across examples/pong/*.ne files and pulled in from a top-level examples/pong.ne. Features: - **Title screen** with a 3-option menu (CPU VS CPU / 1 PLAYER / 2 PLAYERS), a cursor sprite, blinking "PRESS A" prompt, brisk title march on pulse 2, and autopilot that auto-confirms CPU VS CPU after 45 frames of no input so the headless jsnes golden harness reaches gameplay by frame 180. - **Ball physics** with signed-magnitude velocity (u8 magnitude + sign bit per axis), wall bounce at top/bottom, paddle AABB collision with push-out, and score-out detection at left/right exits. - **Multi-ball** via parallel ball_* arrays (MAX_BALLS = 3). Each ball scores a point independently; the round continues until the last ball exits the playfield. - **CPU AI** that tracks the nearest active ball heading toward its side with a per-frame step, 4 px dead zone, and CPU_SPEED = 1 so rallies can end naturally. - **Three powerup types** that spawn every ~4 seconds, bounce off all four walls, and are caught by paddle AABB overlap: 1. LONG — extends the catching paddle from 24 → 40 px for 5 hits 2. FAST — doubles ball x-velocity on the catcher's next hit 3. MULTI — spawns two extra balls on the catcher's next hit - **Victory** at first-to-7 with a "PLAYER N WINS" banner and the builtin fanfare, auto-returning to Title. - **Audio**: 5 user-declared sfx (WallBounce, PaddleHit, Score, PowerSpawn, PowerCatch) plus a title march and the builtin fanfare for victory. Source layout mirrors examples/war: examples/pong.ne top-level game shell examples/pong/PLAN.md living design doc examples/pong/constants.ne layout + gameplay constants examples/pong/assets.ne 45-tile Tileset (paddles, ball, alphabet, digits, cursor, center-line, powerup icons) examples/pong/audio.ne sfx + music declarations examples/pong/state.ne all mutable globals examples/pong/rng.ne 8-bit Galois LFSR examples/pong/render.ne draw helpers examples/pong/input.ne paddle step (human + CPU AI) examples/pong/ball.ne multi-ball physics + paddle collision examples/pong/powerup.ne powerup entity (spawn, bounce, catch, apply) examples/pong/title_state.ne state Title + menu examples/pong/play_state.ne state Playing (P_SERVE/P_PLAY/P_POINT) examples/pong/victory_state.ne state Victory Verification: - 616 compiler unit tests pass (cargo test --all-targets) - cargo fmt / cargo clippy --all-targets -- -D warnings clean - 33/33 emulator harness goldens match - examples/pong.nes builds byte-identically from source https://claude.ai/code/session_0134F5uwDEVTes2Ee9S7JeXy
2026-04-16 01:25:29 +00:00
// pong/ball.ne — multi-ball physics.
//
// Balls live in parallel arrays indexed by slot (ball_x[i], etc.)
// so swapping between single-ball M3 and multi-ball M4 is purely a
// matter of which slots are active — the physics code is the
// same. Velocity is stored as (magnitude, sign) per axis because
// NEScript's u8 arithmetic can't express negatives directly; sign
// 0 = positive (+x right / +y down), sign 1 = negative.
//
// Update order inside update_ball:
// 1. Move the ball by its velocity
// 2. Bounce off top / bottom walls
// 3. Test paddle collisions (left + right)
// 4. Test score-out on left / right exits
//
// The `i` parameter is the ball slot, 0..MAX_BALLS-1.
// ── Serve ────────────────────────────────────────────────
//
// Reset ball slot 0 to the centre of the playfield with a fresh
// random velocity, aimed toward the `target_side` that just lost
// a point (so the serve is into the loser's side, classic Pong).
// Every other ball slot is cleared. Called on game start and
// after every scored point.
fun serve_ball(target_side: u8) {
// Wipe every slot first so M4's multi-ball leftovers never
// carry through a score.
var i: u8 = 0
while i < MAX_BALLS {
ball_active[i] = 0
i += 1
}
// Centre the primary ball.
ball_x[0] = 124
ball_y[0] = 112
ball_dx[0] = BALL_BASE_DX
ball_dy[0] = BALL_BASE_DY
// Horizontal direction is toward target_side. SIDE_LEFT = 0
// means the ball travels left toward the left paddle, so x
// sign = 1 (negative). Right paddle = sign 0.
if target_side == SIDE_LEFT {
ball_dx_sign[0] = 1
} else {
ball_dx_sign[0] = 0
}
// Vertical direction picked by a random bit to avoid the
// same rally pattern every serve. RNG LSB = 0 → +y, 1 → -y.
var r: u8 = rng_next()
ball_dy_sign[0] = r & 1
ball_active[0] = 1
}
// ── Score handling ───────────────────────────────────────
//
// Called when a ball exits the playfield past a paddle. The
// scoring side gets +1. With multi-ball, the round continues
// until ALL active balls are out. Only when the last ball exits
// does the phase machine drop into P_POINT for the serve pause.
fun on_score(scoring_side: u8) {
score[scoring_side] += 1
play Score
// Count remaining active balls (the exiting ball has already
// been marked inactive before this call).
var remaining: u8 = 0
var j: u8 = 0
while j < MAX_BALLS {
if ball_active[j] == 1 {
remaining += 1
}
j += 1
}
if remaining == 0 {
// Last ball out — enter the P_POINT pause. The
// serving_side records the scorer; P_POINT flips it so
// the serve aims at the loser.
serving_side = scoring_side
phase = P_POINT
phase_timer = 0
// Clear the powerup so it doesn't linger during the
// inter-round pause.
powerup_kind = PWR_NONE
powerup_cooldown = 0
}
}
// ── Multi-ball spawn on paddle hit ───────────────────────
//
// When paddle_multi[side] is set, a paddle hit spawns two extra
// balls at the hit point with mirrored y velocities. This
// function is called from within check_paddle_hit's hit branch.
// Returns immediately if no multi flag is set. The flag clears
// on use.
fun spawn_multi_balls(i: u8, side: u8) {
if paddle_multi[side] == 0 {
return
}
paddle_multi[side] = 0
// Find two free slots.
var slot1: u8 = 0
var slot2: u8 = 0
var found1: u8 = 0
var found2: u8 = 0
var k: u8 = 0
while k < MAX_BALLS {
if ball_active[k] == 0 {
if found1 == 0 {
slot1 = k
found1 = 1
} else {
if found2 == 0 {
slot2 = k
found2 = 1
}
}
}
k += 1
}
// Spawn each extra ball as a copy of the hit ball with a
// mirrored or offset y direction.
if found1 == 1 {
ball_active[slot1] = 1
ball_x[slot1] = ball_x[i]
ball_y[slot1] = ball_y[i]
ball_dx[slot1] = ball_dx[i]
ball_dy[slot1] = ball_dy[i]
ball_dx_sign[slot1] = ball_dx_sign[i]
// Mirror y sign from the source ball.
if ball_dy_sign[i] == 0 {
ball_dy_sign[slot1] = 1
} else {
ball_dy_sign[slot1] = 0
}
}
if found2 == 1 {
ball_active[slot2] = 1
ball_x[slot2] = ball_x[i]
ball_y[slot2] = ball_y[i]
ball_dx[slot2] = ball_dx[i]
ball_dy[slot2] = ball_dy[i]
ball_dx_sign[slot2] = ball_dx_sign[i]
// Same y direction as the source (so we have 3 distinct
// trajectories: original, mirrored, and parallel).
ball_dy_sign[slot2] = ball_dy_sign[i]
// Offset the y slightly to avoid all three landing on
// the same pixel.
if ball_y[slot2] + 6 < PLAYFIELD_BOTTOM {
ball_y[slot2] += 6
}
}
}
// ── Paddle collision ─────────────────────────────────────
//
// Classic AABB overlap test against the paddle on `side`. On a
// hit we flip the ball's x velocity sign, push the ball out of
// the paddle to avoid double-bounces, and play the hit sfx.
//
// Powerup effects consumed on hit:
// - LONG: paddle_long already decrements
// - FAST: paddle_fast → doubles ball_dx for the rest of this ball's life
// - MULTI: paddle_multi → spawns 2 extra balls at the hit point
fun check_paddle_hit(i: u8, side: u8) {
var px: u8 = LEFT_PADDLE_X
if side == SIDE_RIGHT {
px = RIGHT_PADDLE_X
}
var py: u8 = paddle_y[side]
var ph: u8 = PADDLE_H
if paddle_long[side] > 0 {
ph = PADDLE_H_LONG
}
var bx: u8 = ball_x[i]
var by: u8 = ball_y[i]
// Four-way AABB overlap test. The operands are small u8
// values so the additions can't overflow past 255.
if bx + BALL_SIZE > px and bx < px + PADDLE_W and by + BALL_SIZE > py and by < py + ph {
if side == SIDE_LEFT {
ball_x[i] = px + PADDLE_W
ball_dx_sign[i] = 0
} else {
ball_x[i] = px - BALL_SIZE
ball_dx_sign[i] = 1
}
// ── Consume long-paddle hit ──────────────────
if paddle_long[side] > 0 {
paddle_long[side] -= 1
}
// ── Consume fast-ball flag ───────────────────
if paddle_fast[side] > 0 {
paddle_fast[side] = 0
ball_dx[i] = BALL_FAST_DX
}
// ── Consume multi-ball flag ──────────────────
spawn_multi_balls(i, side)
play PaddleHit
}
}
// ── Per-ball update ──────────────────────────────────────
fun update_ball(i: u8) {
if ball_active[i] == 0 {
return
}
// ── 1. Move ──────────────────────────────────────
if ball_dx_sign[i] == 0 {
ball_x[i] += ball_dx[i]
} else {
ball_x[i] -= ball_dx[i]
}
if ball_dy_sign[i] == 0 {
ball_y[i] += ball_dy[i]
} else {
ball_y[i] -= ball_dy[i]
}
// ── 2. Wall bounce ───────────────────────────────
//
// Top: ball moving up and the top edge is now above the
// playfield (u8 comparison, safe because PLAYFIELD_TOP = 16
// and ball speeds are 1-2 so ball_y can't wrap around).
if ball_dy_sign[i] == 1 {
if ball_y[i] < PLAYFIELD_TOP {
ball_y[i] = PLAYFIELD_TOP
ball_dy_sign[i] = 0
play WallBounce
}
}
// Bottom: ball moving down and the bottom edge has crossed
// PLAYFIELD_BOTTOM.
if ball_dy_sign[i] == 0 {
if ball_y[i] + BALL_SIZE > PLAYFIELD_BOTTOM {
ball_y[i] = PLAYFIELD_BOTTOM - BALL_SIZE
ball_dy_sign[i] = 1
play WallBounce
}
}
// ── 3. Paddle collision ──────────────────────────
//
// Only check the paddle the ball is travelling toward.
// This is both faster and avoids the edge case of the
// ball hitting a paddle moving away from it.
if ball_dx_sign[i] == 1 {
check_paddle_hit(i, SIDE_LEFT)
} else {
check_paddle_hit(i, SIDE_RIGHT)
}
// ── 4. Score-out ─────────────────────────────────
//
// "ball_x < 8" / "ball_x > 240" is safe because paddles
// live at x = 16 and x = 232 — a ball that has moved past
// the paddle edge with max speed 2 lands at ball_x ∈ [6, 14]
// or [234, 242] before the next frame, never close enough
// to 0 or 255 to wrap the u8.
if ball_dx_sign[i] == 1 {
if ball_x[i] < 8 {
ball_active[i] = 0
on_score(SIDE_RIGHT)
}
} else {
if ball_x[i] > 240 {
ball_active[i] = 0
on_score(SIDE_LEFT)
}
}
}
// ── Sweep every active ball ──────────────────────────────
fun step_balls() {
var i: u8 = 0
while i < MAX_BALLS {
update_ball(i)
i += 1
}
}