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.
- package/LICENSE +661 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audio.d.ts +17 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +315 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/export.d.ts +36 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +257 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/gfx.d.ts +28 -0
- package/dist/commands/gfx.d.ts.map +1 -0
- package/dist/commands/gfx.js +521 -0
- package/dist/commands/gfx.js.map +1 -0
- package/dist/commands/init.d.ts +34 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +151 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +20 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +90 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/minify.d.ts +20 -0
- package/dist/commands/minify.d.ts.map +1 -0
- package/dist/commands/minify.js +129 -0
- package/dist/commands/minify.js.map +1 -0
- package/dist/commands/music.d.ts +13 -0
- package/dist/commands/music.d.ts.map +1 -0
- package/dist/commands/music.js +241 -0
- package/dist/commands/music.js.map +1 -0
- package/dist/commands/playtest.d.ts +29 -0
- package/dist/commands/playtest.d.ts.map +1 -0
- package/dist/commands/playtest.js +631 -0
- package/dist/commands/playtest.js.map +1 -0
- package/dist/commands/run.d.ts +30 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +224 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +50 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +213 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/sfx.d.ts +13 -0
- package/dist/commands/sfx.d.ts.map +1 -0
- package/dist/commands/sfx.js +254 -0
- package/dist/commands/sfx.js.map +1 -0
- package/dist/commands/tokens.d.ts +36 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +129 -0
- package/dist/commands/tokens.js.map +1 -0
- package/dist/commands/verify.d.ts +57 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +213 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +24 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/engine/audio/index.d.ts +14 -0
- package/dist/engine/audio/index.d.ts.map +1 -0
- package/dist/engine/audio/index.js +14 -0
- package/dist/engine/audio/index.js.map +1 -0
- package/dist/engine/audio/music.d.ts +116 -0
- package/dist/engine/audio/music.d.ts.map +1 -0
- package/dist/engine/audio/music.js +138 -0
- package/dist/engine/audio/music.js.map +1 -0
- package/dist/engine/audio/section.d.ts +34 -0
- package/dist/engine/audio/section.d.ts.map +1 -0
- package/dist/engine/audio/section.js +67 -0
- package/dist/engine/audio/section.js.map +1 -0
- package/dist/engine/audio/sfx.d.ts +141 -0
- package/dist/engine/audio/sfx.d.ts.map +1 -0
- package/dist/engine/audio/sfx.js +506 -0
- package/dist/engine/audio/sfx.js.map +1 -0
- package/dist/engine/cart/cart.d.ts +75 -0
- package/dist/engine/cart/cart.d.ts.map +1 -0
- package/dist/engine/cart/cart.js +153 -0
- package/dist/engine/cart/cart.js.map +1 -0
- package/dist/engine/cart/errors.d.ts +29 -0
- package/dist/engine/cart/errors.d.ts.map +1 -0
- package/dist/engine/cart/errors.js +26 -0
- package/dist/engine/cart/errors.js.map +1 -0
- package/dist/engine/cart/gfx.d.ts +79 -0
- package/dist/engine/cart/gfx.d.ts.map +1 -0
- package/dist/engine/cart/gfx.js +171 -0
- package/dist/engine/cart/gfx.js.map +1 -0
- package/dist/engine/cart/index.d.ts +15 -0
- package/dist/engine/cart/index.d.ts.map +1 -0
- package/dist/engine/cart/index.js +15 -0
- package/dist/engine/cart/index.js.map +1 -0
- package/dist/engine/cart/map.d.ts +46 -0
- package/dist/engine/cart/map.d.ts.map +1 -0
- package/dist/engine/cart/map.js +118 -0
- package/dist/engine/cart/map.js.map +1 -0
- package/dist/engine/config/config.d.ts +74 -0
- package/dist/engine/config/config.d.ts.map +1 -0
- package/dist/engine/config/config.js +103 -0
- package/dist/engine/config/config.js.map +1 -0
- package/dist/engine/config/index.d.ts +7 -0
- package/dist/engine/config/index.d.ts.map +1 -0
- package/dist/engine/config/index.js +7 -0
- package/dist/engine/config/index.js.map +1 -0
- package/dist/engine/gfx/grid.d.ts +51 -0
- package/dist/engine/gfx/grid.d.ts.map +1 -0
- package/dist/engine/gfx/grid.js +116 -0
- package/dist/engine/gfx/grid.js.map +1 -0
- package/dist/engine/gfx/index.d.ts +17 -0
- package/dist/engine/gfx/index.d.ts.map +1 -0
- package/dist/engine/gfx/index.js +17 -0
- package/dist/engine/gfx/index.js.map +1 -0
- package/dist/engine/gfx/label.d.ts +34 -0
- package/dist/engine/gfx/label.d.ts.map +1 -0
- package/dist/engine/gfx/label.js +190 -0
- package/dist/engine/gfx/label.js.map +1 -0
- package/dist/engine/gfx/overlap.d.ts +42 -0
- package/dist/engine/gfx/overlap.d.ts.map +1 -0
- package/dist/engine/gfx/overlap.js +57 -0
- package/dist/engine/gfx/overlap.js.map +1 -0
- package/dist/engine/gfx/palette.d.ts +29 -0
- package/dist/engine/gfx/palette.d.ts.map +1 -0
- package/dist/engine/gfx/palette.js +46 -0
- package/dist/engine/gfx/palette.js.map +1 -0
- package/dist/engine/gfx/png.d.ts +31 -0
- package/dist/engine/gfx/png.d.ts.map +1 -0
- package/dist/engine/gfx/png.js +165 -0
- package/dist/engine/gfx/png.js.map +1 -0
- package/dist/engine/gfx/render.d.ts +42 -0
- package/dist/engine/gfx/render.d.ts.map +1 -0
- package/dist/engine/gfx/render.js +72 -0
- package/dist/engine/gfx/render.js.map +1 -0
- package/dist/engine/index.d.ts +22 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +22 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/pico8/adapter.d.ts +267 -0
- package/dist/engine/pico8/adapter.d.ts.map +1 -0
- package/dist/engine/pico8/adapter.js +37 -0
- package/dist/engine/pico8/adapter.js.map +1 -0
- package/dist/engine/pico8/drive.d.ts +308 -0
- package/dist/engine/pico8/drive.d.ts.map +1 -0
- package/dist/engine/pico8/drive.js +581 -0
- package/dist/engine/pico8/drive.js.map +1 -0
- package/dist/engine/pico8/harness.d.ts +146 -0
- package/dist/engine/pico8/harness.d.ts.map +1 -0
- package/dist/engine/pico8/harness.js +251 -0
- package/dist/engine/pico8/harness.js.map +1 -0
- package/dist/engine/pico8/index.d.ts +21 -0
- package/dist/engine/pico8/index.d.ts.map +1 -0
- package/dist/engine/pico8/index.js +21 -0
- package/dist/engine/pico8/index.js.map +1 -0
- package/dist/engine/pico8/sentinel.d.ts +63 -0
- package/dist/engine/pico8/sentinel.d.ts.map +1 -0
- package/dist/engine/pico8/sentinel.js +86 -0
- package/dist/engine/pico8/sentinel.js.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.js +11 -0
- package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
- package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
- package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-main.js +71 -0
- package/dist/engine/pico8/session-daemon-main.js.map +1 -0
- package/dist/engine/pico8/session-daemon.d.ts +68 -0
- package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon.js +264 -0
- package/dist/engine/pico8/session-daemon.js.map +1 -0
- package/dist/engine/pico8/session.d.ts +140 -0
- package/dist/engine/pico8/session.d.ts.map +1 -0
- package/dist/engine/pico8/session.js +250 -0
- package/dist/engine/pico8/session.js.map +1 -0
- package/dist/engine/pico8/shell.d.ts +103 -0
- package/dist/engine/pico8/shell.d.ts.map +1 -0
- package/dist/engine/pico8/shell.js +324 -0
- package/dist/engine/pico8/shell.js.map +1 -0
- package/dist/engine/pico8/supervisor.d.ts +137 -0
- package/dist/engine/pico8/supervisor.d.ts.map +1 -0
- package/dist/engine/pico8/supervisor.js +140 -0
- package/dist/engine/pico8/supervisor.js.map +1 -0
- package/dist/engine/scaffold/index.d.ts +8 -0
- package/dist/engine/scaffold/index.d.ts.map +1 -0
- package/dist/engine/scaffold/index.js +8 -0
- package/dist/engine/scaffold/index.js.map +1 -0
- package/dist/engine/scaffold/scaffold.d.ts +49 -0
- package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
- package/dist/engine/scaffold/scaffold.js +259 -0
- package/dist/engine/scaffold/scaffold.js.map +1 -0
- package/dist/engine/shrinko/adapter.d.ts +167 -0
- package/dist/engine/shrinko/adapter.d.ts.map +1 -0
- package/dist/engine/shrinko/adapter.js +107 -0
- package/dist/engine/shrinko/adapter.js.map +1 -0
- package/dist/engine/shrinko/index.d.ts +13 -0
- package/dist/engine/shrinko/index.d.ts.map +1 -0
- package/dist/engine/shrinko/index.js +13 -0
- package/dist/engine/shrinko/index.js.map +1 -0
- package/dist/engine/shrinko/shell.d.ts +69 -0
- package/dist/engine/shrinko/shell.d.ts.map +1 -0
- package/dist/engine/shrinko/shell.js +156 -0
- package/dist/engine/shrinko/shell.js.map +1 -0
- package/dist/engine/skills/index.d.ts +8 -0
- package/dist/engine/skills/index.d.ts.map +1 -0
- package/dist/engine/skills/index.js +8 -0
- package/dist/engine/skills/index.js.map +1 -0
- package/dist/engine/skills/skills.d.ts +104 -0
- package/dist/engine/skills/skills.d.ts.map +1 -0
- package/dist/engine/skills/skills.js +149 -0
- package/dist/engine/skills/skills.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -2
- package/src/bin.ts +4 -0
- package/src/cli.ts +61 -0
- package/src/commands/audio.test.ts +280 -0
- package/src/commands/audio.ts +410 -0
- package/src/commands/export.test.ts +310 -0
- package/src/commands/export.ts +316 -0
- package/src/commands/gfx-io.test.ts +178 -0
- package/src/commands/gfx.test.ts +470 -0
- package/src/commands/gfx.ts +645 -0
- package/src/commands/init.test.ts +482 -0
- package/src/commands/init.ts +193 -0
- package/src/commands/lint.test.ts +137 -0
- package/src/commands/lint.ts +117 -0
- package/src/commands/minify.test.ts +166 -0
- package/src/commands/minify.ts +156 -0
- package/src/commands/music.test.ts +272 -0
- package/src/commands/music.ts +288 -0
- package/src/commands/playtest.test.ts +317 -0
- package/src/commands/playtest.ts +773 -0
- package/src/commands/run.test.ts +264 -0
- package/src/commands/run.ts +285 -0
- package/src/commands/serve.test.ts +227 -0
- package/src/commands/serve.ts +272 -0
- package/src/commands/sfx.test.ts +349 -0
- package/src/commands/sfx.ts +300 -0
- package/src/commands/tokens.test.ts +212 -0
- package/src/commands/tokens.ts +168 -0
- package/src/commands/verify.test.ts +238 -0
- package/src/commands/verify.ts +256 -0
- package/src/commands/version.test.ts +70 -0
- package/src/commands/version.ts +24 -0
- package/src/engine/audio/index.ts +47 -0
- package/src/engine/audio/music.test.ts +188 -0
- package/src/engine/audio/music.ts +218 -0
- package/src/engine/audio/section.test.ts +124 -0
- package/src/engine/audio/section.ts +79 -0
- package/src/engine/audio/sfx.test.ts +410 -0
- package/src/engine/audio/sfx.ts +694 -0
- package/src/engine/cart/cart.test.ts +304 -0
- package/src/engine/cart/cart.ts +211 -0
- package/src/engine/cart/errors.ts +39 -0
- package/src/engine/cart/fixtures/crlf.p8 +6 -0
- package/src/engine/cart/fixtures/full.p8 +174 -0
- package/src/engine/cart/fixtures/header-only.p8 +2 -0
- package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
- package/src/engine/cart/fixtures/minimal.p8 +10 -0
- package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
- package/src/engine/cart/gfx.ts +195 -0
- package/src/engine/cart/index.ts +27 -0
- package/src/engine/cart/map.ts +134 -0
- package/src/engine/config/config.ts +115 -0
- package/src/engine/config/index.ts +12 -0
- package/src/engine/gfx/grid.test.ts +128 -0
- package/src/engine/gfx/grid.ts +136 -0
- package/src/engine/gfx/index.ts +34 -0
- package/src/engine/gfx/label.test.ts +105 -0
- package/src/engine/gfx/label.ts +229 -0
- package/src/engine/gfx/overlap.test.ts +71 -0
- package/src/engine/gfx/overlap.ts +66 -0
- package/src/engine/gfx/palette.test.ts +35 -0
- package/src/engine/gfx/palette.ts +54 -0
- package/src/engine/gfx/png.test.ts +111 -0
- package/src/engine/gfx/png.ts +196 -0
- package/src/engine/gfx/render.test.ts +180 -0
- package/src/engine/gfx/render.ts +106 -0
- package/src/engine/index.ts +21 -0
- package/src/engine/pico8/adapter.ts +303 -0
- package/src/engine/pico8/drive.test.ts +415 -0
- package/src/engine/pico8/drive.ts +729 -0
- package/src/engine/pico8/harness.test.ts +181 -0
- package/src/engine/pico8/harness.ts +354 -0
- package/src/engine/pico8/index.ts +20 -0
- package/src/engine/pico8/sentinel.test.ts +80 -0
- package/src/engine/pico8/sentinel.ts +93 -0
- package/src/engine/pico8/session-daemon-entry.ts +12 -0
- package/src/engine/pico8/session-daemon-main.ts +98 -0
- package/src/engine/pico8/session-daemon.test.ts +212 -0
- package/src/engine/pico8/session-daemon.ts +306 -0
- package/src/engine/pico8/session.test.ts +239 -0
- package/src/engine/pico8/session.ts +321 -0
- package/src/engine/pico8/shell.test.ts +583 -0
- package/src/engine/pico8/shell.ts +412 -0
- package/src/engine/pico8/supervisor.test.ts +105 -0
- package/src/engine/pico8/supervisor.ts +197 -0
- package/src/engine/scaffold/index.ts +15 -0
- package/src/engine/scaffold/scaffold.ts +278 -0
- package/src/engine/shrinko/adapter.ts +241 -0
- package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
- package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
- package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
- package/src/engine/shrinko/index.ts +33 -0
- package/src/engine/shrinko/shell.ts +224 -0
- package/src/engine/shrinko/shrinko.test.ts +182 -0
- package/src/engine/skills/index.ts +14 -0
- package/src/engine/skills/skills.test.ts +376 -0
- package/src/engine/skills/skills.ts +205 -0
- package/src/index.ts +13 -0
- package/src/skills/game-design-reference/SKILL.md +70 -0
- package/src/skills/game-jam/SKILL.md +45 -0
- package/src/skills/picopilot-art/SKILL.md +62 -0
- package/src/skills/picopilot-audio/SKILL.md +194 -0
- package/src/skills/picopilot-code/SKILL.md +97 -0
- package/src/skills/picopilot-code/reference/README.md +42 -0
- package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
- package/src/skills/picopilot-code/reference/platformer.md +119 -0
- package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
- package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
- package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
- package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
- package/src/skills/picopilot-debug/SKILL.md +195 -0
- package/src/skills/picopilot-overview/SKILL.md +52 -0
- package/src/version.ts +22 -0
|
@@ -0,0 +1,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
|
+
});
|