1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples/war/PLAN.md
Claude 9137b1f713
examples/war: polish pass + README entry + plan close-out
End-of-implementation polish for the War example after the
compiler bugs were fixed:

- Title state now calls draw_big_war_banner instead of inlining
  12 draws — same pixel output, fewer lines.
- P_WAR_BURY redraws the previous round's face-up cards while
  the noise thumps fire so the table doesn't look empty for
  24 frames between the WAR banner and the new face-ups.
- Drop draw_word_war from render.ne (orphaned by the BIG WAR
  metasprite).
- Refresh comments in background.ne (now references the real
  felt tile) and deal_state.ne (drop the stale FRAMES_DEAL_STEP
  reference now that the deal pace is hard-coded at 2 frames).
- README.md and examples/README.md gain a war row.
- PLAN.md marks every implementation step complete and records
  the design revisions made along the way.
- Refresh the war audio hash to match the new ROM (the title
  screen helper change shifts one frame of pulse-2 timing
  enough to flip the FNV-1a). The frame-180 PNG is unchanged.

https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 16:08:03 +00:00

10 KiB
Raw Blame History

War — Implementation Plan

A production-quality card game example for NEScript. This document is the living plan: each step is checked off as it's completed, and any mid-flight design changes are recorded in the "Design revisions" section at the bottom.


1. Scope & quality bar

A production-grade NES version of War:

  • Title screen with a big logo, a 3-option menu (0 PLAYERS / 1 PLAYER / 2 PLAYERS), blinking "PRESS START", and music.
  • Gameplay with an animated deal, two live decks with running card counts, cards that slide between the decks and the center, a readable face-up card on each side.
  • War / tie-breaker flow with a distinct "WAR!" banner, an exciting SFX, and buried cards that actually come out of each player's deck.
  • Victory screen with the winning player highlighted and a fanfare.
  • Sound: title music, card-flip click, distinct round-win cues for A vs B, exciting WAR tie-break sfx, victory fanfare.

2. File layout

examples/war.ne                  # top-level: game decl, palette, background, sprite sheet, includes, start
examples/war/PLAN.md             # this file
examples/war/constants.ne        # gameplay + layout constants
examples/war/audio.ne            # sfx + music declarations
examples/war/state.ne            # global variables (decks, phase, rng, anim timers, ...)
examples/war/rng.ne              # 8-bit LFSR PRNG helpers
examples/war/deck.ne             # queue ops on decks + pot
examples/war/compare.ne          # card rank/suit extraction, round resolver
examples/war/render.ne           # draw_card_front / draw_card_back helpers
examples/war/title_state.ne      # state Title
examples/war/deal_state.ne       # state Deal
examples/war/play_state.ne       # state Playing (phase machine)
examples/war/victory_state.ne    # state Victory

The top-level examples/war.ne is the file the pre-commit hook and emulator harness see. It compiles to examples/war.nes without any tooling changes. The split source files all live under examples/war/ and are pulled in via include "war/<name>.ne" directives.


3. Hardware budget

Sprite budget per frame

All sprites use sprite sub-palette 0 (the NEScript codegen hard-wires the OAM attribute byte to 0). That gives 4 shared colours: transparent, red, white, black — enough for both red and black suits on a white card face with a black outline.

Cards are 16×24 px (2 cols × 3 rows = 6 sprites). Max on-screen simultaneously in the steady state:

Entity Qty Sprites Notes
Deck A top 1 6 card back
Deck B top 1 6 card back
Face-up A 1 6 varies by round
Face-up B 1 6 varies by round
Flying card 1 6 only during anim
Cursor / UI ≤2 ≤2 title selector + misc
32 ≤ 64 OAM slots ✓

8-per-scanline check. Decks are at y = 80, face-up cards at y = 128. Those two bands never overlap vertically, so the worst case is 2 cards side by side within the same band = 4 sprites per scanline.

Tile budget (max 256)

Group Tiles Purpose
Card frame 6 outline corners + top/bottom blank cells
Card back 4 diamond-lattice pattern, symmetric
Rank glyphs 13 A, 2-9, 10, J, Q, K (8×8 each)
Small suit (corner) 4 ♠ ♥ ♦ ♣
Big suit left half 4 left half of large centre pip
Big suit right half 4 right half
Font A-Z 26 title/menu/HUD
Font 0-9 10 deck counts + HUD
Punctuation + space ~4 :, !, space, ?
Big "WAR" banner 12 2×2 block per letter
Felt-table tile 2 background fill + subtle pattern
Border / divider 4 thin framing
Cursor / arrow 2 menu selection
Total ~95 leaves ~150 tiles free

RAM budget

Structure Bytes Notes
deck_a: u8[52] 52 circular buffer, packed rank<<4 | suit
deck_b: u8[52] 52
pot: u8[52] 52 cards currently in play
queue cursors, counts, phase, anim timers ~20
RNG state 1
Total ~180 well under the ~1700 bytes of general RAM

4. Card representation & RNG

Card encoding

One byte per card, packed as (rank << 4) | suit:

  • rank = 1..13 (A=1, 2=2, …, 10=10, J=11, Q=12, K=13)
  • suit = 0..3 (♠=0, ♥=1, ♦=2, ♣=3)

PRNG

8-bit Galois LFSR seeded from the frame counter on title-screen exit.

Shuffle

Bounded random-swap shuffle: 200 swaps between two 6-bit random indices, retrying when either index ≥ 52. Uses only & and compare, no multiply or divide.

Deck as a queue

Each deck is a circular buffer with front / count cursors. draw bumps front, push_back writes at (front + count) % 52. Modulo is implemented with if x >= 52 { x -= 52 } to avoid the expensive % software routine.


5. State machine

Title → Deal → Playing → Victory → Title

Playing contains an inner phase machine driven by a u8 phase var:

Phase Notes
P_WaitA Human: wait for input; CPU: wait for think timer
P_FlyA 16-frame lerp of A's card from deck to play position
P_WaitB symmetric
P_FlyB symmetric
P_Reveal Both cards visible; brief beat
P_Resolve Compare, branch to P_WinA / P_WinB / P_WarBanner
P_WinA / P_WinB Cards slide to winner deck
P_WarBanner Flash "WAR!" banner, play WarFlash
P_WarBury Bury 3 face-down cards from each deck
P_Check Win-condition check, possibly transition Victory

Game modes (0/1/2 players) are captured by two bool flags a_is_cpu / b_is_cpu chosen on title exit.


6. Audio

Name Channel Shape
FlipCard pulse 1 short descending click
CheerA pulse 1 8-frame rising arpeggio, pitch envelope
CheerB pulse 1 8-frame descending arpeggio, pitch envelope
WarFlash pulse 1 16-frame pitch sweep, loud → soft → loud
ThudDown noise 4-frame noise burst (bury animation)
TitleTheme pulse 2 brisk 4/4 march, looping
Victory builtin fanfare one-shot on win

7. Implementation steps

All steps complete. The order they actually shipped in is below (the original 12-step plan got compressed once the early steps turned up enough compiler bugs to demand investigation in parallel).

  • Step 1: Skeleton — top-level file, every included file filled with a real implementation, compiles cleanly.
  • Step 2: Felt background — replaced the builtin-smiley grid with a custom TILE_FELT_BG cross-hatch tile.
  • Step 3: Card art — bold rank glyphs (1 tile each), small corner suits, big centre pip halves, card-back lattice, card frame helpers (draw_card_face / draw_card_back).
  • Step 4: Deck data structures — circular buffers with front/count cursors, packed (rank << 4) | suit cards, Galois LFSR PRNG, bounded random-swap shuffle, deal/split.
  • Step 5: Title state — BIG WAR banner, "CARD GAME" subtitle, 3-line menu with cursor, blinking PRESS A, title-music + autopilot.
  • Step 6: Deal state — animated deal with FlipCard sfx and growing deck-back stacks.
  • Step 7: Play state phase machine — match phase over the 11 P_* phases, fly animation that doesn't overshoot, win cues, debounced human input.
  • Step 8: War tie-break — BIG WAR banner reused as a strobing flash, ThudDown noise sfx for each buried card, pot grows by 6 per side per war.
  • Step 9: Victory state — staggered "PLAYER X / WINS" banner, top-of-deck showcase card, builtin fanfare, auto-return to Title.
  • Step 10: Polish pass — bold sprite font, card-fly timing/overshoot fix, P_WAR_BURY redraws the previous face-ups, draw_word_war removed (was orphaned by the BIG WAR helper), title state shares the BIG WAR helper.
  • Step 11: Capture goldens, verify all 31 ROMs match, war golden lands cleanly on a mid-fly-A frame at frame 180.
  • Step 12: Update README.md and examples/README.md.
  • Code review pass: read every file end-to-end, fix any mistakes found.

Two compiler bugs were discovered along the way and fixed in the same PR (E0506 too-many-params + the wide_hi map leak in IR lowering). Three more compiler limitations are documented in COMPILER_BUGS.md with workarounds in the war source for now.


8. Design revisions

A few things shifted from the approved plan:

  • arm_fly is 4 params, not 6. The 5th and 6th params (fly_card, fly_face_up) are written to globals at every call site instead, because the v0.1 ABI silently drops params past the 4th. The 4-param limit now produces a clean E0506 diagnostic so future authors won't trip on it.
  • card parameter in card_rank / card_suit / draw_card_face was renamed to per-function unique names to dodge the IR lowering's shared-VarId issue documented in COMPILER_BUGS.md §1b. Same for wrap52's v and the push_back_* helpers. x and y are still shared because they sit at the same parameter position in every helper that takes them, so the routed slot mapping is consistent.
  • The Playing state's phase machine uses match, not a flat if-chain. The if-chain shape allowed two phases to run in the same frame after a set_phase transition, which made the card-fly animation overshoot its endpoint by FLY_STEP. match runs only the first matching arm.
  • Card frame outline tiles (TILE_FRAME_TL/TR/BL/BR) are still allocated in the Tileset but unused. The card faces use the rank/suit/pip tiles directly with white card bodies that visually separate from the dark felt — a card frame would have made each tile cramped without much readability gain. Constants are kept for layout stability; the tiles themselves serve as a 4-tile reserve for future art tweaks.
  • The deal animation is a single bouncing card-back, not a full 52-card cascade. Cleaner and cheaper, and the FlipCard click rhythm carries the "we're dealing!" feel by itself.