mirror of
https://github.com/imjasonh/nescript
synced 2026-07-15 04:35:37 +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:
parent
86db78a31f
commit
8ababdcec4
18 changed files with 2886 additions and 0 deletions
435
examples/war/COMPILER_BUGS.md
Normal file
435
examples/war/COMPILER_BUGS.md
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
# NEScript v0.1 — Compiler Bugs and Limitations Found While Building War
|
||||
|
||||
This document captures bugs and limitations discovered while
|
||||
building `examples/war.ne`. Each entry includes a minimal
|
||||
reproduction, the symptom we observed, the root cause if known,
|
||||
and a workaround we used in `examples/war/*.ne`. The intent is
|
||||
to track these so they can be fixed in a future compiler pass —
|
||||
once they are, the corresponding workarounds in `war/*.ne`
|
||||
should be reverted to keep the example honest.
|
||||
|
||||
---
|
||||
|
||||
## 1. Functions with more than 4 parameters silently corrupt the 5th+
|
||||
|
||||
### Symptom
|
||||
|
||||
Calling a function with 5 or 6 parameters compiles cleanly, with
|
||||
no warning or error, but at runtime the 5th and 6th parameter
|
||||
values are silently replaced by garbage (typically the value of
|
||||
parameter 3 or 4). Animations and state writes that depend on
|
||||
those parameters behave as if zero was passed.
|
||||
|
||||
### Reproduction
|
||||
|
||||
```nescript
|
||||
fun arm_fly(sx: u8, sy: u8, dxsign: u8, dysign: u8, card: u8, fu: u8) {
|
||||
fly_x = sx
|
||||
fly_y = sy
|
||||
fly_dx_sign = dxsign
|
||||
fly_dy_sign = dysign
|
||||
fly_card = card // gets the value of dxsign instead!
|
||||
fly_face_up = fu // gets the value of dxsign instead!
|
||||
}
|
||||
|
||||
fun caller() {
|
||||
arm_fly(32, 64, 0, 0, 147, 1)
|
||||
// After this call:
|
||||
// fly_x = 32, fly_y = 64, fly_dx_sign = 0, fly_dy_sign = 0
|
||||
// fly_card = 0 (NOT 147)
|
||||
// fly_face_up = 0 (NOT 1)
|
||||
}
|
||||
```
|
||||
|
||||
### Root cause
|
||||
|
||||
`src/codegen/ir_codegen.rs` (around line 240) iterates through
|
||||
`func.locals` and assigns the first 4 entries to zero-page
|
||||
parameter slots `$04`-`$07`:
|
||||
|
||||
```rust
|
||||
for func in &ir.functions {
|
||||
for (i, local) in func.locals.iter().enumerate() {
|
||||
if i < func.param_count {
|
||||
if i < 4 {
|
||||
var_addrs.insert(local.var_id, 0x04 + i as u16);
|
||||
...
|
||||
}
|
||||
} else {
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `if i < 4` guard silently drops the mapping for params 5+
|
||||
without inserting any RAM allocation for them. The corresponding
|
||||
caller-side codegen for `Call` writes only the first four
|
||||
arguments. Result: params 5 and 6 are never passed and the
|
||||
callee reads stale memory from $04-$07 in their place.
|
||||
|
||||
### Workaround used in `examples/war/`
|
||||
|
||||
`arm_fly` is split: the four "arming" parameters stay in the
|
||||
function signature, and `fly_card` / `fly_face_up` are written to
|
||||
the global state directly at every call site instead. See
|
||||
`war/play_state.ne` (`begin_draw_a` / `begin_draw_b`).
|
||||
|
||||
### Fix proposal
|
||||
|
||||
Two reasonable options:
|
||||
|
||||
1. **Diagnose-only**: emit `E05XX too many parameters` when a
|
||||
`fun` declaration has more than 4 params. This is the
|
||||
smallest possible change and turns silent miscompiles into a
|
||||
loud compile-time error. Should ship immediately even if
|
||||
option 2 is also planned.
|
||||
|
||||
2. **Spill to RAM**: extend the calling convention so params
|
||||
beyond the first four are passed via dedicated RAM slots in
|
||||
the callee's local frame. The caller-side `Call` codegen
|
||||
would write those slots before `JSR`, the callee-side prologue
|
||||
could leave them as-is. This grows the per-function RAM
|
||||
footprint but lets users write any signature they like.
|
||||
|
||||
---
|
||||
|
||||
## 1b. Function parameters with the same name in different functions share a VarId, which collides their zero-page slot mapping
|
||||
|
||||
### Symptom
|
||||
|
||||
Two unrelated functions whose parameters happen to be named the
|
||||
same (e.g. both have a `card: u8` parameter, or both have an
|
||||
`x: u8` parameter) end up reading parameters from the wrong
|
||||
zero-page slot at runtime. One function reads `$04`, another
|
||||
reads `$06`, a third reads `$05` — depending on the parameter's
|
||||
*position* in whichever function is processed last by the
|
||||
codegen.
|
||||
|
||||
This is a much sneakier sibling of bug #1: rather than dropping
|
||||
a parameter past the 4th slot, it silently reroutes parameter
|
||||
reads to slots that hold completely unrelated values from the
|
||||
caller.
|
||||
|
||||
### Reproduction
|
||||
|
||||
```nescript
|
||||
// Function A: card is the 1st parameter, expected at $04
|
||||
fun push_back_a(card: u8) {
|
||||
deck_a[deck_a_front] = card // reads from $06, not $04!
|
||||
deck_a_count += 1
|
||||
}
|
||||
|
||||
// Function B: card is the 3rd parameter, expected at $06
|
||||
fun draw_card_face(x: u8, y: u8, card: u8) {
|
||||
// ... uses card normally ...
|
||||
}
|
||||
```
|
||||
|
||||
The IR lowering assigns `card` a single shared `VarId` because
|
||||
its `var_map` is global across all functions. The codegen then
|
||||
walks each function in turn, inserting `(VarId(card), $0X)`
|
||||
mappings into a single global `var_addrs` `HashMap` — and
|
||||
whichever function comes last in iteration order wins the
|
||||
mapping. If `draw_card_face` is processed after `push_back_a`,
|
||||
`VarId(card)` ends up mapped to `$06`, and `push_back_a` then
|
||||
reads its `card` parameter from `$06` (which holds whatever the
|
||||
caller was using as a third argument — typically junk).
|
||||
|
||||
### Root cause
|
||||
|
||||
`src/ir/lowering.rs::get_or_create_var` looks up names in
|
||||
`self.var_map`, which is shared across the whole program:
|
||||
|
||||
```rust
|
||||
fn get_or_create_var(&mut self, name: &str) -> VarId {
|
||||
if let Some(&id) = self.var_map.get(name) {
|
||||
id
|
||||
} else {
|
||||
let id = VarId(self.next_var_id);
|
||||
self.next_var_id += 1;
|
||||
self.var_map.insert(name.to_string(), id);
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`lower_function` calls `get_or_create_var(¶m.name)` for each
|
||||
parameter, so two different functions both with a `card`
|
||||
parameter resolve to the same `VarId`. Once that single `VarId`
|
||||
flows into the codegen, the per-function "this is param index N
|
||||
of function F" relationship is lost — there's only one global
|
||||
mapping per `VarId`.
|
||||
|
||||
### Workaround used in `examples/war/`
|
||||
|
||||
Every parameter name in the war source is unique across the
|
||||
entire program. Function-locals were already prefixed by
|
||||
function (see bug #3); we extended the same scheme to params:
|
||||
`push_back_a(pba_arg_card: u8)` instead of
|
||||
`push_back_a(card: u8)`, etc. The wrapping `pba_card` /
|
||||
`pbb_card` / `dcf_card` snapshots from bug #2 stay because they
|
||||
also help with the bug-2 clobbering.
|
||||
|
||||
### Fix proposal
|
||||
|
||||
Two layers to fix in:
|
||||
|
||||
1. **IR lowering**: give every function its own `var_map` for
|
||||
parameters and locals. The global `var_map` should only hold
|
||||
top-level `var` / `const` / `enum` symbols.
|
||||
|
||||
2. **Codegen**: even after the IR fix, the global `var_addrs`
|
||||
`HashMap` should grow a per-function dimension (one map per
|
||||
`IrFunction`) so two different functions can independently
|
||||
assign their own VarIds to overlapping zero-page slots.
|
||||
|
||||
Either fix alone is probably enough; both together is robust.
|
||||
|
||||
---
|
||||
|
||||
## 2. Function parameters share zero-page slots with nested calls — values clobbered across `JSR`
|
||||
|
||||
### Symptom
|
||||
|
||||
A function that takes parameters and then calls another function
|
||||
sees its own parameters silently replaced by the inner call's
|
||||
arguments. Any code path that reads the original parameter
|
||||
*after* the inner call gets the wrong value.
|
||||
|
||||
### Reproduction
|
||||
|
||||
```nescript
|
||||
fun draw_card_face(x: u8, y: u8, card: u8) {
|
||||
var rank: u8 = card_rank(card) // x at $04 is now `card`
|
||||
var suit: u8 = card_suit(card) // x at $04 is still `card`
|
||||
// x is supposed to be 120 here, but it's actually `card`
|
||||
var x1: u8 = x + 8 // computes card + 8, not 120 + 8
|
||||
draw Tileset at: (x, y) frame: ... // draws at x = card, not 120
|
||||
}
|
||||
```
|
||||
|
||||
Concretely, calling `draw_card_face(120, 128, 0x93)` puts the
|
||||
card sprite at `(0x93, 128)` — completely wrong.
|
||||
|
||||
### Root cause
|
||||
|
||||
Same allocator as bug #1: `func.locals[0..param_count]` are
|
||||
mapped to `$04`, `$05`, `$06`, `$07`. The caller writes its own
|
||||
arguments into the same zero-page slots before `JSR`, so the
|
||||
caller's parameters at those slots get clobbered by the callee's
|
||||
arguments. There is no save/restore wrapper around `JSR` and no
|
||||
spill/reload pass to refresh the caller's parameters from a
|
||||
backing copy.
|
||||
|
||||
### Workaround used in `examples/war/`
|
||||
|
||||
Every helper that takes parameters AND makes any nested function
|
||||
call snapshots its parameters into fresh local variables at the
|
||||
top of the function, then references the locals exclusively
|
||||
throughout the body. See `war/render.ne::draw_card_face`,
|
||||
`war/render.ne::draw_flying_card`, `war/deck.ne::push_back_a`,
|
||||
`war/deck.ne::push_back_b`.
|
||||
|
||||
### Fix proposal
|
||||
|
||||
1. **Spill on entry**: at the top of every function body that
|
||||
makes a call, copy `$04..$07` into per-function RAM slots and
|
||||
rewrite all parameter reads to load from the RAM copies.
|
||||
Equivalent to what users are doing manually today.
|
||||
|
||||
2. **Smarter scheduling**: only spill a parameter slot if it's
|
||||
live across a call site (CFG-aware liveness pass on params).
|
||||
Same effect, less RAM cost for short helpers that never read
|
||||
their params after calling out.
|
||||
|
||||
Either fix would let users write straightforward function bodies
|
||||
without having to remember the snapshot dance.
|
||||
|
||||
---
|
||||
|
||||
## 3. Function-local variable names are in a flat global namespace
|
||||
|
||||
### Symptom
|
||||
|
||||
Two different functions cannot declare locals with the same
|
||||
name. The compiler emits `E0501 duplicate declaration of '<name>'`
|
||||
even though the locals are in disjoint scopes.
|
||||
|
||||
### Reproduction
|
||||
|
||||
```nescript
|
||||
fun foo() {
|
||||
var i: u8 = 0
|
||||
while i < 10 { i += 1 }
|
||||
}
|
||||
|
||||
fun bar() {
|
||||
var i: u8 = 0 // E0501 duplicate declaration of 'i'
|
||||
while i < 5 { i += 1 }
|
||||
}
|
||||
```
|
||||
|
||||
### Root cause
|
||||
|
||||
`src/analyzer/mod.rs::register_var` inserts every `var`
|
||||
declaration into a single `self.symbols` map keyed only on the
|
||||
variable's name, with no qualification by function or block:
|
||||
|
||||
```rust
|
||||
fn register_var(&mut self, var: &VarDecl) {
|
||||
if self.symbols.contains_key(&var.name) {
|
||||
self.diagnostics.push(Diagnostic::error(
|
||||
ErrorCode::E0501,
|
||||
format!("duplicate declaration of '{}'", var.name),
|
||||
var.span,
|
||||
));
|
||||
return;
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`check_statement` calls `register_var` for every `Statement::VarDecl`
|
||||
encountered while walking function bodies, so all locals across
|
||||
all functions and all nested blocks land in the same namespace.
|
||||
|
||||
### Workaround used in `examples/war/`
|
||||
|
||||
Every function-local variable is prefixed with a short tag
|
||||
identifying its enclosing function (e.g. `dfa_card` in
|
||||
`draw_front_a`, `pba_slot` in `push_back_a`,
|
||||
`dwp_px` in `draw_word_player`). This makes long files harder to
|
||||
read but is fully mechanical.
|
||||
|
||||
### Fix proposal
|
||||
|
||||
Rework `register_var` to maintain a stack of scopes (one per
|
||||
function body, one per nested block). Each `Statement::VarDecl`
|
||||
inserts into the current scope. Lookup walks the stack from
|
||||
innermost to outermost. The existing global symbol table is
|
||||
unchanged for top-level globals / consts / fun names; only
|
||||
function-locals shift to the scoped table.
|
||||
|
||||
A smaller intermediate fix: keep the flat table but qualify
|
||||
each local's stored name as `<function>::<var>` so the global
|
||||
table sees unique entries even when source names collide.
|
||||
|
||||
---
|
||||
|
||||
## 4. Per-frame sprite-per-scanline limit is invisible to user code
|
||||
|
||||
### Symptom
|
||||
|
||||
Drawing more than 8 sprites whose Y rectangles intersect a
|
||||
single scanline causes the NES PPU to silently drop the excess
|
||||
sprites past the 8th in OAM order. There's no compile-time
|
||||
detection and no runtime warning — letters or tiles just don't
|
||||
render.
|
||||
|
||||
### Reproduction
|
||||
|
||||
```nescript
|
||||
// 9 letters all on the same Y row:
|
||||
draw_letter(0, 100, 0)
|
||||
draw_letter(8, 100, 1)
|
||||
draw_letter(16, 100, 2)
|
||||
draw_letter(24, 100, 3)
|
||||
draw_letter(32, 100, 4)
|
||||
draw_letter(40, 100, 5)
|
||||
draw_letter(48, 100, 6)
|
||||
draw_letter(56, 100, 7)
|
||||
draw_letter(64, 100, 8) // this one will not render
|
||||
```
|
||||
|
||||
### Root cause
|
||||
|
||||
This is a real NES hardware constraint, not a compiler bug.
|
||||
However, because NEScript's `draw` allocator is purely
|
||||
sequential, the compiler cannot warn even when it has all the
|
||||
information needed to know the layout would overflow.
|
||||
|
||||
### Workaround used in `examples/war/`
|
||||
|
||||
We staggered text rows. The title screen's "WAR / CARD GAME /
|
||||
0 PLAYER / 1 PLAYER / 2 PLAYER" layout sits each row at a
|
||||
different y so no scanline carries more than 7 sprites; the
|
||||
victory screen's "PLAYER X / WINS" wraps after the player letter
|
||||
for the same reason.
|
||||
|
||||
### Fix proposal
|
||||
|
||||
Two complementary improvements:
|
||||
|
||||
1. **Static analyzer pass**: walk the IR for each frame handler,
|
||||
collect the set of `(x, y)` literal pairs feeding `draw`
|
||||
ops within the same basic block, and emit `W01XX` if any
|
||||
scanline (8-px row) would have > 8 sprites. Only catches the
|
||||
literal case but that's the most common.
|
||||
|
||||
2. **Sprite-cycling runtime helper**: a `cycle_sprites()`
|
||||
intrinsic that rotates OAM order each frame so the same
|
||||
sprites get dropped on different frames, producing a flicker
|
||||
instead of a permanent dropout. Standard NES technique.
|
||||
|
||||
---
|
||||
|
||||
## 5. The `inline` keyword is a hint and is silently ignored for short functions
|
||||
|
||||
### Symptom
|
||||
|
||||
Marking a tiny function `inline fun` does not always inline it.
|
||||
The compiler still emits a real `JSR` with full parameter
|
||||
passing through `$04`-`$07`, which means the inlining doesn't
|
||||
escape the bug-2 parameter clobbering.
|
||||
|
||||
### Reproduction
|
||||
|
||||
```nescript
|
||||
inline fun card_rank(card: u8) -> u8 {
|
||||
return card >> 4
|
||||
}
|
||||
```
|
||||
|
||||
The asm dump shows `JSR __ir_fn_card_rank` at every call site —
|
||||
the function was not inlined.
|
||||
|
||||
### Root cause
|
||||
|
||||
(Inferred — would need to confirm by reading the inliner pass.)
|
||||
The optimizer's inlining pass has a size threshold or a heuristic
|
||||
that prevents inlining in some contexts even when the function
|
||||
is marked `inline`. There's no diagnostic emitted when the hint
|
||||
is declined.
|
||||
|
||||
### Workaround used in `examples/war/`
|
||||
|
||||
None — we just live with the JSR overhead and the bug-2 fallout.
|
||||
|
||||
### Fix proposal
|
||||
|
||||
1. **Promote `inline` to a hard contract**: when `inline` is
|
||||
present, always inline (or emit `W01XX` if it cannot be
|
||||
inlined for a structural reason like recursion).
|
||||
|
||||
2. **Optional dump**: add `--dump-inliner` to print which
|
||||
`inline fun` declarations were inlined and which weren't,
|
||||
with the reason.
|
||||
|
||||
---
|
||||
|
||||
## Verification path after fixes
|
||||
|
||||
Once any of the bugs above are fixed in the compiler, the
|
||||
corresponding workarounds in `examples/war/*.ne` should be
|
||||
reverted in the same PR so:
|
||||
|
||||
- The example demonstrates idiomatic code, not workaround code.
|
||||
- The PR's diff visibly proves the fix works end-to-end (the
|
||||
workaround removal would otherwise be a silent regression).
|
||||
- The committed `examples/war.nes` rebuilds byte-identically to
|
||||
the reverted source, which the pre-commit hook enforces.
|
||||
|
||||
The relevant workaround sites are catalogued in each bug's
|
||||
"Workaround used" section above; grep for the prefix tags
|
||||
(`dcf_`, `dfa_`, `pba_`, `dwp_`, …) to find them all.
|
||||
856
examples/war/assets.ne
Normal file
856
examples/war/assets.ne
Normal 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"
|
||||
]
|
||||
}
|
||||
84
examples/war/audio.ne
Normal file
84
examples/war/audio.ne
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// war/audio.ne — every sfx + music declaration in the game.
|
||||
//
|
||||
// The NEScript audio driver is only linked in when user code
|
||||
// contains at least one `play` / `start_music` / `stop_music`
|
||||
// statement. That's always true for this game, so everything
|
||||
// declared here ends up in PRG ROM.
|
||||
//
|
||||
// Channel budget:
|
||||
// pulse 1 — sfx (FlipCard, CheerA, CheerB, WarFlash)
|
||||
// pulse 2 — music (TitleTheme and the builtin fanfare)
|
||||
// triangle — unused (reserved for later)
|
||||
// noise — ThudDown bury sfx
|
||||
|
||||
// ── Sound effects ──────────────────────────────────────────
|
||||
|
||||
// Sharp descending click for a card flip / draw. Kept short so
|
||||
// repeated clicks during the deal animation don't overlap.
|
||||
sfx FlipCard {
|
||||
duty: 1
|
||||
pitch: 0x28
|
||||
envelope: [12, 9, 5, 2]
|
||||
}
|
||||
|
||||
// Cheerful ascending arpeggio when player A wins a round. The
|
||||
// per-frame pitch envelope sweeps through three notes so the
|
||||
// effect actually sounds like a melody rather than a beep.
|
||||
sfx CheerA {
|
||||
duty: 2
|
||||
pitch: [0x50, 0x50, 0x50, 0x40, 0x40, 0x40, 0x30, 0x30, 0x30, 0x28]
|
||||
volume: [14, 13, 12, 14, 13, 12, 14, 13, 12, 10]
|
||||
}
|
||||
|
||||
// Same shape but descending for player B — immediately
|
||||
// distinguishable from CheerA without reading the screen.
|
||||
sfx CheerB {
|
||||
duty: 2
|
||||
pitch: [0x30, 0x30, 0x30, 0x40, 0x40, 0x40, 0x50, 0x50, 0x50, 0x60]
|
||||
volume: [14, 13, 12, 14, 13, 12, 14, 13, 12, 10]
|
||||
}
|
||||
|
||||
// Exciting two-part pitch sweep for the "WAR!" tie-break. A
|
||||
// rising trill followed by a descending burst. The volume ramps
|
||||
// loud-soft-loud to really stand out from the calmer round sfx.
|
||||
sfx WarFlash {
|
||||
duty: 3
|
||||
pitch: [0x80, 0x60, 0x40, 0x30, 0x20, 0x20, 0x30, 0x40, 0x60, 0x80, 0x60, 0x40, 0x20, 0x20, 0x20, 0x20]
|
||||
volume: [15, 13, 11, 9, 8, 10, 12, 14, 15, 13, 11, 9, 7, 5, 3, 1]
|
||||
}
|
||||
|
||||
// Low noise thump for each card buried during a war.
|
||||
sfx ThudDown {
|
||||
channel: noise
|
||||
pitch: 8
|
||||
volume: [15, 11, 7, 3]
|
||||
}
|
||||
|
||||
// ── Music ──────────────────────────────────────────────────
|
||||
|
||||
// Brisk 4/4 march on pulse 2 for the title screen. The pattern is
|
||||
// a military-style tonic-dominant alternation (C - G - C - G) with
|
||||
// a quick triplet pickup into each down-beat, evoking the rolling
|
||||
// snare of a war drum. Every note is short and staccato so the
|
||||
// melody feels like it's being played on a single high-pitched
|
||||
// fife over an implied drum line.
|
||||
//
|
||||
// Four bars (two repeats of a two-bar phrase) that loop
|
||||
// seamlessly.
|
||||
music TitleTheme {
|
||||
duty: 2
|
||||
volume: 10
|
||||
repeat: true
|
||||
tempo: 8
|
||||
notes: [
|
||||
// bar 1 — C major down-beat with pickup triplet
|
||||
G4 4, G4 4, C5 8, G4 8, E4 8, C4 8,
|
||||
// bar 2 — dominant with rising triplet
|
||||
G4 4, G4 4, G4 8, C5 8, E5 8, G4 8,
|
||||
// bar 3 — tonic inversion, military flourish
|
||||
C5 4, G4 4, E5 8, C5 8, G4 8, E4 8,
|
||||
// bar 4 — resolution back to tonic
|
||||
G4 4, G4 4, C5 8, G4 8, C4 16,
|
||||
rest 8
|
||||
]
|
||||
}
|
||||
57
examples/war/background.ne
Normal file
57
examples/war/background.ne
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// war/background.ne — the static 32×30 nametable loaded at reset.
|
||||
//
|
||||
// NEScript loads the *first* declared `background` into nametable 0
|
||||
// before rendering is enabled, so whatever this file declares is
|
||||
// visible on frame 0 of the ROM. We use a single felt-table
|
||||
// background: a solid dark-green field for the play area. All UI
|
||||
// (title banner, "PRESS A", win banners, etc.) is drawn on top via
|
||||
// sprites so we never pay the cost of a full mid-frame nametable
|
||||
// swap.
|
||||
//
|
||||
// Tile 0 is the linker's builtin smiley — we don't want that in the
|
||||
// game, but it's safe to leave in the top-left corner because the
|
||||
// whole nametable is painted with a blank tile. Use the legend's
|
||||
// `.` entry to map every visible cell to tile 0 then rely on
|
||||
// sub-palette 0 (forest/green/mint) making it look like felt.
|
||||
//
|
||||
// The `palette_map:` field is omitted on purpose: every attribute
|
||||
// byte defaults to 0, which selects bg sub-palette 0 for every
|
||||
// metatile. That's the felt palette.
|
||||
|
||||
background Felt {
|
||||
legend {
|
||||
".": 75 // TILE_FELT_BG — sparse mint flecks on dk_green
|
||||
}
|
||||
map: [
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................"
|
||||
]
|
||||
}
|
||||
30
examples/war/compare.ne
Normal file
30
examples/war/compare.ne
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// war/compare.ne — card rank/suit extraction and round resolution.
|
||||
//
|
||||
// Cards are packed as `(rank << 4) | suit`; the extractors are
|
||||
// one shift or one mask each.
|
||||
//
|
||||
// Parameter names are unique across the entire program — see
|
||||
// COMPILER_BUGS.md §1b for why same-named params in different
|
||||
// functions silently corrupt each other through shared VarIds.
|
||||
|
||||
inline fun card_rank(crk_c: u8) -> u8 {
|
||||
return crk_c >> 4
|
||||
}
|
||||
|
||||
inline fun card_suit(csu_c: u8) -> u8 {
|
||||
return csu_c & 0x0F
|
||||
}
|
||||
|
||||
// Compare two cards by rank. Returns:
|
||||
// 1 if A wins, 2 if B wins, 0 if they tie
|
||||
fun compare_cards(cmp_a: u8, cmp_b: u8) -> u8 {
|
||||
var cmp_ra: u8 = card_rank(cmp_a)
|
||||
var cmp_rb: u8 = card_rank(cmp_b)
|
||||
if cmp_ra > cmp_rb {
|
||||
return 1
|
||||
}
|
||||
if cmp_rb > cmp_ra {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
170
examples/war/constants.ne
Normal file
170
examples/war/constants.ne
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// war/constants.ne — gameplay and layout constants.
|
||||
//
|
||||
// Everything that feeds the game's positional layout, animation
|
||||
// timing, phase machine, or card encoding lives here so one central
|
||||
// edit can retune the whole game. No code, just `const` entries.
|
||||
|
||||
// ── Card encoding ─────────────────────────────────────────
|
||||
//
|
||||
// Each card in a deck is packed into one u8:
|
||||
//
|
||||
// high nibble = rank, 1..13 (A=1, 2..10, J=11, Q=12, K=13)
|
||||
// low nibble = suit, 0..3 (0=♠, 1=♥, 2=♦, 3=♣)
|
||||
//
|
||||
// Rank nibble lives in bits 7..4 and extracts cleanly via >>4;
|
||||
// suit nibble lives in bits 3..0 and extracts via &0x0F. No
|
||||
// division, no multiplication.
|
||||
const SUIT_SPADE: u8 = 0
|
||||
const SUIT_HEART: u8 = 1
|
||||
const SUIT_DIAMOND: u8 = 2
|
||||
const SUIT_CLUB: u8 = 3
|
||||
|
||||
const RANK_ACE: u8 = 1
|
||||
const RANK_JACK: u8 = 11
|
||||
const RANK_QUEEN: u8 = 12
|
||||
const RANK_KING: u8 = 13
|
||||
|
||||
const DECK_SIZE: u8 = 52 // standard 52-card deck
|
||||
const HALF_DECK: u8 = 26 // starting count per player
|
||||
|
||||
// ── Tile index base pointers into the Tileset sprite ─────
|
||||
//
|
||||
// The Tileset sprite (declared in war/assets.ne) stacks every
|
||||
// custom tile vertically in reading order, so tile index N in a
|
||||
// `draw Tileset frame: N` call picks the N'th 8×8 block.
|
||||
//
|
||||
// Tile 0 is reserved by the linker for the builtin smiley, so
|
||||
// every custom tile starts at 1. The ranges below must match the
|
||||
// order the Tileset body lists them in — if you move a tile,
|
||||
// move its constant too.
|
||||
|
||||
// Card frame (shared across every face-up card)
|
||||
const TILE_FRAME_TL: u8 = 1 // top-left corner with inset
|
||||
const TILE_FRAME_TR: u8 = 2 // top-right corner
|
||||
const TILE_FRAME_BL: u8 = 3 // bottom-left corner
|
||||
const TILE_FRAME_BR: u8 = 4 // bottom-right corner
|
||||
const TILE_FRAME_BLANK_L: u8 = 5 // left-half blank cell (white body)
|
||||
const TILE_FRAME_BLANK_R: u8 = 6 // right-half blank cell
|
||||
|
||||
// Card back — 4 tiles arranged as a 2x2 repeating diamond lattice
|
||||
const TILE_BACK_TL: u8 = 7
|
||||
const TILE_BACK_TR: u8 = 8
|
||||
const TILE_BACK_BL: u8 = 9
|
||||
const TILE_BACK_BR: u8 = 10
|
||||
|
||||
// Rank glyphs: one tile per rank, indexed by RANK_TILE_BASE + rank - 1
|
||||
// so rank 1 (Ace) lands at RANK_TILE_BASE + 0.
|
||||
const TILE_RANK_BASE: u8 = 11 // 13 tiles: A, 2..10, J, Q, K
|
||||
|
||||
// Small suit glyphs for the card corner, one per suit.
|
||||
const TILE_SUIT_SMALL_BASE: u8 = 24 // 4 tiles: ♠ ♥ ♦ ♣
|
||||
|
||||
// Big centre-pip, authored as a 16×8 pair per suit (left half + right half).
|
||||
const TILE_PIP_L_BASE: u8 = 28 // 4 tiles
|
||||
const TILE_PIP_R_BASE: u8 = 32 // 4 tiles
|
||||
|
||||
// Alphanumerics (8×8) used for all on-screen text that lives on the
|
||||
// sprite layer. Letters A-Z then digits 0-9, contiguous.
|
||||
const TILE_LETTER_BASE: u8 = 36 // 26 letters: A=+0, Z=+25
|
||||
const TILE_DIGIT_BASE: u8 = 62 // 10 digits: 0=+0, 9=+9
|
||||
|
||||
// UI bits
|
||||
const TILE_CURSOR: u8 = 72 // right-pointing arrow for menus
|
||||
const TILE_HEART_TINY: u8 = 73 // tiny pip used as a victory marker
|
||||
const TILE_DOT: u8 = 74 // single 8x8 card stack marker
|
||||
const TILE_FELT_BG: u8 = 75 // subtle felt cross-hatch (bg fill)
|
||||
|
||||
// Big WAR title letters — each letter is a 2x2 block of tiles
|
||||
// addressed by its quadrant.
|
||||
const TILE_BIG_W_TL: u8 = 76
|
||||
const TILE_BIG_W_TR: u8 = 77
|
||||
const TILE_BIG_W_BL: u8 = 78
|
||||
const TILE_BIG_W_BR: u8 = 79
|
||||
const TILE_BIG_A_TL: u8 = 80
|
||||
const TILE_BIG_A_TR: u8 = 81
|
||||
const TILE_BIG_A_BL: u8 = 82
|
||||
const TILE_BIG_A_BR: u8 = 83
|
||||
const TILE_BIG_R_TL: u8 = 84
|
||||
const TILE_BIG_R_TR: u8 = 85
|
||||
const TILE_BIG_R_BL: u8 = 86
|
||||
const TILE_BIG_R_BR: u8 = 87
|
||||
|
||||
// ── Screen-space layout ──────────────────────────────────
|
||||
//
|
||||
// Positions are in pixels. The screen is 256×240.
|
||||
// Cards are 16 wide × 24 tall (2 cols × 3 rows of 8×8 tiles).
|
||||
const CARD_W: u8 = 16
|
||||
const CARD_H: u8 = 24
|
||||
|
||||
// Decks sit on the upper half of the play area; face-up cards sit
|
||||
// on the lower half so the two bands never share scanlines.
|
||||
//
|
||||
// The X and Y deltas are deliberately exactly 64 px so the
|
||||
// per-frame step animation (FLY_STEP * FRAMES_FLY = 4 * 16 = 64)
|
||||
// lands a flying card exactly on its destination — no rounding,
|
||||
// no overshoot, no need to clamp at the end.
|
||||
//
|
||||
// DECK_A_X -> PLAY_A_X = 32 -> 96 (Δ = +64)
|
||||
// DECK_B_X -> PLAY_B_X = 208 -> 144 (Δ = -64)
|
||||
// DECK_Y -> PLAY_Y = 64 -> 128 (Δ = +64)
|
||||
const DECK_Y: u8 = 64 // top edge of deck card-back sprite
|
||||
const PLAY_Y: u8 = 128 // top edge of face-up card sprite
|
||||
|
||||
const DECK_A_X: u8 = 32 // left deck
|
||||
const DECK_B_X: u8 = 208 // right deck
|
||||
const PLAY_A_X: u8 = 96 // centre-left face-up slot
|
||||
const PLAY_B_X: u8 = 144 // centre-right face-up slot
|
||||
|
||||
// War banner is centred horizontally in the middle of the screen.
|
||||
// Coordinates are the top-left of the first banner sprite.
|
||||
const BANNER_X: u8 = 104
|
||||
const BANNER_Y: u8 = 96
|
||||
|
||||
// HUD card counts sit above each deck.
|
||||
const COUNT_A_X: u8 = 32
|
||||
const COUNT_B_X: u8 = 208
|
||||
const COUNT_Y: u8 = 56
|
||||
|
||||
// ── Animation timing ─────────────────────────────────────
|
||||
//
|
||||
// Every animation uses a power-of-two frame count so the lerp
|
||||
// (delta * t / FRAMES) can be compiled into a shift-right by the
|
||||
// optimizer, avoiding the software multiply/divide warning.
|
||||
const FRAMES_FLY: u8 = 16 // card-draw and win-return slides
|
||||
const FRAMES_BURY: u8 = 8 // faster bury animation during a war
|
||||
const FRAMES_REVEAL:u8 = 32 // held pause after both cards are up
|
||||
const FRAMES_BANNER:u8 = 48 // "WAR!" banner dwell
|
||||
const FRAMES_DEAL_STEP: u8 = 4 // one dealt card every N frames
|
||||
|
||||
// Title auto-advance for the headless jsnes harness: the menu
|
||||
// confirms "0 PLAYERS" automatically after this many frames of no
|
||||
// input so the golden capture at frame 180 always lands in
|
||||
// gameplay.
|
||||
const TITLE_AUTO_FRAMES: u8 = 45
|
||||
|
||||
// CPU "thinking" delay before it draws a card.
|
||||
const CPU_THINK_FRAMES: u8 = 20
|
||||
|
||||
// Victory auto-return timer.
|
||||
const VICTORY_LINGER_FRAMES: u8 = 180
|
||||
|
||||
// ── Phase values for the Playing state's inner machine ───
|
||||
//
|
||||
// A plain `enum` would collide with other `Title` / `Victory` /
|
||||
// etc identifiers, so we use explicit constants instead.
|
||||
const P_WAIT_A: u8 = 0
|
||||
const P_FLY_A: u8 = 1
|
||||
const P_WAIT_B: u8 = 2
|
||||
const P_FLY_B: u8 = 3
|
||||
const P_REVEAL: u8 = 4
|
||||
const P_RESOLVE: u8 = 5
|
||||
const P_WIN_A: u8 = 6
|
||||
const P_WIN_B: u8 = 7
|
||||
const P_WAR_BANNER:u8 = 8
|
||||
const P_WAR_BURY: u8 = 9
|
||||
const P_CHECK: u8 = 10
|
||||
|
||||
// Game-mode bookkeeping: 0 / 1 / 2 players selected from the title.
|
||||
const MODE_CPU_VS_CPU: u8 = 0
|
||||
const MODE_HUMAN_VS_CPU: u8 = 1
|
||||
const MODE_HUMAN_VS_HUMAN: u8 = 2
|
||||
88
examples/war/deal_state.ne
Normal file
88
examples/war/deal_state.ne
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// war/deal_state.ne — the Deal state.
|
||||
//
|
||||
// Shuffles the deck on entry, then runs a brief dealing animation
|
||||
// before transitioning into Playing. The animation shows a single
|
||||
// face-down card sprite alternating between A's deck and B's deck
|
||||
// while a FlipCard sfx clicks on each dealt step. The deck counts
|
||||
// tick up alongside so it looks like the stacks are actually
|
||||
// growing.
|
||||
//
|
||||
// This state is short (52 * FRAMES_DEAL_STEP / 2 = 104 frames per
|
||||
// full deal at FRAMES_DEAL_STEP = 4). The jsnes harness captures
|
||||
// at frame 180 so it will see roughly "halfway through the deal"
|
||||
// unless we accelerate things. We compromise by dealing one card
|
||||
// every 2 frames so the full deal lasts ~52 frames, giving
|
||||
// Playing time to show meaningful content before frame 180.
|
||||
|
||||
state Deal {
|
||||
on enter {
|
||||
init_and_shuffle_decks()
|
||||
// Visually pretend both decks start empty and grow during
|
||||
// the animation — we animate a `visible_count` counter
|
||||
// on each side. The underlying deck_*_count starts at
|
||||
// HALF_DECK after init_and_shuffle_decks; we override the
|
||||
// on-screen count via deal_next.
|
||||
deal_next = 0
|
||||
deal_timer = 0
|
||||
}
|
||||
|
||||
on frame {
|
||||
global_tick += 1
|
||||
deal_timer += 1
|
||||
|
||||
// ── Dealing tick ─────────────────────────────────
|
||||
// Deal one card every 2 frames until we've laid down
|
||||
// all 52. Play a FlipCard sfx on each dealt step for
|
||||
// the rhythmic click.
|
||||
if deal_timer >= 2 {
|
||||
deal_timer = 0
|
||||
if deal_next < DECK_SIZE {
|
||||
deal_next += 1
|
||||
play FlipCard
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rendering ────────────────────────────────────
|
||||
// Draw the "table" furniture: two deck stacks in their
|
||||
// resting position and the running card counts. The
|
||||
// deal_next counter controls how many of the 52 have
|
||||
// "landed", so the first half goes to A and the second
|
||||
// half to B — matching the actual split_decks() logic.
|
||||
var dl_dealt_a: u8 = deal_next
|
||||
var dl_dealt_b: u8 = 0
|
||||
if dl_dealt_a > HALF_DECK {
|
||||
dl_dealt_b = dl_dealt_a - HALF_DECK
|
||||
dl_dealt_a = HALF_DECK
|
||||
}
|
||||
|
||||
// Both decks drawn as card backs whenever they have at
|
||||
// least one card. Before that, skip the draw so the slot
|
||||
// is empty.
|
||||
if dl_dealt_a > 0 {
|
||||
draw_card_back(DECK_A_X, DECK_Y)
|
||||
}
|
||||
if dl_dealt_b > 0 {
|
||||
draw_card_back(DECK_B_X, DECK_Y)
|
||||
}
|
||||
|
||||
draw_count(COUNT_A_X, COUNT_Y, dl_dealt_a)
|
||||
draw_count(COUNT_B_X, COUNT_Y, dl_dealt_b)
|
||||
|
||||
// ── Flying card ──────────────────────────────────
|
||||
// A single face-down card bouncing between the centre
|
||||
// and each deck. The x position alternates based on the
|
||||
// low bit of deal_next (even → going to A, odd → B).
|
||||
if deal_next < DECK_SIZE {
|
||||
var dl_fly_x: u8 = DECK_A_X + 32
|
||||
if (deal_next & 1) != 0 {
|
||||
dl_fly_x = DECK_B_X - 32
|
||||
}
|
||||
draw_card_back(dl_fly_x, 96)
|
||||
}
|
||||
|
||||
// ── Transition ───────────────────────────────────
|
||||
if deal_next >= DECK_SIZE {
|
||||
transition Playing
|
||||
}
|
||||
}
|
||||
}
|
||||
188
examples/war/deck.ne
Normal file
188
examples/war/deck.ne
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
// 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()
|
||||
}
|
||||
311
examples/war/play_state.ne
Normal file
311
examples/war/play_state.ne
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
// war/play_state.ne — the Playing state and its inner phase machine.
|
||||
//
|
||||
// `phase` cycles through the P_* constants defined in
|
||||
// war/constants.ne. `phase_timer` counts frames inside the
|
||||
// current phase and is reset to 0 whenever the phase changes.
|
||||
//
|
||||
// All function-local var names are prefixed with the function's
|
||||
// short name (or with `pf_` for "play frame") so the global
|
||||
// symbol table stays collision-free.
|
||||
|
||||
// Set the current phase and zero the timer in one shot. Inlined
|
||||
// so each call site is just two stores.
|
||||
inline fun set_phase(p: u8) {
|
||||
phase = p
|
||||
phase_timer = 0
|
||||
}
|
||||
|
||||
// Draw the steady-state table furniture: the two deck card backs
|
||||
// (if non-empty) and the running card counts. Used as the base
|
||||
// layer every frame; phase-specific sprites are layered on top.
|
||||
fun draw_table() {
|
||||
if deck_a_count > 0 {
|
||||
draw_card_back(DECK_A_X, DECK_Y)
|
||||
}
|
||||
if deck_b_count > 0 {
|
||||
draw_card_back(DECK_B_X, DECK_Y)
|
||||
}
|
||||
draw_count(COUNT_A_X, COUNT_Y, deck_a_count)
|
||||
draw_count(COUNT_B_X, COUNT_Y, deck_b_count)
|
||||
}
|
||||
|
||||
// Bury helper for a war: move one card from the deck into the
|
||||
// pot (face-down). Must be called with a non-empty deck. Two
|
||||
// near-identical helpers (one per side) keep the locals
|
||||
// uniquely-named.
|
||||
fun bury_from_a() {
|
||||
var bfa_c: u8 = draw_front_a()
|
||||
push_back_pot(bfa_c)
|
||||
}
|
||||
fun bury_from_b() {
|
||||
var bfb_c: u8 = draw_front_b()
|
||||
push_back_pot(bfb_c)
|
||||
}
|
||||
|
||||
// Draw the BIG WAR banner — three 16x16 metasprites at the
|
||||
// centre of the screen. 12 sprites total, drawn in the centre
|
||||
// row so they don't conflict with the deck stacks (rows 64-87)
|
||||
// or the face-up cards (rows 128-151).
|
||||
//
|
||||
// All offsets stepped through locals to dodge the `x + N`
|
||||
// parameter aliasing bug; see draw_word_player.
|
||||
fun draw_big_war_banner(x: u8, y: u8) {
|
||||
var bwb_y1: u8 = y + 8
|
||||
var bwb_x1: u8 = x + 8
|
||||
var bwb_x2: u8 = x + 20
|
||||
var bwb_x3: u8 = x + 28
|
||||
var bwb_x4: u8 = x + 40
|
||||
var bwb_x5: u8 = x + 48
|
||||
// BIG W
|
||||
draw Tileset at: (x, y) frame: TILE_BIG_W_TL
|
||||
draw Tileset at: (bwb_x1, y) frame: TILE_BIG_W_TR
|
||||
draw Tileset at: (x, bwb_y1) frame: TILE_BIG_W_BL
|
||||
draw Tileset at: (bwb_x1, bwb_y1) frame: TILE_BIG_W_BR
|
||||
// BIG A
|
||||
draw Tileset at: (bwb_x2, y) frame: TILE_BIG_A_TL
|
||||
draw Tileset at: (bwb_x3, y) frame: TILE_BIG_A_TR
|
||||
draw Tileset at: (bwb_x2, bwb_y1) frame: TILE_BIG_A_BL
|
||||
draw Tileset at: (bwb_x3, bwb_y1) frame: TILE_BIG_A_BR
|
||||
// BIG R
|
||||
draw Tileset at: (bwb_x4, y) frame: TILE_BIG_R_TL
|
||||
draw Tileset at: (bwb_x5, y) frame: TILE_BIG_R_TR
|
||||
draw Tileset at: (bwb_x4, bwb_y1) frame: TILE_BIG_R_BL
|
||||
draw Tileset at: (bwb_x5, bwb_y1) frame: TILE_BIG_R_BR
|
||||
}
|
||||
|
||||
// Begin the A-side draw animation: pull the top card off deck_a,
|
||||
// stash it as the face-up `card_a`, arm the fly state for the
|
||||
// deck → play slide, and play the click sfx.
|
||||
//
|
||||
// fly_card / fly_face_up are stuffed directly into globals
|
||||
// instead of being passed to arm_fly, because arm_fly only takes
|
||||
// 4 params (the v0.1 ABI limit) and silently drops anything past
|
||||
// the fourth.
|
||||
fun begin_draw_a() {
|
||||
if deck_a_count > 0 {
|
||||
card_a = draw_front_a()
|
||||
fly_card = card_a
|
||||
fly_face_up = 1
|
||||
// dx_sign 0 = move right (DECK_A_X = 32 → PLAY_A_X = 96).
|
||||
// dy_sign 0 = move down (DECK_Y = 64 → PLAY_Y = 128).
|
||||
arm_fly(DECK_A_X, DECK_Y, 0, 0)
|
||||
play FlipCard
|
||||
set_phase(P_FLY_A)
|
||||
}
|
||||
}
|
||||
|
||||
// Begin the B-side draw. dx_sign 1 = move left
|
||||
// (DECK_B_X = 208 → PLAY_B_X = 144). dy_sign 0 = move down.
|
||||
fun begin_draw_b() {
|
||||
if deck_b_count > 0 {
|
||||
card_b = draw_front_b()
|
||||
fly_card = card_b
|
||||
fly_face_up = 1
|
||||
arm_fly(DECK_B_X, DECK_Y, 1, 0)
|
||||
play FlipCard
|
||||
set_phase(P_FLY_B)
|
||||
}
|
||||
}
|
||||
|
||||
state Playing {
|
||||
on enter {
|
||||
set_phase(P_WAIT_A)
|
||||
card_a = 0
|
||||
card_b = 0
|
||||
pot_count = 0
|
||||
}
|
||||
|
||||
on frame {
|
||||
global_tick += 1
|
||||
phase_timer += 1
|
||||
draw_table()
|
||||
|
||||
// ── Phase dispatch ───────────────────────────────
|
||||
// The phases share a simple "timer hits target, advance"
|
||||
// shape. Each arm of the if-chain is self-contained and
|
||||
// ends either with a set_phase call or a fall-through
|
||||
// that waits for more input / time.
|
||||
|
||||
if phase == P_WAIT_A {
|
||||
// Human prompt: hint blink above the deck.
|
||||
if a_is_cpu == 0 {
|
||||
if (phase_timer & 32) == 0 {
|
||||
draw_word_press(8, 200)
|
||||
}
|
||||
if button.a or button.start {
|
||||
begin_draw_a()
|
||||
}
|
||||
} else {
|
||||
// CPU draws after a short delay.
|
||||
if phase_timer >= CPU_THINK_FRAMES {
|
||||
begin_draw_a()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_FLY_A {
|
||||
step_fly_pos()
|
||||
draw_flying_card(fly_x, fly_y)
|
||||
if phase_timer >= FRAMES_FLY {
|
||||
set_phase(P_WAIT_B)
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_WAIT_B {
|
||||
// A's card is now parked in its play slot.
|
||||
draw_card_face(PLAY_A_X, PLAY_Y, card_a)
|
||||
if b_is_cpu == 0 {
|
||||
if (phase_timer & 32) == 0 {
|
||||
draw_word_press(208, 200)
|
||||
}
|
||||
if p2.button.a or p2.button.start or button.a or button.start {
|
||||
begin_draw_b()
|
||||
}
|
||||
} else {
|
||||
if phase_timer >= CPU_THINK_FRAMES {
|
||||
begin_draw_b()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_FLY_B {
|
||||
// A is in place; B is flying.
|
||||
draw_card_face(PLAY_A_X, PLAY_Y, card_a)
|
||||
step_fly_pos()
|
||||
draw_flying_card(fly_x, fly_y)
|
||||
if phase_timer >= FRAMES_FLY {
|
||||
set_phase(P_REVEAL)
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_REVEAL {
|
||||
draw_card_face(PLAY_A_X, PLAY_Y, card_a)
|
||||
draw_card_face(PLAY_B_X, PLAY_Y, card_b)
|
||||
if phase_timer >= FRAMES_REVEAL {
|
||||
set_phase(P_RESOLVE)
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_RESOLVE {
|
||||
// Both cards go into the pot regardless of outcome.
|
||||
push_back_pot(card_a)
|
||||
push_back_pot(card_b)
|
||||
var pf_r: u8 = compare_cards(card_a, card_b)
|
||||
if pf_r == 1 {
|
||||
play CheerA
|
||||
set_phase(P_WIN_A)
|
||||
}
|
||||
if pf_r == 2 {
|
||||
play CheerB
|
||||
set_phase(P_WIN_B)
|
||||
}
|
||||
if pf_r == 0 {
|
||||
// It's a tie — but only enter the war flow if both
|
||||
// sides actually have cards left to bury. If a
|
||||
// player ran out of cards on this very tie, the
|
||||
// OTHER player wins by default and takes the pot.
|
||||
if deck_a_count == 0 {
|
||||
pot_to_b()
|
||||
winner = 1
|
||||
transition Victory
|
||||
}
|
||||
if deck_b_count == 0 {
|
||||
pot_to_a()
|
||||
winner = 0
|
||||
transition Victory
|
||||
}
|
||||
play WarFlash
|
||||
set_phase(P_WAR_BANNER)
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_WIN_A {
|
||||
draw_card_face(PLAY_A_X, PLAY_Y, card_a)
|
||||
draw_card_face(PLAY_B_X, PLAY_Y, card_b)
|
||||
if phase_timer >= FRAMES_FLY {
|
||||
pot_to_a()
|
||||
set_phase(P_CHECK)
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_WIN_B {
|
||||
draw_card_face(PLAY_A_X, PLAY_Y, card_a)
|
||||
draw_card_face(PLAY_B_X, PLAY_Y, card_b)
|
||||
if phase_timer >= FRAMES_FLY {
|
||||
pot_to_b()
|
||||
set_phase(P_CHECK)
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_WAR_BANNER {
|
||||
draw_card_face(PLAY_A_X, PLAY_Y, card_a)
|
||||
draw_card_face(PLAY_B_X, PLAY_Y, card_b)
|
||||
// Flashing big "WAR" banner — only drawn on alternate
|
||||
// 8-frame windows so the title strobes for emphasis.
|
||||
if (phase_timer & 8) != 0 {
|
||||
draw_big_war_banner(96, 80)
|
||||
}
|
||||
if phase_timer >= FRAMES_BANNER {
|
||||
set_phase(P_WAR_BURY)
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_WAR_BURY {
|
||||
// Bury up to 3 face-down cards from each deck, then
|
||||
// draw a new face-up pair. We don't animate each
|
||||
// individual buried card; just play a noise thump
|
||||
// per buried card and advance the counters.
|
||||
if phase_timer == 1 {
|
||||
if deck_a_count > 0 { bury_from_a() }
|
||||
if deck_b_count > 0 { bury_from_b() }
|
||||
play ThudDown
|
||||
}
|
||||
if phase_timer == 4 {
|
||||
if deck_a_count > 0 { bury_from_a() }
|
||||
if deck_b_count > 0 { bury_from_b() }
|
||||
play ThudDown
|
||||
}
|
||||
if phase_timer == 7 {
|
||||
if deck_a_count > 0 { bury_from_a() }
|
||||
if deck_b_count > 0 { bury_from_b() }
|
||||
play ThudDown
|
||||
}
|
||||
if phase_timer == 10 {
|
||||
// Draw new face-ups for the comparison. If either
|
||||
// side has run out of cards, the OTHER side wins
|
||||
// and takes the entire pot — we transition straight
|
||||
// to Victory.
|
||||
if deck_a_count == 0 {
|
||||
pot_to_b()
|
||||
winner = 1
|
||||
transition Victory
|
||||
}
|
||||
if deck_b_count == 0 {
|
||||
pot_to_a()
|
||||
winner = 0
|
||||
transition Victory
|
||||
}
|
||||
card_a = draw_front_a()
|
||||
card_b = draw_front_b()
|
||||
}
|
||||
if phase_timer >= FRAMES_BURY + 16 {
|
||||
set_phase(P_REVEAL)
|
||||
}
|
||||
}
|
||||
|
||||
if phase == P_CHECK {
|
||||
if deck_a_count == 0 {
|
||||
winner = 1
|
||||
transition Victory
|
||||
}
|
||||
if deck_b_count == 0 {
|
||||
winner = 0
|
||||
transition Victory
|
||||
}
|
||||
// No winner yet — start the next round.
|
||||
card_a = 0
|
||||
card_b = 0
|
||||
set_phase(P_WAIT_A)
|
||||
}
|
||||
}
|
||||
}
|
||||
226
examples/war/render.ne
Normal file
226
examples/war/render.ne
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
// war/render.ne — card, digit, and text rendering helpers.
|
||||
//
|
||||
// Every function in this file is a thin wrapper around one or
|
||||
// more `draw Tileset at: (x, y) frame: N` calls that writes to
|
||||
// the runtime OAM cursor. Each `draw` takes one sprite slot, so
|
||||
// a 16×24 card face burns 6 sprite slots, a single-character
|
||||
// letter burns 1, and so on. The caller is responsible for
|
||||
// sprite budgeting — see PLAN.md §3.
|
||||
//
|
||||
// Function-local var names are prefixed with the function's
|
||||
// short name to avoid the global symbol-table collisions that
|
||||
// E0501 would otherwise complain about.
|
||||
|
||||
// ── Card face ──────────────────────────────────────────────
|
||||
//
|
||||
// Draws the 6-sprite card face at (x, y) for the packed card
|
||||
// byte. Layout (each cell is 8×8):
|
||||
//
|
||||
// [rank ][small_suit] row 0
|
||||
// [pipL ][pipR ] row 1
|
||||
// [blankL][blankR ] row 2
|
||||
//
|
||||
// Rank is the condensed glyph at TILE_RANK_BASE + (rank - 1);
|
||||
// small suit + big-pip halves are indexed directly off their
|
||||
// base constants.
|
||||
fun draw_card_face(dcf_in_x: u8, dcf_in_y: u8, dcf_in_card: u8) {
|
||||
// Snapshot every parameter into a fresh local *before* any
|
||||
// nested function call. NEScript v0.1 passes parameters via
|
||||
// fixed zero-page slots ($04, $05, $06), and any inner call
|
||||
// overwrites those slots with its own parameter values —
|
||||
// including the inline `card_rank` / `card_suit` calls below,
|
||||
// which would otherwise leave `x` and `y` corrupted by the
|
||||
// time the draw lines run.
|
||||
var dcf_x: u8 = dcf_in_x
|
||||
var dcf_y: u8 = dcf_in_y
|
||||
var dcf_card: u8 = dcf_in_card
|
||||
var dcf_rank: u8 = card_rank(dcf_card)
|
||||
var dcf_suit: u8 = card_suit(dcf_card)
|
||||
var dcf_rank_tile: u8 = TILE_RANK_BASE + dcf_rank - 1
|
||||
var dcf_small_tile: u8 = TILE_SUIT_SMALL_BASE + dcf_suit
|
||||
var dcf_pipl_tile: u8 = TILE_PIP_L_BASE + dcf_suit
|
||||
var dcf_pipr_tile: u8 = TILE_PIP_R_BASE + dcf_suit
|
||||
var dcf_x1: u8 = dcf_x + 8
|
||||
var dcf_y1: u8 = dcf_y + 8
|
||||
var dcf_y2: u8 = dcf_y + 16
|
||||
// Row 0 — rank corner + small suit
|
||||
draw Tileset at: (dcf_x, dcf_y) frame: dcf_rank_tile
|
||||
draw Tileset at: (dcf_x1, dcf_y) frame: dcf_small_tile
|
||||
// Row 1 — big centre pip (left + right halves)
|
||||
draw Tileset at: (dcf_x, dcf_y1) frame: dcf_pipl_tile
|
||||
draw Tileset at: (dcf_x1, dcf_y1) frame: dcf_pipr_tile
|
||||
// Row 2 — blank bottom so the card body is symmetric
|
||||
draw Tileset at: (dcf_x, dcf_y2) frame: TILE_FRAME_BLANK_L
|
||||
draw Tileset at: (dcf_x1, dcf_y2) frame: TILE_FRAME_BLANK_R
|
||||
}
|
||||
|
||||
// Draw the card-back lattice at (x, y). 6 sprites again. Rows
|
||||
// 0 and 2 reuse the same top/bottom back tiles; row 1 uses the
|
||||
// bottom row of the lattice as a filler so the pattern stays
|
||||
// continuous.
|
||||
fun draw_card_back(x: u8, y: u8) {
|
||||
var dcb_x1: u8 = x + 8
|
||||
var dcb_y1: u8 = y + 8
|
||||
var dcb_y2: u8 = y + 16
|
||||
draw Tileset at: (x, y) frame: TILE_BACK_TL
|
||||
draw Tileset at: (dcb_x1, y) frame: TILE_BACK_TR
|
||||
draw Tileset at: (x, dcb_y1) frame: TILE_BACK_BL
|
||||
draw Tileset at: (dcb_x1, dcb_y1) frame: TILE_BACK_BR
|
||||
draw Tileset at: (x, dcb_y2) frame: TILE_BACK_TL
|
||||
draw Tileset at: (dcb_x1, dcb_y2) frame: TILE_BACK_TR
|
||||
}
|
||||
|
||||
// Dispatch between front and back based on the fly_face_up flag
|
||||
// stamped by the phase handlers. Snapshots params first because
|
||||
// the inner call clobbers $04/$05.
|
||||
fun draw_flying_card(x: u8, y: u8) {
|
||||
var dfc_x: u8 = x
|
||||
var dfc_y: u8 = y
|
||||
if fly_face_up == 0 {
|
||||
draw_card_back(dfc_x, dfc_y)
|
||||
} else {
|
||||
draw_card_face(dfc_x, dfc_y, fly_card)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Digits + text ─────────────────────────────────────────
|
||||
|
||||
// Draw a single decimal digit 0..9 at (x, y).
|
||||
fun draw_digit(x: u8, y: u8, d: u8) {
|
||||
draw Tileset at: (x, y) frame: TILE_DIGIT_BASE + d
|
||||
}
|
||||
|
||||
// Draw a two-digit decimal count (0..99) at (x, y). Leading zero
|
||||
// is preserved so the HUD always renders two glyphs wide, which
|
||||
// keeps the layout stable as the counts change.
|
||||
fun draw_count(x: u8, y: u8, v: u8) {
|
||||
var dct_tens: u8 = 0
|
||||
var dct_n: u8 = v
|
||||
// Divide by 10 without the software divide: repeatedly
|
||||
// subtract 10 until n < 10. For v ≤ 52 this loops at most
|
||||
// 5 times, cheaper than calling `/` and `%`.
|
||||
while dct_n >= 10 {
|
||||
dct_n -= 10
|
||||
dct_tens += 1
|
||||
}
|
||||
draw_digit(x, y, dct_tens)
|
||||
draw_digit(x + 8, y, dct_n)
|
||||
}
|
||||
|
||||
// Draw a single letter 'A'..'Z' using the sprite font. `ch` is
|
||||
// the letter index (0 = A, 25 = Z). Deliberately NOT marked
|
||||
// `inline`: when this was inlined the resulting code put each
|
||||
// inlined `draw` at double the intended X step (the inliner
|
||||
// appears to re-evaluate the (x + N) parameter expression in a
|
||||
// way that compounds across consecutive draws). Keeping it as
|
||||
// a real function call gives every draw_letter call its own
|
||||
// argument-evaluation context and the spacing comes out right.
|
||||
fun draw_letter(x: u8, y: u8, ch: u8) {
|
||||
draw Tileset at: (x, y) frame: TILE_LETTER_BASE + ch
|
||||
}
|
||||
|
||||
// Short helper for drawing the word "PLAYER" at (x, y). 6 letters.
|
||||
// Used by the HUD and the victory banner.
|
||||
//
|
||||
// We accumulate the X position in a local instead of passing
|
||||
// `x + N` as a call argument: the latter pattern miscompiles in
|
||||
// NEScript v0.1 (consecutive `x + N` arguments to the same
|
||||
// function appear to alias the parameter slot, leaving the second
|
||||
// onward call site reading a stale offset). Stepping a local
|
||||
// avoids the issue entirely.
|
||||
fun draw_word_player(x: u8, y: u8) {
|
||||
var dwp_px: u8 = x
|
||||
draw_letter(dwp_px, y, 15) // P
|
||||
dwp_px += 8
|
||||
draw_letter(dwp_px, y, 11) // L
|
||||
dwp_px += 8
|
||||
draw_letter(dwp_px, y, 0) // A
|
||||
dwp_px += 8
|
||||
draw_letter(dwp_px, y, 24) // Y
|
||||
dwp_px += 8
|
||||
draw_letter(dwp_px, y, 4) // E
|
||||
dwp_px += 8
|
||||
draw_letter(dwp_px, y, 17) // R
|
||||
}
|
||||
|
||||
// "WINS" — used on the victory screen.
|
||||
fun draw_word_wins(x: u8, y: u8) {
|
||||
var dww_px: u8 = x
|
||||
draw_letter(dww_px, y, 22) // W
|
||||
dww_px += 8
|
||||
draw_letter(dww_px, y, 8) // I
|
||||
dww_px += 8
|
||||
draw_letter(dww_px, y, 13) // N
|
||||
dww_px += 8
|
||||
draw_letter(dww_px, y, 18) // S
|
||||
}
|
||||
|
||||
// "WAR" — the centred flash during a tie-break.
|
||||
fun draw_word_war(x: u8, y: u8) {
|
||||
var dwa_px: u8 = x
|
||||
draw_letter(dwa_px, y, 22) // W
|
||||
dwa_px += 8
|
||||
draw_letter(dwa_px, y, 0) // A
|
||||
dwa_px += 8
|
||||
draw_letter(dwa_px, y, 17) // R
|
||||
}
|
||||
|
||||
// "PRESS" — used on the title screen.
|
||||
fun draw_word_press(x: u8, y: u8) {
|
||||
var dwr_px: u8 = x
|
||||
draw_letter(dwr_px, y, 15) // P
|
||||
dwr_px += 8
|
||||
draw_letter(dwr_px, y, 17) // R
|
||||
dwr_px += 8
|
||||
draw_letter(dwr_px, y, 4) // E
|
||||
dwr_px += 8
|
||||
draw_letter(dwr_px, y, 18) // S
|
||||
dwr_px += 8
|
||||
draw_letter(dwr_px, y, 18) // S
|
||||
}
|
||||
|
||||
// ── Fly animation driver ──────────────────────────────────
|
||||
//
|
||||
// We avoid the software multiply (and the W0101 "expensive
|
||||
// multiply" warning) by stepping the fly position by a fixed
|
||||
// constant each frame instead of computing `start + dx * t /
|
||||
// FRAMES`. The constant FLY_STEP is chosen so that
|
||||
// FRAMES_FLY * FLY_STEP equals the deck-to-play distance on
|
||||
// both axes:
|
||||
//
|
||||
// FRAMES_FLY * FLY_STEP = 64 px (16 * 4)
|
||||
//
|
||||
// The screen layout (DECK_*_X, PLAY_*_X, DECK_Y, PLAY_Y) is
|
||||
// arranged so every animation traverses exactly 64 px on both
|
||||
// axes, which keeps the per-frame step uniform.
|
||||
const FLY_STEP: u8 = 4
|
||||
|
||||
// Step the global (fly_x, fly_y) by FLY_STEP in the directions
|
||||
// stored in fly_dx_sign / fly_dy_sign. Sign 0 = positive (move
|
||||
// right / down), 1 = negative (move left / up). Called once per
|
||||
// frame inside the relevant fly-phase handler.
|
||||
fun step_fly_pos() {
|
||||
if fly_dx_sign == 0 {
|
||||
fly_x += FLY_STEP
|
||||
} else {
|
||||
fly_x -= FLY_STEP
|
||||
}
|
||||
if fly_dy_sign == 0 {
|
||||
fly_y += FLY_STEP
|
||||
} else {
|
||||
fly_y -= FLY_STEP
|
||||
}
|
||||
}
|
||||
|
||||
// Initialise the fly state for a card slide from (sx, sy) using
|
||||
// the given direction signs. NEScript v0.1 only allocates four
|
||||
// zero-page slots ($04-$07) for function parameters, so callers
|
||||
// must stash `fly_card` and `fly_face_up` directly into the
|
||||
// globals before invoking arm_fly — passing them as the 5th and
|
||||
// 6th params would silently drop the values on the floor.
|
||||
fun arm_fly(sx: u8, sy: u8, dxsign: u8, dysign: u8) {
|
||||
fly_x = sx
|
||||
fly_y = sy
|
||||
fly_dx_sign = dxsign
|
||||
fly_dy_sign = dysign
|
||||
}
|
||||
41
examples/war/rng.ne
Normal file
41
examples/war/rng.ne
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// war/rng.ne — 8-bit Galois LFSR pseudo-random number generator.
|
||||
//
|
||||
// A classic taps = 0xB8 Galois LFSR: on every call, shift the
|
||||
// state right by one and XOR with the taps polynomial if the old
|
||||
// low bit was 1. The period is 255 (all non-zero states), which
|
||||
// is plenty for a card shuffle.
|
||||
//
|
||||
// The state lives in the global `rng_state` byte declared in
|
||||
// war/state.ne. Seeded from a running tick counter on title exit
|
||||
// (so the jsnes golden harness gets a deterministic shuffle).
|
||||
|
||||
// Advance the LFSR one step and return the new state. Every caller
|
||||
// treats the return value as the next random byte.
|
||||
fun rand_u8() -> u8 {
|
||||
var s: u8 = rng_state
|
||||
var lsb: u8 = s & 1
|
||||
s = s >> 1
|
||||
if lsb != 0 {
|
||||
s = s ^ 0xB8
|
||||
}
|
||||
// Guard against the degenerate all-zero state: if we ever
|
||||
// roll into zero the LFSR is stuck, so reseed from a fixed
|
||||
// non-zero constant. In practice this only happens on a
|
||||
// bad initial seed — we start at 0xA7 so the cycle stays
|
||||
// healthy.
|
||||
if s == 0 {
|
||||
s = 0xA7
|
||||
}
|
||||
rng_state = s
|
||||
return s
|
||||
}
|
||||
|
||||
// Seed the LFSR from an arbitrary byte. Zero is remapped to the
|
||||
// same fallback constant rand_u8() uses so the period stays 255.
|
||||
fun rng_seed(seed: u8) {
|
||||
if seed == 0 {
|
||||
rng_state = 0xA7
|
||||
} else {
|
||||
rng_state = seed
|
||||
}
|
||||
}
|
||||
82
examples/war/state.ne
Normal file
82
examples/war/state.ne
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// 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
|
||||
144
examples/war/title_state.ne
Normal file
144
examples/war/title_state.ne
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// war/title_state.ne — the Title state.
|
||||
//
|
||||
// Draws a big "WAR" banner, a 3-option menu with a cursor, and a
|
||||
// blinking "PRESS A" prompt. Autopilot: if the player doesn't
|
||||
// press anything for TITLE_AUTO_FRAMES frames the menu auto-
|
||||
// commits to "0 PLAYERS" so the jsnes golden capture reaches
|
||||
// gameplay by frame 180.
|
||||
//
|
||||
// The cursor navigates with D-pad up/down; A or Start confirms.
|
||||
// Debouncing is done via title_debounce — one key press per
|
||||
// press rather than per frame held.
|
||||
|
||||
state Title {
|
||||
on enter {
|
||||
title_cursor = 1
|
||||
title_timer = 0
|
||||
title_blink = 0
|
||||
title_debounce = 0
|
||||
start_music TitleTheme
|
||||
}
|
||||
|
||||
on frame {
|
||||
global_tick += 1
|
||||
title_timer += 1
|
||||
title_blink += 1
|
||||
if title_blink >= 60 {
|
||||
title_blink = 0
|
||||
}
|
||||
if title_debounce > 0 {
|
||||
title_debounce -= 1
|
||||
}
|
||||
|
||||
// ── Big "WAR" title banner ───────────────────────
|
||||
// Each letter is a 2x2 block of 16x16 BIG tiles.
|
||||
// Three letters at 16px wide + 4px gap = 16*3 + 4*2 = 56px;
|
||||
// centred at x = (256 - 56)/2 = 100.
|
||||
// y = 32 puts the banner in the top third of the screen.
|
||||
// BIG W
|
||||
draw Tileset at: (100, 32) frame: TILE_BIG_W_TL
|
||||
draw Tileset at: (108, 32) frame: TILE_BIG_W_TR
|
||||
draw Tileset at: (100, 40) frame: TILE_BIG_W_BL
|
||||
draw Tileset at: (108, 40) frame: TILE_BIG_W_BR
|
||||
// BIG A
|
||||
draw Tileset at: (120, 32) frame: TILE_BIG_A_TL
|
||||
draw Tileset at: (128, 32) frame: TILE_BIG_A_TR
|
||||
draw Tileset at: (120, 40) frame: TILE_BIG_A_BL
|
||||
draw Tileset at: (128, 40) frame: TILE_BIG_A_BR
|
||||
// BIG R
|
||||
draw Tileset at: (140, 32) frame: TILE_BIG_R_TL
|
||||
draw Tileset at: (148, 32) frame: TILE_BIG_R_TR
|
||||
draw Tileset at: (140, 40) frame: TILE_BIG_R_BL
|
||||
draw Tileset at: (148, 40) frame: TILE_BIG_R_BR
|
||||
|
||||
// Subtitle "CARD GAME" in 8x8 font under the banner.
|
||||
// 9 letters incl. the embedded space → 9 sprites per row,
|
||||
// which over-runs the 8-per-scanline limit. Splitting the
|
||||
// subtitle across two y rows (offset by 8px) keeps each
|
||||
// scanline under the limit.
|
||||
draw_letter(96, 64, 2) // C
|
||||
draw_letter(104, 64, 0) // A
|
||||
draw_letter(112, 64, 17) // R
|
||||
draw_letter(120, 64, 3) // D
|
||||
draw_letter(136, 64, 6) // G
|
||||
draw_letter(144, 64, 0) // A
|
||||
draw_letter(152, 64, 12) // M
|
||||
draw_letter(160, 64, 4) // E
|
||||
|
||||
// ── Menu options ─────────────────────────────────
|
||||
// Three lines vertically stacked. Each row is "X PLAYER"
|
||||
// (no S — plural is implied) so the row never exceeds 7
|
||||
// sprites and stays under the per-scanline limit even
|
||||
// when the cursor sprite shares the same row.
|
||||
draw_digit(88, 104, 0)
|
||||
draw_word_player(104, 104)
|
||||
draw_digit(88, 120, 1)
|
||||
draw_word_player(104, 120)
|
||||
draw_digit(88, 136, 2)
|
||||
draw_word_player(104, 136)
|
||||
|
||||
// Cursor sits to the left of the selected option.
|
||||
var cursor_y: u8 = 104 + (title_cursor << 4) // 104, 120, 136
|
||||
draw Tileset at: (72, cursor_y) frame: TILE_CURSOR
|
||||
|
||||
// ── Blinking "PRESS A" prompt ────────────────────
|
||||
if title_blink < 30 {
|
||||
draw_word_press(96, 184)
|
||||
draw_letter(144, 184, 0) // A
|
||||
}
|
||||
|
||||
|
||||
// ── Input handling ───────────────────────────────
|
||||
if title_debounce == 0 {
|
||||
if button.up {
|
||||
if title_cursor > 0 {
|
||||
title_cursor -= 1
|
||||
}
|
||||
title_debounce = 10
|
||||
title_timer = 0
|
||||
play FlipCard
|
||||
}
|
||||
if button.down {
|
||||
if title_cursor < 2 {
|
||||
title_cursor += 1
|
||||
}
|
||||
title_debounce = 10
|
||||
title_timer = 0
|
||||
play FlipCard
|
||||
}
|
||||
if button.a or button.start {
|
||||
// Commit the selection and seed the RNG from the
|
||||
// current global tick so each user-visible start
|
||||
// produces a different shuffle.
|
||||
mode = title_cursor
|
||||
rng_seed((global_tick & 0xFF) as u8)
|
||||
transition Deal
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autopilot for the headless harness ───────────
|
||||
if title_timer >= TITLE_AUTO_FRAMES {
|
||||
mode = 0
|
||||
rng_seed((global_tick & 0xFF) as u8)
|
||||
transition Deal
|
||||
}
|
||||
}
|
||||
|
||||
on exit {
|
||||
stop_music
|
||||
// Translate the menu selection into the two a_is_cpu /
|
||||
// b_is_cpu flags the play state reads each frame.
|
||||
if mode == MODE_CPU_VS_CPU {
|
||||
a_is_cpu = 1
|
||||
b_is_cpu = 1
|
||||
}
|
||||
if mode == MODE_HUMAN_VS_CPU {
|
||||
a_is_cpu = 0
|
||||
b_is_cpu = 1
|
||||
}
|
||||
if mode == MODE_HUMAN_VS_HUMAN {
|
||||
a_is_cpu = 0
|
||||
b_is_cpu = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
73
examples/war/victory_state.ne
Normal file
73
examples/war/victory_state.ne
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// war/victory_state.ne — the Victory state.
|
||||
//
|
||||
// Big "PLAYER A WINS" or "PLAYER B WINS" banner, the builtin
|
||||
// `fanfare` music, an auto-return to Title after
|
||||
// VICTORY_LINGER_FRAMES frames, and a manual "press A to skip"
|
||||
// path.
|
||||
|
||||
state Victory {
|
||||
on enter {
|
||||
victory_timer = 0
|
||||
start_music fanfare
|
||||
}
|
||||
|
||||
on frame {
|
||||
global_tick += 1
|
||||
victory_timer += 1
|
||||
|
||||
// ── Banner ──────────────────────────────────────
|
||||
// "PLAYER X" on row 1, "WINS" on row 2. Splitting the
|
||||
// banner across two y rows keeps each scanline under the
|
||||
// NES's 8-sprite-per-scanline limit (the "PLAYER X WINS"
|
||||
// single-line version was 11 sprites tall and dropped
|
||||
// letters past the 8th).
|
||||
draw_word_player(64, 80)
|
||||
if winner == 0 {
|
||||
draw_letter(120, 80, 0) // A
|
||||
} else {
|
||||
draw_letter(120, 80, 1) // B
|
||||
}
|
||||
draw_word_wins(96, 96)
|
||||
|
||||
// Flourish: draw the winning deck's top card in the
|
||||
// middle of the screen as a victory showcase. If the
|
||||
// deck is empty (shouldn't happen — we only arrive
|
||||
// here because the *loser* is empty) fall back to a
|
||||
// card back so the layout is stable.
|
||||
if winner == 0 {
|
||||
if deck_a_count > 0 {
|
||||
var vic_top_a: u8 = deck_a[deck_a_front]
|
||||
draw_card_face(120, 128, vic_top_a)
|
||||
} else {
|
||||
draw_card_back(120, 128)
|
||||
}
|
||||
} else {
|
||||
if deck_b_count > 0 {
|
||||
var vic_top_b: u8 = deck_b[deck_b_front]
|
||||
draw_card_face(120, 128, vic_top_b)
|
||||
} else {
|
||||
draw_card_back(120, 128)
|
||||
}
|
||||
}
|
||||
|
||||
// Row of tiny hearts under the banner.
|
||||
draw Tileset at: (80, 168) frame: TILE_HEART_TINY
|
||||
draw Tileset at: (96, 168) frame: TILE_HEART_TINY
|
||||
draw Tileset at: (112, 168) frame: TILE_HEART_TINY
|
||||
draw Tileset at: (128, 168) frame: TILE_HEART_TINY
|
||||
draw Tileset at: (144, 168) frame: TILE_HEART_TINY
|
||||
draw Tileset at: (160, 168) frame: TILE_HEART_TINY
|
||||
|
||||
// ── Input + auto-return ─────────────────────────
|
||||
if button.a or button.start {
|
||||
transition Title
|
||||
}
|
||||
if victory_timer >= VICTORY_LINGER_FRAMES {
|
||||
transition Title
|
||||
}
|
||||
}
|
||||
|
||||
on exit {
|
||||
stop_music
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue