@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,586 @@
1
+ # Multi-team games
2
+
3
+ This guide explains how to build and test games where multiple teams compete, cooperate, or operate independently. It covers play modes, shared capabilities, an aggregate engine, team composition, and how to run multi-team simulations via the dev server and CLI.
4
+
5
+ ## What you'll learn
6
+
7
+ - The two interaction modes: **independent** and **shared-state**
8
+ - How to declare your game's interaction mode and set team composition constraints
9
+ - How to use shared capabilities and an aggregate engine for cross-team interaction
10
+ - How to test multi-team games with `--teams N` in the dev server and CLI
11
+
12
+ ## Play modes
13
+
14
+ Every multi-team game operates in one of two **interaction modes**. The mode is a fixed property of the game definition: the author chooses it once, and facilitators and participants never select it. This keeps a structural, technical concern out of the hands of everyone downstream of authoring.
15
+
16
+ ### Independent mode
17
+
18
+ Teams are fully isolated: each team has its own context, makes its own decisions, and **ticks on its own schedule**. No cross-team data flows between teams. This is the right mode for games where teams solve the same challenge in parallel without affecting each other (e.g. training exercises, individual assessments).
19
+
20
+ An aggregate engine still executes in independent mode, but each call receives a single-element `teams` array containing only that team's context. This means the same engine code works in both modes without branching.
21
+
22
+ ### Shared-state mode
23
+
24
+ Teams interact through **shared capabilities** computed by **an aggregate engine**. All teams **tick together** (synchronised) because an aggregate engine needs every team's state at the same point in time. This is the right mode for competitive markets, cooperative scenarios, or any game where one team's decisions affect another.
25
+
26
+ **Example**: In a market simulation, each team sets their own `Price`, and the resulting `MarketShare` depends on all teams' prices. `Price` and `MarketShare` are both per-team; a session-wide figure computed from every team's decisions, such as `AverageMarketPrice`, is what comes from a shared capability.
27
+
28
+ ### Declaring the interaction mode
29
+
30
+ In your `seriousgaming.sdk.json`:
31
+
32
+ ```json
33
+ {
34
+ "identifier": "com.example.marketsim",
35
+ "teams": {
36
+ "mode": "shared-state"
37
+ }
38
+ }
39
+ ```
40
+
41
+ - A game runs in exactly one interaction mode.
42
+ - If `teams.mode` is omitted, it defaults to `independent`.
43
+ - The mode is fixed at authoring time. If a single game genuinely needs both topologies, ship two game definitions.
44
+
45
+ ### Team composition
46
+
47
+ Games can declare advisory metadata about team and participant counts:
48
+
49
+ ```json
50
+ {
51
+ "identifier": "com.example.marketsim",
52
+ "teams": {
53
+ "mode": "shared-state",
54
+ "min": 2,
55
+ "max": 6,
56
+ "perTeam": {
57
+ "minParticipants": 3,
58
+ "maxParticipants": 8
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ | Field | Meaning |
65
+ | ------------------------- | ------------------------------------------------- |
66
+ | `min` | Minimum number of teams this game is designed for |
67
+ | `max` | Maximum number of teams this game is designed for |
68
+ | `perTeam.minParticipants` | Minimum participants per team |
69
+ | `perTeam.maxParticipants` | Maximum participants per team |
70
+
71
+ All fields are optional, and each is enforced differently. Every field but `mode` is a [game setting](./settings.md#the-game-settings), so a scenario or a setup screen can change it; what follows applies to the value it resolves to. `max` is hard at every point: no session holds more teams, and a runtime may cap it further with its own platform ceiling (40 teams on the reference platform), so the effective maximum is the lower of the two. `min` applies when a session is created and again when play starts: a team count outside `min` and `max` is refused rather than adjusted, and once the session is running a team can be removed down to one. The `perTeam` fields are advisory. The dev server uses `min` as the default team count when `--teams` is not passed.
72
+
73
+ ### Naming teams
74
+
75
+ Every team carries a platform-assigned, session-stable, zero-based `index`. Team ids are `team-${index + 1}`, so the team with index 0 is `team-1`. The index never renumbers: removing a team leaves a gap in the `team-N` sequence, and a later-added team takes the lowest unused index. That team is a new one with the default name: nothing of the removed team comes back with it. Only an empty team in an independent-mode session can be removed (see [Deleting a team](./session.md#deleting-a-team)).
76
+
77
+ By default teams are shown as 'Team 1', 'Team 2', and so on. To give them meaningful default names (for example a game where each team is a predefined brand), declare `teams.defaultNames`: an ordered, localised list applied by index, so entry N names the team with index N.
78
+
79
+ ```json
80
+ {
81
+ "identifier": "com.example.marketsim",
82
+ "teams": {
83
+ "mode": "shared-state",
84
+ "defaultNames": [
85
+ { "en": { "name": "Acme" }, "fr": { "name": "Acmé" } },
86
+ { "en": { "name": "Globex" } },
87
+ { "en": { "name": "Initech" } }
88
+ ]
89
+ }
90
+ }
91
+ ```
92
+
93
+ Each entry is a locale-keyed map, the same shape used for other localised fields. Resolution works as follows:
94
+
95
+ - A name is shown in the session's locale, falling back to another locale the entry declares, then to the positional default ('Team N')
96
+ - Indexes beyond the end of the list fall back to the positional default ('Team N')
97
+ - Because names are applied by index, display names and game-logic identity cannot diverge: a re-added team recovers its `defaultNames` entry along with its index
98
+ - A scenario can replace these names with its own list, under `settings.game.teams.defaultNames`, so one game can present different brands per scenario (see [Scenarios](./scenarios.md#overriding-team-names)). A setup screen can too, for one session
99
+ - Names are advisory defaults: operators can still rename teams at session setup
100
+
101
+ ## Core concepts
102
+
103
+ ### Per-team vs. shared state
104
+
105
+ In independent mode (or a single-team game), each team has its own isolated context: decisions made by one team have no effect on another. Shared-state mode adds two cross-team tools, both declared through the capability's `scope`:
106
+
107
+ - **Shared capabilities** (`scope: 'shared'`): one value for all teams, computed from every team's data by an aggregate engine and distributed back to every team
108
+ - **Team-readable capabilities** (`scope: 'team-readable'`): each team still writes only its own copy, but other teams' user interfaces can observe every team's value live, without waiting for a tick
109
+
110
+ A capability with no `scope` (or `scope: 'team'`) stays private to each team.
111
+
112
+ ### Shared capabilities
113
+
114
+ Mark a capability as shared in `capability.sdk.json`:
115
+
116
+ ```json
117
+ {
118
+ "definition": {
119
+ "scope": "shared",
120
+ "variablesSchema": {
121
+ "type": "object",
122
+ "properties": {
123
+ "AverageMarketPrice": { "type": "number", "default": 0 },
124
+ "PriceChange": { "type": "number", "default": 0 }
125
+ },
126
+ "required": ["AverageMarketPrice", "PriceChange"],
127
+ "additionalProperties": false
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ Earlier SDK versions used a boolean `"shared": true` flag; it has been removed. A capability that still declares it fails the build with a message pointing at `"scope": "shared"`.
134
+
135
+ **Only put genuinely session-wide values in a shared capability.** A shared capability holds exactly one value for the whole session, so a per-team figure cannot live in one, even when an aggregate engine computes it from every team's data. `AverageMarketPrice` is session-wide, so it belongs here; each team's `MarketShare` is not, so it belongs in a separate default-scope capability the aggregate engine writes per team (see [Aggregate engines](#aggregate-engines) below). Splitting a capability along its actual scope is the fix whenever you find yourself wanting to write different values per team to the same shared identifier.
136
+
137
+ **Rules:**
138
+
139
+ - Shared capabilities have exactly two writers: **an aggregate engine** and **session UIs** that declare the capability in `writes` (see below)
140
+ - Regular `Run` functions may read shared capabilities but must not change them
141
+ - The SDK enforces this at runtime: a regular engine that modifies a shared capability throws an error
142
+ - The build also validates this statically: a non-aggregate engine with `writes` to a shared capability fails `pnpm run sdk build`, as does a game UI declaring such a write
143
+ - A team-addressed context update (a game UI's `setData`, `patch --team N`) that would change a shared capability is rejected by the dev server in multi-team games, because a per-team value for shared scope cannot be represented. Echoing the current shared value back (as full-context senders do) is fine
144
+
145
+ ### Facilitator-writable shared capabilities
146
+
147
+ Games with live facilitator dials (prices, thresholds, toggles) need session-wide values that can change at runtime. Settings cannot hold them, because they are fixed when play starts. Instead, declare a capability (here `dials`) with `scope: 'shared'` and add it to the session UI's `writes`:
148
+
149
+ ```json
150
+ {
151
+ "definition": {
152
+ "writes": ["dials"]
153
+ }
154
+ }
155
+ ```
156
+
157
+ The session UI then writes it with the one-argument form of `setData`: no team id, one call, and every team observes the same value:
158
+
159
+ ```typescript
160
+ import type { Settings, SessionData, SharedWrites } from '../types';
161
+
162
+ const { sessionData, setData } = useSessionUserInterfaceData<SessionData, Settings, SharedWrites>();
163
+
164
+ setData({ dials: { BasePrice: 12 } }); // session-wide, atomic
165
+ sessionData.shared.context.dials.BasePrice; // read it back once, not off a team
166
+ ```
167
+
168
+ Generated types include `SharedWrites`, covering exactly the shared capabilities the UI declares in `writes`; pass it as the third generic to type the call. `TeamWrites`, the rest of `writes`, is the fourth, and types the per-team `setData(teamId, update)` so that naming a shared capability there fails to compile. `SharedContext` covers every shared capability the UI declares, reads as well as writes, and is what `sessionData.shared.context` holds.
169
+
170
+ Ordering is well defined: a facilitator write that lands while a tick is executing is queued and applied after the tick, so the engine never silently reverts it. The next tick consumes the new value as input. If an aggregate engine recomputes the capability each tick (for example a scripted per-tick pre-load), the engine's value reasserts at the next tick boundary. That is expected: the facilitator's edit holds until then.
171
+
172
+ In independent-mode games the same write reaches every team in one call, because a shared capability holds one value for the whole session. An engine cannot write a shared capability in a multi-team independent game (the build refuses it), so the facilitator's value stands until they change it.
173
+
174
+ For headless testing, a [test session](./engine-testing.md#injecting-decisions) makes the same facilitator write with `{ type: 'updateSharedContext', payload }`.
175
+
176
+ ### Team-readable capabilities
177
+
178
+ Shared capabilities only update at tick time, because an aggregate engine computes them. When teams need to see one another's **pending** decisions live (for example, watching a shared resource pool settle while everyone negotiates), mark the decision capability as team-readable:
179
+
180
+ ```json
181
+ {
182
+ "definition": {
183
+ "scope": "team-readable",
184
+ "variablesSchema": {
185
+ "type": "object",
186
+ "properties": {
187
+ "Price": { "type": "number", "default": 10 }
188
+ },
189
+ "required": ["Price"],
190
+ "additionalProperties": false
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ Then declare the cross-team read on the user interface that wants to observe it, in `ui.sdk.json`:
197
+
198
+ ```json
199
+ {
200
+ "definition": {
201
+ "reads": ["decisions"],
202
+ "crossTeamReads": ["decisions"],
203
+ "writes": ["decisions"]
204
+ }
205
+ }
206
+ ```
207
+
208
+ In the UI, read every team's live value with the `useUserInterfaceCrossTeamData` hook:
209
+
210
+ ```typescript
211
+ import { useUserInterfaceCrossTeamData } from '@seriousgaming/sdk/ui';
212
+
213
+ import type { CrossTeamContext } from '../types';
214
+
215
+ function PoolGauge() {
216
+ const teams = useUserInterfaceCrossTeamData<CrossTeamContext>();
217
+ // [{ teamId: 'team-1', index: 0, name: 'Acme', context: { decisions: { Price: 12 } } }, ...]
218
+ const total = teams.reduce((sum, team) => sum + team.context.decisions.Price, 0);
219
+ return <div>Combined: {total}</div>;
220
+ }
221
+ ```
222
+
223
+ The hook takes no capability identifier: each entry's `context` carries exactly the capabilities this surface listed in `crossTeamReads`. `CrossTeamContext` is generated beside `Context` from those declarations, so pass it as the type argument and read fields off it: naming a capability left out of `crossTeamReads` is then a compile error rather than a result carrying your own team and nobody else. The array contains every team including your own (identifiable by `teamId`), and updates within about a second of any team calling `setData`, without a tick.
224
+
225
+ **Rules:**
226
+
227
+ - Cross-team observation is read-only: each team still writes only its own copy, so there is no write contention
228
+ - Cross-team reads are only valid on user interfaces, and only against capabilities with `scope: 'team-readable'`; anything else fails the build
229
+ - Team-readable capabilities require `teams.mode: 'shared-state'`; using one in an independent-mode game fails the build
230
+ - Observed values are pending state: they can change again before the tick consumes them, so treat them as live negotiation data, not committed results
231
+ - Keep private data private by splitting capabilities: put publicly observable decisions in a team-readable capability and everything else in a default-scope one
232
+ - To review the cross-team picture of an earlier tick, `getPastState({ fromTick: tick, teams: 'all' })` returns the same per-team entries as they stood at a committed past tick, each carrying the tick it came from. It is the historical counterpart of `useUserInterfaceCrossTeamData` (see [Review a past day](../recipes/review-a-past-day.md))
233
+ - For teams that transact with each other rather than merely observe, see [Negotiate across teams](../recipes/negotiate-across-teams.md). A game UI cannot write another team's state, so a mutual exchange is published as intent by each side and settled authoritatively by an aggregate engine
234
+
235
+ #### Choosing between team-readable and shared
236
+
237
+ The two scopes look like alternative ways to show one team something about the others, and they cost very different amounts as a session grows.
238
+
239
+ A cross-team read is delivered as one entry per team, to every participant's screen, every time any team writes it. So its cost rises with team count, with participant count, and with how often people are typing, all at the same time. Six teams in a workshop will never notice. A hundred teams will.
240
+
241
+ A `shared` capability is one value for the whole session, however many teams there are. Its cost does not change with cohort size at all.
242
+
243
+ Most cross-team screens want a summary rather than a roll call: a ranking, a market price, an average, a distribution, or a count of who has submitted. Every one of those an aggregate engine can compute into a shared capability, and a game written that way runs the same in a hall as in a room.
244
+
245
+ Reach for `team-readable` when the screen genuinely needs each team's own value attributed to that team, live, before the tick consumes it. That is the thing no aggregate can reconstruct, and it is worth paying for. Where either would work, prefer `shared`.
246
+
247
+ ### Aggregate engines
248
+
249
+ An aggregate engine runs once across all teams at both init and tick time, receiving every team's context simultaneously. It computes shared outputs and updates each team's individual context.
250
+
251
+ **An engine is aggregate because of what its logic exports.** Export `InitAggregate` and `RunAggregate` and it is one; there is no flag to set, and nothing to keep in step with the code. The build reads the exports and records what it found in the package it produces. A per-team `Init` and `Run` are both optional: `Init` runs per team before `InitAggregate`, and `Run` per team before `RunAggregate`.
252
+
253
+ Exporting one of `InitAggregate` / `RunAggregate` without the other fails the build, naming the one that is missing: an aggregate engine runs across every team at both initialisation and tick time, so it needs both.
254
+
255
+ The bundle itself carries only the capabilities it reads and writes, and the settings it reads:
256
+
257
+ ```json
258
+ {
259
+ "definition": {
260
+ "reads": ["decisions"],
261
+ "writes": ["market", "marketshare"]
262
+ },
263
+ "logic": {
264
+ "directory": ".",
265
+ "entrypoint": "logic.ts"
266
+ },
267
+ "rootDirectory": "."
268
+ }
269
+ ```
270
+
271
+ Note the two `writes`, one per scope: the shared `market` for the session-wide average, and the default-scope `marketshare` for each team's own share. One aggregate engine routinely writes both.
272
+
273
+ `sdk new engine --aggregate` scaffolds logic with the three exports already in place, so a freshly created aggregate engine builds without changes.
274
+
275
+ ## The aggregate functions
276
+
277
+ Implement all required functions in your engine's `logic.ts`:
278
+
279
+ ```typescript
280
+ import {
281
+ InitializationFunction,
282
+ InitAggregateFunction,
283
+ RunAggregateFunction,
284
+ } from '@seriousgaming/sdk';
285
+ import type { Context, Settings, SharedContext, TeamContext } from './types';
286
+
287
+ // Per-team init: set this team's own default values
288
+ export const Init: InitializationFunction<Context, Settings> = async ({ context }) => {
289
+ return {
290
+ capabilities: {
291
+ ...context,
292
+ marketshare: {
293
+ ...context.marketshare,
294
+ MarketShare: 0,
295
+ },
296
+ },
297
+ };
298
+ };
299
+
300
+ // Aggregate init: runs once with all teams' initial contexts
301
+ export const InitAggregate: InitAggregateFunction<TeamContext, SharedContext, Settings> = async ({
302
+ teams,
303
+ }) => {
304
+ const equalShare = 1 / teams.length;
305
+ return {
306
+ // The session-wide value goes in the shared slice...
307
+ shared: { capabilities: { market: { AverageMarketPrice: 0, PriceChange: 0 } } },
308
+ // ...and the per-team value in each team's own context
309
+ teams: teams.map((t) => ({
310
+ capabilities: {
311
+ ...t.context,
312
+ marketshare: {
313
+ ...t.context.marketshare,
314
+ MarketShare: equalShare,
315
+ },
316
+ },
317
+ })),
318
+ };
319
+ };
320
+
321
+ // Aggregate tick: runs once with all teams' data
322
+ export const RunAggregate: RunAggregateFunction<TeamContext, SharedContext, Settings> = async ({
323
+ shared,
324
+ teams,
325
+ }) => {
326
+ // teams is an array, sorted by index: [{ teamId, index, context }, ...]
327
+ // Each team's context carries team scope only; the shared store arrives once, in shared
328
+ const prices = teams.map((t) => t.context.decisions.Price);
329
+ const avgPrice = prices.reduce((sum, p) => sum + p, 0) / prices.length;
330
+ const previousAverage = shared.context.market.AverageMarketPrice;
331
+
332
+ return {
333
+ // shared: one value for the session, written to the shared store
334
+ shared: {
335
+ capabilities: {
336
+ market: {
337
+ AverageMarketPrice: avgPrice,
338
+ PriceChange: avgPrice - previousAverage,
339
+ },
340
+ },
341
+ },
342
+ // teams: updated capabilities per team (same order as input)
343
+ teams: teams.map((t) => {
344
+ const myPrice = t.context.decisions.Price;
345
+ const marketShare =
346
+ avgPrice > 0 ? (avgPrice - myPrice + 1) / avgPrice : 1 / teams.length;
347
+
348
+ return {
349
+ capabilities: {
350
+ ...t.context,
351
+ marketshare: {
352
+ ...t.context.marketshare,
353
+ MarketShare: Math.max(0, marketShare),
354
+ },
355
+ },
356
+ };
357
+ }),
358
+ };
359
+ };
360
+ ```
361
+
362
+ ### Arguments
363
+
364
+ Both `InitAggregate` and `RunAggregate` receive `{ shared, teams, settings, meta, participants, callbacks }`:
365
+
366
+ | Field | Type | Description |
367
+ | -------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
368
+ | `shared` | `{ context: SharedContext }` | The session's shared store, once: only the shared capabilities the engine declares in `reads` or `writes` |
369
+ | `teams` | `{ teamId: string; index: number; context: TeamContext }[]` | Every team, sorted by index. Each `context` carries that team's own capabilities only, never the shared ones |
370
+ | `participants` | `readonly Participant[]` | The participants of every team in `teams`, by team and then join order, taken when the operation started. Group by `teamId` |
371
+
372
+ The generated `types.ts` exports `TeamContext` and `SharedContext` beside `Context`: each is a `Pick` of `Context` by scope (`SharedContext` is `Record<string, never>` when the engine declares no shared capability). `Context` is still the composed view `Init` and a per-team `Run` receive, with shared values included.
373
+
374
+ Read a shared value from `shared.context`, never off a team. Reading it off `teams[0]` has nothing to read in a session with no teams, and at a hundred teams hands you a hundred copies of one value.
375
+
376
+ ### Return value
377
+
378
+ Both `InitAggregate` and `RunAggregate` return the same shape:
379
+
380
+ | Field | Type | Description |
381
+ | -------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
382
+ | `shared` | `{ capabilities: Partial<SharedContext>, deletes? }` | Shared capability values, field-merged over the shared store, so every team observes them |
383
+ | `teams` | `{ capabilities: TeamContext, deletes? }[]` | Updated capabilities for each team, in the same order as the input `teams` array, shaped like `Run` but team scope only |
384
+
385
+ Either level may carry `deletes`, paths removed after its `capabilities` fold (see [Removing a key](./engine.md#removing-a-key)). `shared.deletes` may name only shared capabilities, and a `teams` entry's only team ones.
386
+
387
+ Every key in `shared.capabilities` must be a `scope: 'shared'` capability identifier: anything else fails the init or tick with an error naming the engine and the capability. Shared capabilities live in one session-wide store, and the `shared` slice is the only way an aggregate engine writes it; every team's composed view re-reads the store, so all teams observe the new value.
388
+
389
+ **A shared capability inside a `teams` entry fails the init or tick**, even when every team carries the same value:
390
+
391
+ ```
392
+ Engine "engine" returned shared capability "market" in teams[0] of RunAggregate. Return shared capabilities once, under "shared": { capabilities: { market: ... } }.
393
+ ```
394
+
395
+ A `teams` entry is a per-team patch, and a capability with one value per session has no per-team value to patch. Earlier SDK versions adopted a value every team carried identically; that channel is gone. Spreading `...t.context` into a `teams` entry is safe, because `t.context` no longer carries shared capabilities. If you want a different figure per team, give it its own default-scope capability.
396
+
397
+ In independent-mode games, the engine cannot write shared capabilities at all (it fails game validation): with teams ticking on their own clocks there is no coherent writer order. Shared capabilities are facilitator-only there, written session-wide from a session UI, and survive per-team undo and reset.
398
+
399
+ ### Execution order
400
+
401
+ **Initialisation**: when the executor processes an aggregate engine during `init`:
402
+
403
+ 1. **Per-team `Init`** (optional): runs once per team, as on a regular engine. Without one, each team starts from the capability defaults
404
+ 2. **`InitAggregate`**: runs once with all teams' post-init contexts (team scope only) and the shared store, as `shared`. May write to shared capabilities
405
+ 3. **Result applied**: the `capabilities` of each entry in `result.teams` merge into the corresponding team's context, then the result's shared values land on the session-wide store and every team's view re-composes from it
406
+
407
+ **Tick**: when the game's engine is aggregate:
408
+
409
+ 1. **Per-team `Run`** (optional): runs once per team. May read shared capabilities but must not write to them
410
+ 2. **`RunAggregate`**: runs once with all teams' contexts (team scope only) and the shared store, as `shared`. May write to shared capabilities
411
+ 3. **Result applied**: the `capabilities` of each entry in `result.teams` merge into the corresponding team's context, then the result's shared values land on the session-wide store and every team's view re-composes from it
412
+
413
+ ### Keying off the team index
414
+
415
+ Each entry in the `teams` array is `{ teamId, index, context }`, and the array is always sorted by `index`. The `index` is the team's session-stable, zero-based identity (see [Naming teams](#naming-teams)), so it is the right key for roster-style logic: per-team economics, targeted documents, or any configuration that must follow a team across its life. Reaching for the array position instead breaks the moment a team is removed and the array shortens, whereas the index does not move.
416
+
417
+ ```typescript
418
+ import { RunAggregateFunction } from '@seriousgaming/sdk';
419
+ import type { Settings, SharedContext, TeamContext } from './types';
420
+
421
+ // Per-team starting capital, indexed by the stable team index
422
+ const ROSTER = [
423
+ { startingCapital: 100_000 },
424
+ { startingCapital: 80_000 },
425
+ { startingCapital: 120_000 },
426
+ ];
427
+
428
+ export const RunAggregate: RunAggregateFunction<TeamContext, SharedContext, Settings> = async ({
429
+ teams,
430
+ }) => {
431
+ return {
432
+ shared: { capabilities: {} },
433
+ teams: teams.map((t) => {
434
+ const slot = ROSTER[t.index] ?? { startingCapital: 100_000 };
435
+ return {
436
+ capabilities: {
437
+ ...t.context,
438
+ finance: {
439
+ ...t.context.finance,
440
+ StartingCapital: slot.startingCapital,
441
+ },
442
+ },
443
+ };
444
+ }),
445
+ };
446
+ };
447
+ ```
448
+
449
+ In a single-team game the index is 0. In independent mode the single-element `teams` array still carries that team's real index, so the same lookup works in every mode.
450
+
451
+ ## Testing in the dev server
452
+
453
+ Start the dev server with multiple teams:
454
+
455
+ ```bash
456
+ pnpm run sdk dev --teams 3
457
+ ```
458
+
459
+ **Auto-derived team count**: If you don't pass `--teams`, the dev server reads `teams.min` from your game. For example, if your game declares `"teams": { "min": 3 }`, running `pnpm run sdk dev` will automatically start with 3 teams. The `--teams` flag always overrides this, but it must sit within the game's bounds: a count below `min` or above `max` is refused, not adjusted.
460
+
461
+ Each team gets its own game UI URL, and the console at `/` is the session UI:
462
+
463
+ | View | URL |
464
+ | ----------- | ------------------------------------- |
465
+ | Team 1 game | http://localhost:3000/team/1/player/1 |
466
+ | Team 2 game | http://localhost:3000/team/2/player/1 |
467
+ | Team 3 game | http://localhost:3000/team/3/player/1 |
468
+ | Console | http://localhost:3000/ |
469
+
470
+ **Participants per team**: Every team starts with one participant, or the game's `perTeam.minParticipants`, so each player URL has somebody behind it. Pass `--participants` to `sdk dev` or `sdk init` to choose: `--participants 4` for a full table everywhere, `--participants 0` to see what your game does before anybody joins, or one count per team such as `--participants 2,0,1`. A player URL for a seat nobody holds is refused rather than mounted, and a team's join page says it is empty. `sdk team add --participants N` seeds a team added later.
471
+
472
+ **Teams in the dev drawer**: The console's dev drawer (press the backtick key) lists every team on its Session tab with its current tick, per-team Tick, Undo and Reset, and 'Add participant'. 'Add team' adds a team at runtime, up to the game's `teams.max`. A team can be removed down to a single team: `teams.min` applies only when the session is created and when play starts.
473
+
474
+ Runtime team changes preserve the stable team index:
475
+
476
+ - Removing a team does not renumber the others. Their ids, URLs, and participants stay put, and the `team-N` sequence keeps a gap where the removed team was. The next added team refills the lowest unused index as a new team with the default name, not the removed one back
477
+ - In shared-state games, adding a team is only allowed before the first tick. Adding one then re-initialises the session so `InitAggregate` sees every team. After the first tick the request is rejected, because the aggregate state has already been computed without the new team
478
+ - In shared-state games, a team cannot be removed at all, before the first tick or after, and in independent mode only an empty team can be
479
+
480
+ **The session UI**: The console (`/`) renders the game's session UI, which receives the state of **all** teams simultaneously, making it the right place to test leaderboards, cross-team views, or facilitator overviews.
481
+
482
+ **Per-team ticks in independent mode**: When the game declares `"mode": "independent"`, each team ticks independently. A tick requested from a team's game UI (via the game's own `requestTick` control, or that team's Tick in the dev drawer) advances only that team, and the dev drawer shows each team's tick. Use each team's game UI to advance teams on their own schedule.
483
+
484
+ **Synced ticks in shared-state mode**: When the game declares `"shared-state"` mode, all teams advance on the same tick.
485
+
486
+ **Context updates**: When a game UI writes a decision (via `setData`), it updates only that team's context.
487
+
488
+ ## Testing with CLI game commands
489
+
490
+ > **Multi-team is set once, at init.** You pass `--teams N` to `init` and nothing else. `tick` and `view` read the team count from the running session, so they advance and display every team automatically, with no per-command flag. `tick` accepts `--teams N` only on the first run (when it initialises the game for you), and `view` has no `--teams` at all; `--team N` only narrows an existing multi-team game to one team, and is refused on a session with one team. This is the headless path for verifying shared-state aggregate behaviour without the dev server.
491
+
492
+ ### Initialise with multiple teams
493
+
494
+ ```bash
495
+ pnpm run sdk init --teams 3
496
+ ```
497
+
498
+ This starts the session with three teams, and every later command detects it automatically. Without `--teams`, the game starts with its `teams.min` teams (one when it declares no minimum). Re-run `init` to change the count (this resets to tick 0).
499
+
500
+ ### Set decisions per-team
501
+
502
+ Use `patch` to set each team's decisions before ticking. In multi-team mode, `--team` is required:
503
+
504
+ ```bash
505
+ pnpm run sdk patch \
506
+ '[{"op":"replace","path":"/decisions/Price","value":8}]' -- --team 1
507
+ pnpm run sdk patch \
508
+ '[{"op":"replace","path":"/decisions/Price","value":6}]' -- --team 2
509
+ pnpm run sdk patch \
510
+ '[{"op":"replace","path":"/decisions/Price","value":4}]' -- --team 3
511
+ ```
512
+
513
+ ### Advance all teams (tick)
514
+
515
+ ```bash
516
+ pnpm run sdk tick
517
+ ```
518
+
519
+ Without `--team`, every team advances. With `--team N` (1-based), what ticks depends on the mode: in independent mode only team N advances, as its per-team Tick in the dev drawer does; in shared-state mode every team still advances together, because they share one tick.
520
+
521
+ `--team N` also scopes a `--patch` to that team before the tick fires:
522
+
523
+ ```bash
524
+ # Team 2 changes price, then team 2 ticks (independent) or every team ticks (shared-state)
525
+ pnpm run sdk tick \
526
+ -- --team 2 --patch '[{"op":"replace","path":"/decisions/Price","value":5}]'
527
+ ```
528
+
529
+ ### View state
530
+
531
+ ```bash
532
+ # All teams
533
+ pnpm run sdk view
534
+
535
+ # Single team
536
+ pnpm run sdk view --team 2
537
+ ```
538
+
539
+ ## Full workflow example
540
+
541
+ Put together, the pieces on this page make a complete multi-team game: a per-team `decisions` capability holding each team's `Price`, a shared `market` capability, a per-team `marketshare`, and the aggregate engine above. From the package root, run it end to end:
542
+
543
+ ```bash
544
+ # Initialise 3 teams
545
+ pnpm run sdk init --teams 3
546
+
547
+ # Each team sets a different price
548
+ pnpm run sdk patch \
549
+ '[{"op":"replace","path":"/decisions/Price","value":8}]' -- --team 1
550
+ pnpm run sdk patch \
551
+ '[{"op":"replace","path":"/decisions/Price","value":6}]' -- --team 2
552
+ pnpm run sdk patch \
553
+ '[{"op":"replace","path":"/decisions/Price","value":4}]' -- --team 3
554
+
555
+ # Run tick: market engine aggregates all prices, computes MarketShare per team
556
+ pnpm run sdk tick
557
+
558
+ # View results: AverageMarketPrice is shared, MarketShare differs per team
559
+ pnpm run sdk view
560
+ ```
561
+
562
+ To play it in the browser with five teams:
563
+
564
+ ```bash
565
+ pnpm run sdk dev --teams 5
566
+ ```
567
+
568
+ ## Single-team and independent-mode compatibility
569
+
570
+ Without `--teams` (or `--teams 1`), everything works exactly as before. An aggregate engine receives a single-element `teams` array with `teamId: 'team-1'`, so it works correctly without any code changes. `shared` is passed on every call, in either mode and with any number of teams.
571
+
572
+ The same principle applies in **independent mode** with multiple teams: an aggregate engine receives a single-element `teams` array for each team, containing only that team. The engine code is identical: the only difference is whether the `teams` array contains one team or all teams:
573
+
574
+ ```
575
+ Independent mode: RunAggregate({ shared, teams: [{ teamId: "team-1", index: 0, context: ctx1 }] }) // one team
576
+ RunAggregate({ shared, teams: [{ teamId: "team-2", index: 1, context: ctx2 }] }) // separate call
577
+
578
+ Shared-state mode: RunAggregate({ shared, teams: [{ teamId: "team-1", index: 0, context: ctx1 },
579
+ { teamId: "team-2", index: 1, context: ctx2 }] }) // single call
580
+ ```
581
+
582
+ ## Next steps
583
+
584
+ - [Creating engines](./engine.md) - Implement engine logic
585
+ - [Creating capabilities](./capabilities.md) - Define shared and per-team variables
586
+ - [Drive a game from the CLI](../cli-simulation-testing.md) - Play a multi-team session from the terminal with `--team` and `--teams`