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.
Files changed (334) hide show
  1. package/LICENSE +661 -0
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +4 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/cli.d.ts +15 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +59 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/audio.d.ts +17 -0
  11. package/dist/commands/audio.d.ts.map +1 -0
  12. package/dist/commands/audio.js +315 -0
  13. package/dist/commands/audio.js.map +1 -0
  14. package/dist/commands/export.d.ts +36 -0
  15. package/dist/commands/export.d.ts.map +1 -0
  16. package/dist/commands/export.js +257 -0
  17. package/dist/commands/export.js.map +1 -0
  18. package/dist/commands/gfx.d.ts +28 -0
  19. package/dist/commands/gfx.d.ts.map +1 -0
  20. package/dist/commands/gfx.js +521 -0
  21. package/dist/commands/gfx.js.map +1 -0
  22. package/dist/commands/init.d.ts +34 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +151 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/lint.d.ts +20 -0
  27. package/dist/commands/lint.d.ts.map +1 -0
  28. package/dist/commands/lint.js +90 -0
  29. package/dist/commands/lint.js.map +1 -0
  30. package/dist/commands/minify.d.ts +20 -0
  31. package/dist/commands/minify.d.ts.map +1 -0
  32. package/dist/commands/minify.js +129 -0
  33. package/dist/commands/minify.js.map +1 -0
  34. package/dist/commands/music.d.ts +13 -0
  35. package/dist/commands/music.d.ts.map +1 -0
  36. package/dist/commands/music.js +241 -0
  37. package/dist/commands/music.js.map +1 -0
  38. package/dist/commands/playtest.d.ts +29 -0
  39. package/dist/commands/playtest.d.ts.map +1 -0
  40. package/dist/commands/playtest.js +631 -0
  41. package/dist/commands/playtest.js.map +1 -0
  42. package/dist/commands/run.d.ts +30 -0
  43. package/dist/commands/run.d.ts.map +1 -0
  44. package/dist/commands/run.js +202 -0
  45. package/dist/commands/run.js.map +1 -0
  46. package/dist/commands/serve.d.ts +50 -0
  47. package/dist/commands/serve.d.ts.map +1 -0
  48. package/dist/commands/serve.js +213 -0
  49. package/dist/commands/serve.js.map +1 -0
  50. package/dist/commands/sfx.d.ts +13 -0
  51. package/dist/commands/sfx.d.ts.map +1 -0
  52. package/dist/commands/sfx.js +254 -0
  53. package/dist/commands/sfx.js.map +1 -0
  54. package/dist/commands/tokens.d.ts +36 -0
  55. package/dist/commands/tokens.d.ts.map +1 -0
  56. package/dist/commands/tokens.js +129 -0
  57. package/dist/commands/tokens.js.map +1 -0
  58. package/dist/commands/verify.d.ts +57 -0
  59. package/dist/commands/verify.d.ts.map +1 -0
  60. package/dist/commands/verify.js +213 -0
  61. package/dist/commands/verify.js.map +1 -0
  62. package/dist/commands/version.d.ts +12 -0
  63. package/dist/commands/version.d.ts.map +1 -0
  64. package/dist/commands/version.js +24 -0
  65. package/dist/commands/version.js.map +1 -0
  66. package/dist/engine/audio/index.d.ts +14 -0
  67. package/dist/engine/audio/index.d.ts.map +1 -0
  68. package/dist/engine/audio/index.js +14 -0
  69. package/dist/engine/audio/index.js.map +1 -0
  70. package/dist/engine/audio/music.d.ts +116 -0
  71. package/dist/engine/audio/music.d.ts.map +1 -0
  72. package/dist/engine/audio/music.js +138 -0
  73. package/dist/engine/audio/music.js.map +1 -0
  74. package/dist/engine/audio/section.d.ts +34 -0
  75. package/dist/engine/audio/section.d.ts.map +1 -0
  76. package/dist/engine/audio/section.js +67 -0
  77. package/dist/engine/audio/section.js.map +1 -0
  78. package/dist/engine/audio/sfx.d.ts +141 -0
  79. package/dist/engine/audio/sfx.d.ts.map +1 -0
  80. package/dist/engine/audio/sfx.js +506 -0
  81. package/dist/engine/audio/sfx.js.map +1 -0
  82. package/dist/engine/cart/cart.d.ts +75 -0
  83. package/dist/engine/cart/cart.d.ts.map +1 -0
  84. package/dist/engine/cart/cart.js +153 -0
  85. package/dist/engine/cart/cart.js.map +1 -0
  86. package/dist/engine/cart/errors.d.ts +29 -0
  87. package/dist/engine/cart/errors.d.ts.map +1 -0
  88. package/dist/engine/cart/errors.js +26 -0
  89. package/dist/engine/cart/errors.js.map +1 -0
  90. package/dist/engine/cart/gfx.d.ts +79 -0
  91. package/dist/engine/cart/gfx.d.ts.map +1 -0
  92. package/dist/engine/cart/gfx.js +171 -0
  93. package/dist/engine/cart/gfx.js.map +1 -0
  94. package/dist/engine/cart/index.d.ts +15 -0
  95. package/dist/engine/cart/index.d.ts.map +1 -0
  96. package/dist/engine/cart/index.js +15 -0
  97. package/dist/engine/cart/index.js.map +1 -0
  98. package/dist/engine/cart/map.d.ts +46 -0
  99. package/dist/engine/cart/map.d.ts.map +1 -0
  100. package/dist/engine/cart/map.js +118 -0
  101. package/dist/engine/cart/map.js.map +1 -0
  102. package/dist/engine/config/config.d.ts +74 -0
  103. package/dist/engine/config/config.d.ts.map +1 -0
  104. package/dist/engine/config/config.js +103 -0
  105. package/dist/engine/config/config.js.map +1 -0
  106. package/dist/engine/config/index.d.ts +7 -0
  107. package/dist/engine/config/index.d.ts.map +1 -0
  108. package/dist/engine/config/index.js +7 -0
  109. package/dist/engine/config/index.js.map +1 -0
  110. package/dist/engine/gfx/grid.d.ts +51 -0
  111. package/dist/engine/gfx/grid.d.ts.map +1 -0
  112. package/dist/engine/gfx/grid.js +116 -0
  113. package/dist/engine/gfx/grid.js.map +1 -0
  114. package/dist/engine/gfx/index.d.ts +17 -0
  115. package/dist/engine/gfx/index.d.ts.map +1 -0
  116. package/dist/engine/gfx/index.js +17 -0
  117. package/dist/engine/gfx/index.js.map +1 -0
  118. package/dist/engine/gfx/label.d.ts +34 -0
  119. package/dist/engine/gfx/label.d.ts.map +1 -0
  120. package/dist/engine/gfx/label.js +190 -0
  121. package/dist/engine/gfx/label.js.map +1 -0
  122. package/dist/engine/gfx/overlap.d.ts +42 -0
  123. package/dist/engine/gfx/overlap.d.ts.map +1 -0
  124. package/dist/engine/gfx/overlap.js +57 -0
  125. package/dist/engine/gfx/overlap.js.map +1 -0
  126. package/dist/engine/gfx/palette.d.ts +29 -0
  127. package/dist/engine/gfx/palette.d.ts.map +1 -0
  128. package/dist/engine/gfx/palette.js +46 -0
  129. package/dist/engine/gfx/palette.js.map +1 -0
  130. package/dist/engine/gfx/png.d.ts +31 -0
  131. package/dist/engine/gfx/png.d.ts.map +1 -0
  132. package/dist/engine/gfx/png.js +165 -0
  133. package/dist/engine/gfx/png.js.map +1 -0
  134. package/dist/engine/gfx/render.d.ts +42 -0
  135. package/dist/engine/gfx/render.d.ts.map +1 -0
  136. package/dist/engine/gfx/render.js +72 -0
  137. package/dist/engine/gfx/render.js.map +1 -0
  138. package/dist/engine/index.d.ts +22 -0
  139. package/dist/engine/index.d.ts.map +1 -0
  140. package/dist/engine/index.js +22 -0
  141. package/dist/engine/index.js.map +1 -0
  142. package/dist/engine/pico8/adapter.d.ts +267 -0
  143. package/dist/engine/pico8/adapter.d.ts.map +1 -0
  144. package/dist/engine/pico8/adapter.js +37 -0
  145. package/dist/engine/pico8/adapter.js.map +1 -0
  146. package/dist/engine/pico8/drive.d.ts +308 -0
  147. package/dist/engine/pico8/drive.d.ts.map +1 -0
  148. package/dist/engine/pico8/drive.js +581 -0
  149. package/dist/engine/pico8/drive.js.map +1 -0
  150. package/dist/engine/pico8/harness.d.ts +146 -0
  151. package/dist/engine/pico8/harness.d.ts.map +1 -0
  152. package/dist/engine/pico8/harness.js +251 -0
  153. package/dist/engine/pico8/harness.js.map +1 -0
  154. package/dist/engine/pico8/index.d.ts +21 -0
  155. package/dist/engine/pico8/index.d.ts.map +1 -0
  156. package/dist/engine/pico8/index.js +21 -0
  157. package/dist/engine/pico8/index.js.map +1 -0
  158. package/dist/engine/pico8/sentinel.d.ts +63 -0
  159. package/dist/engine/pico8/sentinel.d.ts.map +1 -0
  160. package/dist/engine/pico8/sentinel.js +86 -0
  161. package/dist/engine/pico8/sentinel.js.map +1 -0
  162. package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
  163. package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
  164. package/dist/engine/pico8/session-daemon-entry.js +11 -0
  165. package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
  166. package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
  167. package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
  168. package/dist/engine/pico8/session-daemon-main.js +71 -0
  169. package/dist/engine/pico8/session-daemon-main.js.map +1 -0
  170. package/dist/engine/pico8/session-daemon.d.ts +68 -0
  171. package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
  172. package/dist/engine/pico8/session-daemon.js +264 -0
  173. package/dist/engine/pico8/session-daemon.js.map +1 -0
  174. package/dist/engine/pico8/session.d.ts +140 -0
  175. package/dist/engine/pico8/session.d.ts.map +1 -0
  176. package/dist/engine/pico8/session.js +250 -0
  177. package/dist/engine/pico8/session.js.map +1 -0
  178. package/dist/engine/pico8/shell.d.ts +103 -0
  179. package/dist/engine/pico8/shell.d.ts.map +1 -0
  180. package/dist/engine/pico8/shell.js +324 -0
  181. package/dist/engine/pico8/shell.js.map +1 -0
  182. package/dist/engine/pico8/supervisor.d.ts +137 -0
  183. package/dist/engine/pico8/supervisor.d.ts.map +1 -0
  184. package/dist/engine/pico8/supervisor.js +140 -0
  185. package/dist/engine/pico8/supervisor.js.map +1 -0
  186. package/dist/engine/scaffold/index.d.ts +8 -0
  187. package/dist/engine/scaffold/index.d.ts.map +1 -0
  188. package/dist/engine/scaffold/index.js +8 -0
  189. package/dist/engine/scaffold/index.js.map +1 -0
  190. package/dist/engine/scaffold/scaffold.d.ts +49 -0
  191. package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
  192. package/dist/engine/scaffold/scaffold.js +217 -0
  193. package/dist/engine/scaffold/scaffold.js.map +1 -0
  194. package/dist/engine/shrinko/adapter.d.ts +167 -0
  195. package/dist/engine/shrinko/adapter.d.ts.map +1 -0
  196. package/dist/engine/shrinko/adapter.js +107 -0
  197. package/dist/engine/shrinko/adapter.js.map +1 -0
  198. package/dist/engine/shrinko/index.d.ts +13 -0
  199. package/dist/engine/shrinko/index.d.ts.map +1 -0
  200. package/dist/engine/shrinko/index.js +13 -0
  201. package/dist/engine/shrinko/index.js.map +1 -0
  202. package/dist/engine/shrinko/shell.d.ts +69 -0
  203. package/dist/engine/shrinko/shell.d.ts.map +1 -0
  204. package/dist/engine/shrinko/shell.js +156 -0
  205. package/dist/engine/shrinko/shell.js.map +1 -0
  206. package/dist/engine/skills/index.d.ts +8 -0
  207. package/dist/engine/skills/index.d.ts.map +1 -0
  208. package/dist/engine/skills/index.js +8 -0
  209. package/dist/engine/skills/index.js.map +1 -0
  210. package/dist/engine/skills/skills.d.ts +104 -0
  211. package/dist/engine/skills/skills.d.ts.map +1 -0
  212. package/dist/engine/skills/skills.js +149 -0
  213. package/dist/engine/skills/skills.js.map +1 -0
  214. package/dist/index.d.ts +10 -0
  215. package/dist/index.d.ts.map +1 -0
  216. package/dist/index.js +10 -0
  217. package/dist/index.js.map +1 -0
  218. package/dist/version.d.ts +2 -0
  219. package/dist/version.d.ts.map +1 -0
  220. package/dist/version.js +19 -0
  221. package/dist/version.js.map +1 -0
  222. package/package.json +48 -2
  223. package/src/bin.ts +4 -0
  224. package/src/cli.ts +61 -0
  225. package/src/commands/audio.test.ts +280 -0
  226. package/src/commands/audio.ts +410 -0
  227. package/src/commands/export.test.ts +310 -0
  228. package/src/commands/export.ts +316 -0
  229. package/src/commands/gfx-io.test.ts +178 -0
  230. package/src/commands/gfx.test.ts +470 -0
  231. package/src/commands/gfx.ts +645 -0
  232. package/src/commands/init.test.ts +460 -0
  233. package/src/commands/init.ts +193 -0
  234. package/src/commands/lint.test.ts +137 -0
  235. package/src/commands/lint.ts +117 -0
  236. package/src/commands/minify.test.ts +166 -0
  237. package/src/commands/minify.ts +156 -0
  238. package/src/commands/music.test.ts +272 -0
  239. package/src/commands/music.ts +288 -0
  240. package/src/commands/playtest.test.ts +317 -0
  241. package/src/commands/playtest.ts +773 -0
  242. package/src/commands/run.test.ts +238 -0
  243. package/src/commands/run.ts +259 -0
  244. package/src/commands/serve.test.ts +218 -0
  245. package/src/commands/serve.ts +272 -0
  246. package/src/commands/sfx.test.ts +349 -0
  247. package/src/commands/sfx.ts +300 -0
  248. package/src/commands/tokens.test.ts +212 -0
  249. package/src/commands/tokens.ts +168 -0
  250. package/src/commands/verify.test.ts +238 -0
  251. package/src/commands/verify.ts +256 -0
  252. package/src/commands/version.test.ts +70 -0
  253. package/src/commands/version.ts +24 -0
  254. package/src/engine/audio/index.ts +47 -0
  255. package/src/engine/audio/music.test.ts +188 -0
  256. package/src/engine/audio/music.ts +218 -0
  257. package/src/engine/audio/section.test.ts +124 -0
  258. package/src/engine/audio/section.ts +79 -0
  259. package/src/engine/audio/sfx.test.ts +410 -0
  260. package/src/engine/audio/sfx.ts +694 -0
  261. package/src/engine/cart/cart.test.ts +304 -0
  262. package/src/engine/cart/cart.ts +211 -0
  263. package/src/engine/cart/errors.ts +39 -0
  264. package/src/engine/cart/fixtures/crlf.p8 +6 -0
  265. package/src/engine/cart/fixtures/full.p8 +174 -0
  266. package/src/engine/cart/fixtures/header-only.p8 +2 -0
  267. package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
  268. package/src/engine/cart/fixtures/minimal.p8 +10 -0
  269. package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
  270. package/src/engine/cart/gfx.ts +195 -0
  271. package/src/engine/cart/index.ts +27 -0
  272. package/src/engine/cart/map.ts +134 -0
  273. package/src/engine/config/config.ts +115 -0
  274. package/src/engine/config/index.ts +12 -0
  275. package/src/engine/gfx/grid.test.ts +128 -0
  276. package/src/engine/gfx/grid.ts +136 -0
  277. package/src/engine/gfx/index.ts +34 -0
  278. package/src/engine/gfx/label.test.ts +105 -0
  279. package/src/engine/gfx/label.ts +229 -0
  280. package/src/engine/gfx/overlap.test.ts +71 -0
  281. package/src/engine/gfx/overlap.ts +66 -0
  282. package/src/engine/gfx/palette.test.ts +35 -0
  283. package/src/engine/gfx/palette.ts +54 -0
  284. package/src/engine/gfx/png.test.ts +111 -0
  285. package/src/engine/gfx/png.ts +196 -0
  286. package/src/engine/gfx/render.test.ts +180 -0
  287. package/src/engine/gfx/render.ts +106 -0
  288. package/src/engine/index.ts +21 -0
  289. package/src/engine/pico8/adapter.ts +303 -0
  290. package/src/engine/pico8/drive.test.ts +415 -0
  291. package/src/engine/pico8/drive.ts +729 -0
  292. package/src/engine/pico8/harness.test.ts +181 -0
  293. package/src/engine/pico8/harness.ts +354 -0
  294. package/src/engine/pico8/index.ts +20 -0
  295. package/src/engine/pico8/sentinel.test.ts +80 -0
  296. package/src/engine/pico8/sentinel.ts +93 -0
  297. package/src/engine/pico8/session-daemon-entry.ts +12 -0
  298. package/src/engine/pico8/session-daemon-main.ts +98 -0
  299. package/src/engine/pico8/session-daemon.test.ts +212 -0
  300. package/src/engine/pico8/session-daemon.ts +306 -0
  301. package/src/engine/pico8/session.test.ts +239 -0
  302. package/src/engine/pico8/session.ts +321 -0
  303. package/src/engine/pico8/shell.test.ts +583 -0
  304. package/src/engine/pico8/shell.ts +412 -0
  305. package/src/engine/pico8/supervisor.test.ts +105 -0
  306. package/src/engine/pico8/supervisor.ts +197 -0
  307. package/src/engine/scaffold/index.ts +15 -0
  308. package/src/engine/scaffold/scaffold.ts +236 -0
  309. package/src/engine/shrinko/adapter.ts +241 -0
  310. package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
  311. package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
  312. package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
  313. package/src/engine/shrinko/index.ts +33 -0
  314. package/src/engine/shrinko/shell.ts +224 -0
  315. package/src/engine/shrinko/shrinko.test.ts +182 -0
  316. package/src/engine/skills/index.ts +14 -0
  317. package/src/engine/skills/skills.test.ts +376 -0
  318. package/src/engine/skills/skills.ts +205 -0
  319. package/src/index.ts +13 -0
  320. package/src/skills/game-design-reference/SKILL.md +70 -0
  321. package/src/skills/game-jam/SKILL.md +45 -0
  322. package/src/skills/picopilot-art/SKILL.md +62 -0
  323. package/src/skills/picopilot-audio/SKILL.md +194 -0
  324. package/src/skills/picopilot-code/SKILL.md +97 -0
  325. package/src/skills/picopilot-code/reference/README.md +42 -0
  326. package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
  327. package/src/skills/picopilot-code/reference/platformer.md +119 -0
  328. package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
  329. package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
  330. package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
  331. package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
  332. package/src/skills/picopilot-debug/SKILL.md +195 -0
  333. package/src/skills/picopilot-overview/SKILL.md +52 -0
  334. 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.