@playcraft/cli 0.0.53 → 0.0.56

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 (242) hide show
  1. package/README.md +21 -0
  2. package/REMIX.md +103 -19
  3. package/dist/atom-tree/fusion-hints.js +8 -3
  4. package/dist/atom-tree/fusion-intent.js +62 -0
  5. package/dist/atom-tree/selection.js +3 -1
  6. package/dist/commands/create.js +439 -159
  7. package/dist/commands/image.js +79 -69
  8. package/dist/commands/levels.js +29 -0
  9. package/dist/commands/patterns.js +75 -0
  10. package/dist/commands/remix.js +47 -0
  11. package/dist/commands/skills.js +48 -17
  12. package/dist/index.js +13 -6
  13. package/dist/remix/clone-source.js +208 -0
  14. package/dist/remix/clone.js +255 -0
  15. package/dist/scaffold/templates.js +35 -0
  16. package/dist/utils/agent-api-client.js +10 -3
  17. package/dist/utils/remix-image-attachments.js +4 -32
  18. package/dist/utils/sandbox-path.js +20 -0
  19. package/package.json +3 -3
  20. package/project-template/.claude/agents/creative-director.md +114 -0
  21. package/project-template/.claude/agents/developer.md +5 -2
  22. package/project-template/.claude/agents/game-designer.md +41 -20
  23. package/project-template/.claude/agents/refs/README.md +8 -0
  24. package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
  25. package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  26. package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
  27. package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
  28. package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
  29. package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  30. package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
  31. package/project-template/.claude/agents/reviewer.md +11 -0
  32. package/project-template/.claude/hooks/README.md +12 -12
  33. package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
  34. package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
  35. package/project-template/.claude/hooks/user-prompt.mjs +3 -13
  36. package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
  37. package/project-template/.claude/settings.json +2 -2
  38. package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  39. package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
  40. package/project-template/.claude/snapshot.mjs +0 -1
  41. package/project-template/.codebuddy/settings.json +1 -1
  42. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  43. package/project-template/CLAUDE.md +9 -9
  44. package/project-template/docs/project-state.md +1 -1
  45. package/project-template/docs/team/agent-conduct.md +18 -18
  46. package/project-template/docs/team/collaboration.md +19 -7
  47. package/project-template/docs/team/workflow-changelog.md +15 -0
  48. package/project-template/fusion-patterns/catalog.json +77 -0
  49. package/project-template/package.json +2 -0
  50. package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
  51. package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
  52. package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
  53. package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
  54. package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  55. package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  56. package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
  57. package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
  58. package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
  59. package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  60. package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  61. package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  62. package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
  63. package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
  64. package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  65. package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  66. package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
  67. package/project-template/templates/design-brief.template.md +34 -0
  68. package/project-template/templates/design-methodology.md +1 -2
  69. package/project-template/templates/design.template.md +65 -0
  70. package/project-template/templates/developer-log.template.md +12 -1
  71. package/project-template/templates/five-axis-framework.md +12 -0
  72. package/project-template/templates/game-designer-creative-process.template.md +99 -0
  73. package/project-template/templates/level-sketch.template.json +56 -0
  74. package/project-template/templates/north-star.template.md +43 -0
  75. package/project-template/templates/project-state.template.md +1 -1
  76. package/project-template/vite.config.ts +4 -5
  77. package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
  78. package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
  79. package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
  80. package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  81. package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
  82. package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
  83. package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
  84. package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  85. package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
  86. package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  87. package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
  88. package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
  89. package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  90. package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  91. package/project-templates/phaser/CLAUDE.md +88 -54
  92. package/project-templates/phaser/docs/DESIGN.md +25 -0
  93. package/project-templates/phaser/docs/project-state.md +1 -1
  94. package/project-templates/phaser/docs/team/collaboration.md +19 -7
  95. package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
  96. package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
  97. package/project-templates/phaser/package.json +3 -1
  98. package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
  99. package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
  100. package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
  101. package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  102. package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  103. package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
  104. package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
  105. package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  106. package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  107. package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  108. package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
  109. package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
  110. package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  111. package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  112. package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
  113. package/project-templates/phaser/templates/design-brief.template.md +42 -11
  114. package/project-templates/phaser/templates/design-methodology.md +1 -2
  115. package/project-templates/phaser/templates/design.template.md +77 -0
  116. package/project-templates/phaser/templates/five-axis-framework.md +50 -1
  117. package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
  118. package/project-templates/phaser/templates/level-sketch.template.json +56 -0
  119. package/project-templates/phaser/templates/north-star.template.md +43 -0
  120. package/project-templates/phaser/templates/project-state.template.md +1 -1
  121. package/project-templates/phaser/vite.config.ts +4 -5
  122. package/project-templates/playable-template-phaser/.playcraftignore +10 -0
  123. package/project-templates/playable-template-phaser/CLAUDE.md +178 -0
  124. package/project-templates/playable-template-phaser/PLAYCRAFT-MIGRATION.md +328 -0
  125. package/project-templates/playable-template-phaser/README.md +381 -0
  126. package/project-templates/playable-template-phaser/assets/brain.png +0 -0
  127. package/project-templates/playable-template-phaser/assets/button.png +0 -0
  128. package/project-templates/playable-template-phaser/assets/interact.png +0 -0
  129. package/project-templates/playable-template-phaser/assets/logo.webp +0 -0
  130. package/project-templates/playable-template-phaser/assets/warn_brain.png +0 -0
  131. package/project-templates/playable-template-phaser/builds.config.js +12 -0
  132. package/project-templates/playable-template-phaser/globals.d.ts +2 -0
  133. package/project-templates/playable-template-phaser/index.html +12 -0
  134. package/project-templates/playable-template-phaser/package.json +45 -0
  135. package/project-templates/playable-template-phaser/scripts/base64-to-mp3.js +340 -0
  136. package/project-templates/playable-template-phaser/scripts/convert-to-webp.js +370 -0
  137. package/project-templates/playable-template-phaser/src/Game.ts +154 -0
  138. package/project-templates/playable-template-phaser/src/config/LoadingConfig.ts +40 -0
  139. package/project-templates/playable-template-phaser/src/config/ThemeConfig.ts +81 -0
  140. package/project-templates/playable-template-phaser/src/index.css +18 -0
  141. package/project-templates/playable-template-phaser/src/index.html +9 -0
  142. package/project-templates/playable-template-phaser/src/index.ts +30 -0
  143. package/project-templates/playable-template-phaser/src/json5.d.ts +4 -0
  144. package/project-templates/playable-template-phaser/src/managers/AudioManager.ts +559 -0
  145. package/project-templates/playable-template-phaser/src/scenes/MainScene.ts +301 -0
  146. package/project-templates/playable-template-phaser/src/scenes/PreloadScene.ts +256 -0
  147. package/project-templates/playable-template-phaser/src/theme/README.md +194 -0
  148. package/project-templates/playable-template-phaser/src/theme/blue-theme/assets/.gitkeep +24 -0
  149. package/project-templates/playable-template-phaser/src/theme/blue-theme/index.ts +27 -0
  150. package/project-templates/playable-template-phaser/src/theme/blue-theme/theme.data.json5 +19 -0
  151. package/project-templates/playable-template-phaser/src/theme/green-theme/assets/.gitkeep +3 -0
  152. package/project-templates/playable-template-phaser/src/theme/green-theme/index.ts +20 -0
  153. package/project-templates/playable-template-phaser/src/theme/green-theme/theme.data.json5 +19 -0
  154. package/project-templates/playable-template-phaser/src/theme/index.ts +1 -0
  155. package/project-templates/playable-template-phaser/src/theme/red-theme/assets/.gitkeep +3 -0
  156. package/project-templates/playable-template-phaser/src/theme/red-theme/index.ts +20 -0
  157. package/project-templates/playable-template-phaser/src/theme/red-theme/theme.data.json5 +19 -0
  158. package/project-templates/playable-template-phaser/src/theme/theme.metadata.md +60 -0
  159. package/project-templates/playable-template-phaser/src/theme/theme.schema.json5 +149 -0
  160. package/project-templates/playable-template-phaser/src/ui/BrainAgeUI.ts +681 -0
  161. package/project-templates/playable-template-phaser/src/ui/CountdownUI.ts +565 -0
  162. package/project-templates/playable-template-phaser/src/ui/PlayButton.ts +51 -0
  163. package/project-templates/playable-template-phaser/src/ui/ScoreUI.ts +207 -0
  164. package/project-templates/playable-template-phaser/src/ui/SuccessUI.ts +43 -0
  165. package/project-templates/playable-template-phaser/src/ui/index.ts +15 -0
  166. package/project-templates/playable-template-phaser/tsconfig.json +26 -0
  167. package/project-templates/playable-template-phaser/vite.config.ts +64 -0
  168. package/project-templates/playable-template-phaser//346/226/260/346/211/213/345/274/200/345/217/221/346/214/207/345/215/227.md +209 -0
  169. package/project-templates/playable-template-three/README.md +795 -0
  170. package/project-templates/playable-template-three/assets/Stork.glb +0 -0
  171. package/project-templates/playable-template-three/assets/button.png +0 -0
  172. package/project-templates/playable-template-three/assets/logo.webp +0 -0
  173. package/project-templates/playable-template-three/build.json +7 -0
  174. package/project-templates/playable-template-three/builds.config.example.js +151 -0
  175. package/project-templates/playable-template-three/builds.config.js +158 -0
  176. package/project-templates/playable-template-three/package.json +36 -0
  177. package/project-templates/playable-template-three/scripts/convert-to-webp.js +370 -0
  178. package/project-templates/playable-template-three/scripts/theme-builds.js +514 -0
  179. package/project-templates/playable-template-three/src/Game.ts +215 -0
  180. package/project-templates/playable-template-three/src/config/LoadingConfig.ts +27 -0
  181. package/project-templates/playable-template-three/src/config/ThemeConfig.ts +36 -0
  182. package/project-templates/playable-template-three/src/global.d.ts +108 -0
  183. package/project-templates/playable-template-three/src/index.css +18 -0
  184. package/project-templates/playable-template-three/src/index.html +9 -0
  185. package/project-templates/playable-template-three/src/index.ts +20 -0
  186. package/project-templates/playable-template-three/src/managers/AudioManager.ts +538 -0
  187. package/project-templates/playable-template-three/src/scenes/PreloadScene.ts +305 -0
  188. package/project-templates/playable-template-three/src/theme/README.md +225 -0
  189. package/project-templates/playable-template-three/src/theme/blue-theme/assets/.gitkeep +28 -0
  190. package/project-templates/playable-template-three/src/theme/blue-theme/index.ts +45 -0
  191. package/project-templates/playable-template-three/src/theme/current.ts +12 -0
  192. package/project-templates/playable-template-three/src/theme/green-theme/assets/.gitkeep +3 -0
  193. package/project-templates/playable-template-three/src/theme/green-theme/index.ts +34 -0
  194. package/project-templates/playable-template-three/src/theme/red-theme/assets/.gitkeep +3 -0
  195. package/project-templates/playable-template-three/src/theme/red-theme/index.ts +34 -0
  196. package/project-templates/playable-template-three/tsconfig.json +24 -0
  197. package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
  198. package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
  199. package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
  200. package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  201. package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
  202. package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
  203. package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
  204. package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  205. package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
  206. package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  207. package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
  208. package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
  209. package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  210. package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  211. package/project-templates/threejs/CLAUDE.md +88 -54
  212. package/project-templates/threejs/docs/DESIGN.md +25 -0
  213. package/project-templates/threejs/docs/project-state.md +1 -1
  214. package/project-templates/threejs/docs/team/collaboration.md +19 -7
  215. package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
  216. package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
  217. package/project-templates/threejs/package.json +3 -1
  218. package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
  219. package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
  220. package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
  221. package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  222. package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  223. package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
  224. package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
  225. package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  226. package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  227. package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  228. package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
  229. package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
  230. package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  231. package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  232. package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
  233. package/project-templates/threejs/templates/design-brief.template.md +42 -11
  234. package/project-templates/threejs/templates/design-methodology.md +1 -2
  235. package/project-templates/threejs/templates/design.template.md +77 -0
  236. package/project-templates/threejs/templates/five-axis-framework.md +50 -1
  237. package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
  238. package/project-templates/threejs/templates/level-sketch.template.json +56 -0
  239. package/project-templates/threejs/templates/north-star.template.md +43 -0
  240. package/project-templates/threejs/templates/project-state.template.md +1 -1
  241. package/project-templates/threejs/vite.config.ts +4 -5
  242. package/project-template/docs/DESIGN.md +0 -65
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validate docs/level-sketch.json — solvability + puzzle identity + difficulty profiles.
4
+ * Pure JS — no TS runtime dependency.
5
+ *
6
+ * Env: LEVEL_DIFFICULTY_PROFILE=ring1_min|standard|hard
7
+ */
8
+ import { readFileSync, existsSync } from 'node:fs';
9
+ import { join, resolve, basename } from 'node:path';
10
+
11
+ export const DIFFICULTY_PROFILES = {
12
+ ring1_min: {
13
+ boardRow: 8,
14
+ boardCol: 8,
15
+ minPaths: 6,
16
+ minSegments: 12,
17
+ minDependencyDepth: 2,
18
+ maxOpeningRemovable: 2,
19
+ maxTrayPeak: 5,
20
+ },
21
+ standard: {
22
+ boardRow: 8,
23
+ boardCol: 8,
24
+ minPaths: 9,
25
+ minSegments: 18,
26
+ minDependencyDepth: 3,
27
+ maxOpeningRemovable: 3,
28
+ maxTrayPeak: 6,
29
+ },
30
+ hard: {
31
+ boardRow: 8,
32
+ boardCol: 8,
33
+ minPaths: 12,
34
+ minSegments: 24,
35
+ minDependencyDepth: 4,
36
+ maxOpeningRemovable: 1,
37
+ maxTrayPeak: 7,
38
+ },
39
+ };
40
+
41
+ function parseArgs(argv) {
42
+ const opts = { root: process.cwd(), sketch: 'docs/level-sketch.json', json: false };
43
+ for (const arg of argv) {
44
+ if (arg === '--json') opts.json = true;
45
+ else if (arg.startsWith('--root=')) opts.root = resolve(arg.slice('--root='.length));
46
+ else if (arg.startsWith('--sketch=')) opts.sketch = arg.slice('--sketch='.length);
47
+ else if (arg.startsWith('--profile=')) opts.profile = arg.slice('--profile='.length);
48
+ }
49
+ return opts;
50
+ }
51
+
52
+ /** @typedef {{ x: number, y: number }} GridPoint */
53
+ /** @typedef {{ points: GridPoint[], head: GridPoint, direction: string, color: string, indices: number[], segments: number }} LevelPath */
54
+ /** @typedef {{ index: number, rows: number, cols: number, paths: LevelPath[], levelTime: number, trayCapacity: number, matchCount: number, boardPhases: unknown[] | null }} ParsedLevel */
55
+
56
+ function pathSegments(points) {
57
+ return points.length <= 1 ? 0 : points.length - 1;
58
+ }
59
+
60
+ /** @param {unknown} raw @returns {ParsedLevel} */
61
+ export function parseLevelSketch(raw) {
62
+ let target = raw;
63
+ if (Array.isArray(raw)) {
64
+ const maybe = raw[5];
65
+ if (Array.isArray(maybe) && Array.isArray(maybe[0]) && maybe[0].length >= 3) {
66
+ target = maybe[0][2];
67
+ }
68
+ }
69
+ if (typeof target !== 'object' || target === null) {
70
+ throw new Error('Invalid level sketch: expected object');
71
+ }
72
+ const obj = /** @type {Record<string, unknown>} */ (target);
73
+ const rows = typeof obj.row === 'number' ? obj.row : 8;
74
+ const cols = typeof obj.col === 'number' ? obj.col : 8;
75
+ const trayCapacity = typeof obj.trayCapacity === 'number' ? obj.trayCapacity : 7;
76
+ const matchCount = typeof obj.matchCount === 'number' ? obj.matchCount : 3;
77
+ const colorList = Array.isArray(obj.colors) ? obj.colors : [];
78
+ const rawArrows = Array.isArray(obj.arrows) ? obj.arrows : [];
79
+
80
+ const paths = rawArrows.map((rawPath, pathIndex) => {
81
+ if (!Array.isArray(rawPath)) throw new Error(`paths[${pathIndex}] must be array`);
82
+ const points = rawPath.map((p, pi) => {
83
+ if (!Array.isArray(p) || p.length < 2) throw new Error(`paths[${pathIndex}][${pi}] invalid`);
84
+ const [x, cocosY] = /** @type {[number, number]} */ (p);
85
+ const colorFromPoint = typeof p[2] === 'string' ? p[2] : undefined;
86
+ return { x, y: rows > 0 ? rows - 1 - cocosY : cocosY, colorFromPoint };
87
+ });
88
+ if (points.length === 0) {
89
+ return {
90
+ points: [],
91
+ head: { x: 0, y: 0 },
92
+ direction: 'right',
93
+ color: colorList[pathIndex] ?? `color_${pathIndex % 3}`,
94
+ indices: [],
95
+ segments: 0,
96
+ };
97
+ }
98
+ const head = points[points.length - 1];
99
+ const before = points[Math.max(0, points.length - 2)];
100
+ const dx = head.x - before.x;
101
+ const dy = head.y - before.y;
102
+ let direction = 'right';
103
+ if (Math.abs(dx) >= Math.abs(dy)) direction = dx < 0 ? 'left' : 'right';
104
+ else direction = dy < 0 ? 'up' : 'down';
105
+ const indices = points.map((pt) => pt.y * cols + pt.x);
106
+ const color =
107
+ colorList[pathIndex] ??
108
+ points.find((p) => p.colorFromPoint)?.colorFromPoint ??
109
+ `color_${pathIndex % 3}`;
110
+
111
+ return {
112
+ points: points.map((p) => ({ x: p.x, y: p.y })),
113
+ head,
114
+ direction,
115
+ color,
116
+ indices,
117
+ segments: pathSegments(points.map((p) => [p.x, p.y])),
118
+ };
119
+ });
120
+
121
+ return {
122
+ index: typeof obj.name === 'number' ? obj.name : 0,
123
+ rows,
124
+ cols,
125
+ paths,
126
+ levelTime: typeof obj.levelTime === 'number' ? obj.levelTime : 0,
127
+ trayCapacity,
128
+ matchCount,
129
+ boardPhases: Array.isArray(obj.boardPhases) ? obj.boardPhases : null,
130
+ };
131
+ }
132
+
133
+ /** @param {ParsedLevel} level @param {number} pathIndex @param {Set<number>} removed */
134
+ export function canPathExitBoard(level, pathIndex, removed) {
135
+ const path = level.paths[pathIndex];
136
+ if (!path) return { canExit: false };
137
+ const delta =
138
+ path.direction === 'left'
139
+ ? { dx: -1, dy: 0 }
140
+ : path.direction === 'right'
141
+ ? { dx: 1, dy: 0 }
142
+ : path.direction === 'up'
143
+ ? { dx: 0, dy: -1 }
144
+ : { dx: 0, dy: 1 };
145
+ const occupied = new Set();
146
+ for (let i = 0; i < level.paths.length; i++) {
147
+ if (i === pathIndex || removed.has(i)) continue;
148
+ for (const idx of level.paths[i].indices) occupied.add(idx);
149
+ }
150
+ let x = path.head.x;
151
+ let y = path.head.y;
152
+ for (let step = 0; step < Math.max(level.rows, level.cols) + 2; step += 1) {
153
+ x += delta.dx;
154
+ y += delta.dy;
155
+ if (x < 0 || x >= level.cols || y < 0 || y >= level.rows) return { canExit: true };
156
+ if (occupied.has(y * level.cols + x)) return { canExit: false };
157
+ }
158
+ return { canExit: false };
159
+ }
160
+
161
+ /** @param {ParsedLevel} level */
162
+ export function countOpeningRemovable(level) {
163
+ const removed = new Set();
164
+ let count = 0;
165
+ for (let i = 0; i < level.paths.length; i += 1) {
166
+ if (canPathExitBoard(level, i, removed).canExit) count += 1;
167
+ }
168
+ return count;
169
+ }
170
+
171
+ /** @param {ParsedLevel} level */
172
+ export function computeDependencyDepth(level) {
173
+ const removed = new Set();
174
+ let depth = 0;
175
+ let remaining = level.paths.length;
176
+
177
+ while (remaining > 0) {
178
+ const wave = [];
179
+ for (let i = 0; i < level.paths.length; i += 1) {
180
+ if (removed.has(i)) continue;
181
+ if (canPathExitBoard(level, i, removed).canExit) wave.push(i);
182
+ }
183
+ if (wave.length === 0) return -1;
184
+ depth += 1;
185
+ for (const i of wave) removed.add(i);
186
+ remaining -= wave.length;
187
+ }
188
+ return depth;
189
+ }
190
+
191
+ /** @param {ParsedLevel} level */
192
+ export function totalSegments(level) {
193
+ return level.paths.reduce((sum, p) => sum + p.segments, 0);
194
+ }
195
+
196
+ /** @param {ParsedLevel} level */
197
+ export function occupationRatio(level) {
198
+ const cells = new Set();
199
+ for (const path of level.paths) {
200
+ for (const idx of path.indices) cells.add(idx);
201
+ }
202
+ const area = level.rows * level.cols;
203
+ return area > 0 ? cells.size / area : 0;
204
+ }
205
+
206
+ /** @param {ParsedLevel} level */
207
+ export function simulateTrayPeak(level) {
208
+ const colorGroups = new Map();
209
+ for (const path of level.paths) {
210
+ const list = colorGroups.get(path.color) || [];
211
+ list.push(path.pathIndex);
212
+ colorGroups.set(path.color, list);
213
+ }
214
+
215
+ let traySize = 0;
216
+ let peak = 0;
217
+ for (const [, pathIndices] of colorGroups) {
218
+ traySize += pathIndices.length;
219
+ peak = Math.max(peak, traySize);
220
+ const matches = Math.floor(pathIndices.length / level.matchCount);
221
+ traySize -= matches * level.matchCount;
222
+ }
223
+ return peak;
224
+ }
225
+
226
+ /** @param {ParsedLevel} level */
227
+ export function checkOverlap(level) {
228
+ const errors = [];
229
+ const seen = new Map();
230
+ for (const path of level.paths) {
231
+ for (const idx of path.indices) {
232
+ if (seen.has(idx)) {
233
+ errors.push(`Paths ${seen.get(idx)} and ${path.pathIndex} overlap at cell ${idx}`);
234
+ } else {
235
+ seen.set(idx, path.pathIndex);
236
+ }
237
+ }
238
+ }
239
+ return errors;
240
+ }
241
+
242
+ /** @param {ParsedLevel} level */
243
+ export function validateLevelSolvable(level) {
244
+ const removed = new Set();
245
+ while (removed.size < level.paths.length) {
246
+ let found = null;
247
+ for (let i = 0; i < level.paths.length; i++) {
248
+ if (removed.has(i)) continue;
249
+ if (canPathExitBoard(level, i, removed).canExit) {
250
+ found = i;
251
+ break;
252
+ }
253
+ }
254
+ if (found === null) {
255
+ return {
256
+ solvable: false,
257
+ reason: `greedy stuck after removing ${removed.size}/${level.paths.length} paths`,
258
+ };
259
+ }
260
+ removed.add(found);
261
+ }
262
+ return { solvable: true };
263
+ }
264
+
265
+ /**
266
+ * @param {ParsedLevel} level
267
+ * @param {keyof typeof DIFFICULTY_PROFILES} profileName
268
+ */
269
+ export function validateDifficulty(level, profileName = 'ring1_min') {
270
+ const profile = DIFFICULTY_PROFILES[profileName];
271
+ if (!profile) {
272
+ return { errors: [`Unknown difficulty profile: ${profileName}`], warnings: [], metrics: {} };
273
+ }
274
+
275
+ const errors = [];
276
+ const warnings = [];
277
+
278
+ if (level.rows !== profile.boardRow || level.cols !== profile.boardCol) {
279
+ errors.push(
280
+ `Board must be ${profile.boardRow}×${profile.boardCol} for profile ${profileName}, got ${level.rows}×${level.cols}`,
281
+ );
282
+ }
283
+
284
+ if (level.paths.length < profile.minPaths) {
285
+ errors.push(`Too few paths: ${level.paths.length} < ${profile.minPaths} (${profileName})`);
286
+ }
287
+
288
+ const segments = totalSegments(level);
289
+ if (segments < profile.minSegments) {
290
+ errors.push(`Too few segments: ${segments} < ${profile.minSegments} (${profileName})`);
291
+ }
292
+
293
+ const opening = countOpeningRemovable(level);
294
+ if (opening >= level.paths.length && level.paths.length > 1) {
295
+ errors.push('All paths are FREE at opening — zero decision puzzle (anti-pattern)');
296
+ } else if (opening > profile.maxOpeningRemovable) {
297
+ warnings.push(
298
+ `Opening removable ${opening} > max ${profile.maxOpeningRemovable} for ${profileName}`,
299
+ );
300
+ }
301
+
302
+ const depDepth = computeDependencyDepth(level);
303
+ if (depDepth < 0) {
304
+ errors.push('Dependency graph is not solvable (deadlock in greedy waves)');
305
+ } else if (depDepth < profile.minDependencyDepth) {
306
+ errors.push(`Dependency depth ${depDepth} < ${profile.minDependencyDepth} (${profileName})`);
307
+ }
308
+
309
+ const trayPeak = simulateTrayPeak(level);
310
+ if (trayPeak > profile.maxTrayPeak) {
311
+ errors.push(`Tray peak ${trayPeak} > ${profile.maxTrayPeak} (${profileName})`);
312
+ }
313
+
314
+ const occupancy = occupationRatio(level);
315
+ if (occupancy < 0.4) {
316
+ warnings.push(`Occupancy ${(occupancy * 100).toFixed(0)}% < 40% — board may feel sparse`);
317
+ }
318
+
319
+ return {
320
+ errors,
321
+ warnings,
322
+ metrics: { segmentCount: segments, segments, opening, dependencyDepth: depDepth, depDepth, trayPeakSimulated: trayPeak, trayPeak, occupancy },
323
+ };
324
+ }
325
+
326
+ /** @param {ParsedLevel} level */
327
+ export function validateBoardPhases(level) {
328
+ if (!level.boardPhases || level.boardPhases.length === 0) return [];
329
+ const errors = [];
330
+ for (const [i, phase] of level.boardPhases.entries()) {
331
+ if (!phase || typeof phase !== 'object') {
332
+ errors.push(`boardPhases[${i}] must be an object`);
333
+ continue;
334
+ }
335
+ const p = /** @type {Record<string, unknown>} */ (phase);
336
+ if (!p.phaseId) errors.push(`boardPhases[${i}] missing phaseId`);
337
+ if (!p.activeMask && !p.mask) {
338
+ errors.push(`boardPhases[${i}] missing activeMask or mask`);
339
+ }
340
+ if (!p.unlockAfter) {
341
+ errors.push(`boardPhases[${i}] missing unlockAfter (e.g. matchClears or pathsRemoved)`);
342
+ }
343
+ }
344
+ return errors;
345
+ }
346
+
347
+ /** @param {string} root @param {string} sketchRel @param {{ profile?: string }} [options] */
348
+ export function validateLevelSketch(root, sketchRel = 'docs/level-sketch.json', options = {}) {
349
+ const sketchPath = join(root, sketchRel);
350
+ const profileName =
351
+ options.profile ??
352
+ process.env.LEVEL_DIFFICULTY_PROFILE ??
353
+ 'ring1_min';
354
+
355
+ if (!existsSync(sketchPath)) {
356
+ return {
357
+ ok: false,
358
+ problems: [`level sketch not found: ${sketchRel}`],
359
+ metrics: {
360
+ pathCount: 0,
361
+ openingRemovableCount: 0,
362
+ hasBlockedPaths: false,
363
+ solvable: false,
364
+ segmentCount: 0,
365
+ dependencyDepth: 0,
366
+ trayPeakSimulated: 0,
367
+ },
368
+ profile: profileName,
369
+ };
370
+ }
371
+
372
+ const problems = [];
373
+ const warnings = [];
374
+ let level;
375
+ try {
376
+ level = parseLevelSketch(JSON.parse(readFileSync(sketchPath, 'utf8')));
377
+ } catch (err) {
378
+ return {
379
+ ok: false,
380
+ problems: [`parse error: ${err instanceof Error ? err.message : String(err)}`],
381
+ metrics: {
382
+ pathCount: 0,
383
+ openingRemovableCount: 0,
384
+ hasBlockedPaths: false,
385
+ solvable: false,
386
+ segmentCount: 0,
387
+ dependencyDepth: 0,
388
+ trayPeakSimulated: 0,
389
+ },
390
+ profile: profileName,
391
+ };
392
+ }
393
+
394
+ problems.push(...checkOverlap(level));
395
+ problems.push(...validateBoardPhases(level));
396
+
397
+ const openingRemovableCount = countOpeningRemovable(level);
398
+ const solvability = validateLevelSolvable(level);
399
+ const diff = validateDifficulty(level, profileName);
400
+ warnings.push(...(diff.warnings ?? []));
401
+ problems.push(...diff.errors);
402
+
403
+ if (level.paths.length < 3) {
404
+ problems.push('level sketch should include at least 3 paths for Ring 1 gameplay identity');
405
+ }
406
+ if (!solvability.solvable) {
407
+ problems.push(solvability.reason ?? 'level is not solvable');
408
+ }
409
+
410
+ const metrics = {
411
+ pathCount: level.paths.length,
412
+ openingRemovableCount,
413
+ hasBlockedPaths: level.paths.length > openingRemovableCount,
414
+ solvable: solvability.solvable,
415
+ solvabilityReason: solvability.reason,
416
+ segmentCount: diff.metrics?.segmentCount ?? totalSegments(level),
417
+ dependencyDepth: diff.metrics?.dependencyDepth ?? computeDependencyDepth(level),
418
+ trayPeakSimulated: diff.metrics?.trayPeakSimulated ?? simulateTrayPeak(level),
419
+ occupancy: diff.metrics?.occupancy ?? occupationRatio(level),
420
+ boardSize: `${level.rows}x${level.cols}`,
421
+ };
422
+
423
+ return { ok: problems.length === 0, problems, warnings, metrics, profile: profileName };
424
+ }
425
+
426
+ function main() {
427
+ const opts = parseArgs(process.argv.slice(2));
428
+ const result = validateLevelSketch(opts.root, opts.sketch, { profile: opts.profile });
429
+ if (opts.json) {
430
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
431
+ } else if (result.ok) {
432
+ process.stdout.write(
433
+ `[levels validate] OK (${result.profile}) — paths=${result.metrics.pathCount} openingRemovable=${result.metrics.openingRemovableCount} segments=${result.metrics.segmentCount}\n`,
434
+ );
435
+ for (const w of result.warnings ?? []) process.stderr.write(` WARN: ${w}\n`);
436
+ } else {
437
+ process.stderr.write(`[levels validate] FAIL\n - ${result.problems.join('\n - ')}\n`);
438
+ }
439
+ process.exit(result.ok ? 0 : 2);
440
+ }
441
+
442
+ if (process.argv[1] && basename(process.argv[1]) === 'validate-level-sketch.mjs') {
443
+ main();
444
+ }