picopilot 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +224 -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 +259 -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 +482 -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 +264 -0
  243. package/src/commands/run.ts +285 -0
  244. package/src/commands/serve.test.ts +227 -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 +278 -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,196 @@
1
+ /**
2
+ * A tiny, dependency-FREE, deterministic PNG encoder for the `gfx render` JUDGE
3
+ * surface. Encodes an RGB pixel buffer into a valid 8-bit truecolour PNG using
4
+ * hand-rolled CRC32, Adler32, and STORED (uncompressed) zlib deflate blocks.
5
+ *
6
+ * Why hand-rolled + stored blocks (recorded as a `gfx render` encoder choice):
7
+ * the eyes-loop is meant to stay dependency-light (shrinko-free, no npm PNG lib),
8
+ * and `gfx render` output is tiny (~256x256), so compression buys nothing worth a
9
+ * dependency. STORED deflate blocks keep the encoder ~a page of code AND make the
10
+ * output BYTE-DETERMINISTIC (no compressor heuristics), which is exactly what the
11
+ * pixel-byte tests assert against. A standard PNG decoder reads stored blocks
12
+ * identically to compressed ones, so the file is a normal, viewable PNG.
13
+ *
14
+ * This module knows nothing about PICO-8: it takes raw RGB and dimensions. The
15
+ * palette mapping + upscale live in `render.ts`.
16
+ */
17
+
18
+ /** A raw RGB image: `width * height * 3` bytes, row-major, no padding. */
19
+ export interface RgbImage {
20
+ readonly width: number;
21
+ readonly height: number;
22
+ /** `rgb[(y * width + x) * 3 + {0,1,2}]` = R, G, B (0..255) for pixel (x, y). */
23
+ readonly rgb: Uint8Array;
24
+ }
25
+
26
+ const PNG_SIGNATURE = Uint8Array.from([
27
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
28
+ ]);
29
+
30
+ /**
31
+ * Encodes an {@link RgbImage} into a complete PNG file (signature + IHDR + IDAT +
32
+ * IEND) as a `Uint8Array`. 8-bit RGB truecolour (colour type 2), no interlace,
33
+ * each scanline prefixed with filter byte 0 (None) so the pixel bytes appear
34
+ * verbatim in the IDAT payload.
35
+ */
36
+ export function encodePng(image: RgbImage): Uint8Array {
37
+ const {width, height, rgb} = image;
38
+ if (
39
+ !Number.isInteger(width) ||
40
+ !Number.isInteger(height) ||
41
+ width <= 0 ||
42
+ height <= 0
43
+ ) {
44
+ throw new RangeError(
45
+ `PNG dimensions must be positive integers, got ${width}x${height}`,
46
+ );
47
+ }
48
+ if (rgb.length !== width * height * 3) {
49
+ throw new RangeError(
50
+ `RGB buffer length ${rgb.length} does not match ${width}x${height}x3 = ${width * height * 3}`,
51
+ );
52
+ }
53
+
54
+ // Raw scanlines: each row is one filter byte (0 = None) then width*3 RGB bytes.
55
+ const stride = width * 3;
56
+ const raw = new Uint8Array(height * (stride + 1));
57
+ for (let y = 0; y < height; y++) {
58
+ const dst = y * (stride + 1);
59
+ raw[dst] = 0; // filter type None
60
+ raw.set(rgb.subarray(y * stride, y * stride + stride), dst + 1);
61
+ }
62
+
63
+ const ihdr = buildIhdr(width, height);
64
+ const idat = zlibStore(raw);
65
+
66
+ const chunks = [
67
+ chunk('IHDR', ihdr),
68
+ chunk('IDAT', idat),
69
+ chunk('IEND', new Uint8Array(0)),
70
+ ];
71
+
72
+ let total = PNG_SIGNATURE.length;
73
+ for (const c of chunks) total += c.length;
74
+ const out = new Uint8Array(total);
75
+ let off = 0;
76
+ out.set(PNG_SIGNATURE, off);
77
+ off += PNG_SIGNATURE.length;
78
+ for (const c of chunks) {
79
+ out.set(c, off);
80
+ off += c.length;
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /** The 13-byte IHDR data: width, height, bit-depth 8, colour type 2 (RGB), etc. */
86
+ function buildIhdr(width: number, height: number): Uint8Array {
87
+ const d = new Uint8Array(13);
88
+ writeUint32(d, 0, width);
89
+ writeUint32(d, 4, height);
90
+ d[8] = 8; // bit depth
91
+ d[9] = 2; // colour type: truecolour RGB
92
+ d[10] = 0; // compression: deflate
93
+ d[11] = 0; // filter: adaptive (only filter 0 used per scanline)
94
+ d[12] = 0; // interlace: none
95
+ return d;
96
+ }
97
+
98
+ /** Wraps chunk `data` with its 4-byte length, 4-byte type, and 4-byte CRC32. */
99
+ function chunk(type: string, data: Uint8Array): Uint8Array {
100
+ const typeBytes = Uint8Array.from([
101
+ type.charCodeAt(0),
102
+ type.charCodeAt(1),
103
+ type.charCodeAt(2),
104
+ type.charCodeAt(3),
105
+ ]);
106
+ const out = new Uint8Array(4 + 4 + data.length + 4);
107
+ writeUint32(out, 0, data.length);
108
+ out.set(typeBytes, 4);
109
+ out.set(data, 8);
110
+ // CRC covers the type + data (not the length).
111
+ const crcInput = new Uint8Array(4 + data.length);
112
+ crcInput.set(typeBytes, 0);
113
+ crcInput.set(data, 4);
114
+ writeUint32(out, 8 + data.length, crc32(crcInput));
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * Wraps `data` in a zlib stream (RFC 1950) using DEFLATE STORED blocks (RFC 1951,
120
+ * BTYPE=00): a 2-byte zlib header, one or more stored blocks (each ≤ 65535
121
+ * bytes), and a 4-byte big-endian Adler32 of the uncompressed data.
122
+ */
123
+ function zlibStore(data: Uint8Array): Uint8Array {
124
+ const MAX_BLOCK = 0xffff;
125
+ const blockCount = Math.max(1, Math.ceil(data.length / MAX_BLOCK));
126
+ // 2 header bytes + per-block (1 BFINAL/BTYPE + 2 LEN + 2 NLEN) + data + 4 adler.
127
+ const out = new Uint8Array(2 + blockCount * 5 + data.length + 4);
128
+ let off = 0;
129
+
130
+ // zlib header: CMF=0x78 (deflate, 32K window), FLG=0x01 so (CMF*256+FLG)%31==0.
131
+ out[off++] = 0x78;
132
+ out[off++] = 0x01;
133
+
134
+ let pos = 0;
135
+ for (let b = 0; b < blockCount; b++) {
136
+ const len = Math.min(MAX_BLOCK, data.length - pos);
137
+ const isFinal = b === blockCount - 1;
138
+ out[off++] = isFinal ? 1 : 0; // BFINAL bit, BTYPE=00 (stored)
139
+ // LEN then NLEN, both little-endian.
140
+ out[off++] = len & 0xff;
141
+ out[off++] = (len >> 8) & 0xff;
142
+ const nlen = ~len & 0xffff;
143
+ out[off++] = nlen & 0xff;
144
+ out[off++] = (nlen >> 8) & 0xff;
145
+ out.set(data.subarray(pos, pos + len), off);
146
+ off += len;
147
+ pos += len;
148
+ }
149
+
150
+ writeUint32(out, off, adler32(data));
151
+ return out;
152
+ }
153
+
154
+ function writeUint32(buf: Uint8Array, offset: number, value: number): void {
155
+ buf[offset] = (value >>> 24) & 0xff;
156
+ buf[offset + 1] = (value >>> 16) & 0xff;
157
+ buf[offset + 2] = (value >>> 8) & 0xff;
158
+ buf[offset + 3] = value & 0xff;
159
+ }
160
+
161
+ // --- CRC32 (PNG polynomial 0xedb88320) ---
162
+
163
+ const CRC_TABLE = buildCrcTable();
164
+
165
+ function buildCrcTable(): Uint32Array {
166
+ const table = new Uint32Array(256);
167
+ for (let n = 0; n < 256; n++) {
168
+ let c = n;
169
+ for (let k = 0; k < 8; k++) {
170
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
171
+ }
172
+ table[n] = c >>> 0;
173
+ }
174
+ return table;
175
+ }
176
+
177
+ function crc32(data: Uint8Array): number {
178
+ let crc = 0xffffffff;
179
+ for (let i = 0; i < data.length; i++) {
180
+ crc = CRC_TABLE[(crc ^ (data[i] ?? 0)) & 0xff]! ^ (crc >>> 8);
181
+ }
182
+ return (crc ^ 0xffffffff) >>> 0;
183
+ }
184
+
185
+ // --- Adler32 (RFC 1950) ---
186
+
187
+ function adler32(data: Uint8Array): number {
188
+ const MOD = 65521;
189
+ let a = 1;
190
+ let b = 0;
191
+ for (let i = 0; i < data.length; i++) {
192
+ a = (a + (data[i] ?? 0)) % MOD;
193
+ b = (b + a) % MOD;
194
+ }
195
+ return ((b << 16) | a) >>> 0;
196
+ }
@@ -0,0 +1,180 @@
1
+ import {inflateSync} from 'node:zlib';
2
+ import {describe, expect, it} from 'vitest';
3
+
4
+ import {GfxSheet} from '../cart/index.js';
5
+ import {PICO8_PALETTE} from './palette.js';
6
+ import type {RgbImage} from './png.js';
7
+ import {
8
+ renderSheetPng,
9
+ renderSpritePng,
10
+ SHEET_RENDER_SCALE,
11
+ SPRITE_RENDER_SCALE,
12
+ upscale,
13
+ } from './render.js';
14
+
15
+ /** Decodes a PNG (via node's independent inflate) into a raw RGB image. */
16
+ function decodePng(bytes: Uint8Array): RgbImage {
17
+ let off = 8;
18
+ let width = 0;
19
+ let height = 0;
20
+ const idatParts: Buffer[] = [];
21
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
22
+ while (off < bytes.length) {
23
+ const len = view.getUint32(off);
24
+ const type = String.fromCharCode(
25
+ bytes[off + 4]!,
26
+ bytes[off + 5]!,
27
+ bytes[off + 6]!,
28
+ bytes[off + 7]!,
29
+ );
30
+ const data = bytes.subarray(off + 8, off + 8 + len);
31
+ if (type === 'IHDR') {
32
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
33
+ width = dv.getUint32(0);
34
+ height = dv.getUint32(4);
35
+ } else if (type === 'IDAT') {
36
+ idatParts.push(Buffer.from(data));
37
+ }
38
+ off += 12 + len;
39
+ if (type === 'IEND') break;
40
+ }
41
+ const raw = new Uint8Array(inflateSync(Buffer.concat(idatParts)));
42
+ const stride = width * 3;
43
+ const rgb = new Uint8Array(width * height * 3);
44
+ for (let y = 0; y < height; y++) {
45
+ rgb.set(
46
+ raw.subarray(y * (stride + 1) + 1, y * (stride + 1) + 1 + stride),
47
+ y * stride,
48
+ );
49
+ }
50
+ return {width, height, rgb};
51
+ }
52
+
53
+ function pixelAt(
54
+ img: RgbImage,
55
+ x: number,
56
+ y: number,
57
+ ): [number, number, number] {
58
+ const off = (y * img.width + x) * 3;
59
+ return [img.rgb[off]!, img.rgb[off + 1]!, img.rgb[off + 2]!];
60
+ }
61
+
62
+ function rgbOf(index: number): [number, number, number] {
63
+ const c = PICO8_PALETTE[index]!;
64
+ return [c.r, c.g, c.b];
65
+ }
66
+
67
+ /** An 8x8 nibble grid from a generator. */
68
+ function makeGrid(fn: (r: number, c: number) => number): number[][] {
69
+ return Array.from({length: 8}, (_, r) =>
70
+ Array.from({length: 8}, (_, c) => fn(r, c)),
71
+ );
72
+ }
73
+
74
+ describe('upscale (nearest-neighbour, palette-accurate)', () => {
75
+ it('paints each source pixel as a scale x scale block of its palette RGB', () => {
76
+ // 2x1 source: index 8 (red) then index 12 (blue), scale 4.
77
+ const img = upscale(2, 1, (x) => (x === 0 ? 8 : 12), 4);
78
+ expect(img.width).toBe(8);
79
+ expect(img.height).toBe(4);
80
+ // The whole left 4x4 block is red; the whole right 4x4 block is blue.
81
+ for (let y = 0; y < 4; y++) {
82
+ for (let x = 0; x < 4; x++) expect(pixelAt(img, x, y)).toEqual(rgbOf(8));
83
+ for (let x = 4; x < 8; x++) expect(pixelAt(img, x, y)).toEqual(rgbOf(12));
84
+ }
85
+ });
86
+ });
87
+
88
+ describe('renderSpritePng: deterministic palette-accurate pixel bytes', () => {
89
+ it('upscales an 8x8 sprite to 256x256 with the exact palette RGB per index', () => {
90
+ // Paint sprite 0 so pixel (c, r) has colour index (r*8 + c) % 16 — every
91
+ // index appears, at a known position.
92
+ const painted = makeGrid((r, c) => (r * 8 + c) % 16);
93
+ const sheet = GfxSheet.fromBody(undefined);
94
+ sheet.setSprite(0, painted);
95
+
96
+ const png = renderSpritePng(sheet, 0);
97
+ const img = decodePng(png);
98
+ expect(img.width).toBe(8 * SPRITE_RENDER_SCALE);
99
+ expect(img.height).toBe(8 * SPRITE_RENDER_SCALE);
100
+ expect(img.width).toBe(256);
101
+
102
+ // Assert the PIXEL BYTES: the centre of each source-pixel block is exactly
103
+ // that index's palette RGB.
104
+ const s = SPRITE_RENDER_SCALE;
105
+ for (let r = 0; r < 8; r++) {
106
+ for (let c = 0; c < 8; c++) {
107
+ const index = (r * 8 + c) % 16;
108
+ const cx = c * s + (s >> 1);
109
+ const cy = r * s + (s >> 1);
110
+ expect(pixelAt(img, cx, cy)).toEqual(rgbOf(index));
111
+ }
112
+ }
113
+ });
114
+
115
+ it('renders a non-zero sprite (e.g. 5) from its own origin, not sprite 0', () => {
116
+ const sheet = GfxSheet.fromBody(undefined);
117
+ sheet.setSprite(
118
+ 0,
119
+ makeGrid(() => 8),
120
+ ); // sprite 0 is all red — must NOT leak in
121
+ sheet.setSprite(
122
+ 5,
123
+ makeGrid(() => 11),
124
+ ); // sprite 5 is all green
125
+
126
+ const img = decodePng(renderSpritePng(sheet, 5));
127
+ // Every pixel is green (index 11), proving sprite 5's origin was used.
128
+ for (let y = 0; y < img.height; y += 37) {
129
+ for (let x = 0; x < img.width; x += 37) {
130
+ expect(pixelAt(img, x, y)).toEqual(rgbOf(11));
131
+ }
132
+ }
133
+ });
134
+
135
+ it('is byte-deterministic for the same sprite', () => {
136
+ const sheet = GfxSheet.fromBody(undefined);
137
+ sheet.setSprite(
138
+ 0,
139
+ makeGrid((r, c) => (r + c) % 16),
140
+ );
141
+ const a = renderSpritePng(sheet, 0);
142
+ const b = renderSpritePng(sheet, 0);
143
+ expect(Array.from(a)).toEqual(Array.from(b));
144
+ });
145
+ });
146
+
147
+ describe('renderSheetPng: the whole 128x128 spritesheet', () => {
148
+ it('upscales the 128x128 sheet to 256x256 with palette-accurate pixels', () => {
149
+ const sheet = GfxSheet.fromBody(undefined);
150
+ // Distinct colours in distinct sprites, so positions are checkable.
151
+ sheet.setSprite(
152
+ 0,
153
+ makeGrid(() => 8),
154
+ ); // top-left 8x8 (upscaled) = red
155
+ sheet.setSprite(
156
+ 15,
157
+ makeGrid(() => 12),
158
+ ); // top-right sprite = blue
159
+ sheet.setSprite(
160
+ 255,
161
+ makeGrid(() => 10),
162
+ ); // bottom-right sprite = yellow
163
+
164
+ const img = decodePng(renderSheetPng(sheet));
165
+ expect(img.width).toBe(128 * SHEET_RENDER_SCALE);
166
+ expect(img.height).toBe(128 * SHEET_RENDER_SCALE);
167
+ expect(img.width).toBe(256);
168
+
169
+ const s = SHEET_RENDER_SCALE;
170
+ // sprite 0 occupies source pixels [0,8)x[0,8) → dest [0,16)x[0,16).
171
+ expect(pixelAt(img, 0, 0)).toEqual(rgbOf(8));
172
+ expect(pixelAt(img, 8 * s - 1, 8 * s - 1)).toEqual(rgbOf(8));
173
+ // sprite 15 is source cols [120,128) row [0,8) → dest x [240,256).
174
+ expect(pixelAt(img, 120 * s, 0)).toEqual(rgbOf(12));
175
+ // sprite 255 is the bottom-right 8x8 → dest bottom-right corner.
176
+ expect(pixelAt(img, img.width - 1, img.height - 1)).toEqual(rgbOf(10));
177
+ // An untouched pixel (sprite 1 region) is index 0 = black.
178
+ expect(pixelAt(img, 8 * s, 0)).toEqual(rgbOf(0));
179
+ });
180
+ });
@@ -0,0 +1,106 @@
1
+ import {
2
+ GFX_HEIGHT,
3
+ GFX_WIDTH,
4
+ type GfxSheet,
5
+ SPRITE_SIZE,
6
+ spriteOrigin,
7
+ } from '../cart/index.js';
8
+ import {paletteColor} from './palette.js';
9
+ import {encodePng, type RgbImage} from './png.js';
10
+
11
+ /**
12
+ * The `gfx render` JUDGE surface: turn a sprite (or the whole spritesheet) into a
13
+ * nearest-neighbour-upscaled, palette-accurate PNG a multimodal agent can LOOK at.
14
+ *
15
+ * The char grid (`gfx show`/`gfx set`) is the exact EDIT surface, but an LLM reads
16
+ * serialized hex poorly for gestalt/colour (the SVG parallel: models generate
17
+ * structured visual source well but read it back poorly). So this renders the
18
+ * ACTUAL pixels, upscaled enough to be viewable, with each colour index mapped to
19
+ * its exact PICO-8 palette RGB (single source of truth: {@link paletteColor}).
20
+ *
21
+ * Upscale is NEAREST-NEIGHBOUR (each source pixel becomes a `scale`x`scale` block
22
+ * of identical RGB) so the pixel-art stays crisp and the output bytes are
23
+ * deterministic. The scale is chosen so the longer edge lands near ~256px:
24
+ * - a single 8x8 sprite → scale 32 → 256x256 (see {@link SPRITE_RENDER_SCALE}).
25
+ * - the 128x128 sheet → scale 2 → 256x256 (see {@link SHEET_RENDER_SCALE}).
26
+ */
27
+
28
+ /** Nearest-neighbour scale for a single 8x8 sprite: 8 * 32 = 256px. */
29
+ export const SPRITE_RENDER_SCALE = 32;
30
+
31
+ /** Nearest-neighbour scale for the 128x128 spritesheet: 128 * 2 = 256px. */
32
+ export const SHEET_RENDER_SCALE = 2;
33
+
34
+ /** The target viewable edge length both scales aim for (~256px). */
35
+ export const RENDER_TARGET_PX = 256;
36
+
37
+ /**
38
+ * Renders one sprite (0..255) to a PNG buffer: its 8x8 pixels, each mapped to its
39
+ * palette RGB, nearest-neighbour upscaled by {@link SPRITE_RENDER_SCALE}.
40
+ */
41
+ export function renderSpritePng(sheet: GfxSheet, n: number): Uint8Array {
42
+ const {x0, y0} = spriteOrigin(n);
43
+ return encodePng(
44
+ upscale(
45
+ SPRITE_SIZE,
46
+ SPRITE_SIZE,
47
+ (x, y) => sheet.getPixel(x0 + x, y0 + y),
48
+ SPRITE_RENDER_SCALE,
49
+ ),
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Renders the whole 128x128 spritesheet to a PNG buffer: every pixel mapped to
55
+ * its palette RGB, nearest-neighbour upscaled by {@link SHEET_RENDER_SCALE}.
56
+ */
57
+ export function renderSheetPng(sheet: GfxSheet): Uint8Array {
58
+ return encodePng(
59
+ upscale(
60
+ GFX_WIDTH,
61
+ GFX_HEIGHT,
62
+ (x, y) => sheet.getPixel(x, y),
63
+ SHEET_RENDER_SCALE,
64
+ ),
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Builds an upscaled {@link RgbImage} from a source of colour indices. Each
70
+ * source pixel (`getIndex(x, y)`) is mapped to its palette RGB and painted as a
71
+ * `scale`x`scale` block (nearest-neighbour), producing a `(srcW*scale)` x
72
+ * `(srcH*scale)` RGB buffer.
73
+ */
74
+ export function upscale(
75
+ srcWidth: number,
76
+ srcHeight: number,
77
+ getIndex: (x: number, y: number) => number,
78
+ scale: number,
79
+ ): RgbImage {
80
+ if (!Number.isInteger(scale) || scale < 1) {
81
+ throw new RangeError(
82
+ `upscale factor must be a positive integer, got ${scale}`,
83
+ );
84
+ }
85
+ const width = srcWidth * scale;
86
+ const height = srcHeight * scale;
87
+ const rgb = new Uint8Array(width * height * 3);
88
+
89
+ for (let sy = 0; sy < srcHeight; sy++) {
90
+ for (let sx = 0; sx < srcWidth; sx++) {
91
+ const {r, g, b} = paletteColor(getIndex(sx, sy));
92
+ // Paint the scale x scale destination block for this source pixel.
93
+ for (let dy = 0; dy < scale; dy++) {
94
+ const y = sy * scale + dy;
95
+ for (let dx = 0; dx < scale; dx++) {
96
+ const x = sx * scale + dx;
97
+ const off = (y * width + x) * 3;
98
+ rgb[off] = r;
99
+ rgb[off + 1] = g;
100
+ rgb[off + 2] = b;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return {width, height, rgb};
106
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The picopilot engine layer: pure codecs and external-tool adapters, kept
3
+ * separate from the incur command layer (`src/commands/`) so commands stay thin
4
+ * and the engine stays independently testable.
5
+ *
6
+ * Modules (one per seam; more added by later tasks):
7
+ *
8
+ * - `engine/cart`: the `.p8` cart model: parse sections, addressable
9
+ * gfx/map regions, byte-identical serialize (this seam).
10
+ * - `engine/gfx`: TS codec: `__gfx__` hex to/from char grid, and hex to PNG.
11
+ * - `engine/audio`: TS codec: picopilot-MML to `__sfx__`, patterns to `__music__`.
12
+ * - `engine/shrinko`: adapter shelling out to the external `shrinko8` tool
13
+ * (token count, lint, minify, p8/lua, spritesheet PNG).
14
+ * - `engine/pico8`: headless run + capture + export (needs the PICO-8 binary).
15
+ *
16
+ * Later tasks re-export from here without restructuring.
17
+ */
18
+ export * from './audio/index.js';
19
+ export * from './cart/index.js';
20
+ export * from './pico8/index.js';
21
+ export * from './shrinko/index.js';