hayao 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +113 -24
  2. package/bin/create-hayao.mjs +380 -0
  3. package/bin/hayao-mcp-cli.mjs +11 -0
  4. package/dist/app/browser.d.ts +33 -2
  5. package/dist/app/game.d.ts +47 -2
  6. package/dist/app/tuning.d.ts +68 -0
  7. package/dist/art/palette.d.ts +41 -0
  8. package/dist/audio/adaptive.d.ts +58 -0
  9. package/dist/audio/album.d.ts +16 -0
  10. package/dist/audio/analysis.d.ts +59 -0
  11. package/dist/audio/audio.d.ts +21 -0
  12. package/dist/audio/chord.d.ts +17 -0
  13. package/dist/audio/genres.d.ts +11 -0
  14. package/dist/audio/lint.d.ts +29 -0
  15. package/dist/audio/match.d.ts +38 -0
  16. package/dist/audio/music.d.ts +88 -0
  17. package/dist/audio/pcm.d.ts +54 -0
  18. package/dist/audio/quality.d.ts +28 -0
  19. package/dist/audio/reverb.d.ts +15 -0
  20. package/dist/audio/synth.d.ts +56 -0
  21. package/dist/audio/theory.d.ts +64 -0
  22. package/dist/content/campaign.d.ts +69 -0
  23. package/dist/content/generate.d.ts +78 -0
  24. package/dist/content/level.d.ts +93 -0
  25. package/dist/content/worldgraph.d.ts +73 -0
  26. package/dist/core/dmath.d.ts +2 -0
  27. package/dist/hayao.global.js +15 -0
  28. package/dist/index.d.ts +30 -1
  29. package/dist/index.js +4293 -434
  30. package/dist/index.js.map +4 -4
  31. package/dist/index.min.js +15 -0
  32. package/dist/input/source.d.ts +52 -1
  33. package/dist/mcp.js +31225 -0
  34. package/dist/rasterize-worker-lite.mjs +13 -0
  35. package/dist/render/canvas.d.ts +6 -1
  36. package/dist/render/commands.d.ts +46 -0
  37. package/dist/render/paint.d.ts +33 -0
  38. package/dist/render/renderer.d.ts +22 -0
  39. package/dist/render/svg.d.ts +4 -1
  40. package/dist/render/svgString.d.ts +6 -2
  41. package/dist/scene/cameraController.d.ts +42 -0
  42. package/dist/scene/node.d.ts +17 -4
  43. package/dist/scene/nodes.d.ts +14 -0
  44. package/dist/scene/parallax.d.ts +15 -0
  45. package/dist/studio/mcpMain.d.ts +1 -0
  46. package/dist/studio/mcpServer.d.ts +2 -0
  47. package/dist/studio/record.d.ts +54 -0
  48. package/dist/studio/run.d.ts +78 -0
  49. package/dist/studio/session.d.ts +80 -0
  50. package/dist/studio/timeline.d.ts +35 -0
  51. package/dist/studio/vitePlugin.d.ts +6 -0
  52. package/dist/studio-plugin.js +228 -0
  53. package/dist/ui/overlay.d.ts +6 -0
  54. package/dist/verify/audioFilmstrip.d.ts +39 -0
  55. package/dist/verify/ethnography.d.ts +67 -0
  56. package/dist/verify/gates.d.ts +160 -0
  57. package/dist/verify/layout.d.ts +30 -3
  58. package/dist/verify/ramp.d.ts +40 -0
  59. package/dist/world.d.ts +38 -2
  60. package/dist-studio/assets/index-C7tty_Wo.js +109 -0
  61. package/dist-studio/assets/index-CM3tjRQo.css +1 -0
  62. package/dist-studio/index.html +18 -0
  63. package/docs/API.md +233 -9
  64. package/docs/CONVENTIONS.md +223 -0
  65. package/docs/EMBED.md +85 -0
  66. package/docs/QUICKSTART.md +129 -0
  67. package/docs/STUDIO.md +97 -0
  68. package/docs/VERIFICATION.md +226 -0
  69. package/package.json +39 -6
@@ -0,0 +1,226 @@
1
+ # Verification — how an AI proves a hayao game works
2
+
3
+ The whole engine is shaped so you can verify a game **without a browser and
4
+ without playing it**. Build the harness *before* the content; unverified content
5
+ is presumed broken (in-head verification has a documented failure rate).
6
+
7
+ Two channels, in order:
8
+
9
+ 1. **Numeric channel** — headless `step()`, probes, the solver, and determinism
10
+ checks. Proves *behavior* and *beatability*. This is where correctness lives.
11
+ 2. **Visual channel** — a headless SVG screenshot. Judges *looks* only — palette,
12
+ layout, legibility. Never correctness.
13
+
14
+ Everything below runs in Node under Vitest (`npm test`) or the CI script
15
+ (`npm run verify`).
16
+
17
+ ---
18
+
19
+ ## Channel 1a — scripted playthrough (any game)
20
+
21
+ Drive a world with an input log and assert on its probe. No DOM.
22
+
23
+ ```ts
24
+ import { createWorld } from '@hayao';
25
+ import { myGame } from './game';
26
+
27
+ const world = createWorld(myGame); // a live, deterministic World
28
+ world.step(['right']); // one fixed step, 'right' held
29
+ world.step([]); // release (edges need a rising edge)
30
+ world.step(['confirm']);
31
+ expect(world.probe().score).toBe(1); // assert on game state, not pixels
32
+ ```
33
+
34
+ Helpers: `scriptedPlaythrough(world, [hold(['right'], 30), wait(10)])` returns a
35
+ probe after each segment; `pump(world, n, actions)` advances n steps. Hold keys
36
+ for several frames — a 1-frame synthetic tap is treated as a real short tap.
37
+
38
+ ## Channel 1b — winnability solver (puzzles / turn-based)
39
+
40
+ Keep the rules in a pure module implementing `Puzzle<State, Move>` (no engine
41
+ imports), then prove every level beatable by search:
42
+
43
+ ```ts
44
+ import { solve, assertSolvable, type Puzzle } from '@hayao';
45
+
46
+ const puzzle: Puzzle<State, Move> = {
47
+ initial: (level) => …,
48
+ moves: (s) => […],
49
+ apply: (s, m) => …, // fold in a DETERMINISTIC opponent here if any
50
+ isWin: (s) => …,
51
+ isDead: (s) => …, // optional: prune lost states
52
+ key: (s) => canonicalString(s),
53
+ };
54
+
55
+ const r = assertSolvable(puzzle, { level: 0, maxDepth: 80 }); // throws if unwinnable
56
+ console.log(r.depth, r.nodes); // shortest solution length + search cost
57
+ ```
58
+
59
+ A deterministic opponent means winnability is single-agent BFS — no adversarial
60
+ branching. `solve` returns the shortest `path`, which you can replay through the
61
+ real game (Channel 1a) to prove the *view* agrees with the *logic*.
62
+
63
+ ## Channel 1c — determinism (the regression net)
64
+
65
+ Same seed + same input log must produce bit-identical state hashes. If not, some
66
+ nondeterminism leaked in (Math.random, Date.now, Set iteration, unordered map).
67
+
68
+ ```ts
69
+ import { assertDeterministic, assertSnapshotStable } from '@hayao';
70
+
71
+ assertDeterministic(() => createWorld(myGame), inputLog); // runs twice, compares every step's hash
72
+ assertSnapshotStable(() => createWorld(myGame), inputLog, 20); // snapshot→restore reproduces the tail exactly
73
+ ```
74
+
75
+ `world.hash()` is the structural hash; `world.snapshot()` / `world.restore()` are
76
+ save/undo/time-travel. Mark pure-view nodes `cosmetic = true` so transient display
77
+ (move counters, particles, tweened positions) never pollutes the canonical hash.
78
+
79
+ ## Channel 1d — the golden replay corpus (portfolio-wide refactor net)
80
+
81
+ Every example pins its full scripted run's final `world.hash()` in a committed
82
+ `examples/<slug>/golden.json`, checked by `t.golden(label, hash)` in its
83
+ verify suite. This turns "did my engine refactor change ANY game's behavior?"
84
+ into one command: `npm run verify` — a pure refactor leaves every golden hash
85
+ untouched; any divergence names the game and the run that changed.
86
+
87
+ ```ts
88
+ // in examples/<slug>/verify.ts, after the full run:
89
+ t.golden('full run', world.hash());
90
+ ```
91
+
92
+ **Re-record policy.** Golden divergence is a failing gate, on purpose. When a
93
+ behavior change is *intentional* (tuning, new content, a deliberate engine
94
+ change), re-record and say so:
95
+
96
+ 1. `UPDATE_GOLDEN=1 npm run verify` — rewrites the affected `golden.json`.
97
+ 2. Commit the new hashes IN THE SAME COMMIT as the change, with a line in the
98
+ message naming which games' behavior changed and why.
99
+
100
+ Never re-record to silence a divergence you can't explain — an unexplained
101
+ golden change IS the bug the corpus exists to catch.
102
+
103
+ ## Channel 2 — the visual screenshot (looks only)
104
+
105
+ ```ts
106
+ import { HeadlessRenderer, createWorld } from '@hayao';
107
+
108
+ const world = createWorld(myGame);
109
+ world.step([]);
110
+ const r = new HeadlessRenderer({ width: 1280, height: 720, background: '#f3ecdb' });
111
+ r.draw(world.render());
112
+ writeFileSync('shots/title.svg', r.toSVGString()); // a real vector screenshot, in Node
113
+ ```
114
+
115
+ Because rendering is SVG, the screenshot is **resolution-independent and text is
116
+ crisp** — no canvas fuzz, no GPU. In the browser, `?capture=1` exposes
117
+ `window.__hayao` (`pump`, `probe`, `hash`, `shot`, `save`, `key`) for scripted
118
+ sessions and aesthetic captures — but you rarely need it, because the DOM is
119
+ already inspectable and the sim already runs in Node.
120
+
121
+ For MOTION, use the filmstrip instead of a single frame: it samples a whole
122
+ run into one SVG contact sheet, so pacing, readability under load, and
123
+ layering during play are judgeable from a file.
124
+
125
+ ```ts
126
+ import { renderFilmstrip } from '@hayao';
127
+ t.artifact('run-filmstrip.svg', renderFilmstrip(createWorld(myGame), inputFrames, {
128
+ width: myGame.width, height: myGame.height, background: myGame.background, panels: 12,
129
+ }));
130
+ // → shots/<slug>/run-filmstrip.svg — open it and actually look.
131
+ ```
132
+
133
+ ## Channel 3 — feel probes (quality proxies)
134
+
135
+ Correctness channels prove the game is *not broken*; they say nothing about
136
+ whether it is *good*. Feel probes give quality a measurable proxy: compute
137
+ metrics from a per-frame probe timeline of the winning run, and gate them on
138
+ windows YOU tune per game. A window is a design decision recorded as a test —
139
+ retuning that wrecks pacing fails loudly instead of silently.
140
+
141
+ ```ts
142
+ import { recordTimeline, firstFrame, inputDensity, longestLull, changeFrames, isMonotonic } from '@hayao';
143
+
144
+ const tl = recordTimeline(createWorld(myGame), winningLog);
145
+ firstFrame(tl, (p) => p.kills > 0); // time-to-first-meaningful-action
146
+ inputDensity(winningLog); // engagement: waiting ↔ mashing
147
+ longestLull(changeFrames(tl, 'score'), tl.length); // dead-air detector
148
+ isMonotonic(depths, 'up', slack); // difficulty ramps with breathers
149
+ ```
150
+
151
+ What to gate is genre truth: a stealth game gates its hide/move alternation,
152
+ an incremental its unlock cadence, a puzzle its solve-depth curve. Two rules:
153
+ (1) derive windows from a run you have actually judged as feeling right —
154
+ never invent them; (2) keep them generous enough that tuning breathes, tight
155
+ enough that pacing regressions fail. These are proxies, not proof of fun —
156
+ the filmstrip and a human/AI look stay in the loop.
157
+
158
+ ## Channel 4 — feel gates (the professional floor, made checkable)
159
+
160
+ Channel 3 measures *pacing* from a timeline; Channel 4 gates the small set of feel
161
+ **fundamentals** that separate amateur from professional and that are genuinely
162
+ mechanical, not matters of taste. Each returns human-readable issues (empty = pass),
163
+ exactly like `layoutIssues` — so wiring one in is one line. This is the direct
164
+ answer to "green tests, dead game": turn more of *feel* green.
165
+
166
+ ```ts
167
+ import { forgivenessIssues, graceWindowIssues, feedbackIssues,
168
+ salienceIssues, telegraphIssues, cameraIssues, lookAheadIssues } from '@hayao';
169
+ ```
170
+
171
+ 1. **Forgiveness.** `forgivenessIssues(cfg)` audits the grace windows (coyote,
172
+ jump-buffer, corner-nudge) against a floor; `graceWindowIssues(label, W, accepts)`
173
+ *behaviourally* proves a window accepts an input `0..W` frames late and refuses
174
+ `W+1` — the exact edge FUN law 5 demands. (updrift proves a 4-frame coyote window
175
+ by walking a rig off a lip and jumping D frames later.)
176
+ 2. **Feedback completeness.** Declare a `FeedbackContract` (event → channels +
177
+ shake/hit-stop), then `feedbackIssues(contract, events)` proves every significant
178
+ event answers on ≥ 2 senses within the frame, with juice bounded (min to read,
179
+ max to not nauseate).
180
+ 3. **Readability.** `salienceIssues(render(), avatarFill, background)` proves the
181
+ avatar out-contrasts both the background and the median scenery fill (WCAG
182
+ luminance, from pure draw data). `telegraphIssues(timeline, minFrames)` proves
183
+ every threat activation is preceded by ≥ `minFrames` of telegraph — reactive play
184
+ needs danger to announce itself.
185
+ 4. **Camera lawfulness.** `cameraIssues(samples)` proves the follow never snaps
186
+ (bounded speed) or jerks (bounded acceleration); `lookAheadIssues(cam, target)`
187
+ proves it leads the motion rather than trailing. Sample the *follow* position
188
+ (exclude screen shake), and drop the pre-start frame (no camera exists yet).
189
+
190
+ The full four-gate suite wired on a real game lives in
191
+ [`examples/updrift/verify.ts`](../examples/updrift/verify.ts); the authoring side is
192
+ [docs/JUICE.md](JUICE.md). Gate on the fundamentals; the *soul* above the floor
193
+ stays authored (a director, not a proof) — but the floor no amateur clears is now
194
+ machine-enforced.
195
+
196
+ **Portfolio floor (`npm run feel`).** A game declares its contract once —
197
+ `export const feel: FeelSpec` in `game.ts` (avatar fill → salience; controller
198
+ config → forgiveness; feedback contract → feedback; `scrolls` → camera). The audit
199
+ runs every gate each spec enables across all games and exits non-zero on any
200
+ failure; it is part of `npm run verify`, so the floor rises for ALL output, not one
201
+ game. Declare only what is honestly true — a false contract is worse than none.
202
+
203
+ ## Channel 5 — the vision judge (`npm run judge`)
204
+
205
+ The gates prove the floor mechanically; they can't see that a scene is *empty* or
206
+ *flat*. `npm run judge` renders each game headlessly to PNG (SVG → `@resvg/resvg-js`,
207
+ no browser) so a multimodal model LOOKS at the pixels and scores them against
208
+ [JUDGE.md](JUDGE.md) — then fixes what a human would wince at, staying cosmetic (the
209
+ golden hash unchanged). It's the one judge that can't be a pure function, which is
210
+ why an AI-first engine — deterministic sim, headless render — is uniquely built to
211
+ run it in a loop. Drive it with the `/judge` skill.
212
+
213
+ ---
214
+
215
+ ## The gate (`npm run verify`)
216
+
217
+ The CI verifier proves every example winnable, replays a solution through the
218
+ real game, and checks determinism — exiting non-zero on any failure. Wire your
219
+ game's puzzle + game into `scripts/verify.ts` and it becomes a pipeline gate.
220
+
221
+ ## Invariants
222
+
223
+ - All randomness flows through `world.rng`. No `Math.random()` in `src/` or games.
224
+ - No wall-clock in the sim (`Date.now`, `performance.now`, argless `new Date`).
225
+ - Fixed tree order; ordered collections for logic (no `Set`/object-key order).
226
+ - Every shipped level: a solver proof OR a scripted playthrough. No exceptions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hayao",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "An AI-first game engine: a deterministic, headless-native simulation kernel with a Godot-style scene tree, pluggable renderers (SVG/Canvas/headless), and a built-in verification harness — designed so an LLM can author, test, and prove correct a whole game without ever opening a browser.",
6
6
  "keywords": [
@@ -26,15 +26,31 @@
26
26
  "main": "./dist/index.js",
27
27
  "module": "./dist/index.js",
28
28
  "types": "./dist/index.d.ts",
29
+ "bin": {
30
+ "create-hayao": "./bin/create-hayao.mjs",
31
+ "hayao-mcp": "./bin/hayao-mcp-cli.mjs"
32
+ },
29
33
  "exports": {
30
34
  ".": {
31
35
  "types": "./dist/index.d.ts",
32
36
  "import": "./dist/index.js"
37
+ },
38
+ "./studio": {
39
+ "types": "./dist/studio/vitePlugin.d.ts",
40
+ "import": "./dist/studio-plugin.js"
33
41
  }
34
42
  },
35
43
  "files": [
36
44
  "dist",
37
- "docs/API.md"
45
+ "dist-studio",
46
+ "bin/create-hayao.mjs",
47
+ "bin/hayao-mcp-cli.mjs",
48
+ "docs/QUICKSTART.md",
49
+ "docs/API.md",
50
+ "docs/VERIFICATION.md",
51
+ "docs/CONVENTIONS.md",
52
+ "docs/EMBED.md",
53
+ "docs/STUDIO.md"
38
54
  ],
39
55
  "engines": {
40
56
  "node": ">=18"
@@ -45,19 +61,36 @@
45
61
  "scripts": {
46
62
  "dev": "vite",
47
63
  "build": "tsc --noEmit && vite build",
48
- "build:lib": "rm -rf dist && esbuild src/index.ts --bundle --format=esm --platform=neutral --target=es2022 --outfile=dist/index.js --sourcemap && tsc -p tsconfig.build.json",
64
+ "build:lib": "rm -rf dist && esbuild src/index.ts --bundle --format=esm --platform=neutral --target=es2022 --outfile=dist/index.js --sourcemap && esbuild src/index.ts --bundle --minify --format=esm --platform=neutral --target=es2022 --outfile=dist/index.min.js && esbuild src/index.ts --bundle --minify --format=iife --global-name=hayao --platform=neutral --target=es2022 --outfile=dist/hayao.global.js && esbuild src/studio/mcpMain.ts --bundle --format=esm --platform=node --target=es2022 --outfile=dist/mcp.js --external:@resvg/resvg-js --external:hayao && esbuild src/studio/vitePlugin.ts --bundle --format=esm --platform=node --target=es2022 --outfile=dist/studio-plugin.js --external:vite && cp scripts/rasterize-worker-lite.mjs dist/rasterize-worker-lite.mjs && tsc -p tsconfig.build.json",
65
+ "build:studio": "vite build -c vite.studio.config.ts",
49
66
  "check": "tsc --noEmit",
50
67
  "test": "vitest run",
51
68
  "test:watch": "vitest",
52
69
  "api": "tsx scripts/api-digest.ts",
53
70
  "invariants": "tsx scripts/invariants.ts",
54
- "verify": "tsx scripts/invariants.ts && tsx scripts/verify.ts",
55
- "prepublishOnly": "npm run check && npm test && npm run build:lib",
56
- "relay": "node scripts/relay.mjs"
71
+ "verify": "tsx scripts/verify-all.ts",
72
+ "eval": "tsx scripts/eval.ts",
73
+ "create": "node bin/create-hayao.mjs",
74
+ "prepublishOnly": "npm run check && npm test && npm run build:lib && npm run build:studio",
75
+ "relay": "node scripts/relay.mjs",
76
+ "palette": "tsx scripts/palette-audit.ts",
77
+ "audio": "tsx scripts/audio-render.ts",
78
+ "feel": "tsx scripts/feel.ts",
79
+ "judge": "tsx scripts/judge.ts",
80
+ "thumbs": "tsx scripts/site-thumbs.ts"
57
81
  },
58
82
  "devDependencies": {
83
+ "@modelcontextprotocol/sdk": "^1.29.0",
84
+ "@resvg/resvg-js": "^2.6.2",
59
85
  "@types/node": "^22.10.0",
86
+ "@types/qrcode": "^1.5.6",
87
+ "@types/react": "^19.2.17",
88
+ "@types/react-dom": "^19.2.3",
60
89
  "esbuild": "^0.28.0",
90
+ "leva": "^0.10.1",
91
+ "qrcode": "^1.5.4",
92
+ "react": "^19.2.7",
93
+ "react-dom": "^19.2.7",
61
94
  "tsx": "^4.19.0",
62
95
  "typescript": "^5.7.0",
63
96
  "vite": "^6.0.0",