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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playtest SESSION SUPERVISOR (ADR-0011, prd US #6): the daemon that keeps a
|
|
3
|
+
* driven PICO-8 process ALIVE + PAUSED across the agent's separate CLI turns, and
|
|
4
|
+
* the client the per-verb sub-invocations use to address it BY ID.
|
|
5
|
+
*
|
|
6
|
+
* The problem this solves: `playtest step <id>` / `input <id>` / `shot <id>` are
|
|
7
|
+
* SEPARATE process invocations, but the driven PICO-8 must survive between them
|
|
8
|
+
* (staying alive + paused). A one-shot CLI process cannot hold another process's
|
|
9
|
+
* pipe, so `start` spawns a small DETACHED daemon that owns the live process + its
|
|
10
|
+
* stdin/stdout (via {@link DriveSession}) and listens on a per-session Unix domain
|
|
11
|
+
* socket; each later verb CONNECTS to that socket, sends its request, and gets the
|
|
12
|
+
* structured result back. The session is addressed by an id = the socket + a
|
|
13
|
+
* session dir under a controlled temp base (never `~/Desktop`, never the carts
|
|
14
|
+
* root).
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle robustness (US #6 acceptance):
|
|
17
|
+
* - ORPHAN reaping: the daemon self-reaps after an IDLE timeout (no verb within
|
|
18
|
+
* the window), so a session whose agent never `stop`s it does not leak a live
|
|
19
|
+
* PICO-8 forever. `stop <id>` tears it down promptly.
|
|
20
|
+
* - PICO-8 dying mid-session surfaces as a STRUCTURED error (via
|
|
21
|
+
* {@link SessionError}), not a hang; the next verb reports `process-dead`.
|
|
22
|
+
* - `start` with PICO-8 ABSENT returns the same structured `pico8-not-found`
|
|
23
|
+
* boundary as `run`/`audio render`.
|
|
24
|
+
*
|
|
25
|
+
* ONE session per id (the id IS the socket). Multiple concurrent sessions ARE
|
|
26
|
+
* allowed (distinct ids/dirs/daemons); the tool does not serialize them, an agent
|
|
27
|
+
* that wants isolation uses distinct ids (the default id is random, so parallel
|
|
28
|
+
* `start`s never collide).
|
|
29
|
+
*
|
|
30
|
+
* The pure, CI-testable pieces (the id/path registry, the request/response codec)
|
|
31
|
+
* live here alongside the IO daemon; the live daemon end-to-end is the
|
|
32
|
+
* manual/opt-in tier. The handshake core it drives ({@link DriveSession}) is
|
|
33
|
+
* separately unit-tested against a fake process.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import {randomBytes} from 'node:crypto';
|
|
37
|
+
import {existsSync, mkdirSync, readdirSync, rmSync, statSync} from 'node:fs';
|
|
38
|
+
import {tmpdir} from 'node:os';
|
|
39
|
+
import {join} from 'node:path';
|
|
40
|
+
|
|
41
|
+
/** The controlled base dir all session dirs live under (never `~/Desktop`). */
|
|
42
|
+
export const SESSION_BASE_DIRNAME = 'picopilot-playtest-sessions' as const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The default IDLE-reap window: a daemon with no verb for this long tears its
|
|
46
|
+
* session (and the live PICO-8) down, so an orphaned session (the agent walked
|
|
47
|
+
* away without `stop`) cannot leak a process indefinitely. Every verb resets it.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60_000;
|
|
50
|
+
|
|
51
|
+
/** The session verbs a client can send the daemon over the socket. */
|
|
52
|
+
export type SessionVerb = 'input' | 'step' | 'shot' | 'stop' | 'status';
|
|
53
|
+
|
|
54
|
+
/** A request sent from a verb sub-invocation to the session daemon. */
|
|
55
|
+
export type SessionRequest =
|
|
56
|
+
| {readonly verb: 'input'; readonly held: number}
|
|
57
|
+
| {readonly verb: 'step'; readonly frames: number}
|
|
58
|
+
| {readonly verb: 'shot'}
|
|
59
|
+
| {readonly verb: 'stop'}
|
|
60
|
+
| {readonly verb: 'status'};
|
|
61
|
+
|
|
62
|
+
/** A structured daemon response: either an ok payload or a structured error. */
|
|
63
|
+
export type SessionResponse =
|
|
64
|
+
| {readonly ok: true; readonly value: SessionResponseValue}
|
|
65
|
+
| {readonly ok: false; readonly code: string; readonly message: string};
|
|
66
|
+
|
|
67
|
+
/** The ok payload shape (a superset; each verb fills the relevant fields). */
|
|
68
|
+
export interface SessionResponseValue {
|
|
69
|
+
/** The session id. */
|
|
70
|
+
readonly id: string;
|
|
71
|
+
/** Total frames advanced so far. */
|
|
72
|
+
readonly frame: number;
|
|
73
|
+
/** The ACK line the cart printed for this command (settled-frame proof). */
|
|
74
|
+
readonly ack?: string;
|
|
75
|
+
/** For `shot`: the absolute screenshot path the cart just wrote. */
|
|
76
|
+
readonly screenshot?: string;
|
|
77
|
+
/** For `shot`: the basename the cart named the capture. */
|
|
78
|
+
readonly shotName?: string;
|
|
79
|
+
/** Whether the session is still alive after this verb. */
|
|
80
|
+
readonly alive: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Encodes a request/response as a single newline-terminated JSON frame (the
|
|
85
|
+
* socket protocol). Newline-framed so a reader can split a stream into whole
|
|
86
|
+
* messages; kept pure so the codec is unit-tested without a socket.
|
|
87
|
+
*/
|
|
88
|
+
export function encodeFrame(msg: SessionRequest | SessionResponse): string {
|
|
89
|
+
return `${JSON.stringify(msg)}\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A stateful frame decoder mirroring {@link encodeFrame}: feed it raw socket
|
|
94
|
+
* chunks, it yields whole parsed messages (buffering partial lines). Typed loose
|
|
95
|
+
* (`T`) because a client decodes responses and the daemon decodes requests.
|
|
96
|
+
*/
|
|
97
|
+
export class FrameDecoder<T> {
|
|
98
|
+
private buffer = '';
|
|
99
|
+
push(chunk: string): T[] {
|
|
100
|
+
this.buffer += chunk;
|
|
101
|
+
const out: T[] = [];
|
|
102
|
+
let nl: number;
|
|
103
|
+
while ((nl = this.buffer.indexOf('\n')) !== -1) {
|
|
104
|
+
const line = this.buffer.slice(0, nl);
|
|
105
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
106
|
+
if (line.trim().length > 0) out.push(JSON.parse(line) as T);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A resolved session's on-disk layout (all under the controlled base). */
|
|
113
|
+
export interface SessionPaths {
|
|
114
|
+
/** The session id (also the dir name under the base). */
|
|
115
|
+
readonly id: string;
|
|
116
|
+
/** The session dir: `<base>/<id>`. */
|
|
117
|
+
readonly dir: string;
|
|
118
|
+
/** The Unix domain socket the daemon listens on. */
|
|
119
|
+
readonly socket: string;
|
|
120
|
+
/** The `-desktop` dir PICO-8 writes SHOT screenshots into. */
|
|
121
|
+
readonly shotDir: string;
|
|
122
|
+
/** The pidfile written by the daemon (for reap/liveness checks). */
|
|
123
|
+
readonly pidfile: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The session REGISTRY: the pure id/path logic mapping a session id to its
|
|
128
|
+
* on-disk layout under a controlled base dir. No IO beyond the base-dir join, so
|
|
129
|
+
* the id validation + path derivation are unit-tested. The daemon and the client
|
|
130
|
+
* both resolve a session THROUGH this, so they agree on where the socket is.
|
|
131
|
+
*/
|
|
132
|
+
export class SessionRegistry {
|
|
133
|
+
readonly base: string;
|
|
134
|
+
|
|
135
|
+
constructor(base?: string) {
|
|
136
|
+
this.base = base ?? join(tmpdir(), SESSION_BASE_DIRNAME);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A fresh random session id (short, filesystem-safe, collision-free enough). */
|
|
140
|
+
static freshId(): string {
|
|
141
|
+
return `s-${randomBytes(6).toString('hex')}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validates a caller-supplied id: filesystem-safe, no path traversal, so an id
|
|
146
|
+
* can never escape the controlled base. Returns the id or throws a plain Error
|
|
147
|
+
* (the command maps it to a structured refusal).
|
|
148
|
+
*/
|
|
149
|
+
static validateId(id: string): string {
|
|
150
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(id) || id === '.' || id === '..') {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`invalid session id "${id}": use 1-64 chars of [A-Za-z0-9._-]`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return id;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Resolves a session id to its {@link SessionPaths} under the base. */
|
|
159
|
+
resolve(id: string): SessionPaths {
|
|
160
|
+
SessionRegistry.validateId(id);
|
|
161
|
+
const dir = join(this.base, id);
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
dir,
|
|
165
|
+
socket: join(dir, 'sock'),
|
|
166
|
+
shotDir: join(dir, 'shots'),
|
|
167
|
+
pidfile: join(dir, 'pid'),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Creates the session dir + shot dir (idempotent), returning the paths. */
|
|
172
|
+
create(id: string): SessionPaths {
|
|
173
|
+
const paths = this.resolve(id);
|
|
174
|
+
mkdirSync(paths.shotDir, {recursive: true});
|
|
175
|
+
return paths;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The ids of all sessions with a dir present under the base (for listing/reap). */
|
|
179
|
+
list(): string[] {
|
|
180
|
+
if (!existsSync(this.base)) return [];
|
|
181
|
+
return readdirSync(this.base)
|
|
182
|
+
.filter((name) => {
|
|
183
|
+
try {
|
|
184
|
+
return statSync(join(this.base, name)).isDirectory();
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
.sort();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Removes a session's whole dir (the reap/cleanup, after the daemon exits). */
|
|
193
|
+
remove(id: string): void {
|
|
194
|
+
const {dir} = this.resolve(id);
|
|
195
|
+
rmSync(dir, {recursive: true, force: true});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The static content `picopilot init` scaffolds: the `#include`-discipline
|
|
3
|
+
* `main.p8` (built through the cart model), the editable `main.lua`, the curated
|
|
4
|
+
* `AGENTS.md` PICO-8 reference, and the `picopilot.json` config. See
|
|
5
|
+
* `scaffold.ts` for the single scaffolding model and why these are the files.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
agentsMd,
|
|
9
|
+
LUA_INCLUDE_LINE,
|
|
10
|
+
mainLua,
|
|
11
|
+
mainP8,
|
|
12
|
+
picopilotJson,
|
|
13
|
+
type ScaffoldFile,
|
|
14
|
+
scaffoldFiles,
|
|
15
|
+
} from './scaffold.js';
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {Cart} from '../cart/index.js';
|
|
2
|
+
import {defaultConfigFile} from '../config/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The static content `picopilot init` writes into a fresh cart folder.
|
|
6
|
+
*
|
|
7
|
+
* The single scaffolding model (US #1): the `.p8`'s `__lua__` section is exactly
|
|
8
|
+
* `#include main.lua`, and the plain-Lua `main.lua` is the file the agent edits,
|
|
9
|
+
* so the agent never hand-writes the cart's binary hex sections. `main.p8` is
|
|
10
|
+
* built THROUGH the cart model (not a hand-written string) so init and every
|
|
11
|
+
* later cart-touching command share one serializer and the output is guaranteed
|
|
12
|
+
* to round-trip.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The PICO-8 cart format version init emits (the current stable text-format version). */
|
|
16
|
+
const CART_VERSION = 42;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The one line of code in the scaffolded cart. PICO-8's `#include` pulls the
|
|
20
|
+
* external Lua file in at load time; keeping it the ONLY thing in `__lua__` is
|
|
21
|
+
* the `#include` discipline the whole toolchain relies on.
|
|
22
|
+
*/
|
|
23
|
+
export const LUA_INCLUDE_LINE = '#include main.lua';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds the scaffolded `main.p8` via the cart model. `__lua__` holds only the
|
|
27
|
+
* `#include`; the cart carries no binary sections yet (a fresh cart has no gfx /
|
|
28
|
+
* map / sfx / music); those are added later by the gfx/audio commands.
|
|
29
|
+
*/
|
|
30
|
+
export function mainP8(): string {
|
|
31
|
+
const cart = Cart.parse(
|
|
32
|
+
`pico-8 cartridge // http://www.pico-8.com\nversion ${CART_VERSION}\n__lua__\n`,
|
|
33
|
+
);
|
|
34
|
+
cart.setSection('lua', `${LUA_INCLUDE_LINE}\n`);
|
|
35
|
+
return cart.serialize();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The starter `main.lua`: a minimal but RUNNABLE cart (the `_init/_update/_draw`
|
|
40
|
+
* loop the API reference calls out as load-bearing) so a fresh folder boots
|
|
41
|
+
* instead of being inert. Intentionally tiny: it is a starting point the agent
|
|
42
|
+
* replaces, not a demo.
|
|
43
|
+
*/
|
|
44
|
+
export function mainLua(): string {
|
|
45
|
+
return `-- main.lua, the plain-Lua source picopilot's main.p8 #includes.
|
|
46
|
+
-- Edit THIS file; never hand-write main.p8's binary sections.
|
|
47
|
+
|
|
48
|
+
function _init()
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
function _update()
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
function _draw()
|
|
55
|
+
cls()
|
|
56
|
+
print("hello picopilot", 36, 60, 7)
|
|
57
|
+
end
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The scaffolded `picopilot.json` (incur config: precedence argv > config > defaults). */
|
|
62
|
+
export function picopilotJson(): string {
|
|
63
|
+
return defaultConfigFile();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The curated PICO-8 reference written to `AGENTS.md`.
|
|
68
|
+
*
|
|
69
|
+
* This is CONTEXT an agent loads every turn, so it is CURATED from
|
|
70
|
+
* `work/notes/findings/pico8-api-reference.md`, not a dump of the manual: the
|
|
71
|
+
* hard specs, the `_init/_update/_draw` loop, the fixed palette, the memory map
|
|
72
|
+
* + the gfx/map overlap WARNING, the grouped most-used API, and the token
|
|
73
|
+
* discipline, the things models get wrong without a reference.
|
|
74
|
+
*/
|
|
75
|
+
export function agentsMd(): string {
|
|
76
|
+
return `# PICO-8 cart context (generated by \`picopilot init\`)
|
|
77
|
+
|
|
78
|
+
You are editing a PICO-8 cart with picopilot. This file is your always-loaded
|
|
79
|
+
reference: the constraints, the loop, the palette, the memory map (+ the overlap
|
|
80
|
+
warning), the most-used API, and how to stay under the token budget. It is
|
|
81
|
+
curated from PICO-8's manual for the things models get wrong without a reminder.
|
|
82
|
+
|
|
83
|
+
## The \`#include\` discipline (how this cart is laid out)
|
|
84
|
+
|
|
85
|
+
- \`main.p8\` is the cart. Its \`__lua__\` section is a single line: \`#include main.lua\`.
|
|
86
|
+
- \`main.lua\` is the plain Lua you EDIT. Never hand-write \`main.p8\`'s binary
|
|
87
|
+
sections (\`__gfx__\`, \`__map__\`, \`__sfx__\`, \`__music__\`), use picopilot's
|
|
88
|
+
\`gfx\` / audio commands, which edit them safely and round-trippably.
|
|
89
|
+
|
|
90
|
+
## Hard specs (the constraints that shape everything)
|
|
91
|
+
|
|
92
|
+
- Display: **128x128**, a **fixed 16-colour palette** (indices 0..15).
|
|
93
|
+
- Code: **Lua**, max **8192 tokens** (the #1 thing to watch). Run \`picopilot tokens\`.
|
|
94
|
+
- Sprites: 256 8x8 sprites in a base bank (0..127) plus a shared bank (128..255).
|
|
95
|
+
- Map: 128x32 tiles + a shared half that ALIASES sprites 128..255 (see below).
|
|
96
|
+
- Sound: 4 channels, 64 SFX slots; music is patterns of up to 4 SFX references.
|
|
97
|
+
- Numbers: 16.16 fixed point, range \`-32768.0 .. 32767.99\`.
|
|
98
|
+
|
|
99
|
+
## The execution loop (every cart)
|
|
100
|
+
|
|
101
|
+
- \`_init()\`: runs once at startup.
|
|
102
|
+
- \`_update()\`: runs every update tick at 30fps (\`_update60()\` for 60fps).
|
|
103
|
+
- \`_draw()\`: runs once per visible frame.
|
|
104
|
+
|
|
105
|
+
A cart with none of these is inert. Keep them defined.
|
|
106
|
+
|
|
107
|
+
## The build loop (do this every iteration, and to finish)
|
|
108
|
+
|
|
109
|
+
One change = one pass through this loop. Do NOT skip the last step:
|
|
110
|
+
|
|
111
|
+
1. Edit \`main.lua\` (or a cart section via a picopilot command).
|
|
112
|
+
2. \`picopilot tokens\` to watch the 8192-token budget.
|
|
113
|
+
3. \`picopilot verify\` (the static gate: tokens + integrity). GREEN means
|
|
114
|
+
well-formed, it does NOT mean the cart runs or plays.
|
|
115
|
+
4. **SEE it run.** Add a temporary probe to \`main.lua\` and \`picopilot run\`, then
|
|
116
|
+
LOOK at the screenshot PNG. Remove the probe after. Or use
|
|
117
|
+
\`picopilot playtest run\` to drive input and screenshot live gameplay with no
|
|
118
|
+
cart edit. Probe recipe:
|
|
119
|
+
|
|
120
|
+
\`\`\`lua
|
|
121
|
+
-- TEMPORARY: screenshot a couple of frames, then signal done so run exits.
|
|
122
|
+
if t==20 then extcmd("set_filename","probe_0") extcmd("screen") end
|
|
123
|
+
if t==40 then extcmd("set_filename","probe_1") extcmd("screen") end
|
|
124
|
+
if t==45 then printh("__PICOPILOT_DONE__") end
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
**Definition of done: you have looked at a screenshot of the running cart and it
|
|
128
|
+
renders what you intended.** A green \`verify\` is NOT done. A \`picopilot run\`
|
|
129
|
+
that ends in \`exitReason: timeout\` with NO screenshot is NOT proof it works, it
|
|
130
|
+
just means the cart never self-quit; you still have not seen it. Never declare a
|
|
131
|
+
game finished on static checks or a bare timeout alone.
|
|
132
|
+
|
|
133
|
+
## Handing the finished cart to the user
|
|
134
|
+
|
|
135
|
+
When it is verified and looks right, tell the user to play it with:
|
|
136
|
+
|
|
137
|
+
\`\`\`sh
|
|
138
|
+
pico8 -run /absolute/path/to/main.p8
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
(or \`picopilot run\` for a headless screenshot pass). Do NOT reach for
|
|
142
|
+
\`picopilot serve\` to prove your own build: that exports + serves a browser build
|
|
143
|
+
for SHARING a playable cart with a human, needs a cart \`__label__\`, and puts a
|
|
144
|
+
human-in-the-loop where none is needed. Verify with \`run\`/\`playtest\`; serve only
|
|
145
|
+
if the user explicitly wants a browser build. And do NOT emit extra deliverables
|
|
146
|
+
(summary docs, play-guides, shell scripts, ASCII banners): the deliverable is the
|
|
147
|
+
verified cart itself.
|
|
148
|
+
|
|
149
|
+
## The fixed 16-colour palette (index → RGB)
|
|
150
|
+
|
|
151
|
+
In a char grid, \`.\` = transparent and \`0-F\` = these indices.
|
|
152
|
+
|
|
153
|
+
| idx | hex | name | R | G | B |
|
|
154
|
+
|-----|-----|-------------|-----|-----|-----|
|
|
155
|
+
| 0 | 0 | black | 0 | 0 | 0 |
|
|
156
|
+
| 1 | 1 | dark-blue | 29 | 43 | 83 |
|
|
157
|
+
| 2 | 2 | dark-purple | 126 | 37 | 83 |
|
|
158
|
+
| 3 | 3 | dark-green | 0 | 135 | 81 |
|
|
159
|
+
| 4 | 4 | brown | 171 | 82 | 54 |
|
|
160
|
+
| 5 | 5 | dark-grey | 95 | 87 | 79 |
|
|
161
|
+
| 6 | 6 | light-grey | 194 | 195 | 199 |
|
|
162
|
+
| 7 | 7 | white | 255 | 241 | 232 |
|
|
163
|
+
| 8 | 8 | red | 255 | 0 | 77 |
|
|
164
|
+
| 9 | 9 | orange | 255 | 163 | 0 |
|
|
165
|
+
| 10 | a | yellow | 255 | 236 | 39 |
|
|
166
|
+
| 11 | b | green | 0 | 228 | 54 |
|
|
167
|
+
| 12 | c | blue | 41 | 173 | 255 |
|
|
168
|
+
| 13 | d | lavender | 131 | 118 | 156 |
|
|
169
|
+
| 14 | e | pink | 255 | 119 | 168 |
|
|
170
|
+
| 15 | f | peach | 255 | 204 | 170 |
|
|
171
|
+
|
|
172
|
+
## Memory map + the gfx/map overlap warning
|
|
173
|
+
|
|
174
|
+
\`\`\`
|
|
175
|
+
0x0000 gfx (spritesheet, sprites 0..127)
|
|
176
|
+
0x1000 shared gfx / map2 <-- sprites 128..255 ALIAS the bottom half of the map
|
|
177
|
+
0x2000 map
|
|
178
|
+
0x3000 gfx flags
|
|
179
|
+
0x3100 music
|
|
180
|
+
0x3200 sfx
|
|
181
|
+
0x4300 user data
|
|
182
|
+
0x5e00 persistent cart data (256 bytes)
|
|
183
|
+
0x5f00 draw state
|
|
184
|
+
0x5f40 hardware state
|
|
185
|
+
0x5f80 gpio pins
|
|
186
|
+
0x6000 screen (8k)
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
**Overlap warning:** sprites 128..255 (\`0x1000\`-\`0x1fff\`) occupy the SAME memory
|
|
190
|
+
as \`__map__\` rows 32..63. Writing a sprite in that range clobbers the map tiles
|
|
191
|
+
there. \`picopilot gfx set\` smart-refuses such a write when real map tiles are at
|
|
192
|
+
risk and nothing authorised the loss (\`--allow-map-overlap\` or \`allowMapOverlap\`
|
|
193
|
+
in \`picopilot.json\`), so you never silently destroy map data you cannot see.
|
|
194
|
+
|
|
195
|
+
## Most-used API (grouped)
|
|
196
|
+
|
|
197
|
+
- **Graphics:** \`cls([col])\`, \`spr(n,x,y,[w,h],[fx],[fy])\`, \`sspr(...)\`,
|
|
198
|
+
\`map(cx,cy,sx,sy,cw,ch,[layer])\`, \`pset/pget\`, \`sget/sset\`,
|
|
199
|
+
\`print(str,[x,y,[col]])\`, \`rect/rectfill\`, \`circ/circfill\`, \`oval/ovalfill\`,
|
|
200
|
+
\`line\`, \`pal(c0,c1,[p])\`, \`palt(col,t)\`, \`camera([x,y])\`, \`clip([x,y,w,h])\`,
|
|
201
|
+
\`color(col)\`, \`fillp(mask)\`, \`tline(...)\`, \`flip()\`.
|
|
202
|
+
- **Map / flags:** \`mget(x,y)\`, \`mset(x,y,v)\`, \`fget(n,[f])\`, \`fset(n,[f],v)\`.
|
|
203
|
+
- **Input:** \`btn([i,[p]])\`, \`btnp([i,[p]])\` (buttons 0..5 = left,right,up,down,O/Z,X).
|
|
204
|
+
- **Audio:** \`sfx(n,[channel,[offset]])\`, \`music([n,[fade,[mask]]])\`.
|
|
205
|
+
- **Math:** \`abs\`, \`flr\` (\`-flr(-x)\` = ceil), \`min\`, \`max\`, \`mid\`, \`rnd(x)\`,
|
|
206
|
+
\`srand(x)\`, \`sgn\`, \`sqrt\`, \`sin(x)\`/\`cos(x)\` (range 0..1, sin inverted),
|
|
207
|
+
\`atan2(x,y)\`.
|
|
208
|
+
- **Tables:** \`add\`, \`del\`, \`deli\`, \`count\`, \`all(t)\`, \`foreach(t,f)\`, \`pairs\`, \`ipairs\`.
|
|
209
|
+
- **Memory:** \`peek\`, \`poke\`, \`memcpy\`, \`memset\`, \`cstore\`, \`reload\`.
|
|
210
|
+
- **Cart data:** \`cartdata(id)\`, \`dget(i)\`, \`dset(i,v)\`.
|
|
211
|
+
- **Debug:** \`printh(str)\` (to host stdout, \`picopilot run\` captures it), \`stat(n)\`.
|
|
212
|
+
|
|
213
|
+
## Gotchas that silently break carts (these differ from normal Lua/math)
|
|
214
|
+
|
|
215
|
+
These compile and run but behave WRONG. They are the traps models hit most:
|
|
216
|
+
|
|
217
|
+
- \`sin(t)\`/\`cos(t)\` take TURNS (0..1), NOT radians, and \`sin\` is INVERTED for
|
|
218
|
+
screen space (+y is down). A full circle is \`t: 0->1\`. Do not multiply by
|
|
219
|
+
\`2*pi\`; do not re-negate \`sin\`.
|
|
220
|
+
- \`/\` does NOT truncate (\`5/2\`=\`2.5\`). Integer divide is \`\\\` (\`5\\2\`=\`2\`), which
|
|
221
|
+
floors toward negative infinity (\`-5\\2\`=\`-3\`).
|
|
222
|
+
- \`rnd(x)\` returns a FLOAT in [0,x). Use \`flr(rnd(x))\` for an integer;
|
|
223
|
+
\`rnd(table)\` returns a random element.
|
|
224
|
+
- Tables are 1-BASED. \`del(t,val)\` removes by VALUE, \`deli(t,i)\` by INDEX.
|
|
225
|
+
Deleting the current item inside \`foreach\` is safe; inside an ascending
|
|
226
|
+
\`for i=1,#t\` it ERRORS once the list shrinks (iterate descending, or foreach).
|
|
227
|
+
- Draw state PERSISTS across frames: \`camera\`, \`pal\`, \`palt\`, \`clip\`, \`color\`,
|
|
228
|
+
\`fillp\` stay set until reset (no-arg form: \`camera()\`, \`pal()\`). Reset before
|
|
229
|
+
the HUD or offsets/palette swaps leak.
|
|
230
|
+
- Numbers are 16.16 fixed point; overflow WRAPS at ~32767. Drawing coords are
|
|
231
|
+
floored to integer pixels (keep true position as a float, let draw floor it).
|
|
232
|
+
- Variables are GLOBAL unless \`local\`. The \`x = cond and a or b\` ternary returns
|
|
233
|
+
\`b\` when \`a\` is falsy (\`false\`/\`nil\`); use an explicit \`if\` then.
|
|
234
|
+
|
|
235
|
+
(The \`picopilot-code\` skill expands these, and carries per-genre code references:
|
|
236
|
+
platformer / puzzle-grid / twin-stick-arcade / top-down-adventure and more.)
|
|
237
|
+
|
|
238
|
+
## Token discipline (stay under 8192)
|
|
239
|
+
|
|
240
|
+
Models blow the budget by writing verbose Lua. Prefer PICO-8 shorthands:
|
|
241
|
+
|
|
242
|
+
- \`?\` for \`print\`.
|
|
243
|
+
- inline if: \`if(c) x=1\`.
|
|
244
|
+
- compound assignment: \`+=\`, \`-=\`, \`*=\`, \`/=\`.
|
|
245
|
+
- \`\\\` for integer divide.
|
|
246
|
+
- reuse locals; avoid needless globals.
|
|
247
|
+
|
|
248
|
+
Run \`picopilot tokens\` each iteration to see the count and where it goes; when
|
|
249
|
+
over budget, \`picopilot minify\` (safe-only) reclaims tokens without changing
|
|
250
|
+
behaviour.
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* One scaffolded file: its cart-relative name and its exact bytes. `init`
|
|
256
|
+
* iterates these, so adding a file is a one-line change here and the tests
|
|
257
|
+
* assert against this list (the "exactly these files" contract).
|
|
258
|
+
*/
|
|
259
|
+
export interface ScaffoldFile {
|
|
260
|
+
readonly name: string;
|
|
261
|
+
readonly content: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The complete set of files `picopilot init` writes into a fresh cart folder,
|
|
266
|
+
* in a stable order: the cart, its editable Lua, the agent-context reference,
|
|
267
|
+
* and the config. Exactly FOUR files (the acceptance contract). `AGENTS.md` is
|
|
268
|
+
* the cross-agent context filename; Claude Code, Codex, and others read it, so
|
|
269
|
+
* one portable file beats duplicating identical content into `CLAUDE.md`.
|
|
270
|
+
*/
|
|
271
|
+
export function scaffoldFiles(): ScaffoldFile[] {
|
|
272
|
+
return [
|
|
273
|
+
{name: 'main.p8', content: mainP8()},
|
|
274
|
+
{name: 'main.lua', content: mainLua()},
|
|
275
|
+
{name: 'AGENTS.md', content: agentsMd()},
|
|
276
|
+
{name: 'picopilot.json', content: picopilotJson()},
|
|
277
|
+
];
|
|
278
|
+
}
|