mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +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
82 lines
4.2 KiB
Text
82 lines
4.2 KiB
Text
// war/state.ne — every mutable variable the game touches.
|
|
//
|
|
// NEScript allocates top-level `var` declarations in general RAM
|
|
// (the analyzer places them starting at $10 / $18 depending on
|
|
// whether the program declares background/palette updates). We
|
|
// keep everything global so helper functions can read and write
|
|
// without having to take parameters and return values — the
|
|
// 6502's 8-register ABI makes every extra parameter an extra
|
|
// LDA/STA pair.
|
|
|
|
// ── Decks + pot ───────────────────────────────────────────
|
|
// Each deck is a circular buffer backed by a 52-byte array.
|
|
// `*_front` is the index of the next card to draw; `*_count` is
|
|
// the number of cards currently in the buffer. Both fields wrap
|
|
// mod 52 — there's a helper for that in war/deck.ne.
|
|
var deck_a: u8[52]
|
|
var deck_a_front: u8 = 0
|
|
var deck_a_count: u8 = 0
|
|
|
|
var deck_b: u8[52]
|
|
var deck_b_front: u8 = 0
|
|
var deck_b_count: u8 = 0
|
|
|
|
// Pot holds every card currently in play (the normal-round draws
|
|
// and, during a war, the face-down buries too). Drained into the
|
|
// winner's deck after each round resolves.
|
|
var pot: u8[52]
|
|
var pot_count: u8 = 0
|
|
|
|
// ── The two cards showing face-up this round ─────────────
|
|
// These are the packed rank/suit bytes drawn from each deck at
|
|
// the start of a round. During a war, the old face-up cards get
|
|
// pushed into the pot and new values land in these slots.
|
|
var card_a: u8 = 0
|
|
var card_b: u8 = 0
|
|
|
|
// ── Game mode + phase machine ─────────────────────────────
|
|
var mode: u8 = 0 // 0/1/2 players
|
|
var a_is_cpu: u8 = 1 // bool-ish
|
|
var b_is_cpu: u8 = 1
|
|
var phase: u8 = 0 // one of the P_* constants
|
|
var phase_timer: u8 = 0 // counts up during each phase
|
|
|
|
// ── Animation state ───────────────────────────────────────
|
|
// Shared by every card-fly phase. We step (fly_x, fly_y) by a
|
|
// constant FLY_STEP per frame in the directions encoded in
|
|
// fly_dx_sign / fly_dy_sign. The screen layout is arranged so
|
|
// that FRAMES_FLY * FLY_STEP exactly matches the deck-to-play
|
|
// distance on both axes — see render.ne for the math.
|
|
var fly_x: u8 = 0
|
|
var fly_y: u8 = 0
|
|
var fly_dx_sign: u8 = 0 // 0 = +FLY_STEP / 1 = -FLY_STEP
|
|
var fly_dy_sign: u8 = 0
|
|
var fly_card: u8 = 0 // packed rank/suit to show during the fly
|
|
var fly_face_up: u8 = 1 // 0 = show card back, 1 = show face
|
|
|
|
// ── Title menu ────────────────────────────────────────────
|
|
var title_cursor: u8 = 1 // menu index (0/1/2) — default "1 PLAYER"
|
|
var title_timer: u8 = 0 // auto-advance counter
|
|
var title_debounce: u8 = 0 // menu input debounce
|
|
var title_blink: u8 = 0 // "PRESS A" blink counter
|
|
|
|
// ── Deal state ────────────────────────────────────────────
|
|
var deal_next: u8 = 0 // next card index to deal
|
|
var deal_timer: u8 = 0
|
|
|
|
// ── Victory state ─────────────────────────────────────────
|
|
var winner: u8 = 0 // 0 = A wins, 1 = B wins
|
|
var victory_timer: u8 = 0
|
|
|
|
// ── RNG ──────────────────────────────────────────────────
|
|
// 8-bit Galois LFSR state. Seeded from the free-running title
|
|
// frame counter at the moment the title screen transitions to
|
|
// Deal, so the shuffle is deterministic once the user commits
|
|
// to starting a game. The jsnes headless harness always starts
|
|
// at frame 45, so the seed is stable there too.
|
|
var rng_state: u8 = 0xA7
|
|
|
|
// ── Global free-running frame counter ────────────────────
|
|
// Used by any state that needs a coarse "which frame are we
|
|
// on" read without installing its own counter.
|
|
var global_tick: u16 = 0
|