1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/examples/war/deck.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

188 lines
6 KiB
Text

// war/deck.ne — queue operations on the three u8[52] buffers.
//
// Each of deck_a, deck_b, and pot is a 52-entry circular buffer
// storing packed rank/suit bytes. The front index is the next
// card to draw; the count is the number of cards currently in
// the buffer. Everything wraps mod 52 — since 52 isn't a power
// of two, we use `if idx >= 52 { idx -= 52 }` instead of `%` to
// avoid the expensive software mod routine.
//
// NEScript v0.1 has a flat global symbol table for every `var`
// declaration (function-locals included), so each function's
// locals are prefixed with the function's short name to avoid
// E0501 collisions across the program. Function parameters ARE
// scoped per-function, so we can keep their names short.
// ── Helpers shared by every deck ──────────────────────────
//
// Wrap a (front + count) sum back into the 0..51 range. Only
// needed inside push_back because `front` and `count` are both
// ≤ 51 by construction, so the sum is at most 102 and one
// subtraction is enough.
//
// Every parameter name in this file is unique across the entire
// program. NEScript v0.1's IR lowering uses a single global
// var_map for parameter names, so two functions both named
// `wrap52(v: u8)` and `another(v: u8)` end up sharing a VarId
// and the codegen routes their parameter reads to whichever
// zero-page slot the LAST function to be lowered claimed. See
// COMPILER_BUGS.md §1b for the gory details.
inline fun wrap52(w52_v: u8) -> u8 {
if w52_v >= DECK_SIZE {
return w52_v - DECK_SIZE
}
return w52_v
}
// ── deck_a ────────────────────────────────────────────────
fun deck_a_empty() -> u8 {
if deck_a_count == 0 {
return 1
}
return 0
}
fun draw_front_a() -> u8 {
var dfa_card: u8 = deck_a[deck_a_front]
deck_a_front = wrap52(deck_a_front + 1)
deck_a_count -= 1
return dfa_card
}
fun push_back_a(pba_in: u8) {
// Snapshot `pba_in` into a local before calling wrap52,
// because NEScript v0.1's parameter-passing ABI uses fixed
// zero-page slots — wrap52's first param shares slot $04
// with our `pba_in` and would silently clobber it.
var pba_card: u8 = pba_in
var pba_slot: u8 = wrap52(deck_a_front + deck_a_count)
deck_a[pba_slot] = pba_card
deck_a_count += 1
}
// ── deck_b ────────────────────────────────────────────────
fun deck_b_empty() -> u8 {
if deck_b_count == 0 {
return 1
}
return 0
}
fun draw_front_b() -> u8 {
var dfb_card: u8 = deck_b[deck_b_front]
deck_b_front = wrap52(deck_b_front + 1)
deck_b_count -= 1
return dfb_card
}
fun push_back_b(pbb_in: u8) {
var pbb_card: u8 = pbb_in
var pbb_slot: u8 = wrap52(deck_b_front + deck_b_count)
deck_b[pbb_slot] = pbb_card
deck_b_count += 1
}
// ── pot ───────────────────────────────────────────────────
fun push_back_pot(pbp_in: u8) {
pot[pot_count] = pbp_in
pot_count += 1
}
fun clear_pot() {
pot_count = 0
}
// Transfer every card currently in the pot into deck_a, in FIFO
// order (so the face-up and face-down cards layer naturally).
fun pot_to_a() {
var pta_i: u8 = 0
while pta_i < pot_count {
push_back_a(pot[pta_i])
pta_i += 1
}
pot_count = 0
}
fun pot_to_b() {
var ptb_i: u8 = 0
while ptb_i < pot_count {
push_back_b(pot[ptb_i])
ptb_i += 1
}
pot_count = 0
}
// ── Init + shuffle ────────────────────────────────────────
//
// Build a 52-card "master deck" in deck_a's backing array using
// a rank-major loop: for each rank 1..13, each suit 0..3, write
// the packed byte (rank << 4) | suit into deck_a[i]. Then
// bounded-random-swap-shuffle it. Finally, split the first 26
// into deck_b (copied), leaving the second 26 in deck_a's first
// half, and reset both queues' cursors.
//
// The random-swap shuffle is a bounded alternative to
// Fisher-Yates: it does N swaps between two random indices, where
// each index is rand() & 0x3F (0..63) and the swap is only done
// when both indices are < 52. 200 iterations on a 52-card deck is
// empirically well-mixed and uses only bitwise ops (no multiply,
// no divide, no W0101 warning).
fun build_master_deck() {
var bmd_r: u8 = 1
var bmd_i: u8 = 0
while bmd_r <= RANK_KING {
var bmd_s: u8 = 0
while bmd_s < 4 {
// Pack rank into the high nibble, suit into the low.
// rank fits in 4 bits (max 13) and suit fits in 2
// bits, so the shift-and-or is exact.
var bmd_shifted: u8 = bmd_r << 4
deck_a[bmd_i] = bmd_shifted | bmd_s
bmd_i += 1
bmd_s += 1
}
bmd_r += 1
}
}
fun shuffle_deck_a() {
var shf_k: u8 = 0
while shf_k < 200 {
var shf_i: u8 = rand_u8() & 0x3F
var shf_j: u8 = rand_u8() & 0x3F
if shf_i < DECK_SIZE {
if shf_j < DECK_SIZE {
var shf_tmp: u8 = deck_a[shf_i]
deck_a[shf_i] = deck_a[shf_j]
deck_a[shf_j] = shf_tmp
}
}
shf_k += 1
}
}
// After shuffle, split deck_a's 52 cards into two halves: the
// first 26 stay in deck_a, the second 26 move into deck_b.
// Reset both queues' front/count cursors in the process.
fun split_decks() {
var spd_i: u8 = 0
while spd_i < HALF_DECK {
deck_b[spd_i] = deck_a[HALF_DECK + spd_i]
spd_i += 1
}
deck_a_front = 0
deck_a_count = HALF_DECK
deck_b_front = 0
deck_b_count = HALF_DECK
pot_count = 0
}
fun init_and_shuffle_decks() {
build_master_deck()
shuffle_deck_a()
split_decks()
}