1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-13 02:59:36 +00:00
nescript/examples/pong/title_state.ne

122 lines
4.1 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/title_state.ne — the Title state.
//
// Draws "PONG" at the top of the screen, a 3-option menu (CPU VS
// CPU / 1 PLAYER / 2 PLAYERS) with a cursor, and a blinking
// "PRESS A" prompt below. If nothing happens for TITLE_AUTO_FRAMES
// the menu auto-commits to CPU VS CPU so the headless jsnes
// golden capture always reaches gameplay by frame 180.
state Title {
on enter {
title_cursor = 0 // default: CPU VS CPU (autopilot pick)
title_timer = 0
title_blink = 0
title_debounce = 0
start_music TitleTheme
}
on frame {
global_tick += 1
title_timer += 1
title_blink += 1
if title_blink >= 60 {
title_blink = 0
}
if title_debounce > 0 {
title_debounce -= 1
}
// ── "PONG" banner ─────────────────────────────────
//
// Until the BIG PONG banner lands in the polish
// milestone, we draw the name in the 8×8 font at
// roughly 2× scale by spacing letters 16 px apart. That
// reads as a title without eating a dozen extra tiles.
draw_letter(88, 40, 15) // P
draw_letter(104, 40, 14) // O
draw_letter(120, 40, 13) // N
draw_letter(136, 40, 6) // G
// ── Menu options ──────────────────────────────────
//
// Three vertically-stacked rows. Row 0 = CPU VS CPU,
// row 1 = 1 PLAYER, row 2 = 2 PLAYER (no S — plural is
// implied, and dropping the S keeps each row short
// enough that the cursor sprite + row fits under the
// sprites-per-scanline budget).
//
// Row 0: "CPU VS CPU"
draw_word_cpu(80, 96)
draw_word_vs (112, 96)
draw_word_cpu(136, 96)
// Row 1: "1 PLAYER"
draw_digit(88, 120, 1)
draw_word_player(104, 120)
// Row 2: "2 PLAYER"
draw_digit(88, 144, 2)
draw_word_player(104, 144)
// Cursor sits to the left of the selected option, at
// y = 96 / 120 / 144 depending on title_cursor.
var cursor_y: u8 = 96 + (title_cursor << 3) + (title_cursor << 4) // +24 per row
draw Tileset at: (64, cursor_y) frame: TILE_CURSOR
// ── Blinking "PRESS A" prompt ─────────────────────
if title_blink < 30 {
draw_word_press(96, 184)
draw_letter(144, 184, 0) // A
}
// ── Input handling ────────────────────────────────
if title_debounce == 0 {
if button.up {
if title_cursor > 0 {
title_cursor -= 1
}
title_debounce = 10
title_timer = 0
play PaddleHit
}
if button.down {
if title_cursor < 2 {
title_cursor += 1
}
title_debounce = 10
title_timer = 0
play PaddleHit
}
if button.a or button.start {
mode = title_cursor
rng_seed((global_tick & 0xFF) as u8)
transition Playing
}
}
// ── Autopilot for the headless harness ────────────
if title_timer >= TITLE_AUTO_FRAMES {
mode = MODE_CPU_VS_CPU
title_cursor = 0
rng_seed((global_tick & 0xFF) as u8)
transition Playing
}
}
on exit {
stop_music
// Translate the menu selection into per-side is_cpu flags
// the playing state reads each frame.
if mode == MODE_CPU_VS_CPU {
is_cpu[0] = 1
is_cpu[1] = 1
}
if mode == MODE_HUMAN_VS_CPU {
is_cpu[0] = 0
is_cpu[1] = 1
}
if mode == MODE_HUMAN_VS_HUMAN {
is_cpu[0] = 0
is_cpu[1] = 0
}
}
}