1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 00:45:38 +00:00
nescript/examples/pong/title_state.ne
Claude 21b91f6398
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

121 lines
4.1 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}
}
}