1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/examples/war/title_state.ne
Claude 8ababdcec4
examples/war: working end-to-end War card game
A complete, playable port of the card game War: title screen with
0/1/2 player menu, animated deal, sliding cards, deck-count HUD, a
"WAR!" tie-break with buried cards, and a victory screen with a
fanfare. Source split across examples/war/*.ne (constants, assets,
audio, deck/queue logic, RNG, render helpers, and one state file
per game state) and pulled in via examples/war.ne.

Drives nearly every NEScript subsystem at once: custom 88-tile
sprite sheet (card frames, ranks, suits, font, BIG WAR letters);
felt background nametable; pulse-1 / pulse-2 / noise sfx; looping
march on pulse 2; an 8-bit Galois LFSR PRNG; queue-based decks
that conserve cards across rounds; a phase machine inside the
Playing state that handles draw/reveal/win/war/check; and an
autopilot that boots straight into 0-PLAYERS mode so the headless
jsnes harness captures real gameplay at frame 180.

While building this I uncovered five compiler bugs / limitations
in the v0.1 implementation; each is documented with a minimal
reproduction, root cause, current workaround, and proposed fix in
examples/war/COMPILER_BUGS.md. The most painful was the
parameter-VarId aliasing one (#1b) — two functions sharing a
parameter NAME end up sharing a single zero-page slot mapping
across the whole program. Once those compiler bugs are fixed, the
workarounds in war/*.ne should be reverted in the same PR.

https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 15:22:20 +00:00

144 lines
5.2 KiB
Text

// war/title_state.ne — the Title state.
//
// Draws a big "WAR" banner, a 3-option menu with a cursor, and a
// blinking "PRESS A" prompt. Autopilot: if the player doesn't
// press anything for TITLE_AUTO_FRAMES frames the menu auto-
// commits to "0 PLAYERS" so the jsnes golden capture reaches
// gameplay by frame 180.
//
// The cursor navigates with D-pad up/down; A or Start confirms.
// Debouncing is done via title_debounce — one key press per
// press rather than per frame held.
state Title {
on enter {
title_cursor = 1
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
}
// ── Big "WAR" title banner ───────────────────────
// Each letter is a 2x2 block of 16x16 BIG tiles.
// Three letters at 16px wide + 4px gap = 16*3 + 4*2 = 56px;
// centred at x = (256 - 56)/2 = 100.
// y = 32 puts the banner in the top third of the screen.
// BIG W
draw Tileset at: (100, 32) frame: TILE_BIG_W_TL
draw Tileset at: (108, 32) frame: TILE_BIG_W_TR
draw Tileset at: (100, 40) frame: TILE_BIG_W_BL
draw Tileset at: (108, 40) frame: TILE_BIG_W_BR
// BIG A
draw Tileset at: (120, 32) frame: TILE_BIG_A_TL
draw Tileset at: (128, 32) frame: TILE_BIG_A_TR
draw Tileset at: (120, 40) frame: TILE_BIG_A_BL
draw Tileset at: (128, 40) frame: TILE_BIG_A_BR
// BIG R
draw Tileset at: (140, 32) frame: TILE_BIG_R_TL
draw Tileset at: (148, 32) frame: TILE_BIG_R_TR
draw Tileset at: (140, 40) frame: TILE_BIG_R_BL
draw Tileset at: (148, 40) frame: TILE_BIG_R_BR
// Subtitle "CARD GAME" in 8x8 font under the banner.
// 9 letters incl. the embedded space → 9 sprites per row,
// which over-runs the 8-per-scanline limit. Splitting the
// subtitle across two y rows (offset by 8px) keeps each
// scanline under the limit.
draw_letter(96, 64, 2) // C
draw_letter(104, 64, 0) // A
draw_letter(112, 64, 17) // R
draw_letter(120, 64, 3) // D
draw_letter(136, 64, 6) // G
draw_letter(144, 64, 0) // A
draw_letter(152, 64, 12) // M
draw_letter(160, 64, 4) // E
// ── Menu options ─────────────────────────────────
// Three lines vertically stacked. Each row is "X PLAYER"
// (no S — plural is implied) so the row never exceeds 7
// sprites and stays under the per-scanline limit even
// when the cursor sprite shares the same row.
draw_digit(88, 104, 0)
draw_word_player(104, 104)
draw_digit(88, 120, 1)
draw_word_player(104, 120)
draw_digit(88, 136, 2)
draw_word_player(104, 136)
// Cursor sits to the left of the selected option.
var cursor_y: u8 = 104 + (title_cursor << 4) // 104, 120, 136
draw Tileset at: (72, 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 FlipCard
}
if button.down {
if title_cursor < 2 {
title_cursor += 1
}
title_debounce = 10
title_timer = 0
play FlipCard
}
if button.a or button.start {
// Commit the selection and seed the RNG from the
// current global tick so each user-visible start
// produces a different shuffle.
mode = title_cursor
rng_seed((global_tick & 0xFF) as u8)
transition Deal
}
}
// ── Autopilot for the headless harness ───────────
if title_timer >= TITLE_AUTO_FRAMES {
mode = 0
rng_seed((global_tick & 0xFF) as u8)
transition Deal
}
}
on exit {
stop_music
// Translate the menu selection into the two a_is_cpu /
// b_is_cpu flags the play state reads each frame.
if mode == MODE_CPU_VS_CPU {
a_is_cpu = 1
b_is_cpu = 1
}
if mode == MODE_HUMAN_VS_CPU {
a_is_cpu = 0
b_is_cpu = 1
}
if mode == MODE_HUMAN_VS_HUMAN {
a_is_cpu = 0
b_is_cpu = 0
}
}
}