mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +00:00
The IrLowerer's wide_hi map records "this u8 temp's high byte lives at this other temp" pairs whenever a 16-bit value is produced. Both lower_function and lower_handler reset next_temp to 0 at the start of each function, but neither cleared wide_hi — so stale (low_id -> high_id) entries from earlier functions leaked into subsequent ones. When a fresh function reused those temp IDs for unrelated u8 expressions, is_wide() returned spurious true and widen() handed back stale (lo, hi) pairs whose hi happened to coincide with the *next* temp ID fresh_temp() was about to allocate. The result was 16-bit IR ops (CmpEq16 in particular) where the destination temp aliased one of the source operand high bytes — for War this made `match phase` arms past P_WIN_B impossible to enter and the game would freeze with both face-up cards on the table forever. Fix: clear wide_hi alongside the next_temp reset in both lower_function and lower_handler. Adds a regression test (ir::tests::wide_hi_does_not_leak_between_functions) that constructs a function whose body has no u16 ops but follows a function that does, and asserts no CmpEq16 op aliases its dest with an operand high byte. Also: - Convert the war Playing state's phase machine from an if-chain to a `match`, which is what tripped this bug to the surface (it was lurking in earlier ROMs too but their layouts never produced the dest/source collision shape). - Refactor begin_draw_a/b to set fly_card / fly_face_up via globals before calling arm_fly, since arm_fly only takes 4 params (the v0.1 ABI limit, now diagnosed by E0506). - Hoist the P_RESOLVE comparison result to the global pf_result to dodge the param-clobbering issue documented in examples/war/COMPILER_BUGS.md §2. - Document the bug as item #6 in COMPILER_BUGS.md with a minimal repro and reproducer-test pointer. - Refresh the war golden + audio hash to match the new ROM. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
83 lines
4.2 KiB
Text
83 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
|
|
var pf_result: u8 = 0 // P_RESOLVE comparison result (1=A, 2=B, 0=tie)
|
|
|
|
// ── 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
|