mirror of
https://github.com/imjasonh/nescript
synced 2026-07-10 01:37:45 +00:00
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
73 lines
2.5 KiB
Text
73 lines
2.5 KiB
Text
// war/victory_state.ne — the Victory state.
|
|
//
|
|
// Big "PLAYER A WINS" or "PLAYER B WINS" banner, the builtin
|
|
// `fanfare` music, an auto-return to Title after
|
|
// VICTORY_LINGER_FRAMES frames, and a manual "press A to skip"
|
|
// path.
|
|
|
|
state Victory {
|
|
on enter {
|
|
victory_timer = 0
|
|
start_music fanfare
|
|
}
|
|
|
|
on frame {
|
|
global_tick += 1
|
|
victory_timer += 1
|
|
|
|
// ── Banner ──────────────────────────────────────
|
|
// "PLAYER X" on row 1, "WINS" on row 2. Splitting the
|
|
// banner across two y rows keeps each scanline under the
|
|
// NES's 8-sprite-per-scanline limit (the "PLAYER X WINS"
|
|
// single-line version was 11 sprites tall and dropped
|
|
// letters past the 8th).
|
|
draw_word_player(64, 80)
|
|
if winner == 0 {
|
|
draw_letter(120, 80, 0) // A
|
|
} else {
|
|
draw_letter(120, 80, 1) // B
|
|
}
|
|
draw_word_wins(96, 96)
|
|
|
|
// Flourish: draw the winning deck's top card in the
|
|
// middle of the screen as a victory showcase. If the
|
|
// deck is empty (shouldn't happen — we only arrive
|
|
// here because the *loser* is empty) fall back to a
|
|
// card back so the layout is stable.
|
|
if winner == 0 {
|
|
if deck_a_count > 0 {
|
|
var vic_top_a: u8 = deck_a[deck_a_front]
|
|
draw_card_face(120, 128, vic_top_a)
|
|
} else {
|
|
draw_card_back(120, 128)
|
|
}
|
|
} else {
|
|
if deck_b_count > 0 {
|
|
var vic_top_b: u8 = deck_b[deck_b_front]
|
|
draw_card_face(120, 128, vic_top_b)
|
|
} else {
|
|
draw_card_back(120, 128)
|
|
}
|
|
}
|
|
|
|
// Row of tiny hearts under the banner.
|
|
draw Tileset at: (80, 168) frame: TILE_HEART_TINY
|
|
draw Tileset at: (96, 168) frame: TILE_HEART_TINY
|
|
draw Tileset at: (112, 168) frame: TILE_HEART_TINY
|
|
draw Tileset at: (128, 168) frame: TILE_HEART_TINY
|
|
draw Tileset at: (144, 168) frame: TILE_HEART_TINY
|
|
draw Tileset at: (160, 168) frame: TILE_HEART_TINY
|
|
|
|
// ── Input + auto-return ─────────────────────────
|
|
if button.a or button.start {
|
|
transition Title
|
|
}
|
|
if victory_timer >= VICTORY_LINGER_FRAMES {
|
|
transition Title
|
|
}
|
|
}
|
|
|
|
on exit {
|
|
stop_music
|
|
}
|
|
}
|