1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples/platformer.ne

576 lines
20 KiB
Text
Raw Normal View History

platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// Platformer — an end-to-end side-scrolling demo game that
// exercises nearly every subsystem of the NEScript compiler in
// one program: custom CHR tiles, a full 32×30 background nametable
// with per-region attribute palettes, a multi-tile metasprite
// player with gravity/jump physics, wrap-around horizontal
// scrolling, moving enemies, collectible coins, a user-declared
// SFX envelope, a user-declared music track, and a title →
// playing → game-over state machine driven by both controller
// input and an autopilot so the jsnes golden harness (which runs
// headless with no simulated buttons) still captures an
// interesting frame-180 composition.
//
// Controls (when played by a human):
// D-pad left/right — walk
// A — jump
// Start — on title screen: begin; after game over: retry
//
// Build: cargo run --release -- build examples/platformer.ne
// Output: examples/platformer.nes
//
// All CHR tile art, nametable bytes, and attribute bytes in this
// file are generated by `cargo run --bin gen_platformer_tiles`.
// Regenerate them from `scripts/gen_platformer_tiles.rs` if you
// want to tweak the level.
game "Platformer" {
mapper: NROM
// Horizontal arrangement means nametable 1 mirrors nametable 0
// horizontally, so our single loaded nametable tiles endlessly
// as the camera scrolls right.
mirroring: horizontal
}
// ── Palette ──────────────────────────────────────────────────
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
// Authored in grouped form: one shared `universal:` colour fills
// every sub-palette's index-0 slot automatically. That single
// declaration is enough to dodge the notorious `$3F10/$3F14/$3F18/
// $3F1C` PPU mirror trap — when a program writes 8 distinct
// "index 0" bytes sequentially, the last write clobbers the
// shared background colour and the screen turns the wrong colour
// (or all-black). Using one shared universal everywhere is what
// every shipped NES game does; the parser handles it for us.
//
// `0x22` is the lavender-blue NES master palette slot Super Mario
// Bros uses for its iconic sky. The colour names map to the
// curated set documented in `docs/language-guide.md`.
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
palette Main {
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
universal: 0x22 // sky blue (NES $22)
// Background sub-palettes
bg0: [white, gray, black] // sky / clouds
bg1: [red, orange, dk_red] // bricks, Q-blocks
bg2: [bright_green, dk_orange, brown] // grass, hills, bushes, dirt
bg3: [yellow, orange, dk_orange] // reserved
// Sprite sub-palette 0 — every `draw` uses this one slot
// because the OAM attribute byte is always 0 in v0.1.
// 1 = red cap, 2 = orange skin, 3 = white highlight.
sp0: [red, orange, white]
sp1: [brown, dk_orange, orange] // reserved
sp2: [neon_green, bright_green, white] // reserved
sp3: [yellow, olive, cream] // reserved
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
}
// ── Tileset ──────────────────────────────────────────────────
// One big sprite declaration holds every custom CHR tile used by
// both sprites and the background nametable. `draw Tileset ...
// frame: N` overrides the OAM tile-index byte with `N`, so the
// same declaration drives the player, enemies, coins, and every
// background tile after tile 0. Tile 0 is reserved by the linker
// for the built-in smiley — the background leaves it unreferenced.
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
//
// All 15 tiles are authored as 8×8 ASCII pixel art and stacked
// vertically (1 tile wide × 15 tiles tall) so the parser splits
// them into consecutive CHR tile indices in reading order. The
// art uses the `.abc` vocabulary (`. = 0`, `a = 1`, `b = 2`,
// `c = 3`); regenerate with `cargo run --bin gen_platformer_tiles`.
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
sprite Tileset {
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
pixels: [
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 1: Player head L
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"..aaaaaa",
".aaaaaaa",
".aaabbbb",
".aabbbbc",
"aabbcccc",
"aabbccbc",
"aabbbbbb",
"aabbbbbb",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 2: Player head R
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"aaaaaa..",
"aaaaaaa.",
"bbbbaaa.",
"cbbbbbaa",
"cccccbbb",
"bcbbccbb",
"bbbbbbbb",
"bbbbbbbb",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 3: Player body L
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
".aaaaaaa",
"aaacaaaa",
"aaaccaaa",
"aaaaaaaa",
".bbbbbbb",
".bbbbbbb",
".bb..bbb",
"aaa..bbb",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 4: Player body R
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"aaaaaaa.",
"aaaacaaa",
"aaaccaaa",
"aaaaaaaa",
"bbbbbbb.",
"bbbbbbb.",
"bbb..bb.",
"bbb..aaa",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 5: Enemy
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"..aaaa..",
".aaaaaa.",
".aacbaa.",
"aacccaaa",
"abbccbba",
".aaaaaa.",
"a.aaaa.a",
"a..aa..a",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 6: Coin
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"...bb...",
"..bbbb..",
".bbccbb.",
".bcbbcb.",
".bcbbcb.",
".bbccbb.",
"..bbbb..",
"...bb...",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 7: Grass top
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"aaaaaaaa",
"a.aaa.aa",
"aaaaaaaa",
"accacaca",
"ccccaccc",
"cccccccc",
"cbccccbc",
"cccccccc",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 8: Dirt
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"cccccccc",
"cbccccbc",
"cccccccc",
"ccccbccc",
"cccccccc",
"cbccccbc",
"ccccbccc",
"cccccccc",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 9: Brick
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"cccccccc",
"caaaaaca",
"caaaaaca",
"caaaaaca",
"cccccccc",
"aaacaaaa",
"aaacaaaa",
"aaacaaaa",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 10: Cloud L
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"........",
"...aaa..",
"..aaaaa.",
".aaaaaaa",
".aaaaaaa",
".bbaaaaa",
"..bbbbba",
".....bbb",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 11: Cloud R
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"........",
".aaa....",
"aaaaa...",
"aaaaaaa.",
"aaaaaaa.",
"aaaaabb.",
"abbbbb..",
"bbb.....",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 12: Hill
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"........",
"....aa..",
"...aaaa.",
"..aaaaaa",
"..abaaaa",
".abbabaa",
".aaababa",
"aabbabba",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 13: Bush
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"........",
"...aa...",
"..aaaa..",
".aaaaac.",
"aaaaaaca",
"aaaaacca",
"aaaaacca",
"acacacac",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 14: Q Block
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"cccccccc",
"caaaaaac",
"cabbbbac",
"cabccbac",
"cabcbbac",
"cabbbbac",
"caaaaaac",
"cccccccc",
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
// tile 15: Sky (blank)
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
"........",
"........",
"........",
"........",
"........",
"........",
"........",
"........"
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
]
}
// Tile index constants — must match `scripts/gen_platformer_tiles.rs`.
const TILE_PLAYER_HL: u8 = 1
const TILE_PLAYER_HR: u8 = 2
const TILE_PLAYER_BL: u8 = 3
const TILE_PLAYER_BR: u8 = 4
const TILE_ENEMY: u8 = 5
const TILE_COIN: u8 = 6
// ── Background ──────────────────────────────────────────────
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
// The 32×30 nametable is authored as ASCII art with a `legend`
// block naming each tile by a single character. Horizontal
// scrolling pans this single nametable via `scroll()` so it only
// needs to look interesting at any X offset.
//
// `palette_map:` is the friendlier alternative to a 64-byte
// hand-packed attribute table: 16×15 metatile entries, one
// digit per 16×16 metatile, and the parser packs the 2-bit
// `(br<<6)|(bl<<4)|(tr<<2)|tl` quadrants automatically. The
// three palette regions are sky (`0`), brick row (`1`), and
// ground (`2`).
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
background Level {
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
legend {
".": 15 // sky (blank)
"<": 10 // cloud left half
">": 11 // cloud right half
"#": 9 // brick
"Q": 14 // question block
"^": 12 // hill silhouette
"*": 13 // bush
"=": 7 // grass top
"%": 8 // dirt
}
map: [
"................................",
"................................",
"................................",
"................................",
"................................",
"....<>..............<>..........",
"............<>..................",
"................................",
"................................",
"................................",
"................................",
"................................",
"................................",
"................................",
"................................",
"......##Q#........###...........",
"................................",
"................................",
"................................",
"................................",
"..^^^.................^^^^......",
"..........***..............**...",
"================================",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%",
"%%%%%#%%%%%%%%%%%%#%%%%%%%%%%%%%",
"%%%%%%%%%%%#%%%%%%%%%%%%#%%%%%%%",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
]
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
palette_map: [
"0000000000000000", // metatile rows 0-5 → sky sub-palette
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"1111111111111111", // metatile rows 6-7 → brick sub-palette
"1111111111111111",
"0000000000000000", // metatile rows 8-9 → sky again
"0000000000000000",
"2222222222222222", // metatile rows 10-15 → ground sub-palette
"2222222222222222", // (rows 15 sits off-screen but the PPU
"2222222222222222", // still reads its attribute byte, so
"2222222222222222", // we emit it explicitly to match the
"2222222222222222", // hand-packed attribute table the
"2222222222222222" // old form was using.)
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
]
}
// ── Audio ────────────────────────────────────────────────────
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
//
// SFX: scalar `pitch:` matches the v1 driver's latch-once
// behaviour — pulse 1's period is sampled exactly once when the
// effect triggers and never updated, so a single byte is the
// natural form for the current pipeline. `envelope:` is the
// friendlier alias for `volume:`.
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
// Custom boingy jump sound — fixed-pitch arc on pulse 1.
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
sfx Boing {
duty: 2
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
pitch: 0x30
envelope: [12, 11, 10, 9, 7, 5, 2]
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
}
// Custom coin ding — short rising blip.
sfx Ding {
duty: 2
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
pitch: 0x20
envelope: [14, 12, 8, 3]
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
}
// Custom looping underworld theme — playful four-bar loop on
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
// pulse 2 that plays while the player is alive. Note names
// (C4 / E4 / etc.) plus a default `tempo:` so each note only
// has to spell its frame count when it deviates from the beat.
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
music Theme {
duty: 2
volume: 9
repeat: true
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
tempo: 10
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
notes: [
examples: adopt the friendly asset syntax Rewrites every example with non-trivial asset declarations to use the pleasant QoL syntax introduced in the previous commit. Every example still compiles to a byte-identical ROM (verified by a temp-path diff before committing), so the committed `.nes` files and the 23 emulator goldens are unchanged. * platformer.ne — the centerpiece end-to-end demo: - `palette Main` goes to grouped form with a shared `universal: 0x22` (sky blue), one shared colour per sub-palette, and named NES colours throughout; the long-standing `$3F10` mirror-trap warning is now handled by the parser and the manual pitfall comment is gone. - `sprite Tileset` is 15 tiles of ASCII pixel art instead of 240 bytes of inline hex. - `background Level` uses a `legend { '.': 15, '#': 9, ... }` block plus `map:` strings for the 32×30 nametable, and `palette_map:` rows for the attribute table. The map reads top-down like the rendered screen. - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias. - `music Theme` uses note names + `tempo: 10` default. * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a note-name `C4, E4, G4, ...` music track. * palette_and_background.ne — grouped CoolBlues / WarmReds palettes with `universal: black` + named colours, plus `legend` + `map:` tilemaps for the two backgrounds. * sprites_and_palettes.ne — Arrow and Heart sprites rewritten as `pixels:` ASCII art. Along the way, two small parser extensions support the rewrites: - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`, matching the vocabulary every NES editor (and our own gen_platformer_tiles.rs generator) uses. - `palette_map_to_attrs` allows up to 16 metatile rows (the full attribute-table coverage, including the off-screen bottom half) and auto-replicates row 14 → row 15 when only 15 rows are supplied so the visible bottom of the screen gets consistent sub-palette assignments by default. The old 15-row cap couldn't match a hand-packed `0xAA` attribute table for the last row; the platformer required this to stay byte-identical. `scripts/gen_platformer_tiles.rs` is updated to emit the new syntax directly (pixel-art `pixels:` block + `legend`/`map:`/ `palette_map:` for the background), so regenerating the platformer tiles stays a one-liner. 474 lib tests + 64 integration tests pass (3 new parser tests for `palette_map:` 15/16/17 rows and the `abc` alias). All 23 emulator goldens still match pixel- and sample-for-sample. https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00
C4, E4, G4, C5, G4, E4,
D4 15,
rest 5,
B3, E4, G4,
C5 20,
rest 10
platformer: end-to-end side-scroller demo + three runtime bug fixes Adds `examples/platformer.ne`, a full side-scrolling game that exercises nearly every subsystem of the compiler in one program: custom CHR tileset, 32×30 background nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, moving enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with autopilot so the headless jsnes harness captures real gameplay at frame 180. Tile art + nametable are generated by `scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`). Building this out uncovered three independent runtime bugs that together made the example render as black-on-black smileys. All three are fixed in this commit: 1. **`gen_init` enabled sprite rendering before the linker's initial palette/background load runs.** The PPU's v-register auto-increments on every `$2007` write *during active rendering*, so the palette load (32 B) and nametable load (1024 B) were scrambled past the first ~72 bytes — every existing program with a `background Level { ... }` block was silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0` at the end of `gen_init` and emit a new `gen_enable_rendering` call *after* all initial VRAM writes complete. 2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio driver's period-table lookup reused `$02/$03` as a temporary indirect pointer with a comment claiming the slots were free because the tick doesn't call mul/div. But `$03` is also `ZP_CURRENT_STATE` used by the state dispatch loop, so every music note silently overwrote the state index with the high byte of `__period_table` (`0xC5` in the platformer ROM), wedging the state machine forever. Fix: `gen_nmi` now PHAs `$02/$03` on entry and PLA-restores them on exit, and the audio tick JSR moves inside that save/restore window (it used to be spliced by the linker *before* the register saves, so even A/X/Y were technically being trashed pre-save). Only `audio_demo`'s audio hash shifts (its note timings move a few cycles); every other golden is unchanged. 3. **Sub-palette mirroring footgun.** Writing a 32-byte palette blob sequentially causes the sprite sub-palettes' "index 0" slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware mirroring. The example's palette sets all eight first bytes to `$22` (sky blue) for this reason; `docs/future-work.md` picks up a TODO to warn on inconsistent first-byte values in the analyzer. Also: - `docs/platformer.gif` — 6-second recording of the example running in jsnes, generated by the new `tests/emulator/record_gif.mjs` puppeteer helper (encodes via `gifenc`, committed as a dev-dependency under `tests/emulator/package.json`). - README / examples/README tables and the 497-test count are updated to cover the new example. https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
]
}
// ── Gameplay constants ──────────────────────────────────────
const PLAYER_SCREEN_X: u8 = 80 // fixed camera-relative X
const GROUND_Y: u8 = 160 // feet land here (y of head)
const JUMP_RISE: u8 = 12 // upward frames after take-off
const JUMP_PX: u8 = 3 // px moved each rise frame
const GRAVITY_CAP: u8 = 4 // terminal fall speed
const ENEMY_Y: u8 = 168 // enemies walk on the grass
const COIN_Y: u8 = 144 // coins float a bit above
// World-space X positions of the moving entities. Wrap-around
// arithmetic (u8) makes the level feel like an endless loop.
const ENEMY1_WORLD_X: u8 = 100
const ENEMY2_WORLD_X: u8 = 200
const COIN1_WORLD_X: u8 = 50
const COIN2_WORLD_X: u8 = 150
// ── Game state ──────────────────────────────────────────────
// Player physics
var player_y: u8 = 160
var on_ground: u8 = 1
var rise_count: u8 = 0 // frames of upward motion remaining
var fall_vy: u8 = 0 // gravity accumulator
// World/camera
var camera_x: u8 = 0
var frame_tick: u8 = 0 // free-running frame counter
var jump_timer: u8 = 0 // autopilot: jump every N frames
var anim_tick: u8 = 0 // visual animation phase
// ── Helper functions ────────────────────────────────────────
// Try to start a jump. Only valid when the player is on the
// ground (on_ground == 1); otherwise this is a no-op.
fun try_jump() {
if on_ground == 1 {
rise_count = JUMP_RISE
on_ground = 0
fall_vy = 0
play Boing
}
}
// Advance player physics one frame. Handles the jump-rise phase
// (upward movement while rise_count > 0) and the gravity fall
// phase (increment fall_vy each frame up to GRAVITY_CAP, clamp to
// GROUND_Y on landing).
fun step_physics() {
if rise_count > 0 {
// Clamp underflow so a too-high jump doesn't wrap into the
// bottom of the screen on an 8-bit subtraction.
if player_y >= JUMP_PX {
player_y -= JUMP_PX
} else {
player_y = 0
}
rise_count -= 1
} else {
player_y += fall_vy
if fall_vy < GRAVITY_CAP {
fall_vy += 1
}
if player_y >= GROUND_Y {
player_y = GROUND_Y
on_ground = 1
fall_vy = 0
}
}
}
// Draw the 2×2 hero metasprite at (PLAYER_SCREEN_X, player_y).
// Each `draw` statement writes a fresh OAM slot via the runtime
// cursor, so four calls produce four sprites occupying a 16×16
// block.
fun draw_player() {
draw Tileset at: (PLAYER_SCREEN_X, player_y ) frame: TILE_PLAYER_HL
draw Tileset at: (PLAYER_SCREEN_X + 8, player_y ) frame: TILE_PLAYER_HR
draw Tileset at: (PLAYER_SCREEN_X, player_y + 8) frame: TILE_PLAYER_BL
draw Tileset at: (PLAYER_SCREEN_X + 8, player_y + 8) frame: TILE_PLAYER_BR
}
// ── States ──────────────────────────────────────────────────
state Title {
var blink: u8 = 0
on enter {
blink = 0
frame_tick = 0
start_music Theme
}
on frame {
frame_tick += 1
blink += 1
if blink >= 60 {
blink = 0
}
// Blink a "press start" marker at roughly 0.5 Hz. We use
// the coin tile for visual continuity with the game world.
if blink < 30 {
draw Tileset at: (120, 120) frame: TILE_COIN
}
// Auto-advance so the jsnes golden harness (which never
// presses start) still reaches Playing and captures gameplay
// at frame 180. A human player can also press Start to skip
// the wait early.
if frame_tick >= 20 {
transition Playing
}
if button.start {
transition Playing
}
}
}
state Playing {
on enter {
player_y = GROUND_Y
on_ground = 1
rise_count = 0
fall_vy = 0
camera_x = 0
frame_tick = 0
jump_timer = 0
anim_tick = 0
}
on frame {
frame_tick += 1
anim_tick += 1
// ── Input ─────────────────────────────────────────────
// Human controls — these work even while the autopilot
// is running so you can stomp the demo at any time.
if button.a {
try_jump()
}
if button.right {
camera_x += 1 // walk forward by scrolling right
}
if button.left {
// Walk back (wraps the camera — the level is cyclic).
camera_x -= 1
}
// ── Autopilot ────────────────────────────────────────
// The headless golden harness never presses buttons, so
// we advance the camera and periodically hop all by
// ourselves. A human holding right still accelerates
// forward — the two effects compose.
camera_x += 1
jump_timer += 1
if jump_timer >= 40 {
jump_timer = 0
try_jump()
}
// ── Physics ──────────────────────────────────────────
step_physics()
// ── Collisions ───────────────────────────────────────
// Coin pickup when the player's screen X is within ±8 of
// a coin's screen position and the coin's Y range overlaps
// the player. Uses u8 subtraction so "screen X" of a
// world entity is just (world_x - camera_x).
var e1_sx: u8 = ENEMY1_WORLD_X - camera_x
var e2_sx: u8 = ENEMY2_WORLD_X - camera_x
var c1_sx: u8 = COIN1_WORLD_X - camera_x
var c2_sx: u8 = COIN2_WORLD_X - camera_x
// Coin collect: player occupies [80..96). A coin is "hit"
// when its screen X is in [72..96) (8-px tolerance on each
// side of the player's left edge).
if c1_sx >= 72 and c1_sx < 96 {
if player_y <= COIN_Y + 16 {
play Ding
}
}
if c2_sx >= 72 and c2_sx < 96 {
if player_y <= COIN_Y + 16 {
play Ding
}
}
// Enemy stomp: if we're above an enemy horizontally *and*
// we're mid-fall (fall_vy > 0), count the stomp and
// bounce. Otherwise just ignore — the enemy slides past.
if e1_sx >= 72 and e1_sx < 96 {
if fall_vy > 0 {
if player_y + 16 >= ENEMY_Y {
play hit
rise_count = 6
fall_vy = 0
}
}
}
// ── Drawing ──────────────────────────────────────────
draw_player()
// Enemies — walking on the grass line. anim_tick drives
// a small vertical bob so they visibly move on the golden.
var ey: u8 = ENEMY_Y
if (anim_tick & 16) != 0 {
ey = ENEMY_Y - 2
}
draw Tileset at: (e1_sx, ey) frame: TILE_ENEMY
draw Tileset at: (e2_sx, ENEMY_Y) frame: TILE_ENEMY
// Coins — alternate two vertical positions to fake a
// spin via position change (OAM attr flip isn't wired up
// in the current runtime).
var cy: u8 = COIN_Y
if (anim_tick & 8) != 0 {
cy = COIN_Y - 1
}
draw Tileset at: (c1_sx, cy) frame: TILE_COIN
draw Tileset at: (c2_sx, COIN_Y) frame: TILE_COIN
// ── PPU scroll latch ─────────────────────────────────
// Write the scroll at the very end of the frame so it's
// the last $2005 pair before the implicit wait_frame /
// NMI handshake, minimizing mid-frame artifacts.
scroll(camera_x, 0)
}
}
start Title