romdevtools 0.86.0 → 0.87.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -4,7 +4,7 @@ This is romdev's GENERIC orientation — read it once. The platform-specific det
4
4
 
5
5
  ## What this server does
6
6
 
7
- Drives the full homebrew ROM dev loop for 18 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2, plus the open-hardware GameTank — and the 3D consoles Nintendo 64, Sony PlayStation, and Sega Dreamcast). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace where an on-screen graphic comes from (`watch({on:'copy'})` on the 15 classic platforms — writer PC per VRAM write; `watch({on:'dma'})` for Genesis DMA sources), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
7
+ Drives the full homebrew ROM dev loop for 18 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2, plus the open-hardware GameTank — and the 3D consoles Nintendo 64, Sony PlayStation, and Sega Dreamcast) — plus the **PICO-8** fantasy console (via FAKE-08: a Lua VM, so its own tier — `build` PACKAGES a `.p8` from Lua, `disasm({target:'source'})` reads the cart's Lua instead of machine code, `memory({region:'system_ram'})` reads its 64 KB; cpuState/decompile/tile-inspectors are N/A). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace where an on-screen graphic comes from (`watch({on:'copy'})` on the 15 classic platforms — writer PC per VRAM write; `watch({on:'dma'})` for Genesis DMA sources), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
8
8
 
9
9
  You drive the work. The human is a director — they may want a game, a ROM disassembly, a tool-assisted reverse-engineering session, or anything else this server can do.
10
10
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # romdev
2
2
 
3
- The entry point for **romdev** — vibe-code real retro games. Build, run, inspect, and reverse-engineer actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, PC Engine, MSX, GameTank — and the 3D consoles N64, PlayStation, and Dreamcast) with one command — drive it yourself or let a coding assistant do it.
3
+ The entry point for **romdev** — vibe-code real retro games. Build, run, inspect, and reverse-engineer actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, PC Engine, MSX, GameTank — and the 3D consoles N64, PlayStation, and Dreamcast), plus the **PICO-8** fantasy console (via FAKE-08), with one command — drive it yourself or let a coding assistant do it.
4
4
 
5
5
  ```bash
6
6
  npx romdevtools
@@ -13,6 +13,8 @@ npx romdevtools
13
13
  - **Inspect + romhack** — read CPU/video/save RAM, watch memory, write-breakpoints, the Cheat-Engine value-search loop, a bundled cheat database, mapper-aware disassembly, and a byte-exact rebuildable-project disassembler.
14
14
  - **Reverse-engineering analysis engine (all 18 platforms — incl. the 3D consoles' MIPS R3000/R4300 + SH-4)** — control-flow graphs, deep cross-references, auto-detected functions (ranked real-code-first), a one-shot structural map, and a Ghidra **decompiler** (C-like pseudocode, with hardware registers named and 6502 SLEIGH clutter folded to readable C): `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. And the piece no static tool has: **live computed-jumptable recovery** — `breakpoint({on:'jumptable'})` runs the emulator to resolve the `JMP (table,X)` / RTS-trick dispatchers (state machines, script/battle VMs) that static analysis collapses to "could not recover." Understand *how* a routine works before you touch it — no $3,000 IDA license, no install.
15
15
  - **Convert assets** — PNG → platform tiles/tilemaps, quantize-to-palette, audio importers (BRR for SNES, XGM2 PCM for Genesis).
16
+ - **Native game runtimes (beyond emulation)** — the same run/see/drive loop also hosts two *native* game formats: **wasmcart** (`.wasc` — WASM games from any language; 2026) and **jsgame** (`.jsgame` — JavaScript canvas/WebGL games; 2024). `loadMedia({platform:'wasmcart'|'jsgame'})` → `frame({op:'step'|'screenshot'})` → `input`, over the same tools — plus V8/WASM introspection (the cart's live WASM heap + exports; the JS game's globals) an emulator can't offer, and `pack({target:'wasc'|'jsgame'})` to zip a source dir into the distributable archive (the "build" step; romdev doesn't compile the WASM — bring your own). A capability descriptor marks the emulator-only tools (memory regions / cpuState / disasm) *not-applicable* for these kinds. Depends on the [`wasmcart`](https://www.npmjs.com/package/wasmcart) + [`rungame`](https://www.npmjs.com/package/rungame) packages (jsgame runs in a `vm` realm — the server self-re-execs with `--experimental-vm-modules`).
17
+ - **PICO-8 (fantasy console)** — the [FAKE-08](https://github.com/jtothebell/fake-08) player (MIT, no BIOS) runs PICO-8 `.p8` (Lua source) and `.p8.png` (cart-in-a-label-PNG) carts at 128×128 with sound. `loadMedia({platform:'pico8'})` → `frame`/`input` work like any core; `build({platform:'pico8', source: lua})` PACKAGES a runnable `.p8` from Lua (+ optional gfx/sfx/map sections) — it's a cart assembler, not a CPU compiler (the Lua IS the code). PICO-8 is a Lua VM, so instead of machine-code disasm/decompile you read the cart's Lua directly with `disasm({target:'source'})`; `memory({region:'system_ram'})` exposes the full 64KB PICO-8 address space (sprite sheet, map, sfx, general RAM, screen buffer). Its capability descriptor is a `fantasy` tier — cpuState/decompile/tile-inspectors report *not-applicable* (there's no CPU or tile hardware to inspect). Ships the [`romdev-core-fake08`](https://www.npmjs.com/package/romdev-core-fake08) core package.
16
18
 
17
19
  Point any coding agent at it three ways:
18
20
 
@@ -36,13 +38,14 @@ This package contains all the JavaScript — the tool surface, the WASM emulator
36
38
 
37
39
  `romdevtools` depends on the binary/data packages it needs (exact-pinned), so a single install gets a matched, tested set:
38
40
 
39
- - 2D cores: `romdev-core-{fceumm,gambatte,gpgx,vice,handy,prosystem,geargrafx,bluemsx,gametank}`
41
+ - 2D cores: `romdev-core-{fceumm,gambatte,gpgx,vice,handy,prosystem,geargrafx,bluemsx,gametank,fake08}`
40
42
  - 3D / GPU cores (rendered through `native-gles`): `romdev-core-{parallel-n64,beetle-psx-hw,flycast}`
41
43
  - Platforms (core + dedicated toolchain bundled): `romdev-platform-{snes,gba,atari2600}`
42
44
  - Toolchains: `romdev-toolchain-{cc65,sdcc,m68k-gcc,vasm,rgbds,mips-gcc,sh-gcc}` (mips-gcc = N64/PS1 C; sh-gcc = Dreamcast C)
43
45
  - GPU deps (OPTIONAL — only the 3D cores need them; the 2D cores never touch GL): `native-gles` + `webgl-node`. A headless user without a GPU stack can still run all the 2D platforms.
44
46
  - Analysis: `romdev-analysis` (Rizin → WASM: control-flow graphs, cross-references, function detection) and `romdev-analysis-decompiler` (Ghidra's C++ decompiler → WASM + SLEIGH processor specs for all 18 CPUs, incl. MIPS R3000/R4300 + SH-4). Power `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. Lazy-loaded on first use.
45
47
  - Data: `romdev_game_codes` — the bundled game-code / cheat database (a free labeled RAM/code map for thousands of known ROMs), split out so it can grow independently. Lazy-loaded one platform at a time.
48
+ - Native runtimes: [`wasmcart`](https://www.npmjs.com/package/wasmcart) (runs `.wasc` WASM game carts) and [`rungame`](https://www.npmjs.com/package/rungame) (runs `.jsgame` JavaScript games headlessly). Also `romdev-audio-resampler` (WASM+SIMD S16-stereo resampler, used by the live-window audio sink).
46
49
 
47
50
  `@kmamal/sdl` is used only by `playtest()` / `romdevtools-cli play` (the live window). It ships its native binary via its own install script, which npm skips when romdev is a transitive dep (e.g. under `npx`) — so romdev's `postinstall` fetches it, and `playtest()` also self-heals at runtime if the binary is still missing (downloading the prebuilt before the first window open). Either way, if the binary can't be fetched (offline/locked-down network), the headless server is unaffected — only the live window degrades, and the error tells you the one command to fix it.
48
51
 
@@ -27,6 +27,7 @@ Each example fits the convention:
27
27
  | pce | `pce/<template>/main.c` | cc65 (HuC6280) | HuCard homebrew, no BIOS. Ships a direct-register VDC/PSG helper lib (`pce.h` + `pce.lib`) — cc65 has no PCE sprite/sound library. Examples: `sprite_move`, `catch_game`, `music_sfx`, plus the 5 genre games (shmup/platformer/puzzle/sports/racing). **`#include <stdint.h>`** for int8/16/32_t — `pce.h` only typedefs u8/u16. The genre games fill the BAT (32×32 virtual screen); the platformer smooth-scrolls via the VDC BXR register. |
28
28
  | msx | `msx/<template>/main.c` | sdcc (z80) | Boots cartridge homebrew on the open C-BIOS (no proprietary ROM). Ships an AY-3-8910 + TMS9918/V9938 VDP helper lib (`msx_hw.h` + `msx_vdp.c`). Examples: `sprite_move`, `catch_game`, `music_sfx`, plus the 5 genre games. The bundled `msx_crt0.s` (applied by the dir-build recipe automatically) emits the `"AB"` cartridge header at $4000 + INIT pointer — **C-BIOS shows its logo for ~2-3 s, then CALLs INIT**, so run ≥240 frames before screenshotting. The platformer column-streams the SCREEN 2 name table for a tile-by-tile scroll. |
29
29
  | gametank | `gametank/templates/*.c` | cc65 (W65C02S) | Build with `linkerConfig:"sdk"` + the bundled SDK runtime (`src/platforms/gametank/lib/gt/*`). FIVE complete genre games, fully reworked and play-tested on real hardware (RetroDeck): `shmup`, `platformer`, `puzzle` (CHROMA WELL — a Columns-style 3-jewel matcher), `sports` (2-player paddle), `racing`. The GameTank is a **framebuffer + blitter** machine — NO tilemap/nametable: every frame is redrawn from scratch via the SDK **draw QUEUE** (`gt_draw.h`), the only correct way to drive the blitter (synchronous blits corrupt the FB + tank the framerate). Shared helper headers the games fork: `gt_draw.h` (queue wrappers + `rnd8`), `gt_palette.h` (VERIFIED color indices), `gt_sprites.h` (GRAM sprite load/blit), `gt_hud.h` (box-font digits + 3×5 A–Z text), `gt_sound.h` (one-shot ACP SFX). **HARD-WON GOTCHAS (all documented in `gt_draw.h`): (1) a box of width OR height 128 is silently DROPPED — clamp to 127; (2) box top-edge scanlines flicker between the double-buffer pages — finish a frame that draws near the top with `queue_clear_border(topColor)`; (3) use `rnd8()`, NOT the SDK `rnd()`, which corrupts state on this single-bank build.** Per-frame GRAM **sprite** blits are the expensive part — too many overrun vblank and the queue drops draws (flicker), so keep sprites few (e.g. the platformer blits only the hero; coins are rects). |
30
+ | pico8 | `pico8/templates/*.p8` | Lua (FAKE-08) | FIVE complete genre games, each a full `.p8` cart (no compiler/crt0 — the Lua IS the code): `shmup` (STAR SWEEPER), `platformer` (HOP QUEST), `puzzle` (COLOR DROP), `sports` (RALLY VOLLEY, 1P-vs-CPU or 2P), `racing` (LANE RUNNER). Each carries hand-authored `__gfx__` pixel-art sprites (drawn with `spr()`), looping `__music__` + `__sfx__` banks, title/play/game-over states, and the genre's core mechanics. **`build({platform:'pico8', source: <the .p8 text>})` just PACKAGES the cart** (a `.p8` = header + `__lua__` + optional `__gfx__`/`__gff__`/`__map__`/`__sfx__`/`__music__` sections). PICO-8 is a Lua VM (128×128, 16-color, 6 buttons) — read/edit the Lua directly, no disassembly needed. |
30
31
 
31
32
  ## Guides
32
33
 
@@ -0,0 +1,168 @@
1
+ pico-8 cartridge // http://www.pico-8.com
2
+ version 18
3
+ __lua__
4
+ -- hop quest — a platformer scaffold
5
+ -- genre example for romdev/pico8. real hero sprite w/ walk + jump frames
6
+ -- (see __gfx__), looping music, sfx, gravity + solid-box collision,
7
+ -- animated spinning coins, a goal flag, parallax hills. fork it.
8
+
9
+ -- sprites: 1 idle,2 walk,3 jump · 5..8 coin spin frames · 10 flag
10
+
11
+ function _init()
12
+ best=99
13
+ state="title"
14
+ boot=0
15
+ build_level()
16
+ reset_player()
17
+ music(0)
18
+ end
19
+
20
+ function build_level()
21
+ solids={
22
+ {0,120,128,8},{24,100,24,6},{60,84,30,6},
23
+ {98,64,24,6},{6,58,18,6},{40,42,28,6},
24
+ }
25
+ coins={}
26
+ for c in all({{34,90},{72,74},{108,54},{14,48},{50,32}}) do
27
+ add(coins,{x=c[1],y=c[2],f=rnd(4)})
28
+ end
29
+ goal={x=52,y=24}
30
+ end
31
+
32
+ function reset_player()
33
+ p={x=6,y=104,dx=0,dy=0,grounded=false,face=1,anim=0}
34
+ collected=0 total=#coins t=0
35
+ end
36
+
37
+ function solid_at(x,y)
38
+ for s in all(solids) do
39
+ if x>=s[1] and x<s[1]+s[3] and y>=s[2] and y<s[2]+s[4] then return true end
40
+ end
41
+ return false
42
+ end
43
+
44
+ -- the top Y of a platform the feet CROSS this step: feet were at/above the top (oldy)
45
+ -- and reach at/below it (newy). returns the highest such top, or nil. this catch-the-
46
+ -- crossing test is what stops fast falls from tunneling straight through a platform.
47
+ function ground_top(x,oldy,newy)
48
+ local best=nil
49
+ for s in all(solids) do
50
+ local top=s[2]
51
+ if x>=s[1] and x<s[1]+s[3] and oldy<=top+1 and newy>=top then
52
+ if best==nil or top<best then best=top end
53
+ end
54
+ end
55
+ return best
56
+ end
57
+
58
+ function _update()
59
+ t+=1
60
+ for c in all(coins) do c.f+=.2 end
61
+ if state=="title" then
62
+ boot+=1
63
+ if boot>10 and (btnp(4) or btnp(5)) then state="play" build_level() reset_player() music(-1) end
64
+ return
65
+ end
66
+ if state=="win" then
67
+ if btnp(4) or btnp(5) then state="title" end
68
+ return
69
+ end
70
+
71
+ local ax=0
72
+ if btn(0) then ax=-1 p.face=-1 end
73
+ if btn(1) then ax=1 p.face=1 end
74
+ p.dx=ax*1.6
75
+ if ax~=0 then p.anim+=.25 else p.anim=0 end
76
+
77
+ if (btn(4) or btn(2)) and p.grounded then p.dy=-4.2 p.grounded=false sfx(0) end
78
+ p.dy=min(p.dy+.3,4)
79
+
80
+ -- horizontal move (block at body height)
81
+ local nx=p.x+p.dx
82
+ if not solid_at(nx+(p.dx>0 and 3 or -3),p.y-3) then p.x=nx end
83
+
84
+ -- vertical move. p.y = the FEET. land exactly on a platform's top edge.
85
+ local ny=p.y+p.dy
86
+ if p.dy>=0 then
87
+ -- falling: if the feet CROSS a platform top this step, snap onto it (no tunneling).
88
+ local gt=ground_top(p.x,p.y,ny)
89
+ if gt~=nil then
90
+ p.y=gt p.dy=0 p.grounded=true
91
+ else
92
+ p.y=ny p.grounded=false
93
+ end
94
+ else
95
+ -- rising: bonk head on a ceiling
96
+ if solid_at(p.x,ny-7) then p.dy=0 else p.y=ny end
97
+ p.grounded=false
98
+ end
99
+ p.x=mid(4,p.x,124)
100
+ if p.y>140 then reset_player() sfx(3) end
101
+
102
+ for c in all(coins) do
103
+ if abs(c.x-p.x)<6 and abs(c.y-p.y)<7 then del(coins,c) collected+=1 sfx(1) end
104
+ end
105
+ if abs(goal.x-p.x)<7 and abs(goal.y-p.y)<9 then
106
+ best=min(best,t\30) state="win" sfx(2) music(-1)
107
+ end
108
+ end
109
+
110
+ function _draw()
111
+ cls(12)
112
+ -- parallax hills
113
+ for i=0,4 do circfill(i*34-8,126,22,3) end
114
+ for i=0,3 do circfill(i*44+20,128,26,11) end
115
+
116
+ if state=="title" then
117
+ print("hop quest",44,44,7)
118
+ spr(1,60,60)
119
+ print("z/up jump",40,80,6)
120
+ print("z/x start",40,92,7)
121
+ return
122
+ end
123
+
124
+ for s in all(solids) do
125
+ rectfill(s[1],s[2],s[1]+s[3]-1,s[2]+s[4]-1,4)
126
+ rectfill(s[1],s[2],s[1]+s[3]-1,s[2],9) -- grass top
127
+ end
128
+ for c in all(coins) do spr(5+flr(c.f)%4,c.x-4,c.y-4) end
129
+ -- flag
130
+ rectfill(goal.x,goal.y,goal.x,goal.y+12,6)
131
+ spr(10,goal.x,goal.y-1)
132
+ -- hero
133
+ local s=p.grounded and (p.dx~=0 and (5+flr(p.anim)%2==5 and 2 or (flr(p.anim)%2==0 and 1 or 2)) or 1) or 3
134
+ spr(p.dx~=0 and (flr(p.anim)%2==0 and 1 or 2) or (p.grounded and 1 or 3),p.x-4,p.y-7,1,1,p.face<0)
135
+
136
+ rectfill(0,0,127,7,0)
137
+ print("coins "..collected.."/"..total,2,1,10)
138
+ if best<99 then print("best "..best.."s",84,1,6) end
139
+ if state=="win" then
140
+ rectfill(28,50,100,80,0) rect(28,50,100,80,11)
141
+ print("you win!",44,55,11)
142
+ print("time "..(t\30).."s",44,64,7)
143
+ print("z/x title",40,72,6)
144
+ end
145
+ end
146
+
147
+ __gfx__
148
+ 0000000000fff00000fff00080fff0080000000000aaaa00000aa0000000a000000aa00000066000066000000000000000000000000000000000000000000000
149
+ 0000000000fff00000fff00008fff880000000000a9779a000a99a00000a9a0000a99a0000066660066600000000000000000000000000000000000000000000
150
+ 0000000000f4f00000f4f00000f4f00000000000a977779a0a9779a0000a9a000a9779a000060000066660000000000000000000000000000000000000000000
151
+ 0000000008888000088880000088880000000000a977779a0a9779a0000a9a000a9779a000060000066600000000000000000000000000000000000000000000
152
+ 0000000008888000088880000088880000000000a999999a0a9999a0000a9a000a9999a000600000066000000000000000000000000000000000000000000000
153
+ 0000000008880000088800000008800000000000a999999a0a9999a0000a9a000a9999a000060000060000000000000000000000000000000000000000000000
154
+ 00000000008008000088000000800800000000000a9999a000a99a00000a9a0000a99a0000060000060000000000000000000000000000000000000000000000
155
+ 0000000000b00b00000bb00000b00b000000000000aaaa00000aa0000000a000000aa00000060000060000000000000000000000000000000000000000000000
156
+ __sfx__
157
+ 000300001f04025040290400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
158
+ 000200002465527655296550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
159
+ 011000002465521655216551d6551a655000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
160
+ 010600000865505655036550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
161
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
162
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
163
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
164
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
165
+ 001400001c4501c4501d4501f4501f4501d4501c4501a45018450184501a4501c4501c4501a4501a4501a4501c4501c4501d4501f4501f4501d4501c4501a45018450184501a4501c4501a450184501845018450
166
+ 001400000c2300c2300c2301323013230132300c230132300c2300c230132300c2300c2301323013230132300c2300c2300c2301323013230132300c230132300c2300c230132300c230132300c2300c2300c230
167
+ __music__
168
+ 03 08094040
@@ -0,0 +1,240 @@
1
+ pico-8 cartridge // http://www.pico-8.com
2
+ version 18
3
+ __lua__
4
+ -- color drop — a falling-column match puzzle (Columns-style) scaffold
5
+ -- genre example for romdev/pico8. THE GAME: a vertical column of 3 jewels
6
+ -- falls into an 8-wide well. move left/right, soft-drop (down), hard-drop
7
+ -- (x), and CYCLE the 3 colors (o). match 3+ of one color horizontally,
8
+ -- vertically, or DIAGONALLY to clear; gravity pulls survivors down, which
9
+ -- can CASCADE into more clears. fork it and reshape one thing at a time.
10
+
11
+ cols=8 rows=13 cell=9 ox=26 oy=3
12
+ ncol=5 -- jewel colors 1..5
13
+ -- well fits the 128px screen: oy + rows*cell = 3 + 117 = 120 (bottom row fully visible).
14
+
15
+ function _init()
16
+ hi=0
17
+ state="title"
18
+ boot=0
19
+ reset_game()
20
+
21
+ music(0)
22
+ end
23
+
24
+ function reset_game()
25
+ grid={}
26
+ for y=1,rows do grid[y]={} for x=1,cols do grid[y][x]=0 end end
27
+ score=0 chain=0 level=1 cleared=0
28
+ fall=0 t=0
29
+ flashing=nil -- {cells=..,ttl=..} during a clear
30
+ nextp=randcol3()
31
+ spawn_piece()
32
+ end
33
+
34
+ function randcol3()
35
+ return {flr(rnd(ncol))+1,flr(rnd(ncol))+1,flr(rnd(ncol))+1}
36
+ end
37
+
38
+ -- the falling column: 3 colors top->bottom at (px, py=top cell)
39
+ function spawn_piece()
40
+ pcol=nextp
41
+ nextp=randcol3()
42
+ px=flr(cols/2) py=1
43
+ -- game over if the top cells are blocked
44
+ if grid[1][px]~=0 or grid[2][px]~=0 or grid[3][px]~=0 then
45
+ hi=max(hi,score) state="over"
46
+ end
47
+ end
48
+
49
+ function can_be(x,y)
50
+ -- can the 3-tall column sit with its TOP cell at (x,y)?
51
+ for i=0,2 do
52
+ local cy=y+i
53
+ if cy>rows then return false end
54
+ if cy>=1 and grid[cy][x]~=0 then return false end
55
+ end
56
+ return true
57
+ end
58
+
59
+ function _update()
60
+ t+=1
61
+
62
+ -- resolve an in-progress clear (flash then remove + cascade)
63
+ if flashing then
64
+ flashing.ttl-=1
65
+ if flashing.ttl<=0 then
66
+ for c in all(flashing.cells) do grid[c.y][c.x]=0 end
67
+ collapse()
68
+ local m=find_matches()
69
+ if #m>0 then chain+=1 start_clear(m)
70
+ else chain=0 flashing=nil spawn_piece() end
71
+ end
72
+ return
73
+ end
74
+
75
+ if state=="title" then
76
+ boot+=1
77
+ if boot>10 and (btnp(4) or btnp(5)) then state="play" reset_game() music(-1) end
78
+ return
79
+ end
80
+ if state=="over" then
81
+ if btnp(4) or btnp(5) then state="title" end
82
+ return
83
+ end
84
+
85
+ -- move — snappy custom repeat (move on press, then again every 4 frames while held)
86
+ -- so a hold slides smoothly instead of pico-8's slow 15-frame btnp repeat.
87
+ hold_l = btn(0) and (hold_l or 0)+1 or 0
88
+ hold_r = btn(1) and (hold_r or 0)+1 or 0
89
+ if (hold_l==1 or (hold_l>4 and hold_l%3==0)) and px>1 and can_be(px-1,py) then px-=1 sfx(2) end
90
+ if (hold_r==1 or (hold_r>4 and hold_r%3==0)) and px<cols and can_be(px+1,py) then px+=1 sfx(2) end
91
+ -- cycle colors (o = btn4): rotate the 3-jewel column
92
+ if btnp(4) then
93
+ pcol={pcol[3],pcol[1],pcol[2]} sfx(3)
94
+ end
95
+ -- hard drop (x = btn5)
96
+ if btnp(5) then
97
+ while can_be(px,py+1) do py+=1 end
98
+ lock_piece() return
99
+ end
100
+
101
+ -- gravity (soft-drop on down)
102
+ local speed=btn(3) and 2 or max(6,26-level*2)
103
+ fall+=1
104
+ if fall>=speed then
105
+ fall=0
106
+ if can_be(px,py+1) then py+=1
107
+ else lock_piece() end
108
+ end
109
+ end
110
+
111
+ function lock_piece()
112
+ for i=0,2 do
113
+ local cy=py+i
114
+ if cy>=1 and cy<=rows then grid[cy][px]=pcol[i+1] end
115
+ end
116
+ sfx(0)
117
+ local m=find_matches()
118
+ if #m>0 then chain=1 start_clear(m)
119
+ else spawn_piece() end
120
+ end
121
+
122
+ function start_clear(cells)
123
+ flashing={cells=cells,ttl=12}
124
+ score+=#cells*10*chain
125
+ cleared+=#cells
126
+ level=1+flr(cleared/20)
127
+ sfx(1)
128
+ end
129
+
130
+ -- scan H, V, and both diagonals for runs of 3+ same color.
131
+ -- returns a list of {x,y} cells to clear (deduped).
132
+ local dirs={{1,0},{0,1},{1,1},{1,-1}}
133
+ function find_matches()
134
+ local hit={} -- key "x,y" -> {x,y}
135
+ for y=1,rows do for x=1,cols do
136
+ local c=grid[y][x]
137
+ if c~=0 then
138
+ for d in all(dirs) do
139
+ -- only start a run at its beginning (no same-color cell behind us)
140
+ local bx,by=x-d[1],y-d[2]
141
+ local prev = (bx>=1 and bx<=cols and by>=1 and by<=rows) and grid[by][bx] or -1
142
+ if prev~=c then
143
+ local run={}
144
+ local cx,cy=x,y
145
+ while cx>=1 and cx<=cols and cy>=1 and cy<=rows and grid[cy][cx]==c do
146
+ add(run,{x=cx,y=cy}) cx+=d[1] cy+=d[2]
147
+ end
148
+ if #run>=3 then for r in all(run) do hit[r.x..","..r.y]={x=r.x,y=r.y} end end
149
+ end
150
+ end
151
+ end
152
+ end end
153
+ local out={} for _,v in pairs(hit) do add(out,v) end
154
+ return out
155
+ end
156
+
157
+ -- gravity: drop every column's survivors to the bottom
158
+ function collapse()
159
+ for x=1,cols do
160
+ local w=rows
161
+ for y=rows,1,-1 do
162
+ if grid[y][x]~=0 then
163
+ grid[w][x]=grid[y][x] if w~=y then grid[y][x]=0 end w-=1
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ -- ── draw ──
170
+ gemcol={8,9,10,11,12}
171
+ function drawgem(gx,gy,c)
172
+ local base=gemcol[c] or 8
173
+ rectfill(gx+1,gy,gx+6,gy+7,base)
174
+ rectfill(gx,gy+1,gx+7,gy+6,base)
175
+ rectfill(gx+2,gy+1,gx+3,gy+2,7) -- sparkle
176
+ line(gx+1,gy+6,gx+6,gy+6,1) -- shadow
177
+ end
178
+
179
+ function _draw()
180
+ cls(0)
181
+ rectfill(0,0,ox-2,127,1)
182
+ rectfill(ox+cols*cell+1,0,127,127,1)
183
+
184
+ if state=="title" then
185
+ print("color drop",42,40,12)
186
+ -- show a sample 3-jewel column
187
+ for i=1,3 do drawgem(60,50+i*9,i) end
188
+ print("arrows move",34,86,7)
189
+ print("o cycle x drop",26,96,6)
190
+ print("z/x start",40,108,7)
191
+ return
192
+ end
193
+
194
+ rect(ox-1,oy-1,ox+cols*cell,oy+rows*cell,5)
195
+ -- placed gems
196
+ for y=1,rows do for x=1,cols do
197
+ if grid[y][x]~=0 then drawgem(ox+(x-1)*cell,oy+(y-1)*cell,grid[y][x]) end
198
+ end end
199
+ -- flashing clears
200
+ if flashing then
201
+ for c in all(flashing.cells) do
202
+ if t%4<2 then rectfill(ox+(c.x-1)*cell,oy+(c.y-1)*cell,ox+(c.x-1)*cell+8,oy+(c.y-1)*cell+8,7) end
203
+ end
204
+ elseif state=="play" then
205
+ -- the falling 3-jewel column
206
+ for i=0,2 do
207
+ local cy=py+i
208
+ if cy>=1 then drawgem(ox+(px-1)*cell,oy+(cy-1)*cell,pcol[i+1]) end
209
+ end
210
+ end
211
+
212
+ -- panel
213
+ print("score",102,8,7) print(score,102,16,10)
214
+ print("next",102,36,7)
215
+ for i=1,3 do drawgem(106,44+i*9,nextp[i]) end
216
+ print("lv "..level,102,76,7)
217
+ if chain>1 then print("x"..chain,102,88,8) end
218
+
219
+ if state=="over" then
220
+ rectfill(18,52,110,76,0) rect(18,52,110,76,8)
221
+ print("game over",44,56,8)
222
+ print("hi "..hi,52,66,10)
223
+ end
224
+ end
225
+
226
+ __gfx__
227
+ 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
228
+ __sfx__
229
+ 010800001c0551c055000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
230
+ 011000002465527655296552d65500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
231
+ 010400001865518655000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
232
+ 010c00002465521655000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
233
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
234
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
235
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
236
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
237
+ 0014000028450274502845027450284502345026450244502145000000184501c4502145023450000001c450204502345024450000001c4502845027450284502745028450234502645024450214500000000000
238
+ 001400001523000000000000000015230000000000000000102300000000000000000c23000000000000000015230000000000000000152300000000000000001023000000000000000015230000000000000000
239
+ __music__
240
+ 03 08094040
@@ -0,0 +1,122 @@
1
+ pico-8 cartridge // http://www.pico-8.com
2
+ version 18
3
+ __lua__
4
+ -- lane runner — a top-down endless racing scaffold
5
+ -- genre example for romdev/pico8. car sprites (see __gfx__), looping
6
+ -- music, engine/crash sfx, 3-lane dodging, scrolling road + dashes,
7
+ -- speed ramp, distance score + best. fork it.
8
+
9
+ function _init()
10
+ best=0
11
+ state="title"
12
+ boot=0
13
+ reset_run()
14
+ music(0)
15
+
16
+ end
17
+
18
+ function reset_run()
19
+ car={x=64,lane=1}
20
+ lanes={40,64,88}
21
+ rivals={}
22
+ dist=0 speed=2 spawn=0 road=0 t=0 shake=0
23
+ end
24
+
25
+ function _update()
26
+ t+=1
27
+ road=(road+speed)%16
28
+ if shake>0 then shake-=1 end
29
+ if state=="title" then
30
+ boot+=1
31
+ if boot>10 and (btnp(4) or btnp(5)) then state="play" reset_run() music(-1) end
32
+ return
33
+ end
34
+ if state=="over" then
35
+ if btnp(4) or btnp(5) then state="title" end
36
+ return
37
+ end
38
+
39
+ if btnp(0) and car.lane>0 then car.lane-=1 sfx(0) end
40
+ if btnp(1) and car.lane<2 then car.lane+=1 sfx(0) end
41
+ car.x=car.x+(lanes[car.lane+1]-car.x)*.4
42
+
43
+ speed=min(2+dist/700,5.5)
44
+ dist+=speed
45
+
46
+ spawn-=1
47
+ if spawn<=0 then
48
+ add(rivals,{lane=flr(rnd(3)),y=-14,c=flr(rnd(3))})
49
+ spawn=max(16,44-flr(dist/110))
50
+ end
51
+ for r in all(rivals) do
52
+ r.y+=speed
53
+ if r.y>140 then del(rivals,r) end
54
+ if r.lane==car.lane and abs(r.y-104)<13 then
55
+ sfx(2) shake=12 best=max(best,flr(dist/10)) state="over"
56
+ end
57
+ end
58
+ end
59
+
60
+ function _draw()
61
+ local sx=shake>0 and (rnd(4)-2) or 0
62
+ cls(3)
63
+ camera(sx,0)
64
+ -- grass texture
65
+ for i=0,8 do for j=0,8 do if (i+j)%2==0 then pset(i*16+2,j*16+8,11) end end end
66
+ -- road
67
+ rectfill(28,0,100,128,5)
68
+ rectfill(28,0,31,128,10) rectfill(97,0,100,128,10)
69
+ for y=-16,128,16 do
70
+ rectfill(52,y+road,54,y+road+9,7)
71
+ rectfill(74,y+road,76,y+road+9,7)
72
+ end
73
+
74
+ if state=="title" then
75
+ camera()
76
+ rectfill(12,42,116,94,0)
77
+ print("lane runner",40,48,10)
78
+ spr(1,60,64)
79
+ print("left/right swerve",22,80,6)
80
+ print("z/x start",40,88,7)
81
+ if best>0 then print("best "..best,50,100,7) end
82
+ return
83
+ end
84
+
85
+ for r in all(rivals) do spr(2+r.c,lanes[r.lane+1]-4,r.y-6) end
86
+ spr(1,car.x-4,104-6)
87
+
88
+ camera()
89
+ rectfill(0,0,127,8,0)
90
+ print("dist "..flr(dist/10),2,1,7)
91
+ print("spd "..flr(speed*10),88,1,10)
92
+
93
+ if state=="over" then
94
+ rectfill(22,52,106,80,0) rect(22,52,106,80,8)
95
+ print("crashed!",46,56,8)
96
+ print("dist "..flr(dist/10),44,66,7)
97
+ print("z/x title",40,74,6)
98
+ end
99
+ end
100
+
101
+ __gfx__
102
+ 000000000008800000099000000aa000000ee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
103
+ 0000000000888800009999000aaaaa000eeeee000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
104
+ 0000000008777780097777900a7777a00e7777e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
105
+ 0000000008888880099999900aaaaaa00eeeeee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
106
+ 0000000008888880099999900aaaaaa00eeeeee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
107
+ 0000000008777780097777900a7777a00e7777e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
108
+ 000000000088880000999900000aa000000ee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
109
+ 000000000080080000900900000aa000000ee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
110
+ __sfx__
111
+ 010800001505013050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
112
+ 010400001f05522055000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
113
+ 010600000c05008050050550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
114
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
115
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
116
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
117
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
118
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
119
+ 0012000017350183501a3501c3501a35018350173501a350133501f3501c350133501f3501c350133501f35017350183501a3501c3501a35018350173501a3501c3501d3501f3501d3501c3501a3501835017350
120
+ 001200000b230000000b230000000b230000000b230000000c230000000c230000000c230000000c230000000b230000000b230000000b230000000b230000001023000000102300000010230000000b23000000
121
+ __music__
122
+ 03 08094040
@@ -0,0 +1,165 @@
1
+ pico-8 cartridge // http://www.pico-8.com
2
+ version 18
3
+ __lua__
4
+ -- star sweeper — a shmup scaffold
5
+ -- genre example for romdev/pico8. real sprites (see __gfx__), looping
6
+ -- music (__music__), sfx, title/play/gameover states, projectile pool,
7
+ -- wave spawner, aabb collision, score + hi-score, thruster animation.
8
+ -- fork it and reshape one thing at a time.
9
+
10
+ -- sprite ids in __gfx__:
11
+ -- 1 ship, 2 ship-thrust, 3 shot, 4 foe-a, 5 foe-b, 6 explosion
12
+
13
+ function _init()
14
+ hi=0
15
+ make_stars()
16
+ state="title"
17
+ boot=0
18
+ reset_game()
19
+
20
+ music(0)
21
+ end
22
+
23
+ function make_stars()
24
+ stars={}
25
+ for i=1,40 do add(stars,{x=rnd(128),y=rnd(128),s=rnd(2)+.4}) end
26
+ end
27
+
28
+ function reset_game()
29
+ player={x=60,y=100,cool=0,inv=0}
30
+ shots={}
31
+ foes={}
32
+ boom={}
33
+ score=0
34
+ lives=3
35
+ spawn=0
36
+ t=0
37
+ end
38
+
39
+ function _update()
40
+ t+=1
41
+ for s in all(stars) do s.y+=s.s if s.y>128 then s.y=0 s.x=rnd(128) end end
42
+
43
+ if state=="title" then
44
+ boot+=1
45
+ if boot>10 and (btnp(4) or btnp(5)) then state="play" reset_game() music(-1) end
46
+ return
47
+ end
48
+ if state=="over" then
49
+ if btnp(4) or btnp(5) then state="title" end
50
+ return
51
+ end
52
+
53
+ -- move
54
+ local mv=false
55
+ if btn(0) then player.x-=2 mv=true end
56
+ if btn(1) then player.x+=2 mv=true end
57
+ if btn(2) then player.y-=2 end
58
+ if btn(3) then player.y+=2 end
59
+ player.x=mid(4,player.x,116)
60
+ player.y=mid(4,player.y,120)
61
+ player.inv=max(0,player.inv-1)
62
+
63
+ -- fire
64
+ player.cool=max(0,player.cool-1)
65
+ if (btn(4) or btn(5)) and player.cool==0 then
66
+ add(shots,{x=player.x+3,y=player.y-2})
67
+ player.cool=7 sfx(3)
68
+ end
69
+ for s in all(shots) do s.y-=4 if s.y<-4 then del(shots,s) end end
70
+
71
+ -- spawn foes (ramping)
72
+ spawn-=1
73
+ if spawn<=0 then
74
+ add(foes,{x=rnd(104)+12,y=-8,vy=rnd(1)+1,vx=rnd(1)-.5,ty=flr(rnd(2))})
75
+ spawn=max(10,38-flr(t/90))
76
+ end
77
+ for f in all(foes) do
78
+ f.y+=f.vy f.x+=f.vx
79
+ if f.x<8 or f.x>116 then f.vx=-f.vx end
80
+ if f.y>134 then del(foes,f) end
81
+ if player.inv==0 and abs(f.x-player.x)<6 and abs(f.y-player.y)<6 then
82
+ del(foes,f) hurt()
83
+ end
84
+ end
85
+
86
+ -- shot vs foe
87
+ for s in all(shots) do
88
+ for f in all(foes) do
89
+ if abs(f.x-s.x)<5 and abs(f.y-s.y)<5 then
90
+ del(shots,s) del(foes,f)
91
+ add(boom,{x=f.x,y=f.y,f=0})
92
+ score+=10 sfx(4)
93
+ break
94
+ end
95
+ end
96
+ end
97
+ for b in all(boom) do b.f+=1 if b.f>8 then del(boom,b) end end
98
+ end
99
+
100
+ function hurt()
101
+ lives-=1 sfx(5)
102
+ add(boom,{x=player.x,y=player.y,f=0})
103
+ player.inv=60
104
+ if lives<=0 then
105
+ hi=max(hi,score) state="over"
106
+ end
107
+ end
108
+
109
+ function _draw()
110
+ cls(0)
111
+ for s in all(stars) do pset(s.x,s.y,s.s>1.6 and 7 or (s.s>1 and 6 or 5)) end
112
+
113
+ if state=="title" then
114
+ print("star sweeper",34,44,12)
115
+ print("★",26,44,10) print("★",96,44,10)
116
+ spr(1,60,62)
117
+ print("z/x start",40,84,7)
118
+ print("hi "..hi,50,98,6)
119
+ print("arrows move · z/x fire",18,116,5)
120
+ return
121
+ end
122
+
123
+ for f in all(foes) do spr(f.ty==0 and 4 or 5,f.x-4,f.y-4) end
124
+ for s in all(shots) do spr(3,s.x-4,s.y-4) end
125
+ for b in all(boom) do spr(6,b.x-4,b.y-4) end
126
+ -- ship (blink when invulnerable)
127
+ if not (player.inv>0 and player.inv%4<2) then
128
+ spr((t%8<4) and 2 or 1,player.x-4,player.y-4)
129
+ end
130
+
131
+ rectfill(0,0,127,7,1)
132
+ print("score "..score,2,1,7)
133
+ print("hi "..hi,54,1,6)
134
+ for i=1,lives do spr(1,96+i*8,0) end
135
+
136
+ if state=="over" then
137
+ rectfill(28,52,100,80,0) rect(28,52,100,80,8)
138
+ print("game over",44,56,8)
139
+ print("score "..score,42,66,7)
140
+ print("z/x title",40,74,6)
141
+ end
142
+ end
143
+
144
+ __gfx__
145
+ 00000000000c000000000c00000aa000000880000088880000000000000000000000000000000000000000000000000000000000000000000000000000000000
146
+ 0000000000ccc00000ccc0000aaaa000008888000899998000000000000000000000000000000000000000000000000000000000000000000000000000000000
147
+ 0070070000ccc00000ccc0000a00a000088888000988889000000000000000000000000000000000000000000000000000000000000000000000000000000000
148
+ 000770000cccccc00cc7cc00aa00aa008899988089aaaa9800000000000000000000000000000000000000000000000000000000000000000000000000000000
149
+ 000770000c7cc7c00c7cc7c0a0a00a0a0899998008aaaa8000000000000000000000000000000000000000000000000000000000000000000000000000000000
150
+ 0070070000c00c0000cccc00a000000a088888000899998000000000000000000000000000000000000000000000000000000000000000000000000000000000
151
+ 00000000000000000090900000a00a00008888000089980000000000000000000000000000000000000000000000000000000000000000000000000000000000
152
+ 00000000000000000090000000000000000880000008800000000000000000000000000000000000000000000000000000000000000000000000000000000000
153
+ __sfx__
154
+ 010c00000c6530e6530c6530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
155
+ 010c00001805518055180551805518055180551305513055130551305511055110550e0550e0550e0550e0550c0550c0550c0550c055000000000000000000000000000000000000000000000000000000000000
156
+ 011c00002465524655216551d65500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
157
+ 000200003065528655236551e6551a655166551265510655000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
158
+ 00030000186550f6550c6550a65508655066550465502655006550065500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
159
+ 010500000c6530a653086530565303653016530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
160
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
161
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
162
+ 0012000024250242502b2502b2502d2502d2502b2500000029250292502825028250262502625024250000002b2502b2502925029250282502825026250000002b2502b250292502925028250282502625000000
163
+ 00120000182300000018230000001d230000001d230000001a230000001a23000000182300000018230000001d230000001d230000001a230000001a230000001d230000001d2300000018230000001823000000
164
+ __music__
165
+ 03 08094040
@@ -0,0 +1,140 @@
1
+ pico-8 cartridge // http://www.pico-8.com
2
+ version 18
3
+ __lua__
4
+ -- rally volley — a 2-player paddle sports scaffold
5
+ -- genre example for romdev/pico8. paddle + ball sprites (see __gfx__),
6
+ -- looping music, bounce sfx, angle-off-paddle physics, speed-up on rally,
7
+ -- 1p-vs-cpu or 2p couch mode, first to 5. fork it.
8
+
9
+ function _init()
10
+ state="title"
11
+ boot=0
12
+ ai=true
13
+ reset_match()
14
+
15
+ music(0)
16
+ end
17
+
18
+ function reset_match()
19
+ lp={y=64,score=0} rp={y=64,score=0}
20
+ rally=0
21
+ serve(pick_dir())
22
+ end
23
+
24
+ function pick_dir() return (rnd(1)<.5) and -1 or 1 end
25
+
26
+ function serve(dir)
27
+ ball={x=64,y=64,dx=dir*1.4,dy=(rnd(2)-1)*1.1}
28
+ wait=36
29
+ end
30
+
31
+ function _update()
32
+ if state=="title" then
33
+ boot+=1
34
+ if btnp(2) or btnp(3) then ai=not ai sfx(3) end
35
+ if boot>10 and (btnp(4) or btnp(5)) then state="play" reset_match() music(-1) end
36
+ return
37
+ end
38
+ if state=="over" then
39
+ if btnp(4) or btnp(5) then state="title" end
40
+ return
41
+ end
42
+
43
+ if btn(2) then lp.y-=2 end
44
+ if btn(3) then lp.y+=2 end
45
+ lp.y=mid(12,lp.y,116)
46
+
47
+ if ai then
48
+ if ball.dx>0 then rp.y+=mid(-2,(ball.y-rp.y)*.11,2) end
49
+ else
50
+ if btn(4) then rp.y-=2 end
51
+ if btn(5) then rp.y+=2 end
52
+ end
53
+ rp.y=mid(12,rp.y,116)
54
+
55
+ if wait>0 then wait-=1 return end
56
+
57
+ ball.x+=ball.dx ball.y+=ball.dy
58
+ if ball.y<4 or ball.y>123 then ball.dy=-ball.dy sfx(0) end
59
+
60
+ if ball.dx<0 and ball.x<9 and abs(ball.y-lp.y)<11 then
61
+ ball.dx=abs(ball.dx)+.12 ball.dy+=(ball.y-lp.y)*.07 rally+=1 sfx(1)
62
+ end
63
+ if ball.dx>0 and ball.x>119 and abs(ball.y-rp.y)<11 then
64
+ ball.dx=-abs(ball.dx)-.12 ball.dy+=(ball.y-rp.y)*.07 rally+=1 sfx(1)
65
+ end
66
+ ball.dy=mid(-3,ball.dy,3)
67
+
68
+ if ball.x<0 then rp.score+=1 point() end
69
+ if ball.x>128 then lp.score+=1 point() end
70
+ end
71
+
72
+ function point()
73
+ sfx(2) rally=0
74
+ if lp.score>=5 or rp.score>=5 then
75
+ winner=lp.score>rp.score and "left" or "right"
76
+ state="over"
77
+ else
78
+ serve(pick_dir())
79
+ end
80
+ end
81
+
82
+ -- a tall paddle bar (10 tall, 3 wide) with a lighter core so it reads clearly
83
+ function paddle(x,y,col)
84
+ rectfill(x,y-9,x+2,y+9,col)
85
+ rectfill(x+1,y-8,x+1,y+8,7) -- white core stripe
86
+ end
87
+
88
+ function _draw()
89
+ cls(0)
90
+ -- dashed center net
91
+ for y=2,124,8 do rectfill(63,y,64,y+4,5) end
92
+
93
+ if state=="title" then
94
+ print("rally volley",38,36,12)
95
+ paddle(24,64,12) paddle(102,64,8)
96
+ rectfill(62,62,65,65,7) -- ball
97
+ print("mode: "..(ai and "1p vs cpu" or "2 player"),34,80,7)
98
+ print("up/down toggle",30,90,6)
99
+ print("z/x start",40,102,7)
100
+ return
101
+ end
102
+
103
+ paddle(4,lp.y,12) -- left paddle (blue, player 1)
104
+ paddle(121,rp.y,8) -- right paddle (red, cpu/p2)
105
+ rectfill(ball.x-1,ball.y-1,ball.x+1,ball.y+1,7) -- ball
106
+
107
+ rectfill(0,0,127,8,0)
108
+ print(lp.score,40,2,12)
109
+ print(rp.score,84,2,8)
110
+ if rally>2 then print("rally "..rally,52,2,10) end
111
+
112
+ if state=="over" then
113
+ rectfill(24,52,104,78,0) rect(24,52,104,78,11)
114
+ print(winner.." wins!",38,58,11)
115
+ print(lp.score.." - "..rp.score,52,68,7)
116
+ end
117
+ end
118
+
119
+ __gfx__
120
+ 0000000000cccc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
121
+ 000000000cccccc0000cc00000cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
122
+ 000000000c7777c000cccc00cccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
123
+ 000000000c7777c000cccc00cccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
124
+ 000000000c7777c000cccc00cccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
125
+ 000000000cccccc000cccc00cccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
126
+ 0000000000cccc00000cc00000cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
127
+ 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
128
+ __sfx__
129
+ 010800001505015050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
130
+ 010400001f05524055000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
131
+ 011000000c05508055050550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
132
+ 010400001c0551c055000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
133
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
134
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
135
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
136
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
137
+ 00100000183501c3501d3501f35000000183501c3501d3501f35000000183501c3501d3501f3501c350183501c3501a350000001a3501c3501f3501f3501c3501f3501d3501c3501a35018350000000000000000
138
+ 00100000182300000000000000001f230000000000000000182300000000000000001f230000000000000000182300000000000000001d2300000000000000001f23000000000000000018230000000000000000
139
+ __music__
140
+ 03 08094040
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "romdevtools",
3
- "version": "0.86.0",
3
+ "version": "0.87.1",
4
4
  "description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 17 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, PlayStation, N64, Dreamcast, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
@@ -55,6 +55,7 @@
55
55
  "romdev-audio-resampler": "0.1.0",
56
56
  "romdev-core-beetle-psx-hw": "0.2.0",
57
57
  "romdev-core-bluemsx": "0.7.0",
58
+ "romdev-core-fake08": "0.1.0",
58
59
  "romdev-core-fceumm": "0.11.0",
59
60
  "romdev-core-flycast": "0.2.0",
60
61
  "romdev-core-gambatte": "0.10.0",
@@ -198,6 +198,33 @@ export const CAPABILITIES = {
198
198
  cart: true, disasm: true, decompile: true,
199
199
  },
200
200
  },
201
+ pico8: {
202
+ // FAKE-08 (MIT) runs PICO-8 carts. PICO-8 is a Lua VM, not a real CPU — so there's
203
+ // no machine code to disassemble/decompile and no CPU register file (cpuState N/A).
204
+ // But: build=PACKAGE a .p8 cart (Lua + gfx/sfx/map sections); run/screenshot work;
205
+ // memory works — the romdev patch exposes the full 64KB PICO-8 address space as
206
+ // system_ram (sprite sheet, map, flags, music, sfx, general RAM, screen buffer).
207
+ // disasm here is target:'source' — the cart IS Lua source, so we return the Lua
208
+ // itself (the honest "understand this cart" path), not a machine-code listing.
209
+ // It renders to a 128×128 framebuffer (poke to screen memory), so the inspect-*
210
+ // tile/sprite-table tools are N/A like gametank/the disc platforms.
211
+ // tier:"fantasy" — a fantasy console (Lua VM), not a CPU emulator. Held to its OWN
212
+ // conformance, NOT the canonical-14 cross-checks (which assume CPU disasm/decompile/
213
+ // cpuState/tile inspectors every real console has). Excluded from CONTRACT_PLATFORMS
214
+ // via NEXTGEN_TIER_PLATFORMS.
215
+ tier: "fantasy",
216
+ cpuFamily: "lua", decompileQuality: "n/a",
217
+ cpus: { main: "", secondary: [] }, // Lua VM — no CPU register file
218
+ audioChips: [], // audioDebug not wired (PICO-8's synth isn't a fixed-register chip we decode)
219
+ memoryRegions: [...GENERIC_REGIONS],
220
+ renderingKind: "framebuffer", introspection: "shallow",
221
+ ops: {
222
+ build: true, run: true, screenshot: true,
223
+ inspectSprites: false, inspectPalette: false, inspectBackground: false,
224
+ renderingContext: false, cpuState: false, audioDebug: false,
225
+ cart: false, disasm: true, decompile: false,
226
+ },
227
+ },
201
228
  pce: {
202
229
  cpuFamily: "huc6280", decompileQuality: "medium",
203
230
  cpus: { main: "", secondary: [] }, // getCPUState main NOT wired for pce
@@ -316,7 +343,7 @@ export const MIPS_TIER_PLATFORMS = Object.entries(CAPABILITIES)
316
343
  * is held to its OWN conformance, not the "all 14" cross-checks; a new platform
317
344
  * starts here (analysis-first) and graduates as run/build/etc. land. */
318
345
  export const NEXTGEN_TIER_PLATFORMS = Object.entries(CAPABILITIES)
319
- .filter(([, c]) => c.tier === "mips" || c.tier === "sh")
346
+ .filter(([, c]) => c.tier === "mips" || c.tier === "sh" || c.tier === "fantasy")
320
347
  .map(([p]) => p);
321
348
 
322
349
  /** Back-compat: the analysis-only set is now empty (PS1/N64 gained run/screenshot
@@ -54,6 +54,11 @@ export const CORES = {
54
54
  pce: { platform: "pce", coreName: "geargrafx", pkg: "romdev-core-geargrafx", displayName: "PC Engine / TurboGrafx-16 (Geargrafx)", aka: "turbografx,tg16,pcengine" },
55
55
  msx: { platform: "msx", coreName: "bluemsx", pkg: "romdev-core-bluemsx", displayName: "MSX / MSX2 (blueMSX)", aka: "msx2" },
56
56
  gametank: { platform: "gametank", coreName: "gametank", pkg: "romdev-core-gametank", displayName: "GameTank (Clyde Shaffer)", aka: "gtr" },
57
+ // FAKE-08 = open-source (MIT) PICO-8 player, NOT Lexaloffle's PICO-8, no BIOS. Runs
58
+ // .p8 (Lua source) + .p8.png carts at 128×128. It's a Lua VM, not a real CPU — the
59
+ // capability descriptor marks cpuState/disasm/memory-region tools not-applicable; the
60
+ // .p8 Lua source IS the readable "disassembly".
61
+ pico8: { platform: "pico8", coreName: "fake08", pkg: "romdev-core-fake08", displayName: "PICO-8 (FAKE-08)", aka: "p8,fake08" },
57
62
  // 32-bit MIPS tier. These cores HW-render (GL): the host lazy-loads the OPTIONAL
58
63
  // webgl-node bridge only when one of these boots (hwRender:true). The other 14 are
59
64
  // software-rendered and never touch GL, so a headless user without the GPU module
@@ -212,6 +212,7 @@ export const PLATFORM_VIRTUAL_EXT = {
212
212
  atari7800: ".a78",
213
213
  pce: ".pce",
214
214
  msx: ".rom",
215
+ pico8: ".p8",
215
216
  };
216
217
  import { RETRO_DEVICE_JOYPAD, ROMDEV_PIXEL_FORMAT_RGBA8888 } from "./retroConstants.js";
217
218
  import { decodeCode as decodeCheatCode } from "../cheats/gamegenie.js";
@@ -1190,6 +1190,59 @@ function renderBuildMd({ platform, romPath, regions, blobs, build, verifiable, n
1190
1190
  * The default endian follows the CPU (Genesis/m68k → BE; everything else → LE),
1191
1191
  * overridable via `endian`.
1192
1192
  */
1193
+ /**
1194
+ * target='source' — return a cart's high-level SOURCE instead of a disassembly, for
1195
+ * platforms where the "cart" IS source (not machine code). Today: PICO-8 (.p8 = Lua +
1196
+ * data sections; .p8.png = the same cart embedded in a label PNG). This is the honest
1197
+ * "understand this cart" path for a Lua VM — there's no machine code to disassemble, so
1198
+ * we hand back the actual Lua the game runs, plus a map of the other sections.
1199
+ */
1200
+ async function readCartSourceCore(args) {
1201
+ const romPath = requireRomPath(args);
1202
+ const fs = await import("node:fs/promises");
1203
+ const lower = romPath.toLowerCase();
1204
+ const isPng = lower.endsWith(".p8.png");
1205
+ const isP8 = lower.endsWith(".p8");
1206
+ if (!isP8 && !isPng && args.platform !== "pico8") {
1207
+ throw new Error(
1208
+ `disasm({target:'source'}) is for source-form carts (PICO-8 .p8/.p8.png). '${romPath}' isn't one. ` +
1209
+ `For machine-code ROMs use target:'rom'/'project'/'decompile'.`,
1210
+ );
1211
+ }
1212
+ if (isPng) {
1213
+ // A .p8.png embeds the cart in the PNG's low bits — decoding needs the FAKE-08 core.
1214
+ throw new Error(
1215
+ "disasm({target:'source'}): .p8.png carts store the source steganographically in the PNG — " +
1216
+ "load it (loadMedia({platform:'pico8'})) to run it; export the .p8 text form to read the Lua. " +
1217
+ "Pass a plain-text .p8 here to read its source directly.",
1218
+ );
1219
+ }
1220
+ const text = await fs.readFile(romPath, "utf8");
1221
+ // Split on __section__ markers, preserving order.
1222
+ const parts = text.split(/(^__[a-z]+__$)/m);
1223
+ const sections = {};
1224
+ let current = "(header)";
1225
+ let buf = [];
1226
+ const flush = () => { if (buf.length) sections[current] = buf.join("\n").replace(/^\n+|\n+$/g, ""); buf = []; };
1227
+ for (const chunk of parts) {
1228
+ const m = chunk.match(/^__([a-z]+)__$/);
1229
+ if (m) { flush(); current = m[1]; } else { buf.push(chunk); }
1230
+ }
1231
+ flush();
1232
+ return {
1233
+ platform: "pico8",
1234
+ kind: "source",
1235
+ format: ".p8",
1236
+ note: "PICO-8 carts are SOURCE, not machine code — this is the Lua the game runs (+ its data sections). There is no disassembly to do.",
1237
+ lua: sections.lua ?? "",
1238
+ sections: Object.keys(sections),
1239
+ // Include non-lua data sections so callers can see the full cart shape (gfx/map/sfx/music/gff/label).
1240
+ dataSections: Object.fromEntries(
1241
+ Object.entries(sections).filter(([k]) => k !== "lua" && k !== "(header)"),
1242
+ ),
1243
+ };
1244
+ }
1245
+
1193
1246
  async function pointerTableCore(args) {
1194
1247
  const { platform, loBase, hiBase, count, convention = "direct", reverseHandler, bank } = args;
1195
1248
  if (loBase == null) throw new Error("disasm({target:'pointerTable'}): loBase (the table / low-byte base) is required.");
@@ -1485,11 +1538,11 @@ export function registerDisasmTools(server, z) {
1485
1538
  "LLM folds it. `address` for all four comes from target:'functions' (a CPU/virtual address; the file-offset " +
1486
1539
  "mapping is handled for you).",
1487
1540
  {
1488
- target: z.enum(["bytes", "rom", "project", "references", "cfg", "xrefs", "functions", "decompile", "resolveJumptable", "pointerTable", "recompile"]).describe("bytes = raw chunk; rom = mapper-aware ROM; project = full rebuildable disasm; references = flat da65 operand-refs to an address; functions/cfg/xrefs = Rizin RE engine (function list / control-flow graph / deep graph xrefs); decompile = Ghidra C pseudocode; resolveJumptable = recover a computed-jump dispatcher's targets (LIVE — redirects to breakpoint({on:'jumptable'}), which runs the emulator and records the real switch arms a static decompiler can't follow); recompile = EMIT backend — statically recompile a NES ROM's reset routine to SNES 65816 asar source that builds + boots (phase 1: NROM, 6502→65816 emulation mode, PPU/APU seam STUBBED). See the tool description for the RE loop + the decompile altitude rule + per-CPU quality (all 14 platforms)."),
1541
+ target: z.enum(["bytes", "rom", "project", "references", "cfg", "xrefs", "functions", "decompile", "source", "resolveJumptable", "pointerTable", "recompile"]).describe("bytes = raw chunk; rom = mapper-aware ROM; project = full rebuildable disasm; references = flat da65 operand-refs to an address; functions/cfg/xrefs = Rizin RE engine (function list / control-flow graph / deep graph xrefs); decompile = Ghidra C pseudocode; resolveJumptable = recover a computed-jump dispatcher's targets (LIVE — redirects to breakpoint({on:'jumptable'}), which runs the emulator and records the real switch arms a static decompiler can't follow); recompile = EMIT backend — statically recompile a NES ROM's reset routine to SNES 65816 asar source that builds + boots (phase 1: NROM, 6502→65816 emulation mode, PPU/APU seam STUBBED). See the tool description for the RE loop + the decompile altitude rule + per-CPU quality (all 14 platforms)."),
1489
1542
  // shared
1490
1543
  path: z.string().optional().describe("target=bytes: raw binary path. target=rom/project/references: ROM file path."),
1491
1544
  base64: z.string().optional().describe("target=bytes: base64 of the bytes (OR `path`)."),
1492
- platform: z.enum(["nes", "snes", "sms", "gg", "gb", "gbc", "atari2600", "atari7800", "c64", "genesis", "gba", "pce", "msx", "lynx"]).optional().describe("target=rom/project/references: override platform (else sniffed from extension)."),
1545
+ platform: z.enum(["nes", "snes", "sms", "gg", "gb", "gbc", "atari2600", "atari7800", "c64", "genesis", "gba", "pce", "msx", "lynx", "pico8"]).optional().describe("target=rom/project/references: override platform (else sniffed from extension). target=source: pico8 (.p8 carts are Lua source)."),
1493
1546
  startAddress: z.number().int().min(0).max(0xffffffff).default(0x8000).describe("target=bytes/rom: address of the first byte (GBA auto-bumped to 0x08000000)."),
1494
1547
  length: z.number().int().min(1).max(65536).optional().describe("target=rom: bytes to disassemble (default 256; mutually exclusive with endAddress)."),
1495
1548
  addOrigin: z.boolean().default(true).describe("target=bytes/rom: prepend `.org` so the asm re-assembles through ca65."),
@@ -1540,6 +1593,7 @@ export function registerDisasmTools(server, z) {
1540
1593
  case "xrefs": return jsonContent(await analyzeXrefs(requireRomPath(args), args.address, args.platform));
1541
1594
  case "functions": return jsonContent(await analyzeFunctions(requireRomPath(args), args.platform));
1542
1595
  case "decompile": return jsonContent(await analyzeDecompile(requireRomPath(args), args.address, args.platform));
1596
+ case "source": return jsonContent(await readCartSourceCore(args));
1543
1597
  case "recompile": return await recompileCore(args);
1544
1598
  case "pointerTable": return jsonContent(await pointerTableCore(args));
1545
1599
  case "resolveJumptable":
@@ -662,6 +662,33 @@ const TEMPLATES = {
662
662
  mechanics: ["direct framebuffer paint (clear + solid rects)", "recognizable test pattern"],
663
663
  techniques: ["PowerVR2 framebuffer bring-up (FB_R_CTRL/FB_R_SIZE/FB_R_SOF1 + SPG)", "640x480 RGB565 at VRAM 0xA5000000", "SH-4 bare crt0 (stack + .bss + main)", "boots on Flycast reios HLE — no firmware"] },
664
664
  },
665
+ // PICO-8 (FAKE-08): the "source" IS a complete .p8 cart (Lua + __gfx__/__sfx__/__music__
666
+ // sections), so there's no crt0/runtime/linker — fork copies the .p8 and
667
+ // build({platform:'pico8', source:<the .p8 text>}) packages it. Each is a real playable
668
+ // game: hand-authored pixel-art sprites, looping music + SFX, title/play/over states,
669
+ // full genre mechanics. Fork one and reshape a single thing at a time.
670
+ pico8: {
671
+ shmup: { main: "templates/shmup.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
672
+ describe: "STAR SWEEPER — a vertical shooter. Ship sprite w/ thruster animation + i-frame blink, two enemy sprite types, projectile pool, ramping wave spawner, AABB collision, explosion sprites, score + persistent hi-score, looping title/play music that switches on start. The full shmup contract in ~130 lines of Lua.",
673
+ players: "1", mechanics: ["projectile pool", "wave spawner", "AABB collision", "i-frames", "title/play/over state machine"],
674
+ techniques: ["hand-authored __gfx__ sprites drawn with spr()", "__music__ + __sfx__ banks (music switches per state)", "sprite animation via frame flip", "scrolling starfield"] },
675
+ platformer: { main: "templates/platformer.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
676
+ describe: "HOP QUEST — a platformer. Hero sprite with idle/walk/jump frames (h-flipped by facing), gravity + solid-box collision, variable-height jump, animated spinning-coin sprites to collect, a goal flag, parallax hills, best-time tracking. Fork the movement/level and go.",
677
+ players: "1", mechanics: ["gravity + solid-box collision", "variable jump", "coin pickups", "goal + win state", "respawn on fall"],
678
+ techniques: ["hand-authored hero + coin sprites", "spr() h-flip by facing", "walk-cycle animation", "__music__ loop + jump/coin SFX", "parallax backdrop"] },
679
+ puzzle: { main: "templates/puzzle.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
680
+ describe: "COLOR DROP — a falling-gem match puzzle. Five faceted gem sprites, an 8×12 well, slide + soft/hard drop, horizontal match-3 line clears with a flash then gravity-settle + combo scoring, next-gem preview, game-over on stack-out. Looping music + drop/slide/match SFX.",
681
+ players: "1", mechanics: ["grid + falling piece", "match-3 clears", "gravity settle", "combo scoring", "next preview", "stack-out game over"],
682
+ techniques: ["hand-authored gem sprites", "flash-then-clear timing (state machine on a flash queue)", "column gravity compaction", "__music__ loop + SFX"] },
683
+ sports: { main: "templates/sports.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
684
+ describe: "RALLY VOLLEY — a 2-player paddle sports game. Paddle + ball sprites, angle-off-paddle physics, ball speed-up per rally, 1P-vs-CPU (tracking AI) or 2P couch mode toggled on the title, first-to-5 scoring. Looping music + wall/paddle/point SFX. The classic 2P scaffold.",
685
+ players: "1-2 (vs CPU or couch 2P)", mechanics: ["paddle + ball physics", "angle off paddle", "rally speed-up", "AI opponent", "first-to-5 match"],
686
+ techniques: ["hand-authored paddle/ball sprites", "mode toggle on title screen", "simple tracking AI", "__music__ loop + bounce/point SFX"] },
687
+ racing: { main: "templates/racing.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
688
+ describe: "LANE RUNNER — a top-down endless racer. Player + rival car sprites, 3-lane snap-steer dodging, scrolling road with animated lane dashes + shoulders, speed that ramps with distance, screen-shake on crash, distance score + best. Looping engine music + swerve/crash SFX.",
689
+ players: "1", mechanics: ["lane-snap steering", "rival spawner", "speed ramp", "crash + game over", "distance score"],
690
+ techniques: ["hand-authored car sprites", "scrolling-road illusion (moving dashes)", "camera() screen-shake", "__music__ loop + SFX"] },
691
+ },
665
692
  };
666
693
  // R37: GBC has its own scaffold tree at examples/gbc/templates/ +
667
694
  // src/platforms/gbc/lib/c/. Same runtime files as GB (the APU + Z80 +
@@ -1042,8 +1042,46 @@ export async function buildForPlatform(args) {
1042
1042
  };
1043
1043
  }
1044
1044
 
1045
+ if (args.platform === "pico8") {
1046
+ // PICO-8 "build" = PACKAGE a .p8 cart (plain-text sections), NOT compile to machine
1047
+ // code — the Lua is source. Accept bare Lua (`source`/`lua`) wrapped into a valid .p8,
1048
+ // or a complete .p8 passed through. Optional data sections (gfx/map/sfx/music) via
1049
+ // `sections`. The "binary" IS the .p8 text bytes, which FAKE-08 loads + runs directly.
1050
+ const { packP8 } = await import("./pico8/pack.js");
1051
+ try {
1052
+ const lua = args.lua ?? args.source ?? args.code;
1053
+ const { text, bytes, warnings } = packP8({
1054
+ lua,
1055
+ p8: args.p8,
1056
+ version: args.p8Version,
1057
+ sections: args.sections ?? {},
1058
+ });
1059
+ return {
1060
+ ok: true,
1061
+ binary: bytes,
1062
+ listing: text, // the readable .p8 IS the listing
1063
+ symbols: "",
1064
+ log: warnings.length ? warnings.map((w) => `[pico8] ${w}`).join("\n") : "[pico8] packaged .p8 cart",
1065
+ issues: [],
1066
+ exitCode: 0,
1067
+ toolchain: "pico8-pack",
1068
+ };
1069
+ } catch (e) {
1070
+ return {
1071
+ ok: false,
1072
+ binary: null,
1073
+ listing: "",
1074
+ symbols: "",
1075
+ log: String(e.message ?? e),
1076
+ issues: [],
1077
+ exitCode: 1,
1078
+ toolchain: "pico8-pack",
1079
+ };
1080
+ }
1081
+ }
1082
+
1045
1083
  throw new Error(
1046
- `no bundled toolchain for platform '${args.platform}'. Supported: atari2600 (dasm), nes/c64/atari7800/lynx (cc65), snes (C via tcc-65816+wla+PVSnesLib, or asm via asar), genesis (C via m68k-gcc+SGDK, or asm via vasm68k), gba (C via arm-gcc+libtonc/libgba), gb/gbc (sdcc sm83 / rgbds), sms/gg (sdcc). Call listPlatforms for the live matrix.`,
1084
+ `no bundled toolchain for platform '${args.platform}'. Supported: atari2600 (dasm), nes/c64/atari7800/lynx (cc65), snes (C via tcc-65816+wla+PVSnesLib, or asm via asar), genesis (C via m68k-gcc+SGDK, or asm via vasm68k), gba (C via arm-gcc+libtonc/libgba), gb/gbc (sdcc sm83 / rgbds), sms/gg (sdcc), pico8 (.p8 cart packager). Call listPlatforms for the live matrix.`,
1047
1085
  );
1048
1086
  }
1049
1087
 
@@ -0,0 +1,67 @@
1
+ // PICO-8 .p8 cart "builder" — a PACKAGER, not a compiler. PICO-8 carts are plain-text
2
+ // files with labeled sections (__lua__, __gfx__, __gff__, __label__, __map__, __sfx__,
3
+ // __music__); the Lua is source, not machine code. So "build" here = assemble a valid
4
+ // .p8 from the given Lua (+ optional data sections) that FAKE-08 can load and run.
5
+ //
6
+ // Accepts either a full .p8 already (passed through / validated) or bare Lua source
7
+ // (wrapped into a minimal but valid .p8). Data sections can be supplied verbatim.
8
+
9
+ const P8_HEADER = "pico-8 cartridge // http://www.pico-8.com";
10
+ const KNOWN_SECTIONS = ["lua", "gfx", "gff", "label", "map", "sfx", "music"];
11
+
12
+ /** Is this text already a full .p8 cart (has the header + a __lua__ section)? */
13
+ export function isP8Cart(text) {
14
+ return /^pico-8 cartridge/i.test(text.trimStart()) && /(^|\n)__lua__/.test(text);
15
+ }
16
+
17
+ /**
18
+ * Assemble a .p8 cart.
19
+ * @param {object} opts
20
+ * @param {string} opts.lua Lua source for the __lua__ section (required unless `p8` given).
21
+ * @param {string} [opts.p8] A complete .p8 already — validated + passed through.
22
+ * @param {number} [opts.version] PICO-8 format version line (default 18).
23
+ * @param {Object<string,string>} [opts.sections] Extra sections by name → body text
24
+ * (e.g. { gfx: "...", map: "...", sfx: "...", music: "..." }). Bodies are the raw
25
+ * hex/number rows PICO-8 uses; passed through verbatim.
26
+ * @returns {{ text: string, bytes: Uint8Array, warnings: string[] }}
27
+ */
28
+ export function packP8({ lua, p8, version = 18, sections = {} } = {}) {
29
+ const warnings = [];
30
+
31
+ if (p8 != null) {
32
+ if (!isP8Cart(p8)) {
33
+ throw new Error("pico8 build: `p8` was given but doesn't look like a .p8 cart (missing 'pico-8 cartridge' header or __lua__ section).");
34
+ }
35
+ const text = p8.endsWith("\n") ? p8 : p8 + "\n";
36
+ return { text, bytes: new TextEncoder().encode(text), warnings };
37
+ }
38
+
39
+ if (typeof lua !== "string" || lua.trim() === "") {
40
+ throw new Error("pico8 build: provide `lua` (the cart's Lua source) or a full `p8`.");
41
+ }
42
+
43
+ // Validate any extra section names.
44
+ for (const name of Object.keys(sections)) {
45
+ if (!KNOWN_SECTIONS.includes(name)) {
46
+ warnings.push(`unknown .p8 section '__${name}__' — passing through, but PICO-8/FAKE-08 may ignore it.`);
47
+ }
48
+ }
49
+
50
+ const parts = [P8_HEADER, `version ${version}`, "__lua__", lua.replace(/\n$/, "")];
51
+ // Emit data sections in canonical order (skip __lua__, handled above).
52
+ for (const name of KNOWN_SECTIONS) {
53
+ if (name === "lua") continue;
54
+ if (sections[name] != null) {
55
+ parts.push(`__${name}__`, String(sections[name]).replace(/\n$/, ""));
56
+ }
57
+ }
58
+ // Pass through any unknown sections last (so nothing the caller sent is dropped).
59
+ for (const [name, body] of Object.entries(sections)) {
60
+ if (!KNOWN_SECTIONS.includes(name)) {
61
+ parts.push(`__${name}__`, String(body).replace(/\n$/, ""));
62
+ }
63
+ }
64
+
65
+ const text = parts.join("\n") + "\n";
66
+ return { text, bytes: new TextEncoder().encode(text), warnings };
67
+ }