1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 09:18:01 +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
This commit is contained in:
Claude 2026-04-13 18:04:21 +00:00
parent 48832ccb13
commit ad951a835f
No known key found for this signature in database
8 changed files with 676 additions and 353 deletions

View file

@ -822,18 +822,24 @@ impl Parser {
for (ry, row) in rows.iter().enumerate() {
let mut line = Vec::with_capacity(width);
for (rx, ch) in row.chars().enumerate() {
// Three vocabularies map to the same 0-3 index so
// artists can use whichever feels natural:
// `. # % @` — shade-intensity glyphs (dense = hi)
// `0 1 2 3` — literal palette-index digits
// `. a b c` — letter form used by most NES tools
let val = match ch {
'.' | ' ' | '0' => 0u8,
'#' | '1' => 1,
'%' | '2' => 2,
'@' | '3' => 3,
'#' | '1' | 'a' | 'A' => 1,
'%' | '2' | 'b' | 'B' => 2,
'@' | '3' | 'c' | 'C' => 3,
other => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"sprite '{sprite_name}' pixel at ({rx}, {ry}) has \
invalid character '{other}'; use '.'/' '/'0' for \
index 0, '#'/'1' for 1, '%'/'2' for 2, '@'/'3' for 3"
index 0, '#'/'1'/'a' for 1, '%'/'2'/'b' for 2, \
'@'/'3'/'c' for 3"
),
key_span,
));
@ -2947,18 +2953,21 @@ fn tilemap_to_bytes(
/// The attribute layout is notoriously awkward: each attribute byte
/// covers a 32×32-pixel region (four 16×16 metatiles) packed as
/// `BR BL TR TL` — top-left in the low bits, bottom-right in the high
/// bits. A 32×30-pixel nametable has a 16×15 metatile grid but the
/// attribute table is a fixed 8×8 = 64 bytes, so the last half-row
/// of metatiles (rows 14-15, which render below the visible area) is
/// always zero-padded.
/// bits. The attribute table is a fixed 8×8 = 64 bytes covering 16
/// metatile rows, even though only the top 15 (the visible 240
/// scanlines) render on screen. Programs may declare up to 16 rows
/// so the off-screen half picks up sensible attribute bytes; if
/// exactly 15 are given, the parser auto-replicates row 14 down
/// into row 15 so the last attribute byte stays consistent with
/// what's visible.
fn palette_map_to_attrs(bg_name: &str, rows: &[String], span: Span) -> Result<Vec<u8>, Diagnostic> {
// 16 metatile cols × up to 15 metatile rows.
if rows.len() > 15 {
if rows.len() > 16 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"background '{bg_name}' palette_map has {} rows; maximum is 15 \
(one entry per 16×16 metatile row)",
"background '{bg_name}' palette_map has {} rows; maximum is 16 \
(15 visible metatile rows + 1 off-screen row for the bottom \
half of the last attribute byte)",
rows.len()
),
span,
@ -3002,6 +3011,14 @@ fn palette_map_to_attrs(bg_name: &str, rows: &[String], span: Span) -> Result<Ve
grid[ry][rx] = idx;
}
}
// If the user gave exactly 15 rows, replicate row 14 into row 15
// so the last attribute byte's bottom-half picks up the same
// sub-palette as the visible bottom of the screen. Users who
// want explicit control over the off-screen row can supply all
// 16 rows.
if rows.len() == 15 {
grid[15] = grid[14];
}
// Pack into the 8×8 attribute table. Each attribute byte covers
// a 2×2 block of metatiles:
// bits 0-1 = top-left (grid[ay*2 ][ax*2 ])