diff --git a/examples/comparisons.ne b/examples/comparisons.ne new file mode 100644 index 0000000..81d4a47 --- /dev/null +++ b/examples/comparisons.ne @@ -0,0 +1,50 @@ +// Comparisons — uses every comparison operator (==, !=, <, <=, +// >, >=) to drive a different pip on screen. Each pip only +// lights up when its corresponding comparison is true, so at +// any given moment the set of visible pips is a direct readout +// of which comparisons are passing. +// +// What this exercises end-to-end: +// - All six comparison operators in `if` conditions +// - Their lowering through `CmpKind::{Eq,Ne,Lt,LtEq,Gt,GtEq}` +// in `src/codegen/ir_codegen.rs::gen_cmp` +// - Each one correctly mapping to `BEQ`/`BNE`/`BCC`/`BCS` and +// producing the expected truth value in a branch +// +// The `value` variable ramps from 0 to 255 so the six +// comparisons against MIDPOINT (=128) all fire across one cycle. +// +// Build: cargo run -- build examples/comparisons.ne + +game "Comparisons" { + mapper: NROM +} + +const MIDPOINT: u8 = 128 + +var value: u8 = 0 + +on frame { + // Slowly ramp through 0..255 and wrap. At the wrap the u8 + // overflow resets value to 0 — intentional, the pips will + // re-animate. + value += 1 + + // Always-visible player sprite so the harness has at least + // one solid OAM entry every frame. Its position reflects + // the current ramp value — easy to read at a glance. + draw Player at: (value, 120) + + // Each comparison drives a pip at a fixed X position along + // the top. When its condition is true, the pip draws; when + // false, the draw is skipped and the cursor's next slot + // stays hidden from the OAM clear's $FE Y byte. + if value == MIDPOINT { draw Pip at: ( 32, 16) } + if value != MIDPOINT { draw Pip at: ( 64, 16) } + if value < MIDPOINT { draw Pip at: ( 96, 16) } + if value <= MIDPOINT { draw Pip at: (128, 16) } + if value > MIDPOINT { draw Pip at: (160, 16) } + if value >= MIDPOINT { draw Pip at: (192, 16) } +} + +start Main diff --git a/examples/function_chain.ne b/examples/function_chain.ne new file mode 100644 index 0000000..bf9d3e9 --- /dev/null +++ b/examples/function_chain.ne @@ -0,0 +1,93 @@ +// Function Chain — exercises a deep call graph with parameter +// passing and return values across multiple user functions. +// +// The analyzer caps call depth at 8 (hard NES stack limit for +// a cooperative compiler). This example chains five functions: +// +// frame -> compute -> scale -> clamp -> fold -> taper +// +// Each function takes its argument through the zero-page +// parameter slots ($04-$07), computes a small transform, and +// returns a value in A. The chained result is what drives the +// player sprite's X position on screen each frame. +// +// What this exercises end-to-end: +// - Five levels of nested `JSR` without stack corruption +// - Parameter passing via `$04-$07` between callers +// - Return value propagation through A +// - `fun ... -> u8 { return ... }` — the full typed-function +// shape, including an early `return` inside an `if` +// - Interaction of function calls with handler-local vars +// (the `out` result ends up in a local that drives draw) +// +// Build: cargo run -- build examples/function_chain.ne + +game "Fn Chain" { + mapper: NROM +} + +const SCREEN_MIN: u8 = 16 +const SCREEN_MAX: u8 = 232 + +var tick: u8 = 0 + +// Level 5: final transform — fold the input by reflecting any +// overshoot back toward the middle. Pure function, returns u8. +fun taper(v: u8) -> u8 { + if v > 200 { + return 200 + } + return v +} + +// Level 4: fold — bias the input toward the screen center. +fun fold(v: u8) -> u8 { + var biased: u8 = v + if biased < SCREEN_MIN { + biased = SCREEN_MIN + } + return taper(biased) +} + +// Level 3: clamp to the visible screen band. +fun clamp(v: u8) -> u8 { + if v > SCREEN_MAX { + return fold(SCREEN_MAX) + } + return fold(v) +} + +// Level 2: scale the tick into a pixel position. Uses a shift +// instead of multiply so we don't pull in the soft multiply. +fun scale(t: u8) -> u8 { + return clamp(t << 1) +} + +// Level 1: top of the call chain. Takes the raw frame counter, +// adds a small offset, and hands it to `scale`. The returned +// value is the player's X position. +fun compute(counter: u8) -> u8 { + var shifted: u8 = counter + shifted += 16 + return scale(shifted) +} + +on frame { + tick += 1 + + // Single call site that triggers the whole chain. If any + // link in the chain corrupts the param passing or stack, + // the player sprite starts jittering or disappears. + var x: u8 = compute(tick) + + // Player Y is fixed; X comes from the chain. Visually the + // sprite sweeps across the screen as the chain holds. + draw Player at: (x, 112) + + // Also draw a static reference marker so the smoke test + // always has at least one visible sprite even if the chain + // somehow returns 0. + draw Marker at: (8, 8) +} + +start Main diff --git a/examples/mmc3_per_state_split.ne b/examples/mmc3_per_state_split.ne new file mode 100644 index 0000000..5d7c4f5 --- /dev/null +++ b/examples/mmc3_per_state_split.ne @@ -0,0 +1,112 @@ +// MMC3 Per-State Scanline Split — proves the compiler's per-state +// IRQ dispatch and reload logic. Two states each own their own +// `on scanline(N)` handler at a different scanline; pressing START +// transitions between them. Because the MMC3 IRQ latch is +// reloaded from the *current* state's scanline each frame (via +// the `__ir_mmc3_reload` helper), the visible split line moves +// when the state changes. +// +// What this exercises end-to-end: +// - MMC3 scanline IRQ firing at the latched line +// - `__ir_mmc3_reload` walking the dispatch table to pick the +// latch value for the new state after a transition +// - `__irq_user` dispatching to the right per-state handler +// - scroll writes from inside an IRQ handler landing before the +// PPU renders the next visible scanline +// +// Controls: +// START — toggle between Upper-split and Lower-split states +// +// Build: cargo run -- build examples/mmc3_per_state_split.ne + +game "MMC3 Split" { + mapper: MMC3 + mirroring: horizontal +} + +// Scroll values for the two halves of the screen. `frame_scroll` +// drifts every frame so the split is easy to see in motion — the +// top half scrolls right, the bottom scrolls left. +var top_scroll: u8 = 0 +var bottom_scroll: u8 = 0 + +// Tiny debouncer so one press of START doesn't cycle through the +// states multiple times per frame. +var debounce: u8 = 0 + +// Shared drift counter — primarily for observability (the split +// animation works from `top_scroll` / `bottom_scroll` directly). +var _frame_counter: u8 = 0 + +state Upper { + on enter { + // When we arrive, reset the scroll so the split is easy + // to see from the first frame onward. + top_scroll = 0 + bottom_scroll = 0 + } + + on frame { + _frame_counter += 1 + top_scroll += 1 + bottom_scroll -= 1 + + // Initial scroll for the TOP half. The scanline handler + // below will rewrite scroll midway through the frame. + scroll(top_scroll, 0) + + // Draw a marker at the top half and a player sprite in + // the bottom half so the split position is visually + // obvious. + draw Marker at: (40, 40) + draw Player at: (120, 140) + + if debounce > 0 { + debounce -= 1 + } + if button.start and debounce == 0 { + transition Lower + debounce = 30 + } + } + + // Split at line 80 — the top 80 rows use `top_scroll`, the + // rest use `bottom_scroll`. + on scanline(80) { + scroll(bottom_scroll, 0) + } +} + +state Lower { + on enter { + top_scroll = 0 + bottom_scroll = 0 + } + + on frame { + _frame_counter += 1 + top_scroll += 1 + bottom_scroll -= 1 + + scroll(top_scroll, 0) + + draw Marker at: (40, 40) + draw Player at: (120, 140) + + if debounce > 0 { + debounce -= 1 + } + if button.start and debounce == 0 { + transition Upper + debounce = 30 + } + } + + // Split at line 160 — lower split, so the top 160 rows use + // `top_scroll` and only the last ~80 rows use `bottom_scroll`. + on scanline(160) { + scroll(bottom_scroll, 0) + } +} + +start Upper diff --git a/examples/two_player.ne b/examples/two_player.ne new file mode 100644 index 0000000..b2637b0 --- /dev/null +++ b/examples/two_player.ne @@ -0,0 +1,74 @@ +// Two Player — proves the compiler can read both NES controllers. +// +// Player 1 input goes through `button.*` (the implicit default, +// equivalent to `p1.button.*`); player 2 input goes through +// `p2.button.*`. The language guide mentions both; until this +// example, no runtime test actually exercised player 2. +// +// What this exercises end-to-end: +// - `p2.button.up/down/left/right/a` reads from the player-2 +// ZP slot ($08) rather than player 1 ($01) +// - The runtime's NMI controller poll reads both $4016 and +// $4017 per frame and shifts the bits into $01 and $08 +// - Two independently-controlled sprites sharing a frame +// handler's OAM cursor +// +// Controls: +// D-pad, A/B — player 1 moves red sprite +// p2.D-pad, — player 2 moves blue sprite +// p2.A/B — p2 shoots "bullets" (visual marker) +// +// Build: cargo run -- build examples/two_player.ne + +game "Two Player" { + mapper: NROM +} + +var p1x: u8 = 64 +var p1y: u8 = 112 +var p2x: u8 = 192 +var p2y: u8 = 112 + +// A simple "shot" indicator for each player — when any button is +// pressed this frame, we light up a pixel near the player. +var p1_shot: u8 = 0 +var p2_shot: u8 = 0 + +on frame { + // Player 1 — uses the implicit prefix. + if button.left { p1x -= 1 } + if button.right { p1x += 1 } + if button.up { p1y -= 1 } + if button.down { p1y += 1 } + if button.a or button.b { + p1_shot = 1 + } else { + p1_shot = 0 + } + + // Player 2 — explicit `p2.` prefix. + if p2.button.left { p2x -= 1 } + if p2.button.right { p2x += 1 } + if p2.button.up { p2y -= 1 } + if p2.button.down { p2y += 1 } + if p2.button.a or p2.button.b { + p2_shot = 1 + } else { + p2_shot = 0 + } + + // Draw each player, and a shot indicator above their head + // whenever they're holding a face button. Using separate + // `draw` statements so each gets its own OAM slot via the + // runtime cursor. + draw Player1 at: (p1x, p1y) + draw Player2 at: (p2x, p2y) + if p1_shot == 1 { + draw Shot at: (p1x, p1y - 8) + } + if p2_shot == 1 { + draw Shot at: (p2x, p2y - 8) + } +} + +start Main diff --git a/tests/emulator/run_examples.mjs b/tests/emulator/run_examples.mjs index da6a556..8403d18 100644 --- a/tests/emulator/run_examples.mjs +++ b/tests/emulator/run_examples.mjs @@ -36,6 +36,13 @@ const EXAMPLE_FLOORS = { structs_enums_for: [200, "player + 4 enemies drawn by a `for` loop"], sprites_and_palettes: [60, "custom CHR tiles visible"], scanline_split: [80, "banner + player"], + mmc3_per_state_split: [80, "marker + player in the split-screen state"], + two_player: [100, "two player sprites drawn independently"], + function_chain: [100, "player swept by chained function return + a static marker"], + // `comparisons` has at least `value != MIDPOINT` true for 255 of + // 256 frames, plus either `<`/`<=` or `>`/`>=`, plus the player. + // That's 4+ sprites on most frames. + comparisons: [150, "player + pips for each true comparison against MIDPOINT"], }; async function listRoms() { diff --git a/tests/emulator/screenshots/comparisons.png b/tests/emulator/screenshots/comparisons.png new file mode 100644 index 0000000..ade5cd3 Binary files /dev/null and b/tests/emulator/screenshots/comparisons.png differ diff --git a/tests/emulator/screenshots/function_chain.png b/tests/emulator/screenshots/function_chain.png new file mode 100644 index 0000000..8ba5fdf Binary files /dev/null and b/tests/emulator/screenshots/function_chain.png differ diff --git a/tests/emulator/screenshots/mmc3_per_state_split.png b/tests/emulator/screenshots/mmc3_per_state_split.png new file mode 100644 index 0000000..8c59f35 Binary files /dev/null and b/tests/emulator/screenshots/mmc3_per_state_split.png differ diff --git a/tests/emulator/screenshots/two_player.png b/tests/emulator/screenshots/two_player.png new file mode 100644 index 0000000..9ddabc1 Binary files /dev/null and b/tests/emulator/screenshots/two_player.png differ