hayao 0.1.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 (106) hide show
  1. package/README.md +127 -21
  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/autotile.d.ts +77 -0
  8. package/dist/art/bitmapFont.d.ts +113 -0
  9. package/dist/art/font5.d.ts +11 -0
  10. package/dist/art/palette.d.ts +79 -0
  11. package/dist/art/texture.d.ts +78 -0
  12. package/dist/audio/adaptive.d.ts +58 -0
  13. package/dist/audio/album.d.ts +16 -0
  14. package/dist/audio/analysis.d.ts +59 -0
  15. package/dist/audio/audio.d.ts +28 -0
  16. package/dist/audio/chord.d.ts +17 -0
  17. package/dist/audio/genres.d.ts +11 -0
  18. package/dist/audio/lint.d.ts +29 -0
  19. package/dist/audio/match.d.ts +38 -0
  20. package/dist/audio/music.d.ts +88 -0
  21. package/dist/audio/pcm.d.ts +54 -0
  22. package/dist/audio/quality.d.ts +28 -0
  23. package/dist/audio/reverb.d.ts +15 -0
  24. package/dist/audio/synth.d.ts +56 -0
  25. package/dist/audio/theory.d.ts +64 -0
  26. package/dist/content/campaign.d.ts +69 -0
  27. package/dist/content/dsl.d.ts +61 -0
  28. package/dist/content/generate.d.ts +78 -0
  29. package/dist/content/level.d.ts +93 -0
  30. package/dist/content/worldgraph.d.ts +73 -0
  31. package/dist/core/dmath.d.ts +22 -0
  32. package/dist/core/math.d.ts +2 -0
  33. package/dist/hayao.global.js +15 -0
  34. package/dist/index.d.ts +66 -1
  35. package/dist/index.js +9174 -835
  36. package/dist/index.js.map +4 -4
  37. package/dist/index.min.js +15 -0
  38. package/dist/input/source.d.ts +52 -1
  39. package/dist/logic/fsm.d.ts +85 -0
  40. package/dist/logic/graph.d.ts +88 -0
  41. package/dist/logic/history.d.ts +54 -0
  42. package/dist/logic/random.d.ts +32 -0
  43. package/dist/mcp.js +31225 -0
  44. package/dist/net/browser.d.ts +37 -0
  45. package/dist/net/inputBuffer.d.ts +27 -0
  46. package/dist/net/lockstep.d.ts +79 -0
  47. package/dist/net/players.d.ts +27 -0
  48. package/dist/net/protocol.d.ts +100 -0
  49. package/dist/net/rollback.d.ts +89 -0
  50. package/dist/net/room.d.ts +78 -0
  51. package/dist/net/transport.d.ts +78 -0
  52. package/dist/persist/codec.d.ts +4 -0
  53. package/dist/persist/save.d.ts +32 -0
  54. package/dist/persist/storage.d.ts +46 -0
  55. package/dist/physics/rigidBody.d.ts +104 -0
  56. package/dist/physics/rigidCollide.d.ts +16 -0
  57. package/dist/physics/rigidJoints.d.ts +65 -0
  58. package/dist/physics/rigidQueries.d.ts +15 -0
  59. package/dist/physics/rigidStep.d.ts +14 -0
  60. package/dist/procgen/cave.d.ts +21 -0
  61. package/dist/procgen/grid.d.ts +21 -0
  62. package/dist/procgen/rooms.d.ts +34 -0
  63. package/dist/procgen/scatter.d.ts +32 -0
  64. package/dist/procgen/terrain.d.ts +24 -0
  65. package/dist/rasterize-worker-lite.mjs +13 -0
  66. package/dist/render/canvas.d.ts +6 -1
  67. package/dist/render/commands.d.ts +46 -0
  68. package/dist/render/nineSlice.d.ts +32 -0
  69. package/dist/render/paint.d.ts +33 -0
  70. package/dist/render/renderer.d.ts +22 -0
  71. package/dist/render/svg.d.ts +4 -1
  72. package/dist/render/svgString.d.ts +6 -2
  73. package/dist/scene/cameraController.d.ts +42 -0
  74. package/dist/scene/floatingText.d.ts +51 -0
  75. package/dist/scene/node.d.ts +17 -4
  76. package/dist/scene/nodes.d.ts +14 -0
  77. package/dist/scene/parallax.d.ts +15 -0
  78. package/dist/scene/particles.d.ts +64 -0
  79. package/dist/scene/pool.d.ts +16 -0
  80. package/dist/scene/tween.d.ts +26 -0
  81. package/dist/studio/mcpMain.d.ts +1 -0
  82. package/dist/studio/mcpServer.d.ts +2 -0
  83. package/dist/studio/record.d.ts +54 -0
  84. package/dist/studio/run.d.ts +78 -0
  85. package/dist/studio/session.d.ts +80 -0
  86. package/dist/studio/timeline.d.ts +35 -0
  87. package/dist/studio/vitePlugin.d.ts +6 -0
  88. package/dist/studio-plugin.js +228 -0
  89. package/dist/ui/overlay.d.ts +6 -0
  90. package/dist/ui/transition.d.ts +107 -0
  91. package/dist/verify/audioFilmstrip.d.ts +39 -0
  92. package/dist/verify/ethnography.d.ts +67 -0
  93. package/dist/verify/gates.d.ts +160 -0
  94. package/dist/verify/layout.d.ts +66 -0
  95. package/dist/verify/ramp.d.ts +40 -0
  96. package/dist/world.d.ts +38 -2
  97. package/dist-studio/assets/index-C7tty_Wo.js +109 -0
  98. package/dist-studio/assets/index-CM3tjRQo.css +1 -0
  99. package/dist-studio/index.html +18 -0
  100. package/docs/API.md +473 -10
  101. package/docs/CONVENTIONS.md +223 -0
  102. package/docs/EMBED.md +85 -0
  103. package/docs/QUICKSTART.md +129 -0
  104. package/docs/STUDIO.md +97 -0
  105. package/docs/VERIFICATION.md +226 -0
  106. package/package.json +66 -11
package/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  [![ci](https://github.com/hellojanpacan/hayao-js/actions/workflows/ci.yml/badge.svg)](https://github.com/hellojanpacan/hayao-js/actions/workflows/ci.yml)
4
4
 
5
+ **Play the machine-verified example games at [hayao.dev/play](https://hayao.dev/play/).**
6
+ Four are art-finished in the house woodblock style (Lanternway, Rootward,
7
+ Tarnholm, Driftlight); the rest are playable engine slices. See where it's headed
8
+ on the [roadmap](https://hayao.dev/roadmap/).
9
+
5
10
  **An AI-first game engine.** A deterministic, headless-native simulation kernel
6
11
  with a Godot-style scene tree, pluggable renderers (SVG / Canvas / headless),
7
12
  and a built-in verification harness — designed so an LLM can author, test, and
@@ -39,7 +44,74 @@ npm run verify # run the determinism + solver harness over all examples
39
44
  ```
40
45
 
41
46
  A game is one folder under `examples/<slug>/`, imports **only** from `@hayao`,
42
- and is a `defineGame()` call. See [docs/CONVENTIONS.md](docs/CONVENTIONS.md).
47
+ and is a `defineGame()` call. See [docs/CONVENTIONS.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/CONVENTIONS.md).
48
+
49
+ ## Start a new game in one command
50
+
51
+ ```sh
52
+ npm create hayao@latest my-game # scaffolds a runnable project
53
+ cd my-game && npm install
54
+ npm run verify # prove every level winnable — no browser
55
+ npm run dev # play it
56
+ ```
57
+
58
+ The scaffolded starter isn't a hello-world — it already **generates a solver-proven,
59
+ ramped campaign**, so an AI author lands in a project that knows the invariants
60
+ (`AGENTS.md`) and can prove its own output from the first commit.
61
+
62
+ ## Generate an hour of proven content, don't hand-author it
63
+
64
+ The hard part of "make me a whole game" was never one level — it was *forty* balanced
65
+ ones. hayao closes that gap: express a level as a `Puzzle` factory and let the
66
+ **solver-backed generator** compose the campaign.
67
+
68
+ ```ts
69
+ import { composeCampaign, assertRamp } from '@hayao';
70
+
71
+ const campaign = composeCampaign({
72
+ seed: 7,
73
+ acts: [
74
+ { name: 'Shallows', count: 12, minDepth: 2, maxDepth: 4, factory: makeLevel },
75
+ { name: 'Deep', count: 14, minDepth: 5, maxDepth: 7, factory: makeLevel },
76
+ { name: 'Abyss', count: 16, minDepth: 8, maxDepth: 12, factory: makeLevel },
77
+ ],
78
+ });
79
+ assertRamp(campaign.difficulty); // the whole curve is proven well-shaped
80
+ ```
81
+
82
+ Every level is `solve()`-proven winnable inside its difficulty band; `assertRamp`
83
+ proves the curve escalates without cliffs. An hour of content ships as a **list of
84
+ seeds**, each provable — not forty hand-drawn maps. The reference is
85
+ [examples/lanternfold](examples/lanternfold) — a 42-level lantern-lighting campaign
86
+ composed entirely this way, with `npm run eval` scoring proof coverage across the
87
+ whole portfolio.
88
+
89
+ ## Close the one channel a machine can't: fun
90
+
91
+ Hayao proves winnability, determinism, ramp, and feel-floors by machine. The one
92
+ thing no headless gate can score is **fun** — and that takes a human. **Hayao
93
+ Studio** is the instrument for that channel: `npm run dev`, open `/studio/`, and
94
+ every human playtest becomes a bit-exactly replayable artifact the agent can read.
95
+
96
+ ```ts
97
+ // main.ts — Studio-instrumented driver (drop-in for runBrowser)
98
+ import { runStudio } from 'hayao';
99
+ runStudio(game, document.getElementById('app')!, { hot: import.meta.hot });
100
+ ```
101
+
102
+ ```ts
103
+ // vite.config.ts — the dev-server harness (sessions, live knobs, A/B, /studio/)
104
+ import { hayaoStudio } from 'hayao/studio';
105
+ export default { plugins: [hayaoStudio()] };
106
+ ```
107
+
108
+ A session `(seed, tuning, inputLog, axes, knobEvents)` re-simulates the whole run
109
+ in Node, so any metric is computable after the fact and any tick is re-inspectable.
110
+ The bundled `hayao-mcp` sidecar is the agent's window in — `list_sessions`,
111
+ `get_playtest_report` (hesitations, deaths, quit context), `inspect_moment`
112
+ (replay any tick → probe + screenshot), `get_knob_state` (values the human
113
+ accepted, to write back into `tuning:` defaults). Telemetry describes; the human
114
+ directs. Full doctrine in [docs/STUDIO.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/STUDIO.md).
43
115
 
44
116
  ## The layers
45
117
 
@@ -52,15 +124,23 @@ public surface is greppable in one file (`src/index.ts`).
52
124
  | **app/** | `runBrowser` (rAF loop) · `runHeadless` | plugs the kernel into a host |
53
125
  | **ui/** | DOM overlays: menus, HUD, pause/settings shell | observer (never mutates sim) |
54
126
  | **render/** | display list → `SvgRenderer` \| `Canvas2DRenderer` \| `HeadlessRenderer` | observer (projection → paint) |
127
+ | **art/** | code-as-art: palettes, shapes, textures, bitmap fonts, autotile | observer (paints, never hashed) |
55
128
  | **audio/** | procedural Web Audio bus (no-op in Node) | observer |
56
- | **verify/** | probes · replay · `assertDeterministic` · solver | the AI-first harness |
129
+ | **verify/** | probes · replay · `assertDeterministic` · solver · bots · filmstrips | the AI-first harness |
57
130
  | **scene/** | `Node`, `Node2D`, `Sprite`, `Text`, `Camera2D`, `Timer`, `AnimationPlayer`, behaviors | **THE STATE** |
131
+ | **physics/** | tilemap + kinematic AABB · character controllers · deterministic rigid-body dynamics | deterministic |
132
+ | **procgen/** | seeded grids, caves, terrain, rooms, stateless scatter | deterministic |
133
+ | **logic/** | pure primitives: FSM, weighted tables, graph search, history | deterministic |
134
+ | **content/** | data-driven wave/spawn directors + upgrade trees | deterministic |
135
+ | **net/** | deterministic multiplayer: lockstep + rollback over a pluggable transport | deterministic |
136
+ | **persist/** | save/load over a pluggable storage adapter + compact codecs | deterministic |
58
137
  | **input/** | action map · per-step sampling · record / replay | deterministic |
59
138
  | **core/** | `Rng` · `Clock` · `EventBus` · `World` · state hash | **THE KERNEL** (headless, pure) |
60
139
 
61
- `core + scene + input` are **deterministic and run in Node**. `render + audio +
62
- ui + app` are the browser-only observer shell. Break that boundary and the
63
- verification harness stops being able to prove anything.
140
+ `core + scene + input + physics + procgen + logic + content + net + persist` are
141
+ **deterministic and run in Node**. `render + art + audio + ui + app` are the
142
+ browser-only observer shell. Break that boundary and the verification harness
143
+ stops being able to prove anything.
64
144
 
65
145
  ## Why not Godot or a raw canvas engine?
66
146
 
@@ -70,28 +150,54 @@ GUIs, binary scenes, and a bespoke language, all opaque to an LLM; hayao keeps
70
150
  the model and makes every part **text, typed, greppable, and headlessly
71
151
  verifiable**. Canvas engines couple the sim to the render loop and the browser;
72
152
  hayao decouples them so the sim is a pure function you can test in Node. Full
73
- reasoning in [docs/ENGINE.md](docs/ENGINE.md).
153
+ reasoning in [docs/ENGINE.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/ENGINE.md).
74
154
 
75
155
  ## Documentation
76
156
 
77
- - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — the authoritative design and the determinism contract.
78
- - [docs/CONVENTIONS.md](docs/CONVENTIONS.md) — how games are structured, house style, definition of done.
79
- - [docs/VERIFICATION.md](docs/VERIFICATION.md) — the two verification channels; how to prove a game correct.
80
- - [docs/ENGINE.md](docs/ENGINE.md) — why a custom engine, and when NOT to use one.
81
- - [docs/LESSONS.md](docs/LESSONS.md) — transferable lessons from real LLM-authored game builds.
82
- - [docs/FRICTION.md](docs/FRICTION.md) — process-lesson log: what fought an AI session and what check/doc now prevents it.
83
- - [AGENTS.md](AGENTS.md) — the operating manual for an AI author working in this repo.
157
+ - [docs/QUICKSTART.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/QUICKSTART.md) — **using `hayao` from npm**: install (it's ESM-only), define a game, and prove it correct headlessly — a complete runnable example.
158
+ - [docs/API.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/API.md) — the full, greppable public surface (every export + signature). Ships in the package.
159
+ - [docs/ARCHITECTURE.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/ARCHITECTURE.md) — the authoritative design and the determinism contract.
160
+ - [docs/CONVENTIONS.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/CONVENTIONS.md) — how games are structured, house style, definition of done.
161
+ - [docs/VERIFICATION.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/VERIFICATION.md) — the two verification channels; how to prove a game correct.
162
+ - [docs/STUDIO.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/STUDIO.md) — the human/AI playtest loop: sessions, live knobs, A/B variants, the MCP sidecar.
163
+ - [docs/GALLERY.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/GALLERY.md) — the verified gallery: what "machine-proven" means per game, and how to run the proof yourself.
164
+ - [docs/ENGINE.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/ENGINE.md) — why a custom engine, and when NOT to use one.
165
+ - [docs/LESSONS.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/LESSONS.md) — transferable lessons from real LLM-authored game builds.
166
+ - [docs/FRICTION.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/FRICTION.md) — process-lesson log: what fought an AI session and what check/doc now prevents it.
167
+ - [AGENTS.md](https://github.com/hellojanpacan/hayao-js/blob/main/AGENTS.md) — the operating manual for an AI author working in this repo.
168
+
169
+ ## The site
170
+
171
+ Three doors, split by audience: [hayao.dev](https://hayao.dev/) is the marketing
172
+ landing (`index.html`), [hayao.dev/play](https://hayao.dev/play/) is the example
173
+ store (`play/index.html`), [hayao.dev/roadmap](https://hayao.dev/roadmap/) is the
174
+ public roadmap + js13k benchmark ladder (`roadmap/index.html`), and developer docs
175
+ live at [hayao.js.org](https://hayao.js.org/). Featured-game thumbnails are
176
+ regenerated with `npm run thumbs`.
84
177
 
85
178
  ## Status
86
179
 
87
- **v0.1complete and verified.** All layers are implemented and tested:
88
- `core/`, `scene/`, `input/`, `render/` (SVG + Canvas2D + headless backends),
89
- `audio/`, `ui/`, `verify/`, and `app/`. The flagship **Sokoban** example
90
- (`examples/sokoban/`) is solver-proven winnable on every level and plays in the
91
- browser. 40 tests pass; `npm run check`, `npm run verify`, and `npm run build`
92
- are all green. Roadmap: more node types (physics bodies, particles), an ECS
93
- option for large entity counts, additional example genres, and asset-free audio
94
- tracks. Contributions welcome.
180
+ **v0.3Hayao Studio: the human/AI playtest loop ships with the engine.**
181
+ The v0.2 campaign covered the most popular 2D indie genres (platformer,
182
+ metroidvania, Zelda-like, stealth, horde survival, bullet hell, tower defense,
183
+ RTS, roguelike, deckbuilder, tactics, match-3, idle, farming, horror, city
184
+ builder, rhythm, physics arcade, racing, narrative plus the original Sokoban),
185
+ and grew from there: js13k-benchmark reproductions (Seamfold, Gravewell), a
186
+ deterministic rigid-body physics wave (Rookspire demolition, Brasswick pinball),
187
+ a netplay showcase (Fernclash — lockstep + rollback across tabs), a 2-player
188
+ co-op survival (Kinfall), and the flagship metroidvania (Kintsugi). 30+ example
189
+ games now live under `examples/`, each shipping a `verify.ts` suite that
190
+ machine-proves its truth: solver-proven puzzles, bot-beaten levels, duel-proven
191
+ counter systems, win-rate-tuned balance, fairness gates for procgen, frame-exact
192
+ timing windows, bit-for-bit peer agreement, golden replay hashes.
193
+
194
+ v0.3 adds the one channel a machine can't close on its own — **fun** — as a
195
+ first-class, replayable instrument (Hayao Studio, above), plus a de-anchoring
196
+ `sandboxes/` shelf: single-mechanic labs (physics, particles, camera,
197
+ pathfinding, procgen, synth, juice) for learning one primitive in isolation.
198
+ 740+ tests; `npm run verify` runs the whole portfolio. What each genre demanded
199
+ of the engine and what generalized lives in
200
+ [docs/BUILDLOG.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/BUILDLOG.md).
95
201
 
96
202
  ## License
97
203
 
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+ // create-hayao — scaffold a fresh, runnable hayao project in one command.
3
+ //
4
+ // npm create hayao@latest my-game
5
+ // # or: npx create-hayao my-game
6
+ //
7
+ // The point is AI-first distribution: the generated project is shaped so a coding
8
+ // agent lands in a repo that already knows the invariants (AGENTS.md), already has
9
+ // the verify gate wired (`npm run verify`), and ships a STARTER that demonstrates
10
+ // the headline capability — a solver-backed generator composing a proven, ramped
11
+ // campaign. An agent opening this folder can author a game and prove it correct
12
+ // without ever opening a browser. Zero network beyond `npm install`.
13
+
14
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
15
+ import { join, resolve } from 'node:path';
16
+
17
+ const name = process.argv[2] || 'my-hayao-game';
18
+ const dir = resolve(process.cwd(), name);
19
+ const slug = name.replace(/[^a-z0-9-]/gi, '-').toLowerCase();
20
+
21
+ if (existsSync(dir)) {
22
+ console.error(`✗ ${dir} already exists — pick a new directory name.`);
23
+ process.exit(1);
24
+ }
25
+
26
+ /** Files to write, relative to the new project root. */
27
+ const files = {
28
+ 'package.json': JSON.stringify(
29
+ {
30
+ name: slug,
31
+ private: true,
32
+ type: 'module',
33
+ scripts: {
34
+ dev: 'vite',
35
+ check: 'tsc --noEmit',
36
+ test: 'vitest run',
37
+ verify: 'tsx verify.ts',
38
+ build: 'vite build',
39
+ },
40
+ dependencies: { hayao: '^0.3' },
41
+ devDependencies: {
42
+ '@types/node': '^22.10.0',
43
+ tsx: '^4.19.0',
44
+ typescript: '^5.7.0',
45
+ vite: '^6.0.0',
46
+ vitest: '^2.1.0',
47
+ },
48
+ },
49
+ null,
50
+ 2,
51
+ ) + '\n',
52
+
53
+ 'tsconfig.json': JSON.stringify(
54
+ {
55
+ compilerOptions: {
56
+ target: 'ES2022',
57
+ module: 'ESNext',
58
+ moduleResolution: 'bundler',
59
+ strict: true,
60
+ noUnusedLocals: true,
61
+ skipLibCheck: true,
62
+ types: ['vite/client', 'node'],
63
+ // Runtime resolution for tsx (verify.ts, the MCP sidecar loading game
64
+ // modules); TYPES come from hayao-env.d.ts's ambient declaration.
65
+ paths: { '@hayao': ['./node_modules/hayao/dist/index.js'] },
66
+ },
67
+ include: ['*.ts', '*.d.ts'],
68
+ },
69
+ null,
70
+ 2,
71
+ ) + '\n',
72
+
73
+ 'vite.config.ts': `import { defineConfig } from 'vite';
74
+ import { hayaoStudio } from 'hayao/studio';
75
+ // Mirror the hayao convention: games import from the single '@hayao' seam.
76
+ // hayaoStudio() is the Studio dev harness: playtest sessions record to
77
+ // .studio/, live tuning knobs, A/B variants, and the /studio/ page — see
78
+ // docs/STUDIO.md in the hayao repo. Dev-only; production builds are untouched.
79
+ export default defineConfig({
80
+ resolve: { alias: { '@hayao': 'hayao' } },
81
+ plugins: [hayaoStudio()],
82
+ });
83
+ `,
84
+
85
+ 'hayao-env.d.ts': `// Types for the '@hayao' seam: runtime resolution is the tsconfig paths entry
86
+ // (tsx) and the vite alias; this ambient declaration gives TS the package's
87
+ // real types for both.
88
+ declare module '@hayao' {
89
+ export * from 'hayao';
90
+ }
91
+ `,
92
+
93
+ '.mcp.json': JSON.stringify(
94
+ { mcpServers: { 'hayao-studio': { command: 'npx', args: ['hayao-mcp'] } } },
95
+ null,
96
+ 2,
97
+ ) + '\n',
98
+
99
+ 'index.html': `<!doctype html>
100
+ <html lang="en">
101
+ <head>
102
+ <meta charset="UTF-8" />
103
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
104
+ <title>${slug} · hayao</title>
105
+ <style>html,body{margin:0;height:100%;background:#12121a;font-family:Georgia,serif}#app{position:absolute;inset:0}</style>
106
+ </head>
107
+ <body>
108
+ <div id="app"></div>
109
+ <script type="module" src="./main.ts"></script>
110
+ </body>
111
+ </html>
112
+ `,
113
+
114
+ 'logic.ts': `// Pure puzzle logic — no engine imports beyond the Puzzle type. This is the truth
115
+ // the solver proves and the scene tree merely views. A tiny "toggle" puzzle: tap a
116
+ // cell to flip it and its plus-neighbours; win when the whole grid is lit. Every
117
+ // tap is self-inverse, so any board reached by scrambling from solved is winnable —
118
+ // which makes it the perfect target for the solver-backed generator.
119
+ import type { Puzzle } from '@hayao';
120
+ import { Rng } from '@hayao';
121
+
122
+ export interface ToggleState { lit: number[]; cols: number; rows: number; }
123
+
124
+ export function flip(lit: readonly number[], i: number, cols: number, rows: number): number[] {
125
+ const out = lit.slice();
126
+ const x = i % cols, y = Math.floor(i / cols);
127
+ const set = (xx: number, yy: number) => { if (xx>=0&&yy>=0&&xx<cols&&yy<rows){ const j=yy*cols+xx; out[j]=out[j]?0:1; } };
128
+ set(x,y); set(x-1,y); set(x+1,y); set(x,y-1); set(x,y+1);
129
+ return out;
130
+ }
131
+
132
+ export function makeToggle(rng: Rng, cols = 4, rows = 4, scramble = 8): Puzzle<ToggleState, number> {
133
+ let lit = new Array<number>(cols*rows).fill(1);
134
+ for (let k=0;k<scramble;k++) lit = flip(lit, rng.int(cols*rows), cols, rows);
135
+ const start = lit.slice();
136
+ const all = Array.from({length: cols*rows}, (_,i)=>i);
137
+ return {
138
+ initial: () => ({ lit: start.slice(), cols, rows }),
139
+ moves: () => all,
140
+ apply: (s, i) => ({ lit: flip(s.lit, i, cols, rows), cols, rows }),
141
+ isWin: (s) => s.lit.every(v=>v===1),
142
+ key: (s) => s.lit.join(''),
143
+ };
144
+ }
145
+ `,
146
+
147
+ 'game.ts': `// The game: pure logic (logic.ts) → a scene-tree view. Content is generated and
148
+ // PROVEN by the solver-backed generator, then composed into a ramped campaign.
149
+ import { Node, Sprite, Text, KENTO, withAlpha, linearGradient, glow, registerNode, audio,
150
+ defineGame, composeCampaign, knob, type World } from '@hayao';
151
+ import { makeToggle, flip, type ToggleState } from './logic';
152
+
153
+ // Generate a small, proven, ramped campaign at load — every level solver-winnable.
154
+ const CAMPAIGN = composeCampaign<ToggleState, number>({
155
+ seed: 7,
156
+ acts: [
157
+ { name: 'Warmup', count: 6, minDepth: 2, maxDepth: 3, factory: (r) => makeToggle(r, 3, 3, 5) },
158
+ { name: 'Deeper', count: 8, minDepth: 4, maxDepth: 6, factory: (r) => makeToggle(r, 4, 4, 12) },
159
+ ],
160
+ });
161
+ export const LEVELS = CAMPAIGN.levels;
162
+
163
+ const W = 1280, H = 720;
164
+
165
+ class GameView extends Node {
166
+ override readonly type = 'GameView';
167
+ idx = 0;
168
+ lit: number[] = LEVELS[0].puzzle.initial().lit;
169
+ cursor = 0;
170
+ private layer = new Node({ name: 'layer' });
171
+
172
+ protected override onReady(): void { this.layer.cosmetic = true; this.addChild(this.layer); this.paint(); }
173
+ private dims() { const s = LEVELS[this.idx].puzzle.initial(); return { cols: s.cols, rows: s.rows }; }
174
+
175
+ private paint(): void {
176
+ for (const c of this.layer.children.slice()) this.layer.removeChild(c);
177
+ const { cols, rows } = this.dims();
178
+ // Studio knob: adjust live on /studio/ while playing, accept, then ask the
179
+ // agent to write the accepted value back into the tuning default below.
180
+ const round = (this.world as World).tune<number>('cellRound');
181
+ const cell = 120, ox = W/2 - (cols*cell)/2 + cell/2, oy = H/2 - (rows*cell)/2 + cell/2;
182
+ for (let i=0;i<cols*rows;i++){
183
+ const x = ox + (i%cols)*cell, y = oy + Math.floor(i/cols)*cell, on = this.lit[i]===1;
184
+ this.layer.addChild(new Sprite({ name:\`c\${i}\`, pos:{x,y}, z:1,
185
+ shape:{kind:'rect', w:cell*0.7, h:cell*0.7, r:round},
186
+ fill: on ? KENTO.kaki : withAlpha(KENTO.kinako, 0.15),
187
+ gradient: on ? linearGradient([KENTO.ko, KENTO.shuDeep], 90) : undefined,
188
+ stroke: withAlpha(KENTO.gofun, on?0.5:0.2), strokeWidth: 2,
189
+ shadow: on ? glow(withAlpha(KENTO.kaki,0.8), 40) : undefined }));
190
+ }
191
+ const cp = { x: ox + (this.cursor%cols)*cell, y: oy + Math.floor(this.cursor/cols)*cell };
192
+ this.layer.addChild(new Sprite({ name:'cursor', pos:cp, z:5, shape:{kind:'rect', w:cell*0.82, h:cell*0.82, r:22}, fill:'none', stroke:KENTO.asagi, strokeWidth:3 }));
193
+ this.layer.addChild(new Text({ name:'hud', text:\`\${LEVELS[this.idx].actName} — level \${this.idx+1}/\${LEVELS.length} · arrows move · space tap\`, pos:{x:W/2,y:60}, size:24, align:'center', fill:KENTO.gofun }));
194
+ }
195
+
196
+ private tap(): void {
197
+ const { cols, rows } = this.dims();
198
+ this.lit = flip(this.lit, this.cursor, cols, rows); audio.blip(360);
199
+ if (this.lit.every(v=>v===1)) { this.idx = (this.idx+1) % LEVELS.length; this.lit = LEVELS[this.idx].puzzle.initial().lit; this.cursor = 0; audio.success(); }
200
+ this.paint();
201
+ }
202
+ private move(dx:number,dy:number): void {
203
+ const { cols, rows } = this.dims();
204
+ const x = Math.max(0,Math.min(cols-1,(this.cursor%cols)+dx));
205
+ const y = Math.max(0,Math.min(rows-1,Math.floor(this.cursor/cols)+dy));
206
+ this.cursor = y*cols+x; this.paint();
207
+ }
208
+ protected override onProcess(): void {
209
+ if (!this.world) return; const inp = (this.world as World).input;
210
+ if (inp.justPressed('up')) this.move(0,-1);
211
+ if (inp.justPressed('down')) this.move(0,1);
212
+ if (inp.justPressed('left')) this.move(-1,0);
213
+ if (inp.justPressed('right')) this.move(1,0);
214
+ if (inp.justPressed('confirm')) this.tap();
215
+ }
216
+ protected override serializeProps() { return { idx: this.idx, lit: this.lit, cursor: this.cursor }; }
217
+ override applyProps(p: Record<string, unknown>) { if (p.lit){ this.idx=p.idx as number; this.lit=p.lit as number[]; this.cursor=p.cursor as number; this.paint(); } }
218
+ }
219
+ registerNode('GameView', () => new GameView());
220
+
221
+ export const game = defineGame({
222
+ title: '${slug}', width: W, height: H, background: KENTO.yohaku,
223
+ // Live-tunable knobs: the /studio/ page builds sliders from this spec, values
224
+ // are hashed sim state read via world.tune(). Defaults ARE the config.
225
+ tuning: { knobs: [knob.num('cellRound', { default: 18, min: 0, max: 40, step: 1, group: 'look' })] },
226
+ build: () => new GameView({ name: 'game' }),
227
+ probe: (w) => { const v = w.root.find('game') as GameView | null; return { level: v?.idx ?? 0, lit: v ? v.lit.reduce((a,b)=>a+b,0) : 0 }; },
228
+ });
229
+ `,
230
+
231
+ 'main.ts': `import { runStudio } from '@hayao';
232
+ import { game } from './game';
233
+ import { variants } from './variants';
234
+
235
+ // Studio-instrumented dev driver: playtests record to the dev server,
236
+ // ?seed=/?tuning=/?variant= override, the /studio/ page drives the knobs, and
237
+ // \`hot\` carries the live world across code edits. Production builds behave
238
+ // like plain runBrowser (the Studio endpoints simply aren't there).
239
+ runStudio(game, document.getElementById('app')!, { variants, hot: import.meta.hot });
240
+ // Literal self-accept — Vite marks HMR boundaries by static source scan, so
241
+ // this line (not a call inside the engine) is what prevents full reloads.
242
+ import.meta.hot?.accept();
243
+ `,
244
+
245
+ 'variants.ts': `// A/B variants for Studio playtests: pick with ?variant=<name> or compare two
246
+ // side by side on the /studio/ page. Tuning-only variants hot-toggle mid-play.
247
+ import type { Variant } from '@hayao';
248
+
249
+ export const variants: Record<string, Variant> = {
250
+ chunky: { label: 'Chunky — big rounded cells', tuning: { cellRound: 34 } },
251
+ sharp: { label: 'Sharp — square cells', tuning: { cellRound: 2 } },
252
+ };
253
+ `,
254
+
255
+ 'verify.ts': `// Prove the game correct — no browser. Run: npm run verify
256
+ import { solve, rampIssues } from '@hayao';
257
+ import { LEVELS } from './game';
258
+
259
+ let failures = 0;
260
+ const ok = (label: string, pass: boolean) => { console.log(\` \${pass?'✓':'✗'} \${label}\`); if (!pass) failures++; };
261
+
262
+ // Every generated level is solver-proven winnable.
263
+ let allWin = true;
264
+ for (const l of LEVELS) { const r = solve(l.puzzle); if (!r.solvable) allWin = false; }
265
+ ok(\`all \${LEVELS.length} levels solver-proven winnable\`, allWin);
266
+
267
+ // The difficulty curve is well-shaped.
268
+ const problems = rampIssues(LEVELS.map(l => l.depth));
269
+ ok(problems.length === 0 ? 'difficulty ramps cleanly' : \`ramp: \${problems[0]}\`, problems.length === 0);
270
+
271
+ console.log(failures === 0 ? '\\nAll checks passed.' : \`\\n\${failures} failed.\`);
272
+ process.exit(failures === 0 ? 0 : 1);
273
+ `,
274
+
275
+ 'game.test.ts': `import { describe, expect, it } from 'vitest';
276
+ import { Rng, solve } from '@hayao';
277
+ import { makeToggle, flip } from './logic';
278
+ import { LEVELS } from './game';
279
+
280
+ describe('toggle puzzle', () => {
281
+ it('flip is self-inverse', () => {
282
+ const a = new Array(9).fill(0);
283
+ expect(flip(flip(a, 4, 3, 3), 4, 3, 3)).toEqual(a);
284
+ });
285
+ it('generated boards are always winnable', () => {
286
+ for (let s=0;s<20;s++) expect(solve(makeToggle(new Rng(s), 4, 4, 10)).solvable).toBe(true);
287
+ });
288
+ it('the shipped campaign is winnable end to end', () => {
289
+ for (const l of LEVELS) expect(solve(l.puzzle).solvable).toBe(true);
290
+ });
291
+ });
292
+ `,
293
+
294
+ 'AGENTS.md': `# AGENTS.md — working in this hayao project
295
+
296
+ You are the game developer. The engine (npm package \`hayao\`) pre-solves the traps:
297
+ headless verification, determinism, crisp rendering, DOM menus. Hold the invariants.
298
+
299
+ ## Commands
300
+ - \`npm run dev\` — vite dev server (play in a browser)
301
+ - \`npm run check\` — typecheck (must pass before handoff)
302
+ - \`npm test\` — vitest (headless, runs in Node)
303
+ - \`npm run verify\` — prove every level winnable + the ramp well-shaped, no browser
304
+
305
+ ## Invariants (determinism is sacred)
306
+ - **Import only from \`@hayao\`.** The whole public surface is one seam.
307
+ - **All randomness flows through a seeded \`Rng\`** — never \`Math.random\`, \`Date.now\`,
308
+ or argless \`new Date\` in game logic.
309
+ - **Turn-based/puzzle rules live in a pure \`Puzzle<State, Move>\` module** (logic.ts);
310
+ the scene tree is a *view*. Every level is machine-proven winnable via \`solve()\`.
311
+ - **Menus are DOM overlays; mark pure-view nodes \`cosmetic = true\`** so they stay out
312
+ of \`world.hash()\`.
313
+ - **Generate content, don't hand-author volume.** \`generateLevels\` / \`composeCampaign\`
314
+ emit solver-verified levels inside a difficulty band; \`rampIssues\`/\`assertRamp\`
315
+ prove the curve. See game.ts for the pattern.
316
+
317
+ ## Where things are
318
+ - \`logic.ts\` — pure puzzle rules (the truth)
319
+ - \`game.ts\` — scene-tree view + the generated campaign + tuning knobs
320
+ - \`variants.ts\` — named A/B alternatives for Studio playtests
321
+ - \`verify.ts\` — the proof harness (winnable + ramp)
322
+
323
+ ## The Studio (human playtests → your context)
324
+ \`npm run dev\` then open \`/studio/\` — the human plays there; every session
325
+ records to \`.studio/\` as a bit-exactly replayable artifact. The
326
+ \`hayao-studio\` MCP server (.mcp.json) is YOUR window into them:
327
+ \`list_sessions\`, \`get_playtest_report\` (hesitations, deaths, futile verbs,
328
+ quit context), \`inspect_moment\` (replay any tick → probe + screenshot),
329
+ \`get_knob_state\` (values the human accepted — write them back into the
330
+ \`tuning:\` defaults in game.ts, then \`npm run verify\`). Telemetry describes,
331
+ the human directs: propose fixes from the data, never auto-apply.
332
+ Install \`@resvg/resvg-js\` as a devDependency to enable inspect_moment PNGs.
333
+
334
+ Full docs & the greppable API digest: https://github.com/hellojanpacan/hayao-js
335
+ `,
336
+
337
+ 'llms.txt': `# ${slug} (built with hayao)
338
+
339
+ > A hayao game. hayao is an AI-first, deterministic, headless-verifiable game engine.
340
+ > Author in logic.ts (pure Puzzle) + game.ts (scene view); prove with \`npm run verify\`.
341
+
342
+ - Import only from \`@hayao\`. All randomness through a seeded \`Rng\`.
343
+ - Content is generated and solver-proven: see \`composeCampaign\` in game.ts.
344
+ - Engine docs: https://raw.githubusercontent.com/hellojanpacan/hayao-js/main/AGENTS.md
345
+ - API digest: https://raw.githubusercontent.com/hellojanpacan/hayao-js/main/docs/API.md
346
+ `,
347
+
348
+ 'README.md': `# ${slug}
349
+
350
+ A game built with [hayao](https://github.com/hellojanpacan/hayao-js) — an AI-first,
351
+ deterministic, headless-verifiable game engine.
352
+
353
+ \`\`\`sh
354
+ npm install
355
+ npm run dev # play in a browser
356
+ npm test # headless tests
357
+ npm run verify # prove every level winnable + the ramp well-shaped
358
+ \`\`\`
359
+
360
+ Every level in this game is **machine-generated and solver-proven winnable** — the
361
+ content is composed by \`composeCampaign\` (see \`game.ts\`), not hand-authored. Read
362
+ \`AGENTS.md\` for the conventions an AI author should hold.
363
+ `,
364
+
365
+ '.gitignore': `node_modules\ndist\n*.log\n.studio/\n`,
366
+ };
367
+
368
+ mkdirSync(dir, { recursive: true });
369
+ for (const [rel, content] of Object.entries(files)) {
370
+ const p = join(dir, rel);
371
+ mkdirSync(join(p, '..'), { recursive: true });
372
+ writeFileSync(p, content);
373
+ }
374
+
375
+ console.log(`\n✨ Scaffolded a hayao game in ${name}/\n`);
376
+ console.log(` cd ${name}`);
377
+ console.log(' npm install');
378
+ console.log(' npm run verify # prove it correct — no browser');
379
+ console.log(' npm run dev # play it\n');
380
+ console.log('An agent working here should read AGENTS.md first.\n');
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // hayao-mcp — start the Studio MCP sidecar in a hayao project.
3
+ // Spawns dist/mcp.js under tsx so the project's TS game modules and its
4
+ // tsconfig '@hayao' path resolve. Register in .mcp.json as:
5
+ // { "mcpServers": { "hayao-studio": { "command": "npx", "args": ["hayao-mcp"] } } }
6
+ import { spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const server = fileURLToPath(new URL('../dist/mcp.js', import.meta.url));
10
+ const child = spawn('npx', ['tsx', server], { stdio: 'inherit' });
11
+ child.on('exit', (code) => process.exit(code ?? 0));
@@ -1,5 +1,6 @@
1
- import type { GameDefinition } from './game';
2
- import { KeyboardSource } from '../input/source';
1
+ import type { CreateWorldOptions, GameDefinition } from './game';
2
+ import { KeyboardSource, PointerSource } from '../input/source';
3
+ import type { Vec2 } from '../core/math';
3
4
  import type { Renderer } from '../render/renderer';
4
5
  import type { World } from '../world';
5
6
  export interface RunOptions {
@@ -7,12 +8,42 @@ export interface RunOptions {
7
8
  /** Start the pause/settings shell (Esc). Default true. */
8
9
  shell?: boolean;
9
10
  onRestart?: () => void;
11
+ /** World creation overrides (seed, tuning) — applied on start AND restart. */
12
+ world?: CreateWorldOptions;
13
+ /**
14
+ * Observer hook: called after each advance that ran ≥1 step, with the step
15
+ * count and the action set those steps saw. Every step in the batch saw the
16
+ * SAME actions and axes, so recording `steps × (actions, axes)` replays
17
+ * exactly. Studio's session recorder rides this; it must never mutate.
18
+ */
19
+ onAdvance?: (world: World, steps: number, actions: readonly string[]) => void;
20
+ /** Shell pause/resume observer (true = paused). */
21
+ onPause?: (paused: boolean) => void;
22
+ /**
23
+ * Freeze gate: while it returns true the loop keeps rendering but runs no
24
+ * steps (no pause overlay — Studio's scrubber holds the sim with this).
25
+ */
26
+ isHeld?: () => boolean;
10
27
  }
11
28
  export interface GameHandle {
12
29
  world: World;
13
30
  renderer: Renderer;
14
31
  /** The live input source — game UI calls input.press('action') for buttons. */
15
32
  input: KeyboardSource;
33
+ /**
34
+ * Continuous pointer/touch in DESIGN space, sampled into world.input.axes each
35
+ * step (pointer.x / pointer.y / pointer.down). Read `handle.pointer.read()` for
36
+ * a one-off, or `world.input.axis('pointer.x')` inside the sim.
37
+ */
38
+ pointer: PointerSource;
39
+ /** The mounted canvas/svg node (attach your own listeners here if needed). */
40
+ canvas: HTMLElement | SVGElement | undefined;
41
+ /** Map a pointer event's clientX/Y to design coordinates (undoes the letterbox). */
42
+ toDesign(clientX: number, clientY: number): Vec2;
43
+ /** Resolves after preload completes and the first real frame has rendered. */
44
+ ready: Promise<void>;
45
+ /** Run a callback once the game is ready (fires immediately if already ready). */
46
+ onReady(cb: () => void): void;
16
47
  stop(): void;
17
48
  restart(): void;
18
49
  }