@seriousgaming/sdk-tools 0.0.0-stage → 1.0.0-alpha.2
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.
- package/LICENSE +93 -0
- package/README.md +38 -2
- package/dist/bundling/logic-exports.d.ts +34 -0
- package/dist/bundling/node-builtins.d.ts +21 -0
- package/dist/bundling/transpiler.d.ts +7 -0
- package/dist/cli.js +27404 -0
- package/dist/client/dev-server.d.ts +59 -0
- package/dist/client/index.d.ts +28 -0
- package/dist/client/stub-services.d.ts +32 -0
- package/dist/client/test-session.d.ts +195 -0
- package/dist/client.js +16920 -0
- package/dist/core/asset-reference-translator.d.ts +46 -0
- package/dist/core/clock.d.ts +57 -0
- package/dist/core/faults.d.ts +65 -0
- package/dist/core/game-settings.d.ts +65 -0
- package/dist/core/history.d.ts +89 -0
- package/dist/core/listen-with-fallback.d.ts +30 -0
- package/dist/core/mutation-queue.d.ts +61 -0
- package/dist/core/package-scenario.d.ts +89 -0
- package/dist/core/participant-registry.d.ts +109 -0
- package/dist/core/pending-changes.d.ts +14 -0
- package/dist/core/protocol.d.ts +83 -0
- package/dist/core/scenario-drafts.d.ts +32 -0
- package/dist/core/session-endpoints.d.ts +108 -0
- package/dist/core/session-gates.d.ts +75 -0
- package/dist/core/session-host.d.ts +94 -0
- package/dist/core/session-refusals.d.ts +56 -0
- package/dist/core/session-rewind.d.ts +32 -0
- package/dist/core/session-roster.d.ts +128 -0
- package/dist/core/session-surfaces.d.ts +32 -0
- package/dist/core/session-ticks.d.ts +18 -0
- package/dist/core/session-timers.d.ts +84 -0
- package/dist/core/session-writes.d.ts +40 -0
- package/dist/core/session.d.ts +385 -0
- package/dist/core/settings-locks.d.ts +22 -0
- package/dist/core/team-names.d.ts +52 -0
- package/dist/core/team-state-store.d.ts +218 -0
- package/dist/core/tick-timer.d.ts +75 -0
- package/dist/dev/blob-links.d.ts +10 -0
- package/dist/dev/console/assets/index-DgFglsnA.js +204 -0
- package/dist/dev/console/index.html +12 -0
- package/dist/dev/console-path.d.ts +19 -0
- package/dist/dev/lockfile.d.ts +124 -0
- package/dist/dev/package-watcher.d.ts +21 -0
- package/dist/dev/routes.d.ts +67 -0
- package/dist/dev/shell.d.ts +260 -0
- package/dist/dev/surfaces.d.ts +41 -0
- package/dist/dev/tools-identity.d.ts +23 -0
- package/dist/dev/view/batch.d.ts +172 -0
- package/dist/dev/view/commands.d.ts +75 -0
- package/dist/dev/view/route.d.ts +58 -0
- package/dist/dev/view/view.d.ts +78 -0
- package/dist/dev/view/viewer.d.ts +32 -0
- package/dist/discovery/config.d.ts +9 -0
- package/dist/discovery/manifest-paths.d.ts +21 -0
- package/dist/discovery/removed-fields.d.ts +90 -0
- package/dist/docs/api/deck.md +386 -0
- package/dist/docs/api/index.md +67 -0
- package/dist/docs/api/sdk-commands.md +286 -0
- package/dist/docs/api/sdk-e2e.md +433 -0
- package/dist/docs/api/sdk-projection.md +207 -0
- package/dist/docs/api/sdk-testing.md +214 -0
- package/dist/docs/api/sdk-tools-client.md +560 -0
- package/dist/docs/api/sdk-ui-testing.md +231 -0
- package/dist/docs/api/sdk-ui.md +1269 -0
- package/dist/docs/api/sdk.md +2140 -0
- package/dist/docs/changelog-0.x.md +1156 -0
- package/dist/docs/changelog.md +241 -0
- package/dist/docs/cli-and-dev-server.md +217 -0
- package/dist/docs/cli-error-codes.md +156 -0
- package/dist/docs/cli-reference.md +2489 -0
- package/dist/docs/cli-simulation-testing.md +222 -0
- package/dist/docs/concepts/index.md +46 -0
- package/dist/docs/concepts/terminology.md +322 -0
- package/dist/docs/environment-variables.md +102 -0
- package/dist/docs/getting-started.md +145 -0
- package/dist/docs/guides/ai-facilitation.md +220 -0
- package/dist/docs/guides/capabilities.md +460 -0
- package/dist/docs/guides/endpoints.md +335 -0
- package/dist/docs/guides/engine-testing.md +477 -0
- package/dist/docs/guides/engine.md +527 -0
- package/dist/docs/guides/file-handling.md +405 -0
- package/dist/docs/guides/games.md +176 -0
- package/dist/docs/guides/global-settings.md +94 -0
- package/dist/docs/guides/guide.md +123 -0
- package/dist/docs/guides/hooks.md +183 -0
- package/dist/docs/guides/index.md +51 -0
- package/dist/docs/guides/internationalization.md +288 -0
- package/dist/docs/guides/llm-integration.md +635 -0
- package/dist/docs/guides/multi-team-games.md +586 -0
- package/dist/docs/guides/patterns.md +391 -0
- package/dist/docs/guides/presentation.md +196 -0
- package/dist/docs/guides/production-constraints.md +130 -0
- package/dist/docs/guides/scenario-editor.md +143 -0
- package/dist/docs/guides/scenarios.md +515 -0
- package/dist/docs/guides/session.md +972 -0
- package/dist/docs/guides/settings.md +458 -0
- package/dist/docs/guides/setup.md +193 -0
- package/dist/docs/guides/speech-generation.md +233 -0
- package/dist/docs/guides/starter-game.md +164 -0
- package/dist/docs/guides/testing.md +577 -0
- package/dist/docs/guides/tick-controller.md +569 -0
- package/dist/docs/guides/type-safety.md +516 -0
- package/dist/docs/guides/ui.md +1389 -0
- package/dist/docs/guides/upgrading.md +636 -0
- package/dist/docs/guides/validators.md +217 -0
- package/dist/docs/guides/waiting-room.md +244 -0
- package/dist/docs/index.md +41 -0
- package/dist/docs/recipes/act-on-the-session-from-an-endpoint.md +95 -0
- package/dist/docs/recipes/adapt-a-game-for-walkthroughs.md +133 -0
- package/dist/docs/recipes/adapt-ui-for-observers.md +74 -0
- package/dist/docs/recipes/call-endpoint-from-ui.md +109 -0
- package/dist/docs/recipes/check-a-package-from-a-script.md +117 -0
- package/dist/docs/recipes/create-capability.md +66 -0
- package/dist/docs/recipes/create-endpoint.md +127 -0
- package/dist/docs/recipes/create-engine.md +111 -0
- package/dist/docs/recipes/create-game.md +70 -0
- package/dist/docs/recipes/create-guide.md +75 -0
- package/dist/docs/recipes/create-hook.md +80 -0
- package/dist/docs/recipes/create-presentation.md +142 -0
- package/dist/docs/recipes/create-scenario-editor.md +102 -0
- package/dist/docs/recipes/create-scenario.md +98 -0
- package/dist/docs/recipes/create-session-ui.md +116 -0
- package/dist/docs/recipes/create-settings.md +88 -0
- package/dist/docs/recipes/create-setup.md +115 -0
- package/dist/docs/recipes/create-tick-controller.md +65 -0
- package/dist/docs/recipes/create-user-interface.md +96 -0
- package/dist/docs/recipes/create-validator.md +67 -0
- package/dist/docs/recipes/create-waiting-room-ui.md +109 -0
- package/dist/docs/recipes/declare-api-uses.md +67 -0
- package/dist/docs/recipes/design-state-for-concurrent-writes.md +96 -0
- package/dist/docs/recipes/distribute-package.md +80 -0
- package/dist/docs/recipes/e2e-testing.md +116 -0
- package/dist/docs/recipes/end-game-on-condition.md +154 -0
- package/dist/docs/recipes/fix-duplicate-packages.md +101 -0
- package/dist/docs/recipes/index.md +86 -0
- package/dist/docs/recipes/make-an-endpoint-external.md +101 -0
- package/dist/docs/recipes/negotiate-across-teams.md +164 -0
- package/dist/docs/recipes/observe-other-teams-state.md +119 -0
- package/dist/docs/recipes/publish-a-package.md +139 -0
- package/dist/docs/recipes/remember-a-viewers-choice.md +55 -0
- package/dist/docs/recipes/review-a-past-day.md +73 -0
- package/dist/docs/recipes/share-assets-across-scenarios.md +120 -0
- package/dist/docs/recipes/share-logic-between-engine-and-ui.md +130 -0
- package/dist/docs/recipes/show-all-teams-participants.md +109 -0
- package/dist/docs/recipes/start-from-an-empty-package.md +169 -0
- package/dist/docs/recipes/take-a-file-in-an-endpoint.md +82 -0
- package/dist/docs/recipes/test-a-multi-role-game.md +51 -0
- package/dist/docs/recipes/test-user-interface.md +95 -0
- package/dist/docs/recipes/test-with-engine.md +115 -0
- package/dist/docs/recipes/upload-and-display-files.md +148 -0
- package/dist/docs/recipes/use-request-play.md +65 -0
- package/dist/docs/recipes/use-request-tick.md +75 -0
- package/dist/docs/troubleshooting.md +816 -0
- package/dist/execution/blob-inputs.d.ts +57 -0
- package/dist/execution/bundle-reload.d.ts +42 -0
- package/dist/execution/config-resolver.d.ts +127 -0
- package/dist/execution/configuration-builder.d.ts +66 -0
- package/dist/execution/context-composition.d.ts +161 -0
- package/dist/execution/endpoint-actions.d.ts +9 -0
- package/dist/execution/endpoint-payloads.d.ts +32 -0
- package/dist/execution/endpoint-runner.d.ts +129 -0
- package/dist/execution/engine-state.d.ts +40 -0
- package/dist/execution/executor-callbacks.d.ts +56 -0
- package/dist/execution/executor.d.ts +436 -0
- package/dist/execution/gates.d.ts +45 -0
- package/dist/execution/hook-chain.d.ts +25 -0
- package/dist/execution/logic-calls.d.ts +190 -0
- package/dist/execution/logic-failure.d.ts +84 -0
- package/dist/execution/logic-loader.d.ts +54 -0
- package/dist/execution/logic-result.d.ts +76 -0
- package/dist/execution/tick-controller-call.d.ts +32 -0
- package/dist/execution/types.d.ts +48 -0
- package/dist/generation/ts-type.d.ts +39 -0
- package/dist/guide/locale.d.ts +12 -0
- package/dist/guide/video-registry.d.ts +42 -0
- package/dist/help-examples.d.ts +125 -0
- package/dist/host/adapter.d.ts +67 -0
- package/dist/host/host-channel.d.ts +222 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/protocol.d.ts +783 -0
- package/dist/host/surfaces.d.ts +42 -0
- package/dist/host/theme.d.ts +55 -0
- package/dist/host.js +583 -0
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/schema-builder.d.ts +29 -0
- package/dist/prompts/types.d.ts +108 -0
- package/dist/prompts/validators.d.ts +32 -0
- package/dist/runtime/capability-scope.d.ts +6 -0
- package/dist/runtime/context-merge.d.ts +29 -0
- package/dist/runtime/context-projection.d.ts +41 -0
- package/dist/runtime/guarded-callbacks.d.ts +15 -0
- package/dist/runtime/index.d.ts +27 -0
- package/dist/runtime/manifest-version.d.ts +55 -0
- package/dist/runtime/tick-validation.d.ts +25 -0
- package/dist/runtime.js +346 -0
- package/dist/schema.json +1546 -0
- package/dist/server/vite-plugin.d.ts +46 -0
- package/dist/services/tts/client.d.ts +51 -0
- package/dist/services/tts/elevenlabs.d.ts +24 -0
- package/dist/services/tts/types.d.ts +44 -0
- package/dist/storage/asset-resolver.d.ts +49 -0
- package/dist/storage/blob-store.d.ts +41 -0
- package/dist/templates/endpoint/logic.test.ts.liquid +61 -0
- package/dist/templates/endpoint/logic.ts.liquid +25 -0
- package/dist/templates/engine/init.json.liquid +7 -0
- package/dist/templates/engine/logic-aggregate.test.ts.liquid +90 -0
- package/dist/templates/engine/logic-aggregate.ts.liquid +46 -0
- package/dist/templates/engine/logic.test.ts.liquid +61 -0
- package/dist/templates/engine/logic.ts.liquid +32 -0
- package/dist/templates/engine/run.json.liquid +7 -0
- package/dist/templates/guide/01-at-a-glance.md.liquid +19 -0
- package/dist/templates/guide/02-preparation.md.liquid +18 -0
- package/dist/templates/guide/03-running-the-session.md.liquid +37 -0
- package/dist/templates/hook/logic.test.ts.liquid +46 -0
- package/dist/templates/hook/logic.ts.liquid +12 -0
- package/dist/templates/package/CLAUDE.md.template +338 -0
- package/dist/templates/package/DESIGN.md.liquid +144 -0
- package/dist/templates/package/README.md.liquid +143 -0
- package/dist/templates/package/claude/rules/workflow-create-capability.md +26 -0
- package/dist/templates/package/claude/rules/workflow-create-endpoint.md +35 -0
- package/dist/templates/package/claude/rules/workflow-create-engine.md +35 -0
- package/dist/templates/package/claude/rules/workflow-create-guide.md +40 -0
- package/dist/templates/package/claude/rules/workflow-create-presentation.md +37 -0
- package/dist/templates/package/claude/rules/workflow-create-user-interface.md +31 -0
- package/dist/templates/package/claude/rules/workflow-publish-package.md +54 -0
- package/dist/templates/package/claude/rules/workflow-test-engine-or-ui.md +30 -0
- package/dist/templates/package/claude/rules/workflow-update-sdk-version.md +43 -0
- package/dist/templates/package/claude/settings.json.template +16 -0
- package/dist/templates/package/gitignore.template +15 -0
- package/dist/templates/package/global-setup.ts.template +21 -0
- package/dist/templates/package/globals.d.ts.template +2 -0
- package/dist/templates/package/oxfmtrc.json.template +14 -0
- package/dist/templates/package/oxlintrc.json.template +238 -0
- package/dist/templates/package/package.json.liquid +65 -0
- package/dist/templates/package/playwright.config.ts.liquid +19 -0
- package/dist/templates/package/pnpm-workspace.yaml.template +27 -0
- package/dist/templates/package/test-setup.ts.liquid +7 -0
- package/dist/templates/package/tsconfig.json.liquid +15 -0
- package/dist/templates/package/vite.config.ts.liquid +19 -0
- package/dist/templates/package/vitest.config.ts.liquid +25 -0
- package/dist/templates/package/vitest.engine.config.ts.liquid +12 -0
- package/dist/templates/presentation/index.html.liquid +20 -0
- package/dist/templates/presentation/src/App.tsx.liquid +49 -0
- package/dist/templates/presentation/src/Slides.tsx.liquid +81 -0
- package/dist/templates/presentation/src/i18n.ts.liquid +54 -0
- package/dist/templates/presentation/src/main.tsx.liquid +14 -0
- package/dist/templates/scenario_editor/src/App.test.tsx.liquid +30 -0
- package/dist/templates/scenario_editor/src/App.tsx.liquid +95 -0
- package/dist/templates/scenario_editor/src/i18n.ts.liquid +44 -0
- package/dist/templates/session/src/App.test.tsx.liquid +60 -0
- package/dist/templates/session/src/App.tsx.liquid +128 -0
- package/dist/templates/session/src/i18n.ts.liquid +53 -0
- package/dist/templates/session/src/main.tsx.liquid +48 -0
- package/dist/templates/session/src/theme.ts.liquid +46 -0
- package/dist/templates/settings-editor/index.html.liquid +12 -0
- package/dist/templates/settings-editor/src/main.tsx.liquid +48 -0
- package/dist/templates/settings-editor/src/theme.ts.liquid +24 -0
- package/dist/templates/setup/src/App.test.tsx.liquid +39 -0
- package/dist/templates/setup/src/App.tsx.liquid +102 -0
- package/dist/templates/setup/src/i18n.ts.liquid +47 -0
- package/dist/templates/starter-game/capabilities/briefing/capability.sdk.json +18 -0
- package/dist/templates/starter-game/capabilities/plan/capability.sdk.json +17 -0
- package/dist/templates/starter-game/capabilities/results/capability.sdk.json +61 -0
- package/dist/templates/starter-game/capabilities/roles/capability.sdk.json +17 -0
- package/dist/templates/starter-game/capabilities/weather/capability.sdk.json +23 -0
- package/dist/templates/starter-game/engine/defaults.ts +81 -0
- package/dist/templates/starter-game/engine/engine.sdk.json +12 -0
- package/dist/templates/starter-game/engine/logic.test.ts +221 -0
- package/dist/templates/starter-game/engine/logic.ts +149 -0
- package/dist/templates/starter-game/engine/test-helpers.ts +74 -0
- package/dist/templates/starter-game/engine/types.ts +139 -0
- package/dist/templates/starter-game/guide/content/en/01-at-a-glance.md +51 -0
- package/dist/templates/starter-game/guide/content/en/02-preparation.md +35 -0
- package/dist/templates/starter-game/guide/content/en/03-running-the-day.md +58 -0
- package/dist/templates/starter-game/guide/guide.sdk.json +10 -0
- package/dist/templates/starter-game/guide/guide.test.ts +108 -0
- package/dist/templates/starter-game/hooks/assignproducts/defaults.ts +37 -0
- package/dist/templates/starter-game/hooks/assignproducts/hook.sdk.json +13 -0
- package/dist/templates/starter-game/hooks/assignproducts/logic/logic.test.ts +77 -0
- package/dist/templates/starter-game/hooks/assignproducts/logic/logic.ts +47 -0
- package/dist/templates/starter-game/hooks/assignproducts/types.ts +55 -0
- package/dist/templates/starter-game/package.json +68 -0
- package/dist/templates/starter-game/playwright.config.ts +19 -0
- package/dist/templates/starter-game/scenarios/heatwave/scenario.sdk.json +20 -0
- package/dist/templates/starter-game/scenarios/ordinary_week/scenario.sdk.json +12 -0
- package/dist/templates/starter-game/seriousgaming.sdk.json +23 -0
- package/dist/templates/starter-game/session/defaults.ts +80 -0
- package/dist/templates/starter-game/session/index.html +12 -0
- package/dist/templates/starter-game/session/session.sdk.json +11 -0
- package/dist/templates/starter-game/session/src/App.test.tsx +307 -0
- package/dist/templates/starter-game/session/src/App.tsx +309 -0
- package/dist/templates/starter-game/session/src/Section.tsx +37 -0
- package/dist/templates/starter-game/session/src/TeamDoor.tsx +96 -0
- package/dist/templates/starter-game/session/src/format.ts +8 -0
- package/dist/templates/starter-game/session/src/i18n.ts +51 -0
- package/dist/templates/starter-game/session/src/main.tsx +48 -0
- package/dist/templates/starter-game/session/src/theme.ts +45 -0
- package/dist/templates/starter-game/session/types.ts +135 -0
- package/dist/templates/starter-game/settings/engine/settings.sdk.json +50 -0
- package/dist/templates/starter-game/settings/menu/settings.sdk.json +78 -0
- package/dist/templates/starter-game/settings/session/settings.sdk.json +23 -0
- package/dist/templates/starter-game/settings/ui/settings.sdk.json +25 -0
- package/dist/templates/starter-game/test-setup.ts +7 -0
- package/dist/templates/starter-game/testing/e2e/game.spec.ts +139 -0
- package/dist/templates/starter-game/testing/e2e/global-setup.ts +21 -0
- package/dist/templates/starter-game/testing/engine/game.engine.test.ts +171 -0
- package/dist/templates/starter-game/tsconfig.json +19 -0
- package/dist/templates/starter-game/ui/defaults.ts +60 -0
- package/dist/templates/starter-game/ui/index.html +12 -0
- package/dist/templates/starter-game/ui/src/App.test.tsx +138 -0
- package/dist/templates/starter-game/ui/src/App.tsx +175 -0
- package/dist/templates/starter-game/ui/src/DayBar.test.tsx +78 -0
- package/dist/templates/starter-game/ui/src/DayBar.tsx +72 -0
- package/dist/templates/starter-game/ui/src/HistoryPanel.test.tsx +74 -0
- package/dist/templates/starter-game/ui/src/HistoryPanel.tsx +120 -0
- package/dist/templates/starter-game/ui/src/MakeRow.tsx +129 -0
- package/dist/templates/starter-game/ui/src/Section.tsx +37 -0
- package/dist/templates/starter-game/ui/src/ThemeToggle.tsx +49 -0
- package/dist/templates/starter-game/ui/src/TodayPanel.test.tsx +179 -0
- package/dist/templates/starter-game/ui/src/TodayPanel.tsx +92 -0
- package/dist/templates/starter-game/ui/src/YesterdayPanel.tsx +142 -0
- package/dist/templates/starter-game/ui/src/format.ts +12 -0
- package/dist/templates/starter-game/ui/src/i18n.ts +95 -0
- package/dist/templates/starter-game/ui/src/main.tsx +48 -0
- package/dist/templates/starter-game/ui/src/menu.ts +18 -0
- package/dist/templates/starter-game/ui/src/test-helpers.ts +58 -0
- package/dist/templates/starter-game/ui/src/theme.ts +45 -0
- package/dist/templates/starter-game/ui/src/writes.ts +43 -0
- package/dist/templates/starter-game/ui/types.ts +137 -0
- package/dist/templates/starter-game/ui/ui.sdk.json +11 -0
- package/dist/templates/starter-game/validators/capacity/defaults.ts +37 -0
- package/dist/templates/starter-game/validators/capacity/logic/logic.test.ts +96 -0
- package/dist/templates/starter-game/validators/capacity/logic/logic.ts +45 -0
- package/dist/templates/starter-game/validators/capacity/types.ts +55 -0
- package/dist/templates/starter-game/validators/capacity/validator.sdk.json +11 -0
- package/dist/templates/starter-game/vite.config.ts +19 -0
- package/dist/templates/starter-game/vitest.config.ts +15 -0
- package/dist/templates/starter-game/vitest.engine.config.ts +19 -0
- package/dist/templates/starter-game/waiting_room/defaults.ts +40 -0
- package/dist/templates/starter-game/waiting_room/index.html +12 -0
- package/dist/templates/starter-game/waiting_room/src/App.test.tsx +102 -0
- package/dist/templates/starter-game/waiting_room/src/App.tsx +194 -0
- package/dist/templates/starter-game/waiting_room/src/i18n.ts +33 -0
- package/dist/templates/starter-game/waiting_room/src/main.tsx +48 -0
- package/dist/templates/starter-game/waiting_room/src/theme.ts +45 -0
- package/dist/templates/starter-game/waiting_room/types.ts +59 -0
- package/dist/templates/starter-game/waiting_room/waiting_room.sdk.json +10 -0
- package/dist/templates/tick-controller/logic.ts.liquid +8 -0
- package/dist/templates/ui/index.html.liquid +12 -0
- package/dist/templates/ui/src/App.test.tsx.liquid +41 -0
- package/dist/templates/ui/src/App.tsx.liquid +103 -0
- package/dist/templates/ui/src/i18n.ts.liquid +47 -0
- package/dist/templates/ui/src/main.tsx.liquid +48 -0
- package/dist/templates/ui/src/theme.ts.liquid +46 -0
- package/dist/templates/validator/logic.test.ts.liquid +42 -0
- package/dist/templates/validator/logic.ts.liquid +21 -0
- package/dist/templates/waiting-room/index.html.liquid +12 -0
- package/dist/templates/waiting-room/src/App.test.tsx.liquid +57 -0
- package/dist/templates/waiting-room/src/App.tsx.liquid +118 -0
- package/dist/templates/waiting-room/src/i18n.ts.liquid +44 -0
- package/dist/templates/waiting-room/src/main.tsx.liquid +48 -0
- package/dist/templates/waiting-room/src/theme.ts.liquid +46 -0
- package/dist/templating/resource-metadata.d.ts +131 -0
- package/dist/types.d.ts +14 -0
- package/dist/utils/atomic-write.d.ts +40 -0
- package/dist/utils/cli-error.d.ts +64 -0
- package/dist/utils/document-parser-client.d.ts +20 -0
- package/dist/utils/env-loader.d.ts +40 -0
- package/dist/utils/error-codes.d.ts +339 -0
- package/dist/utils/errors.d.ts +19 -0
- package/dist/utils/global-settings.d.ts +65 -0
- package/dist/utils/llm-client.d.ts +34 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/output.d.ts +109 -0
- package/dist/utils/platform.d.ts +14 -0
- package/dist/utils/prompt.d.ts +36 -0
- package/dist/utils/real-path.d.ts +6 -0
- package/dist/utils/schema-properties.d.ts +35 -0
- package/dist/validation/ajv.d.ts +17 -0
- package/dist/validation/facilitator-guide-validator.d.ts +27 -0
- package/dist/validation/findings.d.ts +40 -0
- package/dist/validation/game-constraints.d.ts +10 -0
- package/dist/validation/game-validator.d.ts +34 -0
- package/dist/validation/package-validation.d.ts +41 -0
- package/dist/validation/package-validator.d.ts +19 -0
- package/dist/validation/resource-validator.d.ts +79 -0
- package/dist/validation/schema-defaults-validator.d.ts +29 -0
- package/dist/validation/schema-validation.d.ts +115 -0
- package/dist/validation/semver.d.ts +9 -0
- package/package.json +123 -3
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
# Tick controllers
|
|
2
|
+
|
|
3
|
+
A tick controller makes ticks happen on their own, on a schedule your logic decides, and can say when the game is finished. This guide explains the model, then is the reference for writing one.
|
|
4
|
+
|
|
5
|
+
## The model
|
|
6
|
+
|
|
7
|
+
By default, ticks are manual: a participant or facilitator presses a control, or a command is sent, to advance the game. That suits a game that moves one decision at a time. Some games need time to move on its own, or at a pace that depends on what is happening in the simulation. Some also need to end when a goal is met rather than after a fixed number of ticks.
|
|
8
|
+
|
|
9
|
+
A tick controller answers both. It is a function, `GetTickController`, that the runtime calls after each tick. It looks at the game state and returns how long to wait before the next automatic tick, and whether the game is now over.
|
|
10
|
+
|
|
11
|
+
- **Logic-driven.** The interval is code, not a static setting, so it can vary with game state, participant decisions, or anything else in the context it reads
|
|
12
|
+
- **Called after the fact.** The runtime asks after every tick (including initialisation, so the function always sees a fully initialised state), and again whenever a write changes a capability it reads. It never runs the engine itself
|
|
13
|
+
- **Manual ticks still work.** Automatic timing supplements the manual tick control rather than replacing it. A manual tick simply restarts the countdown from the next answer
|
|
14
|
+
- **Two ways to schedule.** Return an interval ('every 60 seconds') for a cadence, or an absolute time ('every day at midnight') for a wall-clock schedule
|
|
15
|
+
- **Paused is not complete.** Turning the timer off leaves a game that still accepts manual ticks. Declaring it complete ends it: no further ticks, manual or automatic, until it is reset or undone
|
|
16
|
+
|
|
17
|
+
In an independent game each team has its own timer; in a shared-state game one timer serves the cohort.
|
|
18
|
+
|
|
19
|
+
### When to use one
|
|
20
|
+
|
|
21
|
+
Use a tick controller when:
|
|
22
|
+
|
|
23
|
+
- Your game should advance in real time without manual intervention
|
|
24
|
+
- The pace of the simulation should vary based on game state
|
|
25
|
+
- You want to switch between automatic and manual modes during a game
|
|
26
|
+
- The game must end the moment a participant's write meets a goal, rather than at the next tick (see [Game completion](#game-completion))
|
|
27
|
+
|
|
28
|
+
You do not need one for a game where participants advance each tick themselves, and you do not need one just to end a game on a goal: the engine can do that alone.
|
|
29
|
+
|
|
30
|
+
## The result
|
|
31
|
+
|
|
32
|
+
`GetTickController` returns a [`TickControllerResult`](../api/sdk.md#tickcontrollerresult):
|
|
33
|
+
|
|
34
|
+
- **`enabled`**: `true` sets a timer, and when it fires the next tick executes and `GetTickController` is called again to schedule the one after. `false` schedules nothing, and the game behaves as if it had no tick controller: ticks are manual only
|
|
35
|
+
- **`intervalMs`**: milliseconds until the next automatic tick (`60_000` is one minute). Required when `enabled` is `true` and there is no `nextTickAt`; beside a `nextTickAt` it is only a display hint, and it may be left out when the timer is off
|
|
36
|
+
- **`nextTickAt`**: when set, the next tick is scheduled at that absolute time instead of after `intervalMs` (see [Scheduling at fixed times](#scheduling-at-fixed-times))
|
|
37
|
+
- **`complete`**: `true` permanently ends the game. No further ticks are allowed, manual or automatic, until the game is reset or undone (see [Game completion](#game-completion))
|
|
38
|
+
|
|
39
|
+
Every duration is in milliseconds and every timestamp is milliseconds since the epoch.
|
|
40
|
+
|
|
41
|
+
## Creating a tick controller
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm run sdk new tick_controller '{
|
|
45
|
+
"definition": {},
|
|
46
|
+
"logic": {
|
|
47
|
+
"directory": ".",
|
|
48
|
+
"entrypoint": "logic.ts"
|
|
49
|
+
},
|
|
50
|
+
"rootDirectory": "."
|
|
51
|
+
}'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This creates:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
tick_controller/
|
|
58
|
+
├── tick_controller.sdk.json # Definition
|
|
59
|
+
├── logic.ts # GetTickController function
|
|
60
|
+
└── types.ts # Auto-generated
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The bundle holds `reads`, `uses`, and `settings` (the settings resources it reads); `logic.ts` holds the `GetTickController` function.
|
|
64
|
+
|
|
65
|
+
### Wiring it to a game
|
|
66
|
+
|
|
67
|
+
Nothing to do: a package declares whatever is on disk, and a game uses whichever tick controller its package declares, so the directory `pnpm run sdk new tick_controller` created is the whole of the wiring. A package declares at most one.
|
|
68
|
+
|
|
69
|
+
## Writing the logic
|
|
70
|
+
|
|
71
|
+
The generated `tick_controller/logic.ts` exports a `GetTickController` function. Build interval-based results with `intervalTick(intervalMs)`, the sanctioned helper that validates the cadence for you. It takes **milliseconds**:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { GetTickControllerFunction, intervalTick } from '@seriousgaming/sdk';
|
|
75
|
+
import type { Context, Settings } from './types';
|
|
76
|
+
|
|
77
|
+
export const GetTickController: GetTickControllerFunction<Context, Settings> = async ({
|
|
78
|
+
context,
|
|
79
|
+
settings,
|
|
80
|
+
}) => {
|
|
81
|
+
return intervalTick(60_000); // fire next tick in 60 seconds
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Interval bounds
|
|
86
|
+
|
|
87
|
+
The effective interval (the gap until the next automatic tick) is bounded: a minimum of 30 seconds and a maximum of 7 days. `intervalTick()` rejects anything outside that range, as well as fractional, zero, negative, `NaN`, and `Infinity` values, with a message that names the limit and points you back at the helper. A value written in seconds fails loudly: `intervalTick(60)` is 60 milliseconds, and is refused with 'Effective tick cadence of 60 ms (under a second) is below the 30-second minimum (30000 ms)'. The same bounds apply to `scheduledTick()` (see [Scheduling at fixed times](#scheduling-at-fixed-times)), measured against the real gap to `nextTickAt`. If a game needs to tick less often than once a week, set `enabled: false` and drive ticks by another means.
|
|
88
|
+
|
|
89
|
+
### Cadence cookbook
|
|
90
|
+
|
|
91
|
+
Common cadences, ready to drop into `GetTickController`:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { intervalTick, scheduledTick, nextDailyAt, nextWeeklyAt } from '@seriousgaming/sdk';
|
|
95
|
+
|
|
96
|
+
// Every 30 seconds (the fastest allowed cadence)
|
|
97
|
+
return intervalTick(30_000);
|
|
98
|
+
|
|
99
|
+
// Every 5 minutes
|
|
100
|
+
return intervalTick(5 * 60_000);
|
|
101
|
+
|
|
102
|
+
// Every day at midnight UTC
|
|
103
|
+
return scheduledTick(nextDailyAt(0, 0, { now: meta.now }));
|
|
104
|
+
|
|
105
|
+
// Every Monday at 09:00 in New York
|
|
106
|
+
return scheduledTick(nextWeeklyAt(1, 9, 0, { timezone: 'America/New_York', now: meta.now }));
|
|
107
|
+
|
|
108
|
+
// Manual ticks only (no automatic timer)
|
|
109
|
+
return { enabled: false };
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Returning a dynamic interval
|
|
113
|
+
|
|
114
|
+
The function receives the capabilities it reads, so you can vary the interval based on state:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
export const GetTickController: GetTickControllerFunction<Context, Settings> = async ({
|
|
118
|
+
context,
|
|
119
|
+
settings,
|
|
120
|
+
}) => {
|
|
121
|
+
// Speed up as the game progresses, but never below the 30-second floor
|
|
122
|
+
const { CurrentTick } = context.progress;
|
|
123
|
+
const intervalMs = Math.max(
|
|
124
|
+
30_000,
|
|
125
|
+
settings.tick_controller.BaseIntervalMs - CurrentTick * 2_000
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return intervalTick(intervalMs);
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Disabling the timer conditionally
|
|
133
|
+
|
|
134
|
+
Return `enabled: false` to switch to manual-only mode:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
export const GetTickController: GetTickControllerFunction<Context, Settings> = async ({
|
|
138
|
+
context,
|
|
139
|
+
settings,
|
|
140
|
+
}) => {
|
|
141
|
+
const { WaitingForInput } = context.progress;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
intervalMs: settings.tick_controller.TickIntervalMs,
|
|
145
|
+
enabled: !WaitingForInput,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Stopping the clock while the waiting room is open
|
|
151
|
+
|
|
152
|
+
Opening the session's waiting room swaps every participant's game UI for the game's [waiting room UI](./waiting-room.md). It pauses nothing by itself (automatic ticks keep firing while participants wait), so if you want play genuinely halted, say so here:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
export const GetTickController: GetTickControllerFunction<Context, Settings> = async ({
|
|
156
|
+
settings,
|
|
157
|
+
waitingRoomOpen,
|
|
158
|
+
}) => {
|
|
159
|
+
return {
|
|
160
|
+
intervalMs: settings.tick_controller.TickIntervalMs,
|
|
161
|
+
enabled: !waitingRoomOpen,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A controller also receives `participants`, the participants of every team in `teams`, so 'tick once everyone present has submitted' can count who is present without a roster in context, and `meta.now`, the instant the evaluation's operation started.
|
|
167
|
+
|
|
168
|
+
`waitingRoomOpen` is on every `GetTickController` call, whether or not the game declares a waiting room UI. The decision sits here rather than in the runtime because only the author knows which kind of game this is: one where parking the session should stop the world, or one whose simulation should keep advancing in the background while participants are held. Without the check, a facilitator who parks a session for a ten-minute discussion returns to teams whose game has moved on without them.
|
|
169
|
+
|
|
170
|
+
Combine it with your other conditions the usual way: `enabled: !waitingRoomOpen && !WaitingForInput`.
|
|
171
|
+
|
|
172
|
+
## Reading capabilities
|
|
173
|
+
|
|
174
|
+
Tick controllers can declare capabilities they read, just like the engine. This enables type-safe access to context variables and triggers `types.ts` generation.
|
|
175
|
+
|
|
176
|
+
Reads do a second job here that they do not do for an engine: they decide when your controller runs again.
|
|
177
|
+
|
|
178
|
+
### Declaring reads
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pnpm run sdk new tick_controller '{
|
|
182
|
+
"definition": {
|
|
183
|
+
"reads": ["progress"]
|
|
184
|
+
},
|
|
185
|
+
"logic": {
|
|
186
|
+
"directory": ".",
|
|
187
|
+
"entrypoint": "logic.ts"
|
|
188
|
+
},
|
|
189
|
+
"rootDirectory": "."
|
|
190
|
+
}'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
After running `pnpm run generate`, a `types.ts` file is created:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// tick_controller/types.ts (auto-generated)
|
|
197
|
+
export interface Context {
|
|
198
|
+
progress: {
|
|
199
|
+
CurrentTick: number;
|
|
200
|
+
Phase: string;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export type Settings = Record<string, never>; // no settings declared
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Import this in `logic.ts` for full type safety:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import type { Context, Settings } from './types';
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Reads decide when you are called again
|
|
213
|
+
|
|
214
|
+
Your controller runs after every tick. It also runs whenever a context write changes one of the capabilities you declared you read.
|
|
215
|
+
|
|
216
|
+
That second rule is what makes conditions like 'tick as soon as every team has submitted' possible. Without it your controller would only ever see state as it stood after the previous tick, so a submission that should have triggered a tick would sit there until the next one came round anyway.
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"reads": ["decisions"]
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
export const GetTickController: GetTickControllerFunction<Context, Settings> = async ({
|
|
226
|
+
teams,
|
|
227
|
+
}) => {
|
|
228
|
+
// Called again the moment any team writes `decisions`, so this answer is
|
|
229
|
+
// never more than a write behind. `teams` holds every team in a
|
|
230
|
+
// shared-state game, and the one team otherwise.
|
|
231
|
+
const waitingOn = teams.filter((t) => !t.context.decisions.Submitted).map((t) => t.teamId);
|
|
232
|
+
return { intervalMs: 60_000, enabled: waitingOn.length > 0 };
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Declare no reads and you run on every write.** That is the right choice for a controller whose answer comes from somewhere other than context, such as one computing a deadline from the wall clock:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"reads": []
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**The trap to know about.** If you compute a deadline from the wall clock **and** declare reads, you are called only when those reads change, so a deadline you would have recomputed on an unrelated write is not recomputed. In practice this rarely matters: the host schedules from the last answer you gave, and fires an immediate catch-up tick if the target has already passed. If you want to be called on every write regardless, declare no reads.
|
|
245
|
+
|
|
246
|
+
**Why it is gated at all.** A re-run is an execution of your logic. Ungated, that is once per keystroke per participant, and in a large session most of those cannot change your answer: a controller that reads only a tick's outputs is unaffected by writes to that tick's decisions. Your `reads` declaration already says which is which, so nothing extra is needed from you.
|
|
247
|
+
|
|
248
|
+
A re-run only recomputes timing. The engine does not run, no snapshot is taken, and the tick counter does not move. Returning `complete: true` from one still ends the game.
|
|
249
|
+
|
|
250
|
+
A re-run that returns the same answer as before changes nothing: the countdown carries on, a facilitator's extension stands, and a paused timer keeps the time it had left. Only a different answer starts a new window, so a write that switches the timer on or changes `intervalMs` takes effect at once, while a busy team writing all round cannot push its own tick back.
|
|
251
|
+
|
|
252
|
+
## Adding settings
|
|
253
|
+
|
|
254
|
+
If your tick controller's interval or behaviour needs to be tunable per game, put the value in a settings resource and list it in the controller's `settings`:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
pnpm run sdk new settings '{
|
|
258
|
+
"definition": {
|
|
259
|
+
"identifier": "tick_controller",
|
|
260
|
+
"settingsSchema": {
|
|
261
|
+
"type": "object",
|
|
262
|
+
"properties": {
|
|
263
|
+
"TickIntervalMs": { "type": "integer", "default": 60000, "minimum": 30000 }
|
|
264
|
+
},
|
|
265
|
+
"additionalProperties": false
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}'
|
|
269
|
+
|
|
270
|
+
pnpm run sdk new tick_controller '{
|
|
271
|
+
"definition": { "settings": ["tick_controller"] },
|
|
272
|
+
"logic": {
|
|
273
|
+
"directory": ".",
|
|
274
|
+
"entrypoint": "logic.ts"
|
|
275
|
+
},
|
|
276
|
+
"rootDirectory": "."
|
|
277
|
+
}'
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Then use `settings.tick_controller.TickIntervalMs` in your logic. Naming the unit in the setting keeps a facilitator from typing seconds where milliseconds are meant. It resolves like any setting, under the settings identifier `tick_controller` (see [Overriding settings](./settings.md#overriding-settings)). Naming the settings resource after the slot is a convention, not a rule: a controller can read the engine's settings too, by listing them (`"settings": ["engine", "tick_controller"]`).
|
|
281
|
+
|
|
282
|
+
## Game completion
|
|
283
|
+
|
|
284
|
+
A game ends after its `maxTicks`, or earlier when something declares it complete. A game that ends because a condition is met (a revenue target reached, a resource depleted, a deadline passed) rather than after a fixed count is a goal-based game, and there are two ways to build one.
|
|
285
|
+
|
|
286
|
+
### From the engine
|
|
287
|
+
|
|
288
|
+
For most goal-based games the simpler route is the engine's own `complete`, returned from `Run`. It needs no tick controller at all:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
export const Run: RunFunction<Context, Settings> = async ({ context, settings }) => {
|
|
292
|
+
const next = { ...context /* ...this round's updates */ };
|
|
293
|
+
return {
|
|
294
|
+
capabilities: next,
|
|
295
|
+
complete: next.financials.Profit >= settings.goal.ProfitTarget,
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`complete: true` ends the game for the team this `Run` ran for. In an independent game each team finishes on its own; in a shared-state game one team's completion ends it for every team, because they share one clock. An aggregate engine returns `complete` under `shared` to end it for every team, or on a `teams` entry to end it for that team alone. `complete: false`, or leaving it out, carries on: completion cannot be withdrawn this way.
|
|
301
|
+
|
|
302
|
+
### From the tick controller
|
|
303
|
+
|
|
304
|
+
The engine's `complete` is read at a tick, so a goal reached by a participant's write between ticks ends the game at the next tick. Reach for the controller's `complete` when the game must end on the write itself: the runtime calls the tick controller again after a write that changes a capability it reads (see [Reads decide when you are called again](#reads-decide-when-you-are-called-again)), so it can end the game there and then.
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
export const GetTickController: GetTickControllerFunction<Context, Settings> = async ({
|
|
308
|
+
context,
|
|
309
|
+
settings,
|
|
310
|
+
}) => {
|
|
311
|
+
const profit = context.financials?.Profit ?? 0;
|
|
312
|
+
return { enabled: false, complete: profit >= settings.tick_controller.ProfitTarget };
|
|
313
|
+
};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
With the timer off, `intervalMs` can be left out. A controller that also times ticks passes `complete` to the helpers instead: `intervalTick(60_000, { complete })` and `scheduledTick(nextTickAt, { complete })`.
|
|
317
|
+
|
|
318
|
+
### Leaving out `maxTicks`
|
|
319
|
+
|
|
320
|
+
A goal-based game can omit `maxTicks` from `seriousgaming.sdk.json` entirely:
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{
|
|
324
|
+
"identifier": "com.example.goaldriven",
|
|
325
|
+
"supportedLocales": ["en"],
|
|
326
|
+
"locales": { "en": { "name": "Goal Driven" } }
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Or keep `maxTicks` as a safety ceiling: both apply, and whichever triggers first ends the game.
|
|
331
|
+
|
|
332
|
+
With neither `maxTicks` nor a tick controller, only the engine can end the game, by returning `complete: true` from `Run`. The build warns rather than failing, because it cannot see whether the engine ever does. A tick controller that exists only to report completion can be replaced by the engine's `complete`.
|
|
333
|
+
|
|
334
|
+
### When a game is complete
|
|
335
|
+
|
|
336
|
+
- The dev server rejects further tick requests, disables the dev drawer's **Tick** button, and reports the session as complete (`complete: true` on the state every client reads, including the output of `sdk tick` and `sdk view`)
|
|
337
|
+
- A further `sdk tick` fails with `TICK_FAILED` (exit code 4) and the message 'The game is complete', or 'Team is already complete' for one team of an independent game
|
|
338
|
+
- Undo and reset both clear the completion flag, so the game can tick again after either operation
|
|
339
|
+
|
|
340
|
+
### `enabled: false` or `complete: true`
|
|
341
|
+
|
|
342
|
+
| Behaviour | `enabled: false` | `complete: true` |
|
|
343
|
+
| ------------------ | ---------------- | ---------------------------- |
|
|
344
|
+
| Automatic timer | Disabled | Disabled |
|
|
345
|
+
| Manual ticks | Still allowed | Rejected |
|
|
346
|
+
| CLI ticks | Still allowed | Rejected (exit code 4) |
|
|
347
|
+
| After reset / undo | N/A | Cleared, game can tick again |
|
|
348
|
+
|
|
349
|
+
Use `enabled: false` when you want to pause automatic progression but still allow manual advancement. Use `complete: true` when the game is genuinely over.
|
|
350
|
+
|
|
351
|
+
### Multi-team completion
|
|
352
|
+
|
|
353
|
+
The tick controller always receives a `teams` argument, one `{ teamId, index, context }` entry per team the call covers, sorted by index: the same entry shape an aggregate engine's `teams` uses. In a shared-state game it holds every team, so you can check whether all teams (or any team) have reached the goal, and name the ones you are still waiting for by `teamId` or `index`. In an independent game each team has its own timer and its own controller call, so `teams` holds only that team. A single-team game gets one entry. Each `context` is projected onto the controller's declared `reads`, like its own `context`:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
export const GetTickController: GetTickControllerFunction<Context, Settings> = async ({
|
|
357
|
+
settings,
|
|
358
|
+
teams,
|
|
359
|
+
}) => {
|
|
360
|
+
// The game ends when every team hits the target
|
|
361
|
+
const allDone = teams.every(
|
|
362
|
+
(t) => (t.context.financials?.Profit ?? 0) >= settings.tick_controller.ProfitTarget
|
|
363
|
+
);
|
|
364
|
+
return { enabled: false, complete: allDone };
|
|
365
|
+
};
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
A game that ends this way can leave `maxTicks` out of `seriousgaming.sdk.json` altogether, so the controller is the only thing that completes it.
|
|
369
|
+
|
|
370
|
+
## How timing works
|
|
371
|
+
|
|
372
|
+
`GetTickController` is called **after each tick completes** (including the initialisation tick), and again after a write that changes a capability it reads. This means:
|
|
373
|
+
|
|
374
|
+
1. The initial game state is fully set up before any timing decisions are made
|
|
375
|
+
2. Every tick updates context before the next interval is calculated
|
|
376
|
+
3. Manual ticks reset the countdown: returning `enabled: true` does not disable manual ticks, and after each one the timer restarts from the new answer
|
|
377
|
+
|
|
378
|
+
If a timer fires while a tick is still executing, the timer firing is dropped. The timer reschedules itself once the running tick completes, so ticks never stack up.
|
|
379
|
+
|
|
380
|
+
Each call has a time limit, like an engine call (see [Time limits](./engine.md#time-limits)).
|
|
381
|
+
|
|
382
|
+
### Results outside the bounds
|
|
383
|
+
|
|
384
|
+
A result the helpers would refuse, such as a hand-written `intervalMs: 10_000`, is not honoured as it stands. The dev server clamps it to the nearest bound, 30 seconds or 7 days, and reports the clamp as an error so you see it, which is what a production runtime does too. Build your results with `intervalTick()` and `scheduledTick()` to catch this while you write the controller rather than while it runs.
|
|
385
|
+
|
|
386
|
+
A result holds only `enabled`, `intervalMs`, `nextTickAt` and `complete`, and `enabled` and `complete` are booleans. A result with any other key, or with `complete: 'no'`, is not clamped: the evaluation fails, the failure is reported naming the key, and the timer stops, as it does when the controller throws.
|
|
387
|
+
|
|
388
|
+
## Pausing, resuming and extending
|
|
389
|
+
|
|
390
|
+
A facilitator controls the timer from the session UI. Under `sdk dev` you can also drive it from the dev drawer's Session tab, or from the command line with [`sdk timer`](../cli-reference.md#the-tick-timer):
|
|
391
|
+
|
|
392
|
+
| Control | Effect |
|
|
393
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
394
|
+
| `pauseTimer` | Stops the automatic timer. Manual ticks still work |
|
|
395
|
+
| `resumeTimer` | Restarts it. An interval schedule continues from the time that was left; a `nextTickAt` schedule asks `GetTickController` again, so a target that passed during the pause is not fired |
|
|
396
|
+
| `extendTick` | Moves the scheduled tick by a number of milliseconds, forwards or back, for this tick only. The interval your controller returns for later ticks is untouched |
|
|
397
|
+
|
|
398
|
+
Each takes an optional team. In an independent game every team has its own timer, and leaving the team out applies the control to all of them.
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
pnpm run sdk timer # what the timer is doing
|
|
402
|
+
pnpm run sdk timer pause
|
|
403
|
+
pnpm run sdk timer extend 120 # two more minutes (the CLI takes seconds)
|
|
404
|
+
pnpm run sdk timer resume --team 2 # one team, in an independent game
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
The current schedule, a [`TimerState`](../api/sdk-ui.md#timerstate) with every duration and timestamp in milliseconds, is pushed to every game UI. UIs subscribe with `useUserInterfaceTimer` and render their own countdown or progress bar. See [Displaying the tick timer](./ui.md#displaying-the-tick-timer) in the user interfaces guide.
|
|
408
|
+
|
|
409
|
+
## Testing without waiting
|
|
410
|
+
|
|
411
|
+
A one-minute timer takes a minute to test, and the 30-second floor means none takes less. Start the dev server on a fake clock instead, and move time yourself:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
pnpm run sdk dev --fake-clock
|
|
415
|
+
pnpm run sdk clock advance 180 # three minutes: a one-minute timer ticks three times
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Time stands still until you move it, and `clock advance` runs every automatic tick that falls due on the way, so a test says how much time passes rather than sleeping for it. A paused timer does not tick however far you move the clock, and time only moves forward. From a Playwright test, post `{ "type": "advanceClock", "byMs": 180000 }` to `/api/command`.
|
|
419
|
+
|
|
420
|
+
Your UIs count down against the browser's own clock, not the session's, so after an advance a countdown reads long by however far you moved it. The ticks, the tick count and `nextTickAt` are all the session's, and are right.
|
|
421
|
+
|
|
422
|
+
## Scheduling at fixed times
|
|
423
|
+
|
|
424
|
+
Instead of ticking on an interval ('every 60 seconds'), you can schedule ticks at specific wall-clock times ('every day at midnight'). Return `nextTickAt`, an absolute timestamp in milliseconds since epoch, and the runtime fires the tick at that exact time.
|
|
425
|
+
|
|
426
|
+
There are two timing modes:
|
|
427
|
+
|
|
428
|
+
- **Interval-based**: return an `intervalMs` in milliseconds. The next tick fires that long after the current tick completes. Good for simulations that run on a cadence
|
|
429
|
+
- **Scheduled**: return a `nextTickAt` timestamp. The next tick fires at that exact wall-clock time. Good for games that tick at fixed times of day
|
|
430
|
+
|
|
431
|
+
When `nextTickAt` is set, it takes precedence over `intervalMs`, which is then optional and only a display hint for UIs.
|
|
432
|
+
|
|
433
|
+
The SDK provides helpers that compute the next occurrence of a daily or weekly time, with optional timezone support:
|
|
434
|
+
|
|
435
|
+
### Daily schedule
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
import { GetTickControllerFunction, nextDailyAt, scheduledTick } from '@seriousgaming/sdk';
|
|
439
|
+
import type { Context, Settings } from './types';
|
|
440
|
+
|
|
441
|
+
export const GetTickController: GetTickControllerFunction<Context, Settings> = async ({
|
|
442
|
+
settings,
|
|
443
|
+
meta,
|
|
444
|
+
}) => {
|
|
445
|
+
// Tick once per day at the configured time
|
|
446
|
+
return scheduledTick(
|
|
447
|
+
nextDailyAt(settings.tick_controller.TickHour, settings.tick_controller.TickMinute, {
|
|
448
|
+
timezone: settings.tick_controller.Timezone,
|
|
449
|
+
now: meta.now,
|
|
450
|
+
})
|
|
451
|
+
);
|
|
452
|
+
};
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Multiple times per day
|
|
456
|
+
|
|
457
|
+
Use `nextDailyFromSchedule` to pick the soonest time from a list:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
import { nextDailyFromSchedule, scheduledTick } from '@seriousgaming/sdk';
|
|
461
|
+
|
|
462
|
+
// Tick at 09:00 and 17:00 UTC every day
|
|
463
|
+
return scheduledTick(
|
|
464
|
+
nextDailyFromSchedule(
|
|
465
|
+
[
|
|
466
|
+
{ hours: 9, minutes: 0 },
|
|
467
|
+
{ hours: 17, minutes: 0 },
|
|
468
|
+
],
|
|
469
|
+
{ now: meta.now }
|
|
470
|
+
)
|
|
471
|
+
);
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Weekly schedule
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
import { nextWeeklyAt, scheduledTick } from '@seriousgaming/sdk';
|
|
478
|
+
|
|
479
|
+
// Tick every Monday at 09:00 in New York
|
|
480
|
+
return scheduledTick(nextWeeklyAt(1, 9, 0, { timezone: 'America/New_York', now: meta.now }));
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Timezone support
|
|
484
|
+
|
|
485
|
+
Pass `now: meta.now` to every schedule helper, so the next tick is computed from the operation's time rather than the host clock. All schedule helpers also accept an optional `timezone` parameter using IANA timezone names. DST transitions are handled automatically: the helpers always return the correct UTC timestamp for the next occurrence of the target wall-clock time.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// Midnight in Sydney: resolves to 14:00 UTC (AEST) or 13:00 UTC (AEDT)
|
|
489
|
+
nextDailyAt(0, 0, { timezone: 'Australia/Sydney', now: meta.now });
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Put the timezone in the tick controller's settings to make it configurable per game, so facilitators can set the schedule to match their participants' timezone.
|
|
493
|
+
|
|
494
|
+
### How `scheduledTick` works
|
|
495
|
+
|
|
496
|
+
`scheduledTick(nextTickAt)` builds a complete `TickControllerResult`:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
{
|
|
500
|
+
enabled: true,
|
|
501
|
+
intervalMs: /* computed milliseconds until nextTickAt */,
|
|
502
|
+
nextTickAt: /* the timestamp you passed in */,
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
The `intervalMs` field is computed for you, so a UI drawing a progress bar has the window's full length to draw against.
|
|
507
|
+
|
|
508
|
+
`scheduledTick()` validates the effective cadence (the real gap to `nextTickAt`) against the same 30-second floor and 7-day ceiling as `intervalTick()`. A schedule that would tick sooner than 30 seconds out, or further than 7 days out, is rejected with a clear message rather than honoured. A `nextTickAt` already in the past is exempt: it fires one immediate catch-up tick (see [Missed ticks](#missed-ticks)).
|
|
509
|
+
|
|
510
|
+
### Missed ticks
|
|
511
|
+
|
|
512
|
+
If the server was down when a scheduled tick should have fired, the runtime detects that `nextTickAt` is in the past and fires one tick immediately. The tick controller then re-evaluates and returns the next future target time.
|
|
513
|
+
|
|
514
|
+
### Pause and resume
|
|
515
|
+
|
|
516
|
+
Pause/resume behaves differently for scheduled ticks than for interval-based ticks:
|
|
517
|
+
|
|
518
|
+
| Scenario | Interval mode | Scheduled mode (`nextTickAt`) |
|
|
519
|
+
| ---------------------------------- | ----------------------------- | --------------------------------------- |
|
|
520
|
+
| Resume after short pause | Continues from remaining time | Re-evaluates against current wall clock |
|
|
521
|
+
| Resume after scheduled time passed | N/A | Skips to next future occurrence |
|
|
522
|
+
| Server was down at scheduled time | N/A | Fires immediately on restart |
|
|
523
|
+
|
|
524
|
+
When a facilitator pauses a scheduled game and resumes after the scheduled time has passed, the missed tick is **not** fired. The tick controller re-evaluates and returns the next future occurrence. This is intentional: pausing is an explicit 'stop the game' action.
|
|
525
|
+
|
|
526
|
+
This is different from a server outage, where `nextTickAt` is in the past and the runtime fires one tick immediately to catch up.
|
|
527
|
+
|
|
528
|
+
## Updating a tick controller
|
|
529
|
+
|
|
530
|
+
A game holds one tick controller, in the `tick_controller/` slot, and the slot is its identifier. There is nothing to name and nothing to rename, so `update` takes only the change:
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Patch one field
|
|
534
|
+
pnpm run sdk update tick_controller --patch '[
|
|
535
|
+
{ "op": "replace", "path": "/definition/reads", "value": ["progress"] }
|
|
536
|
+
]'
|
|
537
|
+
|
|
538
|
+
# Replace the whole definition
|
|
539
|
+
pnpm run sdk update tick_controller '{
|
|
540
|
+
"definition": { "reads": ["progress"] },
|
|
541
|
+
"logic": { "directory": ".", "entrypoint": "logic.ts" },
|
|
542
|
+
"rootDirectory": "."
|
|
543
|
+
}'
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
There is no interactive mode: with nothing to name and nothing to pick, the
|
|
547
|
+
command prints these two invocations instead of prompting.
|
|
548
|
+
|
|
549
|
+
## Deleting a tick controller
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
pnpm run sdk delete tick_controller
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
This empties the `tick_controller/` slot. A package declares whatever is on disk, so removing the directory is the whole of the removal: there is no reference anywhere to clear.
|
|
556
|
+
|
|
557
|
+
## Best practices
|
|
558
|
+
|
|
559
|
+
1. **Start with a fixed interval.** Get the game working first, then add dynamic timing
|
|
560
|
+
2. **Use `enabled: false` over a very long interval.** It is more explicit, and it disables UI timer indicators
|
|
561
|
+
3. **Keep logic simple.** `GetTickController` runs frequently, and each call has a time limit (see [Time limits](./engine.md#time-limits)); avoid expensive operations
|
|
562
|
+
4. **Use settings for the interval.** They make the interval easy to tune per game without changing code
|
|
563
|
+
|
|
564
|
+
## Next steps
|
|
565
|
+
|
|
566
|
+
- [End a game when a condition is met](../recipes/end-game-on-condition.md): the goal-based game, step by step
|
|
567
|
+
- [Shaping games](./games.md): what a game declares, including `maxTicks`
|
|
568
|
+
- [Type safety](./type-safety.md): understanding generated `types.ts` files
|
|
569
|
+
- [Create a tick controller](../recipes/create-tick-controller.md): quick step-by-step recipe
|