diff --git a/examples/bitwise_ops.ne b/examples/bitwise_ops.ne new file mode 100644 index 0000000..67eec70 --- /dev/null +++ b/examples/bitwise_ops.ne @@ -0,0 +1,98 @@ +// Bitwise Ops — demonstrates shift and mask operators on a packed +// "status" byte that holds several small flags. +// +// The byte layout: +// bit 7 — alive +// bit 6 — shield +// bit 5 — powered +// bit 4 — invulnerable +// bits 0..3 — health (0..15) +// +// The player toggles each high-bit flag with the face buttons and +// heals/damages the low nibble with the d-pad. The drawing code +// reads each flag with an `and` mask after shifting the status byte +// into place — a typical pattern on the 6502. +// +// Build: cargo run -- build examples/bitwise_ops.ne + +game "Bitwise Ops" { + mapper: NROM +} + +const ALIVE: u8 = 0x80 +const SHIELD: u8 = 0x40 +const POWERED: u8 = 0x20 +const INVULN: u8 = 0x10 +const HEALTH_MASK: u8 = 0x0F + +var status: u8 = 0x8F // alive + full health +var debounce: u8 = 0 + +on frame { + if debounce > 0 { + debounce -= 1 + } + + // Toggle each flag on a different face button. `^` is XOR, + // which flips exactly the masked bit. + if button.a and debounce == 0 { + status = status ^ SHIELD + debounce = 15 + } + if button.b and debounce == 0 { + status = status ^ POWERED + debounce = 15 + } + if button.start and debounce == 0 { + status = status ^ INVULN + debounce = 15 + } + if button.select and debounce == 0 { + status = status ^ ALIVE + debounce = 15 + } + + // D-pad tweaks the low-nibble health counter. Clear the low + // nibble, compute new health, then OR it back in — the + // canonical "update a bitfield" pattern. + var health: u8 = status & HEALTH_MASK + if button.right and debounce == 0 { + if health < 15 { + health += 1 + } + debounce = 8 + } + if button.left and debounce == 0 { + if health > 0 { + health -= 1 + } + debounce = 8 + } + status = (status & 0xF0) | health + + // Read back each flag with a mask + nonzero test. + if (status & ALIVE) != 0 { + draw Player at: (120, 120) + } + if (status & SHIELD) != 0 { + draw Ring at: (120, 120) + } + if (status & POWERED) != 0 { + draw Spark at: (136, 120) + } + if (status & INVULN) != 0 { + draw Spark at: (104, 120) + } + + // Draw a health bar by shifting the low nibble into a loop + // counter. `>>` divides by 2^n on the 6502 with a shift, which + // the optimizer also emits for `/ 2`. + var bars: u8 = health >> 1 // 0..7 bars + var i: u8 = 0 + while i < bars { + draw Pip at: (40 + i * 10, 200) + i += 1 + } +} + +start Main diff --git a/examples/logic_ops.ne b/examples/logic_ops.ne new file mode 100644 index 0000000..9d7cda1 --- /dev/null +++ b/examples/logic_ops.ne @@ -0,0 +1,80 @@ +// Logic Ops — demonstrates keyword-based boolean operators +// (`and`, `or`, `not`) along with comparison operators. +// +// The player sprite is drawn only when it's "alive AND unpaused", +// the score counter freezes when paused OR dead, and movement is +// allowed when NOT paused. +// +// Build: cargo run -- build examples/logic_ops.ne + +game "Logic Ops" { + mapper: NROM +} + +var alive: bool = true +var paused: bool = false +var px: u8 = 100 +var py: u8 = 120 +var score: u8 = 0 +var tick: u8 = 0 +var debounce: u8 = 0 + +on frame { + if debounce > 0 { + debounce -= 1 + } + + // Start toggles pause; B kills the player (both with debounce). + if button.start and debounce == 0 { + if paused { + paused = false + } else { + paused = true + } + debounce = 20 + } + if button.b and debounce == 0 { + alive = false + debounce = 20 + } + if button.a and debounce == 0 { + alive = true + debounce = 20 + } + + // Movement is only allowed when alive AND not paused. + if alive and (not paused) { + if button.right { px += 1 } + if button.left { px -= 1 } + if button.up { py -= 1 } + if button.down { py += 1 } + } + + // Score ticks up 1/frame unless paused or dead. Using OR to + // short-circuit when either flag blocks scoring. + tick += 1 + if paused or (not alive) { + // frozen + } else { + if tick >= 30 { + tick = 0 + if score < 240 { + score += 1 + } + } + } + + // Only draw the player when alive and unpaused. + if alive and (not paused) { + draw Player at: (px, py) + } + + // A simple score bar: draw one marker per 30 score points so the + // effect of the pause/dead gating is visible without text. + if score >= 30 { draw Pip at: (20, 20) } + if score >= 60 { draw Pip at: (30, 20) } + if score >= 90 { draw Pip at: (40, 20) } + if score >= 120 { draw Pip at: (50, 20) } +} + +start Main diff --git a/examples/loop_break_continue.ne b/examples/loop_break_continue.ne new file mode 100644 index 0000000..fb9cebf --- /dev/null +++ b/examples/loop_break_continue.ne @@ -0,0 +1,108 @@ +// 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 diff --git a/examples/match_demo.ne b/examples/match_demo.ne new file mode 100644 index 0000000..480f12b --- /dev/null +++ b/examples/match_demo.ne @@ -0,0 +1,76 @@ +// Match Demo — demonstrates the `match` statement. +// +// Match dispatches on a value against a sequence of patterns. Each +// arm runs only when its pattern compares equal to the scrutinee, +// and the underscore arm catches everything else. Here we drive a +// tiny title/playing/paused/game-over menu purely through a match +// on a u8 screen mode, cycled by the d-pad buttons. +// +// Build: cargo run -- build examples/match_demo.ne + +game "Match Demo" { + mapper: NROM +} + +enum Screen { Title, Playing, Paused, GameOver } + +var screen: u8 = Title +var x: u8 = 120 +var y: u8 = 112 +var debounce: u8 = 0 + +on frame { + // Debounce so a held button doesn't spam transitions each frame. + if debounce > 0 { + debounce -= 1 + } + + match screen { + Title => { + // Blink the pointer by alternating its position. + draw Arrow at: (80, 112) + if button.start and debounce == 0 { + screen = Playing + debounce = 20 + } + } + Playing => { + if button.right { x += 2 } + if button.left { x -= 2 } + if button.down { y += 2 } + if button.up { y -= 2 } + draw Player at: (x, y) + + if button.select and debounce == 0 { + screen = Paused + debounce = 20 + } + if button.b and debounce == 0 { + screen = GameOver + debounce = 20 + } + } + Paused => { + // Freeze the player where it was and show a pause marker. + draw Player at: (x, y) + draw Arrow at: (120, 80) + if button.select and debounce == 0 { + screen = Playing + debounce = 20 + } + } + GameOver => { + draw Skull at: (120, 112) + if button.a and debounce == 0 { + screen = Title + debounce = 20 + } + } + _ => { + // Defensive: an unexpected value lands back on the title. + screen = Title + } + } +} + +start Main diff --git a/examples/scanline_split.ne b/examples/scanline_split.ne new file mode 100644 index 0000000..169a870 --- /dev/null +++ b/examples/scanline_split.ne @@ -0,0 +1,61 @@ +// Scanline Split — demonstrates MMC3's per-scanline IRQ via an +// `on scanline(N)` handler. The handler runs at PPU scanline 120, +// roughly mid-screen, and changes the horizontal scroll register +// so the top half and bottom half appear to scroll independently. +// This is the classic "status bar + parallax" trick used in many +// NES games. +// +// Build: cargo run -- build examples/scanline_split.ne + +game "Scanline Split" { + mapper: MMC3 + mirroring: horizontal +} + +var top_scroll: u8 = 0 +var bottom_scroll: u8 = 0 +var px: u8 = 120 +var py: u8 = 160 + +state Main { + on enter { + top_scroll = 0 + bottom_scroll = 0 + px = 120 + py = 160 + } + + on frame { + // Drift the top layer left and the bottom layer right so + // the split is easy to see when the example runs. + top_scroll += 1 + bottom_scroll -= 1 + + // Player on the bottom half. + if button.right { px += 2 } + if button.left { px -= 2 } + if button.up { py -= 2 } + if button.down { py += 2 } + + // Top-half banner. + draw Banner at: (40, 24) + + // Bottom-half player. + draw Player at: (px, py) + + // Set scroll for the TOP half. The scanline handler will + // overwrite this partway through the frame for the bottom + // half. Without a handler call the PPU would use this one + // value for the entire visible screen. + scroll(top_scroll, 0) + } + + // Fires ~halfway down the visible screen. MMC3's scanline IRQ + // counts rendered lines and fires when the reload value + // elapses; the compiler emits the reload + acknowledge glue. + on scanline(120) { + scroll(bottom_scroll, 0) + } +} + +start Main diff --git a/tests/emulator/screenshots/bitwise_ops.png b/tests/emulator/screenshots/bitwise_ops.png new file mode 100644 index 0000000..46b2560 Binary files /dev/null and b/tests/emulator/screenshots/bitwise_ops.png differ diff --git a/tests/emulator/screenshots/logic_ops.png b/tests/emulator/screenshots/logic_ops.png new file mode 100644 index 0000000..497c1f5 Binary files /dev/null and b/tests/emulator/screenshots/logic_ops.png differ diff --git a/tests/emulator/screenshots/loop_break_continue.png b/tests/emulator/screenshots/loop_break_continue.png new file mode 100644 index 0000000..d5e2f5b Binary files /dev/null and b/tests/emulator/screenshots/loop_break_continue.png differ diff --git a/tests/emulator/screenshots/match_demo.png b/tests/emulator/screenshots/match_demo.png new file mode 100644 index 0000000..fbdb32b Binary files /dev/null and b/tests/emulator/screenshots/match_demo.png differ diff --git a/tests/emulator/screenshots/scanline_split.png b/tests/emulator/screenshots/scanline_split.png new file mode 100644 index 0000000..9d03f50 Binary files /dev/null and b/tests/emulator/screenshots/scanline_split.png differ