mirror of
https://github.com/imjasonh/nescript
synced 2026-07-15 20:58:20 +00:00
compiler: fix three scoping bugs; war: revert all local/param workarounds
Three related scoping bugs from examples/war/COMPILER_BUGS.md,
all fixed in one pass because they're different layer
manifestations of the same "flat global namespace" problem:
## §3: function-local `var` declarations lived in one namespace
`src/analyzer/mod.rs::register_var` inserted every `var` it
saw — top-level, state-local, AND function-body local — into
the same `self.symbols: HashMap<String, Symbol>`. Two different
functions declaring `var i` collided on E0501, which is why
every local in war/*.ne had a function-prefix like `dfa_card`
or `dwp_px`.
Fix: add a `current_scope_prefix: Option<String>` to the
Analyzer, set it to `Some("<fn_name>")` when checking a
function body (or `Some("Title__frame")` for state handler
bodies), and have `register_var` store the declaration under
an internal key `"__local__{prefix}__{name}"`. New
`resolve_symbol` / `resolve_key` helpers try the
scope-qualified key first and fall back to the bare key for
globals / consts / enum variants / state-level vars / function
names. Every existing `self.symbols.get(name)` inside
body-checking code was swapped over.
Two `var i` declarations inside the SAME function body still
collide with E0501 — we scoped per function body, not per
nested block. Per-block scoping would require live-range
analysis to reuse RAM slots.
## §1b: same-named params across functions shared VarIds
`src/ir/lowering.rs::get_or_create_var` looked up names in a
single global `var_map`, so two functions both with a `card:
u8` parameter resolved to the same `VarId`. Whichever function
was lowered last won the zero-page slot mapping, silently
rerouting the other function's param reads to the wrong slot.
Fix: the IR lowerer now mirrors the analyzer's scope logic.
`LoweringContext` gains a `current_scope_prefix` field that
gets set in `lower_function` / `lower_handler`, and
`get_or_create_var` uses a new `scoped_key` helper that
prepends `"__local__{prefix}__"` when the qualified key exists
in `var_map` or `var_types`. Each function's parameters and
locals therefore get distinct VarIds, and the codegen's
`var_addrs` map naturally has no collisions.
## §2: param transport slots $04-$07 clobbered across nested JSRs
Parameters were passed AND kept in `$04-$07` for the lifetime
of a function. Any nested call overwrote those slots with its
own arguments, so the caller's params were silently corrupted
as soon as it invoked anything. Every war helper that took
params and called other helpers (draw_card_face, push_back_a,
etc) snapshotted its params into fresh locals at the top of
the body.
Fix: in `codegen/ir_codegen.rs::IrCodeGen::new`, every
function-local — including parameters — now gets a dedicated
per-function RAM slot at `$0300+`. Parameters are still passed
via the zero-page transport slots `$04-$07` as the calling
convention, but `gen_function` now emits a **prologue** at
every function entry:
LDA $04
STA <param_0_addr>
LDA $05
STA <param_1_addr>
... etc, up to 4 ...
By the time the body runs, every parameter lives in the
function's dedicated RAM slot, so any nested call can freely
clobber $04-$07 (writing its own arguments there) without
corrupting the caller's saved parameters. Costs 4 LDA/STA
pairs (≈ 20 bytes of ROM, 16 cycles) at every function entry
— worth it to make the calling convention sound.
## War cleanup
With all three fixes in place, every workaround prefix in
`examples/war/*.ne` is gone:
- `card_rank(card)` instead of `card_rank(crk_c)` — bug #1b
- `compare_cards(a, b)` instead of `compare_cards(cmp_a, cmp_b)`
- `push_back_a(card)` instead of `push_back_a(pba_in)` — bug #1b
- `var card: u8 = draw_front_a()` in bury_from_* — bug #3
- `var i: u8 = 0` freely in multiple functions — bug #3
- `fun push_back_a(card)` body no longer snapshots `card` into
`pba_card` before calling wrap52 — bug #2
- `fun draw_card_face` body no longer snapshots x/y/card into
locals before calling card_rank/card_suit — bug #2
- `draw_word_player` steps its own x without needing a
`dwp_px` accumulator to avoid the `x + N` arg compilation
quirk — that quirk was a downstream symptom of bug #2 and
is also gone
The source is now about 300 lines shorter and significantly
more readable.
## Regression tests
Seven new tests nail these bugs down:
- `analyzer::tests::analyze_allows_same_local_name_in_two_functions`
- `analyzer::tests::analyze_allows_same_param_name_in_two_functions`
- `analyzer::tests::analyze_allows_same_local_name_in_two_state_handlers`
- `analyzer::tests::analyze_still_rejects_duplicate_local_in_same_function`
- `codegen::ir_codegen::gen_function_prologue_spills_params_to_local_ram`
Plus the four param-arity tests from the earlier E0506 fix
and the wide_hi-leak regression test from the previous
compiler fix. Total suite: 591 unit tests, all passing.
## Golden drift
The prologue change adds a few cycles to every function entry,
which shifts NMI sampling by a handful of cycles and flips
the audio-hash of any example that plays sfx or music
(platformer, war). `arrays_and_functions.png` also picks up a
1-pixel shift in its enemy positions due to the same timing
drift. All three golden updates are pure "compiler produces
different but functionally-identical output" — no game
behavior changed.
## What's still open in COMPILER_BUGS.md
- §4: 8-sprites-per-scanline hardware limit is invisible to
user code. A static analyzer hint could help; deferred.
- §5: `inline` keyword is silently declined for short
functions that the optimizer's inliner doesn't recognize
(it only removes empty functions). Deferred pending a real
single-return-expression inlining pass.
https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
This commit is contained in:
parent
e10d09db76
commit
76dd8eacb0
23 changed files with 762 additions and 412 deletions
|
|
@ -2,15 +2,30 @@
|
|||
|
||||
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.
|
||||
reproduction, the symptom we observed, the root cause, the
|
||||
workaround originally used in `examples/war/*.ne`, and the
|
||||
compiler fix that shipped (when shipped).
|
||||
|
||||
## Status summary
|
||||
|
||||
| # | Short name | Status | Fix commit | Regression test |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `fun` with > 4 params silently drops the rest | **FIXED** (E0506 diagnostic) | `analyzer: reject functions with more than 4 parameters (E0506)` | `analyze_rejects_function_with_more_than_4_params`, `analyze_accepts_function_with_exactly_4_params` |
|
||||
| 1b | Same-named params share VarIds across functions | **FIXED** (scope-qualified keys) | `analyzer/ir: scope function locals per function body` | `analyze_allows_same_param_name_in_two_functions` |
|
||||
| 2 | Param transport slots $04-$07 clobbered by nested calls | **FIXED** (codegen prologue spill) | `codegen: spill parameters from $04-$07 into per-function RAM slots` | `codegen::ir_codegen::gen_function_prologue_spills_params_to_local_ram` |
|
||||
| 3 | Function-local `var` declarations share one flat namespace | **FIXED** (scope-qualified keys) | `analyzer/ir: scope function locals per function body` | `analyze_allows_same_local_name_in_two_functions`, `analyze_allows_same_local_name_in_two_state_handlers`, `analyze_still_rejects_duplicate_local_in_same_function` |
|
||||
| 4 | 8-sprites-per-scanline limit invisible to user code | Open (hardware limit; static analyzer hint could help) | — | — |
|
||||
| 5 | `inline` keyword silently declined for short functions | Open | — | — |
|
||||
| 6 | `wide_hi` IR map leaked between functions (u16→u8 aliasing) | **FIXED** (cleared per function) | `ir: clear wide_hi between functions to fix 16-bit op aliasing` | `ir::tests::wide_hi_does_not_leak_between_functions` |
|
||||
|
||||
**Once a fix lands, revert the workaround in `examples/war/*.ne`
|
||||
in the same commit** so the example keeps the game honest and
|
||||
the PR diff visibly proves the fix works end-to-end. Bugs #1,
|
||||
#1b, #2, #3, and #6 have had their workarounds reverted.
|
||||
|
||||
---
|
||||
|
||||
## 1. Functions with more than 4 parameters silently corrupt the 5th+
|
||||
## 1. Functions with more than 4 parameters silently corrupt the 5th+ *(FIXED)*
|
||||
|
||||
### Symptom
|
||||
|
||||
|
|
@ -94,7 +109,7 @@ Two reasonable options:
|
|||
|
||||
---
|
||||
|
||||
## 1b. Function parameters with the same name in different functions share a VarId, which collides their zero-page slot mapping
|
||||
## 1b. Function parameters with the same name in different functions share a VarId, which collides their zero-page slot mapping *(FIXED)*
|
||||
|
||||
### Symptom
|
||||
|
||||
|
|
@ -171,24 +186,35 @@ function (see bug #3); we extended the same scheme to params:
|
|||
`pbb_card` / `dcf_card` snapshots from bug #2 stay because they
|
||||
also help with the bug-2 clobbering.
|
||||
|
||||
### Fix proposal
|
||||
### Fix
|
||||
|
||||
Two layers to fix in:
|
||||
Both the analyzer and the IR lowerer now qualify function-body
|
||||
`var` / parameter declarations with the enclosing function name
|
||||
(or state handler name) under an internal key
|
||||
`"__local__{scope}__{name}"`. Each function's locals and
|
||||
parameters therefore get **distinct** symbol-table entries and
|
||||
VarIds even when the source names collide.
|
||||
|
||||
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.
|
||||
Lookups inside a function body go through
|
||||
`Analyzer::resolve_symbol` / `LoweringContext::scoped_key`,
|
||||
which prefer the scope-qualified key over the bare one — so
|
||||
a function-local `var x` correctly shadows a same-named global
|
||||
(or another function's `var x`).
|
||||
|
||||
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.
|
||||
State-level locals (declared at `state Foo { var x: u8 }`
|
||||
outside any handler) stay in the global namespace so every
|
||||
handler in the state can read/write them across frames.
|
||||
|
||||
Either fix alone is probably enough; both together is robust.
|
||||
See `src/analyzer/mod.rs::resolve_symbol` / `resolve_key` /
|
||||
`scoped_name` and `src/ir/lowering.rs::scoped_key`.
|
||||
|
||||
Together with fix #2 below, bugs #1b and #2 are completely
|
||||
gone: the workaround-prefixed locals and params in `war/*.ne`
|
||||
(the `dcf_`, `dwp_`, `pba_`, etc tags) are all reverted.
|
||||
|
||||
---
|
||||
|
||||
## 2. Function parameters share zero-page slots with nested calls — values clobbered across `JSR`
|
||||
## 2. Function parameters share zero-page slots with nested calls — values clobbered across `JSR` *(FIXED)*
|
||||
|
||||
### Symptom
|
||||
|
||||
|
|
@ -231,24 +257,38 @@ 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
|
||||
### Fix
|
||||
|
||||
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.
|
||||
`codegen::ir_codegen::IrCodeGen::new` now allocates every
|
||||
function-local — including its parameters — into a dedicated
|
||||
per-function RAM slot at `$0300+`. Parameters are still passed
|
||||
via the zero-page transport slots `$04-$07` as the calling
|
||||
convention, but `gen_function` now emits a 4-instruction
|
||||
**prologue** at every function entry:
|
||||
|
||||
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.
|
||||
```
|
||||
LDA $04 ; transport slot 0
|
||||
STA <param_0_addr>
|
||||
LDA $05 ; transport slot 1
|
||||
STA <param_1_addr>
|
||||
... etc ...
|
||||
```
|
||||
|
||||
Either fix would let users write straightforward function bodies
|
||||
without having to remember the snapshot dance.
|
||||
By the time the body runs, every parameter lives in the
|
||||
function's dedicated RAM slot, so any nested call can freely
|
||||
clobber `$04-$07` (passing its own arguments to _its_ callee)
|
||||
without corrupting the caller's saved parameters.
|
||||
|
||||
The cost is 4 LDA/STA pairs at every function entry (≈ 20
|
||||
bytes of ROM, 16 cycles). Worth it to make the calling
|
||||
convention sound.
|
||||
|
||||
See `codegen::ir_codegen::gen_function_prologue_spills_params_to_local_ram`
|
||||
for the regression test.
|
||||
|
||||
---
|
||||
|
||||
## 3. Function-local variable names are in a flat global namespace
|
||||
## 3. Function-local variable names are in a flat global namespace *(FIXED)*
|
||||
|
||||
### Symptom
|
||||
|
||||
|
|
@ -302,18 +342,23 @@ identifying its enclosing function (e.g. `dfa_card` in
|
|||
`dwp_px` in `draw_word_player`). This makes long files harder to
|
||||
read but is fully mechanical.
|
||||
|
||||
### Fix proposal
|
||||
### Fix
|
||||
|
||||
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.
|
||||
Same as #1b: the analyzer and IR lowerer now internally
|
||||
qualify function-body `var` declarations with the enclosing
|
||||
scope's name, so `foo`'s `var i` and `bar`'s `var i` resolve
|
||||
to `__local__foo__i` and `__local__bar__i` respectively. The
|
||||
two entries coexist peacefully in the (still-flat) symbol
|
||||
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.
|
||||
What *didn't* change: two `var i` declarations inside the
|
||||
same function body still collide with E0501 (we scoped per
|
||||
function body, not per nested block). That's a deliberate
|
||||
trade-off — per-block scoping would require live-range
|
||||
analysis to reuse RAM slots across blocks, which is a much
|
||||
bigger change. The analyzer test
|
||||
`analyze_still_rejects_duplicate_local_in_same_function`
|
||||
pins this behaviour.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2,28 +2,24 @@
|
|||
//
|
||||
// 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_rank(card: u8) -> u8 {
|
||||
return card >> 4
|
||||
}
|
||||
|
||||
inline fun card_suit(csu_c: u8) -> u8 {
|
||||
return csu_c & 0x0F
|
||||
inline fun card_suit(card: u8) -> u8 {
|
||||
return card & 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 {
|
||||
// 1 if A wins, 2 if B wins, 0 if they tie.
|
||||
fun compare_cards(a: u8, b: u8) -> u8 {
|
||||
var ra: u8 = card_rank(a)
|
||||
var rb: u8 = card_rank(b)
|
||||
if ra > rb {
|
||||
return 1
|
||||
}
|
||||
if cmp_rb > cmp_ra {
|
||||
if rb > ra {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -47,36 +47,36 @@ state Deal {
|
|||
// 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
|
||||
var dealt_a: u8 = deal_next
|
||||
var dealt_b: u8 = 0
|
||||
if dealt_a > HALF_DECK {
|
||||
dealt_b = dealt_a - HALF_DECK
|
||||
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 {
|
||||
if dealt_a > 0 {
|
||||
draw_card_back(DECK_A_X, DECK_Y)
|
||||
}
|
||||
if dl_dealt_b > 0 {
|
||||
if 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)
|
||||
draw_count(COUNT_A_X, COUNT_Y, dealt_a)
|
||||
draw_count(COUNT_B_X, COUNT_Y, 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
|
||||
var fly_x_pos: u8 = DECK_A_X + 32
|
||||
if (deal_next & 1) != 0 {
|
||||
dl_fly_x = DECK_B_X - 32
|
||||
fly_x_pos = DECK_B_X - 32
|
||||
}
|
||||
draw_card_back(dl_fly_x, 96)
|
||||
draw_card_back(fly_x_pos, 96)
|
||||
}
|
||||
|
||||
// ── Transition ───────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -6,12 +6,6 @@
|
|||
// 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 ──────────────────────────
|
||||
//
|
||||
|
|
@ -19,19 +13,11 @@
|
|||
// 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
|
||||
inline fun wrap52(v: u8) -> u8 {
|
||||
if v >= DECK_SIZE {
|
||||
return v - DECK_SIZE
|
||||
}
|
||||
return w52_v
|
||||
return v
|
||||
}
|
||||
|
||||
// ── deck_a ────────────────────────────────────────────────
|
||||
|
|
@ -44,20 +30,15 @@ fun deck_a_empty() -> u8 {
|
|||
}
|
||||
|
||||
fun draw_front_a() -> u8 {
|
||||
var dfa_card: u8 = deck_a[deck_a_front]
|
||||
var card: u8 = deck_a[deck_a_front]
|
||||
deck_a_front = wrap52(deck_a_front + 1)
|
||||
deck_a_count -= 1
|
||||
return dfa_card
|
||||
return 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
|
||||
fun push_back_a(card: u8) {
|
||||
var slot: u8 = wrap52(deck_a_front + deck_a_count)
|
||||
deck_a[slot] = card
|
||||
deck_a_count += 1
|
||||
}
|
||||
|
||||
|
|
@ -71,23 +52,22 @@ fun deck_b_empty() -> u8 {
|
|||
}
|
||||
|
||||
fun draw_front_b() -> u8 {
|
||||
var dfb_card: u8 = deck_b[deck_b_front]
|
||||
var card: u8 = deck_b[deck_b_front]
|
||||
deck_b_front = wrap52(deck_b_front + 1)
|
||||
deck_b_count -= 1
|
||||
return dfb_card
|
||||
return 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
|
||||
fun push_back_b(card: u8) {
|
||||
var slot: u8 = wrap52(deck_b_front + deck_b_count)
|
||||
deck_b[slot] = card
|
||||
deck_b_count += 1
|
||||
}
|
||||
|
||||
// ── pot ───────────────────────────────────────────────────
|
||||
|
||||
fun push_back_pot(pbp_in: u8) {
|
||||
pot[pot_count] = pbp_in
|
||||
fun push_back_pot(card: u8) {
|
||||
pot[pot_count] = card
|
||||
pot_count += 1
|
||||
}
|
||||
|
||||
|
|
@ -98,19 +78,19 @@ fun clear_pot() {
|
|||
// 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
|
||||
var i: u8 = 0
|
||||
while i < pot_count {
|
||||
push_back_a(pot[i])
|
||||
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
|
||||
var i: u8 = 0
|
||||
while i < pot_count {
|
||||
push_back_b(pot[i])
|
||||
i += 1
|
||||
}
|
||||
pot_count = 0
|
||||
}
|
||||
|
|
@ -125,43 +105,42 @@ fun pot_to_b() {
|
|||
// 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).
|
||||
// Fisher-Yates: it does 200 swaps between two random indices,
|
||||
// where each index is rand() & 0x3F (0..63) and the swap is
|
||||
// only done when both indices are < 52. Uses only bitwise ops
|
||||
// (no multiply, no divide).
|
||||
|
||||
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 {
|
||||
var r: u8 = 1
|
||||
var i: u8 = 0
|
||||
while r <= RANK_KING {
|
||||
var s: u8 = 0
|
||||
while 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
|
||||
var packed: u8 = r << 4
|
||||
deck_a[i] = packed | s
|
||||
i += 1
|
||||
s += 1
|
||||
}
|
||||
bmd_r += 1
|
||||
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
|
||||
var k: u8 = 0
|
||||
while k < 200 {
|
||||
var i: u8 = rand_u8() & 0x3F
|
||||
var j: u8 = rand_u8() & 0x3F
|
||||
if i < DECK_SIZE {
|
||||
if j < DECK_SIZE {
|
||||
var tmp: u8 = deck_a[i]
|
||||
deck_a[i] = deck_a[j]
|
||||
deck_a[j] = tmp
|
||||
}
|
||||
}
|
||||
shf_k += 1
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,10 +148,10 @@ fun shuffle_deck_a() {
|
|||
// 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
|
||||
var i: u8 = 0
|
||||
while i < HALF_DECK {
|
||||
deck_b[i] = deck_a[HALF_DECK + i]
|
||||
i += 1
|
||||
}
|
||||
deck_a_front = 0
|
||||
deck_a_count = HALF_DECK
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@
|
|||
// `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.
|
||||
|
|
@ -30,57 +26,46 @@ fun draw_table() {
|
|||
}
|
||||
|
||||
// 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.
|
||||
// pot (face-down). Must be called with a non-empty deck.
|
||||
fun bury_from_a() {
|
||||
var bfa_c: u8 = draw_front_a()
|
||||
push_back_pot(bfa_c)
|
||||
var c: u8 = draw_front_a()
|
||||
push_back_pot(c)
|
||||
}
|
||||
fun bury_from_b() {
|
||||
var bfb_c: u8 = draw_front_b()
|
||||
push_back_pot(bfb_c)
|
||||
var c: u8 = draw_front_b()
|
||||
push_back_pot(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.
|
||||
// Draw the BIG WAR banner — three 16×16 metasprite letters 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).
|
||||
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
|
||||
draw Tileset at: (x, y) frame: TILE_BIG_W_TL
|
||||
draw Tileset at: (x + 8, y) frame: TILE_BIG_W_TR
|
||||
draw Tileset at: (x, y + 8) frame: TILE_BIG_W_BL
|
||||
draw Tileset at: (x + 8, y + 8) 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
|
||||
draw Tileset at: (x + 20, y) frame: TILE_BIG_A_TL
|
||||
draw Tileset at: (x + 28, y) frame: TILE_BIG_A_TR
|
||||
draw Tileset at: (x + 20, y + 8) frame: TILE_BIG_A_BL
|
||||
draw Tileset at: (x + 28, y + 8) 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
|
||||
draw Tileset at: (x + 40, y) frame: TILE_BIG_R_TL
|
||||
draw Tileset at: (x + 48, y) frame: TILE_BIG_R_TR
|
||||
draw Tileset at: (x + 40, y + 8) frame: TILE_BIG_R_BL
|
||||
draw Tileset at: (x + 48, y + 8) 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.
|
||||
// fly_card / fly_face_up are written directly to globals
|
||||
// instead of being passed to arm_fly, because arm_fly already
|
||||
// takes 4 parameters and the v0.1 ABI caps function signatures
|
||||
// at 4 parameters (E0506).
|
||||
fun begin_draw_a() {
|
||||
if deck_a_count > 0 {
|
||||
card_a = draw_front_a()
|
||||
|
|
@ -189,16 +174,16 @@ state Playing {
|
|||
// Both cards go into the pot regardless of outcome.
|
||||
push_back_pot(card_a)
|
||||
push_back_pot(card_b)
|
||||
pf_result = compare_cards(card_a, card_b)
|
||||
if pf_result == 1 {
|
||||
var result: u8 = compare_cards(card_a, card_b)
|
||||
if result == 1 {
|
||||
play CheerA
|
||||
set_phase(P_WIN_A)
|
||||
}
|
||||
if pf_result == 2 {
|
||||
if result == 2 {
|
||||
play CheerB
|
||||
set_phase(P_WIN_B)
|
||||
}
|
||||
if pf_result == 0 {
|
||||
if result == 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
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
// 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 ──────────────────────────────────────────────
|
||||
//
|
||||
|
|
@ -22,69 +18,46 @@
|
|||
//
|
||||
// Every tile in the card has a fully-opaque white background
|
||||
// (palette index 2) so the felt green behind the card does not
|
||||
// bleed through — see `assets.ne` for the art itself. The big
|
||||
// centre pip is a single 16×16 shape split across the bottom
|
||||
// two rows (4 tiles total); `rank` sits in the top-left corner
|
||||
// and `small_suit` in the top-right.
|
||||
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_pip_tl: u8 = TILE_PIP_TL_BASE + dcf_suit
|
||||
var dcf_pip_tr: u8 = TILE_PIP_TR_BASE + dcf_suit
|
||||
var dcf_pip_bl: u8 = TILE_PIP_BL_BASE + dcf_suit
|
||||
var dcf_pip_br: u8 = TILE_PIP_BR_BASE + dcf_suit
|
||||
var dcf_x1: u8 = dcf_x + 8
|
||||
var dcf_y1: u8 = dcf_y + 8
|
||||
var dcf_y2: u8 = dcf_y + 16
|
||||
// bleed through — see `assets.ne` for the art itself.
|
||||
fun draw_card_face(x: u8, y: u8, card: u8) {
|
||||
var rank: u8 = card_rank(card)
|
||||
var suit: u8 = card_suit(card)
|
||||
var rank_tile: u8 = TILE_RANK_BASE + rank - 1
|
||||
var small_tile: u8 = TILE_SUIT_SMALL_BASE + suit
|
||||
var pip_tl: u8 = TILE_PIP_TL_BASE + suit
|
||||
var pip_tr: u8 = TILE_PIP_TR_BASE + suit
|
||||
var pip_bl: u8 = TILE_PIP_BL_BASE + suit
|
||||
var pip_br: u8 = TILE_PIP_BR_BASE + suit
|
||||
// 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
|
||||
draw Tileset at: (x, y) frame: rank_tile
|
||||
draw Tileset at: (x + 8, y) frame: small_tile
|
||||
// Row 1 — top half of the 16×16 big pip
|
||||
draw Tileset at: (dcf_x, dcf_y1) frame: dcf_pip_tl
|
||||
draw Tileset at: (dcf_x1, dcf_y1) frame: dcf_pip_tr
|
||||
draw Tileset at: (x, y + 8) frame: pip_tl
|
||||
draw Tileset at: (x + 8, y + 8) frame: pip_tr
|
||||
// Row 2 — bottom half of the 16×16 big pip
|
||||
draw Tileset at: (dcf_x, dcf_y2) frame: dcf_pip_bl
|
||||
draw Tileset at: (dcf_x1, dcf_y2) frame: dcf_pip_br
|
||||
draw Tileset at: (x, y + 16) frame: pip_bl
|
||||
draw Tileset at: (x + 8, y + 16) frame: pip_br
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Draw the card-back checkerboard at (x, y). 6 sprites. The
|
||||
// back tiles tile seamlessly so every cell of the 16×24 card
|
||||
// body carries a 2-pixel square of the same black/white grid.
|
||||
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
|
||||
draw Tileset at: (x, y) frame: TILE_BACK_TL
|
||||
draw Tileset at: (x + 8, y) frame: TILE_BACK_TR
|
||||
draw Tileset at: (x, y + 8) frame: TILE_BACK_BL
|
||||
draw Tileset at: (x + 8, y + 8) frame: TILE_BACK_BR
|
||||
draw Tileset at: (x, y + 16) frame: TILE_BACK_TL
|
||||
draw Tileset at: (x + 8, y + 16) 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.
|
||||
// stamped by the phase handlers.
|
||||
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)
|
||||
draw_card_back(x, y)
|
||||
} else {
|
||||
draw_card_face(dfc_x, dfc_y, fly_card)
|
||||
draw_card_face(x, y, fly_card)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,91 +68,62 @@ 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.
|
||||
// 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
|
||||
var tens: u8 = 0
|
||||
var 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
|
||||
while n >= 10 {
|
||||
n -= 10
|
||||
tens += 1
|
||||
}
|
||||
draw_digit(x, y, dct_tens)
|
||||
draw_digit(x + 8, y, dct_n)
|
||||
draw_digit(x, y, tens)
|
||||
draw_digit(x + 8, y, 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.
|
||||
// the letter index (0 = A, 25 = Z).
|
||||
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
|
||||
draw_letter(x, y, 15) // P
|
||||
draw_letter(x + 8, y, 11) // L
|
||||
draw_letter(x + 16, y, 0) // A
|
||||
draw_letter(x + 24, y, 24) // Y
|
||||
draw_letter(x + 32, y, 4) // E
|
||||
draw_letter(x + 40, 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
|
||||
draw_letter(x, y, 22) // W
|
||||
draw_letter(x + 8, y, 8) // I
|
||||
draw_letter(x + 16, y, 13) // N
|
||||
draw_letter(x + 24, y, 18) // S
|
||||
}
|
||||
|
||||
// "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
|
||||
draw_letter(x, y, 15) // P
|
||||
draw_letter(x + 8, y, 17) // R
|
||||
draw_letter(x + 16, y, 4) // E
|
||||
draw_letter(x + 24, y, 18) // S
|
||||
draw_letter(x + 32, 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
|
||||
// Avoiding a software multiply: instead of computing
|
||||
// `start + dx * t / FRAMES` we step the fly position by a
|
||||
// fixed constant each frame. FLY_STEP is chosen so that
|
||||
// FRAMES_FLY * FLY_STEP equals the deck-to-play distance on
|
||||
// both axes:
|
||||
//
|
||||
|
|
@ -192,8 +136,8 @@ 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.
|
||||
// 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
|
||||
|
|
@ -208,11 +152,9 @@ fun step_fly_pos() {
|
|||
}
|
||||
|
||||
// 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.
|
||||
// the given direction signs, card byte, and face-up flag. The
|
||||
// caller is responsible for picking signs so the end position
|
||||
// lands where it should after FRAMES_FLY frames.
|
||||
fun arm_fly(sx: u8, sy: u8, dxsign: u8, dysign: u8) {
|
||||
fly_x = sx
|
||||
fly_y = sy
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ var a_is_cpu: u8 = 1 // bool-ish
|
|||
var b_is_cpu: u8 = 1
|
||||
var phase: u8 = 0 // one of the P_* constants
|
||||
var phase_timer: u8 = 0 // counts up during each phase
|
||||
var pf_result: u8 = 0 // P_RESOLVE comparison result (1=A, 2=B, 0=tie)
|
||||
|
||||
// ── Animation state ───────────────────────────────────────
|
||||
// Shared by every card-fly phase. We step (fly_x, fly_y) by a
|
||||
|
|
|
|||
|
|
@ -34,17 +34,21 @@ state Victory {
|
|||
// 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.
|
||||
// Pull the winner's top card into a single local. Only
|
||||
// one `var top` declaration per on-frame body — NEScript
|
||||
// scopes locals per function body, not per if-block.
|
||||
var top: u8 = 0
|
||||
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)
|
||||
top = deck_a[deck_a_front]
|
||||
draw_card_face(120, 128, top)
|
||||
} 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)
|
||||
top = deck_b[deck_b_front]
|
||||
draw_card_face(120, 128, top)
|
||||
} else {
|
||||
draw_card_back(120, 128)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue