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
package/README.md CHANGED
@@ -2,7 +2,10 @@
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 26 machine-verified example games at [hayao.dev](https://hayao.dev/).**
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/).
6
9
 
7
10
  **An AI-first game engine.** A deterministic, headless-native simulation kernel
8
11
  with a Godot-style scene tree, pluggable renderers (SVG / Canvas / headless),
@@ -41,7 +44,74 @@ npm run verify # run the determinism + solver harness over all examples
41
44
  ```
42
45
 
43
46
  A game is one folder under `examples/<slug>/`, imports **only** from `@hayao`,
44
- 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).
45
115
 
46
116
  ## The layers
47
117
 
@@ -80,35 +150,54 @@ GUIs, binary scenes, and a bespoke language, all opaque to an LLM; hayao keeps
80
150
  the model and makes every part **text, typed, greppable, and headlessly
81
151
  verifiable**. Canvas engines couple the sim to the render loop and the browser;
82
152
  hayao decouples them so the sim is a pure function you can test in Node. Full
83
- 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).
84
154
 
85
155
  ## Documentation
86
156
 
87
- - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — the authoritative design and the determinism contract.
88
- - [docs/CONVENTIONS.md](docs/CONVENTIONS.md) — how games are structured, house style, definition of done.
89
- - [docs/VERIFICATION.md](docs/VERIFICATION.md) — the two verification channels; how to prove a game correct.
90
- - [docs/ENGINE.md](docs/ENGINE.md) — why a custom engine, and when NOT to use one.
91
- - [docs/LESSONS.md](docs/LESSONS.md) — transferable lessons from real LLM-authored game builds.
92
- - [docs/FRICTION.md](docs/FRICTION.md) — process-lesson log: what fought an AI session and what check/doc now prevents it.
93
- - [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`.
94
177
 
95
178
  ## Status
96
179
 
97
- **v0.2 — the 20-genre campaign is complete, plus the waves it triggered.**
98
- Twenty-six games live under `examples/`. The core twenty-one cover the most
99
- popular 2D indie genres (platformer, metroidvania, Zelda-like, stealth, horde
100
- survival, bullet hell, tower defense, RTS, roguelike, deckbuilder, tactics,
101
- match-3, idle, farming, horror, city builder, rhythm, physics arcade, racing,
102
- narrative plus the original Sokoban). On top of those sit two js13k-benchmark
103
- reproductions (Seamfold, Gravewell), a deterministic rigid-body physics wave
104
- (Rookspire demolition, Brasswick pinball), and a netplay showcase (Fernclash —
105
- lockstep + rollback across tabs). Every one ships a `verify.ts` suite that
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
106
190
  machine-proves its truth: solver-proven puzzles, bot-beaten levels, duel-proven
107
- counter systems, win-rate-tuned balance, fairness gates for procgen,
108
- frame-exact timing windows, bit-for-bit peer agreement, golden replay hashes.
109
- 400+ tests; `npm run verify` runs the whole portfolio. The campaign's findings — what each
110
- genre demanded of the engine and what generalizedlive in
111
- [docs/BUILDLOG.md](docs/BUILDLOG.md).
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).
112
201
 
113
202
  ## License
114
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
  }