romdevtools 0.85.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/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to `romdevtools`. Dates are release dates.
4
4
  (Published as `romdev-mcp` through 0.11.0; renamed to `romdevtools` in 0.13.0 —
5
5
  the `romdev-mcp` bin is kept as an alias.)
6
6
 
7
+ ## 0.86.0 — 2026-07-01
8
+
9
+ - **GameTank Game Genie — a brand-new cheat-code format.** Nobody had made GameTank cheat codes
10
+ before; this adds a Game-Genie-style read-substitution device to the emulated GameTank core plus
11
+ a code format that is HARDWARE-COMPATIBLE (the same codes would work on a physical Game Genie
12
+ built for the console's open cart bus).
13
+ - Core (`romdev-core-gametank@0.2.0`): a value-override cheat device in the shared debug lib
14
+ (`romdev_cheat_set`/`romdev_cheat_read`/`romdev_cheat_get`, 24 slots). `MemoryRead` returns the
15
+ substitute byte on an address match, with optional compare-against-original (survives bank
16
+ switching) — exactly what a hardware Game Genie does. Any core that adds the one-line
17
+ `romdev_cheat_read` call to its bus read gets this for free.
18
+ - Format: `encodeGameTankGameGenie`/`decodeGameTankGameGenie` — a distinct 16-letter wheel
19
+ (`KLMNPQRSTVWXYZ23`), 16-bit address + 8-bit value (+ optional compare), scrambled with a
20
+ checksum. Plain codes are `XXX-XXXX`, compare codes `XXXX-XXXXX` (e.g. `$8100→0x42` = KTM-LPK3).
21
+ Wired into every device dispatcher; `cheats({op:'make', platform:'gametank', …})` generates
22
+ codes and `cheats({op:'apply'})` applies them live. GameTank's `retro_cheat_set` is a stub, so
23
+ `LibretroHost.setCheat` routes GameTank cheats through the romdev value-override device.
24
+ - **jsgame test no longer skips.** It ran only under `--experimental-vm-modules`, so the default
25
+ suite skipped it. It now runs in an isolated forked child process (SDL is main-thread-only;
26
+ rungame leaks handles) driven over rawr JSON-RPC — the child carries the flag and is killed when
27
+ the RPC resolves, so the main suite stays clean. Suite: 1099 tests, 1099 pass, 0 skipped.
28
+
7
29
  ## 0.85.0 — 2026-07-01
8
30
 
9
31
  - **Native-runtime game kinds — wasmcart + jsgame are now first-class hosts.** romdev is a
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