@dreamboard-games/sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +96 -0
- package/README.md +12 -0
- package/dist/HandView-ncJIVLhN.d.ts +193 -0
- package/dist/ResourceCounter-CTREyF73.d.ts +102 -0
- package/dist/ThemeProvider-fy0_QzgO.d.ts +99 -0
- package/dist/bundle-TIZcw8LB.d.ts +281 -0
- package/dist/cards-Sl3b40Mv.d.ts +13 -0
- package/dist/chunk-7YAHLYBR.js +481 -0
- package/dist/chunk-7YAHLYBR.js.map +1 -0
- package/dist/chunk-FDNZTDD6.js +8085 -0
- package/dist/chunk-FDNZTDD6.js.map +1 -0
- package/dist/chunk-GKKBPPSW.js +598 -0
- package/dist/chunk-GKKBPPSW.js.map +1 -0
- package/dist/chunk-I46YJSOD.js +1 -0
- package/dist/chunk-I46YJSOD.js.map +1 -0
- package/dist/chunk-KAELH4KC.js +104 -0
- package/dist/chunk-KAELH4KC.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-T3ZKNUZ7.js +1 -0
- package/dist/chunk-T3ZKNUZ7.js.map +1 -0
- package/dist/chunk-T52J5RMF.js +1 -0
- package/dist/chunk-T52J5RMF.js.map +1 -0
- package/dist/chunk-TDSWKVZ4.js +5401 -0
- package/dist/chunk-TDSWKVZ4.js.map +1 -0
- package/dist/chunk-U5C6BONG.js +34 -0
- package/dist/chunk-U5C6BONG.js.map +1 -0
- package/dist/chunk-VDXOF4FW.js +69 -0
- package/dist/chunk-VDXOF4FW.js.map +1 -0
- package/dist/chunk-VFTAA4WO.js +115 -0
- package/dist/chunk-VFTAA4WO.js.map +1 -0
- package/dist/chunk-WN74KVNY.js +17 -0
- package/dist/chunk-WN74KVNY.js.map +1 -0
- package/dist/chunk-WYPQ3GG5.js +10990 -0
- package/dist/chunk-WYPQ3GG5.js.map +1 -0
- package/dist/components-D5ZRE2Hl.d.ts +1451 -0
- package/dist/generated/runtime/primitives.d.ts +12 -0
- package/dist/generated/runtime/primitives.js +180 -0
- package/dist/generated/runtime/primitives.js.map +1 -0
- package/dist/generated/runtime-api.d.ts +3 -0
- package/dist/generated/runtime-api.js +2 -0
- package/dist/generated/runtime-api.js.map +1 -0
- package/dist/generated/runtime.d.ts +14 -0
- package/dist/generated/runtime.js +18 -0
- package/dist/generated/runtime.js.map +1 -0
- package/dist/generated/workspace-contract.d.ts +14 -0
- package/dist/generated/workspace-contract.js +14 -0
- package/dist/generated/workspace-contract.js.map +1 -0
- package/dist/hex-board-view-D_07hO6O.d.ts +933 -0
- package/dist/hex-color-MhOyuY-o.d.ts +8 -0
- package/dist/index-BwqPQtBu.d.ts +1433 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/reducer-bundle-abi.d.ts +1083 -0
- package/dist/infrastructure/reducer-bundle-abi.js +14 -0
- package/dist/infrastructure/reducer-bundle-abi.js.map +1 -0
- package/dist/infrastructure/workspace-codegen.d.ts +53 -0
- package/dist/infrastructure/workspace-codegen.js +44 -0
- package/dist/infrastructure/workspace-codegen.js.map +1 -0
- package/dist/manifest-contract-BNHVGFtU.d.ts +9 -0
- package/dist/package-set.d.ts +13 -0
- package/dist/package-set.js +12 -0
- package/dist/package-set.js.map +1 -0
- package/dist/primitive-props-DpKs-GCr.d.ts +11 -0
- package/dist/reducer.d.ts +3786 -0
- package/dist/reducer.js +8131 -0
- package/dist/reducer.js.map +1 -0
- package/dist/runtime/primitives.d.ts +226 -0
- package/dist/runtime/primitives.js +180 -0
- package/dist/runtime/primitives.js.map +1 -0
- package/dist/runtime/types/runtime-api.d.ts +1 -0
- package/dist/runtime/types/runtime-api.js +2 -0
- package/dist/runtime/types/runtime-api.js.map +1 -0
- package/dist/runtime/workspace-contract.d.ts +172 -0
- package/dist/runtime/workspace-contract.js +14 -0
- package/dist/runtime/workspace-contract.js.map +1 -0
- package/dist/runtime-api-3dshj6kK.d.ts +101 -0
- package/dist/runtime-api-DWxvTr-O.d.ts +379 -0
- package/dist/runtime.d.ts +58 -0
- package/dist/runtime.js +13 -0
- package/dist/runtime.js.map +1 -0
- package/dist/slots-1GPGihk8.d.ts +8 -0
- package/dist/testing.d.ts +149 -0
- package/dist/testing.js +513 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +496 -0
- package/dist/types.js +28 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/components.d.ts +16 -0
- package/dist/ui/components.js +192 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/defaults.d.ts +19 -0
- package/dist/ui/defaults.js +104 -0
- package/dist/ui/defaults.js.map +1 -0
- package/dist/ui/plugin-styles.css +250 -0
- package/dist/ui/types/player-state.d.ts +365 -0
- package/dist/ui/types/player-state.js +1 -0
- package/dist/ui/types/player-state.js.map +1 -0
- package/dist/ui-contract-iQfTtUSL.d.ts +1161 -0
- package/dist/ui.d.ts +320 -0
- package/dist/ui.js +253 -0
- package/dist/ui.js.map +1 -0
- package/package.json +199 -0
- package/src/generated/reducer-contract/builders.ts +90 -0
- package/src/generated/reducer-contract/version.ts +9 -0
- package/src/generated/reducer-contract/wire.ts +100 -0
- package/src/generated/reducer-contract/zod.ts +101 -0
- package/src/generated/runtime/primitives.ts +2 -0
- package/src/generated/runtime-api.ts +5 -0
- package/src/generated/runtime.ts +35 -0
- package/src/generated/workspace-contract.ts +2 -0
- package/src/index.ts +7 -0
- package/src/infrastructure/reducer-bundle-abi.ts +8 -0
- package/src/infrastructure/reducer-contract/bundle.ts +37 -0
- package/src/infrastructure/workspace-codegen/hex-geometry.ts +69 -0
- package/src/infrastructure/workspace-codegen/index.ts +64 -0
- package/src/infrastructure/workspace-codegen/manifest-contract.ts +6632 -0
- package/src/infrastructure/workspace-codegen/manifest-validation.ts +795 -0
- package/src/infrastructure/workspace-codegen/ownership.ts +131 -0
- package/src/infrastructure/workspace-codegen/preset-card-sets.ts +169 -0
- package/src/infrastructure/workspace-codegen/seeds.ts +1705 -0
- package/src/infrastructure/workspace-codegen.ts +1 -0
- package/src/package-set.ts +19 -0
- package/src/reducer/authoring/contract.ts +157 -0
- package/src/reducer/authoring/effect.ts +224 -0
- package/src/reducer/authoring/game.ts +23 -0
- package/src/reducer/authoring/interaction.ts +98 -0
- package/src/reducer/authoring/phase.ts +300 -0
- package/src/reducer/authoring/types.ts +70 -0
- package/src/reducer/authoring/validation.ts +382 -0
- package/src/reducer/authoring/view-stage.ts +68 -0
- package/src/reducer/authoring.ts +29 -0
- package/src/reducer/bundle/ingress-bundle.ts +491 -0
- package/src/reducer/bundle/trusted/engine-instruction-resolver.ts +254 -0
- package/src/reducer/bundle/trusted/flow-instruction-resolver.ts +73 -0
- package/src/reducer/bundle/trusted/instruction-runner.ts +414 -0
- package/src/reducer/bundle/trusted/interaction-authorization.ts +137 -0
- package/src/reducer/bundle/trusted/interaction-collectors.ts +859 -0
- package/src/reducer/bundle/trusted/interaction-decision.ts +747 -0
- package/src/reducer/bundle/trusted/interaction-resolver.ts +95 -0
- package/src/reducer/bundle/trusted/interaction-types.ts +171 -0
- package/src/reducer/bundle/trusted/lifecycle-runner.ts +427 -0
- package/src/reducer/bundle/trusted/projection-builder.ts +356 -0
- package/src/reducer/bundle/trusted/projection-context.ts +39 -0
- package/src/reducer/bundle/trusted/rng-sampler.ts +150 -0
- package/src/reducer/bundle/trusted/runtime-registry.ts +120 -0
- package/src/reducer/bundle/trusted/runtime-scope.ts +336 -0
- package/src/reducer/bundle/trusted/simultaneous-player.ts +97 -0
- package/src/reducer/bundle/trusted/stage-resolver.ts +87 -0
- package/src/reducer/bundle/trusted/static-projection.ts +116 -0
- package/src/reducer/bundle/trusted/trusted-runtime-args.ts +97 -0
- package/src/reducer/bundle/trusted/trusted-runtime-result.ts +39 -0
- package/src/reducer/bundle/trusted/trusted-setup-profiles.ts +43 -0
- package/src/reducer/bundle/trusted/trusted-state-codec.ts +48 -0
- package/src/reducer/bundle/trusted-bundle.ts +97 -0
- package/src/reducer/bundle/types.ts +171 -0
- package/src/reducer/bundle.ts +2 -0
- package/src/reducer/client-param-schemas.ts +57 -0
- package/src/reducer/compose.ts +34 -0
- package/src/reducer/core/runtime-input.ts +30 -0
- package/src/reducer/core/runtime-instruction.ts +59 -0
- package/src/reducer/core/types.ts +62 -0
- package/src/reducer/definition-index.ts +277 -0
- package/src/reducer/derived.ts +106 -0
- package/src/reducer/effects.ts +92 -0
- package/src/reducer/engine/runtime-instruction-engine.ts +155 -0
- package/src/reducer/ingress/decode-runtime-input.ts +7 -0
- package/src/reducer/ingress/decode-session-state.ts +9 -0
- package/src/reducer/ingress/encode-session-state.ts +6 -0
- package/src/reducer/ingress/input-codec.ts +18 -0
- package/src/reducer/ingress/phase-schemas.ts +62 -0
- package/src/reducer/ingress/raw-types.ts +107 -0
- package/src/reducer/ingress/runtime-codec.ts +14 -0
- package/src/reducer/ingress/runtime-payload.ts +13 -0
- package/src/reducer/ingress/session-codec.ts +392 -0
- package/src/reducer/ingress/types.ts +6 -0
- package/src/reducer/inputs/boardInput.ts +217 -0
- package/src/reducer/inputs/boardTarget.ts +190 -0
- package/src/reducer/inputs/cardInput.ts +86 -0
- package/src/reducer/inputs/cardTarget.ts +101 -0
- package/src/reducer/inputs/choiceTarget.ts +104 -0
- package/src/reducer/inputs/defineInputs.ts +71 -0
- package/src/reducer/inputs/formInput.ts +809 -0
- package/src/reducer/inputs/many.ts +120 -0
- package/src/reducer/inputs/promptInput.ts +87 -0
- package/src/reducer/inputs/rngInput.ts +58 -0
- package/src/reducer/inputs/targetRule.ts +123 -0
- package/src/reducer/inputs.ts +41 -0
- package/src/reducer/model/definition.ts +1072 -0
- package/src/reducer/model/extract.ts +745 -0
- package/src/reducer/model/manifest.ts +570 -0
- package/src/reducer/model/queries.ts +641 -0
- package/src/reducer/model/runtime.ts +264 -0
- package/src/reducer/model/spec.ts +1386 -0
- package/src/reducer/model/table.ts +260 -0
- package/src/reducer/model.ts +7 -0
- package/src/reducer/ops.ts +1034 -0
- package/src/reducer/parse-utils.ts +28 -0
- package/src/reducer/per-player.ts +422 -0
- package/src/reducer/rng.ts +69 -0
- package/src/reducer/schema-helpers.ts +185 -0
- package/src/reducer/setup-bootstrap-helpers.ts +171 -0
- package/src/reducer/setup-bootstrap.ts +481 -0
- package/src/reducer/table-ops.ts +2671 -0
- package/src/reducer/table-queries.ts +372 -0
- package/src/reducer/transaction.ts +120 -0
- package/src/reducer.ts +314 -0
- package/src/runtime/primitives.ts +1 -0
- package/src/runtime/types/runtime-api.ts +1 -0
- package/src/runtime/workspace-contract.ts +32 -0
- package/src/runtime-internal/components/InteractionForm.tsx +1309 -0
- package/src/runtime-internal/components/PluginRuntime.tsx +103 -0
- package/src/runtime-internal/components/board/target-layer.ts +70 -0
- package/src/runtime-internal/context/ClientParamSchemaContext.tsx +44 -0
- package/src/runtime-internal/context/InteractionDraftContext.tsx +279 -0
- package/src/runtime-internal/context/PluginSessionContext.tsx +47 -0
- package/src/runtime-internal/context/PluginStateContext.tsx +262 -0
- package/src/runtime-internal/context/RuntimeContext.tsx +96 -0
- package/src/runtime-internal/defaults/components.tsx +409 -0
- package/src/runtime-internal/defaults/index.ts +11 -0
- package/src/runtime-internal/errors/ValidationError.ts +29 -0
- package/src/runtime-internal/hooks/useActivePlayers.ts +33 -0
- package/src/runtime-internal/hooks/useBoardInteractions.ts +665 -0
- package/src/runtime-internal/hooks/useGameSelector.ts +105 -0
- package/src/runtime-internal/hooks/useGameView.ts +9 -0
- package/src/runtime-internal/hooks/useInteractionByKey.ts +354 -0
- package/src/runtime-internal/hooks/useInteractionHandle.ts +438 -0
- package/src/runtime-internal/hooks/useIsMyTurn.ts +20 -0
- package/src/runtime-internal/hooks/useLobby.ts +76 -0
- package/src/runtime-internal/hooks/useMe.ts +48 -0
- package/src/runtime-internal/hooks/usePlayerInfo.ts +28 -0
- package/src/runtime-internal/hooks/usePlayerTurnOrder.ts +23 -0
- package/src/runtime-internal/hooks/usePluginRuntime.ts +147 -0
- package/src/runtime-internal/hooks/useSeatInbox.ts +61 -0
- package/src/runtime-internal/hooks/useSimultaneousPhase.ts +10 -0
- package/src/runtime-internal/index.ts +42 -0
- package/src/runtime-internal/internal.ts +43 -0
- package/src/runtime-internal/plugin-styles.css +250 -0
- package/src/runtime-internal/primitives/board.tsx +459 -0
- package/src/runtime-internal/primitives/dialog-lifecycle.ts +58 -0
- package/src/runtime-internal/primitives/dice.tsx +79 -0
- package/src/runtime-internal/primitives/game-ui-provider.tsx +35 -0
- package/src/runtime-internal/primitives/game.tsx +387 -0
- package/src/runtime-internal/primitives/hand-intent-adapter.ts +147 -0
- package/src/runtime-internal/primitives/hand-surface.tsx +594 -0
- package/src/runtime-internal/primitives/index.ts +196 -0
- package/src/runtime-internal/primitives/interaction-form-binding.tsx +56 -0
- package/src/runtime-internal/primitives/interaction-submit.ts +90 -0
- package/src/runtime-internal/primitives/interaction.tsx +987 -0
- package/src/runtime-internal/primitives/phase.tsx +43 -0
- package/src/runtime-internal/primitives/player-roster.tsx +302 -0
- package/src/runtime-internal/primitives/primitive-props.tsx +101 -0
- package/src/runtime-internal/primitives/prompt.tsx +255 -0
- package/src/runtime-internal/primitives/ui.tsx +60 -0
- package/src/runtime-internal/primitives/zone.tsx +791 -0
- package/src/runtime-internal/reducer.ts +30 -0
- package/src/runtime-internal/runtime/createPluginRuntimeAPI.ts +605 -0
- package/src/runtime-internal/types/plugin-state.ts +508 -0
- package/src/runtime-internal/types/reducer-state.ts +24 -0
- package/src/runtime-internal/types/runtime-api.ts +114 -0
- package/src/runtime-internal/ui-contract.ts +519 -0
- package/src/runtime-internal/utils/card-intent-adapter.ts +546 -0
- package/src/runtime-internal/utils/interaction-inputs.ts +492 -0
- package/src/runtime-internal/utils/interaction-labels.ts +23 -0
- package/src/runtime-internal/utils/interaction-router.ts +273 -0
- package/src/runtime-internal/utils/interaction-status.ts +74 -0
- package/src/runtime-internal/workspace-contract.ts +1170 -0
- package/src/runtime.ts +34 -0
- package/src/testing/create-expect-api.ts +352 -0
- package/src/testing/create-test-runtime.ts +381 -0
- package/src/testing/definitions.ts +127 -0
- package/src/testing/index.ts +3 -0
- package/src/testing.ts +1 -0
- package/src/type-stubs/manifest-contract.d.ts +42 -0
- package/src/type-stubs/manifest-contract.js +72 -0
- package/src/type-stubs/ui-contract.d.ts +5 -0
- package/src/type-stubs/ui-contract.js +1 -0
- package/src/types/authoring-card-properties.type-test.ts +266 -0
- package/src/types/authoring.ts +1282 -0
- package/src/types/cards.ts +19 -0
- package/src/types/contracts.ts +1550 -0
- package/src/types/generated-helpers.ts +35 -0
- package/src/types/index.ts +147 -0
- package/src/types/slots.ts +11 -0
- package/src/types.ts +1 -0
- package/src/ui/components/ActionButton.tsx +97 -0
- package/src/ui/components/ActionPanel.tsx +315 -0
- package/src/ui/components/Card.tsx +378 -0
- package/src/ui/components/CardDragSurface.tsx +1076 -0
- package/src/ui/components/ChromeSuppressionContext.tsx +70 -0
- package/src/ui/components/CostDisplay.tsx +145 -0
- package/src/ui/components/DiceRoller.tsx +581 -0
- package/src/ui/components/Drawer.tsx +180 -0
- package/src/ui/components/ErrorBoundary.tsx +275 -0
- package/src/ui/components/GameEndDisplay.tsx +398 -0
- package/src/ui/components/GameSkeleton.tsx +260 -0
- package/src/ui/components/Hand.tsx +468 -0
- package/src/ui/components/HandDock.tsx +299 -0
- package/src/ui/components/HandView.tsx +441 -0
- package/src/ui/components/MobileHandTray.tsx +381 -0
- package/src/ui/components/MoreActions.tsx +143 -0
- package/src/ui/components/PhaseIndicator.tsx +341 -0
- package/src/ui/components/PlayArea.tsx +146 -0
- package/src/ui/components/PrimaryActionButton.tsx +336 -0
- package/src/ui/components/PrimaryButton.tsx +45 -0
- package/src/ui/components/ResourceCounter.tsx +270 -0
- package/src/ui/components/StagingZone.tsx +134 -0
- package/src/ui/components/ThemedButton.tsx +113 -0
- package/src/ui/components/Toast.tsx +264 -0
- package/src/ui/components/board/HexGrid.tsx +1294 -0
- package/src/ui/components/board/NetworkGraph.tsx +476 -0
- package/src/ui/components/board/SlotSystem.tsx +388 -0
- package/src/ui/components/board/SquareGrid.tsx +1165 -0
- package/src/ui/components/board/TrackBoard.tsx +496 -0
- package/src/ui/components/board/ZoneMap.tsx +448 -0
- package/src/ui/components/board/hex-board-view.ts +123 -0
- package/src/ui/components/board/index.ts +142 -0
- package/src/ui/components/board/interaction-accessibility.ts +21 -0
- package/src/ui/components/board/target-layer.ts +66 -0
- package/src/ui/components/card-render-content.type-test.ts +27 -0
- package/src/ui/components/hand-layout-math.ts +163 -0
- package/src/ui/components/hand-pointer-engine.ts +413 -0
- package/src/ui/components/index.ts +245 -0
- package/src/ui/components.ts +1 -0
- package/src/ui/defaults/components.tsx +106 -0
- package/src/ui/defaults/index.ts +8 -0
- package/src/ui/defaults.ts +1 -0
- package/src/ui/errors/ValidationError.ts +29 -0
- package/src/ui/helpers/cards.ts +19 -0
- package/src/ui/helpers/track-board.ts +211 -0
- package/src/ui/hooks/useBoardTopology.ts +316 -0
- package/src/ui/hooks/useCards.ts +10 -0
- package/src/ui/hooks/useHandCardPointer.ts +381 -0
- package/src/ui/hooks/useHandLayout.ts +378 -0
- package/src/ui/hooks/useHandPresentation.ts +121 -0
- package/src/ui/hooks/useHexBoard.ts +74 -0
- package/src/ui/hooks/useHexGrid.ts +185 -0
- package/src/ui/hooks/useIsMobile.ts +35 -0
- package/src/ui/hooks/usePanZoom.ts +278 -0
- package/src/ui/hooks/useSquareBoard.ts +124 -0
- package/src/ui/hooks/useSquareGrid.ts +328 -0
- package/src/ui/index.ts +98 -0
- package/src/ui/internal/ui/alert.tsx +51 -0
- package/src/ui/internal/ui/button.tsx +58 -0
- package/src/ui/internal/ui/dialog.tsx +134 -0
- package/src/ui/internal/ui/input.tsx +21 -0
- package/src/ui/internal/ui/label.tsx +21 -0
- package/src/ui/internal/ui/select.tsx +129 -0
- package/src/ui/internal/ui/tooltip.tsx +54 -0
- package/src/ui/internal/ui/utils.ts +5 -0
- package/src/ui/plugin-styles.css +250 -0
- package/src/ui/primitives/dialog-lifecycle.ts +58 -0
- package/src/ui/primitives/dice.tsx +79 -0
- package/src/ui/primitives/primitive-props.tsx +101 -0
- package/src/ui/theme/ThemeProvider.tsx +252 -0
- package/src/ui/theme/board.ts +61 -0
- package/src/ui/theme/css-vars.ts +105 -0
- package/src/ui/theme/derive.ts +240 -0
- package/src/ui/theme/index.ts +61 -0
- package/src/ui/theme/presets/arcade.ts +261 -0
- package/src/ui/theme/presets/studio.ts +261 -0
- package/src/ui/theme/presets/tabletop.ts +266 -0
- package/src/ui/theme/tokens.ts +392 -0
- package/src/ui/types/hex-color.ts +20 -0
- package/src/ui/types/player-state.ts +463 -0
- package/src/ui/types/tiled-board.ts +785 -0
- package/src/ui/types/visual-state.ts +137 -0
- package/src/ui.ts +1 -0
|
@@ -0,0 +1,2671 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CardCollection,
|
|
3
|
+
ViewCard,
|
|
4
|
+
ViewSlotOccupant,
|
|
5
|
+
} from "../types/index.js";
|
|
6
|
+
import type {
|
|
7
|
+
BoardContainerIdOfTable,
|
|
8
|
+
BoardIdOfTable,
|
|
9
|
+
BoardTypeIdOfTable,
|
|
10
|
+
CardIdOfTable,
|
|
11
|
+
CardIdOfState,
|
|
12
|
+
ComponentLocationOfTable,
|
|
13
|
+
ComponentIdOfTable,
|
|
14
|
+
CompatibleCardIdForHandAndDeck,
|
|
15
|
+
CompatibleCardIdForTwoPlayerZones,
|
|
16
|
+
DeckCardsOfTable,
|
|
17
|
+
DeckIdOfTable,
|
|
18
|
+
HandCardsOfTable,
|
|
19
|
+
HandIdOfTable,
|
|
20
|
+
HexBoardIdOfTable,
|
|
21
|
+
HexEdgeIdOfTable,
|
|
22
|
+
HexSpaceIdOfTable,
|
|
23
|
+
HexVertexIdOfTable,
|
|
24
|
+
PlayerIdOfState,
|
|
25
|
+
PlayerIdOfTable,
|
|
26
|
+
PlayerZoneIdOfTable,
|
|
27
|
+
RelationTypeIdOfTable,
|
|
28
|
+
ResolvedContainerLocation,
|
|
29
|
+
ResolvedDeckLocation,
|
|
30
|
+
ResolvedEdgeLocation,
|
|
31
|
+
ResolvedHandLocation,
|
|
32
|
+
ResolvedSlotLocation,
|
|
33
|
+
ResolvedSpaceLocation,
|
|
34
|
+
ResolvedVertexLocation,
|
|
35
|
+
ResolvedZoneLocation,
|
|
36
|
+
ResourceBalancesOfTable,
|
|
37
|
+
RuntimeBoardState,
|
|
38
|
+
RuntimeComponentLocation,
|
|
39
|
+
RuntimeRecord,
|
|
40
|
+
RuntimeTableRecord,
|
|
41
|
+
SquareBoardIdOfTable,
|
|
42
|
+
SquareSpaceIdOfTable,
|
|
43
|
+
SharedZoneIdOfTable,
|
|
44
|
+
SpaceIdOfTable,
|
|
45
|
+
SpaceTypeIdOfTable,
|
|
46
|
+
TableOfState,
|
|
47
|
+
TiledBoardIdOfTable,
|
|
48
|
+
TiledEdgeIdOfTable,
|
|
49
|
+
TiledEdgeStateOfTable,
|
|
50
|
+
TiledEdgeTypeIdOfTable,
|
|
51
|
+
TiledVertexIdOfTable,
|
|
52
|
+
TiledVertexStateOfTable,
|
|
53
|
+
TiledVertexTypeIdOfTable,
|
|
54
|
+
} from "./model";
|
|
55
|
+
import type { PerPlayer, PlayerId } from "./per-player";
|
|
56
|
+
import { perPlayerGet, perPlayerMap, perPlayerSet } from "./per-player";
|
|
57
|
+
|
|
58
|
+
// Thin wrappers that preserve the existing "look up by player id" idiom used
|
|
59
|
+
// throughout this file. `hands`, `zones.perPlayer`, and `resources` are
|
|
60
|
+
// `PerPlayer<T>`-shaped at runtime (the ingress codec rejects anything else),
|
|
61
|
+
// so these helpers assume the wrapper is always present.
|
|
62
|
+
function ppRead<Value>(
|
|
63
|
+
value: PerPlayer<Value> | undefined,
|
|
64
|
+
playerId: string,
|
|
65
|
+
): Value | undefined {
|
|
66
|
+
if (value === undefined) return undefined;
|
|
67
|
+
return perPlayerGet(value, playerId as PlayerId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ppWrite<Value>(
|
|
71
|
+
value: PerPlayer<Value> | undefined,
|
|
72
|
+
playerId: string,
|
|
73
|
+
next: Value,
|
|
74
|
+
): PerPlayer<Value> {
|
|
75
|
+
const base: PerPlayer<Value> = value ?? { __perPlayer: true, entries: [] };
|
|
76
|
+
return perPlayerSet(base, playerId as PlayerId, next);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type ViewCardForTable<
|
|
80
|
+
Table extends RuntimeTableRecord,
|
|
81
|
+
CardId extends CardIdOfTable<Table>,
|
|
82
|
+
> =
|
|
83
|
+
CardId extends CardIdOfTable<Table>
|
|
84
|
+
? ViewCard<
|
|
85
|
+
CardId & string,
|
|
86
|
+
Table["cards"][CardId]["cardType"] & string,
|
|
87
|
+
Extract<Table["cards"][CardId]["properties"], Record<string, unknown>>
|
|
88
|
+
>
|
|
89
|
+
: never;
|
|
90
|
+
|
|
91
|
+
type ViewSlotOccupantForTable<Table extends RuntimeTableRecord> =
|
|
92
|
+
ViewSlotOccupant<
|
|
93
|
+
ComponentIdOfTable<Table> & string,
|
|
94
|
+
PlayerIdOfTable<Table> & string,
|
|
95
|
+
string,
|
|
96
|
+
Record<string, unknown>
|
|
97
|
+
>;
|
|
98
|
+
|
|
99
|
+
export function ensureArray<T>(value: readonly T[] | T[] | undefined): T[] {
|
|
100
|
+
return Array.isArray(value) ? [...value] : [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function locationPosition(location: RuntimeComponentLocation): number {
|
|
104
|
+
return "position" in location && typeof location.position === "number"
|
|
105
|
+
? location.position
|
|
106
|
+
: Number.MAX_SAFE_INTEGER;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function orderedComponentIdsForLocation(
|
|
110
|
+
table: RuntimeTableRecord,
|
|
111
|
+
predicate: (location: RuntimeComponentLocation) => boolean,
|
|
112
|
+
): string[] {
|
|
113
|
+
return Object.entries(table.componentLocations)
|
|
114
|
+
.filter(([, location]) => predicate(location))
|
|
115
|
+
.sort(
|
|
116
|
+
(left, right) => locationPosition(left[1]) - locationPosition(right[1]),
|
|
117
|
+
)
|
|
118
|
+
.map(([componentId]) => componentId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function allowedCardSetIdsForZone(
|
|
122
|
+
table: RuntimeTableRecord,
|
|
123
|
+
zoneId: string,
|
|
124
|
+
): readonly string[] {
|
|
125
|
+
return table.zones.cardSetIdsByZoneId?.[zoneId] ?? [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasOwnKey(record: Record<string, unknown>, key: string): boolean {
|
|
129
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isSharedZoneId(table: RuntimeTableRecord, zoneId: string): boolean {
|
|
133
|
+
return (
|
|
134
|
+
hasOwnKey(table.decks, zoneId) || hasOwnKey(table.zones.shared, zoneId)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isPlayerZoneId(table: RuntimeTableRecord, zoneId: string): boolean {
|
|
139
|
+
return (
|
|
140
|
+
hasOwnKey(table.hands, zoneId) ||
|
|
141
|
+
hasOwnKey(table.zones.perPlayer, zoneId) ||
|
|
142
|
+
hasOwnKey(table.handVisibility, zoneId)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function zoneScopeForId(
|
|
147
|
+
table: RuntimeTableRecord,
|
|
148
|
+
zoneId: string,
|
|
149
|
+
): "shared" | "perPlayer" | null {
|
|
150
|
+
if (isPlayerZoneId(table, zoneId)) {
|
|
151
|
+
return "perPlayer";
|
|
152
|
+
}
|
|
153
|
+
if (isSharedZoneId(table, zoneId)) {
|
|
154
|
+
return "shared";
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function assertZoneScope(
|
|
160
|
+
table: RuntimeTableRecord,
|
|
161
|
+
zoneId: string,
|
|
162
|
+
expectedScope: "shared" | "perPlayer",
|
|
163
|
+
operation: string,
|
|
164
|
+
argumentName: string,
|
|
165
|
+
): void {
|
|
166
|
+
const actualScope = zoneScopeForId(table, zoneId);
|
|
167
|
+
if (actualScope === expectedScope) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (actualScope === null) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Unknown zone '${zoneId}' passed as ${argumentName} to ${operation}.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Zone '${zoneId}' has scope '${actualScope}', but ${operation} requires ${argumentName} to be a ${expectedScope === "shared" ? "shared" : "perPlayer"} zone.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function matchesSlotHost(
|
|
183
|
+
location: RuntimeComponentLocation,
|
|
184
|
+
host: Extract<RuntimeComponentLocation, { type: "InSlot" }>["host"],
|
|
185
|
+
slotId?: string,
|
|
186
|
+
): location is Extract<RuntimeComponentLocation, { type: "InSlot" }> {
|
|
187
|
+
return (
|
|
188
|
+
location.type === "InSlot" &&
|
|
189
|
+
location.host.kind === host.kind &&
|
|
190
|
+
location.host.id === host.id &&
|
|
191
|
+
(slotId === undefined || location.slotId === slotId)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function componentPlayerId<Table extends RuntimeTableRecord>(
|
|
196
|
+
table: Table,
|
|
197
|
+
componentId: ComponentIdOfTable<Table>,
|
|
198
|
+
): (PlayerIdOfTable<Table> & string) | null {
|
|
199
|
+
const piece = table.pieces[componentId];
|
|
200
|
+
if (piece) {
|
|
201
|
+
return (piece.ownerId ?? null) as (PlayerIdOfTable<Table> & string) | null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const die = table.dice[componentId];
|
|
205
|
+
if (die) {
|
|
206
|
+
return (die.ownerId ?? null) as (PlayerIdOfTable<Table> & string) | null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const owner = table.ownerOfCard[componentId];
|
|
210
|
+
if (owner !== undefined) {
|
|
211
|
+
return (owner ?? null) as (PlayerIdOfTable<Table> & string) | null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function componentData<Table extends RuntimeTableRecord>(
|
|
218
|
+
table: Table,
|
|
219
|
+
componentId: ComponentIdOfTable<Table>,
|
|
220
|
+
): Record<string, unknown> | undefined {
|
|
221
|
+
const piece = table.pieces[componentId];
|
|
222
|
+
if (piece) {
|
|
223
|
+
return piece.properties as Record<string, unknown>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const die = table.dice[componentId];
|
|
227
|
+
if (die) {
|
|
228
|
+
return die.properties as Record<string, unknown>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const card = table.cards[componentId];
|
|
232
|
+
if (card) {
|
|
233
|
+
return card.properties as Record<string, unknown>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function assertCardAllowedInZone<Table extends RuntimeTableRecord>(
|
|
240
|
+
table: Table,
|
|
241
|
+
zoneId: string,
|
|
242
|
+
componentId: string,
|
|
243
|
+
): void {
|
|
244
|
+
const card = table.cards[componentId];
|
|
245
|
+
if (!card) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const allowedCardSetIds = allowedCardSetIdsForZone(table, zoneId);
|
|
250
|
+
if (
|
|
251
|
+
allowedCardSetIds.length > 0 &&
|
|
252
|
+
!allowedCardSetIds.includes(card.cardSetId)
|
|
253
|
+
) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Card '${componentId}' from card set '${card.cardSetId}' cannot enter zone '${zoneId}'.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function cloneRuntimeBoardState(board: RuntimeBoardState): RuntimeBoardState {
|
|
261
|
+
if (board.layout !== "generic") {
|
|
262
|
+
return {
|
|
263
|
+
...board,
|
|
264
|
+
fields: { ...board.fields },
|
|
265
|
+
spaces: Object.fromEntries(
|
|
266
|
+
Object.entries(board.spaces).map(([spaceId, space]) => [
|
|
267
|
+
spaceId,
|
|
268
|
+
{
|
|
269
|
+
...space,
|
|
270
|
+
fields: { ...space.fields },
|
|
271
|
+
},
|
|
272
|
+
]),
|
|
273
|
+
),
|
|
274
|
+
relations: board.relations.map((relation) => ({
|
|
275
|
+
...relation,
|
|
276
|
+
fields: { ...relation.fields },
|
|
277
|
+
})),
|
|
278
|
+
containers: Object.fromEntries(
|
|
279
|
+
Object.entries(board.containers).map(([containerId, container]) => [
|
|
280
|
+
containerId,
|
|
281
|
+
{
|
|
282
|
+
...container,
|
|
283
|
+
fields: { ...container.fields },
|
|
284
|
+
},
|
|
285
|
+
]),
|
|
286
|
+
),
|
|
287
|
+
edges: board.edges.map((edge) => ({
|
|
288
|
+
...edge,
|
|
289
|
+
spaceIds: [...edge.spaceIds],
|
|
290
|
+
fields: { ...edge.fields },
|
|
291
|
+
})),
|
|
292
|
+
vertices: board.vertices.map((vertex) => ({
|
|
293
|
+
...vertex,
|
|
294
|
+
spaceIds: [...vertex.spaceIds],
|
|
295
|
+
fields: { ...vertex.fields },
|
|
296
|
+
})),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
...board,
|
|
302
|
+
fields: { ...board.fields },
|
|
303
|
+
spaces: Object.fromEntries(
|
|
304
|
+
Object.entries(board.spaces).map(([spaceId, space]) => [
|
|
305
|
+
spaceId,
|
|
306
|
+
{
|
|
307
|
+
...space,
|
|
308
|
+
fields: { ...space.fields },
|
|
309
|
+
},
|
|
310
|
+
]),
|
|
311
|
+
),
|
|
312
|
+
relations: board.relations.map((relation) => ({
|
|
313
|
+
...relation,
|
|
314
|
+
fields: { ...relation.fields },
|
|
315
|
+
})),
|
|
316
|
+
containers: Object.fromEntries(
|
|
317
|
+
Object.entries(board.containers).map(([containerId, container]) => [
|
|
318
|
+
containerId,
|
|
319
|
+
{
|
|
320
|
+
...container,
|
|
321
|
+
fields: { ...container.fields },
|
|
322
|
+
},
|
|
323
|
+
]),
|
|
324
|
+
),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function cloneRuntimeTable<Table extends RuntimeTableRecord>(
|
|
329
|
+
table: Table,
|
|
330
|
+
): Table {
|
|
331
|
+
return {
|
|
332
|
+
...table,
|
|
333
|
+
zones: {
|
|
334
|
+
shared: Object.fromEntries(
|
|
335
|
+
Object.entries(table.zones.shared).map(([zoneId, componentIds]) => [
|
|
336
|
+
zoneId,
|
|
337
|
+
[...componentIds],
|
|
338
|
+
]),
|
|
339
|
+
) as Table["zones"]["shared"],
|
|
340
|
+
perPlayer: Object.fromEntries(
|
|
341
|
+
Object.entries(table.zones.perPlayer).map(([zoneId, players]) => [
|
|
342
|
+
zoneId,
|
|
343
|
+
perPlayerMap(players as PerPlayer<string[]>, (componentIds) => [
|
|
344
|
+
...componentIds,
|
|
345
|
+
]),
|
|
346
|
+
]),
|
|
347
|
+
) as Table["zones"]["perPlayer"],
|
|
348
|
+
visibility: { ...table.zones.visibility },
|
|
349
|
+
cardSetIdsByZoneId: table.zones.cardSetIdsByZoneId
|
|
350
|
+
? Object.fromEntries(
|
|
351
|
+
Object.entries(table.zones.cardSetIdsByZoneId).map(
|
|
352
|
+
([zoneId, cardSetIds]) => [zoneId, [...cardSetIds]],
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
: table.zones.cardSetIdsByZoneId,
|
|
356
|
+
} as Table["zones"],
|
|
357
|
+
decks: Object.fromEntries(
|
|
358
|
+
Object.entries(table.decks).map(([deckId, cards]) => [
|
|
359
|
+
deckId,
|
|
360
|
+
[...cards],
|
|
361
|
+
]),
|
|
362
|
+
) as Table["decks"],
|
|
363
|
+
hands: Object.fromEntries(
|
|
364
|
+
Object.entries(table.hands).map(([handId, players]) => [
|
|
365
|
+
handId,
|
|
366
|
+
perPlayerMap(players as PerPlayer<string[]>, (cards) => [...cards]),
|
|
367
|
+
]),
|
|
368
|
+
) as Table["hands"],
|
|
369
|
+
handVisibility: { ...table.handVisibility },
|
|
370
|
+
pieces: Object.fromEntries(
|
|
371
|
+
Object.entries(table.pieces).map(([pieceId, piece]) => [
|
|
372
|
+
pieceId,
|
|
373
|
+
{ ...piece },
|
|
374
|
+
]),
|
|
375
|
+
) as Table["pieces"],
|
|
376
|
+
componentLocations: { ...table.componentLocations },
|
|
377
|
+
ownerOfCard: { ...table.ownerOfCard },
|
|
378
|
+
visibility: { ...table.visibility },
|
|
379
|
+
resources: perPlayerMap(
|
|
380
|
+
table.resources as PerPlayer<RuntimeRecord>,
|
|
381
|
+
(resources) => ({ ...resources }),
|
|
382
|
+
) as Table["resources"],
|
|
383
|
+
boards: {
|
|
384
|
+
...table.boards,
|
|
385
|
+
byId: Object.fromEntries(
|
|
386
|
+
Object.entries(table.boards.byId).map(([boardId, board]) => [
|
|
387
|
+
boardId,
|
|
388
|
+
cloneRuntimeBoardState(board),
|
|
389
|
+
]),
|
|
390
|
+
),
|
|
391
|
+
hex: Object.fromEntries(
|
|
392
|
+
Object.entries(table.boards.hex ?? {}).map(([boardId, board]) => [
|
|
393
|
+
boardId,
|
|
394
|
+
cloneRuntimeBoardState(board),
|
|
395
|
+
]),
|
|
396
|
+
),
|
|
397
|
+
square: Object.fromEntries(
|
|
398
|
+
Object.entries(table.boards.square ?? {}).map(([boardId, board]) => [
|
|
399
|
+
boardId,
|
|
400
|
+
cloneRuntimeBoardState(board),
|
|
401
|
+
]),
|
|
402
|
+
),
|
|
403
|
+
} as Table["boards"],
|
|
404
|
+
dice: Object.fromEntries(
|
|
405
|
+
Object.entries(table.dice).map(([dieId, die]) => [dieId, { ...die }]),
|
|
406
|
+
) as Table["dice"],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function syncSharedZoneWithDeck<
|
|
411
|
+
Table extends RuntimeTableRecord,
|
|
412
|
+
ZoneId extends SharedZoneIdOfTable<Table>,
|
|
413
|
+
>(table: Table, zoneId: ZoneId, nextCards: readonly string[]): void {
|
|
414
|
+
table.decks[zoneId] = [...nextCards] as Table["decks"][ZoneId];
|
|
415
|
+
table.zones.shared[zoneId] = [
|
|
416
|
+
...nextCards,
|
|
417
|
+
] as Table["zones"]["shared"][ZoneId];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Read or write a per-player zone's card list by mutating `table` in place.
|
|
422
|
+
* Called with no `nextCards` to read; called with `nextCards` to write the
|
|
423
|
+
* new ordering. Returns the list at the read site as a plain array.
|
|
424
|
+
*
|
|
425
|
+
* Used by engine-side per-player shuffle resolution; authors should reach for
|
|
426
|
+
* the named ops in this file.
|
|
427
|
+
*/
|
|
428
|
+
export function shufflePlayerZoneCards(
|
|
429
|
+
table: RuntimeTableRecord,
|
|
430
|
+
zoneId: string,
|
|
431
|
+
playerId: string,
|
|
432
|
+
nextCards?: readonly string[],
|
|
433
|
+
): string[] {
|
|
434
|
+
if (nextCards === undefined) {
|
|
435
|
+
const fromZone = ppRead(
|
|
436
|
+
table.zones.perPlayer[zoneId] ?? table.hands[zoneId],
|
|
437
|
+
playerId,
|
|
438
|
+
) as readonly string[] | undefined;
|
|
439
|
+
return [...ensureArray(fromZone)];
|
|
440
|
+
}
|
|
441
|
+
table.hands[zoneId] = ppWrite(table.hands[zoneId], playerId, [
|
|
442
|
+
...nextCards,
|
|
443
|
+
]) as (typeof table.hands)[string];
|
|
444
|
+
table.zones.perPlayer[zoneId] = ppWrite(
|
|
445
|
+
table.zones.perPlayer[zoneId],
|
|
446
|
+
playerId,
|
|
447
|
+
[...nextCards],
|
|
448
|
+
) as (typeof table.zones.perPlayer)[string];
|
|
449
|
+
return [...nextCards];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function syncPlayerZoneWithHand<
|
|
453
|
+
Table extends RuntimeTableRecord,
|
|
454
|
+
ZoneId extends PlayerZoneIdOfTable<Table>,
|
|
455
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
456
|
+
>(
|
|
457
|
+
table: Table,
|
|
458
|
+
zoneId: ZoneId,
|
|
459
|
+
playerId: PlayerId,
|
|
460
|
+
nextCards: readonly string[],
|
|
461
|
+
): void {
|
|
462
|
+
table.hands[zoneId] = ppWrite(table.hands[zoneId], playerId as string, [
|
|
463
|
+
...nextCards,
|
|
464
|
+
]) as Table["hands"][ZoneId];
|
|
465
|
+
table.zones.perPlayer[zoneId] = ppWrite(
|
|
466
|
+
table.zones.perPlayer[zoneId],
|
|
467
|
+
playerId as string,
|
|
468
|
+
[...nextCards],
|
|
469
|
+
) as Table["zones"]["perPlayer"][ZoneId];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function reindexSpaceOccupants<
|
|
473
|
+
Table extends RuntimeTableRecord,
|
|
474
|
+
BoardId extends BoardIdOfTable<Table>,
|
|
475
|
+
SpaceId extends SpaceIdOfTable<Table, BoardId>,
|
|
476
|
+
>(table: Table, boardId: BoardId, spaceId: SpaceId): void {
|
|
477
|
+
getComponentsOnSpace(table, boardId, spaceId).forEach(
|
|
478
|
+
(componentId, index) => {
|
|
479
|
+
const location = table.componentLocations[componentId];
|
|
480
|
+
if (location?.type === "OnSpace") {
|
|
481
|
+
table.componentLocations[componentId] = {
|
|
482
|
+
...location,
|
|
483
|
+
position: index,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function reindexContainerOccupants<
|
|
491
|
+
Table extends RuntimeTableRecord,
|
|
492
|
+
BoardId extends BoardIdOfTable<Table>,
|
|
493
|
+
ContainerId extends BoardContainerIdOfTable<Table, BoardId>,
|
|
494
|
+
>(table: Table, boardId: BoardId, containerId: ContainerId): void {
|
|
495
|
+
getComponentsInContainer(table, boardId, containerId).forEach(
|
|
496
|
+
(componentId, index) => {
|
|
497
|
+
const location = table.componentLocations[componentId];
|
|
498
|
+
if (location?.type === "InContainer") {
|
|
499
|
+
table.componentLocations[componentId] = {
|
|
500
|
+
...location,
|
|
501
|
+
position: index,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function reindexEdgeOccupants<
|
|
509
|
+
Table extends RuntimeTableRecord,
|
|
510
|
+
BoardId extends HexBoardIdOfTable<Table>,
|
|
511
|
+
EdgeId extends HexEdgeIdOfTable<Table, BoardId>,
|
|
512
|
+
>(table: Table, boardId: BoardId, edgeId: EdgeId): void {
|
|
513
|
+
orderedComponentIdsForLocation(
|
|
514
|
+
table,
|
|
515
|
+
(location) =>
|
|
516
|
+
location.type === "OnEdge" &&
|
|
517
|
+
location.boardId === boardId &&
|
|
518
|
+
location.edgeId === edgeId,
|
|
519
|
+
).forEach((componentId, index) => {
|
|
520
|
+
const location = table.componentLocations[componentId];
|
|
521
|
+
if (location?.type === "OnEdge") {
|
|
522
|
+
table.componentLocations[componentId] = {
|
|
523
|
+
...location,
|
|
524
|
+
position: index,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function reindexVertexOccupants<
|
|
531
|
+
Table extends RuntimeTableRecord,
|
|
532
|
+
BoardId extends HexBoardIdOfTable<Table>,
|
|
533
|
+
VertexId extends HexVertexIdOfTable<Table, BoardId>,
|
|
534
|
+
>(table: Table, boardId: BoardId, vertexId: VertexId): void {
|
|
535
|
+
orderedComponentIdsForLocation(
|
|
536
|
+
table,
|
|
537
|
+
(location) =>
|
|
538
|
+
location.type === "OnVertex" &&
|
|
539
|
+
location.boardId === boardId &&
|
|
540
|
+
location.vertexId === vertexId,
|
|
541
|
+
).forEach((componentId, index) => {
|
|
542
|
+
const location = table.componentLocations[componentId];
|
|
543
|
+
if (location?.type === "OnVertex") {
|
|
544
|
+
table.componentLocations[componentId] = {
|
|
545
|
+
...location,
|
|
546
|
+
position: index,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function reindexSlotOccupants<Table extends RuntimeTableRecord>(
|
|
553
|
+
table: Table,
|
|
554
|
+
host: Extract<RuntimeComponentLocation, { type: "InSlot" }>["host"],
|
|
555
|
+
slotId: string,
|
|
556
|
+
): void {
|
|
557
|
+
orderedComponentIdsForLocation(
|
|
558
|
+
table,
|
|
559
|
+
(location) =>
|
|
560
|
+
location.type === "InSlot" &&
|
|
561
|
+
location.host.kind === host.kind &&
|
|
562
|
+
location.host.id === host.id &&
|
|
563
|
+
location.slotId === slotId,
|
|
564
|
+
).forEach((componentId, index) => {
|
|
565
|
+
const location = table.componentLocations[componentId];
|
|
566
|
+
if (location?.type === "InSlot") {
|
|
567
|
+
table.componentLocations[componentId] = {
|
|
568
|
+
...location,
|
|
569
|
+
position: index,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function removeComponentFromCurrentLocation<
|
|
576
|
+
Table extends RuntimeTableRecord,
|
|
577
|
+
ComponentId extends ComponentIdOfTable<Table>,
|
|
578
|
+
>(table: Table, componentId: ComponentId): void {
|
|
579
|
+
const currentLocation = table.componentLocations[componentId];
|
|
580
|
+
if (!currentLocation) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (currentLocation.type === "InDeck") {
|
|
585
|
+
const nextCards = ensureArray(table.decks[currentLocation.deckId]).filter(
|
|
586
|
+
(candidate) => candidate !== componentId,
|
|
587
|
+
);
|
|
588
|
+
syncSharedZoneWithDeck(
|
|
589
|
+
table,
|
|
590
|
+
currentLocation.deckId as SharedZoneIdOfTable<Table>,
|
|
591
|
+
nextCards,
|
|
592
|
+
);
|
|
593
|
+
nextCards.forEach((cardId, index) => {
|
|
594
|
+
const location = table.componentLocations[cardId];
|
|
595
|
+
if (location?.type === "InDeck") {
|
|
596
|
+
table.componentLocations[cardId] = {
|
|
597
|
+
...location,
|
|
598
|
+
position: index,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (currentLocation.type === "InHand") {
|
|
606
|
+
const nextCards = ensureArray(
|
|
607
|
+
ppRead(table.hands[currentLocation.handId], currentLocation.playerId),
|
|
608
|
+
).filter((candidate) => candidate !== componentId);
|
|
609
|
+
syncPlayerZoneWithHand(
|
|
610
|
+
table,
|
|
611
|
+
currentLocation.handId as PlayerZoneIdOfTable<Table>,
|
|
612
|
+
currentLocation.playerId as PlayerIdOfTable<Table>,
|
|
613
|
+
nextCards,
|
|
614
|
+
);
|
|
615
|
+
nextCards.forEach((cardId, index) => {
|
|
616
|
+
const location = table.componentLocations[cardId];
|
|
617
|
+
if (location?.type === "InHand") {
|
|
618
|
+
table.componentLocations[cardId] = {
|
|
619
|
+
...location,
|
|
620
|
+
position: index,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (currentLocation.type === "OnSpace") {
|
|
628
|
+
delete table.componentLocations[componentId];
|
|
629
|
+
reindexSpaceOccupants(
|
|
630
|
+
table,
|
|
631
|
+
currentLocation.boardId as BoardIdOfTable<Table>,
|
|
632
|
+
currentLocation.spaceId as SpaceIdOfTable<Table, BoardIdOfTable<Table>>,
|
|
633
|
+
);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (currentLocation.type === "InZone") {
|
|
638
|
+
if (currentLocation.zoneId in table.zones.shared) {
|
|
639
|
+
const nextComponents = ensureArray(
|
|
640
|
+
table.zones.shared[currentLocation.zoneId],
|
|
641
|
+
).filter((candidate) => candidate !== componentId);
|
|
642
|
+
syncSharedZoneWithDeck(
|
|
643
|
+
table,
|
|
644
|
+
currentLocation.zoneId as SharedZoneIdOfTable<Table>,
|
|
645
|
+
nextComponents,
|
|
646
|
+
);
|
|
647
|
+
nextComponents.forEach((currentComponentId, index) => {
|
|
648
|
+
const location = table.componentLocations[currentComponentId];
|
|
649
|
+
if (location?.type === "InZone") {
|
|
650
|
+
table.componentLocations[currentComponentId] = {
|
|
651
|
+
...location,
|
|
652
|
+
position: index,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
delete table.componentLocations[componentId];
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (currentLocation.type === "InContainer") {
|
|
662
|
+
delete table.componentLocations[componentId];
|
|
663
|
+
reindexContainerOccupants(
|
|
664
|
+
table,
|
|
665
|
+
currentLocation.boardId as BoardIdOfTable<Table>,
|
|
666
|
+
currentLocation.containerId as BoardContainerIdOfTable<
|
|
667
|
+
Table,
|
|
668
|
+
BoardIdOfTable<Table>
|
|
669
|
+
>,
|
|
670
|
+
);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (currentLocation.type === "OnEdge") {
|
|
675
|
+
delete table.componentLocations[componentId];
|
|
676
|
+
reindexEdgeOccupants(
|
|
677
|
+
table,
|
|
678
|
+
currentLocation.boardId as HexBoardIdOfTable<Table>,
|
|
679
|
+
currentLocation.edgeId as HexEdgeIdOfTable<
|
|
680
|
+
Table,
|
|
681
|
+
HexBoardIdOfTable<Table>
|
|
682
|
+
>,
|
|
683
|
+
);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (currentLocation.type === "OnVertex") {
|
|
688
|
+
delete table.componentLocations[componentId];
|
|
689
|
+
reindexVertexOccupants(
|
|
690
|
+
table,
|
|
691
|
+
currentLocation.boardId as HexBoardIdOfTable<Table>,
|
|
692
|
+
currentLocation.vertexId as HexVertexIdOfTable<
|
|
693
|
+
Table,
|
|
694
|
+
HexBoardIdOfTable<Table>
|
|
695
|
+
>,
|
|
696
|
+
);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (currentLocation.type === "InSlot") {
|
|
701
|
+
delete table.componentLocations[componentId];
|
|
702
|
+
reindexSlotOccupants(table, currentLocation.host, currentLocation.slotId);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
delete table.componentLocations[componentId];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function appendToDeck<
|
|
710
|
+
Table extends RuntimeTableRecord,
|
|
711
|
+
DeckId extends DeckIdOfTable<Table>,
|
|
712
|
+
>(
|
|
713
|
+
table: Table,
|
|
714
|
+
deckId: DeckId,
|
|
715
|
+
cardId: DeckCardsOfTable<Table, DeckId>[number],
|
|
716
|
+
playedBy: PlayerIdOfTable<Table> | null = null,
|
|
717
|
+
position: "top" | "bottom" = "bottom",
|
|
718
|
+
): Table {
|
|
719
|
+
const nextTable = cloneRuntimeTable(table);
|
|
720
|
+
assertZoneScope(
|
|
721
|
+
nextTable,
|
|
722
|
+
deckId as string,
|
|
723
|
+
"shared",
|
|
724
|
+
"addCardToSharedZone",
|
|
725
|
+
"zoneId",
|
|
726
|
+
);
|
|
727
|
+
const deckCards = [
|
|
728
|
+
...ensureArray(nextTable.decks[deckId]),
|
|
729
|
+
] as DeckCardsOfTable<Table, DeckId>;
|
|
730
|
+
assertCardAllowedInZone(nextTable, deckId, cardId);
|
|
731
|
+
if (position === "top") {
|
|
732
|
+
deckCards.unshift(cardId);
|
|
733
|
+
} else {
|
|
734
|
+
deckCards.push(cardId);
|
|
735
|
+
}
|
|
736
|
+
syncSharedZoneWithDeck(nextTable, deckId, deckCards);
|
|
737
|
+
for (const [index, currentCardId] of deckCards.entries()) {
|
|
738
|
+
if (currentCardId === cardId) {
|
|
739
|
+
nextTable.componentLocations[currentCardId] = {
|
|
740
|
+
type: "InDeck",
|
|
741
|
+
deckId,
|
|
742
|
+
playedBy,
|
|
743
|
+
position: index,
|
|
744
|
+
};
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const existing = nextTable.componentLocations[currentCardId];
|
|
748
|
+
nextTable.componentLocations[currentCardId] = {
|
|
749
|
+
type: "InDeck",
|
|
750
|
+
deckId,
|
|
751
|
+
playedBy: existing?.type === "InDeck" ? existing.playedBy : null,
|
|
752
|
+
position: index,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
nextTable.ownerOfCard[cardId] = playedBy;
|
|
756
|
+
nextTable.visibility[cardId] = {
|
|
757
|
+
faceUp: true,
|
|
758
|
+
};
|
|
759
|
+
return nextTable;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function removeFromDeck<
|
|
763
|
+
Table extends RuntimeTableRecord,
|
|
764
|
+
DeckId extends DeckIdOfTable<Table>,
|
|
765
|
+
>(
|
|
766
|
+
table: Table,
|
|
767
|
+
deckId: DeckId,
|
|
768
|
+
cardId: DeckCardsOfTable<Table, DeckId>[number],
|
|
769
|
+
): Table {
|
|
770
|
+
const nextTable = cloneRuntimeTable(table);
|
|
771
|
+
assertZoneScope(
|
|
772
|
+
nextTable,
|
|
773
|
+
deckId as string,
|
|
774
|
+
"shared",
|
|
775
|
+
"removeCardFromSharedZone",
|
|
776
|
+
"zoneId",
|
|
777
|
+
);
|
|
778
|
+
const remaining = ensureArray(nextTable.decks[deckId]).filter(
|
|
779
|
+
(candidate) => candidate !== cardId,
|
|
780
|
+
);
|
|
781
|
+
syncSharedZoneWithDeck(nextTable, deckId, remaining);
|
|
782
|
+
for (const [index, currentCardId] of remaining.entries()) {
|
|
783
|
+
const currentLocation = nextTable.componentLocations[currentCardId];
|
|
784
|
+
if (currentLocation?.type === "InDeck") {
|
|
785
|
+
nextTable.componentLocations[currentCardId] = {
|
|
786
|
+
...currentLocation,
|
|
787
|
+
position: index,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return nextTable;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function computeVisibilityForPlayerZone(
|
|
795
|
+
table: RuntimeTableRecord,
|
|
796
|
+
zoneId: string,
|
|
797
|
+
playerId: string,
|
|
798
|
+
): { faceUp: boolean; visibleTo?: string[] } {
|
|
799
|
+
const mode = table.handVisibility[zoneId];
|
|
800
|
+
if (mode === "all" || mode === "public") {
|
|
801
|
+
return { faceUp: true };
|
|
802
|
+
}
|
|
803
|
+
return { faceUp: false, visibleTo: [playerId] };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function moveFromHandToDeck<
|
|
807
|
+
Table extends RuntimeTableRecord,
|
|
808
|
+
HandId extends HandIdOfTable<Table>,
|
|
809
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
810
|
+
DeckId extends DeckIdOfTable<Table>,
|
|
811
|
+
>(options: {
|
|
812
|
+
table: Table;
|
|
813
|
+
playerId: PlayerId;
|
|
814
|
+
handId: HandId;
|
|
815
|
+
cardId: CompatibleCardIdForHandAndDeck<Table, HandId, DeckId>;
|
|
816
|
+
deckId: DeckId;
|
|
817
|
+
playedBy?: PlayerIdOfTable<Table> | null;
|
|
818
|
+
position?: "top" | "bottom";
|
|
819
|
+
}): Table {
|
|
820
|
+
const nextTable = cloneRuntimeTable(options.table);
|
|
821
|
+
assertZoneScope(
|
|
822
|
+
nextTable,
|
|
823
|
+
options.handId as string,
|
|
824
|
+
"perPlayer",
|
|
825
|
+
"moveCardFromPlayerZoneToSharedZone",
|
|
826
|
+
"fromZoneId",
|
|
827
|
+
);
|
|
828
|
+
assertZoneScope(
|
|
829
|
+
nextTable,
|
|
830
|
+
options.deckId as string,
|
|
831
|
+
"shared",
|
|
832
|
+
"moveCardFromPlayerZoneToSharedZone",
|
|
833
|
+
"toZoneId",
|
|
834
|
+
);
|
|
835
|
+
const currentHand = ensureArray(
|
|
836
|
+
ppRead(nextTable.hands[options.handId], options.playerId as string) as
|
|
837
|
+
| readonly string[]
|
|
838
|
+
| undefined,
|
|
839
|
+
).filter((candidate) => candidate !== options.cardId);
|
|
840
|
+
syncPlayerZoneWithHand(
|
|
841
|
+
nextTable,
|
|
842
|
+
options.handId,
|
|
843
|
+
options.playerId,
|
|
844
|
+
currentHand,
|
|
845
|
+
);
|
|
846
|
+
for (const [index, currentCardId] of currentHand.entries()) {
|
|
847
|
+
nextTable.componentLocations[currentCardId as string] = {
|
|
848
|
+
type: "InHand",
|
|
849
|
+
handId: options.handId,
|
|
850
|
+
playerId: options.playerId,
|
|
851
|
+
position: index,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
nextTable.ownerOfCard[options.cardId] = options.playedBy ?? options.playerId;
|
|
855
|
+
nextTable.visibility[options.cardId] = {
|
|
856
|
+
faceUp: true,
|
|
857
|
+
};
|
|
858
|
+
return appendToDeck(
|
|
859
|
+
nextTable,
|
|
860
|
+
options.deckId,
|
|
861
|
+
options.cardId,
|
|
862
|
+
options.playedBy ?? options.playerId,
|
|
863
|
+
options.position ?? "bottom",
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export function setActivePlayers<
|
|
868
|
+
State extends { flow: { activePlayers: PlayerIdOfState<State>[] } },
|
|
869
|
+
>(state: State, activePlayers: PlayerIdOfState<State>[]): State {
|
|
870
|
+
return {
|
|
871
|
+
...state,
|
|
872
|
+
flow: {
|
|
873
|
+
...state.flow,
|
|
874
|
+
activePlayers,
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export function setPhaseState<State extends { phase: object }, PhaseState>(
|
|
880
|
+
state: State,
|
|
881
|
+
phaseState: PhaseState,
|
|
882
|
+
): State {
|
|
883
|
+
return {
|
|
884
|
+
...state,
|
|
885
|
+
phase: phaseState,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
type DeckCardsForZone<
|
|
890
|
+
Table extends RuntimeTableRecord,
|
|
891
|
+
ZoneId extends DeckIdOfTable<Table>,
|
|
892
|
+
> = ZoneId extends infer Each extends DeckIdOfTable<Table>
|
|
893
|
+
? DeckCardsOfTable<Table, Each>
|
|
894
|
+
: never;
|
|
895
|
+
|
|
896
|
+
type HandCardsForZone<
|
|
897
|
+
Table extends RuntimeTableRecord,
|
|
898
|
+
ZoneId extends HandIdOfTable<Table>,
|
|
899
|
+
> = ZoneId extends infer Each extends HandIdOfTable<Table>
|
|
900
|
+
? HandCardsOfTable<Table, Each>
|
|
901
|
+
: never;
|
|
902
|
+
|
|
903
|
+
export function getSharedZoneCards<
|
|
904
|
+
Table extends RuntimeTableRecord,
|
|
905
|
+
ZoneId extends SharedZoneIdOfTable<Table>,
|
|
906
|
+
>(table: Table, zoneId: ZoneId): DeckCardsForZone<Table, ZoneId> {
|
|
907
|
+
assertZoneScope(
|
|
908
|
+
table,
|
|
909
|
+
zoneId as string,
|
|
910
|
+
"shared",
|
|
911
|
+
"getSharedZoneCards",
|
|
912
|
+
"zoneId",
|
|
913
|
+
);
|
|
914
|
+
return [
|
|
915
|
+
...ensureArray(table.zones.shared[zoneId] ?? table.decks[zoneId]),
|
|
916
|
+
] as DeckCardsForZone<Table, ZoneId>;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export function getPlayerZoneCards<
|
|
920
|
+
Table extends RuntimeTableRecord,
|
|
921
|
+
ZoneId extends PlayerZoneIdOfTable<Table>,
|
|
922
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
923
|
+
>(
|
|
924
|
+
table: Table,
|
|
925
|
+
playerId: PlayerId,
|
|
926
|
+
zoneId: ZoneId,
|
|
927
|
+
): HandCardsForZone<Table, ZoneId> {
|
|
928
|
+
assertZoneScope(
|
|
929
|
+
table,
|
|
930
|
+
zoneId as string,
|
|
931
|
+
"perPlayer",
|
|
932
|
+
"getPlayerZoneCards",
|
|
933
|
+
"zoneId",
|
|
934
|
+
);
|
|
935
|
+
const cards =
|
|
936
|
+
ppRead(table.zones.perPlayer[zoneId], playerId as string) ??
|
|
937
|
+
ppRead(table.hands[zoneId], playerId as string);
|
|
938
|
+
return [
|
|
939
|
+
...ensureArray(cards as readonly string[] | undefined),
|
|
940
|
+
] as unknown as HandCardsForZone<Table, ZoneId>;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function collectZoneIds(
|
|
944
|
+
...sources: ReadonlyArray<Record<string, unknown> | undefined>
|
|
945
|
+
): string[] {
|
|
946
|
+
const seen = new Set<string>();
|
|
947
|
+
for (const source of sources) {
|
|
948
|
+
if (!source) continue;
|
|
949
|
+
for (const key of Object.keys(source)) {
|
|
950
|
+
seen.add(key);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return [...seen];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export function getAllSharedZoneCards<Table extends RuntimeTableRecord>(
|
|
957
|
+
table: Table,
|
|
958
|
+
): {
|
|
959
|
+
readonly [Z in SharedZoneIdOfTable<Table>]: DeckCardsOfTable<Table, Z>;
|
|
960
|
+
} {
|
|
961
|
+
const zoneIds = collectZoneIds(
|
|
962
|
+
table.zones.shared as Record<string, unknown> | undefined,
|
|
963
|
+
table.decks as Record<string, unknown> | undefined,
|
|
964
|
+
);
|
|
965
|
+
const result: Record<string, readonly unknown[]> = {};
|
|
966
|
+
for (const zoneId of zoneIds) {
|
|
967
|
+
result[zoneId] = getSharedZoneCards(
|
|
968
|
+
table,
|
|
969
|
+
zoneId as SharedZoneIdOfTable<Table>,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return result as {
|
|
973
|
+
readonly [Z in SharedZoneIdOfTable<Table>]: DeckCardsOfTable<Table, Z>;
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export function getAllPlayerZoneCards<
|
|
978
|
+
Table extends RuntimeTableRecord,
|
|
979
|
+
ZoneId extends PlayerZoneIdOfTable<Table>,
|
|
980
|
+
>(
|
|
981
|
+
table: Table,
|
|
982
|
+
zoneId: ZoneId,
|
|
983
|
+
): {
|
|
984
|
+
readonly [P in PlayerIdOfTable<Table>]: HandCardsOfTable<Table, ZoneId>;
|
|
985
|
+
} {
|
|
986
|
+
const result: Record<string, readonly unknown[]> = {};
|
|
987
|
+
for (const playerId of table.playerOrder) {
|
|
988
|
+
result[playerId] = getPlayerZoneCards(
|
|
989
|
+
table,
|
|
990
|
+
playerId as PlayerIdOfTable<Table>,
|
|
991
|
+
zoneId,
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return result as {
|
|
995
|
+
readonly [P in PlayerIdOfTable<Table>]: HandCardsOfTable<Table, ZoneId>;
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export function getCard<
|
|
1000
|
+
Table extends RuntimeTableRecord,
|
|
1001
|
+
CardId extends CardIdOfTable<NoInfer<Table>>,
|
|
1002
|
+
>(table: Table, cardId: CardId): ViewCardForTable<Table, CardId> {
|
|
1003
|
+
const card = table.cards[cardId] as Table["cards"][CardId];
|
|
1004
|
+
|
|
1005
|
+
return {
|
|
1006
|
+
id: card.id,
|
|
1007
|
+
cardType: card.cardType,
|
|
1008
|
+
name: card.name,
|
|
1009
|
+
text: card.text,
|
|
1010
|
+
properties: card.properties,
|
|
1011
|
+
} as ViewCardForTable<Table, CardId>;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export function getCardsById<
|
|
1015
|
+
Table extends RuntimeTableRecord,
|
|
1016
|
+
const CardIds extends readonly CardIdOfTable<NoInfer<Table>>[],
|
|
1017
|
+
>(
|
|
1018
|
+
table: Table,
|
|
1019
|
+
cardIds: CardIds,
|
|
1020
|
+
): Readonly<{
|
|
1021
|
+
[Id in CardIds[number]]: ViewCardForTable<Table, Id> | undefined;
|
|
1022
|
+
}> {
|
|
1023
|
+
return Object.fromEntries(
|
|
1024
|
+
cardIds.map((cardId) => [
|
|
1025
|
+
cardId,
|
|
1026
|
+
table.cards[cardId] ? getCard(table, cardId) : undefined,
|
|
1027
|
+
]),
|
|
1028
|
+
) as Readonly<{
|
|
1029
|
+
[Id in CardIds[number]]: ViewCardForTable<Table, Id> | undefined;
|
|
1030
|
+
}>;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
export function getSharedZoneCardCollection<
|
|
1034
|
+
Table extends RuntimeTableRecord,
|
|
1035
|
+
ZoneId extends SharedZoneIdOfTable<Table>,
|
|
1036
|
+
>(
|
|
1037
|
+
table: Table,
|
|
1038
|
+
zoneId: ZoneId,
|
|
1039
|
+
): CardCollection<
|
|
1040
|
+
CardIdOfTable<Table> & string,
|
|
1041
|
+
ViewCardForTable<Table, CardIdOfTable<Table>>
|
|
1042
|
+
> {
|
|
1043
|
+
const cardIds = getSharedZoneCards(table, zoneId);
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
cardIds: cardIds as readonly (CardIdOfTable<Table> & string)[],
|
|
1047
|
+
cardsById: getCardsById(table, cardIds as readonly CardIdOfTable<Table>[]),
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export function getPlayerZoneCardCollection<
|
|
1052
|
+
Table extends RuntimeTableRecord,
|
|
1053
|
+
ZoneId extends PlayerZoneIdOfTable<Table>,
|
|
1054
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
1055
|
+
>(
|
|
1056
|
+
table: Table,
|
|
1057
|
+
playerId: PlayerId,
|
|
1058
|
+
zoneId: ZoneId,
|
|
1059
|
+
): CardCollection<
|
|
1060
|
+
CardIdOfTable<Table> & string,
|
|
1061
|
+
ViewCardForTable<Table, CardIdOfTable<Table>>
|
|
1062
|
+
> {
|
|
1063
|
+
const cardIds = getPlayerZoneCards(table, playerId, zoneId);
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
cardIds: cardIds as readonly (CardIdOfTable<Table> & string)[],
|
|
1067
|
+
cardsById: getCardsById(table, cardIds as readonly CardIdOfTable<Table>[]),
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export function getSlotOccupants<Table extends RuntimeTableRecord>(
|
|
1072
|
+
table: Table,
|
|
1073
|
+
host: Extract<RuntimeComponentLocation, { type: "InSlot" }>["host"],
|
|
1074
|
+
slotId: string,
|
|
1075
|
+
): ViewSlotOccupantForTable<Table>[] {
|
|
1076
|
+
return orderedComponentIdsForLocation(table, (location) =>
|
|
1077
|
+
matchesSlotHost(location, host, slotId),
|
|
1078
|
+
).map((componentId) => ({
|
|
1079
|
+
pieceId: componentId as ComponentIdOfTable<Table> & string,
|
|
1080
|
+
playerId: componentPlayerId(
|
|
1081
|
+
table,
|
|
1082
|
+
componentId as ComponentIdOfTable<Table>,
|
|
1083
|
+
),
|
|
1084
|
+
slotId,
|
|
1085
|
+
data: componentData(table, componentId as ComponentIdOfTable<Table>),
|
|
1086
|
+
}));
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export function getSlotOccupantsByHost<Table extends RuntimeTableRecord>(
|
|
1090
|
+
table: Table,
|
|
1091
|
+
host: Extract<RuntimeComponentLocation, { type: "InSlot" }>["host"],
|
|
1092
|
+
): Readonly<Record<string, ViewSlotOccupantForTable<Table>[]>> {
|
|
1093
|
+
const occupantsBySlot: Record<string, ViewSlotOccupantForTable<Table>[]> = {};
|
|
1094
|
+
|
|
1095
|
+
orderedComponentIdsForLocation(table, (location) =>
|
|
1096
|
+
matchesSlotHost(location, host),
|
|
1097
|
+
).forEach((componentId) => {
|
|
1098
|
+
const location = table.componentLocations[componentId];
|
|
1099
|
+
if (!location || !matchesSlotHost(location, host)) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const slotOccupant: ViewSlotOccupantForTable<Table> = {
|
|
1104
|
+
pieceId: componentId as ComponentIdOfTable<Table> & string,
|
|
1105
|
+
playerId: componentPlayerId(
|
|
1106
|
+
table,
|
|
1107
|
+
componentId as ComponentIdOfTable<Table>,
|
|
1108
|
+
),
|
|
1109
|
+
slotId: location.slotId,
|
|
1110
|
+
data: componentData(table, componentId as ComponentIdOfTable<Table>),
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
(occupantsBySlot[location.slotId] ??= []).push(slotOccupant);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
return occupantsBySlot;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
export function getCardOwner<
|
|
1120
|
+
Table extends RuntimeTableRecord,
|
|
1121
|
+
CardId extends CardIdOfTable<NoInfer<Table>>,
|
|
1122
|
+
>(table: Table, cardId: CardId): Table["ownerOfCard"][CardId] {
|
|
1123
|
+
return table.ownerOfCard[cardId] as Table["ownerOfCard"][CardId];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
export function getCardVisibility<
|
|
1127
|
+
Table extends RuntimeTableRecord,
|
|
1128
|
+
CardId extends CardIdOfTable<NoInfer<Table>>,
|
|
1129
|
+
>(table: Table, cardId: CardId): Table["visibility"][CardId] {
|
|
1130
|
+
return table.visibility[cardId] as Table["visibility"][CardId];
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
export function getPlayerOrder<Table extends RuntimeTableRecord>(
|
|
1134
|
+
table: Table,
|
|
1135
|
+
): Table["playerOrder"] {
|
|
1136
|
+
return table.playerOrder;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export function getPlayerResources<Table extends RuntimeTableRecord>(
|
|
1140
|
+
table: Table,
|
|
1141
|
+
playerId: PlayerIdOfTable<NoInfer<Table>>,
|
|
1142
|
+
): ResourceBalancesOfTable<Table> {
|
|
1143
|
+
return perPlayerGet(
|
|
1144
|
+
table.resources as PerPlayer<RuntimeRecord>,
|
|
1145
|
+
playerId as unknown as PlayerId,
|
|
1146
|
+
) as ResourceBalancesOfTable<Table>;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Read the amount a player holds of a single resource. Returns `0` when the
|
|
1151
|
+
* player has never accumulated that resource.
|
|
1152
|
+
*/
|
|
1153
|
+
export function getPlayerResourceAmount<Table extends RuntimeTableRecord>(
|
|
1154
|
+
table: Table,
|
|
1155
|
+
playerId: string,
|
|
1156
|
+
resourceId: string,
|
|
1157
|
+
): number {
|
|
1158
|
+
const playerResources = perPlayerGet(
|
|
1159
|
+
table.resources as PerPlayer<RuntimeRecord>,
|
|
1160
|
+
playerId as PlayerId,
|
|
1161
|
+
);
|
|
1162
|
+
if (!playerResources) return 0;
|
|
1163
|
+
const value = (playerResources as Record<string, unknown>)[resourceId];
|
|
1164
|
+
return typeof value === "number" ? value : 0;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Sum of every resource amount for a player (e.g. "total cards in hand"
|
|
1169
|
+
* games). Skips `undefined` and non-number values.
|
|
1170
|
+
*/
|
|
1171
|
+
export function getPlayerResourceTotal<Table extends RuntimeTableRecord>(
|
|
1172
|
+
table: Table,
|
|
1173
|
+
playerId: string,
|
|
1174
|
+
): number {
|
|
1175
|
+
const playerResources = perPlayerGet(
|
|
1176
|
+
table.resources as PerPlayer<RuntimeRecord>,
|
|
1177
|
+
playerId as PlayerId,
|
|
1178
|
+
);
|
|
1179
|
+
if (!playerResources) return 0;
|
|
1180
|
+
let total = 0;
|
|
1181
|
+
for (const key of Object.keys(playerResources)) {
|
|
1182
|
+
const value = (playerResources as Record<string, unknown>)[key];
|
|
1183
|
+
if (typeof value === "number") total += value;
|
|
1184
|
+
}
|
|
1185
|
+
return total;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Next player in seating order after `playerId`, wrapping around to the
|
|
1190
|
+
* first seat. Returns `null` when `playerId` is not in the player order or
|
|
1191
|
+
* the order is empty.
|
|
1192
|
+
*/
|
|
1193
|
+
export function getNextPlayerInOrder<Table extends RuntimeTableRecord>(
|
|
1194
|
+
table: Table,
|
|
1195
|
+
playerId: string,
|
|
1196
|
+
): PlayerIdOfTable<Table> | null {
|
|
1197
|
+
const order = table.playerOrder as unknown as ReadonlyArray<
|
|
1198
|
+
PlayerIdOfTable<Table>
|
|
1199
|
+
>;
|
|
1200
|
+
if (order.length === 0) return null;
|
|
1201
|
+
const idx = order.indexOf(playerId as PlayerIdOfTable<Table>);
|
|
1202
|
+
if (idx < 0) return null;
|
|
1203
|
+
return order[(idx + 1) % order.length] ?? null;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Iterate a resource-amounts record, skipping undefined / non-positive entries.
|
|
1208
|
+
* Shared by the resource mutation helpers below.
|
|
1209
|
+
*/
|
|
1210
|
+
function forEachResourceEntry(
|
|
1211
|
+
amounts: Readonly<Record<string, number | undefined>>,
|
|
1212
|
+
visit: (resourceId: string, amount: number) => void,
|
|
1213
|
+
): void {
|
|
1214
|
+
for (const resourceId of Object.keys(amounts)) {
|
|
1215
|
+
const amount = amounts[resourceId];
|
|
1216
|
+
if (typeof amount !== "number" || amount === 0) continue;
|
|
1217
|
+
visit(resourceId, amount);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Return `true` when `playerId` has at least the requested `amounts` of each
|
|
1223
|
+
* resource. Unknown resource ids are treated as zero-balance (i.e. requesting
|
|
1224
|
+
* one of them returns `false` unless the requested amount is also zero).
|
|
1225
|
+
*/
|
|
1226
|
+
export function canAffordResources<Table extends RuntimeTableRecord>(
|
|
1227
|
+
table: Table,
|
|
1228
|
+
playerId: string,
|
|
1229
|
+
amounts: Readonly<Record<string, number | undefined>>,
|
|
1230
|
+
): boolean {
|
|
1231
|
+
for (const resourceId of Object.keys(amounts)) {
|
|
1232
|
+
const required = amounts[resourceId];
|
|
1233
|
+
if (typeof required !== "number" || required <= 0) continue;
|
|
1234
|
+
if (getPlayerResourceAmount(table, playerId, resourceId) < required) {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Return the subset of `amounts` that the player cannot afford. The returned
|
|
1243
|
+
* record maps resource id → shortfall. Empty when the player can afford the
|
|
1244
|
+
* full cost.
|
|
1245
|
+
*/
|
|
1246
|
+
export function getMissingResources<Table extends RuntimeTableRecord>(
|
|
1247
|
+
table: Table,
|
|
1248
|
+
playerId: string,
|
|
1249
|
+
amounts: Readonly<Record<string, number | undefined>>,
|
|
1250
|
+
): Record<string, number> {
|
|
1251
|
+
const missing: Record<string, number> = {};
|
|
1252
|
+
for (const resourceId of Object.keys(amounts)) {
|
|
1253
|
+
const required = amounts[resourceId];
|
|
1254
|
+
if (typeof required !== "number" || required <= 0) continue;
|
|
1255
|
+
const have = getPlayerResourceAmount(table, playerId, resourceId);
|
|
1256
|
+
if (have < required) missing[resourceId] = required - have;
|
|
1257
|
+
}
|
|
1258
|
+
return missing;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function withPlayerResources<Table extends RuntimeTableRecord>(
|
|
1262
|
+
table: Table,
|
|
1263
|
+
playerId: string,
|
|
1264
|
+
nextForPlayer: Record<string, number>,
|
|
1265
|
+
): Table {
|
|
1266
|
+
return {
|
|
1267
|
+
...table,
|
|
1268
|
+
resources: perPlayerSet(
|
|
1269
|
+
table.resources as PerPlayer<RuntimeRecord>,
|
|
1270
|
+
playerId as PlayerId,
|
|
1271
|
+
nextForPlayer as RuntimeRecord,
|
|
1272
|
+
),
|
|
1273
|
+
} as Table;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Increment each resource in `amounts` for `playerId`. Negative entries are
|
|
1278
|
+
* rejected — prefer {@link spendPlayerResources} for deductions so that
|
|
1279
|
+
* affordability is checked explicitly.
|
|
1280
|
+
*/
|
|
1281
|
+
export function addPlayerResources<Table extends RuntimeTableRecord>(
|
|
1282
|
+
table: Table,
|
|
1283
|
+
playerId: string,
|
|
1284
|
+
amounts: Readonly<Record<string, number | undefined>>,
|
|
1285
|
+
): Table {
|
|
1286
|
+
const prev = (perPlayerGet(
|
|
1287
|
+
table.resources as PerPlayer<RuntimeRecord>,
|
|
1288
|
+
playerId as PlayerId,
|
|
1289
|
+
) ?? {}) as Record<string, number>;
|
|
1290
|
+
const next: Record<string, number> = { ...prev };
|
|
1291
|
+
forEachResourceEntry(amounts, (resourceId, amount) => {
|
|
1292
|
+
if (amount < 0) {
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
`addPlayerResources: negative amount for resource '${resourceId}'. ` +
|
|
1295
|
+
`Use spendPlayerResources or transferPlayerResources instead.`,
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
next[resourceId] = (next[resourceId] ?? 0) + amount;
|
|
1299
|
+
});
|
|
1300
|
+
return withPlayerResources(table, playerId, next);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Deduct each resource in `amounts` from `playerId`. Throws when the player
|
|
1305
|
+
* cannot afford the full cost — callers must check `canAfford` in their
|
|
1306
|
+
* `validate` phase before invoking this op.
|
|
1307
|
+
*/
|
|
1308
|
+
export function spendPlayerResources<Table extends RuntimeTableRecord>(
|
|
1309
|
+
table: Table,
|
|
1310
|
+
playerId: string,
|
|
1311
|
+
amounts: Readonly<Record<string, number | undefined>>,
|
|
1312
|
+
): Table {
|
|
1313
|
+
if (!canAffordResources(table, playerId, amounts)) {
|
|
1314
|
+
const missing = getMissingResources(table, playerId, amounts);
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
`spendPlayerResources: player '${playerId}' cannot afford ${JSON.stringify(
|
|
1317
|
+
missing,
|
|
1318
|
+
)}. Check canAfford in your validate step first.`,
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const prev = (perPlayerGet(
|
|
1322
|
+
table.resources as PerPlayer<RuntimeRecord>,
|
|
1323
|
+
playerId as PlayerId,
|
|
1324
|
+
) ?? {}) as Record<string, number>;
|
|
1325
|
+
const next: Record<string, number> = { ...prev };
|
|
1326
|
+
forEachResourceEntry(amounts, (resourceId, amount) => {
|
|
1327
|
+
if (amount < 0) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
`spendPlayerResources: negative amount for resource '${resourceId}'. ` +
|
|
1330
|
+
`Pass positive amounts — the op deducts them from the player.`,
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
next[resourceId] = Math.max(0, (next[resourceId] ?? 0) - amount);
|
|
1334
|
+
});
|
|
1335
|
+
return withPlayerResources(table, playerId, next);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Transfer the specified `amounts` from one player to another. Fails when the
|
|
1340
|
+
* source player cannot afford the full cost; on success the destination
|
|
1341
|
+
* gains exactly what the source loses.
|
|
1342
|
+
*/
|
|
1343
|
+
export function transferPlayerResources<Table extends RuntimeTableRecord>(
|
|
1344
|
+
table: Table,
|
|
1345
|
+
fromPlayerId: string,
|
|
1346
|
+
toPlayerId: string,
|
|
1347
|
+
amounts: Readonly<Record<string, number | undefined>>,
|
|
1348
|
+
): Table {
|
|
1349
|
+
const afterSpend = spendPlayerResources(table, fromPlayerId, amounts);
|
|
1350
|
+
return addPlayerResources(afterSpend, toPlayerId, amounts);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Overwrite a single resource balance for a player. Prefer the additive or
|
|
1355
|
+
* subtractive helpers — use this only when the new balance is an absolute
|
|
1356
|
+
* (e.g. "set coins to 10" for a scripted setup).
|
|
1357
|
+
*/
|
|
1358
|
+
export function setPlayerResource<Table extends RuntimeTableRecord>(
|
|
1359
|
+
table: Table,
|
|
1360
|
+
playerId: string,
|
|
1361
|
+
resourceId: string,
|
|
1362
|
+
amount: number,
|
|
1363
|
+
): Table {
|
|
1364
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
1365
|
+
throw new Error(
|
|
1366
|
+
`setPlayerResource: amount must be a non-negative finite number, got ${amount}.`,
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
const prev = (perPlayerGet(
|
|
1370
|
+
table.resources as PerPlayer<RuntimeRecord>,
|
|
1371
|
+
playerId as PlayerId,
|
|
1372
|
+
) ?? {}) as Record<string, number>;
|
|
1373
|
+
return withPlayerResources(table, playerId, {
|
|
1374
|
+
...prev,
|
|
1375
|
+
[resourceId]: amount,
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
export function getComponentLocation<
|
|
1380
|
+
Table extends RuntimeTableRecord,
|
|
1381
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1382
|
+
>(
|
|
1383
|
+
table: Table,
|
|
1384
|
+
componentId: ComponentId,
|
|
1385
|
+
): ComponentLocationOfTable<Table, ComponentId> | undefined {
|
|
1386
|
+
return table.componentLocations[componentId] as
|
|
1387
|
+
| ComponentLocationOfTable<Table, ComponentId>
|
|
1388
|
+
| undefined;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
export function getComponentDeckLocation<
|
|
1392
|
+
Table extends RuntimeTableRecord,
|
|
1393
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1394
|
+
>(
|
|
1395
|
+
table: Table,
|
|
1396
|
+
componentId: ComponentId,
|
|
1397
|
+
): ResolvedDeckLocation<Table, ComponentId> | null {
|
|
1398
|
+
const location = getComponentLocation(table, componentId);
|
|
1399
|
+
if (location?.type !== "InDeck") {
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
return {
|
|
1404
|
+
componentId,
|
|
1405
|
+
deckId: location.deckId as DeckIdOfTable<Table>,
|
|
1406
|
+
cards: table.decks[location.deckId as DeckIdOfTable<Table>],
|
|
1407
|
+
location,
|
|
1408
|
+
} as ResolvedDeckLocation<Table, ComponentId>;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
export function getComponentHandLocation<
|
|
1412
|
+
Table extends RuntimeTableRecord,
|
|
1413
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1414
|
+
>(
|
|
1415
|
+
table: Table,
|
|
1416
|
+
componentId: ComponentId,
|
|
1417
|
+
): ResolvedHandLocation<Table, ComponentId> | null {
|
|
1418
|
+
const location = getComponentLocation(table, componentId);
|
|
1419
|
+
if (location?.type !== "InHand") {
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
return {
|
|
1424
|
+
componentId,
|
|
1425
|
+
handId: location.handId as HandIdOfTable<Table>,
|
|
1426
|
+
playerId: location.playerId as PlayerIdOfTable<Table>,
|
|
1427
|
+
cards: ppRead(
|
|
1428
|
+
table.hands[location.handId as HandIdOfTable<Table>],
|
|
1429
|
+
location.playerId as string,
|
|
1430
|
+
),
|
|
1431
|
+
location,
|
|
1432
|
+
} as unknown as ResolvedHandLocation<Table, ComponentId>;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
export function getComponentZoneLocation<
|
|
1436
|
+
Table extends RuntimeTableRecord,
|
|
1437
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1438
|
+
>(
|
|
1439
|
+
table: Table,
|
|
1440
|
+
componentId: ComponentId,
|
|
1441
|
+
): ResolvedZoneLocation<Table, ComponentId> | null {
|
|
1442
|
+
const location = getComponentLocation(table, componentId);
|
|
1443
|
+
if (location?.type !== "InZone") {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
return {
|
|
1448
|
+
componentId,
|
|
1449
|
+
zoneId: location.zoneId,
|
|
1450
|
+
location,
|
|
1451
|
+
} as ResolvedZoneLocation<Table, ComponentId>;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
export function getComponentSpaceLocation<
|
|
1455
|
+
Table extends RuntimeTableRecord,
|
|
1456
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1457
|
+
>(
|
|
1458
|
+
table: Table,
|
|
1459
|
+
componentId: ComponentId,
|
|
1460
|
+
): ResolvedSpaceLocation<Table, ComponentId> | null {
|
|
1461
|
+
const location = getComponentLocation(table, componentId);
|
|
1462
|
+
if (location?.type !== "OnSpace") {
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const boardId = location.boardId as BoardIdOfTable<Table>;
|
|
1467
|
+
const spaceId = location.spaceId as SpaceIdOfTable<Table, typeof boardId>;
|
|
1468
|
+
return {
|
|
1469
|
+
componentId,
|
|
1470
|
+
boardId,
|
|
1471
|
+
board: getBoard(table, boardId),
|
|
1472
|
+
spaceId,
|
|
1473
|
+
space: getSpace(table, boardId, spaceId),
|
|
1474
|
+
location,
|
|
1475
|
+
} as ResolvedSpaceLocation<Table, ComponentId>;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
export function getComponentContainerLocation<
|
|
1479
|
+
Table extends RuntimeTableRecord,
|
|
1480
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1481
|
+
>(
|
|
1482
|
+
table: Table,
|
|
1483
|
+
componentId: ComponentId,
|
|
1484
|
+
): ResolvedContainerLocation<Table, ComponentId> | null {
|
|
1485
|
+
const location = getComponentLocation(table, componentId);
|
|
1486
|
+
if (location?.type !== "InContainer") {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const boardId = location.boardId as BoardIdOfTable<Table>;
|
|
1491
|
+
const containerId = location.containerId as BoardContainerIdOfTable<
|
|
1492
|
+
Table,
|
|
1493
|
+
typeof boardId
|
|
1494
|
+
>;
|
|
1495
|
+
return {
|
|
1496
|
+
componentId,
|
|
1497
|
+
boardId,
|
|
1498
|
+
board: getBoard(table, boardId),
|
|
1499
|
+
containerId,
|
|
1500
|
+
container: getContainer(table, boardId, containerId),
|
|
1501
|
+
location,
|
|
1502
|
+
} as ResolvedContainerLocation<Table, ComponentId>;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
export function getComponentEdgeLocation<
|
|
1506
|
+
Table extends RuntimeTableRecord,
|
|
1507
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1508
|
+
>(
|
|
1509
|
+
table: Table,
|
|
1510
|
+
componentId: ComponentId,
|
|
1511
|
+
): ResolvedEdgeLocation<Table, ComponentId> | null {
|
|
1512
|
+
const location = getComponentLocation(table, componentId);
|
|
1513
|
+
if (location?.type !== "OnEdge") {
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const boardId = location.boardId as TiledBoardIdOfTable<Table>;
|
|
1518
|
+
const edgeId = location.edgeId as TiledEdgeIdOfTable<Table, typeof boardId>;
|
|
1519
|
+
return {
|
|
1520
|
+
componentId,
|
|
1521
|
+
boardId,
|
|
1522
|
+
board: getTiledBoard(table, boardId),
|
|
1523
|
+
edgeId,
|
|
1524
|
+
edge: getEdge(table, boardId, edgeId),
|
|
1525
|
+
location,
|
|
1526
|
+
} as ResolvedEdgeLocation<Table, ComponentId>;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
export function getComponentVertexLocation<
|
|
1530
|
+
Table extends RuntimeTableRecord,
|
|
1531
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1532
|
+
>(
|
|
1533
|
+
table: Table,
|
|
1534
|
+
componentId: ComponentId,
|
|
1535
|
+
): ResolvedVertexLocation<Table, ComponentId> | null {
|
|
1536
|
+
const location = getComponentLocation(table, componentId);
|
|
1537
|
+
if (location?.type !== "OnVertex") {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
const boardId = location.boardId as TiledBoardIdOfTable<Table>;
|
|
1542
|
+
const vertexId = location.vertexId as TiledVertexIdOfTable<
|
|
1543
|
+
Table,
|
|
1544
|
+
typeof boardId
|
|
1545
|
+
>;
|
|
1546
|
+
return {
|
|
1547
|
+
componentId,
|
|
1548
|
+
boardId,
|
|
1549
|
+
board: getTiledBoard(table, boardId),
|
|
1550
|
+
vertexId,
|
|
1551
|
+
vertex: getVertex(table, boardId, vertexId),
|
|
1552
|
+
location,
|
|
1553
|
+
} as ResolvedVertexLocation<Table, ComponentId>;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
export function getComponentSlotLocation<
|
|
1557
|
+
Table extends RuntimeTableRecord,
|
|
1558
|
+
ComponentId extends ComponentIdOfTable<NoInfer<Table>>,
|
|
1559
|
+
>(
|
|
1560
|
+
table: Table,
|
|
1561
|
+
componentId: ComponentId,
|
|
1562
|
+
): ResolvedSlotLocation<Table, ComponentId> | null {
|
|
1563
|
+
const location = getComponentLocation(table, componentId);
|
|
1564
|
+
if (location?.type !== "InSlot") {
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return {
|
|
1569
|
+
componentId,
|
|
1570
|
+
host: location.host,
|
|
1571
|
+
slotId: location.slotId,
|
|
1572
|
+
location,
|
|
1573
|
+
} as ResolvedSlotLocation<Table, ComponentId>;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
export function getBoard<
|
|
1577
|
+
Table extends RuntimeTableRecord,
|
|
1578
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
1579
|
+
>(table: Table, boardId: BoardId): Table["boards"]["byId"][BoardId] {
|
|
1580
|
+
return table.boards.byId[boardId] as Table["boards"]["byId"][BoardId];
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
export function getHexBoard<
|
|
1584
|
+
Table extends RuntimeTableRecord,
|
|
1585
|
+
BoardId extends HexBoardIdOfTable<NoInfer<Table>>,
|
|
1586
|
+
>(
|
|
1587
|
+
table: Table,
|
|
1588
|
+
boardId: BoardId,
|
|
1589
|
+
): Extract<Table["boards"]["byId"][BoardId], { layout: "hex" }> {
|
|
1590
|
+
return getBoard(table, boardId) as Extract<
|
|
1591
|
+
Table["boards"]["byId"][BoardId],
|
|
1592
|
+
{ layout: "hex" }
|
|
1593
|
+
>;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
export function getTiledBoard<
|
|
1597
|
+
Table extends RuntimeTableRecord,
|
|
1598
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
1599
|
+
>(
|
|
1600
|
+
table: Table,
|
|
1601
|
+
boardId: BoardId,
|
|
1602
|
+
): Extract<Table["boards"]["byId"][BoardId], { layout: "hex" | "square" }> {
|
|
1603
|
+
return getBoard(table, boardId) as Extract<
|
|
1604
|
+
Table["boards"]["byId"][BoardId],
|
|
1605
|
+
{ layout: "hex" | "square" }
|
|
1606
|
+
>;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
export function getSquareBoard<
|
|
1610
|
+
Table extends RuntimeTableRecord,
|
|
1611
|
+
BoardId extends SquareBoardIdOfTable<NoInfer<Table>>,
|
|
1612
|
+
>(
|
|
1613
|
+
table: Table,
|
|
1614
|
+
boardId: BoardId,
|
|
1615
|
+
): Extract<Table["boards"]["byId"][BoardId], { layout: "square" }> {
|
|
1616
|
+
return getBoard(table, boardId) as Extract<
|
|
1617
|
+
Table["boards"]["byId"][BoardId],
|
|
1618
|
+
{ layout: "square" }
|
|
1619
|
+
>;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
export function getSpace<
|
|
1623
|
+
Table extends RuntimeTableRecord,
|
|
1624
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
1625
|
+
SpaceId extends SpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1626
|
+
>(
|
|
1627
|
+
table: Table,
|
|
1628
|
+
boardId: BoardId,
|
|
1629
|
+
spaceId: SpaceId,
|
|
1630
|
+
): Table["boards"]["byId"][BoardId]["spaces"][SpaceId] {
|
|
1631
|
+
return getBoard(table, boardId).spaces[
|
|
1632
|
+
spaceId
|
|
1633
|
+
] as Table["boards"]["byId"][BoardId]["spaces"][SpaceId];
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
export function getHexSpace<
|
|
1637
|
+
Table extends RuntimeTableRecord,
|
|
1638
|
+
BoardId extends HexBoardIdOfTable<NoInfer<Table>>,
|
|
1639
|
+
SpaceId extends HexSpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1640
|
+
>(
|
|
1641
|
+
table: Table,
|
|
1642
|
+
boardId: BoardId,
|
|
1643
|
+
spaceId: SpaceId,
|
|
1644
|
+
): Extract<
|
|
1645
|
+
Table["boards"]["byId"][BoardId],
|
|
1646
|
+
{ layout: "hex" }
|
|
1647
|
+
>["spaces"][SpaceId] {
|
|
1648
|
+
return getHexBoard(table, boardId).spaces[spaceId] as Extract<
|
|
1649
|
+
Table["boards"]["byId"][BoardId],
|
|
1650
|
+
{ layout: "hex" }
|
|
1651
|
+
>["spaces"][SpaceId];
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
export function getSquareSpace<
|
|
1655
|
+
Table extends RuntimeTableRecord,
|
|
1656
|
+
BoardId extends SquareBoardIdOfTable<NoInfer<Table>>,
|
|
1657
|
+
SpaceId extends SquareSpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1658
|
+
>(
|
|
1659
|
+
table: Table,
|
|
1660
|
+
boardId: BoardId,
|
|
1661
|
+
spaceId: SpaceId,
|
|
1662
|
+
): Extract<
|
|
1663
|
+
Table["boards"]["byId"][BoardId],
|
|
1664
|
+
{ layout: "square" }
|
|
1665
|
+
>["spaces"][SpaceId] {
|
|
1666
|
+
return getSquareBoard(table, boardId).spaces[spaceId] as Extract<
|
|
1667
|
+
Table["boards"]["byId"][BoardId],
|
|
1668
|
+
{ layout: "square" }
|
|
1669
|
+
>["spaces"][SpaceId];
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
export function getContainer<
|
|
1673
|
+
Table extends RuntimeTableRecord,
|
|
1674
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
1675
|
+
ContainerId extends BoardContainerIdOfTable<NoInfer<Table>, BoardId>,
|
|
1676
|
+
>(
|
|
1677
|
+
table: Table,
|
|
1678
|
+
boardId: BoardId,
|
|
1679
|
+
containerId: ContainerId,
|
|
1680
|
+
): Table["boards"]["byId"][BoardId]["containers"][ContainerId] {
|
|
1681
|
+
return getBoard(table, boardId).containers[
|
|
1682
|
+
containerId
|
|
1683
|
+
] as Table["boards"]["byId"][BoardId]["containers"][ContainerId];
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
export function assertCardAllowedInContainer<
|
|
1687
|
+
Table extends RuntimeTableRecord,
|
|
1688
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
1689
|
+
ContainerId extends BoardContainerIdOfTable<NoInfer<Table>, BoardId>,
|
|
1690
|
+
>(
|
|
1691
|
+
table: Table,
|
|
1692
|
+
boardId: BoardId,
|
|
1693
|
+
containerId: ContainerId,
|
|
1694
|
+
componentId: string,
|
|
1695
|
+
): void {
|
|
1696
|
+
const card = table.cards[componentId];
|
|
1697
|
+
if (!card) {
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const allowedCardSetIds =
|
|
1702
|
+
getContainer(table, boardId, containerId).allowedCardSetIds ?? [];
|
|
1703
|
+
if (
|
|
1704
|
+
allowedCardSetIds.length > 0 &&
|
|
1705
|
+
!allowedCardSetIds.includes(card.cardSetId)
|
|
1706
|
+
) {
|
|
1707
|
+
throw new Error(
|
|
1708
|
+
`Card '${componentId}' from card set '${card.cardSetId}' cannot enter container '${containerId}' on board '${boardId}'.`,
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
export function getEdge<
|
|
1714
|
+
Table extends RuntimeTableRecord,
|
|
1715
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
1716
|
+
EdgeId extends TiledEdgeIdOfTable<NoInfer<Table>, BoardId>,
|
|
1717
|
+
>(
|
|
1718
|
+
table: Table,
|
|
1719
|
+
boardId: BoardId,
|
|
1720
|
+
edgeId: EdgeId,
|
|
1721
|
+
): TiledEdgeStateOfTable<Table, BoardId, EdgeId> {
|
|
1722
|
+
const edge = getTiledBoard(table, boardId).edges.find(
|
|
1723
|
+
(candidate) => candidate.id === edgeId,
|
|
1724
|
+
);
|
|
1725
|
+
if (!edge) {
|
|
1726
|
+
throw new Error(`Unknown edge '${edgeId}' on board '${boardId}'.`);
|
|
1727
|
+
}
|
|
1728
|
+
return edge as TiledEdgeStateOfTable<Table, BoardId, EdgeId>;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
export function getVertex<
|
|
1732
|
+
Table extends RuntimeTableRecord,
|
|
1733
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
1734
|
+
VertexId extends TiledVertexIdOfTable<NoInfer<Table>, BoardId>,
|
|
1735
|
+
>(
|
|
1736
|
+
table: Table,
|
|
1737
|
+
boardId: BoardId,
|
|
1738
|
+
vertexId: VertexId,
|
|
1739
|
+
): TiledVertexStateOfTable<Table, BoardId, VertexId> {
|
|
1740
|
+
const vertex = getTiledBoard(table, boardId).vertices.find(
|
|
1741
|
+
(candidate) => candidate.id === vertexId,
|
|
1742
|
+
);
|
|
1743
|
+
if (!vertex) {
|
|
1744
|
+
throw new Error(`Unknown vertex '${vertexId}' on board '${boardId}'.`);
|
|
1745
|
+
}
|
|
1746
|
+
return vertex as TiledVertexStateOfTable<Table, BoardId, VertexId>;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
export function getHexSpaceAt<
|
|
1750
|
+
Table extends RuntimeTableRecord,
|
|
1751
|
+
BoardId extends HexBoardIdOfTable<NoInfer<Table>>,
|
|
1752
|
+
>(
|
|
1753
|
+
table: Table,
|
|
1754
|
+
boardId: BoardId,
|
|
1755
|
+
q: number,
|
|
1756
|
+
r: number,
|
|
1757
|
+
):
|
|
1758
|
+
| Extract<
|
|
1759
|
+
Table["boards"]["byId"][BoardId],
|
|
1760
|
+
{ layout: "hex" }
|
|
1761
|
+
>["spaces"][HexSpaceIdOfTable<NoInfer<Table>, BoardId>]
|
|
1762
|
+
| undefined {
|
|
1763
|
+
return Object.values(getHexBoard(table, boardId).spaces).find(
|
|
1764
|
+
(space) => space.q === q && space.r === r,
|
|
1765
|
+
) as
|
|
1766
|
+
| Extract<
|
|
1767
|
+
Table["boards"]["byId"][BoardId],
|
|
1768
|
+
{ layout: "hex" }
|
|
1769
|
+
>["spaces"][HexSpaceIdOfTable<NoInfer<Table>, BoardId>]
|
|
1770
|
+
| undefined;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
export function getSquareSpaceAt<
|
|
1774
|
+
Table extends RuntimeTableRecord,
|
|
1775
|
+
BoardId extends SquareBoardIdOfTable<NoInfer<Table>>,
|
|
1776
|
+
>(
|
|
1777
|
+
table: Table,
|
|
1778
|
+
boardId: BoardId,
|
|
1779
|
+
row: number,
|
|
1780
|
+
col: number,
|
|
1781
|
+
):
|
|
1782
|
+
| Extract<
|
|
1783
|
+
Table["boards"]["byId"][BoardId],
|
|
1784
|
+
{ layout: "square" }
|
|
1785
|
+
>["spaces"][SquareSpaceIdOfTable<NoInfer<Table>, BoardId>]
|
|
1786
|
+
| undefined {
|
|
1787
|
+
return Object.values(getSquareBoard(table, boardId).spaces).find(
|
|
1788
|
+
(space) => space.row === row && space.col === col,
|
|
1789
|
+
) as
|
|
1790
|
+
| Extract<
|
|
1791
|
+
Table["boards"]["byId"][BoardId],
|
|
1792
|
+
{ layout: "square" }
|
|
1793
|
+
>["spaces"][SquareSpaceIdOfTable<NoInfer<Table>, BoardId>]
|
|
1794
|
+
| undefined;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
export function getSpaceEdges<
|
|
1798
|
+
Table extends RuntimeTableRecord,
|
|
1799
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
1800
|
+
>(
|
|
1801
|
+
table: Table,
|
|
1802
|
+
boardId: BoardId,
|
|
1803
|
+
spaceId: SpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1804
|
+
): TiledEdgeIdOfTable<Table, BoardId>[] {
|
|
1805
|
+
return getTiledBoard(table, boardId)
|
|
1806
|
+
.edges.filter((edge) => edge.spaceIds.includes(spaceId))
|
|
1807
|
+
.map((edge) => edge.id as TiledEdgeIdOfTable<Table, BoardId>);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
export function getSpaceVertices<
|
|
1811
|
+
Table extends RuntimeTableRecord,
|
|
1812
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
1813
|
+
>(
|
|
1814
|
+
table: Table,
|
|
1815
|
+
boardId: BoardId,
|
|
1816
|
+
spaceId: SpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1817
|
+
): TiledVertexIdOfTable<Table, BoardId>[] {
|
|
1818
|
+
return getTiledBoard(table, boardId)
|
|
1819
|
+
.vertices.filter((vertex) => vertex.spaceIds.includes(spaceId))
|
|
1820
|
+
.map((vertex) => vertex.id as TiledVertexIdOfTable<Table, BoardId>);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
export function getIncidentEdges<
|
|
1824
|
+
Table extends RuntimeTableRecord,
|
|
1825
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
1826
|
+
VertexId extends TiledVertexIdOfTable<NoInfer<Table>, BoardId>,
|
|
1827
|
+
>(
|
|
1828
|
+
table: Table,
|
|
1829
|
+
boardId: BoardId,
|
|
1830
|
+
vertexId: VertexId,
|
|
1831
|
+
): TiledEdgeIdOfTable<Table, BoardId>[] {
|
|
1832
|
+
const tiledBoard = getTiledBoard(table, boardId);
|
|
1833
|
+
const vertex = tiledBoard.vertices.find(
|
|
1834
|
+
(candidate) => candidate.id === vertexId,
|
|
1835
|
+
);
|
|
1836
|
+
if (!vertex) {
|
|
1837
|
+
throw new Error(`Unknown vertex '${vertexId}' on board '${boardId}'.`);
|
|
1838
|
+
}
|
|
1839
|
+
const vertexSpaceIds = new Set(vertex.spaceIds);
|
|
1840
|
+
return tiledBoard.edges
|
|
1841
|
+
.filter((edge) =>
|
|
1842
|
+
edge.spaceIds.every((spaceId) => vertexSpaceIds.has(spaceId)),
|
|
1843
|
+
)
|
|
1844
|
+
.map((edge) => edge.id as TiledEdgeIdOfTable<Table, BoardId>);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
export function getIncidentVertices<
|
|
1848
|
+
Table extends RuntimeTableRecord,
|
|
1849
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
1850
|
+
EdgeId extends TiledEdgeIdOfTable<NoInfer<Table>, BoardId>,
|
|
1851
|
+
>(
|
|
1852
|
+
table: Table,
|
|
1853
|
+
boardId: BoardId,
|
|
1854
|
+
edgeId: EdgeId,
|
|
1855
|
+
): TiledVertexIdOfTable<Table, BoardId>[] {
|
|
1856
|
+
const tiledBoard = getTiledBoard(table, boardId);
|
|
1857
|
+
const edge = tiledBoard.edges.find((candidate) => candidate.id === edgeId);
|
|
1858
|
+
if (!edge) {
|
|
1859
|
+
throw new Error(`Unknown edge '${edgeId}' on board '${boardId}'.`);
|
|
1860
|
+
}
|
|
1861
|
+
const edgeSpaceIds = new Set(edge.spaceIds);
|
|
1862
|
+
return tiledBoard.vertices
|
|
1863
|
+
.filter((vertex) =>
|
|
1864
|
+
Array.from(edgeSpaceIds).every((spaceId) =>
|
|
1865
|
+
vertex.spaceIds.includes(spaceId),
|
|
1866
|
+
),
|
|
1867
|
+
)
|
|
1868
|
+
.map((vertex) => vertex.id as TiledVertexIdOfTable<Table, BoardId>);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
export function getRelatedSpaces<
|
|
1872
|
+
Table extends RuntimeTableRecord,
|
|
1873
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
1874
|
+
SpaceId extends SpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1875
|
+
TypeId extends RelationTypeIdOfTable<NoInfer<Table>, BoardId>,
|
|
1876
|
+
>(
|
|
1877
|
+
table: Table,
|
|
1878
|
+
boardId: BoardId,
|
|
1879
|
+
spaceId: SpaceId,
|
|
1880
|
+
relationTypeId: TypeId,
|
|
1881
|
+
): SpaceId[] {
|
|
1882
|
+
const board = getBoard(table, boardId);
|
|
1883
|
+
const related = new Set<SpaceId>();
|
|
1884
|
+
|
|
1885
|
+
for (const relation of board.relations) {
|
|
1886
|
+
if (relation.typeId !== relationTypeId) {
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
if (relation.fromSpaceId === spaceId) {
|
|
1890
|
+
related.add(relation.toSpaceId as SpaceId);
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
if (!relation.directed && relation.toSpaceId === spaceId) {
|
|
1894
|
+
related.add(relation.fromSpaceId as SpaceId);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
return [...related];
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
export function getAdjacentSpaces<
|
|
1902
|
+
Table extends RuntimeTableRecord,
|
|
1903
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
1904
|
+
SpaceId extends SpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1905
|
+
>(table: Table, boardId: BoardId, spaceId: SpaceId): SpaceId[] {
|
|
1906
|
+
return getRelatedSpaces(
|
|
1907
|
+
table,
|
|
1908
|
+
boardId,
|
|
1909
|
+
spaceId,
|
|
1910
|
+
"adjacent" as RelationTypeIdOfTable<NoInfer<Table>, BoardId>,
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
export function getSpaceDistance<
|
|
1915
|
+
Table extends RuntimeTableRecord,
|
|
1916
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
1917
|
+
SpaceId extends SpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1918
|
+
>(
|
|
1919
|
+
table: Table,
|
|
1920
|
+
boardId: BoardId,
|
|
1921
|
+
fromSpaceId: SpaceId,
|
|
1922
|
+
toSpaceId: SpaceId,
|
|
1923
|
+
): number {
|
|
1924
|
+
if (fromSpaceId === toSpaceId) {
|
|
1925
|
+
return 0;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const visited = new Set<string>([fromSpaceId]);
|
|
1929
|
+
let frontier: string[] = [fromSpaceId];
|
|
1930
|
+
let distance = 0;
|
|
1931
|
+
|
|
1932
|
+
while (frontier.length > 0) {
|
|
1933
|
+
distance += 1;
|
|
1934
|
+
const nextFrontier: string[] = [];
|
|
1935
|
+
|
|
1936
|
+
for (const currentSpaceId of frontier) {
|
|
1937
|
+
for (const neighborId of getAdjacentSpaces(
|
|
1938
|
+
table,
|
|
1939
|
+
boardId,
|
|
1940
|
+
currentSpaceId as SpaceId,
|
|
1941
|
+
)) {
|
|
1942
|
+
if (neighborId === toSpaceId) {
|
|
1943
|
+
return distance;
|
|
1944
|
+
}
|
|
1945
|
+
if (!visited.has(neighborId)) {
|
|
1946
|
+
visited.add(neighborId);
|
|
1947
|
+
nextFrontier.push(neighborId);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
frontier = nextFrontier;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
return Number.POSITIVE_INFINITY;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
export function getSquareNeighbors<
|
|
1959
|
+
Table extends RuntimeTableRecord,
|
|
1960
|
+
BoardId extends SquareBoardIdOfTable<NoInfer<Table>>,
|
|
1961
|
+
SpaceId extends SquareSpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
1962
|
+
>(
|
|
1963
|
+
table: Table,
|
|
1964
|
+
boardId: BoardId,
|
|
1965
|
+
spaceId: SpaceId,
|
|
1966
|
+
options: { mode?: "orthogonal" | "diagonal" | "all" } = {},
|
|
1967
|
+
): SquareSpaceIdOfTable<Table, BoardId>[] {
|
|
1968
|
+
const board = getSquareBoard(table, boardId);
|
|
1969
|
+
const space = getSquareSpace(table, boardId, spaceId);
|
|
1970
|
+
const offsets: ReadonlyArray<readonly [number, number]> =
|
|
1971
|
+
options.mode === "diagonal"
|
|
1972
|
+
? [
|
|
1973
|
+
[-1, -1],
|
|
1974
|
+
[-1, 1],
|
|
1975
|
+
[1, -1],
|
|
1976
|
+
[1, 1],
|
|
1977
|
+
]
|
|
1978
|
+
: options.mode === "all"
|
|
1979
|
+
? [
|
|
1980
|
+
[-1, 0],
|
|
1981
|
+
[0, 1],
|
|
1982
|
+
[1, 0],
|
|
1983
|
+
[0, -1],
|
|
1984
|
+
[-1, -1],
|
|
1985
|
+
[-1, 1],
|
|
1986
|
+
[1, -1],
|
|
1987
|
+
[1, 1],
|
|
1988
|
+
]
|
|
1989
|
+
: [
|
|
1990
|
+
[-1, 0],
|
|
1991
|
+
[0, 1],
|
|
1992
|
+
[1, 0],
|
|
1993
|
+
[0, -1],
|
|
1994
|
+
];
|
|
1995
|
+
|
|
1996
|
+
return offsets
|
|
1997
|
+
.map(([rowOffset, colOffset]) =>
|
|
1998
|
+
Object.values(board.spaces).find(
|
|
1999
|
+
(candidate) =>
|
|
2000
|
+
candidate.row === space.row + rowOffset &&
|
|
2001
|
+
candidate.col === space.col + colOffset,
|
|
2002
|
+
),
|
|
2003
|
+
)
|
|
2004
|
+
.filter((candidate): candidate is typeof space => candidate !== undefined)
|
|
2005
|
+
.map((candidate) => candidate.id as SquareSpaceIdOfTable<Table, BoardId>);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
export function getSquareDistance<
|
|
2009
|
+
Table extends RuntimeTableRecord,
|
|
2010
|
+
BoardId extends SquareBoardIdOfTable<NoInfer<Table>>,
|
|
2011
|
+
SpaceId extends SquareSpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
2012
|
+
>(
|
|
2013
|
+
table: Table,
|
|
2014
|
+
boardId: BoardId,
|
|
2015
|
+
fromSpaceId: SpaceId,
|
|
2016
|
+
toSpaceId: SpaceId,
|
|
2017
|
+
options: { metric?: "manhattan" | "chebyshev" } = {},
|
|
2018
|
+
): number {
|
|
2019
|
+
const from = getSquareSpace(table, boardId, fromSpaceId);
|
|
2020
|
+
const to = getSquareSpace(table, boardId, toSpaceId);
|
|
2021
|
+
const rowDistance = Math.abs(from.row - to.row);
|
|
2022
|
+
const colDistance = Math.abs(from.col - to.col);
|
|
2023
|
+
|
|
2024
|
+
return options.metric === "chebyshev"
|
|
2025
|
+
? Math.max(rowDistance, colDistance)
|
|
2026
|
+
: rowDistance + colDistance;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
export function getBoardsByTypeId<
|
|
2030
|
+
Table extends RuntimeTableRecord,
|
|
2031
|
+
TypeId extends BoardTypeIdOfTable<NoInfer<Table>>,
|
|
2032
|
+
>(table: Table, typeId: TypeId): BoardIdOfTable<Table>[] {
|
|
2033
|
+
return Object.entries(table.boards.byId)
|
|
2034
|
+
.filter(([, board]) => board.typeId === typeId)
|
|
2035
|
+
.map(([boardId]) => boardId as BoardIdOfTable<Table>);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
export function getSpacesByTypeId<
|
|
2039
|
+
Table extends RuntimeTableRecord,
|
|
2040
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
2041
|
+
TypeId extends SpaceTypeIdOfTable<NoInfer<Table>, BoardId>,
|
|
2042
|
+
>(
|
|
2043
|
+
table: Table,
|
|
2044
|
+
boardId: BoardId,
|
|
2045
|
+
typeId: TypeId,
|
|
2046
|
+
): SpaceIdOfTable<Table, BoardId>[] {
|
|
2047
|
+
return Object.entries(getBoard(table, boardId).spaces)
|
|
2048
|
+
.filter(([, space]) => space.typeId === typeId)
|
|
2049
|
+
.map(([spaceId]) => spaceId as SpaceIdOfTable<Table, BoardId>);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
export function getEdgesByTypeId<
|
|
2053
|
+
Table extends RuntimeTableRecord,
|
|
2054
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
2055
|
+
TypeId extends TiledEdgeTypeIdOfTable<NoInfer<Table>, BoardId>,
|
|
2056
|
+
>(
|
|
2057
|
+
table: Table,
|
|
2058
|
+
boardId: BoardId,
|
|
2059
|
+
typeId: TypeId,
|
|
2060
|
+
): TiledEdgeIdOfTable<Table, BoardId>[] {
|
|
2061
|
+
return getTiledBoard(table, boardId)
|
|
2062
|
+
.edges.filter((edge) => edge.typeId === typeId)
|
|
2063
|
+
.map((edge) => edge.id as TiledEdgeIdOfTable<Table, BoardId>);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
export function getVerticesByTypeId<
|
|
2067
|
+
Table extends RuntimeTableRecord,
|
|
2068
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
2069
|
+
TypeId extends TiledVertexTypeIdOfTable<NoInfer<Table>, BoardId>,
|
|
2070
|
+
>(
|
|
2071
|
+
table: Table,
|
|
2072
|
+
boardId: BoardId,
|
|
2073
|
+
typeId: TypeId,
|
|
2074
|
+
): TiledVertexIdOfTable<Table, BoardId>[] {
|
|
2075
|
+
return getTiledBoard(table, boardId)
|
|
2076
|
+
.vertices.filter((vertex) => vertex.typeId === typeId)
|
|
2077
|
+
.map((vertex) => vertex.id as TiledVertexIdOfTable<Table, BoardId>);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
export function getComponentsOnSpace<
|
|
2081
|
+
Table extends RuntimeTableRecord,
|
|
2082
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
2083
|
+
SpaceId extends SpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
2084
|
+
>(
|
|
2085
|
+
table: Table,
|
|
2086
|
+
boardId: BoardId,
|
|
2087
|
+
spaceId: SpaceId,
|
|
2088
|
+
): ComponentIdOfTable<Table>[] {
|
|
2089
|
+
const zoneId = getSpace(table, boardId, spaceId).zoneId;
|
|
2090
|
+
return orderedComponentIdsForLocation(
|
|
2091
|
+
table,
|
|
2092
|
+
(location) =>
|
|
2093
|
+
(location.type === "OnSpace" &&
|
|
2094
|
+
location.boardId === boardId &&
|
|
2095
|
+
location.spaceId === spaceId) ||
|
|
2096
|
+
(location.type === "InZone" &&
|
|
2097
|
+
typeof zoneId === "string" &&
|
|
2098
|
+
zoneId.length > 0 &&
|
|
2099
|
+
location.zoneId === zoneId),
|
|
2100
|
+
) as ComponentIdOfTable<Table>[];
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
export function getComponentsInContainer<
|
|
2104
|
+
Table extends RuntimeTableRecord,
|
|
2105
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
2106
|
+
ContainerId extends BoardContainerIdOfTable<NoInfer<Table>, BoardId>,
|
|
2107
|
+
>(
|
|
2108
|
+
table: Table,
|
|
2109
|
+
boardId: BoardId,
|
|
2110
|
+
containerId: ContainerId,
|
|
2111
|
+
): ComponentIdOfTable<Table>[] {
|
|
2112
|
+
const zoneId = getContainer(table, boardId, containerId).zoneId;
|
|
2113
|
+
return orderedComponentIdsForLocation(
|
|
2114
|
+
table,
|
|
2115
|
+
(location) =>
|
|
2116
|
+
(location.type === "InContainer" &&
|
|
2117
|
+
location.boardId === boardId &&
|
|
2118
|
+
location.containerId === containerId) ||
|
|
2119
|
+
(location.type === "InZone" &&
|
|
2120
|
+
typeof zoneId === "string" &&
|
|
2121
|
+
zoneId.length > 0 &&
|
|
2122
|
+
location.zoneId === zoneId),
|
|
2123
|
+
) as ComponentIdOfTable<Table>[];
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
export function getComponentsOnEdge<
|
|
2127
|
+
Table extends RuntimeTableRecord,
|
|
2128
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
2129
|
+
EdgeId extends TiledEdgeIdOfTable<NoInfer<Table>, BoardId>,
|
|
2130
|
+
>(table: Table, boardId: BoardId, edgeId: EdgeId): ComponentIdOfTable<Table>[] {
|
|
2131
|
+
return orderedComponentIdsForLocation(
|
|
2132
|
+
table,
|
|
2133
|
+
(location) =>
|
|
2134
|
+
location.type === "OnEdge" &&
|
|
2135
|
+
location.boardId === boardId &&
|
|
2136
|
+
location.edgeId === edgeId,
|
|
2137
|
+
) as ComponentIdOfTable<Table>[];
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
export function getComponentsOnVertex<
|
|
2141
|
+
Table extends RuntimeTableRecord,
|
|
2142
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
2143
|
+
VertexId extends TiledVertexIdOfTable<NoInfer<Table>, BoardId>,
|
|
2144
|
+
>(
|
|
2145
|
+
table: Table,
|
|
2146
|
+
boardId: BoardId,
|
|
2147
|
+
vertexId: VertexId,
|
|
2148
|
+
): ComponentIdOfTable<Table>[] {
|
|
2149
|
+
return orderedComponentIdsForLocation(
|
|
2150
|
+
table,
|
|
2151
|
+
(location) =>
|
|
2152
|
+
location.type === "OnVertex" &&
|
|
2153
|
+
location.boardId === boardId &&
|
|
2154
|
+
location.vertexId === vertexId,
|
|
2155
|
+
) as ComponentIdOfTable<Table>[];
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
export function moveComponentToSpace<
|
|
2159
|
+
Table extends RuntimeTableRecord,
|
|
2160
|
+
ComponentId extends ComponentIdOfTable<Table>,
|
|
2161
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
2162
|
+
SpaceId extends SpaceIdOfTable<NoInfer<Table>, BoardId>,
|
|
2163
|
+
>(
|
|
2164
|
+
table: Table,
|
|
2165
|
+
componentId: ComponentId,
|
|
2166
|
+
boardId: BoardId,
|
|
2167
|
+
spaceId: SpaceId,
|
|
2168
|
+
): Table {
|
|
2169
|
+
const nextTable = cloneRuntimeTable(table);
|
|
2170
|
+
const position = getComponentsOnSpace(nextTable, boardId, spaceId).length;
|
|
2171
|
+
removeComponentFromCurrentLocation(nextTable, componentId);
|
|
2172
|
+
nextTable.componentLocations[componentId] = {
|
|
2173
|
+
type: "OnSpace",
|
|
2174
|
+
boardId,
|
|
2175
|
+
spaceId,
|
|
2176
|
+
position,
|
|
2177
|
+
};
|
|
2178
|
+
return nextTable;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
export function moveComponentToContainer<
|
|
2182
|
+
Table extends RuntimeTableRecord,
|
|
2183
|
+
ComponentId extends ComponentIdOfTable<Table>,
|
|
2184
|
+
BoardId extends BoardIdOfTable<NoInfer<Table>>,
|
|
2185
|
+
ContainerId extends BoardContainerIdOfTable<NoInfer<Table>, BoardId>,
|
|
2186
|
+
>(
|
|
2187
|
+
table: Table,
|
|
2188
|
+
componentId: ComponentId,
|
|
2189
|
+
boardId: BoardId,
|
|
2190
|
+
containerId: ContainerId,
|
|
2191
|
+
): Table {
|
|
2192
|
+
const nextTable = cloneRuntimeTable(table);
|
|
2193
|
+
assertCardAllowedInContainer(nextTable, boardId, containerId, componentId);
|
|
2194
|
+
const position = getComponentsInContainer(
|
|
2195
|
+
nextTable,
|
|
2196
|
+
boardId,
|
|
2197
|
+
containerId,
|
|
2198
|
+
).length;
|
|
2199
|
+
removeComponentFromCurrentLocation(nextTable, componentId);
|
|
2200
|
+
nextTable.componentLocations[componentId] = {
|
|
2201
|
+
type: "InContainer",
|
|
2202
|
+
boardId,
|
|
2203
|
+
containerId,
|
|
2204
|
+
position,
|
|
2205
|
+
};
|
|
2206
|
+
return nextTable;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
export function moveComponentToDetached<
|
|
2210
|
+
Table extends RuntimeTableRecord,
|
|
2211
|
+
ComponentId extends ComponentIdOfTable<Table>,
|
|
2212
|
+
>(table: Table, componentId: ComponentId): Table {
|
|
2213
|
+
const nextTable = cloneRuntimeTable(table);
|
|
2214
|
+
removeComponentFromCurrentLocation(nextTable, componentId);
|
|
2215
|
+
nextTable.componentLocations[componentId] = { type: "Detached" };
|
|
2216
|
+
return nextTable;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
export function moveComponentToEdge<
|
|
2220
|
+
Table extends RuntimeTableRecord,
|
|
2221
|
+
ComponentId extends ComponentIdOfTable<Table>,
|
|
2222
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
2223
|
+
EdgeId extends TiledEdgeIdOfTable<NoInfer<Table>, BoardId>,
|
|
2224
|
+
>(
|
|
2225
|
+
table: Table,
|
|
2226
|
+
componentId: ComponentId,
|
|
2227
|
+
boardId: BoardId,
|
|
2228
|
+
edgeId: EdgeId,
|
|
2229
|
+
): Table {
|
|
2230
|
+
const nextTable = cloneRuntimeTable(table);
|
|
2231
|
+
getEdge(nextTable, boardId, edgeId);
|
|
2232
|
+
const position = getComponentsOnEdge(nextTable, boardId, edgeId).length;
|
|
2233
|
+
removeComponentFromCurrentLocation(nextTable, componentId);
|
|
2234
|
+
nextTable.componentLocations[componentId] = {
|
|
2235
|
+
type: "OnEdge",
|
|
2236
|
+
boardId,
|
|
2237
|
+
edgeId,
|
|
2238
|
+
position,
|
|
2239
|
+
};
|
|
2240
|
+
return nextTable;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
export function moveComponentToVertex<
|
|
2244
|
+
Table extends RuntimeTableRecord,
|
|
2245
|
+
ComponentId extends ComponentIdOfTable<Table>,
|
|
2246
|
+
BoardId extends TiledBoardIdOfTable<NoInfer<Table>>,
|
|
2247
|
+
VertexId extends TiledVertexIdOfTable<NoInfer<Table>, BoardId>,
|
|
2248
|
+
>(
|
|
2249
|
+
table: Table,
|
|
2250
|
+
componentId: ComponentId,
|
|
2251
|
+
boardId: BoardId,
|
|
2252
|
+
vertexId: VertexId,
|
|
2253
|
+
): Table {
|
|
2254
|
+
const nextTable = cloneRuntimeTable(table);
|
|
2255
|
+
getVertex(nextTable, boardId, vertexId);
|
|
2256
|
+
const position = getComponentsOnVertex(nextTable, boardId, vertexId).length;
|
|
2257
|
+
removeComponentFromCurrentLocation(nextTable, componentId);
|
|
2258
|
+
nextTable.componentLocations[componentId] = {
|
|
2259
|
+
type: "OnVertex",
|
|
2260
|
+
boardId,
|
|
2261
|
+
vertexId,
|
|
2262
|
+
position,
|
|
2263
|
+
};
|
|
2264
|
+
return nextTable;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
export function moveCardFromPlayerZoneToSharedZone<
|
|
2268
|
+
Table extends RuntimeTableRecord,
|
|
2269
|
+
HandId extends PlayerZoneIdOfTable<Table>,
|
|
2270
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
2271
|
+
DeckId extends SharedZoneIdOfTable<Table>,
|
|
2272
|
+
>(options: {
|
|
2273
|
+
table: Table;
|
|
2274
|
+
playerId: PlayerId;
|
|
2275
|
+
fromZoneId: HandId;
|
|
2276
|
+
toZoneId: DeckId;
|
|
2277
|
+
cardId: CompatibleCardIdForHandAndDeck<Table, HandId, DeckId>;
|
|
2278
|
+
playedBy?: PlayerIdOfTable<Table> | null;
|
|
2279
|
+
position?: "top" | "bottom";
|
|
2280
|
+
}): Table {
|
|
2281
|
+
return moveFromHandToDeck({
|
|
2282
|
+
table: options.table,
|
|
2283
|
+
playerId: options.playerId,
|
|
2284
|
+
handId: options.fromZoneId,
|
|
2285
|
+
cardId: options.cardId,
|
|
2286
|
+
deckId: options.toZoneId,
|
|
2287
|
+
playedBy: options.playedBy,
|
|
2288
|
+
position: options.position,
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Move a named card from a shared zone (supply pile, deck) to a perPlayer zone
|
|
2294
|
+
* (e.g. discard, hand, in-play). Use this for the "gain" verb — distinct from
|
|
2295
|
+
* `dealCardsToPlayerZone`, which draws an unspecified count of top cards from
|
|
2296
|
+
* a deck. Owner flips to the receiving player; visibility is recomputed from
|
|
2297
|
+
* the destination zone's `handVisibility` mode.
|
|
2298
|
+
*/
|
|
2299
|
+
/**
|
|
2300
|
+
* Move the top `count` cards from one perPlayer zone to another for the same
|
|
2301
|
+
* player. Companion to {@link dealCardsFromDeckToHand} for the perPlayer →
|
|
2302
|
+
* perPlayer case (most importantly, "draw N from your deck to your hand"
|
|
2303
|
+
* in deck-builders). Visibility is recomputed for the destination; owner is
|
|
2304
|
+
* preserved.
|
|
2305
|
+
*/
|
|
2306
|
+
export function dealCardsBetweenPlayerZones<
|
|
2307
|
+
Table extends RuntimeTableRecord,
|
|
2308
|
+
FromZoneId extends PlayerZoneIdOfTable<Table>,
|
|
2309
|
+
ToZoneId extends PlayerZoneIdOfTable<Table>,
|
|
2310
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
2311
|
+
>(options: {
|
|
2312
|
+
table: Table;
|
|
2313
|
+
playerId: PlayerId;
|
|
2314
|
+
fromZoneId: FromZoneId;
|
|
2315
|
+
toZoneId: ToZoneId;
|
|
2316
|
+
count: number;
|
|
2317
|
+
}): Table {
|
|
2318
|
+
let nextTable = options.table;
|
|
2319
|
+
for (let i = 0; i < options.count; i += 1) {
|
|
2320
|
+
const sourceCards = ensureArray(
|
|
2321
|
+
ppRead(
|
|
2322
|
+
nextTable.zones.perPlayer[options.fromZoneId] ??
|
|
2323
|
+
nextTable.hands[options.fromZoneId],
|
|
2324
|
+
options.playerId as string,
|
|
2325
|
+
) as readonly string[] | undefined,
|
|
2326
|
+
);
|
|
2327
|
+
const topCardId = sourceCards[0];
|
|
2328
|
+
if (topCardId === undefined) {
|
|
2329
|
+
break;
|
|
2330
|
+
}
|
|
2331
|
+
nextTable = moveCardBetweenPlayerZones({
|
|
2332
|
+
table: nextTable,
|
|
2333
|
+
playerId: options.playerId,
|
|
2334
|
+
fromZoneId: options.fromZoneId,
|
|
2335
|
+
toZoneId: options.toZoneId,
|
|
2336
|
+
cardId: topCardId as CompatibleCardIdForTwoPlayerZones<
|
|
2337
|
+
Table,
|
|
2338
|
+
FromZoneId,
|
|
2339
|
+
ToZoneId
|
|
2340
|
+
>,
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
return nextTable;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
export function moveCardFromSharedZoneToPlayerZone<
|
|
2347
|
+
Table extends RuntimeTableRecord,
|
|
2348
|
+
FromZoneId extends SharedZoneIdOfTable<Table>,
|
|
2349
|
+
ToZoneId extends PlayerZoneIdOfTable<Table>,
|
|
2350
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
2351
|
+
>(options: {
|
|
2352
|
+
table: Table;
|
|
2353
|
+
playerId: PlayerId;
|
|
2354
|
+
fromZoneId: FromZoneId;
|
|
2355
|
+
toZoneId: ToZoneId;
|
|
2356
|
+
cardId: CompatibleCardIdForHandAndDeck<Table, ToZoneId, FromZoneId>;
|
|
2357
|
+
position?: "top" | "bottom";
|
|
2358
|
+
}): Table {
|
|
2359
|
+
const nextTable = cloneRuntimeTable(options.table);
|
|
2360
|
+
assertZoneScope(
|
|
2361
|
+
nextTable,
|
|
2362
|
+
options.fromZoneId as string,
|
|
2363
|
+
"shared",
|
|
2364
|
+
"moveCardFromSharedZoneToPlayerZone",
|
|
2365
|
+
"fromZoneId",
|
|
2366
|
+
);
|
|
2367
|
+
assertZoneScope(
|
|
2368
|
+
nextTable,
|
|
2369
|
+
options.toZoneId as string,
|
|
2370
|
+
"perPlayer",
|
|
2371
|
+
"moveCardFromSharedZoneToPlayerZone",
|
|
2372
|
+
"toZoneId",
|
|
2373
|
+
);
|
|
2374
|
+
|
|
2375
|
+
const sourceCards = ensureArray(
|
|
2376
|
+
nextTable.zones.shared[options.fromZoneId] ??
|
|
2377
|
+
nextTable.decks[options.fromZoneId],
|
|
2378
|
+
);
|
|
2379
|
+
if (!sourceCards.includes(options.cardId as string)) {
|
|
2380
|
+
throw new Error(
|
|
2381
|
+
`Card '${String(options.cardId)}' is not in shared zone '${String(
|
|
2382
|
+
options.fromZoneId,
|
|
2383
|
+
)}'.`,
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
assertCardAllowedInZone(
|
|
2387
|
+
nextTable,
|
|
2388
|
+
options.toZoneId as string,
|
|
2389
|
+
options.cardId as string,
|
|
2390
|
+
);
|
|
2391
|
+
|
|
2392
|
+
const remainingSource = sourceCards.filter(
|
|
2393
|
+
(candidate) => candidate !== options.cardId,
|
|
2394
|
+
);
|
|
2395
|
+
syncSharedZoneWithDeck(nextTable, options.fromZoneId, remainingSource);
|
|
2396
|
+
for (const [index, currentCardId] of remainingSource.entries()) {
|
|
2397
|
+
const existing = nextTable.componentLocations[currentCardId];
|
|
2398
|
+
if (existing?.type === "InDeck") {
|
|
2399
|
+
nextTable.componentLocations[currentCardId] = {
|
|
2400
|
+
...existing,
|
|
2401
|
+
position: index,
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
const destinationCards = ensureArray(
|
|
2407
|
+
ppRead(
|
|
2408
|
+
nextTable.zones.perPlayer[options.toZoneId] ??
|
|
2409
|
+
nextTable.hands[options.toZoneId],
|
|
2410
|
+
options.playerId as string,
|
|
2411
|
+
) as readonly string[] | undefined,
|
|
2412
|
+
);
|
|
2413
|
+
const nextDestination =
|
|
2414
|
+
options.position === "top"
|
|
2415
|
+
? [options.cardId as string, ...destinationCards]
|
|
2416
|
+
: [...destinationCards, options.cardId as string];
|
|
2417
|
+
syncPlayerZoneWithHand(
|
|
2418
|
+
nextTable,
|
|
2419
|
+
options.toZoneId,
|
|
2420
|
+
options.playerId,
|
|
2421
|
+
nextDestination,
|
|
2422
|
+
);
|
|
2423
|
+
for (const [index, currentCardId] of nextDestination.entries()) {
|
|
2424
|
+
nextTable.componentLocations[currentCardId] = {
|
|
2425
|
+
type: "InHand",
|
|
2426
|
+
handId: options.toZoneId as string,
|
|
2427
|
+
playerId: options.playerId as string,
|
|
2428
|
+
position: index,
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
nextTable.ownerOfCard[options.cardId as string] = options.playerId as string;
|
|
2432
|
+
nextTable.visibility[options.cardId as string] =
|
|
2433
|
+
computeVisibilityForPlayerZone(
|
|
2434
|
+
nextTable,
|
|
2435
|
+
options.toZoneId as string,
|
|
2436
|
+
options.playerId as string,
|
|
2437
|
+
);
|
|
2438
|
+
|
|
2439
|
+
return nextTable;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/**
|
|
2443
|
+
* Move a card between two perPlayer zones owned by the same player. Use this
|
|
2444
|
+
* for hand → in-play, in-play → discard, and similar same-player transitions
|
|
2445
|
+
* that do not change ownership. Owner is preserved; visibility is recomputed
|
|
2446
|
+
* from the destination zone's `handVisibility` mode.
|
|
2447
|
+
*/
|
|
2448
|
+
export function moveCardBetweenPlayerZones<
|
|
2449
|
+
Table extends RuntimeTableRecord,
|
|
2450
|
+
FromZoneId extends PlayerZoneIdOfTable<Table>,
|
|
2451
|
+
ToZoneId extends PlayerZoneIdOfTable<Table>,
|
|
2452
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
2453
|
+
>(options: {
|
|
2454
|
+
table: Table;
|
|
2455
|
+
playerId: PlayerId;
|
|
2456
|
+
fromZoneId: FromZoneId;
|
|
2457
|
+
toZoneId: ToZoneId;
|
|
2458
|
+
cardId: CompatibleCardIdForTwoPlayerZones<Table, FromZoneId, ToZoneId>;
|
|
2459
|
+
position?: "top" | "bottom";
|
|
2460
|
+
}): Table {
|
|
2461
|
+
const nextTable = cloneRuntimeTable(options.table);
|
|
2462
|
+
assertZoneScope(
|
|
2463
|
+
nextTable,
|
|
2464
|
+
options.fromZoneId as string,
|
|
2465
|
+
"perPlayer",
|
|
2466
|
+
"moveCardBetweenPlayerZones",
|
|
2467
|
+
"fromZoneId",
|
|
2468
|
+
);
|
|
2469
|
+
assertZoneScope(
|
|
2470
|
+
nextTable,
|
|
2471
|
+
options.toZoneId as string,
|
|
2472
|
+
"perPlayer",
|
|
2473
|
+
"moveCardBetweenPlayerZones",
|
|
2474
|
+
"toZoneId",
|
|
2475
|
+
);
|
|
2476
|
+
|
|
2477
|
+
const sourceCards = ensureArray(
|
|
2478
|
+
ppRead(
|
|
2479
|
+
nextTable.zones.perPlayer[options.fromZoneId] ??
|
|
2480
|
+
nextTable.hands[options.fromZoneId],
|
|
2481
|
+
options.playerId as string,
|
|
2482
|
+
) as readonly string[] | undefined,
|
|
2483
|
+
);
|
|
2484
|
+
if (!sourceCards.includes(options.cardId as string)) {
|
|
2485
|
+
throw new Error(
|
|
2486
|
+
`Card '${String(options.cardId)}' is not in zone '${String(
|
|
2487
|
+
options.fromZoneId,
|
|
2488
|
+
)}' for player '${String(options.playerId)}'.`,
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
assertCardAllowedInZone(
|
|
2492
|
+
nextTable,
|
|
2493
|
+
options.toZoneId as string,
|
|
2494
|
+
options.cardId as string,
|
|
2495
|
+
);
|
|
2496
|
+
|
|
2497
|
+
const remainingSource = sourceCards.filter(
|
|
2498
|
+
(candidate) => candidate !== options.cardId,
|
|
2499
|
+
);
|
|
2500
|
+
syncPlayerZoneWithHand(
|
|
2501
|
+
nextTable,
|
|
2502
|
+
options.fromZoneId,
|
|
2503
|
+
options.playerId,
|
|
2504
|
+
remainingSource,
|
|
2505
|
+
);
|
|
2506
|
+
for (const [index, currentCardId] of remainingSource.entries()) {
|
|
2507
|
+
const existing = nextTable.componentLocations[currentCardId];
|
|
2508
|
+
if (existing?.type === "InHand") {
|
|
2509
|
+
nextTable.componentLocations[currentCardId] = {
|
|
2510
|
+
...existing,
|
|
2511
|
+
position: index,
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
const destinationCards = ensureArray(
|
|
2517
|
+
ppRead(
|
|
2518
|
+
nextTable.zones.perPlayer[options.toZoneId] ??
|
|
2519
|
+
nextTable.hands[options.toZoneId],
|
|
2520
|
+
options.playerId as string,
|
|
2521
|
+
) as readonly string[] | undefined,
|
|
2522
|
+
);
|
|
2523
|
+
const nextDestination =
|
|
2524
|
+
options.position === "top"
|
|
2525
|
+
? [options.cardId as string, ...destinationCards]
|
|
2526
|
+
: [...destinationCards, options.cardId as string];
|
|
2527
|
+
syncPlayerZoneWithHand(
|
|
2528
|
+
nextTable,
|
|
2529
|
+
options.toZoneId,
|
|
2530
|
+
options.playerId,
|
|
2531
|
+
nextDestination,
|
|
2532
|
+
);
|
|
2533
|
+
for (const [index, currentCardId] of nextDestination.entries()) {
|
|
2534
|
+
nextTable.componentLocations[currentCardId] = {
|
|
2535
|
+
type: "InHand",
|
|
2536
|
+
handId: options.toZoneId as string,
|
|
2537
|
+
playerId: options.playerId as string,
|
|
2538
|
+
position: index,
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
nextTable.visibility[options.cardId as string] =
|
|
2542
|
+
computeVisibilityForPlayerZone(
|
|
2543
|
+
nextTable,
|
|
2544
|
+
options.toZoneId as string,
|
|
2545
|
+
options.playerId as string,
|
|
2546
|
+
);
|
|
2547
|
+
|
|
2548
|
+
return nextTable;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
export function moveCardBetweenSharedZones<
|
|
2552
|
+
Table extends RuntimeTableRecord,
|
|
2553
|
+
FromZoneId extends SharedZoneIdOfTable<Table>,
|
|
2554
|
+
ToZoneId extends SharedZoneIdOfTable<Table>,
|
|
2555
|
+
>(options: {
|
|
2556
|
+
table: Table;
|
|
2557
|
+
fromZoneId: FromZoneId;
|
|
2558
|
+
toZoneId: ToZoneId;
|
|
2559
|
+
cardId: DeckCardsOfTable<Table, FromZoneId>[number];
|
|
2560
|
+
playedBy?: PlayerIdOfTable<Table> | null;
|
|
2561
|
+
position?: "top" | "bottom";
|
|
2562
|
+
}): Table {
|
|
2563
|
+
const removed = removeFromDeck(
|
|
2564
|
+
options.table,
|
|
2565
|
+
options.fromZoneId,
|
|
2566
|
+
options.cardId,
|
|
2567
|
+
);
|
|
2568
|
+
return appendToDeck(
|
|
2569
|
+
removed,
|
|
2570
|
+
options.toZoneId,
|
|
2571
|
+
options.cardId as DeckCardsOfTable<Table, ToZoneId>[number],
|
|
2572
|
+
options.playedBy ?? null,
|
|
2573
|
+
options.position ?? "bottom",
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
export function removeCardFromSharedZone<
|
|
2578
|
+
Table extends RuntimeTableRecord,
|
|
2579
|
+
DeckId extends DeckIdOfTable<Table>,
|
|
2580
|
+
>(
|
|
2581
|
+
table: Table,
|
|
2582
|
+
deckId: DeckId,
|
|
2583
|
+
cardId: DeckCardsOfTable<Table, DeckId>[number],
|
|
2584
|
+
): Table {
|
|
2585
|
+
return removeFromDeck(table, deckId, cardId);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
export function addCardToSharedZone<
|
|
2589
|
+
Table extends RuntimeTableRecord,
|
|
2590
|
+
DeckId extends DeckIdOfTable<Table>,
|
|
2591
|
+
>(
|
|
2592
|
+
table: Table,
|
|
2593
|
+
deckId: DeckId,
|
|
2594
|
+
cardId: DeckCardsOfTable<Table, DeckId>[number],
|
|
2595
|
+
playedBy: PlayerIdOfTable<Table> | null = null,
|
|
2596
|
+
position: "top" | "bottom" = "bottom",
|
|
2597
|
+
): Table {
|
|
2598
|
+
return appendToDeck(table, deckId, cardId, playedBy, position);
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* Deterministically draw the top `count` cards from a shared deck and deal
|
|
2603
|
+
* them into a player's hand zone. No RNG is consumed — call
|
|
2604
|
+
* {@link shuffleSharedZone} (via `fx.shuffleSharedZone`) first if the deck
|
|
2605
|
+
* needs randomising.
|
|
2606
|
+
*/
|
|
2607
|
+
export function dealCardsFromDeckToHand<
|
|
2608
|
+
Table extends RuntimeTableRecord,
|
|
2609
|
+
DeckId extends DeckIdOfTable<Table>,
|
|
2610
|
+
PlayerId extends PlayerIdOfTable<Table>,
|
|
2611
|
+
HandId extends HandIdOfTable<Table>,
|
|
2612
|
+
>(
|
|
2613
|
+
table: Table,
|
|
2614
|
+
fromZoneId: DeckId,
|
|
2615
|
+
playerId: PlayerId,
|
|
2616
|
+
toZoneId: HandId,
|
|
2617
|
+
count: number,
|
|
2618
|
+
): Table {
|
|
2619
|
+
const nextTable = cloneRuntimeTable(table);
|
|
2620
|
+
const publicHands = new Set(
|
|
2621
|
+
Object.entries(nextTable.handVisibility)
|
|
2622
|
+
.filter(([, mode]) => mode === "all" || mode === "public")
|
|
2623
|
+
.map(([handId]) => handId),
|
|
2624
|
+
);
|
|
2625
|
+
|
|
2626
|
+
for (let index = 0; index < count; index += 1) {
|
|
2627
|
+
const nextCard = ensureArray(nextTable.decks[fromZoneId])[0];
|
|
2628
|
+
if (!nextCard) {
|
|
2629
|
+
break;
|
|
2630
|
+
}
|
|
2631
|
+
nextTable.decks[fromZoneId] = ensureArray(
|
|
2632
|
+
nextTable.decks[fromZoneId],
|
|
2633
|
+
).slice(1) as (typeof nextTable.decks)[DeckId];
|
|
2634
|
+
nextTable.zones.shared[fromZoneId] = [
|
|
2635
|
+
...ensureArray(nextTable.decks[fromZoneId]),
|
|
2636
|
+
] as (typeof nextTable.zones.shared)[DeckId];
|
|
2637
|
+
const prevHand = ppRead(nextTable.hands[toZoneId], playerId as string) as
|
|
2638
|
+
| readonly string[]
|
|
2639
|
+
| undefined;
|
|
2640
|
+
const nextHand = [...ensureArray(prevHand), nextCard];
|
|
2641
|
+
assertCardAllowedInZone(nextTable, toZoneId, nextCard);
|
|
2642
|
+
nextTable.hands[toZoneId] = ppWrite(
|
|
2643
|
+
nextTable.hands[toZoneId],
|
|
2644
|
+
playerId as string,
|
|
2645
|
+
nextHand,
|
|
2646
|
+
) as (typeof nextTable.hands)[HandId];
|
|
2647
|
+
nextTable.zones.perPlayer[toZoneId] = ppWrite(
|
|
2648
|
+
nextTable.zones.perPlayer[toZoneId],
|
|
2649
|
+
playerId as string,
|
|
2650
|
+
[...nextHand],
|
|
2651
|
+
) as (typeof nextTable.zones.perPlayer)[HandId];
|
|
2652
|
+
nextTable.componentLocations[nextCard] = {
|
|
2653
|
+
type: "InHand",
|
|
2654
|
+
handId: toZoneId,
|
|
2655
|
+
playerId,
|
|
2656
|
+
position: nextHand.length - 1,
|
|
2657
|
+
};
|
|
2658
|
+
nextTable.ownerOfCard[nextCard] = playerId;
|
|
2659
|
+
nextTable.visibility[nextCard] = publicHands.has(toZoneId as string)
|
|
2660
|
+
? {
|
|
2661
|
+
faceUp: true,
|
|
2662
|
+
}
|
|
2663
|
+
: {
|
|
2664
|
+
faceUp: false,
|
|
2665
|
+
visibleTo: [playerId],
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
return nextTable;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
export type { RuntimeTableRecord, TableOfState, CardIdOfState };
|