1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-18 14:45:58 +00:00

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
This commit is contained in:
Claude 2026-04-15 15:22:20 +00:00
parent 86db78a31f
commit 8ababdcec4
No known key found for this signature in database
18 changed files with 2886 additions and 0 deletions

856
examples/war/assets.ne Normal file
View file

@ -0,0 +1,856 @@
// war/assets.ne — the one big Tileset sprite block.
//
// Every custom 8×8 CHR tile in the game is stacked vertically inside
// a single `sprite Tileset { pixels: [...] }` declaration. Stacking
// them 1-tile-wide × N-tiles-tall means the parser splits each tile
// into consecutive tile indices in reading order, so tile index N in
// a `draw Tileset frame: N` call picks the N'th 8×8 block.
//
// Every constant in constants.ne points at a position in this list.
// If you add or remove a tile, update BOTH this file AND the matching
// constant range.
//
// The colour vocabulary (per the palette declaration in war.ne):
//
// . transparent
// 1 red (palette index 1 — hearts, diamonds, accents)
// 2 white (palette index 2 — card body, letters)
// 3 black (palette index 3 — outlines, spades, clubs, text)
//
// All tiles below use this 4-colour alphabet. Red is only used for
// red suits; black is used for text, outlines, and black suits.
sprite Tileset {
pixels: [
// ── 01: card frame top-left corner ───────────────
// Rounded top-left corner of a card body. Black outline
// two pixels thick on the exterior, white interior so
// rank glyphs drawn with black pixels read cleanly on
// top. The interior corner is pure white so the rank
// tile (drawn next to it) overpaints naturally.
"..333333",
".3222222",
".3222222",
"3222....",
"3222....",
"3222....",
"3222....",
"3222....",
// ── 02: card frame top-right corner ──────────────
"333333..",
"2222223.",
"2222223.",
"....2223",
"....2223",
"....2223",
"....2223",
"....2223",
// ── 03: card frame bottom-left corner ────────────
"3222....",
"3222....",
"3222....",
"3222....",
"3222....",
".3222222",
".3222222",
"..333333",
// ── 04: card frame bottom-right corner ───────────
"....2223",
"....2223",
"....2223",
"....2223",
"....2223",
"2222223.",
"2222223.",
"333333..",
// ── 05: card frame blank cell — left half ────────
// Pure white interior with a left black edge (for the
// card's left border). Used for the blank middle /
// bottom of any card face that doesn't carry art.
"32222222",
"32222222",
"32222222",
"32222222",
"32222222",
"32222222",
"32222222",
"32222222",
// ── 06: card frame blank cell — right half ───────
"22222223",
"22222223",
"22222223",
"22222223",
"22222223",
"22222223",
"22222223",
"22222223",
// ── 07: card back top-left ───────────────────────
// The back is a black-and-white diamond lattice,
// painted as a 2×2 repeating block. Every card back
// uses all four corners tiled into a 16×16 / 16×24
// block.
"..333333",
".3333333",
".3322333",
"333.2.33",
"33.222.3",
"33.2.233",
"3322.333",
"3333.333",
// ── 08: card back top-right ──────────────────────
"333333..",
"3333333.",
"33322.33",
"33.2.333",
"3.222.33",
"33.2.333",
"333.2233",
"3333.333",
// ── 09: card back bottom-left ────────────────────
"3333.333",
"3322.333",
"33.2.233",
"33.222.3",
"333.2.33",
".3322333",
".3333333",
"..333333",
// ── 10: card back bottom-right ───────────────────
"3333.333",
"333.2233",
"33.2.333",
"3.222.33",
"33.2.333",
"33322.33",
"3333333.",
"333333..",
// ── 11: rank A (Ace) ─────────────────────────────
// Bold black glyph on a white card body. The rank is
// drawn 6×6 px and centred horizontally in the 8×8 tile.
// The card body fills index 2 (white) so the glyph reads
// as black-on-white when the card frame's outline tiles
// wrap it.
"22222222",
"22.33.22",
"2.3333.2",
"2.3..3.2",
"2.3333.2",
"2.3..3.2",
"2.3..3.2",
"22222222",
// ── 12: rank 2 ───────────────────────────────────
"22222222",
"2.3333.2",
"2....3.2",
"22.33.22",
"2.3...22",
"2.3...22",
"2.3333.2",
"22222222",
// ── 13: rank 3 ───────────────────────────────────
"22222222",
"2.3333.2",
"22...3.2",
"22.33.22",
"22...3.2",
"2....3.2",
"2.3333.2",
"22222222",
// ── 14: rank 4 ───────────────────────────────────
"22222222",
"2.3..3.2",
"2.3..3.2",
"2.3..3.2",
"2.3333.2",
"22...3.2",
"22...3.2",
"22222222",
// ── 15: rank 5 ───────────────────────────────────
"22222222",
"2.3333.2",
"2.3...22",
"2.3333.2",
"22...3.2",
"22...3.2",
"2.3333.2",
"22222222",
// ── 16: rank 6 ───────────────────────────────────
"22222222",
"2.3333.2",
"2.3...22",
"2.3333.2",
"2.3..3.2",
"2.3..3.2",
"2.3333.2",
"22222222",
// ── 17: rank 7 ───────────────────────────────────
"22222222",
"2.3333.2",
"22...3.2",
"22..3.22",
"22.3..22",
"22.3..22",
"22.3..22",
"22222222",
// ── 18: rank 8 ───────────────────────────────────
"22222222",
"2.3333.2",
"2.3..3.2",
"2.3333.2",
"2.3..3.2",
"2.3..3.2",
"2.3333.2",
"22222222",
// ── 19: rank 9 ───────────────────────────────────
"22222222",
"2.3333.2",
"2.3..3.2",
"2.3..3.2",
"2.3333.2",
"22...3.2",
"2.3333.2",
"22222222",
// ── 20: rank 10 (condensed "1" + "0") ─────────────
// "10" squeezed into an 8x8 tile. The "1" takes columns
// 0-1, a gap, the "0" takes columns 4-7.
"22222222",
"2.3.33.2",
"233.3..3",
"2.3.3..3",
"2.3.3..3",
"2.3.3..3",
"2.3.33.2",
"22222222",
// ── 21: rank J ───────────────────────────────────
"22222222",
"222.33.2",
"222..3.2",
"222..3.2",
"222..3.2",
"22.3.3.2",
"22.333.2",
"22222222",
// ── 22: rank Q ───────────────────────────────────
"22222222",
"2.3333.2",
"2.3..3.2",
"2.3..3.2",
"2.3.33.2",
"2.3333.2",
"22.33.32",
"22222222",
// ── 23: rank K ───────────────────────────────────
"22222222",
"2.3..3.2",
"2.3.3.22",
"2.33..22",
"2.3.3.22",
"2.3..3.2",
"2.3..3.2",
"22222222",
// ── 24: small spade ♠ ────────────────────────────
// 6x6 centred pip on a white 8x8 tile.
"22222222",
"2222.222",
"222.3.22",
"22.333.2",
"2.33333.",
"2.33333.",
"222.3.22",
"222333.2",
// ── 25: small heart ♥ ────────────────────────────
// Red pip on white.
"22222222",
"22.11.22",
"2.1111.2",
"2.1111.2",
"2.1111.2",
"22.11.22",
"222.1.22",
"22222222",
// ── 26: small diamond ♦ ──────────────────────────
"22222222",
"22221222",
"2221.122",
"221...12",
"221...12",
"2221.122",
"22221222",
"22222222",
// ── 27: small club ♣ ─────────────────────────────
"22222222",
"2222.222",
"2223332.",
"2223332.",
"22.333.2",
"2.33333.",
"22.3.3.2",
"222333.2",
// ── 28: big spade — left half ────────────────────
// The big centre-pip is drawn 16×8: two tiles side by
// side. Each suit's (left half, right half) pair is
// authored explicitly so the centre art can be bolder
// and more symmetric than the corner pips.
"2222222.",
"222222.3",
"22222.33",
"2222.333",
"222.3333",
"22.33333",
"2.333333",
".3333333",
// ── 29: big heart — left half ────────────────────
"2222.111",
"222.1111",
"22.11111",
"22.11111",
"22.11111",
"222.1111",
"2222.111",
"22222.11",
// ── 30: big diamond — left half ──────────────────
"22222222",
"22222.12",
"2222.112",
"222.1112",
"22.11112",
"2.111112",
"22.11112",
"222.1112",
// ── 31: big club — left half ─────────────────────
"2222.333",
"2222.333",
"222.3333",
"22.33333",
"2.333333",
"2.333333",
"22.333.3",
"2223.333",
// ── 32: big spade — right half ───────────────────
".3333333",
"2.333333",
"22.33333",
"222.3333",
"2222.333",
"22222.33",
"222222.3",
"2222222.",
// ── 33: big heart — right half ───────────────────
"111.2222",
"1111.222",
"11111.22",
"11111.22",
"11111.22",
"1111.222",
"111.2222",
"11.22222",
// ── 34: big diamond — right half ─────────────────
"22222222",
"21.22222",
"211.2222",
"2111.222",
"21111.22",
"211111.2",
"21111.22",
"2111.222",
// ── 35: big club — right half ────────────────────
"333.2222",
"333.2222",
"3333.222",
"33333.22",
"333333.2",
"333333.2",
"3.333.22",
"333.3222",
// ── 36-61: alphabet A..Z (8x8, white on transparent) ─
// Bold sans-serif letters drawn with 2-pixel strokes so
// they still read over the felt at NES resolution. Every
// letter uses palette index 2 (white) on a transparent
// background. Letters are 6 wide, centred horizontally
// in the 8-wide tile with a 1-pixel top/bottom margin.
// A
"........",
"..2222..",
".22..22.",
".22..22.",
".222222.",
".22..22.",
".22..22.",
"........",
// B
"........",
".22222..",
".22..22.",
".22222..",
".22..22.",
".22..22.",
".22222..",
"........",
// C
"........",
"..22222.",
".22..22.",
".22.....",
".22.....",
".22..22.",
"..22222.",
"........",
// D
"........",
".2222...",
".22.22..",
".22..22.",
".22..22.",
".22.22..",
".2222...",
"........",
// E
"........",
".222222.",
".22.....",
".22222..",
".22.....",
".22.....",
".222222.",
"........",
// F
"........",
".222222.",
".22.....",
".22222..",
".22.....",
".22.....",
".22.....",
"........",
// G
"........",
"..22222.",
".22.....",
".22.222.",
".22..22.",
".22..22.",
"..2222..",
"........",
// H
"........",
".22..22.",
".22..22.",
".222222.",
".22..22.",
".22..22.",
".22..22.",
"........",
// I
"........",
"..2222..",
"...22...",
"...22...",
"...22...",
"...22...",
"..2222..",
"........",
// J
"........",
"....222.",
".....22.",
".....22.",
".....22.",
".22..22.",
"..2222..",
"........",
// K
"........",
".22..22.",
".22.22..",
".2222...",
".22.22..",
".22..22.",
".22..22.",
"........",
// L
"........",
".22.....",
".22.....",
".22.....",
".22.....",
".22.....",
".222222.",
"........",
// M
"........",
".22..22.",
".222222.",
".222222.",
".22..22.",
".22..22.",
".22..22.",
"........",
// N
"........",
".22..22.",
".222.22.",
".222222.",
".22.222.",
".22..22.",
".22..22.",
"........",
// O
"........",
"..2222..",
".22..22.",
".22..22.",
".22..22.",
".22..22.",
"..2222..",
"........",
// P
"........",
".22222..",
".22..22.",
".22..22.",
".22222..",
".22.....",
".22.....",
"........",
// Q
"........",
"..2222..",
".22..22.",
".22..22.",
".22.222.",
".22.22..",
"..222.2.",
"........",
// R
"........",
".22222..",
".22..22.",
".22..22.",
".22222..",
".22.22..",
".22..22.",
"........",
// S
"........",
"..22222.",
".22.....",
"..2222..",
".....22.",
".....22.",
".22222..",
"........",
// T
"........",
".222222.",
"...22...",
"...22...",
"...22...",
"...22...",
"...22...",
"........",
// U
"........",
".22..22.",
".22..22.",
".22..22.",
".22..22.",
".22..22.",
"..2222..",
"........",
// V
"........",
".22..22.",
".22..22.",
".22..22.",
".22..22.",
"..2222..",
"...22...",
"........",
// W
"........",
".22..22.",
".22..22.",
".22..22.",
".222222.",
".222222.",
".22..22.",
"........",
// X
"........",
".22..22.",
"..2222..",
"...22...",
"...22...",
"..2222..",
".22..22.",
"........",
// Y
"........",
".22..22.",
".22..22.",
"..2222..",
"...22...",
"...22...",
"...22...",
"........",
// Z
"........",
".222222.",
".....22.",
"....22..",
"...22...",
"..22....",
".222222.",
"........",
// ── 62-71: digits 0..9 (bold) ──────────────────
// 0
"........",
"..2222..",
".22..22.",
".22..22.",
".22..22.",
".22..22.",
"..2222..",
"........",
// 1
"........",
"...22...",
"..222...",
".2.22...",
"...22...",
"...22...",
"..2222..",
"........",
// 2
"........",
"..2222..",
".22..22.",
".....22.",
"....22..",
"..22....",
".222222.",
"........",
// 3
"........",
"..2222..",
".22..22.",
"....22..",
".....22.",
".22..22.",
"..2222..",
"........",
// 4
"........",
"....22..",
"...222..",
"..2.22..",
".22.22..",
".222222.",
"....22..",
"........",
// 5
"........",
".222222.",
".22.....",
".22222..",
".....22.",
".22..22.",
"..2222..",
"........",
// 6
"........",
"..2222..",
".22.....",
".22222..",
".22..22.",
".22..22.",
"..2222..",
"........",
// 7
"........",
".222222.",
".....22.",
"....22..",
"...22...",
"..22....",
"..22....",
"........",
// 8
"........",
"..2222..",
".22..22.",
"..2222..",
".22..22.",
".22..22.",
"..2222..",
"........",
// 9
"........",
"..2222..",
".22..22.",
".22..22.",
"..22222.",
".....22.",
"..2222..",
"........",
// ── 72: cursor (▶) ────────────────────────────
"........",
"..2.....",
"..22....",
"..222...",
"..222...",
"..22....",
"..2.....",
"........",
// ── 73: tiny heart (victory marker) ───────────
"........",
".11..11.",
"11111111",
"11111111",
".111111.",
"..1111..",
"...11...",
"........",
// ── 74: single dot (stack marker / spacer) ────
"........",
"...22...",
"..2222..",
"..2222..",
"..2222..",
"..2222..",
"...22...",
"........",
// ── 75: felt background fill ─────────────────
// Subtle cross-hatch on the felt. When the background
// layer renders this tile through bg sub-palette 0
// (universal dk_green + forest/green/mint), index 0
// becomes dk_green and index 3 becomes mint — so the
// tile shows as dk_green felt with sparse mint flecks.
// The same CHR rendered as a sprite would show as
// transparent + black, which we never do.
"........",
"....3...",
"........",
"..3.....",
"........",
"......3.",
"........",
"...3....",
// ── 76-87: BIG WAR title letters ─────────────
// Each letter is 16×16 = a 2x2 block of tiles. The
// letters are designed bold and chunky so the title
// reads from across the room. Each tile draws a
// quadrant of the letter using palette index 2 (white
// when rendered via sp0, white when rendered via the
// bg cream sub-palette).
//
// 76: BIG W top-left
"22....22",
"22....22",
"22....22",
"22....22",
"22....22",
"22....22",
"22....22",
"22....22",
// 77: BIG W top-right
"22....22",
"22....22",
"22....22",
"22....22",
"22....22",
"22....22",
"22....22",
"22....22",
// 78: BIG W bottom-left
"22....22",
"22....22",
"22.22.22",
"22.22.22",
"22.22.22",
"2222.222",
"2222.222",
".22..22.",
// 79: BIG W bottom-right
"22....22",
"22....22",
"22.22.22",
"22.22.22",
"22.22.22",
"222.2222",
"222.2222",
".22..22.",
// 80: BIG A top-left
"....2222",
"...22222",
"..222222",
".2222.22",
"2222..22",
"2222..22",
"222....2",
"222....2",
// 81: BIG A top-right
"2222....",
"22222...",
"222222..",
"22.2222.",
"22..2222",
"22..2222",
"2....222",
"2....222",
// 82: BIG A bottom-left
"22222222",
"22222222",
"22222222",
"222....2",
"222....2",
"222....2",
"222....2",
"222....2",
// 83: BIG A bottom-right
"22222222",
"22222222",
"22222222",
"2....222",
"2....222",
"2....222",
"2....222",
"2....222",
// 84: BIG R top-left
"22222222",
"22222222",
"222....2",
"222....2",
"222....2",
"222....2",
"22222222",
"22222222",
// 85: BIG R top-right
"2222....",
"22222...",
"..222...",
"..222...",
"..222...",
"..2222..",
".22222..",
"22222...",
// 86: BIG R bottom-left
"22222222",
"22222222",
"222.2222",
"222..222",
"222...22",
"222...22",
"222....2",
"222....2",
// 87: BIG R bottom-right
"22......",
"222.....",
"2222....",
"22222...",
"222222..",
"2222222.",
"22222222",
".2222222"
]
}