frogoe 0.3.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ /** The pixel analyzers — shared between Node (unit tests, CLI decisions)
2
+ * and browser pages (raster/vision injection). This file is shipped
3
+ * VERBATIM (never bundled — esbuild renames cross-references and breaks
4
+ * toString() injection; see the bun-vs-esbuild divergence incident).
5
+ * In the page it is injected as-is; in Node it is imported as-is. */
6
+
7
+ // ── luminance / contrast (WCAG) ──────────────────────────────────────────
8
+ const luminance = (r, g, b) => {
9
+ const ch = (c) => {
10
+ const s = c / 255;
11
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
12
+ };
13
+ return 0.2126 * ch(r) + 0.7152 * ch(g) + 0.0722 * ch(b);
14
+ };
15
+
16
+ const contrastRatio = (a, b) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
17
+
18
+ // ── composition metrics (coverage + dead rows) ───────────────────────────
19
+ const compositionMetrics = (data, width, height, rows, palette) => {
20
+ const hex = (palette && palette.bg) || "#000000";
21
+ const pr = parseInt(hex.slice(1, 3), 16);
22
+ const pg = parseInt(hex.slice(3, 5), 16);
23
+ const pb = parseInt(hex.slice(5, 7), 16);
24
+ let ink = 0,
25
+ total = 0,
26
+ deadRows = 0;
27
+ for (let gy = 0; gy < rows; gy++) {
28
+ let rowInk = 0,
29
+ rowTotal = 0;
30
+ const y0 = Math.floor((gy * height) / rows);
31
+ const y1 = Math.floor(((gy + 1) * height) / rows);
32
+ for (let y = y0; y < y1; y += 2) {
33
+ for (let x = 0; x < width; x += 2) {
34
+ const at = (y * width + x) * 4;
35
+ const isBg =
36
+ Math.abs((data[at] || 0) - pr) +
37
+ Math.abs((data[at + 1] || 0) - pg) +
38
+ Math.abs((data[at + 2] || 0) - pb) <
39
+ 60;
40
+ rowTotal++;
41
+ rowInk += isBg ? 0 : 1;
42
+ }
43
+ }
44
+ if (rowTotal > 0 && rowInk / rowTotal < 0.08) deadRows++;
45
+ ink += rowInk;
46
+ total += rowTotal;
47
+ }
48
+ return { coverage: total > 0 ? ink / total : 0, deadRows: deadRows, rows: rows };
49
+ };
50
+
51
+ // ── title-band analyzer (upper field, logotype-scale ink) ────────────────
52
+ const analyzeTitleBand = (data, width, height) => {
53
+ const fieldHeight = Math.floor(height * 0.65);
54
+ const fieldBuckets = new Map();
55
+ for (let y = 0; y < fieldHeight; y += 2) {
56
+ for (let x = 0; x < width; x += 2) {
57
+ const at = (y * width + x) * 4;
58
+ const key =
59
+ (((data[at] || 0) >> 4) << 8) |
60
+ (((data[at + 1] || 0) >> 4) << 4) |
61
+ ((data[at + 2] || 0) >> 4);
62
+ fieldBuckets.set(key, (fieldBuckets.get(key) || 0) + 1);
63
+ }
64
+ }
65
+ let groundKey = 0,
66
+ groundHits = -1;
67
+ for (const [key, hits] of fieldBuckets) {
68
+ if (hits > groundHits) {
69
+ groundHits = hits;
70
+ groundKey = key;
71
+ }
72
+ }
73
+ const ground = [
74
+ ((groundKey >> 8) & 15) * 17,
75
+ ((groundKey >> 4) & 15) * 17,
76
+ (groundKey & 15) * 17,
77
+ ];
78
+ const groundLum = luminance(ground[0], ground[1], ground[2]);
79
+ const isInk = (x, y) => {
80
+ const at = (y * width + x) * 4;
81
+ return (
82
+ contrastRatio(luminance(data[at] || 0, data[at + 1] || 0, data[at + 2] || 0), groundLum) >= 3
83
+ );
84
+ };
85
+ const rowInkMin = Math.ceil((width / 2) * 0.15);
86
+ const wideRowMin = Math.ceil((width / 2) * 0.4);
87
+ let inkCount = 0,
88
+ inkTotal = 0,
89
+ textBottom = -1;
90
+ let bbox = null;
91
+ for (let y = 0; y < fieldHeight; y += 2) {
92
+ let rowInk = 0;
93
+ for (let x = 0; x < width; x += 2) {
94
+ if (isInk(x, y)) rowInk++;
95
+ }
96
+ inkTotal += Math.ceil(width / 2);
97
+ if (rowInk >= wideRowMin) textBottom = y;
98
+ if (rowInk >= rowInkMin) {
99
+ inkCount += Math.ceil(width / 2);
100
+ if (bbox === null) bbox = [width, y, 0, y];
101
+ bbox = [bbox[0], bbox[1], bbox[2], y];
102
+ }
103
+ }
104
+ if (bbox !== null) {
105
+ const bins = new Map();
106
+ for (let y = bbox[1]; y <= bbox[3]; y += 2) {
107
+ for (let x = 0; x < width; x += 2) {
108
+ if (isInk(x, y)) {
109
+ const bin = x >> 2;
110
+ bins.set(bin, (bins.get(bin) || 0) + 1);
111
+ }
112
+ }
113
+ }
114
+ const sorted = [...bins.keys()].sort((a, b) => a - b);
115
+ const GAP_BINS = 6;
116
+ let best = null,
117
+ runFrom = -1,
118
+ runTo = -1,
119
+ runInk = 0;
120
+ for (let i = 0; i <= sorted.length; i++) {
121
+ const bin = sorted[i] !== undefined ? sorted[i] : Number.MAX_SAFE_INTEGER;
122
+ const prev = sorted[i - 1] !== undefined ? sorted[i - 1] : bin;
123
+ const starts = i === 0 || bin - prev > GAP_BINS;
124
+ if (starts && runFrom !== -1) {
125
+ if (best === null || runInk > best[2]) best = [runFrom, runTo, runInk];
126
+ runFrom = -1;
127
+ runInk = 0;
128
+ }
129
+ if (i === sorted.length) break;
130
+ if (runFrom === -1) runFrom = bin;
131
+ runTo = bin;
132
+ runInk += bins.get(bin) || 0;
133
+ }
134
+ if (best !== null) bbox = [best[0] * 4, bbox[1], best[1] * 4 + 2, bbox[3]];
135
+ else bbox = null;
136
+ }
137
+ return {
138
+ bandHeight: fieldHeight,
139
+ ground: ground,
140
+ inkBBox: bbox,
141
+ inkCount: inkCount,
142
+ inkTotal: inkTotal,
143
+ textBottom: textBottom,
144
+ };
145
+ };
146
+
147
+ // ── icon corner analyzer ──────────────────────────────────────────────────
148
+ const analyzeIconCorners = (data, width, _height) => {
149
+ const read = (x, y) => {
150
+ let r = 0,
151
+ g = 0,
152
+ b = 0,
153
+ a = 0;
154
+ for (let dy = 0; dy < 4; dy++) {
155
+ for (let dx = 0; dx < 4; dx++) {
156
+ const at = ((y + dy) * width + (x + dx)) * 4;
157
+ r += data[at] || 0;
158
+ g += data[at + 1] || 0;
159
+ b += data[at + 2] || 0;
160
+ a += data[at + 3] || 0;
161
+ }
162
+ }
163
+ return [Math.round(r / 16), Math.round(g / 16), Math.round(b / 16), Math.round(a / 16)];
164
+ };
165
+ const last = width - 4;
166
+ return { corners: [read(0, 0), read(last, 0), read(0, last), read(last, last)] };
167
+ };
168
+
169
+ // ── safe-zone collision (declared band clearance) ─────────────────────────
170
+ const findTitleZoneCollision = (data, width, height, band) => {
171
+ if (band === null || band === undefined) return null;
172
+ const x0 = Math.max(0, Math.round(band[0]));
173
+ const y0 = Math.max(0, Math.round(band[1]));
174
+ const x1 = Math.min(width - 1, Math.round(band[2]));
175
+ const y1 = Math.min(height - 1, Math.round(band[3]));
176
+ const lumOf = (x, y) => {
177
+ const at = (y * width + x) * 4;
178
+ return luminance(data[at] || 0, data[at + 1] || 0, data[at + 2] || 0);
179
+ };
180
+ const buckets = new Map();
181
+ for (let y = y0; y <= y1; y += 2) {
182
+ for (let x = 0; x < width; x += 2) {
183
+ const at = (y * width + x) * 4;
184
+ const key =
185
+ (((data[at] || 0) >> 4) << 8) |
186
+ (((data[at + 1] || 0) >> 4) << 4) |
187
+ ((data[at + 2] || 0) >> 4);
188
+ buckets.set(key, (buckets.get(key) || 0) + 1);
189
+ }
190
+ }
191
+ let gk = 0,
192
+ gh = -1;
193
+ for (const [k, h] of buckets) {
194
+ if (h > gh) {
195
+ gh = h;
196
+ gk = k;
197
+ }
198
+ }
199
+ const groundLum = luminance(((gk >> 8) & 15) * 17, ((gk >> 4) & 15) * 17, (gk & 15) * 17);
200
+ const isLnk = (x, y) => contrastRatio(lumOf(x, y), groundLum) >= 3;
201
+ const ceiling = [];
202
+ for (let x = 0; x < width; x += 4) {
203
+ for (let y = 0; y < 6; y += 2) {
204
+ if (isLnk(x, y)) {
205
+ ceiling.push(x);
206
+ break;
207
+ }
208
+ }
209
+ }
210
+ const nearCeiling = (x) => {
211
+ for (const c of ceiling) {
212
+ if (Math.abs(x - c) <= 8) return true;
213
+ }
214
+ return false;
215
+ };
216
+ const unit = width / 540;
217
+ const margin = Math.round(24 * unit);
218
+ const blobMin = Math.round(12 * unit);
219
+ const countMin = Math.round(30 * (unit / 2));
220
+ let floaters = 0,
221
+ widest = 0;
222
+ const windowBottom = Math.min(height - 1, y1 + margin);
223
+ for (let y = y1 + 2; y <= windowBottom; y += 2) {
224
+ let run = 0;
225
+ for (let x = x0; x <= x1 + 2; x += 2) {
226
+ if (x <= x1 && isLnk(x, y) && !nearCeiling(x)) {
227
+ run += 2;
228
+ floaters++;
229
+ } else {
230
+ if (run > widest) widest = run;
231
+ run = 0;
232
+ }
233
+ }
234
+ }
235
+ if (floaters >= countMin && widest >= blobMin) {
236
+ return (
237
+ "bundle/art-title-collision — a free-floating blob (" +
238
+ widest +
239
+ "px wide) parks in the clearance margin under the lettering block (rows " +
240
+ (y1 + 2) +
241
+ ".." +
242
+ windowBottom +
243
+ "): free world must clear the block by ~its own radius — relocate below or compose around it (ceiling-hung world is the one legal crosser) — frogoe-creative → references/art.md"
244
+ );
245
+ }
246
+ return null;
247
+ };
@@ -6,9 +6,9 @@
6
6
 
7
7
  **Doing anything with frogoe?** Read the `/frogoe` skill — it confirms the BRIEF (verb, mood, palette) up front and routes every request. The domain skills it routes to:
8
8
 
9
- - `/frogoe-core` — the technical contract: folder form, `defineGame` closure, four nouns, HUD bindings, external libraries. Read before writing any game code.
10
- - `/frogoe-creative` — house style: three dials (VARIANCE/MOTION/DENSITY), lazy defaults, typography, palettes, game feel. Read when choosing how a game looks.
11
- - `/frogoe-cli` — CLI dev loop: init, add, run, check, bundle, report. Finding codes split into `finding-codes.md` / `live-sandbox.md` / `bundle.md` for self-healing.
9
+ - `/frogoe-core` — the technical contract: folder form, `defineGame` closure, four nouns, HUD bindings, external libraries, the identity-art assets (`assets/poster.js` + `assets/icon.js`). Read before writing any game code.
10
+ - `/frogoe-creative` — house style: three dials (VARIANCE/MOTION/DENSITY), lazy defaults, typography, palettes, game feel, identity art (the poster + icon are authored, 1:1 with gameplay). Read when choosing how a game looks — including its face.
11
+ - `/frogoe-cli` — CLI dev loop: init, add, run, vision (ASCII eyes for draw code), check, bundle, embed (the card + manifest), report. Finding codes split into `finding-codes.md` / `live-sandbox.md` / `bundle.md` / `embed.md` / `vision.md` for self-healing.
12
12
  - `/frogoe-registry` — HUD block catalog: find, evaluate, install, author new blocks.
13
13
 
14
14
  Skills live at `.claude/skills/` and `.agents/skills/` (install via `npx skills add frogoe/engine`; both mirrors stay byte-identical). Missing or stale? Re-run the install and restart the agent session. Check freshness: `frogoe skills check`.
@@ -57,7 +57,9 @@ frogoe run --tunnel # + public URL — phone works on any network (cloud
57
57
  frogoe add <block> # copy a HUD block into blocks/ (score, hearts, fuel, game-over, etc.)
58
58
  frogoe lint # fast static contract lint (stable finding codes; --json for CI)
59
59
  frogoe check # full gate: lint + headless Chrome — FPS, playability, HUD outline, screenshots
60
+ frogoe vision # eyes: your draw code as ASCII maps (objects, frames, identity art)
60
61
  frogoe bundle # one self-contained HTML (externals dissolved) — only after check passes
62
+ frogoe embed # the card: poster loading state + sandboxed game + manifest — after bundle
61
63
  ```
62
64
 
63
65
  > **Agents must run `frogoe check` after ANY code change** and fix all errors before
@@ -70,6 +72,7 @@ bundle`. Use `--json` for machine-readable findings that can be fixed programmat
70
72
  - `index.html` — entry shell: `<canvas id="c">` + import map + `.hud` layer (HUD blocks land here)
71
73
  - `game.js` — the whole simulation: `defineGame(({stage, input, loop, finish}) => {...})`
72
74
  - `BRIEF.md` — the game's identity: verb, mood, palette (validated by `frogoe check`)
75
+ - `assets/poster.js` + `assets/icon.js` — REQUIRED identity art: canvas scenes importing the game's own sprites (`art/missing` gates the check; frogoe-creative → `references/art.md`)
73
76
  - `frogoe.json` — contract version pin
74
77
  - `.frogoe/` — tool-owned, gitignored (the contract runtime — never edit)
75
78
  - `blocks/` — HUD blocks copied from the registry (themed via `.hud` CSS custom properties)
@@ -86,15 +89,17 @@ frogoe check # full gate: + browser — runtime errors, canvas paint
86
89
 
87
90
  Fix all errors before presenting the result. Common findings:
88
91
 
89
- | Code | Meaning | Fix |
90
- | ------------------------ | ------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
91
- | `brief/todo` | BRIEF.md still has TODO markers | Fill in verb, mood, palette |
92
- | `input/incremental-drag` | `x += p.dx` (wall-rocket bug) | Use `x = grabX + p.dx` or track lastX |
93
- | `folder/touch-select` | Phone long-press summons text selection (iOS + Android) | Add `-webkit-user-select: none; user-select: none; -webkit-touch-callout: none` on html/body |
94
- | `audio/suspended-only` | Resume gated on `=== "suspended"` (iOS silent bug) | Resume when `state !== "running"` see frogoe-core `references/audio.md` |
95
- | `live/hud-outline` | HUD text missing text-shadow/stroke | Add `text-shadow: 0 2px 0 <dark>` |
96
- | `live/fps` | Below 30fps | Cache gradients, reduce shadowBlur, cut particles |
97
- | `live/not-playable` | Scripted taps changed nothing | Wire `input.on("down")` to actual game logic |
92
+ | Code | Meaning | Fix |
93
+ | ------------------------ | ------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
94
+ | `brief/todo` | BRIEF.md still has TODO markers | Fill in verb, mood, palette |
95
+ | `art/missing` | `assets/poster.js` / `assets/icon.js` absent | Author both scenes they import sprites from game.js (frogoe-creative `references/art.md`) |
96
+ | `art/title-band` | poster declares no lettering-block band | Set `ctx.__frogoeTitleBand = [x0,y0,x1,y1]` from the layout variables (never hand-typed) |
97
+ | `input/incremental-drag` | `x += p.dx` (wall-rocket bug) | Use `x = grabX + p.dx` or track lastX |
98
+ | `folder/touch-select` | Phone long-press summons text selection (iOS + Android) | Add `-webkit-user-select: none; user-select: none; -webkit-touch-callout: none` on html/body |
99
+ | `audio/suspended-only` | Resume gated on `=== "suspended"` (iOS silent bug) | Resume when `state !== "running"` — see frogoe-core `references/audio.md` |
100
+ | `live/hud-outline` | HUD text missing text-shadow/stroke | Add `text-shadow: 0 2px 0 <dark>` |
101
+ | `live/fps` | Below 30fps | Cache gradients, reduce shadowBlur, cut particles |
102
+ | `live/not-playable` | Scripted taps changed nothing | Wire `input.on("down")` to actual game logic |
98
103
 
99
104
  ## Key rules
100
105
 
@@ -104,3 +109,4 @@ Fix all errors before presenting the result. Common findings:
104
109
  4. **Fixed furniture respects `stage.safe`** — notches cover screen edges. Score at fixed y=34 sits under the Dynamic Island on modern phones.
105
110
  5. **One page, zero runtime requests** after bundling. Author-time CDN dependencies are fine — `frogoe bundle` dissolves them (allowlist + pin + hash + inline).
106
111
  6. **`finish(score)`** ends the run. The results card is a HUD block (`game-over-card`), never something the platform draws.
112
+ 7. **Identity art is authored, never captured** — `assets/poster.js` + `assets/icon.js` draw with the game's own sprite functions (1:1 by construction; `art/*` findings gate them). Iterate with `frogoe vision` — render, LOOK at the ASCII map, fix; agents that draw blind ship blobs.
@@ -6,9 +6,9 @@
6
6
 
7
7
  **Doing anything with frogoe?** Start at the `/frogoe` skill — it confirms the BRIEF (verb, mood, palette) up front and routes every request. The domain skills it routes to:
8
8
 
9
- - `/frogoe-core` — the technical contract: folder form, `defineGame` closure, four nouns, HUD bindings, external libraries. Read before writing any game code.
10
- - `/frogoe-creative` — house style: three dials (VARIANCE/MOTION/DENSITY), lazy defaults, typography, palettes, game feel. Read when choosing how a game looks.
11
- - `/frogoe-cli` — CLI dev loop: init, add, run, check, bundle, report. Finding codes split into `finding-codes.md` / `live-sandbox.md` / `bundle.md` for self-healing.
9
+ - `/frogoe-core` — the technical contract: folder form, `defineGame` closure, four nouns, HUD bindings, external libraries, the identity-art assets (`assets/poster.js` + `assets/icon.js`). Read before writing any game code.
10
+ - `/frogoe-creative` — house style: three dials (VARIANCE/MOTION/DENSITY), lazy defaults, typography, palettes, game feel, identity art (the poster + icon are authored, 1:1 with gameplay). Read when choosing how a game looks — including its face.
11
+ - `/frogoe-cli` — CLI dev loop: init, add, run, vision (ASCII eyes for draw code), check, bundle, embed (the card + manifest), report. Finding codes split into `finding-codes.md` / `live-sandbox.md` / `bundle.md` / `embed.md` / `vision.md` for self-healing.
12
12
  - `/frogoe-registry` — HUD block catalog: find, evaluate, install, author new blocks.
13
13
 
14
14
  Skills live at `.claude/skills/` and `.agents/skills/` (install via `npx skills add frogoe/engine`; both mirrors stay byte-identical). Missing or stale? Re-run the install and restart the agent session. Check freshness: `frogoe skills check`.
@@ -57,7 +57,9 @@ frogoe run --tunnel # + public URL — phone works on any network (cloud
57
57
  frogoe add <block> # copy a HUD block into blocks/ (score, hearts, fuel, game-over, etc.)
58
58
  frogoe lint # fast static contract lint (stable finding codes; --json for CI)
59
59
  frogoe check # full gate: lint + headless Chrome — FPS, playability, HUD outline, screenshots
60
+ frogoe vision # eyes: your draw code as ASCII maps (objects, frames, identity art)
60
61
  frogoe bundle # one self-contained HTML (externals dissolved) — only after check passes
62
+ frogoe embed # the card: poster loading state + sandboxed game + manifest — after bundle
61
63
  ```
62
64
 
63
65
  > **Agents must run `frogoe check` after ANY code change** and fix all errors before
@@ -70,6 +72,7 @@ bundle`. Use `--json` for machine-readable findings that can be fixed programmat
70
72
  - `index.html` — entry shell: `<canvas id="c">` + import map + `.hud` layer (HUD blocks land here)
71
73
  - `game.js` — the whole simulation: `defineGame(({stage, input, loop, finish}) => {...})`
72
74
  - `BRIEF.md` — the game's identity: verb, mood, palette (validated by `frogoe check`)
75
+ - `assets/poster.js` + `assets/icon.js` — REQUIRED identity art: canvas scenes importing the game's own sprites (`art/missing` gates the check; frogoe-creative → `references/art.md`)
73
76
  - `frogoe.json` — contract version pin
74
77
  - `.frogoe/` — tool-owned, gitignored (the contract runtime — never edit)
75
78
  - `blocks/` — HUD blocks copied from the registry (themed via `.hud` CSS custom properties)
@@ -86,15 +89,17 @@ frogoe check # full gate: + browser — runtime errors, canvas paint
86
89
 
87
90
  Fix all errors before presenting the result. Common findings:
88
91
 
89
- | Code | Meaning | Fix |
90
- | ------------------------ | ------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
91
- | `brief/todo` | BRIEF.md still has TODO markers | Fill in verb, mood, palette |
92
- | `input/incremental-drag` | `x += p.dx` (wall-rocket bug) | Use `x = grabX + p.dx` or track lastX |
93
- | `folder/touch-select` | Phone long-press summons text selection (iOS + Android) | Add `-webkit-user-select: none; user-select: none; -webkit-touch-callout: none` on html/body |
94
- | `audio/suspended-only` | Resume gated on `=== "suspended"` (iOS silent bug) | Resume when `state !== "running"` see frogoe-core `references/audio.md` |
95
- | `live/hud-outline` | HUD text missing text-shadow/stroke | Add `text-shadow: 0 2px 0 <dark>` |
96
- | `live/fps` | Below 30fps | Cache gradients, reduce shadowBlur, cut particles |
97
- | `live/not-playable` | Scripted taps changed nothing | Wire `input.on("down")` to actual game logic |
92
+ | Code | Meaning | Fix |
93
+ | ------------------------ | ------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
94
+ | `brief/todo` | BRIEF.md still has TODO markers | Fill in verb, mood, palette |
95
+ | `art/missing` | `assets/poster.js` / `assets/icon.js` absent | Author both scenes they import sprites from game.js (frogoe-creative `references/art.md`) |
96
+ | `art/title-band` | poster declares no lettering-block band | Set `ctx.__frogoeTitleBand = [x0,y0,x1,y1]` from the layout variables (never hand-typed) |
97
+ | `input/incremental-drag` | `x += p.dx` (wall-rocket bug) | Use `x = grabX + p.dx` or track lastX |
98
+ | `folder/touch-select` | Phone long-press summons text selection (iOS + Android) | Add `-webkit-user-select: none; user-select: none; -webkit-touch-callout: none` on html/body |
99
+ | `audio/suspended-only` | Resume gated on `=== "suspended"` (iOS silent bug) | Resume when `state !== "running"` — see frogoe-core `references/audio.md` |
100
+ | `live/hud-outline` | HUD text missing text-shadow/stroke | Add `text-shadow: 0 2px 0 <dark>` |
101
+ | `live/fps` | Below 30fps | Cache gradients, reduce shadowBlur, cut particles |
102
+ | `live/not-playable` | Scripted taps changed nothing | Wire `input.on("down")` to actual game logic |
98
103
 
99
104
  ## Key rules
100
105
 
@@ -104,3 +109,4 @@ Fix all errors before presenting the result. Common findings:
104
109
  4. **Fixed furniture respects `stage.safe`** — notches cover screen edges. Score at fixed y=34 sits under the Dynamic Island on modern phones.
105
110
  5. **One page, zero runtime requests** after bundling. Author-time CDN dependencies are fine — `frogoe bundle` dissolves them (allowlist + pin + hash + inline).
106
111
  6. **`finish(score)`** ends the run. The results card is a HUD block (`game-over-card`), never something the platform draws.
112
+ 7. **Identity art is authored, never captured** — `assets/poster.js` + `assets/icon.js` draw with the game's own sprite functions (1:1 by construction; `art/*` findings gate them). Iterate with `frogoe vision` — render, LOOK at the ASCII map, fix; agents that draw blind ship blobs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frogoe",
3
- "version": "0.3.2",
3
+ "version": "0.6.0",
4
4
  "description": "froge CLI — the agent's hands: init, add, run, check, bundle",
5
5
  "homepage": "https://github.com/frogoe/engine#readme",
6
6
  "bugs": "https://github.com/frogoe/engine/issues",