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,115 @@
1
+ import {z} from 'incur';
2
+
3
+ /**
4
+ * The per-cart `picopilot.json` config, US #10.
5
+ *
6
+ * picopilot's config is incur's built-in `config: { files: ['picopilot.json'] }`
7
+ * mechanism (declared in `createCli`): a JSON file whose values become option
8
+ * DEFAULTS for the matching command, with precedence `argv > config > zod
9
+ * defaults`. incur keys config by command path, a command's options live under
10
+ * `commands.<group>.commands.<name>.options.<key>`, so a policy an agent sets
11
+ * once (like `allowMapOverlap`) is stored where the command that reads it looks.
12
+ *
13
+ * This module is the single source of truth for the config SHAPE: `init`
14
+ * scaffolds a file from {@link defaultConfigFile}, and the later `gfx set`
15
+ * command reads `allowMapOverlap` through incur's config layer (its own
16
+ * `--allow-map-overlap` option default). Keeping the shape here, rather than
17
+ * inlining a blob in `init`, means the scaffolded file and the reader cannot
18
+ * drift.
19
+ */
20
+
21
+ /**
22
+ * The `gfx set` options that `picopilot.json` may set as defaults.
23
+ *
24
+ * `allowMapOverlap` authorises `gfx set` to write sprites 128-255 even when the
25
+ * overlapping `__map__` rows hold real tiles (see ADR-0004). It is a rarely /
26
+ * once-set PROJECT setting: OFF by default (the smart-refuse is the safety), so
27
+ * a fresh cart never silently loses map data. The matching invocation flag is
28
+ * `--allow-map-overlap`; `argv > config > default` means the flag always wins.
29
+ */
30
+ export const GfxSetOptions = z.object({
31
+ allowMapOverlap: z
32
+ .boolean()
33
+ .default(false)
34
+ .describe(
35
+ 'Authorise `gfx set` to overwrite __map__ tiles that alias sprites 128-255 (ADR-0004). Off by default; the smart-refuse is the safety.',
36
+ ),
37
+ });
38
+
39
+ /**
40
+ * The full `picopilot.json` document shape, mirroring incur's config tree
41
+ * (`commands.<path>.options`). Only the keys picopilot actually reads are
42
+ * modelled; the schema is `.strict()`-free on purpose so a human/agent may add
43
+ * config for commands this version does not yet know about without a parse
44
+ * failure.
45
+ */
46
+ export const PicopilotConfig = z.object({
47
+ $schema: z.string().optional().describe('JSON Schema reference (optional).'),
48
+ commands: z
49
+ .object({
50
+ gfx: z
51
+ .object({
52
+ commands: z
53
+ .object({
54
+ set: z
55
+ .object({
56
+ options: GfxSetOptions.partial().optional(),
57
+ })
58
+ .optional(),
59
+ })
60
+ .optional(),
61
+ })
62
+ .optional(),
63
+ })
64
+ .optional()
65
+ .describe(
66
+ 'Per-command option defaults, keyed by command path (incur config layout).',
67
+ ),
68
+ });
69
+
70
+ export type PicopilotConfig = z.infer<typeof PicopilotConfig>;
71
+
72
+ /**
73
+ * The default `picopilot.json` written by `init`: valid, minimal, and
74
+ * self-documenting via `$schema`. It sets `allowMapOverlap: false` explicitly
75
+ * so the one policy knob an agent is likely to flip is visible in the file (an
76
+ * agent is FREE to read/write this to match a higher-level instruction, US #10)
77
+ * rather than hidden behind a zod default.
78
+ */
79
+ export const defaultConfig: PicopilotConfig = {
80
+ $schema: './picopilot.schema.json',
81
+ commands: {
82
+ gfx: {
83
+ commands: {
84
+ set: {
85
+ options: {
86
+ allowMapOverlap: false,
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ };
93
+
94
+ /** The default `picopilot.json` serialized as it is written to disk (trailing newline). */
95
+ export function defaultConfigFile(): string {
96
+ return `${JSON.stringify(defaultConfig, null, 2)}\n`;
97
+ }
98
+
99
+ /**
100
+ * Reads `allowMapOverlap` out of a parsed `picopilot.json` tree, tolerating a
101
+ * partially-shaped or absent config. Returns `false` when the key is missing or
102
+ * the file is malformed enough that the value cannot be found, the SAFE default
103
+ * (the smart-refuse still guards the genuine data-loss corner).
104
+ *
105
+ * `gfx set` normally gets this value through incur's config → option-default
106
+ * layer; this helper exists for callers that hold a raw config object (and to
107
+ * keep the "where allowMapOverlap lives" knowledge in one place).
108
+ */
109
+ export function readAllowMapOverlap(config: unknown): boolean {
110
+ const parsed = PicopilotConfig.safeParse(config);
111
+ if (!parsed.success) return false;
112
+ return (
113
+ parsed.data.commands?.gfx?.commands?.set?.options?.allowMapOverlap ?? false
114
+ );
115
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The per-cart `picopilot.json` config model: the schema `init` scaffolds from
3
+ * and the later `gfx set` reads `allowMapOverlap` through (via incur's config
4
+ * layer). See `config.ts` for why the shape lives here as one source of truth.
5
+ */
6
+ export {
7
+ defaultConfig,
8
+ defaultConfigFile,
9
+ GfxSetOptions,
10
+ PicopilotConfig,
11
+ readAllowMapOverlap,
12
+ } from './config.js';
@@ -0,0 +1,128 @@
1
+ import {describe, expect, it} from 'vitest';
2
+
3
+ import {GfxSheet} from '../cart/index.js';
4
+ import {
5
+ GfxGridError,
6
+ GRID_SIZE,
7
+ gridToNibbles,
8
+ nibblesToGrid,
9
+ TRANSPARENT_CHAR,
10
+ } from './grid.js';
11
+
12
+ /** An 8x8 nibble grid built from a generator, for exhaustive round-trip tests. */
13
+ function makeGrid(fn: (r: number, c: number) => number): number[][] {
14
+ const grid: number[][] = [];
15
+ for (let r = 0; r < GRID_SIZE; r++) {
16
+ const row: number[] = [];
17
+ for (let c = 0; c < GRID_SIZE; c++) row.push(fn(r, c));
18
+ grid.push(row);
19
+ }
20
+ return grid;
21
+ }
22
+
23
+ describe('gfx char-grid codec: encode', () => {
24
+ it('renders colour 0 as "." (transparent) and 1..15 as lowercase hex', () => {
25
+ // Row of 0..7 then 8..15 across two rows, rest zero.
26
+ const grid = makeGrid((r, c) => (r === 0 ? c : r === 1 ? c + 8 : 0));
27
+ const text = nibblesToGrid(grid);
28
+ const lines = text.split('\n');
29
+ expect(lines).toHaveLength(GRID_SIZE);
30
+ // Row 0: 0..7 → ".123457"? no — index 0 is ".", 1..7 are hex.
31
+ expect(lines[0]).toBe('.1234567');
32
+ // Row 1: 8..15 → hex 8,9,a..f.
33
+ expect(lines[1]).toBe('89abcdef');
34
+ // Empty rows are all dots.
35
+ expect(lines[2]).toBe(TRANSPARENT_CHAR.repeat(GRID_SIZE));
36
+ });
37
+
38
+ it('produces 8 lines of 8 chars, no trailing newline', () => {
39
+ const text = nibblesToGrid(makeGrid(() => 0));
40
+ expect(text.endsWith('\n')).toBe(false);
41
+ const lines = text.split('\n');
42
+ expect(lines).toHaveLength(GRID_SIZE);
43
+ for (const line of lines) expect(line).toHaveLength(GRID_SIZE);
44
+ });
45
+ });
46
+
47
+ describe('gfx char-grid codec: decode', () => {
48
+ it('decodes "." to 0 and hex digits (any case) to 1..15', () => {
49
+ const text = ['.1234567', '89ABCDEF', ...Array(6).fill('........')].join(
50
+ '\n',
51
+ );
52
+ const grid = gridToNibbles(text);
53
+ expect(grid[0]).toEqual([0, 1, 2, 3, 4, 5, 6, 7]);
54
+ // Upper-case hex decodes the same as lower-case.
55
+ expect(grid[1]).toEqual([8, 9, 10, 11, 12, 13, 14, 15]);
56
+ expect(grid[2]).toEqual([0, 0, 0, 0, 0, 0, 0, 0]);
57
+ });
58
+
59
+ it('tolerates a single trailing newline (a pasted grid)', () => {
60
+ const text = `${Array(GRID_SIZE).fill('........').join('\n')}\n`;
61
+ expect(() => gridToNibbles(text)).not.toThrow();
62
+ });
63
+
64
+ it('accepts CRLF line endings', () => {
65
+ const text = Array(GRID_SIZE).fill('........').join('\r\n');
66
+ const grid = gridToNibbles(text);
67
+ expect(grid).toHaveLength(GRID_SIZE);
68
+ });
69
+
70
+ it('rejects a grid with the wrong number of rows', () => {
71
+ const tooFew = Array(GRID_SIZE - 1)
72
+ .fill('........')
73
+ .join('\n');
74
+ expect(() => gridToNibbles(tooFew)).toThrow(GfxGridError);
75
+ });
76
+
77
+ it('rejects a row with the wrong number of cells', () => {
78
+ const rows = Array(GRID_SIZE).fill('........');
79
+ rows[3] = '.......'; // 7 cells
80
+ expect(() => gridToNibbles(rows.join('\n'))).toThrow(GfxGridError);
81
+ });
82
+
83
+ it('rejects an invalid char', () => {
84
+ const rows = Array(GRID_SIZE).fill('........');
85
+ rows[0] = '.......X';
86
+ expect(() => gridToNibbles(rows.join('\n'))).toThrow(GfxGridError);
87
+ });
88
+ });
89
+
90
+ describe('gfx char-grid codec: round-trip is an IDENTITY (the acceptance guard)', () => {
91
+ it('nibbles → grid → nibbles is identity for a ramp covering every colour', () => {
92
+ // Cover all 16 indices densely across the 8x8 (index = (r*8+c) % 16).
93
+ const grid = makeGrid((r, c) => (r * GRID_SIZE + c) % 16);
94
+ const round = gridToNibbles(nibblesToGrid(grid));
95
+ expect(round).toEqual(grid);
96
+ });
97
+
98
+ it('grid → nibbles → grid is identity for a hand-written grid', () => {
99
+ const text = [
100
+ '.8888888',
101
+ '8.7.7.7.',
102
+ '88888888',
103
+ '.a.a.a.a',
104
+ 'ffffffff',
105
+ '........',
106
+ '0.1.2.3.', // wait: "0" is a valid nibble too (1..15 use hex, 0 uses ".")
107
+ 'c.d.e.f.',
108
+ ];
109
+ // Fix row 6: "0" is NOT how zero is written; zero is ".". Use non-zero hex.
110
+ text[6] = '1.2.3.4.';
111
+ const grid = gridToNibbles(text.join('\n'));
112
+ const round = nibblesToGrid(grid);
113
+ expect(round).toBe(text.join('\n'));
114
+ });
115
+
116
+ it('every colour value survives the round trip via a real GfxSheet sprite', () => {
117
+ // The codec must compose with the cart model: read a sprite off a GfxSheet,
118
+ // encode, decode, and the sheet write-back must reproduce the same pixels.
119
+ const sheet = GfxSheet.fromBody(undefined);
120
+ // Paint sprite 5 with a full-range pattern.
121
+ const painted = makeGrid((r, c) => (r * GRID_SIZE + c) % 16);
122
+ sheet.setSprite(5, painted);
123
+
124
+ const text = nibblesToGrid(sheet.getSprite(5));
125
+ const decoded = gridToNibbles(text);
126
+ expect(decoded).toEqual(painted);
127
+ });
128
+ });
@@ -0,0 +1,136 @@
1
+ import {SPRITE_SIZE} from '../cart/index.js';
2
+
3
+ /**
4
+ * The shrinko-free char-grid codec: converts a sprite's per-pixel colour
5
+ * nibbles (as {@link import('../cart/index.js').GfxSheet} reads/writes them)
6
+ * to and from a readable CHAR GRID, and back, as an IDENTITY.
7
+ *
8
+ * The char grid is the "SVG for pixels" (CONTEXT.md `char grid`): the EDIT
9
+ * surface an agent reads with `gfx show` and writes with `gfx set`. Each pixel
10
+ * is one character:
11
+ *
12
+ * - `.` = transparent = colour index 0 (PICO-8's index-0 doubles as the default
13
+ * transparent colour; the grid renders it as `.` so an agent SEES the empty
14
+ * pixels rather than a wall of `0`s).
15
+ * - `0`..`f` (lowercase hex) = colour indices 1..15 (`__gfx__` stores nibbles
16
+ * lowercase, so decode is case-insensitive but encode is lowercase to match
17
+ * what PICO-8 writes).
18
+ *
19
+ * The grid is 8 lines of 8 characters (one 8x8 sprite). The round-trip
20
+ * `nibbles → grid → nibbles` is an identity for every valid sprite (asserted in
21
+ * the tests), because `.` maps to 0 and 0 maps to `.` with no other collision.
22
+ *
23
+ * This codec is deliberately SEPARABLE from the cart model and the commands: it
24
+ * only knows nibble grids and strings, so the later `gfx render` task can reuse
25
+ * it and a test can round-trip it without a cart.
26
+ */
27
+
28
+ /** The width/height (in pixels) of one sprite; a char grid is this square. */
29
+ export const GRID_SIZE = SPRITE_SIZE;
30
+
31
+ /** The character that renders colour index 0 (transparent) in a char grid. */
32
+ export const TRANSPARENT_CHAR = '.';
33
+
34
+ /**
35
+ * Encodes an 8x8 grid of colour nibbles (0..15) into a char-grid string: 8
36
+ * lines of 8 chars, `\n`-joined, no trailing newline. Index 0 → `.`; 1..15 →
37
+ * the lowercase hex digit.
38
+ *
39
+ * @throws {GfxGridError} if the grid is not 8x8 or a cell is out of 0..15.
40
+ */
41
+ export function nibblesToGrid(grid: number[][]): string {
42
+ assertNibbleGrid(grid);
43
+ const lines: string[] = [];
44
+ for (let r = 0; r < GRID_SIZE; r++) {
45
+ let line = '';
46
+ for (let c = 0; c < GRID_SIZE; c++) line += nibbleToChar(grid[r]?.[c] ?? 0);
47
+ lines.push(line);
48
+ }
49
+ return lines.join('\n');
50
+ }
51
+
52
+ /**
53
+ * Decodes a char-grid string back into an 8x8 grid of colour nibbles (0..15),
54
+ * the inverse of {@link nibblesToGrid}. Accepts `.` (→ 0) and hex digits
55
+ * (case-insensitive). Blank lines / trailing whitespace-only content are
56
+ * tolerated so an agent may paste a grid with a trailing newline; the parsed
57
+ * content must still be exactly 8 rows of 8 cells.
58
+ *
59
+ * @throws {GfxGridError} if the parsed grid is not 8x8 or a char is invalid.
60
+ */
61
+ export function gridToNibbles(text: string): number[][] {
62
+ // Split on either newline style; drop a single trailing empty line (a paste
63
+ // with a final newline) but keep interior blank lines so a genuine malformed
64
+ // grid (too few rows) is caught rather than silently accepted.
65
+ const rawLines = text.split(/\r?\n/);
66
+ while (rawLines.length > 0 && rawLines[rawLines.length - 1] === '')
67
+ rawLines.pop();
68
+
69
+ if (rawLines.length !== GRID_SIZE) {
70
+ throw new GfxGridError(
71
+ `char grid must have ${GRID_SIZE} rows, got ${rawLines.length}`,
72
+ );
73
+ }
74
+
75
+ const grid: number[][] = [];
76
+ for (let r = 0; r < GRID_SIZE; r++) {
77
+ const line = rawLines[r] ?? '';
78
+ if (line.length !== GRID_SIZE) {
79
+ throw new GfxGridError(
80
+ `char grid row ${r} must have ${GRID_SIZE} cells, got ${line.length}`,
81
+ );
82
+ }
83
+ const row: number[] = [];
84
+ for (let c = 0; c < GRID_SIZE; c++)
85
+ row.push(charToNibble(line[c] as string, r, c));
86
+ grid.push(row);
87
+ }
88
+ return grid;
89
+ }
90
+
91
+ /** A malformed char grid or nibble grid (wrong shape or an invalid cell/char). */
92
+ export class GfxGridError extends Error {
93
+ constructor(message: string) {
94
+ super(message);
95
+ this.name = 'GfxGridError';
96
+ }
97
+ }
98
+
99
+ function nibbleToChar(n: number): string {
100
+ return n === 0 ? TRANSPARENT_CHAR : n.toString(16);
101
+ }
102
+
103
+ function charToNibble(ch: string, r: number, c: number): number {
104
+ if (ch === TRANSPARENT_CHAR) return 0;
105
+ const v = Number.parseInt(ch, 16);
106
+ if (Number.isNaN(v) || v < 0 || v > 15) {
107
+ throw new GfxGridError(
108
+ `invalid char "${ch}" at (${c}, ${r}); expected "${TRANSPARENT_CHAR}" or a hex digit 0-f`,
109
+ );
110
+ }
111
+ return v;
112
+ }
113
+
114
+ function assertNibbleGrid(grid: number[][]): void {
115
+ if (grid.length !== GRID_SIZE) {
116
+ throw new GfxGridError(
117
+ `nibble grid must have ${GRID_SIZE} rows, got ${grid.length}`,
118
+ );
119
+ }
120
+ for (let r = 0; r < GRID_SIZE; r++) {
121
+ const row = grid[r];
122
+ if (row === undefined || row.length !== GRID_SIZE) {
123
+ throw new GfxGridError(
124
+ `nibble grid row ${r} must have ${GRID_SIZE} cells`,
125
+ );
126
+ }
127
+ for (let c = 0; c < GRID_SIZE; c++) {
128
+ const v = row[c] ?? 0;
129
+ if (!Number.isInteger(v) || v < 0 || v > 15) {
130
+ throw new GfxGridError(
131
+ `nibble grid cell (${c}, ${r}) = ${v} out of range 0..15`,
132
+ );
133
+ }
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `engine/gfx` — the shrinko-free char-grid codec (`__gfx__` nibbles ↔ char
3
+ * grid) plus the gfx/map overlap smart-refuse decision.
4
+ *
5
+ * This module OWNS the char-grid representation (the "SVG for pixels" EDIT
6
+ * surface), the overlap safety logic, and the palette-accurate PNG render (the
7
+ * JUDGE surface). The `gfx show`/`gfx set`/`gfx render` commands are thin wiring
8
+ * over it. It is separable from the cart model (it only speaks nibble grids,
9
+ * strings, and RGB buffers) so it round-trips in isolation.
10
+ */
11
+ export {
12
+ GRID_SIZE,
13
+ GfxGridError,
14
+ gridToNibbles,
15
+ nibblesToGrid,
16
+ TRANSPARENT_CHAR,
17
+ } from './grid.js';
18
+ export {
19
+ LABEL_SIZE,
20
+ LabelError,
21
+ labelHexFromPng,
22
+ nearestPaletteIndex,
23
+ } from './label.js';
24
+ export {decideOverlap, type OverlapDecision, spriteHasData} from './overlap.js';
25
+ export {PICO8_PALETTE, type PaletteColor, paletteColor} from './palette.js';
26
+ export {encodePng, type RgbImage} from './png.js';
27
+ export {
28
+ RENDER_TARGET_PX,
29
+ renderSheetPng,
30
+ renderSpritePng,
31
+ SHEET_RENDER_SCALE,
32
+ SPRITE_RENDER_SCALE,
33
+ upscale,
34
+ } from './render.js';
@@ -0,0 +1,105 @@
1
+ import {describe, expect, it} from 'vitest';
2
+ import {
3
+ LABEL_SIZE,
4
+ LabelError,
5
+ labelHexFromPng,
6
+ nearestPaletteIndex,
7
+ } from './label.js';
8
+ import {PICO8_PALETTE} from './palette.js';
9
+ import {encodePng, type RgbImage} from './png.js';
10
+
11
+ /**
12
+ * The label builder is exercised against PNGs produced by our OWN encoder
13
+ * (`encodePng`, RGB truecolour, stored-deflate), so the decode + nearest-palette
14
+ * round-trip is verified end-to-end with no external fixtures. Node's `zlib`
15
+ * inflate reads our stored blocks fine (a standard zlib stream).
16
+ */
17
+
18
+ /** Build a solid-colour 128x128 RGB PNG from a PICO-8 palette index. */
19
+ function solidLabelPng(paletteIndex: number): Uint8Array {
20
+ const c = PICO8_PALETTE[paletteIndex]!;
21
+ const rgb = new Uint8Array(LABEL_SIZE * LABEL_SIZE * 3);
22
+ for (let i = 0; i < LABEL_SIZE * LABEL_SIZE; i++) {
23
+ rgb[i * 3] = c.r;
24
+ rgb[i * 3 + 1] = c.g;
25
+ rgb[i * 3 + 2] = c.b;
26
+ }
27
+ return encodePng({width: LABEL_SIZE, height: LABEL_SIZE, rgb});
28
+ }
29
+
30
+ describe('nearestPaletteIndex', () => {
31
+ it('maps each exact palette RGB back to its own index', () => {
32
+ for (let i = 0; i < PICO8_PALETTE.length; i++) {
33
+ const c = PICO8_PALETTE[i]!;
34
+ expect(nearestPaletteIndex(c.r, c.g, c.b)).toBe(i);
35
+ }
36
+ });
37
+
38
+ it('snaps a near colour to the closest palette entry (pure black -> 0)', () => {
39
+ expect(nearestPaletteIndex(3, 2, 1)).toBe(0);
40
+ // Near PICO-8 red (255,0,77) -> index 8.
41
+ expect(nearestPaletteIndex(250, 5, 80)).toBe(8);
42
+ });
43
+ });
44
+
45
+ describe('labelHexFromPng', () => {
46
+ it('produces 128 rows of 128 nibbles with a trailing newline', () => {
47
+ const hex = labelHexFromPng(solidLabelPng(1));
48
+ const rows = hex.split('\n');
49
+ // 128 rows + a trailing empty from the final newline.
50
+ expect(rows.length).toBe(LABEL_SIZE + 1);
51
+ expect(rows[LABEL_SIZE]).toBe('');
52
+ for (let y = 0; y < LABEL_SIZE; y++) {
53
+ expect(rows[y]!.length).toBe(LABEL_SIZE);
54
+ }
55
+ });
56
+
57
+ it('maps a solid palette colour to that nibble everywhere (index 1 -> all "1")', () => {
58
+ const hex = labelHexFromPng(solidLabelPng(1));
59
+ const firstRow = hex.split('\n')[0]!;
60
+ expect(firstRow).toBe('1'.repeat(LABEL_SIZE));
61
+ });
62
+
63
+ it('round-trips an arbitrary palette index (e.g. 10 -> "a")', () => {
64
+ const hex = labelHexFromPng(solidLabelPng(10));
65
+ expect(hex.split('\n')[0]!).toBe('a'.repeat(LABEL_SIZE));
66
+ });
67
+
68
+ it('preserves per-pixel colours (a two-colour split image)', () => {
69
+ // Left half index 8 (red -> "8"), right half index 12 (blue -> "c").
70
+ const left = PICO8_PALETTE[8]!;
71
+ const right = PICO8_PALETTE[12]!;
72
+ const rgb = new Uint8Array(LABEL_SIZE * LABEL_SIZE * 3);
73
+ for (let y = 0; y < LABEL_SIZE; y++) {
74
+ for (let x = 0; x < LABEL_SIZE; x++) {
75
+ const c = x < LABEL_SIZE / 2 ? left : right;
76
+ const d = (y * LABEL_SIZE + x) * 3;
77
+ rgb[d] = c.r;
78
+ rgb[d + 1] = c.g;
79
+ rgb[d + 2] = c.b;
80
+ }
81
+ }
82
+ const png = encodePng({width: LABEL_SIZE, height: LABEL_SIZE, rgb});
83
+ const row = labelHexFromPng(png).split('\n')[0]!;
84
+ expect(row.slice(0, LABEL_SIZE / 2)).toBe('8'.repeat(LABEL_SIZE / 2));
85
+ expect(row.slice(LABEL_SIZE / 2)).toBe('c'.repeat(LABEL_SIZE / 2));
86
+ });
87
+
88
+ it('rejects a non-PNG with a structured LabelError', () => {
89
+ expect(() => labelHexFromPng(new Uint8Array([1, 2, 3, 4]))).toThrowError(
90
+ LabelError,
91
+ );
92
+ });
93
+
94
+ it('rejects a wrong-size image (must be exactly 128x128)', () => {
95
+ const rgb = new Uint8Array(64 * 64 * 3);
96
+ const png = encodePng({width: 64, height: 64, rgb} as RgbImage);
97
+ try {
98
+ labelHexFromPng(png);
99
+ expect.unreachable('should have thrown');
100
+ } catch (e) {
101
+ expect(e).toBeInstanceOf(LabelError);
102
+ expect((e as LabelError).code).toBe('wrong-size');
103
+ }
104
+ });
105
+ });