mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
Fills the biggest feature-coverage gaps in the existing example set:
- match_demo.ne — match statement over a Screen enum,
driving a title / playing / paused /
game-over flow with a debounced controller.
- loop_break_continue.ne — `loop { ... }` with `break` and `continue`,
scanning an enemy array for the first hit.
- logic_ops.ne — keyword-based `and` / `or` / `not` gating
movement and scoring on alive/paused flags.
- bitwise_ops.ne — packed status-byte flags with `&` / `|` /
`^` / `>>` plus a health-bar render loop.
- scanline_split.ne — MMC3 `on scanline(120)` handler rewriting
the scroll register mid-frame for a
classic status-bar split.
All 14 examples (9 existing + 5 new) pass the jsnes smoke test
(`14/14 ROMs rendered successfully`) and still pass `cargo fmt`,
`cargo clippy -D warnings`, and `cargo test`.
Known limitations surfaced while authoring these examples, to be
fixed in follow-up commits:
1. Array-literal global initializers (`var xs: u8[4] = [1,2,3,4]`)
are silently dropped by `lower_program` — `eval_const` returns
None for `Expr::ArrayLiteral` and no synthetic per-element
init code is emitted. Affects `arrays_and_functions`,
`structs_enums_for`, `loop_break_continue`, and any future
array-using example. Arrays effectively boot at all-zero.
2. `draw` inside a loop body reuses one static OAM slot —
`next_oam_slot` increments at IR-codegen time rather than at
runtime, so N iterations all write to the same 4-byte OAM
entry. Affects `arrays_and_functions`, `structs_enums_for`,
`bitwise_ops` (health pips), and any loop that wants to
render per-iteration sprites.
Both bugs are latent and didn't surface until I tried to write
examples that exercise the relevant features — the existing
integration tests only check iNES header structure, and the
jsnes smoke test's "at least one sprite rendered" bar is
satisfied by one sprite even when several were intended.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
108 lines
3.2 KiB
Text
108 lines
3.2 KiB
Text
// Loop / Break / Continue — demonstrates `loop`, `break`, and
|
|
// `continue` via a linear scan that finds the first "hazard" the
|
|
// player is overlapping.
|
|
//
|
|
// NEScript's sprite allocator assigns a static OAM slot per `draw`
|
|
// statement at compile time, so a `draw` inside a loop body would
|
|
// reuse the same slot on every iteration. We instead use the loop
|
|
// to COMPUTE state — "is the player touching a hazard, and which
|
|
// one?" — and then fold that result into how (and whether) the
|
|
// single player sprite is drawn. The hazards themselves get one
|
|
// unrolled `draw` per slot so each hazard gets its own OAM entry.
|
|
//
|
|
// Features shown:
|
|
// - `loop { ... }` as an infinite loop
|
|
// - `break` to exit on first match
|
|
// - `continue` to skip inactive slots
|
|
// - array indexing with a loop-carried variable
|
|
//
|
|
// Build: cargo run -- build examples/loop_break_continue.ne
|
|
|
|
game "Loop Demo" {
|
|
mapper: NROM
|
|
}
|
|
|
|
const NUM_HAZARDS: u8 = 4
|
|
const HIT_RADIUS: u8 = 12
|
|
|
|
// Four hazard slots. `active` is 1 if the slot is live.
|
|
var active: u8[4] = [1, 1, 0, 1]
|
|
var hazard_x: u8[4] = [40, 96, 160, 200]
|
|
var hazard_y: u8[4] = [60, 140, 100, 180]
|
|
|
|
var px: u8 = 120
|
|
var py: u8 = 120
|
|
|
|
// Search results, written by the loop each frame.
|
|
var hit: u8 = 0 // 1 if the player is touching any hazard
|
|
var hit_idx: u8 = 0 // which slot was hit (only meaningful if hit == 1)
|
|
|
|
// Small absolute-difference helper. Saves lines at each call site.
|
|
inline fun abs_diff(a: u8, b: u8) -> u8 {
|
|
if a > b {
|
|
return a - b
|
|
}
|
|
return b - a
|
|
}
|
|
|
|
on frame {
|
|
// Player movement.
|
|
if button.right { px += 1 }
|
|
if button.left { px -= 1 }
|
|
if button.down { py += 1 }
|
|
if button.up { py -= 1 }
|
|
|
|
// Linear scan for the first active hazard the player is
|
|
// touching. The loop exits via `break` on the first hit, or
|
|
// by running i past the end.
|
|
hit = 0
|
|
var i: u8 = 0
|
|
loop {
|
|
if i >= NUM_HAZARDS {
|
|
break
|
|
}
|
|
if active[i] == 0 {
|
|
// Skip inactive slots — this is where `continue`
|
|
// shines: the increment and re-test happen at the top.
|
|
i += 1
|
|
continue
|
|
}
|
|
|
|
var dx: u8 = abs_diff(px, hazard_x[i])
|
|
var dy: u8 = abs_diff(py, hazard_y[i])
|
|
if dx < HIT_RADIUS {
|
|
if dy < HIT_RADIUS {
|
|
hit = 1
|
|
hit_idx = i
|
|
break
|
|
}
|
|
}
|
|
i += 1
|
|
}
|
|
|
|
// One draw per hazard slot — unrolled so each sprite gets its
|
|
// own static OAM entry. Inactive slots draw off-screen ($FE)
|
|
// so they don't flicker the visible ones.
|
|
if active[0] == 1 {
|
|
draw Hazard at: (hazard_x[0], hazard_y[0])
|
|
}
|
|
if active[1] == 1 {
|
|
draw Hazard at: (hazard_x[1], hazard_y[1])
|
|
}
|
|
if active[2] == 1 {
|
|
draw Hazard at: (hazard_x[2], hazard_y[2])
|
|
}
|
|
if active[3] == 1 {
|
|
draw Hazard at: (hazard_x[3], hazard_y[3])
|
|
}
|
|
|
|
// The player sprite reflects the collision state: when hit,
|
|
// draw a "bang" marker above the player head. The `hit_idx`
|
|
// is unused visually but proves the loop ran to completion.
|
|
draw Player at: (px, py)
|
|
if hit == 1 {
|
|
draw Spark at: (px, py - 8)
|
|
}
|
|
}
|
|
|
|
start Main
|