mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
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
162 lines
5.8 KiB
Text
162 lines
5.8 KiB
Text
// pong/render.ne — drawing helpers.
|
|
//
|
|
// Everything on-screen that isn't a static background tile goes
|
|
// through one of these functions. The idea is the same as war:
|
|
// the phase handlers stay readable and the sprite-budgeting math
|
|
// all lives in one file.
|
|
|
|
// ── Letters and digits ───────────────────────────────────
|
|
|
|
// Draw a single 'A'..'Z' glyph at (x, y). `ch` is the letter
|
|
// index (0 = A, 25 = Z).
|
|
fun draw_letter(x: u8, y: u8, ch: u8) {
|
|
draw Tileset at: (x, y) frame: TILE_LETTER_BASE + ch
|
|
}
|
|
|
|
// Draw a single digit 0..9 at (x, y).
|
|
fun draw_digit(x: u8, y: u8, d: u8) {
|
|
draw Tileset at: (x, y) frame: TILE_DIGIT_BASE + d
|
|
}
|
|
|
|
// Two-digit score display (0..99) at (x, y). Leading zero is
|
|
// preserved so the HUD never shifts as the score ticks up.
|
|
fun draw_count(x: u8, y: u8, v: u8) {
|
|
var tens: u8 = 0
|
|
var n: u8 = v
|
|
while n >= 10 {
|
|
n -= 10
|
|
tens += 1
|
|
}
|
|
draw_digit(x, y, tens)
|
|
draw_digit(x + 8, y, n)
|
|
}
|
|
|
|
// ── Words ────────────────────────────────────────────────
|
|
|
|
// "PRESS" — used on the title's blinking prompt.
|
|
fun draw_word_press(x: u8, y: u8) {
|
|
draw_letter(x, y, 15) // P
|
|
draw_letter(x + 8, y, 17) // R
|
|
draw_letter(x + 16, y, 4) // E
|
|
draw_letter(x + 24, y, 18) // S
|
|
draw_letter(x + 32, y, 18) // S
|
|
}
|
|
|
|
// "PONG" — used on the title screen and the victory banner until
|
|
// we wire up the BIG PONG banner in the polish milestone.
|
|
fun draw_word_pong(x: u8, y: u8) {
|
|
draw_letter(x, y, 15) // P
|
|
draw_letter(x + 8, y, 14) // O
|
|
draw_letter(x + 16, y, 13) // N
|
|
draw_letter(x + 24, y, 6) // G
|
|
}
|
|
|
|
// "CPU" — used on the title menu options.
|
|
fun draw_word_cpu(x: u8, y: u8) {
|
|
draw_letter(x, y, 2) // C
|
|
draw_letter(x + 8, y, 15) // P
|
|
draw_letter(x + 16, y, 20) // U
|
|
}
|
|
|
|
// "VS" — used in the CPU-vs-CPU title line.
|
|
fun draw_word_vs(x: u8, y: u8) {
|
|
draw_letter(x, y, 21) // V
|
|
draw_letter(x + 8, y, 18) // S
|
|
}
|
|
|
|
// "PLAYER" — used on the title menu and the victory banner.
|
|
fun draw_word_player(x: u8, y: u8) {
|
|
draw_letter(x, y, 15) // P
|
|
draw_letter(x + 8, y, 11) // L
|
|
draw_letter(x + 16, y, 0) // A
|
|
draw_letter(x + 24, y, 24) // Y
|
|
draw_letter(x + 32, y, 4) // E
|
|
draw_letter(x + 40, y, 17) // R
|
|
}
|
|
|
|
// "WINS" — used on the victory banner.
|
|
fun draw_word_wins(x: u8, y: u8) {
|
|
draw_letter(x, y, 22) // W
|
|
draw_letter(x + 8, y, 8) // I
|
|
draw_letter(x + 16, y, 13) // N
|
|
draw_letter(x + 24, y, 18) // S
|
|
}
|
|
|
|
// ── Center-line divider ──────────────────────────────────
|
|
//
|
|
// Classic dashed Pong divider at x = 124. We draw 7 dashes at
|
|
// y = 24, 56, 88, 120, 152, 184, 216 (every 32 px), which is
|
|
// always one sprite per scanline — safely under the per-scanline
|
|
// budget.
|
|
fun draw_center_line() {
|
|
draw Tileset at: (124, 24) frame: TILE_CENTER_DASH
|
|
draw Tileset at: (124, 56) frame: TILE_CENTER_DASH
|
|
draw Tileset at: (124, 88) frame: TILE_CENTER_DASH
|
|
draw Tileset at: (124, 120) frame: TILE_CENTER_DASH
|
|
draw Tileset at: (124, 152) frame: TILE_CENTER_DASH
|
|
draw Tileset at: (124, 184) frame: TILE_CENTER_DASH
|
|
draw Tileset at: (124, 216) frame: TILE_CENTER_DASH
|
|
}
|
|
|
|
// ── HUD scores ───────────────────────────────────────────
|
|
fun draw_scores() {
|
|
draw_count(SCORE_LEFT_X, SCORE_Y, score[0])
|
|
draw_count(SCORE_RIGHT_X, SCORE_Y, score[1])
|
|
}
|
|
|
|
// ── Paddles ──────────────────────────────────────────────
|
|
//
|
|
// Draw the paddle on the given side at its current y. Side 0 =
|
|
// left (x = LEFT_PADDLE_X), side 1 = right (x = RIGHT_PADDLE_X).
|
|
// Normal paddles are 3 tiles tall (top, mid, bot). Long paddles
|
|
// are 5 tiles tall (top, mid, mid, mid, bot). The height choice
|
|
// is driven by `paddle_long[side]` — nonzero = extended.
|
|
fun draw_paddle(side: u8) {
|
|
var px: u8 = LEFT_PADDLE_X
|
|
if side == SIDE_RIGHT {
|
|
px = RIGHT_PADDLE_X
|
|
}
|
|
var py: u8 = paddle_y[side]
|
|
draw Tileset at: (px, py) frame: TILE_PADDLE_TOP
|
|
if paddle_long[side] > 0 {
|
|
// 5-tile long paddle (40 px).
|
|
draw Tileset at: (px, py + 8) frame: TILE_PADDLE_MID
|
|
draw Tileset at: (px, py + 16) frame: TILE_PADDLE_MID
|
|
draw Tileset at: (px, py + 24) frame: TILE_PADDLE_MID
|
|
draw Tileset at: (px, py + 32) frame: TILE_PADDLE_BOT
|
|
} else {
|
|
// 3-tile normal paddle (24 px).
|
|
draw Tileset at: (px, py + 8) frame: TILE_PADDLE_MID
|
|
draw Tileset at: (px, py + 16) frame: TILE_PADDLE_BOT
|
|
}
|
|
}
|
|
|
|
// Draw both paddles in one call. Used by every gameplay phase.
|
|
fun draw_paddles() {
|
|
draw_paddle(SIDE_LEFT)
|
|
draw_paddle(SIDE_RIGHT)
|
|
}
|
|
|
|
// ── Balls ────────────────────────────────────────────────
|
|
//
|
|
// Draw every active ball in the ball_* arrays. Inactive slots
|
|
// are skipped.
|
|
fun draw_balls() {
|
|
var i: u8 = 0
|
|
while i < MAX_BALLS {
|
|
if ball_active[i] == 1 {
|
|
draw Tileset at: (ball_x[i], ball_y[i]) frame: TILE_BALL
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
// ── Powerup ──────────────────────────────────────────────
|
|
//
|
|
// Draw the on-screen powerup iff one is active. Tile index picks
|
|
// up the right icon via TILE_POWERUP_BASE + (kind - 1).
|
|
fun draw_powerup() {
|
|
if powerup_kind != PWR_NONE {
|
|
draw Tileset at: (powerup_x, powerup_y) frame: (TILE_POWERUP_BASE - 1) + powerup_kind
|
|
}
|
|
}
|