mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
4 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
40dec7907a
|
examples: move state-scoped globals to state-local in coin_cavern + platformer
Both examples declared their gameplay variables at the top level even though every read and write happened inside one specific state. That pattern hid the overlay feature from new users and kept the state-local code path from being exercised outside the dedicated `state_machine.ne` demo (which is how the "state-locals silently drop their writes" bug survived so long). `coin_cavern.ne`: the five Playing-only physics/position/inventory vars (`player_x`, `player_y`, `player_vy`, `on_ground`, `coins_left`) move onto Playing's state block. `score` stays global because GameOver-era code could reasonably grow to read it. The `on_enter` body loses its redundant resets — the declared initializers on the state-locals re-run on every entry, so retrying after `transition Title` comes back to a fresh state. `platformer.ne`: player physics, camera, liveness, animation phase, and the autopilot budget (`player_y`, `on_ground`, `rise_count`, `fall_vy`, `camera_x`, `anim_tick`, `alive`, `auto_jumps`) all move onto Playing. `frame_tick` and `stomp_count` stay global — Title reads the former to auto-advance, GameOver reads the latter to tally coins on the death screen. The analyzer now overlays Title's `blink`, Playing's eight physics vars, and GameOver's `linger` starting at the same ZP byte (`$1A`), so the three scenes share a 9-byte window instead of each claiming their own slots. Byte-level ROM bytes for both examples shift because variable addresses moved. Video goldens stay pixel-identical (the harness doesn't see Playing in coin_cavern, and the pre-transition Title→Playing timing in platformer is preserved); the platformer audio hash needed one more refresh because the now-slightly-shorter reset prologue shifts APU writes within each frame. https://claude.ai/code/session_015kvJu3iEFLSRJoShPBfm3X |
||
|
|
169a481099
|
feat(platformer): add stomp-or-die enemy collisions, live HUD, GameOver state
The previous platformer example drew enemies but had almost no interaction with them: only enemy 1 had a stomp check, the stomp window was unreachable under the default +1-px-per-frame-plus-a- jump-every-40-frames autopilot, contact from any other angle was a silent no-op, and the header comment promised a "title → playing → game-over state machine" that didn't actually exist. The README demo gif and the committed golden both froze that state — a level the player could walk through indefinitely with no consequence. Flesh the enemy interaction model out into something real: - `resolve_enemy_hit(e_sx)`: one helper, called symmetrically for both enemies. Computes the player/enemy hitbox overlap (horizontal in `e_sx ∈ (72, 96)`, vertical in `player_y ∈ (152, 176)`) and branches three ways — falling onto the head is a stomp bounce (`rise_count = 6`, `fall_vy = 0`, `stomp_count += 1`, `play Boing`); overlap while `rise_count > 0` is a grace pass-through so the stomp bounce itself can't retrigger contact on the same enemy; anything else (walking into the side, standing on the ground against the enemy) is fatal — `alive = 0` and `play hit`. - New `GameOver` state: draws four enemy tiles across the middle of the screen plus a coin row sized to `stomp_count`, stops the music, lingers 60 frames then auto-retries, and also honours Start for an instant retry. - Proximity-based autopilot: pre-jump when an enemy is exactly 19 px ahead (`e1_sx == 99` or `e2_sx == 99`), capped at two jumps per life by `auto_jumps < AUTOPILOT_JUMPS`. Tuning: a JUMP_RISE=12, GRAVITY_CAP=4 jump lands the player's feet at enemy-head height exactly 21 frames after lift-off, by which point the autopilot camera has scrolled the enemy under the player. The first jump fires on Playing frame 1 and stomps enemy 1 on frame 22; the second fires on Playing frame 101 and stomps enemy 2 on frame 122. After that the autopilot is exhausted and the third enemy encounter (camera wraps back past enemy 1) is fatal — the golden harness now sees the full stomp, stomp, die, retry, stomp loop instead of a frozen walk. - Live HUD: up to four coin sprites in the top-left, one per stomp, rendered both during `Playing` and on the `GameOver` screen so the score is visible in the death frame. `Playing`'s player draw is now guarded by `if alive == 1` so the hero disappears on the fatal-contact frame and the enemy that killed them is visible underneath. Verified with a per-frame ZP trace through the patched puppeteer + jsnes harness: first stomp at emu frame 44 (camera_x=22), second at emu frame 144 (camera_x=122), death at emu frame 283 (camera_x=5 after a 256-px wrap), `Playing` restart at emu frame 343, third stomp at emu frame 365. All 22 emulator goldens still match after the update, and `docs/platformer.gif` regenerated from the new ROM now shows two clean stomps, a clean side-collision death, the GameOver screen, and the retry cycle all inside the 6-second demo window. Golden updates: - `tests/emulator/goldens/platformer.png` — the frame-180 capture now shows the hero walking forward with a two-coin HUD after both autopilot stomps (previously: a frozen bouncing hero). - `tests/emulator/goldens/platformer.audio.hash` — the track now includes two `Boing` stomp bounces, which shifts the hash. - `examples/platformer.nes` — rebuilt from the rewritten source. Also updates the platformer rows in `README.md` and `examples/README.md` to match the new gameplay. https://claude.ai/code/session_013Bi4H4YQ5or5HtMB4doUFi |
||
|
|
ad951a835f
|
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
|
||
|
|
688d9afcec
|
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
|