@seriousgaming/sdk-tools 0.0.0-stage → 1.0.0-alpha.1

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 (391) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +36 -2
  3. package/dist/bundling/logic-exports.d.ts +34 -0
  4. package/dist/bundling/node-builtins.d.ts +21 -0
  5. package/dist/bundling/transpiler.d.ts +7 -0
  6. package/dist/cli.js +27368 -0
  7. package/dist/client/dev-server.d.ts +59 -0
  8. package/dist/client/index.d.ts +28 -0
  9. package/dist/client/stub-services.d.ts +32 -0
  10. package/dist/client/test-session.d.ts +195 -0
  11. package/dist/client.js +16884 -0
  12. package/dist/core/asset-reference-translator.d.ts +46 -0
  13. package/dist/core/clock.d.ts +57 -0
  14. package/dist/core/faults.d.ts +65 -0
  15. package/dist/core/game-settings.d.ts +65 -0
  16. package/dist/core/history.d.ts +89 -0
  17. package/dist/core/listen-with-fallback.d.ts +30 -0
  18. package/dist/core/mutation-queue.d.ts +61 -0
  19. package/dist/core/package-scenario.d.ts +89 -0
  20. package/dist/core/participant-registry.d.ts +109 -0
  21. package/dist/core/pending-changes.d.ts +14 -0
  22. package/dist/core/protocol.d.ts +83 -0
  23. package/dist/core/scenario-drafts.d.ts +32 -0
  24. package/dist/core/session-endpoints.d.ts +108 -0
  25. package/dist/core/session-gates.d.ts +75 -0
  26. package/dist/core/session-host.d.ts +94 -0
  27. package/dist/core/session-refusals.d.ts +56 -0
  28. package/dist/core/session-rewind.d.ts +32 -0
  29. package/dist/core/session-roster.d.ts +128 -0
  30. package/dist/core/session-surfaces.d.ts +32 -0
  31. package/dist/core/session-ticks.d.ts +18 -0
  32. package/dist/core/session-timers.d.ts +84 -0
  33. package/dist/core/session-writes.d.ts +40 -0
  34. package/dist/core/session.d.ts +385 -0
  35. package/dist/core/settings-locks.d.ts +22 -0
  36. package/dist/core/team-names.d.ts +52 -0
  37. package/dist/core/team-state-store.d.ts +209 -0
  38. package/dist/core/tick-timer.d.ts +75 -0
  39. package/dist/dev/blob-links.d.ts +10 -0
  40. package/dist/dev/console/assets/index-DgFglsnA.js +204 -0
  41. package/dist/dev/console/index.html +12 -0
  42. package/dist/dev/console-path.d.ts +19 -0
  43. package/dist/dev/lockfile.d.ts +124 -0
  44. package/dist/dev/package-watcher.d.ts +21 -0
  45. package/dist/dev/routes.d.ts +67 -0
  46. package/dist/dev/shell.d.ts +260 -0
  47. package/dist/dev/surfaces.d.ts +41 -0
  48. package/dist/dev/tools-identity.d.ts +23 -0
  49. package/dist/dev/view/batch.d.ts +172 -0
  50. package/dist/dev/view/commands.d.ts +75 -0
  51. package/dist/dev/view/route.d.ts +58 -0
  52. package/dist/dev/view/view.d.ts +78 -0
  53. package/dist/dev/view/viewer.d.ts +32 -0
  54. package/dist/discovery/config.d.ts +9 -0
  55. package/dist/discovery/manifest-paths.d.ts +21 -0
  56. package/dist/discovery/removed-fields.d.ts +90 -0
  57. package/dist/docs/api/deck.md +386 -0
  58. package/dist/docs/api/index.md +67 -0
  59. package/dist/docs/api/sdk-commands.md +286 -0
  60. package/dist/docs/api/sdk-e2e.md +433 -0
  61. package/dist/docs/api/sdk-projection.md +207 -0
  62. package/dist/docs/api/sdk-testing.md +214 -0
  63. package/dist/docs/api/sdk-tools-client.md +560 -0
  64. package/dist/docs/api/sdk-ui-testing.md +231 -0
  65. package/dist/docs/api/sdk-ui.md +1269 -0
  66. package/dist/docs/api/sdk.md +2140 -0
  67. package/dist/docs/changelog-0.x.md +1156 -0
  68. package/dist/docs/changelog.md +236 -0
  69. package/dist/docs/cli-and-dev-server.md +217 -0
  70. package/dist/docs/cli-error-codes.md +154 -0
  71. package/dist/docs/cli-reference.md +2488 -0
  72. package/dist/docs/cli-simulation-testing.md +222 -0
  73. package/dist/docs/concepts/index.md +46 -0
  74. package/dist/docs/concepts/terminology.md +322 -0
  75. package/dist/docs/environment-variables.md +102 -0
  76. package/dist/docs/getting-started.md +145 -0
  77. package/dist/docs/guides/ai-facilitation.md +220 -0
  78. package/dist/docs/guides/capabilities.md +460 -0
  79. package/dist/docs/guides/endpoints.md +335 -0
  80. package/dist/docs/guides/engine-testing.md +477 -0
  81. package/dist/docs/guides/engine.md +527 -0
  82. package/dist/docs/guides/file-handling.md +405 -0
  83. package/dist/docs/guides/games.md +176 -0
  84. package/dist/docs/guides/global-settings.md +94 -0
  85. package/dist/docs/guides/guide.md +123 -0
  86. package/dist/docs/guides/hooks.md +183 -0
  87. package/dist/docs/guides/index.md +51 -0
  88. package/dist/docs/guides/internationalization.md +288 -0
  89. package/dist/docs/guides/llm-integration.md +635 -0
  90. package/dist/docs/guides/multi-team-games.md +586 -0
  91. package/dist/docs/guides/patterns.md +391 -0
  92. package/dist/docs/guides/presentation.md +196 -0
  93. package/dist/docs/guides/production-constraints.md +130 -0
  94. package/dist/docs/guides/scenario-editor.md +143 -0
  95. package/dist/docs/guides/scenarios.md +515 -0
  96. package/dist/docs/guides/session.md +972 -0
  97. package/dist/docs/guides/settings.md +458 -0
  98. package/dist/docs/guides/setup.md +193 -0
  99. package/dist/docs/guides/speech-generation.md +233 -0
  100. package/dist/docs/guides/starter-game.md +164 -0
  101. package/dist/docs/guides/testing.md +575 -0
  102. package/dist/docs/guides/tick-controller.md +569 -0
  103. package/dist/docs/guides/type-safety.md +516 -0
  104. package/dist/docs/guides/ui.md +1389 -0
  105. package/dist/docs/guides/upgrading.md +636 -0
  106. package/dist/docs/guides/validators.md +217 -0
  107. package/dist/docs/guides/waiting-room.md +244 -0
  108. package/dist/docs/index.md +41 -0
  109. package/dist/docs/recipes/act-on-the-session-from-an-endpoint.md +95 -0
  110. package/dist/docs/recipes/adapt-a-game-for-walkthroughs.md +133 -0
  111. package/dist/docs/recipes/adapt-ui-for-observers.md +74 -0
  112. package/dist/docs/recipes/call-endpoint-from-ui.md +109 -0
  113. package/dist/docs/recipes/check-a-package-from-a-script.md +117 -0
  114. package/dist/docs/recipes/create-capability.md +66 -0
  115. package/dist/docs/recipes/create-endpoint.md +127 -0
  116. package/dist/docs/recipes/create-engine.md +111 -0
  117. package/dist/docs/recipes/create-game.md +70 -0
  118. package/dist/docs/recipes/create-guide.md +75 -0
  119. package/dist/docs/recipes/create-hook.md +80 -0
  120. package/dist/docs/recipes/create-presentation.md +142 -0
  121. package/dist/docs/recipes/create-scenario-editor.md +102 -0
  122. package/dist/docs/recipes/create-scenario.md +98 -0
  123. package/dist/docs/recipes/create-session-ui.md +116 -0
  124. package/dist/docs/recipes/create-settings.md +88 -0
  125. package/dist/docs/recipes/create-setup.md +115 -0
  126. package/dist/docs/recipes/create-tick-controller.md +65 -0
  127. package/dist/docs/recipes/create-user-interface.md +96 -0
  128. package/dist/docs/recipes/create-validator.md +67 -0
  129. package/dist/docs/recipes/create-waiting-room-ui.md +109 -0
  130. package/dist/docs/recipes/declare-api-uses.md +67 -0
  131. package/dist/docs/recipes/design-state-for-concurrent-writes.md +96 -0
  132. package/dist/docs/recipes/distribute-package.md +80 -0
  133. package/dist/docs/recipes/e2e-testing.md +116 -0
  134. package/dist/docs/recipes/end-game-on-condition.md +154 -0
  135. package/dist/docs/recipes/fix-duplicate-packages.md +101 -0
  136. package/dist/docs/recipes/index.md +86 -0
  137. package/dist/docs/recipes/make-an-endpoint-external.md +101 -0
  138. package/dist/docs/recipes/negotiate-across-teams.md +164 -0
  139. package/dist/docs/recipes/observe-other-teams-state.md +119 -0
  140. package/dist/docs/recipes/publish-a-package.md +139 -0
  141. package/dist/docs/recipes/remember-a-viewers-choice.md +55 -0
  142. package/dist/docs/recipes/review-a-past-day.md +73 -0
  143. package/dist/docs/recipes/share-assets-across-scenarios.md +120 -0
  144. package/dist/docs/recipes/share-logic-between-engine-and-ui.md +130 -0
  145. package/dist/docs/recipes/show-all-teams-participants.md +109 -0
  146. package/dist/docs/recipes/start-from-an-empty-package.md +169 -0
  147. package/dist/docs/recipes/take-a-file-in-an-endpoint.md +82 -0
  148. package/dist/docs/recipes/test-a-multi-role-game.md +51 -0
  149. package/dist/docs/recipes/test-user-interface.md +95 -0
  150. package/dist/docs/recipes/test-with-engine.md +115 -0
  151. package/dist/docs/recipes/upload-and-display-files.md +148 -0
  152. package/dist/docs/recipes/use-request-play.md +65 -0
  153. package/dist/docs/recipes/use-request-tick.md +75 -0
  154. package/dist/docs/troubleshooting.md +816 -0
  155. package/dist/execution/blob-inputs.d.ts +57 -0
  156. package/dist/execution/bundle-reload.d.ts +42 -0
  157. package/dist/execution/config-resolver.d.ts +127 -0
  158. package/dist/execution/configuration-builder.d.ts +66 -0
  159. package/dist/execution/context-composition.d.ts +161 -0
  160. package/dist/execution/endpoint-actions.d.ts +9 -0
  161. package/dist/execution/endpoint-payloads.d.ts +32 -0
  162. package/dist/execution/endpoint-runner.d.ts +129 -0
  163. package/dist/execution/engine-state.d.ts +40 -0
  164. package/dist/execution/executor-callbacks.d.ts +56 -0
  165. package/dist/execution/executor.d.ts +436 -0
  166. package/dist/execution/gates.d.ts +45 -0
  167. package/dist/execution/hook-chain.d.ts +25 -0
  168. package/dist/execution/logic-calls.d.ts +190 -0
  169. package/dist/execution/logic-failure.d.ts +84 -0
  170. package/dist/execution/logic-loader.d.ts +54 -0
  171. package/dist/execution/logic-result.d.ts +76 -0
  172. package/dist/execution/tick-controller-call.d.ts +32 -0
  173. package/dist/execution/types.d.ts +48 -0
  174. package/dist/generation/ts-type.d.ts +39 -0
  175. package/dist/guide/locale.d.ts +12 -0
  176. package/dist/guide/video-registry.d.ts +42 -0
  177. package/dist/help-examples.d.ts +125 -0
  178. package/dist/host/adapter.d.ts +67 -0
  179. package/dist/host/host-channel.d.ts +222 -0
  180. package/dist/host/index.d.ts +20 -0
  181. package/dist/host/protocol.d.ts +783 -0
  182. package/dist/host/surfaces.d.ts +42 -0
  183. package/dist/host/theme.d.ts +55 -0
  184. package/dist/host.js +583 -0
  185. package/dist/prompts/index.d.ts +7 -0
  186. package/dist/prompts/schema-builder.d.ts +29 -0
  187. package/dist/prompts/types.d.ts +108 -0
  188. package/dist/prompts/validators.d.ts +32 -0
  189. package/dist/runtime/capability-scope.d.ts +6 -0
  190. package/dist/runtime/context-merge.d.ts +29 -0
  191. package/dist/runtime/context-projection.d.ts +41 -0
  192. package/dist/runtime/guarded-callbacks.d.ts +15 -0
  193. package/dist/runtime/index.d.ts +27 -0
  194. package/dist/runtime/manifest-version.d.ts +55 -0
  195. package/dist/runtime/tick-validation.d.ts +25 -0
  196. package/dist/runtime.js +346 -0
  197. package/dist/schema.json +1546 -0
  198. package/dist/server/vite-plugin.d.ts +46 -0
  199. package/dist/services/tts/client.d.ts +51 -0
  200. package/dist/services/tts/elevenlabs.d.ts +24 -0
  201. package/dist/services/tts/types.d.ts +44 -0
  202. package/dist/storage/asset-resolver.d.ts +49 -0
  203. package/dist/storage/blob-store.d.ts +41 -0
  204. package/dist/templates/endpoint/logic.test.ts.liquid +61 -0
  205. package/dist/templates/endpoint/logic.ts.liquid +25 -0
  206. package/dist/templates/engine/init.json.liquid +7 -0
  207. package/dist/templates/engine/logic-aggregate.test.ts.liquid +90 -0
  208. package/dist/templates/engine/logic-aggregate.ts.liquid +46 -0
  209. package/dist/templates/engine/logic.test.ts.liquid +61 -0
  210. package/dist/templates/engine/logic.ts.liquid +32 -0
  211. package/dist/templates/engine/run.json.liquid +7 -0
  212. package/dist/templates/guide/01-at-a-glance.md.liquid +19 -0
  213. package/dist/templates/guide/02-preparation.md.liquid +18 -0
  214. package/dist/templates/guide/03-running-the-session.md.liquid +37 -0
  215. package/dist/templates/hook/logic.test.ts.liquid +46 -0
  216. package/dist/templates/hook/logic.ts.liquid +12 -0
  217. package/dist/templates/package/CLAUDE.md.template +338 -0
  218. package/dist/templates/package/DESIGN.md.liquid +144 -0
  219. package/dist/templates/package/README.md.liquid +143 -0
  220. package/dist/templates/package/claude/rules/workflow-create-capability.md +26 -0
  221. package/dist/templates/package/claude/rules/workflow-create-endpoint.md +35 -0
  222. package/dist/templates/package/claude/rules/workflow-create-engine.md +35 -0
  223. package/dist/templates/package/claude/rules/workflow-create-guide.md +40 -0
  224. package/dist/templates/package/claude/rules/workflow-create-presentation.md +37 -0
  225. package/dist/templates/package/claude/rules/workflow-create-user-interface.md +31 -0
  226. package/dist/templates/package/claude/rules/workflow-publish-package.md +54 -0
  227. package/dist/templates/package/claude/rules/workflow-test-engine-or-ui.md +30 -0
  228. package/dist/templates/package/claude/rules/workflow-update-sdk-version.md +43 -0
  229. package/dist/templates/package/claude/settings.json.template +16 -0
  230. package/dist/templates/package/gitignore.template +15 -0
  231. package/dist/templates/package/global-setup.ts.template +21 -0
  232. package/dist/templates/package/globals.d.ts.template +2 -0
  233. package/dist/templates/package/oxfmtrc.json.template +14 -0
  234. package/dist/templates/package/oxlintrc.json.template +238 -0
  235. package/dist/templates/package/package.json.liquid +65 -0
  236. package/dist/templates/package/playwright.config.ts.liquid +19 -0
  237. package/dist/templates/package/pnpm-workspace.yaml.template +27 -0
  238. package/dist/templates/package/test-setup.ts.liquid +7 -0
  239. package/dist/templates/package/tsconfig.json.liquid +15 -0
  240. package/dist/templates/package/vite.config.ts.liquid +19 -0
  241. package/dist/templates/package/vitest.config.ts.liquid +25 -0
  242. package/dist/templates/package/vitest.engine.config.ts.liquid +12 -0
  243. package/dist/templates/presentation/index.html.liquid +20 -0
  244. package/dist/templates/presentation/src/App.tsx.liquid +49 -0
  245. package/dist/templates/presentation/src/Slides.tsx.liquid +81 -0
  246. package/dist/templates/presentation/src/i18n.ts.liquid +54 -0
  247. package/dist/templates/presentation/src/main.tsx.liquid +14 -0
  248. package/dist/templates/scenario_editor/src/App.test.tsx.liquid +30 -0
  249. package/dist/templates/scenario_editor/src/App.tsx.liquid +95 -0
  250. package/dist/templates/scenario_editor/src/i18n.ts.liquid +44 -0
  251. package/dist/templates/session/src/App.test.tsx.liquid +60 -0
  252. package/dist/templates/session/src/App.tsx.liquid +128 -0
  253. package/dist/templates/session/src/i18n.ts.liquid +53 -0
  254. package/dist/templates/session/src/main.tsx.liquid +48 -0
  255. package/dist/templates/session/src/theme.ts.liquid +46 -0
  256. package/dist/templates/settings-editor/index.html.liquid +12 -0
  257. package/dist/templates/settings-editor/src/main.tsx.liquid +48 -0
  258. package/dist/templates/settings-editor/src/theme.ts.liquid +24 -0
  259. package/dist/templates/setup/src/App.test.tsx.liquid +39 -0
  260. package/dist/templates/setup/src/App.tsx.liquid +102 -0
  261. package/dist/templates/setup/src/i18n.ts.liquid +47 -0
  262. package/dist/templates/starter-game/capabilities/briefing/capability.sdk.json +18 -0
  263. package/dist/templates/starter-game/capabilities/plan/capability.sdk.json +17 -0
  264. package/dist/templates/starter-game/capabilities/results/capability.sdk.json +61 -0
  265. package/dist/templates/starter-game/capabilities/roles/capability.sdk.json +17 -0
  266. package/dist/templates/starter-game/capabilities/weather/capability.sdk.json +23 -0
  267. package/dist/templates/starter-game/engine/defaults.ts +81 -0
  268. package/dist/templates/starter-game/engine/engine.sdk.json +12 -0
  269. package/dist/templates/starter-game/engine/logic.test.ts +221 -0
  270. package/dist/templates/starter-game/engine/logic.ts +149 -0
  271. package/dist/templates/starter-game/engine/test-helpers.ts +74 -0
  272. package/dist/templates/starter-game/engine/types.ts +139 -0
  273. package/dist/templates/starter-game/guide/content/en/01-at-a-glance.md +51 -0
  274. package/dist/templates/starter-game/guide/content/en/02-preparation.md +35 -0
  275. package/dist/templates/starter-game/guide/content/en/03-running-the-day.md +58 -0
  276. package/dist/templates/starter-game/guide/guide.sdk.json +10 -0
  277. package/dist/templates/starter-game/guide/guide.test.ts +108 -0
  278. package/dist/templates/starter-game/hooks/assignproducts/defaults.ts +37 -0
  279. package/dist/templates/starter-game/hooks/assignproducts/hook.sdk.json +13 -0
  280. package/dist/templates/starter-game/hooks/assignproducts/logic/logic.test.ts +77 -0
  281. package/dist/templates/starter-game/hooks/assignproducts/logic/logic.ts +47 -0
  282. package/dist/templates/starter-game/hooks/assignproducts/types.ts +55 -0
  283. package/dist/templates/starter-game/package.json +68 -0
  284. package/dist/templates/starter-game/playwright.config.ts +19 -0
  285. package/dist/templates/starter-game/scenarios/heatwave/scenario.sdk.json +20 -0
  286. package/dist/templates/starter-game/scenarios/ordinary_week/scenario.sdk.json +12 -0
  287. package/dist/templates/starter-game/seriousgaming.sdk.json +23 -0
  288. package/dist/templates/starter-game/session/defaults.ts +80 -0
  289. package/dist/templates/starter-game/session/index.html +12 -0
  290. package/dist/templates/starter-game/session/session.sdk.json +11 -0
  291. package/dist/templates/starter-game/session/src/App.test.tsx +307 -0
  292. package/dist/templates/starter-game/session/src/App.tsx +309 -0
  293. package/dist/templates/starter-game/session/src/Section.tsx +37 -0
  294. package/dist/templates/starter-game/session/src/TeamDoor.tsx +96 -0
  295. package/dist/templates/starter-game/session/src/format.ts +8 -0
  296. package/dist/templates/starter-game/session/src/i18n.ts +51 -0
  297. package/dist/templates/starter-game/session/src/main.tsx +48 -0
  298. package/dist/templates/starter-game/session/src/theme.ts +45 -0
  299. package/dist/templates/starter-game/session/types.ts +135 -0
  300. package/dist/templates/starter-game/settings/engine/settings.sdk.json +50 -0
  301. package/dist/templates/starter-game/settings/menu/settings.sdk.json +78 -0
  302. package/dist/templates/starter-game/settings/session/settings.sdk.json +23 -0
  303. package/dist/templates/starter-game/settings/ui/settings.sdk.json +25 -0
  304. package/dist/templates/starter-game/test-setup.ts +7 -0
  305. package/dist/templates/starter-game/testing/e2e/game.spec.ts +139 -0
  306. package/dist/templates/starter-game/testing/e2e/global-setup.ts +21 -0
  307. package/dist/templates/starter-game/testing/engine/game.engine.test.ts +171 -0
  308. package/dist/templates/starter-game/tsconfig.json +19 -0
  309. package/dist/templates/starter-game/ui/defaults.ts +60 -0
  310. package/dist/templates/starter-game/ui/index.html +12 -0
  311. package/dist/templates/starter-game/ui/src/App.test.tsx +138 -0
  312. package/dist/templates/starter-game/ui/src/App.tsx +175 -0
  313. package/dist/templates/starter-game/ui/src/DayBar.test.tsx +78 -0
  314. package/dist/templates/starter-game/ui/src/DayBar.tsx +72 -0
  315. package/dist/templates/starter-game/ui/src/HistoryPanel.test.tsx +74 -0
  316. package/dist/templates/starter-game/ui/src/HistoryPanel.tsx +120 -0
  317. package/dist/templates/starter-game/ui/src/MakeRow.tsx +129 -0
  318. package/dist/templates/starter-game/ui/src/Section.tsx +37 -0
  319. package/dist/templates/starter-game/ui/src/ThemeToggle.tsx +49 -0
  320. package/dist/templates/starter-game/ui/src/TodayPanel.test.tsx +179 -0
  321. package/dist/templates/starter-game/ui/src/TodayPanel.tsx +92 -0
  322. package/dist/templates/starter-game/ui/src/YesterdayPanel.tsx +142 -0
  323. package/dist/templates/starter-game/ui/src/format.ts +12 -0
  324. package/dist/templates/starter-game/ui/src/i18n.ts +95 -0
  325. package/dist/templates/starter-game/ui/src/main.tsx +48 -0
  326. package/dist/templates/starter-game/ui/src/menu.ts +18 -0
  327. package/dist/templates/starter-game/ui/src/test-helpers.ts +58 -0
  328. package/dist/templates/starter-game/ui/src/theme.ts +45 -0
  329. package/dist/templates/starter-game/ui/src/writes.ts +43 -0
  330. package/dist/templates/starter-game/ui/types.ts +137 -0
  331. package/dist/templates/starter-game/ui/ui.sdk.json +11 -0
  332. package/dist/templates/starter-game/validators/capacity/defaults.ts +37 -0
  333. package/dist/templates/starter-game/validators/capacity/logic/logic.test.ts +96 -0
  334. package/dist/templates/starter-game/validators/capacity/logic/logic.ts +45 -0
  335. package/dist/templates/starter-game/validators/capacity/types.ts +55 -0
  336. package/dist/templates/starter-game/validators/capacity/validator.sdk.json +11 -0
  337. package/dist/templates/starter-game/vite.config.ts +19 -0
  338. package/dist/templates/starter-game/vitest.config.ts +15 -0
  339. package/dist/templates/starter-game/vitest.engine.config.ts +19 -0
  340. package/dist/templates/starter-game/waiting_room/defaults.ts +40 -0
  341. package/dist/templates/starter-game/waiting_room/index.html +12 -0
  342. package/dist/templates/starter-game/waiting_room/src/App.test.tsx +102 -0
  343. package/dist/templates/starter-game/waiting_room/src/App.tsx +194 -0
  344. package/dist/templates/starter-game/waiting_room/src/i18n.ts +33 -0
  345. package/dist/templates/starter-game/waiting_room/src/main.tsx +48 -0
  346. package/dist/templates/starter-game/waiting_room/src/theme.ts +45 -0
  347. package/dist/templates/starter-game/waiting_room/types.ts +59 -0
  348. package/dist/templates/starter-game/waiting_room/waiting_room.sdk.json +10 -0
  349. package/dist/templates/tick-controller/logic.ts.liquid +8 -0
  350. package/dist/templates/ui/index.html.liquid +12 -0
  351. package/dist/templates/ui/src/App.test.tsx.liquid +41 -0
  352. package/dist/templates/ui/src/App.tsx.liquid +103 -0
  353. package/dist/templates/ui/src/i18n.ts.liquid +47 -0
  354. package/dist/templates/ui/src/main.tsx.liquid +48 -0
  355. package/dist/templates/ui/src/theme.ts.liquid +46 -0
  356. package/dist/templates/validator/logic.test.ts.liquid +42 -0
  357. package/dist/templates/validator/logic.ts.liquid +21 -0
  358. package/dist/templates/waiting-room/index.html.liquid +12 -0
  359. package/dist/templates/waiting-room/src/App.test.tsx.liquid +57 -0
  360. package/dist/templates/waiting-room/src/App.tsx.liquid +118 -0
  361. package/dist/templates/waiting-room/src/i18n.ts.liquid +44 -0
  362. package/dist/templates/waiting-room/src/main.tsx.liquid +48 -0
  363. package/dist/templates/waiting-room/src/theme.ts.liquid +46 -0
  364. package/dist/templating/resource-metadata.d.ts +131 -0
  365. package/dist/types.d.ts +14 -0
  366. package/dist/utils/atomic-write.d.ts +40 -0
  367. package/dist/utils/cli-error.d.ts +64 -0
  368. package/dist/utils/document-parser-client.d.ts +20 -0
  369. package/dist/utils/env-loader.d.ts +40 -0
  370. package/dist/utils/error-codes.d.ts +335 -0
  371. package/dist/utils/errors.d.ts +19 -0
  372. package/dist/utils/global-settings.d.ts +65 -0
  373. package/dist/utils/llm-client.d.ts +34 -0
  374. package/dist/utils/logger.d.ts +36 -0
  375. package/dist/utils/output.d.ts +109 -0
  376. package/dist/utils/platform.d.ts +14 -0
  377. package/dist/utils/prompt.d.ts +36 -0
  378. package/dist/utils/real-path.d.ts +6 -0
  379. package/dist/utils/schema-properties.d.ts +35 -0
  380. package/dist/validation/ajv.d.ts +17 -0
  381. package/dist/validation/facilitator-guide-validator.d.ts +27 -0
  382. package/dist/validation/findings.d.ts +40 -0
  383. package/dist/validation/game-constraints.d.ts +10 -0
  384. package/dist/validation/game-validator.d.ts +34 -0
  385. package/dist/validation/package-validation.d.ts +41 -0
  386. package/dist/validation/package-validator.d.ts +19 -0
  387. package/dist/validation/resource-validator.d.ts +79 -0
  388. package/dist/validation/schema-defaults-validator.d.ts +29 -0
  389. package/dist/validation/schema-validation.d.ts +115 -0
  390. package/dist/validation/semver.d.ts +9 -0
  391. package/package.json +122 -3
@@ -0,0 +1,527 @@
1
+ # Engines
2
+
3
+ The engine is the logic that moves your game forward: it sets up the board, then plays one round each tick. This guide explains how an engine is shaped and why, then covers everything you write in one.
4
+
5
+ ## The model
6
+
7
+ A game holds one engine, in the `engine/` slot. It:
8
+
9
+ - Declares the capabilities it reads (`reads`) and writes (`writes`), and the settings resources it reads (`settings`)
10
+ - Exports an optional `Init`, which sets up each team's state before the first tick
11
+ - Exports `Run`, which plays one tick for one team
12
+ - Or, in a multi-team game where teams affect one another, exports `InitAggregate` and `RunAggregate`, which see every team at once
13
+
14
+ The declarations are what make the data flow explicit. The runtime hands the engine exactly the capabilities it declares, and accepts writes only to the ones in `writes`, so a reader of `engine.sdk.json` knows what the engine touches without reading its code. The engine returns what it writes as a patch rather than mutating anything, so a tick can be replayed, undone and checked.
15
+
16
+ A simulation with several steps (demand, then sales, then finance) is still one engine: `Run` calls the steps as functions, in the order it wants them. Ordering is a line of code, not a manifest setting.
17
+
18
+ ## Creating an engine
19
+
20
+ Create the settings the engine reads, then an engine that reads them and writes to a capability:
21
+
22
+ ```bash
23
+ pnpm run sdk new settings '{
24
+ "definition": {
25
+ "identifier": "engine",
26
+ "settingsSchema": {
27
+ "type": "object",
28
+ "properties": {
29
+ "DailySales": { "type": "number", "default": 10 }
30
+ },
31
+ "additionalProperties": false
32
+ }
33
+ }
34
+ }'
35
+
36
+ pnpm run sdk new engine '{
37
+ "definition": {
38
+ "writes": ["inventory"],
39
+ "settings": ["engine"]
40
+ },
41
+ "logic": {
42
+ "directory": ".",
43
+ "entrypoint": "logic.ts"
44
+ },
45
+ "rootDirectory": "."
46
+ }'
47
+ ```
48
+
49
+ This creates a settings resource, `settings/engine/`, holding the daily sales amount, and an engine that writes one capability and reads those settings:
50
+
51
+ ```
52
+ engine/
53
+ ├── engine.sdk.json # Reads, writes and settings
54
+ ├── logic.ts # Init and Run
55
+ ├── logic.test.ts # Tests
56
+ └── types.ts # Generated: Context, Settings, Callbacks
57
+ ```
58
+
59
+ `sdk new engine --aggregate` scaffolds an [aggregate engine](#aggregate-engines) instead.
60
+
61
+ ## Declaring reads, writes and settings
62
+
63
+ `engine.sdk.json` declares the engine's data flow:
64
+
65
+ ```json
66
+ {
67
+ "definition": {
68
+ "reads": ["inventory", "pricing"],
69
+ "writes": ["revenue"],
70
+ "settings": ["engine"]
71
+ },
72
+ "logic": { "directory": ".", "entrypoint": "logic.ts" },
73
+ "rootDirectory": "."
74
+ }
75
+ ```
76
+
77
+ - **`reads`**: the capabilities the engine reads. Every declared capability is present in `context` from the first tick, holding its schema defaults until something writes it, so a read is never conditional
78
+ - **`writes`**: the capabilities the engine produces values for, or removes keys from
79
+ - **`settings`**: the settings resources the engine reads. It receives exactly those, keyed by identifier, as `settings`
80
+
81
+ The build fails on an identifier the package does not declare. A capability with `"lifetime": "session"` cannot appear in either list (see [Lifetime](./capabilities.md#lifetime)). For the rules every resource follows, including what a user interface receives, see [Declaring reads and writes](./capabilities.md#declaring-reads-and-writes).
82
+
83
+ Your logic reads what it declared by identifier:
84
+
85
+ ```typescript
86
+ export const Run: RunFunction<Context, Settings> = async ({ context, settings }) => {
87
+ const { Stock } = context.inventory;
88
+ const { Price } = context.pricing;
89
+ const revenue = Stock * Price * settings.engine.ConversionRate;
90
+
91
+ return {
92
+ capabilities: {
93
+ ...context,
94
+ revenue: { ...context.revenue, Revenue: revenue },
95
+ },
96
+ };
97
+ };
98
+ ```
99
+
100
+ ### Settings
101
+
102
+ Settings are read-only and fixed when play starts. Each one resolves from its default and any overrides before `Init` runs, so `settings` always carries every property: see [Overriding settings](./settings.md#overriding-settings) for the layers.
103
+
104
+ The schema default is the single source of truth for the game's own value, so a code-level fallback such as `settings.engine.DailySales ?? 10` never fires. Drop it: every property must declare a `default`, so the generated `Settings` type marks none of them optional. To change the default, edit the settings schema and run `pnpm run sdk generate`, not the logic. A `settingsSchema` may not declare `required`, and the build rejects one that does. For a property that may genuinely have no value, widen its type instead (for example `"type": ["string", "null"]` with `"default": null`).
105
+
106
+ To change a value for one scenario, override it in the scenario's `settings` under the settings identifier. A value participants must never see, such as an answer key, goes in a settings resource that only the engine declares. See [Settings](./settings.md#reading-settings) and [Create settings](../recipes/create-settings.md).
107
+
108
+ ## Implementing Init
109
+
110
+ `Init` runs once when the game starts, once per team. Use it to set up initial state. It is optional: an engine that exports no `Init` starts every team from its capabilities' `default` values, so leave it out when those defaults already are the starting board. The scaffold writes one for you to fill in or delete:
111
+
112
+ ```typescript
113
+ import { InitializationFunction } from '@seriousgaming/sdk';
114
+ import type { Context, Settings } from './types';
115
+
116
+ export const Init: InitializationFunction<Context, Settings> = async ({ context, settings }) => {
117
+ return {
118
+ capabilities: {
119
+ ...context,
120
+ inventory: {
121
+ ...context.inventory,
122
+ Stock: settings.engine.InitialStock,
123
+ },
124
+ },
125
+ };
126
+ };
127
+ ```
128
+
129
+ ## Implementing Run
130
+
131
+ `Run` executes every tick. This is where your core logic lives:
132
+
133
+ ```typescript
134
+ import { RunFunction } from '@seriousgaming/sdk';
135
+ import type { Context, Settings } from './types';
136
+
137
+ export const Run: RunFunction<Context, Settings> = async ({ context, settings }) => {
138
+ const { Stock } = context.inventory;
139
+ const newStock = Math.max(0, Stock - settings.engine.DailySales);
140
+
141
+ return {
142
+ capabilities: {
143
+ ...context,
144
+ inventory: {
145
+ ...context.inventory,
146
+ Stock: newStock,
147
+ },
148
+ },
149
+ };
150
+ };
151
+ ```
152
+
153
+ Never assign to `context`: build a new object with spreads and return it. See [Immutable updates](./capabilities.md#immutable-updates).
154
+
155
+ ### What an engine returns
156
+
157
+ `Init` and `Run` return what they write, named by kind: capabilities go under `capabilities`, as a patch merged into the team's context, and paths to remove go under `deletes` (see [Removing a key](#removing-a-key)). Hooks and aggregate engines follow the same rule, and a writing endpoint returns `{ response, capabilities, deletes? }`. The runtime refuses a top-level key it does not recognise, so a result that returns capabilities flat (`{ inventory: { ... } }`) fails the call rather than losing its writes.
158
+
159
+ ### How a result folds into the context
160
+
161
+ What you return under `capabilities` is folded into the team's context one level deep:
162
+
163
+ - A capability you leave out keeps its value
164
+ - Within a capability you return, each variable you include replaces its old value, and each variable you leave out keeps its value
165
+ - A variable is replaced whole, never merged into. That holds for objects too: return `PendingJunction: {}` and `PendingJunction` becomes `{}`, whatever it held before
166
+
167
+ So to clear a field, return the value you want it to hold. `{}` empties an object variable, `[]` an array, and `null` clears either, as long as the variable's schema allows `null`.
168
+
169
+ ```typescript
170
+ return {
171
+ capabilities: {
172
+ ...context,
173
+ route: {
174
+ ...context.route,
175
+ PendingJunction: {}, // cleared, not merged with the old choice
176
+ },
177
+ },
178
+ };
179
+ ```
180
+
181
+ This is not how a UI's `setData` behaves. A UI deep-merges its patch, because it may hold only part of the context. An engine is handed every capability it writes, so it always has the whole variable in hand to return.
182
+
183
+ ### Removing a key
184
+
185
+ A variable you leave out keeps its value, so a fold can never take an entry out of a map keyed by something arbitrary, such as an order or team id. Name the entry in `deletes` instead:
186
+
187
+ ```typescript
188
+ return {
189
+ capabilities: { ...context },
190
+ deletes: [`orders.Open.${orderId}`],
191
+ };
192
+ ```
193
+
194
+ - The runtime folds `capabilities` first and removes the paths after, so one result can replace an entry: write the new key under `capabilities` and delete the old one
195
+ - A path is segments joined by `.`, or an array of segments when a key could contain a dot: `['orders', 'Open', orderId]`
196
+ - A path that does not resolve is not an error
197
+ - Every path must sit under a capability in your `writes`. One under a capability you only read, or did not declare, fails the call, naming it, even if the path does not exist
198
+
199
+ In an aggregate engine, each `teams` entry carries its own `deletes`, and `shared.deletes` removes paths from shared capabilities. A shared path in a `teams` entry fails, as a shared write there does. These are the same paths, with the same rules, as a UI's `setData(update, { deletes })`.
200
+
201
+ ### Keeping memory between ticks
202
+
203
+ Some of what an engine knows is nobody else's business: a record of every round so far, a model it carries forward, a random draw it froze at the start. Return it under `state`, beside `capabilities`, and the next call receives it back as `state`:
204
+
205
+ ```typescript
206
+ import type { InitializationFunction, RunFunction, SignatureCallbacks } from '@seriousgaming/sdk';
207
+ import type { Context, Settings } from './types';
208
+
209
+ interface EngineState {
210
+ BestProfit: number | null;
211
+ }
212
+
213
+ export const Init: InitializationFunction<
214
+ Context,
215
+ Settings,
216
+ SignatureCallbacks,
217
+ EngineState
218
+ > = async ({ context }) => ({ capabilities: context, state: { BestProfit: null } });
219
+
220
+ export const Run: RunFunction<Context, Settings, SignatureCallbacks, EngineState> = async ({
221
+ context,
222
+ state,
223
+ }) => {
224
+ const best = state?.BestProfit ?? null;
225
+ const { Profit } = context.profit;
226
+ return {
227
+ capabilities: context,
228
+ state: { BestProfit: best === null ? Profit : Math.max(best, Profit) },
229
+ };
230
+ };
231
+ ```
232
+
233
+ The rules are short:
234
+
235
+ - **It is typed, not declared.** The fourth type parameter of `InitializationFunction` and `RunFunction` is the state's type, and the compiler checks it from the value one call returns to the value the next receives. There is no schema, because nothing but the engine reads it
236
+ - **It is the engine's alone.** No surface, hook, validator, tick controller or endpoint ever receives it, and nothing but the engine can write it
237
+ - **It moves with the round.** Each tick snapshot records it, so undo rewinds it and a per-team reset returns it to what `Init` left
238
+ - **It replaces rather than merges.** A result carrying `state` replaces the previous value whole. Leave `state` out and the previous value is kept
239
+ - **It must be JSON.** Plain objects, arrays, strings, finite numbers, booleans and `null`. A `Date`, a `Map`, `NaN` or an `undefined` property fails the tick, naming the engine, because a hosted runtime stores it as JSON and would hand back something different after a restart
240
+
241
+ A step (`StepFunction`) receives `state` too, so a step can read it; only the exported `Init` and `Run` return it. An aggregate engine has the same pair at both levels: each `teams` entry carries that team's `state`, and `shared.state` is one value for the whole session. A summary step, for instance, can read `state.BestProfit` from the example above to call out a record round.
242
+
243
+ Reach for a capability instead when anything else reads the value. `scope: 'private'` keeps a capability off every screen while leaving it readable by hooks and endpoints, so a value an endpoint reports on, such as last round's profit for a trend, stays a capability.
244
+
245
+ ## Ordering the simulation
246
+
247
+ A game holds one engine, so there is no cross-engine ordering to resolve. Split the simulation into steps, one responsibility each, and order them by the order `Run` calls them:
248
+
249
+ ```typescript
250
+ export const Run: RunFunction<Context, Settings> = async (args) => {
251
+ const withDemand = await runDemand(args);
252
+ const withSales = await runSales({ ...args, context: withDemand });
253
+ return { capabilities: await runFinancials({ ...args, context: withSales }) };
254
+ };
255
+ ```
256
+
257
+ Type each step as `StepFunction<Context, Settings>`. A step takes the engine's arguments and returns the next context flat, so steps chain directly, and only the exported `Run` wraps the final context in `{ capabilities }`. If one step reads what another writes, call the writer first.
258
+
259
+ Below the steps, plain helper functions keep the arithmetic readable and testable on its own:
260
+
261
+ ```typescript
262
+ function calculateDemand(price: number, baseRate: number): number {
263
+ return baseRate / (1 + price * 0.1);
264
+ }
265
+ ```
266
+
267
+ ## Aggregate engines
268
+
269
+ An aggregate engine runs once across every team rather than once per team. Reach for one when a value depends on every team's data, such as market share from all teams' prices.
270
+
271
+ An engine is aggregate because of what its logic exports: `InitAggregate` and `RunAggregate`. There is no flag in the bundle. Exporting one without the other fails the build, naming the one that is missing. A per-team `Init` and `Run` are optional beside them; when present, `Run` executes for each team before `RunAggregate`.
272
+
273
+ Both aggregate functions receive `{ shared, teams, settings, meta, participants, callbacks }`. `shared.context` is the session's shared store, once; `teams` is every team as `{ teamId, index, context }`, sorted by index, each `context` carrying that team's own capabilities only. They return `shared: { capabilities, deletes? }` for shared capabilities and one `{ capabilities, deletes? }` per team, in input order. The generated `types.ts` exports `TeamContext` and `SharedContext` beside `Context` to type the two halves.
274
+
275
+ Only an aggregate engine may write to a [shared capability](./multi-team-games.md#shared-capabilities), and only under `shared`: a shared capability in a `teams` entry fails the init or tick, even when every team carries the same value. A per-team figure belongs in its own default-scope capability. The build refuses a shared write declared by a hook, a surface or an engine that is not aggregate. A per-team `Init` or `Run` in an aggregate engine that changes a shared capability fails when it runs: the session does not start, or the tick fails.
276
+
277
+ See [The aggregate functions](./multi-team-games.md#the-aggregate-functions) for a complete example, the argument and return tables, and the execution order.
278
+
279
+ ## The language environment your logic runs in
280
+
281
+ Engine logic runs in a **bare V8 isolate**. The runtime provides the ECMAScript intrinsics (`Object`, `Array`, `String`, `Number`, `Math`, `JSON`, `Date`, `RegExp`, `Promise`, `Map`, `Set`, the typed-array family, the error types, and the rest) and nothing else. Anything you might think of as 'just there' because Node or a browser provides it is **absent**:
282
+
283
+ `process`, `require`, `fetch`, `XMLHttpRequest`, `setTimeout` / `setInterval` / `queueMicrotask` and every other timer, `structuredClone`, `Buffer`, `TextEncoder` / `TextDecoder` and the streams family, `URL` / `URLSearchParams`, `crypto`, `atob` / `btoa`, `window`, `document`, `localStorage`.
284
+
285
+ Referencing one throws `ReferenceError` at tick time. Two things protect you:
286
+
287
+ - **The dev server and CLI enforce it.** `sdk dev` and `tick` run your logic in the same restricted environment, so the failure surfaces locally rather than on deploy
288
+ - **Lint catches it before you run anything.** A scaffolded package restricts these globals in `engine/`, `tick_controller/`, `hooks/*/logic/`, `validators/*/logic/` and `endpoints/*/logic/`, so `pnpm run lint` (and your editor) flags the reference with the replacement to use. The UI slots are deliberately out of scope: `ui/`, `session/` and `waiting_room/` run in a browser, where these globals exist
289
+
290
+ ### What to use instead
291
+
292
+ | Instead of | Use |
293
+ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
294
+ | `structuredClone(value)` | `JSON.parse(JSON.stringify(value))`: context is JSON-serialisable by contract |
295
+ | `new TextEncoder().encode(text)` | `encodeUtf8(text)` from `@seriousgaming/sdk` |
296
+ | `new TextDecoder().decode(bytes)` | `decodeUtf8(bytes)` from `@seriousgaming/sdk` |
297
+ | `fetch(...)` | The injected `callbacks` (see [declaring runtime callbacks](#declaring-runtime-callbacks)), or an endpoint |
298
+ | `setTimeout(...)` | Nothing. Ticks are the scheduling primitive: logic runs to completion within one tick |
299
+ | `process.env.X` | The `settings` argument |
300
+ | `crypto.randomUUID()` / `Math.random()` for anything you keep | `deriveSeed` and `seededRandom` from `@seriousgaming/sdk`, seeded from `meta.seed` (see [Randomness you can keep](#randomness-you-can-keep)) |
301
+ | `Date.now()` / `new Date()` | `meta.now` (see [The time](#the-time)) |
302
+
303
+ `console.log` is the exception and works normally: the SDK compiles a `console` shim into every bundle, so it prints in development and is discarded in production instead of throwing.
304
+
305
+ `Math.random()` and `Date` are intrinsics and available, so nothing stops you calling them. But your logic may run more than once for one logical tick, and only the last run is kept, so neither is a safe basis for a value you store.
306
+
307
+ > **Two caveats on the lint rule.** It matches bare identifiers, so `globalThis.fetch(...)` slips past it. Do not route around the rule that way. And it is off for colocated test files, because those run under Vitest in Node where these globals genuinely exist. That means a `logic.test.ts` can pass while the logic it tests would throw in production; the dev server and `tick` are what catch that. See [Testing](./testing.md#the-runtime-your-tests-do-not-reproduce).
308
+
309
+ ### No Node built-ins
310
+
311
+ Logic cannot import a Node built-in module: not `node:fs` or any other `node:` specifier, and not a bare built-in name such as `fs`, `path`, `os`, `crypto` or `child_process`, whether by `import`, `require` or `import()`. There is no `require` in production to resolve one. `sdk build`, `sdk dev` and `tick` refuse it, naming the file, line and import:
312
+
313
+ ```
314
+ LOGIC_NODE_BUILTIN: engine/stamp.ts:1:25 imports 'os', a Node built-in. ...
315
+ ```
316
+
317
+ The same applies to a dependency you bundle: a library that imports a built-in cannot run in game logic. Move the work to a callback, or compute it from the context and settings.
318
+
319
+ ### The time
320
+
321
+ `meta.now` is when the operation your call belongs to started, in milliseconds since the epoch: the tick, the hook dispatch, the endpoint call or the validator run. The host takes it once, so every team's `Run` in one tick reads the same value, the tick controller after it reads it too, and a re-run of the tick reads it again. Use it wherever you would have called `Date.now()`:
322
+
323
+ ```typescript
324
+ export const Run: RunFunction<Context, Settings> = async ({ context, meta }) => {
325
+ return {
326
+ capabilities: {
327
+ ...context,
328
+ orders: { ...context.orders, LastPlacedAt: meta.now },
329
+ },
330
+ };
331
+ };
332
+ ```
333
+
334
+ In `sdk dev` it reads the session's clock, so `pnpm run sdk clock advance` moves it on a `--fake-clock` session, and a test passing a `FakeClock` to `createTestSession` controls it.
335
+
336
+ ### Who is on the team
337
+
338
+ Every call into logic also receives `participants`: a read-only copy of the roster, taken when the operation started. `Init`, `Run` and a team-scoped endpoint see their team's participants; aggregates and the tick controller see every team in `teams`; a hook sees its team on a join and everyone on a move. Read it instead of keeping a roster in context from presence hooks:
339
+
340
+ ```typescript
341
+ export const Run: RunFunction<Context, Settings> = async ({ context, participants }) => {
342
+ const present = participants.filter((p) => p.connected).length;
343
+ return {
344
+ capabilities: {
345
+ ...context,
346
+ staffing: { ...context.staffing, Present: present },
347
+ },
348
+ };
349
+ };
350
+ ```
351
+
352
+ Each call gets its own copy, so changing it changes nothing. At session start a team may have no participants yet, so `Init` often sees an empty list.
353
+
354
+ ### Randomness you can keep
355
+
356
+ `meta.seed` is a random seed the host gives you. It is the same for the whole session, the same if a tick re-runs, and the same after a restart. Combine it with whatever identifies the thing you are seeding:
357
+
358
+ ```typescript
359
+ import { deriveSeed, seededRandom } from '@seriousgaming/sdk';
360
+
361
+ export const RunAggregate: RunAggregateFunction<TeamContext, SharedContext, Settings> = async ({
362
+ teams,
363
+ meta,
364
+ }) => {
365
+ return {
366
+ shared: { capabilities: {} },
367
+ teams: teams.map((team) => {
368
+ // meta.index is not in scope here: each team carries its own,
369
+ // which is what makes the derived value differ between them.
370
+ const random = seededRandom(deriveSeed(meta.seed, team.index, 'weather'));
371
+ return {
372
+ capabilities: {
373
+ ...team.context,
374
+ weather: { ...team.context.weather, Rainfall: Math.round(random() * 10) },
375
+ },
376
+ };
377
+ }),
378
+ };
379
+ };
380
+ ```
381
+
382
+ Per-team functions get the same thing from `meta.index`:
383
+
384
+ ```typescript
385
+ const random = seededRandom(deriveSeed(meta.seed, meta.index, 'weather'));
386
+ ```
387
+
388
+ Two rules worth knowing before you reach for it:
389
+
390
+ 1. **Variation that depends on the other teams belongs in `InitAggregate`.** `Init` runs once for each team, but sees only that team's context, so it cannot tell teams apart beyond `meta.index` or balance one against another. `InitAggregate` sees every team at once, each with its own `index`. See [Multi-team games](./multi-team-games.md)
391
+ 2. **Freeze what later ticks depend on.** If tick 5 needs the same value tick 1 dealt, write it into context in tick 1 rather than re-deriving it from parts that may have changed
392
+
393
+ `meta` also carries `teamId` and `index`, which name the team whose context you were given (`'team-1'` in a single-team game), and `tick` (the tick being executed), `locale`, `now`, and `runKind`.
394
+
395
+ ### Walkthroughs
396
+
397
+ `meta.runKind` is `'session'` when your game is being played and `'walkthrough'` when it is being demonstrated. A walkthrough is a disposable second run beside the session: one team, one participant, always from tick 0, touching nothing real. It is what a facilitator puts on a projector while talking a room through the game before they play it.
398
+
399
+ Most games ignore it and are right to. Reach for it when a solo run is missing something a group has, or when a tick reaches outside:
400
+
401
+ ```typescript
402
+ export const Run: RunFunction<Context, Settings> = async ({ context, meta, callbacks }) => {
403
+ // A walkthrough has one team, so a leaderboard has nobody on it. Fill it
404
+ // in rather than showing a room an empty board they will never see again.
405
+ const rivals = meta.runKind === 'walkthrough' ? demonstrationRivals : context.market.Rivals;
406
+
407
+ // The model costs money and takes time, and answers differently each run.
408
+ // Neither is what you want in front of a room.
409
+ const commentary =
410
+ meta.runKind === 'walkthrough'
411
+ ? writtenCommentary(context)
412
+ : getText(await callbacks.generateText(prompt(context)));
413
+
414
+ return {
415
+ capabilities: {
416
+ ...context,
417
+ market: { ...context.market, Rivals: rivals },
418
+ report: { commentary },
419
+ },
420
+ };
421
+ };
422
+ ```
423
+
424
+ **The line to hold:** a walkthrough is a solo run of something built for a group, so fill in the social context it genuinely lacks, and suppress spoilers and anything that reaches outside. Do not change how the game plays. A walkthrough that behaves differently demonstrates a different game, and the room finds out on the first tick.
425
+
426
+ The value is the host's, never a surface's, and it is the same everywhere in one run and for the whole of it, so a decision your engine takes on it and a decision your interface takes on it cannot disagree. Your surfaces read it with `useRunKind()`. Games that never look at it behave exactly as they always have, because every session reports `'session'`; only the walkthrough beside it differs.
427
+
428
+ Try it with `sdk dev` and open `/walkthrough`, or from the command line with `sdk tick --walkthrough`.
429
+
430
+ ### Time limits
431
+
432
+ Every call into your engine (`Init`, `Run`, `InitAggregate`, `RunAggregate`) and into a tick controller has a time limit. A call that runs past it is abandoned: the tick fails, the error is reported to the facilitator, and nothing it would have written is kept. The dev server allows two minutes per call; a production runtime sets its own limit, which may be shorter.
433
+
434
+ The limit catches a call that is waiting, such as on a slow LLM call. The dev server cannot catch synchronous code that never returns, such as a `while (true)` that never meets its exit condition: it runs logic on its own thread, so the loop blocks the timer too. A command then fails with `DEV_SERVER_UNRESPONSIVE`, and `sdk dev stop` recovers (see [Troubleshooting](../troubleshooting.md#a-command-fails-with-dev_server_unresponsive-or-hangs-after-a-tick)). Do not rely on a production runtime to stop such a loop either.
435
+
436
+ Keep each tick well inside that. An engine that waits on several LLM calls in a row is the usual way to get close, so run independent calls together with `Promise.all` rather than one after another.
437
+
438
+ ## Declaring runtime callbacks
439
+
440
+ Engines that call `callbacks.generateText` or `callbacks.extractText` must list the API in the bundle's `uses` array. Calls to undeclared callbacks throw `UndeclaredApiUseError` at runtime, in dev and production alike.
441
+
442
+ ```json
443
+ {
444
+ "definition": {
445
+ "reads": ["text"],
446
+ "writes": ["summary"],
447
+ "uses": ["generateText"]
448
+ },
449
+ "logic": { "directory": ".", "entrypoint": "logic.ts" },
450
+ "rootDirectory": "."
451
+ }
452
+ ```
453
+
454
+ The per-resource `types.ts` generator narrows the `Callbacks` type alias to the declared APIs. Each declared one is always present, so call it directly, with no `?.` or presence check: a runtime that cannot provide a declared service refuses the package at load instead.
455
+
456
+ ```typescript
457
+ import type { RunFunction } from '@seriousgaming/sdk';
458
+ import type { Context, Settings, Callbacks } from './types';
459
+
460
+ export const Run: RunFunction<Context, Settings, Callbacks> = async ({ context, callbacks }) => {
461
+ const summary = await callbacks.generateText('Summarise: ' + context.text);
462
+ return { capabilities: { ...context, summary: { content: summary.content } } };
463
+ };
464
+ ```
465
+
466
+ See the [declare API uses recipe](../recipes/declare-api-uses.md) and [Declare the APIs a resource uses](./llm-integration.md#declare-the-apis-a-resource-uses) for the full mental model.
467
+
468
+ ## Testing engines
469
+
470
+ Build the arguments with `logicArgs` from `@seriousgaming/sdk/testing`, which fills in `meta`, `participants` and `callbacks`, and start from the generated defaults:
471
+
472
+ ```typescript
473
+ import { logicArgs } from '@seriousgaming/sdk/testing';
474
+ import { describe, it, expect } from 'vitest';
475
+ import { defaultContext, defaultSettings } from './defaults';
476
+ import { Run } from './logic';
477
+ import type { Context, Settings } from './types';
478
+
479
+ describe('warehouse engine', () => {
480
+ it('decreases stock by daily sales', async () => {
481
+ const context: Context = { ...defaultContext, inventory: { Stock: 100 } };
482
+ const settings: Settings = { engine: { ...defaultSettings.engine, DailySales: 10 } };
483
+
484
+ const result = await Run(logicArgs.run({ context, settings }));
485
+
486
+ expect(result.capabilities.inventory.Stock).toBe(90);
487
+ });
488
+ });
489
+ ```
490
+
491
+ See [Testing](./testing.md#testing-engine-logic) for `Init`, aggregate functions, and LLM callbacks.
492
+
493
+ ## Best practices
494
+
495
+ 1. **Keep steps focused**: one responsibility per step function the engine calls
496
+ - ✅ One step for inventory, one for pricing
497
+ - ❌ A single step handling inventory, pricing, and sales
498
+
499
+ 2. **Declare exactly what you use**: list every capability the engine reads or writes, and nothing it does not
500
+ - ✅ `reads` and `writes` that match the logic
501
+ - ❌ A capability read 'just in case', or one the logic touches without declaring
502
+
503
+ 3. **Use immutable updates**: always use spread operators
504
+ - ✅ `{ capabilities: { ...context, sales: { ...context.sales, newField } } }`
505
+ - ❌ `context.sales.newField = value`
506
+
507
+ 4. **Use generated types**: never use `any` or write types by hand
508
+ - ✅ `import type { Context, Settings } from './types'` and use them in function signatures
509
+ - ❌ `const { Price }: any = context.pricing`
510
+ - See the [type safety guide](./type-safety.md)
511
+
512
+ 5. **Handle edge cases**: prevent invalid states
513
+ - ✅ Define constraints in the schema, such as `"minimum": 0`, so the range is declared and settings overrides are checked against it
514
+ - ✅ Bound computed values in logic: the runtime does not clamp what the engine returns to the schema's range
515
+ - ❌ Allow negative inventory
516
+ - ❌ Assume a schema constraint corrects a value the engine computes
517
+
518
+ 6. **Test logic**: write unit tests for your functions, then exercise the whole engine with `pnpm run sdk tick` or the dev server
519
+ - ✅ Test each calculation with different inputs
520
+ - ❌ Assume it works
521
+
522
+ ## Next steps
523
+
524
+ - [Capabilities](./capabilities.md): the state model the engine reads and writes
525
+ - [Shaping games](./games.md): wire the engine into a game
526
+ - [Multi-team games](./multi-team-games.md): aggregate engines and shared capabilities
527
+ - [Testing](./testing.md): write comprehensive tests