picopilot 0.0.0 → 0.1.0
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/LICENSE +661 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audio.d.ts +17 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +315 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/export.d.ts +36 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +257 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/gfx.d.ts +28 -0
- package/dist/commands/gfx.d.ts.map +1 -0
- package/dist/commands/gfx.js +521 -0
- package/dist/commands/gfx.js.map +1 -0
- package/dist/commands/init.d.ts +34 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +151 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +20 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +90 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/minify.d.ts +20 -0
- package/dist/commands/minify.d.ts.map +1 -0
- package/dist/commands/minify.js +129 -0
- package/dist/commands/minify.js.map +1 -0
- package/dist/commands/music.d.ts +13 -0
- package/dist/commands/music.d.ts.map +1 -0
- package/dist/commands/music.js +241 -0
- package/dist/commands/music.js.map +1 -0
- package/dist/commands/playtest.d.ts +29 -0
- package/dist/commands/playtest.d.ts.map +1 -0
- package/dist/commands/playtest.js +631 -0
- package/dist/commands/playtest.js.map +1 -0
- package/dist/commands/run.d.ts +30 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +50 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +213 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/sfx.d.ts +13 -0
- package/dist/commands/sfx.d.ts.map +1 -0
- package/dist/commands/sfx.js +254 -0
- package/dist/commands/sfx.js.map +1 -0
- package/dist/commands/tokens.d.ts +36 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +129 -0
- package/dist/commands/tokens.js.map +1 -0
- package/dist/commands/verify.d.ts +57 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +213 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +24 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/engine/audio/index.d.ts +14 -0
- package/dist/engine/audio/index.d.ts.map +1 -0
- package/dist/engine/audio/index.js +14 -0
- package/dist/engine/audio/index.js.map +1 -0
- package/dist/engine/audio/music.d.ts +116 -0
- package/dist/engine/audio/music.d.ts.map +1 -0
- package/dist/engine/audio/music.js +138 -0
- package/dist/engine/audio/music.js.map +1 -0
- package/dist/engine/audio/section.d.ts +34 -0
- package/dist/engine/audio/section.d.ts.map +1 -0
- package/dist/engine/audio/section.js +67 -0
- package/dist/engine/audio/section.js.map +1 -0
- package/dist/engine/audio/sfx.d.ts +141 -0
- package/dist/engine/audio/sfx.d.ts.map +1 -0
- package/dist/engine/audio/sfx.js +506 -0
- package/dist/engine/audio/sfx.js.map +1 -0
- package/dist/engine/cart/cart.d.ts +75 -0
- package/dist/engine/cart/cart.d.ts.map +1 -0
- package/dist/engine/cart/cart.js +153 -0
- package/dist/engine/cart/cart.js.map +1 -0
- package/dist/engine/cart/errors.d.ts +29 -0
- package/dist/engine/cart/errors.d.ts.map +1 -0
- package/dist/engine/cart/errors.js +26 -0
- package/dist/engine/cart/errors.js.map +1 -0
- package/dist/engine/cart/gfx.d.ts +79 -0
- package/dist/engine/cart/gfx.d.ts.map +1 -0
- package/dist/engine/cart/gfx.js +171 -0
- package/dist/engine/cart/gfx.js.map +1 -0
- package/dist/engine/cart/index.d.ts +15 -0
- package/dist/engine/cart/index.d.ts.map +1 -0
- package/dist/engine/cart/index.js +15 -0
- package/dist/engine/cart/index.js.map +1 -0
- package/dist/engine/cart/map.d.ts +46 -0
- package/dist/engine/cart/map.d.ts.map +1 -0
- package/dist/engine/cart/map.js +118 -0
- package/dist/engine/cart/map.js.map +1 -0
- package/dist/engine/config/config.d.ts +74 -0
- package/dist/engine/config/config.d.ts.map +1 -0
- package/dist/engine/config/config.js +103 -0
- package/dist/engine/config/config.js.map +1 -0
- package/dist/engine/config/index.d.ts +7 -0
- package/dist/engine/config/index.d.ts.map +1 -0
- package/dist/engine/config/index.js +7 -0
- package/dist/engine/config/index.js.map +1 -0
- package/dist/engine/gfx/grid.d.ts +51 -0
- package/dist/engine/gfx/grid.d.ts.map +1 -0
- package/dist/engine/gfx/grid.js +116 -0
- package/dist/engine/gfx/grid.js.map +1 -0
- package/dist/engine/gfx/index.d.ts +17 -0
- package/dist/engine/gfx/index.d.ts.map +1 -0
- package/dist/engine/gfx/index.js +17 -0
- package/dist/engine/gfx/index.js.map +1 -0
- package/dist/engine/gfx/label.d.ts +34 -0
- package/dist/engine/gfx/label.d.ts.map +1 -0
- package/dist/engine/gfx/label.js +190 -0
- package/dist/engine/gfx/label.js.map +1 -0
- package/dist/engine/gfx/overlap.d.ts +42 -0
- package/dist/engine/gfx/overlap.d.ts.map +1 -0
- package/dist/engine/gfx/overlap.js +57 -0
- package/dist/engine/gfx/overlap.js.map +1 -0
- package/dist/engine/gfx/palette.d.ts +29 -0
- package/dist/engine/gfx/palette.d.ts.map +1 -0
- package/dist/engine/gfx/palette.js +46 -0
- package/dist/engine/gfx/palette.js.map +1 -0
- package/dist/engine/gfx/png.d.ts +31 -0
- package/dist/engine/gfx/png.d.ts.map +1 -0
- package/dist/engine/gfx/png.js +165 -0
- package/dist/engine/gfx/png.js.map +1 -0
- package/dist/engine/gfx/render.d.ts +42 -0
- package/dist/engine/gfx/render.d.ts.map +1 -0
- package/dist/engine/gfx/render.js +72 -0
- package/dist/engine/gfx/render.js.map +1 -0
- package/dist/engine/index.d.ts +22 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +22 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/pico8/adapter.d.ts +267 -0
- package/dist/engine/pico8/adapter.d.ts.map +1 -0
- package/dist/engine/pico8/adapter.js +37 -0
- package/dist/engine/pico8/adapter.js.map +1 -0
- package/dist/engine/pico8/drive.d.ts +308 -0
- package/dist/engine/pico8/drive.d.ts.map +1 -0
- package/dist/engine/pico8/drive.js +581 -0
- package/dist/engine/pico8/drive.js.map +1 -0
- package/dist/engine/pico8/harness.d.ts +146 -0
- package/dist/engine/pico8/harness.d.ts.map +1 -0
- package/dist/engine/pico8/harness.js +251 -0
- package/dist/engine/pico8/harness.js.map +1 -0
- package/dist/engine/pico8/index.d.ts +21 -0
- package/dist/engine/pico8/index.d.ts.map +1 -0
- package/dist/engine/pico8/index.js +21 -0
- package/dist/engine/pico8/index.js.map +1 -0
- package/dist/engine/pico8/sentinel.d.ts +63 -0
- package/dist/engine/pico8/sentinel.d.ts.map +1 -0
- package/dist/engine/pico8/sentinel.js +86 -0
- package/dist/engine/pico8/sentinel.js.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.js +11 -0
- package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
- package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
- package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-main.js +71 -0
- package/dist/engine/pico8/session-daemon-main.js.map +1 -0
- package/dist/engine/pico8/session-daemon.d.ts +68 -0
- package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon.js +264 -0
- package/dist/engine/pico8/session-daemon.js.map +1 -0
- package/dist/engine/pico8/session.d.ts +140 -0
- package/dist/engine/pico8/session.d.ts.map +1 -0
- package/dist/engine/pico8/session.js +250 -0
- package/dist/engine/pico8/session.js.map +1 -0
- package/dist/engine/pico8/shell.d.ts +103 -0
- package/dist/engine/pico8/shell.d.ts.map +1 -0
- package/dist/engine/pico8/shell.js +324 -0
- package/dist/engine/pico8/shell.js.map +1 -0
- package/dist/engine/pico8/supervisor.d.ts +137 -0
- package/dist/engine/pico8/supervisor.d.ts.map +1 -0
- package/dist/engine/pico8/supervisor.js +140 -0
- package/dist/engine/pico8/supervisor.js.map +1 -0
- package/dist/engine/scaffold/index.d.ts +8 -0
- package/dist/engine/scaffold/index.d.ts.map +1 -0
- package/dist/engine/scaffold/index.js +8 -0
- package/dist/engine/scaffold/index.js.map +1 -0
- package/dist/engine/scaffold/scaffold.d.ts +49 -0
- package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
- package/dist/engine/scaffold/scaffold.js +217 -0
- package/dist/engine/scaffold/scaffold.js.map +1 -0
- package/dist/engine/shrinko/adapter.d.ts +167 -0
- package/dist/engine/shrinko/adapter.d.ts.map +1 -0
- package/dist/engine/shrinko/adapter.js +107 -0
- package/dist/engine/shrinko/adapter.js.map +1 -0
- package/dist/engine/shrinko/index.d.ts +13 -0
- package/dist/engine/shrinko/index.d.ts.map +1 -0
- package/dist/engine/shrinko/index.js +13 -0
- package/dist/engine/shrinko/index.js.map +1 -0
- package/dist/engine/shrinko/shell.d.ts +69 -0
- package/dist/engine/shrinko/shell.d.ts.map +1 -0
- package/dist/engine/shrinko/shell.js +156 -0
- package/dist/engine/shrinko/shell.js.map +1 -0
- package/dist/engine/skills/index.d.ts +8 -0
- package/dist/engine/skills/index.d.ts.map +1 -0
- package/dist/engine/skills/index.js +8 -0
- package/dist/engine/skills/index.js.map +1 -0
- package/dist/engine/skills/skills.d.ts +104 -0
- package/dist/engine/skills/skills.d.ts.map +1 -0
- package/dist/engine/skills/skills.js +149 -0
- package/dist/engine/skills/skills.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -2
- package/src/bin.ts +4 -0
- package/src/cli.ts +61 -0
- package/src/commands/audio.test.ts +280 -0
- package/src/commands/audio.ts +410 -0
- package/src/commands/export.test.ts +310 -0
- package/src/commands/export.ts +316 -0
- package/src/commands/gfx-io.test.ts +178 -0
- package/src/commands/gfx.test.ts +470 -0
- package/src/commands/gfx.ts +645 -0
- package/src/commands/init.test.ts +460 -0
- package/src/commands/init.ts +193 -0
- package/src/commands/lint.test.ts +137 -0
- package/src/commands/lint.ts +117 -0
- package/src/commands/minify.test.ts +166 -0
- package/src/commands/minify.ts +156 -0
- package/src/commands/music.test.ts +272 -0
- package/src/commands/music.ts +288 -0
- package/src/commands/playtest.test.ts +317 -0
- package/src/commands/playtest.ts +773 -0
- package/src/commands/run.test.ts +238 -0
- package/src/commands/run.ts +259 -0
- package/src/commands/serve.test.ts +218 -0
- package/src/commands/serve.ts +272 -0
- package/src/commands/sfx.test.ts +349 -0
- package/src/commands/sfx.ts +300 -0
- package/src/commands/tokens.test.ts +212 -0
- package/src/commands/tokens.ts +168 -0
- package/src/commands/verify.test.ts +238 -0
- package/src/commands/verify.ts +256 -0
- package/src/commands/version.test.ts +70 -0
- package/src/commands/version.ts +24 -0
- package/src/engine/audio/index.ts +47 -0
- package/src/engine/audio/music.test.ts +188 -0
- package/src/engine/audio/music.ts +218 -0
- package/src/engine/audio/section.test.ts +124 -0
- package/src/engine/audio/section.ts +79 -0
- package/src/engine/audio/sfx.test.ts +410 -0
- package/src/engine/audio/sfx.ts +694 -0
- package/src/engine/cart/cart.test.ts +304 -0
- package/src/engine/cart/cart.ts +211 -0
- package/src/engine/cart/errors.ts +39 -0
- package/src/engine/cart/fixtures/crlf.p8 +6 -0
- package/src/engine/cart/fixtures/full.p8 +174 -0
- package/src/engine/cart/fixtures/header-only.p8 +2 -0
- package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
- package/src/engine/cart/fixtures/minimal.p8 +10 -0
- package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
- package/src/engine/cart/gfx.ts +195 -0
- package/src/engine/cart/index.ts +27 -0
- package/src/engine/cart/map.ts +134 -0
- package/src/engine/config/config.ts +115 -0
- package/src/engine/config/index.ts +12 -0
- package/src/engine/gfx/grid.test.ts +128 -0
- package/src/engine/gfx/grid.ts +136 -0
- package/src/engine/gfx/index.ts +34 -0
- package/src/engine/gfx/label.test.ts +105 -0
- package/src/engine/gfx/label.ts +229 -0
- package/src/engine/gfx/overlap.test.ts +71 -0
- package/src/engine/gfx/overlap.ts +66 -0
- package/src/engine/gfx/palette.test.ts +35 -0
- package/src/engine/gfx/palette.ts +54 -0
- package/src/engine/gfx/png.test.ts +111 -0
- package/src/engine/gfx/png.ts +196 -0
- package/src/engine/gfx/render.test.ts +180 -0
- package/src/engine/gfx/render.ts +106 -0
- package/src/engine/index.ts +21 -0
- package/src/engine/pico8/adapter.ts +303 -0
- package/src/engine/pico8/drive.test.ts +415 -0
- package/src/engine/pico8/drive.ts +729 -0
- package/src/engine/pico8/harness.test.ts +181 -0
- package/src/engine/pico8/harness.ts +354 -0
- package/src/engine/pico8/index.ts +20 -0
- package/src/engine/pico8/sentinel.test.ts +80 -0
- package/src/engine/pico8/sentinel.ts +93 -0
- package/src/engine/pico8/session-daemon-entry.ts +12 -0
- package/src/engine/pico8/session-daemon-main.ts +98 -0
- package/src/engine/pico8/session-daemon.test.ts +212 -0
- package/src/engine/pico8/session-daemon.ts +306 -0
- package/src/engine/pico8/session.test.ts +239 -0
- package/src/engine/pico8/session.ts +321 -0
- package/src/engine/pico8/shell.test.ts +583 -0
- package/src/engine/pico8/shell.ts +412 -0
- package/src/engine/pico8/supervisor.test.ts +105 -0
- package/src/engine/pico8/supervisor.ts +197 -0
- package/src/engine/scaffold/index.ts +15 -0
- package/src/engine/scaffold/scaffold.ts +236 -0
- package/src/engine/shrinko/adapter.ts +241 -0
- package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
- package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
- package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
- package/src/engine/shrinko/index.ts +33 -0
- package/src/engine/shrinko/shell.ts +224 -0
- package/src/engine/shrinko/shrinko.test.ts +182 -0
- package/src/engine/skills/index.ts +14 -0
- package/src/engine/skills/skills.test.ts +376 -0
- package/src/engine/skills/skills.ts +205 -0
- package/src/index.ts +13 -0
- package/src/skills/game-design-reference/SKILL.md +70 -0
- package/src/skills/game-jam/SKILL.md +45 -0
- package/src/skills/picopilot-art/SKILL.md +62 -0
- package/src/skills/picopilot-audio/SKILL.md +194 -0
- package/src/skills/picopilot-code/SKILL.md +97 -0
- package/src/skills/picopilot-code/reference/README.md +42 -0
- package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
- package/src/skills/picopilot-code/reference/platformer.md +119 -0
- package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
- package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
- package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
- package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
- package/src/skills/picopilot-debug/SKILL.md +195 -0
- package/src/skills/picopilot-overview/SKILL.md +52 -0
- package/src/version.ts +22 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Puzzle / grid reference (discrete cells, tile-step movement, rules)
|
|
2
|
+
|
|
3
|
+
Structure for a grid puzzle (block-pushing, rule-based, or match games). The
|
|
4
|
+
world is a 2D array of CELLS, the player moves ONE cell per input (not by
|
|
5
|
+
velocity), and a
|
|
6
|
+
turn is: read intent, test the move against rules, commit if legal. There is no
|
|
7
|
+
gravity and no sub-pixel motion here; do NOT reach for the platformer shape.
|
|
8
|
+
|
|
9
|
+
## The load-bearing rules
|
|
10
|
+
|
|
11
|
+
1. State is a GRID, not a pixel position. Store `col,row` (integers), render by
|
|
12
|
+
multiplying by 8 (`x=col*8`). Movement changes `col,row` by exactly +/-1.
|
|
13
|
+
2. Input is DISCRETE: use `btnp` (fires once per press), never `btn` (fires every
|
|
14
|
+
frame = the player rockets across the board).
|
|
15
|
+
3. A move is a TRANSACTION: compute the target cell, check every rule (in bounds?
|
|
16
|
+
wall? pushable block that itself can move?), and only then mutate the grid.
|
|
17
|
+
Never move first and undo, it corrupts multi-object pushes.
|
|
18
|
+
4. Grids are 1-based in Lua. Guard bounds explicitly (`c>=1 and c<=cols`).
|
|
19
|
+
|
|
20
|
+
## Grid setup (rows as split strings = cheap + readable levels)
|
|
21
|
+
|
|
22
|
+
```lua
|
|
23
|
+
cols,rows=16,16
|
|
24
|
+
-- a level as one string per row: .=floor #=wall @=player $=box
|
|
25
|
+
level=split("################, #@.....#......#, #..$..........#, ...(more rows)...", ",")
|
|
26
|
+
|
|
27
|
+
grid={} -- grid[r][c] = tile char
|
|
28
|
+
player={c=2,r=2}
|
|
29
|
+
|
|
30
|
+
function load_level()
|
|
31
|
+
grid={}
|
|
32
|
+
for r=1,rows do
|
|
33
|
+
grid[r]={}
|
|
34
|
+
local row=level[r]
|
|
35
|
+
for c=1,cols do
|
|
36
|
+
local ch=sub(row,c,c)
|
|
37
|
+
grid[r][c]=ch
|
|
38
|
+
if ch=="@" then player.c,player.r=c,r grid[r][c]="." end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Discrete move as a transaction (with box-push)
|
|
45
|
+
|
|
46
|
+
```lua
|
|
47
|
+
-- dir -> delta. split pairs are the idiomatic lookup.
|
|
48
|
+
dc=split"0,0,-1,1" -- up,down,left,right dx (matches btnp 2,3,0,1 order below)
|
|
49
|
+
dr=split"-1,1,0,0"
|
|
50
|
+
|
|
51
|
+
function cell(c,r) -- safe read; out of bounds reads as wall
|
|
52
|
+
if c<1 or c>cols or r<1 or r>rows then return "#" end
|
|
53
|
+
return grid[r][c]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
function try_move(dx,dy)
|
|
57
|
+
local nc,nr=player.c+dx,player.r+dy
|
|
58
|
+
local t=cell(nc,nr)
|
|
59
|
+
if t=="#" then return end -- wall: no move
|
|
60
|
+
if t=="$" then -- box: can it be pushed one further?
|
|
61
|
+
local bc,br=nc+dx,nr+dy
|
|
62
|
+
if cell(bc,br)~="." then return end -- blocked behind: whole move illegal
|
|
63
|
+
grid[br][bc]="$" grid[nr][nc]="." -- commit the push
|
|
64
|
+
end
|
|
65
|
+
player.c,player.r=nc,nr -- commit the player move
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
function _update()
|
|
69
|
+
-- btnp order: 0 left,1 right,2 up,3 down
|
|
70
|
+
if btnp(0) then try_move(-1,0) end
|
|
71
|
+
if btnp(1) then try_move(1,0) end
|
|
72
|
+
if btnp(2) then try_move(0,-1) end
|
|
73
|
+
if btnp(3) then try_move(0,1) end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Render (grid -> screen, cell*8)
|
|
78
|
+
|
|
79
|
+
```lua
|
|
80
|
+
function _draw()
|
|
81
|
+
cls(1)
|
|
82
|
+
for r=1,rows do for c=1,cols do
|
|
83
|
+
local t=grid[r][c]
|
|
84
|
+
if t=="#" then rectfill((c-1)*8,(r-1)*8,(c-1)*8+7,(r-1)*8+7,5)
|
|
85
|
+
elseif t=="$" then spr(2,(c-1)*8,(r-1)*8) end
|
|
86
|
+
end end
|
|
87
|
+
spr(1,(player.c-1)*8,(player.r-1)*8) -- player
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Win / rule evaluation
|
|
92
|
+
|
|
93
|
+
Evaluate win AFTER a committed move, by scanning the grid (e.g. every box on a
|
|
94
|
+
target). For a game where the RULES are themselves objects on the grid, the rule
|
|
95
|
+
set is DATA you re-parse each turn (scan the grid for rule triples, rebuild a
|
|
96
|
+
properties table), then movement/legality queries that table. Keep the rule pass
|
|
97
|
+
separate from the move pass; do not interleave them.
|
|
98
|
+
|
|
99
|
+
## Genre pitfalls checklist
|
|
100
|
+
|
|
101
|
+
- [ ] Position is integer `col,row`, rendered as `*8`, not a pixel velocity.
|
|
102
|
+
- [ ] Input uses `btnp`, not `btn`.
|
|
103
|
+
- [ ] Moves are validated fully BEFORE any grid mutation (transaction).
|
|
104
|
+
- [ ] Multi-object pushes check the space behind the pushed object.
|
|
105
|
+
- [ ] Bounds guarded (1-based); out-of-bounds reads treated as wall.
|
|
106
|
+
- [ ] Win/rule evaluation is a separate pass after the move commits.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# RPG menus + dialog reference (UI state machines, text reveal)
|
|
2
|
+
|
|
3
|
+
Structure for the menu/dialog side of an RPG or any UI-driven game (title menus,
|
|
4
|
+
inventory, shop, dialog boxes, turn-based battle menus). The defining problem is
|
|
5
|
+
NOT movement, it is a STACK of UI STATES, a cursor over a list of options, and
|
|
6
|
+
text that reveals over time. Get the state stack and the cursor-wrap right; the
|
|
7
|
+
"game feel" here is menu responsiveness and readable text pacing.
|
|
8
|
+
|
|
9
|
+
## The load-bearing rules
|
|
10
|
+
|
|
11
|
+
1. UI is a STACK of modes (`{"map"}` -> push `"menu"` -> push `"item"`). Input and
|
|
12
|
+
draw dispatch on the TOP of the stack. Opening a submenu PUSHES; canceling
|
|
13
|
+
(btnp 5 / X) POPS. This is cleaner than a flat `mode` string once menus nest.
|
|
14
|
+
2. Menu input is DISCRETE: `btnp` only. A cursor is an index into an options list;
|
|
15
|
+
moving wraps with `%`. Confirm = btnp(4), cancel = btnp(5).
|
|
16
|
+
3. Dialog text REVEALS over time (one char every few frames), not all at once.
|
|
17
|
+
Track a reveal counter; a confirm press either fast-forwards to full text or,
|
|
18
|
+
if already full, advances to the next line.
|
|
19
|
+
4. Draw menus in SCREEN space. If the field behind is camera-offset, `camera()`
|
|
20
|
+
(reset) before drawing the box, or it slides with the world.
|
|
21
|
+
|
|
22
|
+
## Mode stack + dispatch
|
|
23
|
+
|
|
24
|
+
```lua
|
|
25
|
+
stack={"field"}
|
|
26
|
+
function top() return stack[#stack] end
|
|
27
|
+
function push(m) add(stack,m) end
|
|
28
|
+
function pop() if #stack>1 then deli(stack) end end -- never pop the base mode
|
|
29
|
+
|
|
30
|
+
function _update()
|
|
31
|
+
local m=top()
|
|
32
|
+
if m=="field" then
|
|
33
|
+
update_field()
|
|
34
|
+
if btnp(4) then push("menu") menu_cur=1 end
|
|
35
|
+
elseif m=="menu" then
|
|
36
|
+
update_menu()
|
|
37
|
+
elseif m=="dialog" then
|
|
38
|
+
update_dialog()
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Cursor over a list, with wrap
|
|
44
|
+
|
|
45
|
+
```lua
|
|
46
|
+
menu_items=split"items,magic,status,save" -- data as a split string
|
|
47
|
+
menu_cur=1
|
|
48
|
+
|
|
49
|
+
function update_menu()
|
|
50
|
+
-- up/down move the cursor, wrapping (1-based, so the %+1 dance)
|
|
51
|
+
if btnp(2) then menu_cur=(menu_cur-2)%#menu_items+1 end
|
|
52
|
+
if btnp(3) then menu_cur=menu_cur%#menu_items+1 end
|
|
53
|
+
if btnp(4) then choose(menu_items[menu_cur]) end -- confirm
|
|
54
|
+
if btnp(5) then pop() end -- cancel closes the menu
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
function draw_menu()
|
|
58
|
+
camera() -- SCREEN space
|
|
59
|
+
rectfill(80,8,120,40,0) rect(80,8,120,40,7)
|
|
60
|
+
for i,it in ipairs(menu_items) do
|
|
61
|
+
local y=12+(i-1)*7
|
|
62
|
+
print(it,88,y, i==menu_cur and 7 or 5)
|
|
63
|
+
if i==menu_cur then print(">",83,y,7) end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Dialog with timed text reveal
|
|
69
|
+
|
|
70
|
+
```lua
|
|
71
|
+
dlg={text="",shown=0}
|
|
72
|
+
function say(s) dlg.text=s dlg.shown=0 push("dialog") end
|
|
73
|
+
|
|
74
|
+
function update_dialog()
|
|
75
|
+
if dlg.shown < #dlg.text then
|
|
76
|
+
dlg.shown+=0.5 -- ~2 chars/frame; tune for pacing
|
|
77
|
+
if btnp(4) then dlg.shown=#dlg.text end -- confirm fast-forwards to full
|
|
78
|
+
else
|
|
79
|
+
if btnp(4) then pop() end -- fully shown: confirm closes/advances
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
function draw_dialog()
|
|
84
|
+
camera()
|
|
85
|
+
rectfill(4,100,123,123,1) rect(4,100,123,123,7)
|
|
86
|
+
print(sub(dlg.text,1,flr(dlg.shown)), 8,104, 7) -- sub reveals up to `shown`
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
(For multi-line/word-wrapped dialog, pre-split the text into lines that fit the box
|
|
91
|
+
width and reveal line by line. `print` glyphs are ~4px wide, so ~28 chars per line
|
|
92
|
+
in a full-width box.)
|
|
93
|
+
|
|
94
|
+
## Turn-based battle menu (same primitives)
|
|
95
|
+
|
|
96
|
+
A battle is the SAME machinery: a mode on the stack (`"battle"`), a cursor over
|
|
97
|
+
actions (`Fight/Item/Run`), pushing a target-select submenu, then running the
|
|
98
|
+
action and popping back. Keep the battle STATE (hp, turn order) separate from the
|
|
99
|
+
MENU state (which option is highlighted); do not entangle them.
|
|
100
|
+
|
|
101
|
+
## Coroutine for scripted sequences (optional)
|
|
102
|
+
|
|
103
|
+
For a scripted intro / cutscene (text, then move a sprite, then more text), a
|
|
104
|
+
coroutine reads far cleaner than a hand-rolled step counter:
|
|
105
|
+
|
|
106
|
+
```lua
|
|
107
|
+
function intro()
|
|
108
|
+
say("long ago...") yield_until_closed()
|
|
109
|
+
-- move the king sprite, wait, etc.
|
|
110
|
+
say("...a hero was needed.") yield_until_closed()
|
|
111
|
+
end
|
|
112
|
+
-- drive it: seq=cocreate(intro); each frame if costatus(seq)~="dead" then coresume(seq) end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Genre pitfalls checklist
|
|
116
|
+
|
|
117
|
+
- [ ] UI is a STACK (push submenu / pop on cancel), dispatched on the top mode.
|
|
118
|
+
- [ ] Menu input is `btnp` only; cursor index WRAPS with `%` (mind 1-based).
|
|
119
|
+
- [ ] Dialog text reveals over time; confirm fast-forwards, then advances.
|
|
120
|
+
- [ ] Menus/boxes drawn in SCREEN space (`camera()` reset first).
|
|
121
|
+
- [ ] Battle/menu STATE kept separate from which option is highlighted.
|
|
122
|
+
- [ ] Word-wrap pre-computed to the box width (~28 chars full-width).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Top-down adventure reference (tiled world, 4-dir movement, interaction)
|
|
2
|
+
|
|
3
|
+
Structure for a top-down explorer (action-adventure, RPG overworld). The player walks a
|
|
4
|
+
tiled world in 4 (or 8) directions, collides with solid tiles, the camera follows
|
|
5
|
+
across a map larger than the screen, and the player interacts with specific tiles
|
|
6
|
+
(doors, chests, NPCs). Unlike the platformer there is no gravity; unlike the
|
|
7
|
+
puzzle grid, movement is smooth pixel motion, not cell-stepping.
|
|
8
|
+
|
|
9
|
+
## The load-bearing rules
|
|
10
|
+
|
|
11
|
+
1. The player has a pixel position and a FACING direction. Movement is smooth
|
|
12
|
+
(pixels/frame), but collision is still tested against map tiles via
|
|
13
|
+
`mget`/`fget`.
|
|
14
|
+
2. Resolve X and Y separately (same axis-by-axis idea as the platformer) so the
|
|
15
|
+
player slides along walls instead of sticking on corners.
|
|
16
|
+
3. Facing is state you keep for animation AND for "what am I interacting with"
|
|
17
|
+
(the tile in front of the player).
|
|
18
|
+
4. The map is bigger than 128x128: `camera` follows the player and CLAMPs to the
|
|
19
|
+
map's pixel bounds. `map()` draws the whole tilemap in world space.
|
|
20
|
+
|
|
21
|
+
## Movement + per-axis wall collision
|
|
22
|
+
|
|
23
|
+
```lua
|
|
24
|
+
speed=1.5
|
|
25
|
+
player={x=64,y=64,face=3} -- face: 0 left,1 right,2 up,3 down (btn order)
|
|
26
|
+
|
|
27
|
+
function solid(x,y) return fget(mget(x\8,y\8),0) end -- flag 0 = solid
|
|
28
|
+
function blocked(x,y) -- 8x8 body, four corners
|
|
29
|
+
return solid(x,y) or solid(x+7,y) or solid(x,y+7) or solid(x+7,y+7)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
function update_player(p)
|
|
33
|
+
local dx,dy=0,0
|
|
34
|
+
if btn(0) then dx=-speed p.face=0 end
|
|
35
|
+
if btn(1) then dx=speed p.face=1 end
|
|
36
|
+
if btn(2) then dy=-speed p.face=2 end
|
|
37
|
+
if btn(3) then dy=speed p.face=3 end
|
|
38
|
+
|
|
39
|
+
-- resolve X then Y separately (slide along walls)
|
|
40
|
+
if not blocked(p.x+dx,p.y) then p.x+=dx end
|
|
41
|
+
if not blocked(p.x,p.y+dy) then p.y+=dy end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The tile in front (interaction target)
|
|
46
|
+
|
|
47
|
+
```lua
|
|
48
|
+
fdx=split"-1,1,0,0" -- indexed by face+1: left,right,up,down
|
|
49
|
+
fdy=split"0,0,-1,1"
|
|
50
|
+
|
|
51
|
+
function front_tile(p)
|
|
52
|
+
-- the map cell one step ahead in the facing direction
|
|
53
|
+
local fx=p.x+4 + fdx[p.face+1]*8 -- +4 = center of the 8px body
|
|
54
|
+
local fy=p.y+4 + fdy[p.face+1]*8
|
|
55
|
+
return fx\8, fy\8
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
function _update()
|
|
59
|
+
update_player(player)
|
|
60
|
+
if btnp(4) then -- action button: interact with the tile ahead
|
|
61
|
+
local tx,ty=front_tile(player)
|
|
62
|
+
local s=mget(tx,ty)
|
|
63
|
+
if s==CHEST then mset(tx,ty,CHEST_OPEN) got_item() end
|
|
64
|
+
-- doors, signs, NPCs: branch on the sprite id at (tx,ty)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Camera follow with map-edge clamp
|
|
70
|
+
|
|
71
|
+
```lua
|
|
72
|
+
-- map_w,map_h = map size in PIXELS (tiles*8).
|
|
73
|
+
function update_camera(p)
|
|
74
|
+
local cx=mid(0, p.x-64, map_w-128)
|
|
75
|
+
local cy=mid(0, p.y-64, map_h-128)
|
|
76
|
+
camera(cx,cy)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
function _draw()
|
|
80
|
+
cls()
|
|
81
|
+
update_camera(player)
|
|
82
|
+
map() -- whole tilemap, world space
|
|
83
|
+
spr(walk_frame(player), flr(player.x), flr(player.y), 1,1, player.face==0)
|
|
84
|
+
camera() -- RESET before the HUD
|
|
85
|
+
draw_hud()
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Rooms / streaming (optional, for large worlds)
|
|
90
|
+
|
|
91
|
+
Two common approaches:
|
|
92
|
+
- One big shared map, camera just moves over it (simplest; limited by the 128x32
|
|
93
|
+
map size, extendable with the shared bank).
|
|
94
|
+
- Room-based: each screen is a fixed 16x16 tile region; when the player crosses
|
|
95
|
+
an edge, snap the camera to the next room and reposition the player. Store room
|
|
96
|
+
coords, draw `map(room_c*16, room_r*16, 0,0, 16,16)`.
|
|
97
|
+
|
|
98
|
+
## Animation (frame per facing + walk cycle)
|
|
99
|
+
|
|
100
|
+
```lua
|
|
101
|
+
-- 2-frame walk per direction; base sprite per facing in a split lookup.
|
|
102
|
+
face_base=split"16,16,32,48" -- left/right share sprites, flipped
|
|
103
|
+
function walk_frame(p)
|
|
104
|
+
local moving = btn(0) or btn(1) or btn(2) or btn(3)
|
|
105
|
+
local step = moving and flr(t()*4%2) or 0
|
|
106
|
+
return face_base[p.face+1]+step
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Genre pitfalls checklist
|
|
111
|
+
|
|
112
|
+
- [ ] Movement is smooth pixel motion, but collision still tests map tiles.
|
|
113
|
+
- [ ] X and Y resolved separately (wall-slide, no corner-stick).
|
|
114
|
+
- [ ] Facing is kept as state and drives BOTH animation and interaction target.
|
|
115
|
+
- [ ] Interaction reads the tile ONE step ahead in the facing direction.
|
|
116
|
+
- [ ] Camera follows and clamps to the map's PIXEL bounds; reset before HUD.
|
|
117
|
+
- [ ] `btnp` for the discrete action button; `btn` for continuous walking.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Twin-stick / arcade reference (many entities, spawn/despawn, juice)
|
|
2
|
+
|
|
3
|
+
Structure for an action arcade cart (shooter, bullet-hell, wave survival). The
|
|
4
|
+
defining problem is MANY short-lived entities at once (bullets, enemies,
|
|
5
|
+
particles) that spawn and despawn every frame. Get the entity-list lifecycle and
|
|
6
|
+
the despawn-while-iterating right, then layer on the "juice" that makes arcade
|
|
7
|
+
carts feel good. Shipped arcade carts are `rnd`-heavy for exactly that juice.
|
|
8
|
+
|
|
9
|
+
## The load-bearing rules
|
|
10
|
+
|
|
11
|
+
1. One flat list per KIND (`bullets`, `enemies`, `particles`), each entity a
|
|
12
|
+
table. `add` to spawn, `del`/`deli` to despawn.
|
|
13
|
+
2. Despawn safely. Deleting the CURRENT item inside `foreach` is safe. Deleting
|
|
14
|
+
inside an ASCENDING `for i=1,#list` is NOT (it errors once the list shrinks).
|
|
15
|
+
Use `foreach`, or iterate DESCENDING when you delete by index.
|
|
16
|
+
3. Off-screen or dead entities must be removed, or the list grows unbounded and
|
|
17
|
+
the cart slows/crashes. Cull every frame.
|
|
18
|
+
4. Movement is velocity-based and free (no grid, no gravity unless you want it).
|
|
19
|
+
`sin`/`cos` take TURNS (0..1) for aiming/spread.
|
|
20
|
+
|
|
21
|
+
## Entity lists + safe cull
|
|
22
|
+
|
|
23
|
+
```lua
|
|
24
|
+
bullets={}
|
|
25
|
+
enemies={}
|
|
26
|
+
parts={}
|
|
27
|
+
|
|
28
|
+
function fire(x,y,ang,spd)
|
|
29
|
+
-- ang in TURNS (0..1). sin is screen-inverted, so this aims correctly on screen.
|
|
30
|
+
add(bullets,{x=x,y=y,dx=cos(ang)*spd,dy=sin(ang)*spd,life=90})
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
function update_bullets()
|
|
34
|
+
foreach(bullets,function(b)
|
|
35
|
+
b.x+=b.dx b.y+=b.dy b.life-=1
|
|
36
|
+
-- cull off-screen OR expired; del of the CURRENT item under foreach is safe
|
|
37
|
+
if b.life<=0 or b.x<-4 or b.x>131 or b.y<-4 or b.y>131 then
|
|
38
|
+
del(bullets,b)
|
|
39
|
+
end
|
|
40
|
+
end)
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Spawning waves + aiming at the player
|
|
45
|
+
|
|
46
|
+
```lua
|
|
47
|
+
function spawn_enemy()
|
|
48
|
+
-- rnd picks a random screen edge; flr for an integer choice
|
|
49
|
+
add(enemies,{x=flr(rnd(128)),y=-8,hp=3})
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
function enemy_shoot(e)
|
|
53
|
+
-- atan2 gives the turn-angle from e to the player; feed it straight to fire()
|
|
54
|
+
fire(e.x,e.y, atan2(player.x-e.x, player.y-e.y), 1.5)
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Particles / juice (why arcade carts call rnd a lot)
|
|
59
|
+
|
|
60
|
+
```lua
|
|
61
|
+
function burst(x,y,col,n)
|
|
62
|
+
for i=1,n do
|
|
63
|
+
local a=rnd() -- random turn 0..1
|
|
64
|
+
local s=0.5+rnd(1.5) -- random speed
|
|
65
|
+
add(parts,{x=x,y=y,dx=cos(a)*s,dy=sin(a)*s,life=10+rnd(10),col=col})
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
function update_parts()
|
|
70
|
+
foreach(parts,function(p)
|
|
71
|
+
p.x+=p.dx p.y+=p.dy p.dx*=0.9 p.dy*=0.9 p.life-=1
|
|
72
|
+
if p.life<=0 then del(parts,p) end
|
|
73
|
+
end)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
function draw_parts()
|
|
77
|
+
foreach(parts,function(p) pset(p.x,p.y,p.col) end)
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Screen shake is a cheap, high-impact juice: offset the camera by a decaying
|
|
82
|
+
random amount, then RESET it.
|
|
83
|
+
|
|
84
|
+
```lua
|
|
85
|
+
shake=0
|
|
86
|
+
function add_shake(n) shake=max(shake,n) end
|
|
87
|
+
function apply_shake()
|
|
88
|
+
if shake>0 then
|
|
89
|
+
camera(rnd(shake)-shake/2, rnd(shake)-shake/2) -- random offset
|
|
90
|
+
shake*=0.8
|
|
91
|
+
if shake<0.5 then shake=0 end
|
|
92
|
+
else
|
|
93
|
+
camera() -- no shake: ensure camera is reset (draw state persists!)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Collision (cheap circle test for round-ish sprites)
|
|
99
|
+
|
|
100
|
+
```lua
|
|
101
|
+
-- squared-distance test avoids sqrt. r = sum of radii.
|
|
102
|
+
function hit(a,b,r)
|
|
103
|
+
local dx,dy=a.x-b.x,a.y-b.y
|
|
104
|
+
return dx*dx+dy*dy < r*r
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Draw + reset discipline
|
|
109
|
+
|
|
110
|
+
```lua
|
|
111
|
+
function _draw()
|
|
112
|
+
cls()
|
|
113
|
+
apply_shake()
|
|
114
|
+
map() ; foreach(enemies,draw_enemy) ; foreach(bullets,draw_bullet) ; draw_parts()
|
|
115
|
+
camera() -- RESET before HUD (shake/camera persist otherwise)
|
|
116
|
+
print("score "..score,2,2,7)
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Genre pitfalls checklist
|
|
121
|
+
|
|
122
|
+
- [ ] Each entity KIND is its own list; entities are tables.
|
|
123
|
+
- [ ] Despawn uses `foreach`+`del` (safe) or a descending index loop, never an
|
|
124
|
+
ascending `for i=1,#list` with in-loop `del`.
|
|
125
|
+
- [ ] Off-screen/dead entities are culled EVERY frame (no unbounded growth).
|
|
126
|
+
- [ ] Angles are TURNS (0..1); `atan2`/`sin`/`cos` used consistently.
|
|
127
|
+
- [ ] `rnd(x)` wrapped with `flr` when an integer is needed.
|
|
128
|
+
- [ ] `camera()` reset after any shake, before the HUD.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: picopilot-debug
|
|
3
|
+
description: Close the run-test loop for a PICO-8 cart with picopilot. Run the cart headless and read printh output, use the static verify gate honestly (well-formed is not proven-to-run), and understand the structured pico8-not-found / shrinko-not-found boundaries. Use when a cart misbehaves or you need to confirm it boots.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# picopilot debug
|
|
7
|
+
|
|
8
|
+
Static checks tell you a cart is well-formed; only running it tells you it works.
|
|
9
|
+
picopilot gives you both a static gate and a headless run so you can drive a
|
|
10
|
+
closed loop. (See `picopilot-overview` for the `#include` discipline and
|
|
11
|
+
`picopilot-code` for the token loop.)
|
|
12
|
+
|
|
13
|
+
## Static first: `picopilot verify`
|
|
14
|
+
|
|
15
|
+
`picopilot verify` runs tokens + integrity and returns ONE structured pass/fail
|
|
16
|
+
envelope. It is STATIC and NEVER runs the cart, so:
|
|
17
|
+
|
|
18
|
+
- A green verify means "well-formed", NOT "it runs". The passing result points
|
|
19
|
+
you at `picopilot run` to confirm the cart boots. Do not stop at green.
|
|
20
|
+
- A fail lists WHICH check flunked (over budget, or a malformed cart), so you fix
|
|
21
|
+
the named thing.
|
|
22
|
+
- With shrinko absent, verify is `gate-incapable` (a distinct nonzero outcome,
|
|
23
|
+
NEVER green): it cannot check token bloat, so it refuses to report pass.
|
|
24
|
+
|
|
25
|
+
## Then run it: `picopilot run`
|
|
26
|
+
|
|
27
|
+
`picopilot run <cart>` launches PICO-8 headless (`pico8 -x`), streams stdout,
|
|
28
|
+
ENDS the run the moment the cart prints a done-sentinel, and collects one
|
|
29
|
+
structured result: the screenshot PNG paths, the captured `printh` stdout, and
|
|
30
|
+
the exit reason (`sentinel` / `timeout` / `exit`). Read the screenshots to SEE
|
|
31
|
+
the running game; read printh for text traces. On a backstop `timeout` it CTAs
|
|
32
|
+
you to `verify`/`tokens` to catch a code fault.
|
|
33
|
+
|
|
34
|
+
`run` requires PICO-8 (a licensed, paid binary with no pip/npm path). If PICO-8
|
|
35
|
+
is absent, `run` returns a structured `pico8-not-found` result with a remedy
|
|
36
|
+
(`set PICO8_PATH or install PICO-8`) and a nonzero exit, not a crash or a hang.
|
|
37
|
+
Use the static `verify` loop while PICO-8 is unavailable; it never needs it.
|
|
38
|
+
|
|
39
|
+
### NEVER run `pico8 --help` / `--version` / bare `pico8` (they BLOCK your tool call)
|
|
40
|
+
|
|
41
|
+
PICO-8 is a GUI app with NO headless diagnostic mode. `pico8 --help`,
|
|
42
|
+
`pico8 --version`, and bare `pico8` do NOT print text and exit like a normal
|
|
43
|
+
CLI, they LAUNCH THE INTERACTIVE APP and hang forever (verified: `timeout 5
|
|
44
|
+
pico8 --help` exits 124 with zero output). Running one from an agent/automation
|
|
45
|
+
context STALLS your own tool call indefinitely (it has already bitten this
|
|
46
|
+
project's agents more than once, hanging a whole build). Do NOT invoke `pico8`
|
|
47
|
+
to "see its flags" or check it works. The only safe, non-blocking calls are the
|
|
48
|
+
automation ones, ALWAYS with `</dev/null` + a `timeout` backstop:
|
|
49
|
+
`pico8 -x <cart>`, `pico8 -run <cart>`, `pico8 <cart> -export foo.p8.png`. And
|
|
50
|
+
prefer to let picopilot (`run`, `audio render`, etc.) shell out for you rather
|
|
51
|
+
than calling `pico8` by hand. To learn a CLI flag, read the PICO-8 manual, never
|
|
52
|
+
ask the binary.
|
|
53
|
+
|
|
54
|
+
### The cart must COOPERATE (the recipe `run` orchestrates)
|
|
55
|
+
|
|
56
|
+
There is no "screenshot an arbitrary running cart" hook and a cart CANNOT quit
|
|
57
|
+
the PICO-8 app itself (`extcmd("shutdown")` works only in EXPORTED binaries).
|
|
58
|
+
So the cart takes its own screenshots and signals when it is done, and `run`
|
|
59
|
+
does the watch + kill + collect. Add these debug lines to the cart, run, then
|
|
60
|
+
remove them:
|
|
61
|
+
|
|
62
|
+
```lua
|
|
63
|
+
-- screenshot on a frame timer (the verb is "screen", NOT "screenshot")
|
|
64
|
+
if t==20 then extcmd("set_filename","frame_0") extcmd("screen") end
|
|
65
|
+
if t==40 then extcmd("set_filename","frame_1") extcmd("screen") end
|
|
66
|
+
-- signal done so run kills PICO-8 promptly (it can't self-quit)
|
|
67
|
+
if t==45 then printh("__PICOPILOT_DONE__") end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Load-bearing details (each an easy wrong guess, all tested):
|
|
71
|
+
|
|
72
|
+
- The screenshot verb is **`extcmd("screen")`**. `extcmd("screenshot")` ERRORS
|
|
73
|
+
("unknown extcmd"). `extcmd("set_filename","frame_0")` before it names the next
|
|
74
|
+
shot deterministically (so you can read `frame_0.png`, `frame_1.png`, ... in
|
|
75
|
+
order and PERCEIVE MOTION by comparing frames).
|
|
76
|
+
- The done-sentinel is the default `__PICOPILOT_DONE__` on its OWN line via
|
|
77
|
+
`printh`. `run` kills PICO-8 the instant it matches, so the run lasts exactly
|
|
78
|
+
as long as the cart's logic needs, no blind wait. `--sentinel` changes it.
|
|
79
|
+
- A cart that never prints the sentinel is killed by the backstop
|
|
80
|
+
(`--backstop-ms`, default 15000) and reported as `exitReason: timeout`.
|
|
81
|
+
- `-x` IS genuinely headless (runs with no display); the external kill is needed
|
|
82
|
+
only because a cart cannot self-quit, NOT because of a window.
|
|
83
|
+
- Screenshots go to a run-controlled dir (`--shot-dir`, default an isolated temp
|
|
84
|
+
dir), never `~/Desktop`.
|
|
85
|
+
|
|
86
|
+
To SEE the game actually being PLAYED (drive input + capture gameplay), use
|
|
87
|
+
`picopilot playtest`, next. (`run` boots the cart and captures what it does on
|
|
88
|
+
its own; `playtest` drives it.) There is also a legacy `run --input` that passes
|
|
89
|
+
a one-shot `-p`/`stat(6)` string, but it only works if the cart hand-decodes
|
|
90
|
+
`stat(6)`; for driving an arbitrary game, `playtest` is the tool, not `run
|
|
91
|
+
--input`.
|
|
92
|
+
|
|
93
|
+
## Actually PLAY it: `picopilot playtest`
|
|
94
|
+
|
|
95
|
+
`picopilot playtest` drives ANY normally-written cart (one that reads real
|
|
96
|
+
`btn`/`btnp`) with NO change to it: on a THROWAWAY copy it redefines `btn`/`btnp`
|
|
97
|
+
to read a harness-piped held-buttons byte (reconstructing `btnp` edges) and OWNS the frame
|
|
98
|
+
loop, so it advances an exact number of frames, PAUSES (freezing all state,
|
|
99
|
+
including logic in `_draw`) for a clean screenshot, and captures gameplay (not
|
|
100
|
+
the title). Your `main.p8` is never mutated. `playtest` requires PICO-8 and
|
|
101
|
+
returns the same structured `pico8-not-found` boundary as `run` when it is
|
|
102
|
+
absent. (Mechanism: ADR-0011.)
|
|
103
|
+
|
|
104
|
+
There are three ways to drive a cart; pick by how much continuity you need.
|
|
105
|
+
|
|
106
|
+
### One-shot: `picopilot playtest run <cart>`
|
|
107
|
+
|
|
108
|
+
The whole scripted input is known up front, so the cart replays it to completion
|
|
109
|
+
and exits, returning ONE envelope (screenshot paths + captured printh + the
|
|
110
|
+
steps run). Great for a scripted playtest or benchmark judging.
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
picopilot playtest run main.p8 # generic driver (press-to-start + gentle presses)
|
|
114
|
+
picopilot playtest run game.p8 --input "z" # just press O (a bare button = a press at frame 0)
|
|
115
|
+
picopilot playtest run game.p8 --input "3:o, 18-22:o, 20:right" # a timeline: press/hold at specific frames
|
|
116
|
+
picopilot playtest run game.p8 --seed 1 --input "o" # opt-in srand for deterministic replay
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`--input` accepts EITHER a bare list of buttons (`"z"`, `"o right"`, pressed at
|
|
120
|
+
frame 0, the same "just press these" form as the session `input` verb) OR a
|
|
121
|
+
timeline of `frame:button` (a single press, one clean `btnp` edge) /
|
|
122
|
+
`from-to:button` (a hold). A button is a NAME (`o x left right up down`, aliases
|
|
123
|
+
`l r u d`, PICO-8 keys `z`/`x`) or a bit `0..5`. Omit `--input` for the generic
|
|
124
|
+
one-button/runner/flappy driver. `--seed n` injects `srand(n)` at cart start
|
|
125
|
+
(never silent) so a random cart replays identically. Screenshots go to
|
|
126
|
+
`--shot-dir` (default an isolated temp dir, NEVER `~/Desktop`).
|
|
127
|
+
|
|
128
|
+
### (A) Resumable LIVE session: `start` -> `step`/`input`/`shot` -> `stop`
|
|
129
|
+
|
|
130
|
+
When you want to PLAY across turns (look at the last frame, decide, inject input,
|
|
131
|
+
step, look again), keep the game ALIVE and PAUSED between your turns. `start`
|
|
132
|
+
launches a persistent driven PICO-8 and returns a SESSION ID; every other verb
|
|
133
|
+
addresses that id in a SEPARATE invocation, and the game stays alive + paused in
|
|
134
|
+
between. Stepping is deterministic via a stdout ACK handshake (the host waits for
|
|
135
|
+
the cart's ack before returning), so you always act on a settled, known frame, no
|
|
136
|
+
wall-clock guessing.
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
ID=$(picopilot playtest start main.p8 --format json | jq -r .id) # launch, get the id
|
|
140
|
+
picopilot playtest input $ID "right o" # hold buttons for the next steps (names: left/right/up/down/o/x)
|
|
141
|
+
picopilot playtest step $ID --frames 30 # advance EXACTLY 30 frames, then pause
|
|
142
|
+
picopilot playtest shot $ID # screenshot the current frozen frame -> a PNG path
|
|
143
|
+
picopilot playtest status $ID # is it alive? how many frames in?
|
|
144
|
+
picopilot playtest stop $ID # tear it down (quit PICO-8, reap the dir)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Lifecycle notes (each verified/handled, not a hope):
|
|
148
|
+
|
|
149
|
+
- Each verb WRITES a fixed-size command block and WAITS for the cart's ACK, so a
|
|
150
|
+
`step N` advances exactly N frames and a `shot` between steps captures the
|
|
151
|
+
frozen intended frame. Between verbs the game is frozen (stable framebuffer).
|
|
152
|
+
- ONE session per id; MULTIPLE concurrent sessions are allowed (distinct ids get
|
|
153
|
+
distinct daemons/dirs). Omit `--id` for a fresh random id; pass `--id name` for
|
|
154
|
+
a stable one.
|
|
155
|
+
- ORPHAN reaping: a session you never `stop` self-reaps after an idle window
|
|
156
|
+
(`--idle-timeout-ms`, default 10 min), so a walked-away session cannot leak a
|
|
157
|
+
live PICO-8 forever. `stop` is idempotent (safe on an already-dead session).
|
|
158
|
+
- PICO-8 dying mid-session surfaces as a STRUCTURED error on the next verb
|
|
159
|
+
(`playtest-session-process-dead`), never a hang; a command with no ACK by the
|
|
160
|
+
deadline is `playtest-session-ack-timeout`. `start` with PICO-8 absent is
|
|
161
|
+
`pico8-not-found`, same as `run`.
|
|
162
|
+
- Screenshots + the throwaway driven cart live under a controlled temp session
|
|
163
|
+
dir, never `~/Desktop` or your carts root.
|
|
164
|
+
|
|
165
|
+
### (C) Stateless REPLAY (the free fallback, no daemon)
|
|
166
|
+
|
|
167
|
+
You do NOT always need a live session. Because the one-shot is deterministic
|
|
168
|
+
under `--seed`, you can "resume" by RE-INVOKING `playtest run` with an
|
|
169
|
+
ACCUMULATING script each turn: re-run from frame 0 with the same seed plus the
|
|
170
|
+
new presses appended. It is cheap, reproducible, and daemon-free.
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
picopilot playtest run game.p8 --seed 1 --input "3:4" # turn 1
|
|
174
|
+
picopilot playtest run game.p8 --seed 1 --input "3:4, 40:1" # turn 2 (replays turn 1, then the new press)
|
|
175
|
+
picopilot playtest run game.p8 --seed 1 --input "3:4, 40:1, 55:5" # turn 3 ...
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Prefer (C) for short, replay-safe games (it needs no live process). Use the live
|
|
179
|
+
session (A) for true continuity: long games, or games that reseed from a
|
|
180
|
+
NON-deterministic source (wall-clock/entropy) mid-run and so cannot be replayed
|
|
181
|
+
from frame 0. Both drive the SAME cart-side transform; only the host lifecycle
|
|
182
|
+
differs.
|
|
183
|
+
|
|
184
|
+
## The two structured boundaries
|
|
185
|
+
|
|
186
|
+
Both dependency boundaries are soft and well-signposted, so read the result, do
|
|
187
|
+
not guess:
|
|
188
|
+
|
|
189
|
+
- `shrinko-not-found` (remedy `uv pip install shrinko`): gates the token/lint
|
|
190
|
+
commands and therefore `verify`.
|
|
191
|
+
- `pico8-not-found` (remedy `set PICO8_PATH or install PICO-8`): gates `run`,
|
|
192
|
+
`playtest`, `audio render`, and binary/PNG exports.
|
|
193
|
+
|
|
194
|
+
Each is a structured `{ ok: false, reason, remedy }` envelope with a nonzero
|
|
195
|
+
exit, telling you exactly which capability is gated and how to enable it.
|