@dreamboard-games/ui-sdk 0.0.43 → 0.0.45
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/dist/components/ActionButton.d.ts.map +1 -1
- package/dist/components/ActionButton.js +2 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/DiceRoller.d.ts +3 -2
- package/dist/components/DiceRoller.d.ts.map +1 -1
- package/dist/components/DiceRoller.js +4 -13
- package/dist/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/ErrorBoundary.js +94 -2
- package/dist/components/InteractionForm.d.ts +1 -1
- package/dist/components/InteractionForm.d.ts.map +1 -1
- package/dist/components/InteractionForm.js +29 -15
- package/dist/components/PrimaryActionButton.d.ts.map +1 -1
- package/dist/components/PrimaryActionButton.js +7 -6
- package/dist/components/ResourceCounter.d.ts +59 -25
- package/dist/components/ResourceCounter.d.ts.map +1 -1
- package/dist/components/ResourceCounter.js +106 -115
- package/dist/components/Toast.d.ts +13 -6
- package/dist/components/Toast.d.ts.map +1 -1
- package/dist/components/Toast.js +10 -5
- package/dist/components/board/HexGrid.js +6 -6
- package/dist/components/board/target-layer.d.ts +18 -2
- package/dist/components/board/target-layer.d.ts.map +1 -1
- package/dist/components/board/target-layer.js +20 -3
- package/dist/components/index.d.ts +3 -4
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -4
- package/dist/components/surfaces/InboxSurface.d.ts.map +1 -1
- package/dist/components/surfaces/InboxSurface.js +2 -6
- package/dist/components/surfaces/PlayerCardsSurface.js +2 -2
- package/dist/components/surfaces/internal/CardZoneRoutedForm.d.ts +7 -0
- package/dist/components/surfaces/internal/CardZoneRoutedForm.d.ts.map +1 -0
- package/dist/components/surfaces/internal/CardZoneRoutedForm.js +9 -0
- package/dist/components/surfaces/internal/DefaultInteractionButton.d.ts.map +1 -1
- package/dist/components/surfaces/internal/DefaultInteractionButton.js +5 -8
- package/dist/components/surfaces/internal/useCardZoneInteractions.d.ts +2 -2
- package/dist/components/surfaces/internal/useCardZoneInteractions.d.ts.map +1 -1
- package/dist/components/surfaces/internal/useCardZoneInteractions.js +19 -43
- package/dist/context/InteractionDraftContext.d.ts +11 -2
- package/dist/context/InteractionDraftContext.d.ts.map +1 -1
- package/dist/context/InteractionDraftContext.js +41 -4
- package/dist/defaults/components.d.ts +0 -5
- package/dist/defaults/components.d.ts.map +1 -1
- package/dist/defaults/components.js +7 -11
- package/dist/hooks/useBoardInteractions.d.ts +35 -12
- package/dist/hooks/useBoardInteractions.d.ts.map +1 -1
- package/dist/hooks/useBoardInteractions.js +186 -82
- package/dist/hooks/useInteractionHandle.d.ts +1 -1
- package/dist/hooks/useInteractionHandle.d.ts.map +1 -1
- package/dist/hooks/useInteractionHandle.js +12 -27
- package/dist/index.d.ts +11 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -14
- package/dist/primitives/board.d.ts +53 -3
- package/dist/primitives/board.d.ts.map +1 -1
- package/dist/primitives/board.js +65 -41
- package/dist/primitives/dialog-lifecycle.d.ts +17 -0
- package/dist/primitives/dialog-lifecycle.d.ts.map +1 -0
- package/dist/primitives/dialog-lifecycle.js +24 -0
- package/dist/primitives/dice.d.ts +31 -0
- package/dist/primitives/dice.d.ts.map +1 -0
- package/dist/primitives/dice.js +33 -0
- package/dist/primitives/game.d.ts +55 -0
- package/dist/primitives/game.d.ts.map +1 -0
- package/dist/primitives/game.js +101 -0
- package/dist/primitives/index.d.ts +7 -4
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +7 -4
- package/dist/primitives/interaction-form-binding.d.ts +12 -0
- package/dist/primitives/interaction-form-binding.d.ts.map +1 -0
- package/dist/primitives/interaction-form-binding.js +14 -0
- package/dist/primitives/interaction-submit.d.ts +23 -0
- package/dist/primitives/interaction-submit.d.ts.map +1 -0
- package/dist/primitives/interaction-submit.js +41 -0
- package/dist/primitives/interaction.d.ts +76 -6
- package/dist/primitives/interaction.d.ts.map +1 -1
- package/dist/primitives/interaction.js +210 -26
- package/dist/primitives/player-roster.d.ts +2 -1
- package/dist/primitives/player-roster.d.ts.map +1 -1
- package/dist/primitives/prompt.d.ts +36 -11
- package/dist/primitives/prompt.d.ts.map +1 -1
- package/dist/primitives/prompt.js +29 -17
- package/dist/primitives/ui.d.ts +9 -0
- package/dist/primitives/ui.d.ts.map +1 -0
- package/dist/primitives/ui.js +7 -0
- package/dist/primitives/zone.d.ts +111 -5
- package/dist/primitives/zone.d.ts.map +1 -1
- package/dist/primitives/zone.js +349 -9
- package/dist/reducer.d.ts +2 -14
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +1 -14
- package/dist/runtime/createPluginRuntimeAPI.js +1 -1
- package/dist/types/hex-color.d.ts +7 -0
- package/dist/types/hex-color.d.ts.map +1 -0
- package/dist/types/hex-color.js +13 -0
- package/dist/types/player-state.d.ts +28 -14
- package/dist/types/player-state.d.ts.map +1 -1
- package/dist/types/plugin-state.d.ts +9 -3
- package/dist/types/plugin-state.d.ts.map +1 -1
- package/dist/ui-contract.d.ts +119 -14
- package/dist/ui-contract.d.ts.map +1 -1
- package/dist/ui-contract.js +4 -3
- package/dist/ui-sdk.d.ts +1637 -1245
- package/dist/utils/interaction-inputs.d.ts +8 -5
- package/dist/utils/interaction-inputs.d.ts.map +1 -1
- package/dist/utils/interaction-inputs.js +82 -14
- package/dist/utils/interaction-router.d.ts +31 -0
- package/dist/utils/interaction-router.d.ts.map +1 -0
- package/dist/utils/interaction-router.js +114 -0
- package/package.json +1 -1
- package/src/components/ActionButton.tsx +2 -1
- package/src/components/Card.tsx +1 -1
- package/src/components/DiceRoller.tsx +13 -22
- package/src/components/ErrorBoundary.test.tsx +19 -0
- package/src/components/ErrorBoundary.tsx +113 -24
- package/src/components/InteractionForm.test.tsx +24 -0
- package/src/components/InteractionForm.tsx +48 -23
- package/src/components/PrimaryActionButton.tsx +19 -5
- package/src/components/ResourceCounter.test.tsx +13 -13
- package/src/components/ResourceCounter.tsx +238 -244
- package/src/components/Toast.tsx +23 -10
- package/src/components/__fixtures__/ResourceCounter.fixture.tsx +70 -169
- package/src/components/board/HexGrid.tsx +6 -6
- package/src/components/board/target-layer.ts +44 -5
- package/src/components/index.ts +17 -10
- package/src/components/surfaces/InboxSurface.tsx +7 -5
- package/src/components/surfaces/PlayerCardsSurface.tsx +6 -6
- package/src/components/surfaces/internal/CardZoneRoutedForm.tsx +35 -0
- package/src/components/surfaces/internal/DefaultInteractionButton.tsx +17 -7
- package/src/components/surfaces/internal/useCardZoneInteractions.ts +25 -67
- package/src/context/InteractionDraftContext.tsx +51 -5
- package/src/defaults/components.tsx +12 -50
- package/src/defaults/defaults.test.tsx +1 -50
- package/src/hooks/useBoardInteractions.test.tsx +240 -17
- package/src/hooks/useBoardInteractions.ts +330 -105
- package/src/hooks/useInteractionHandle.ts +23 -28
- package/src/index.test.ts +60 -40
- package/src/index.ts +30 -36
- package/src/primitives/board.test.tsx +73 -0
- package/src/primitives/board.tsx +191 -40
- package/src/primitives/dialog-lifecycle.ts +58 -0
- package/src/primitives/dice.test.tsx +47 -0
- package/src/primitives/dice.tsx +79 -0
- package/src/primitives/game.test.tsx +98 -0
- package/src/primitives/game.tsx +213 -0
- package/src/primitives/index.ts +84 -0
- package/src/primitives/interaction-form-binding.tsx +56 -0
- package/src/primitives/interaction-submit.ts +90 -0
- package/src/primitives/interaction.test.tsx +396 -0
- package/src/primitives/interaction.tsx +451 -31
- package/src/primitives/player-roster.tsx +2 -1
- package/src/primitives/prompt.test.tsx +94 -3
- package/src/primitives/prompt.tsx +87 -48
- package/src/primitives/ui.test.tsx +131 -0
- package/src/primitives/ui.tsx +13 -0
- package/src/primitives/zone.test.tsx +305 -0
- package/src/primitives/zone.tsx +660 -12
- package/src/reducer.ts +7 -20
- package/src/runtime/createPluginRuntimeAPI.ts +1 -1
- package/src/types/hex-color.ts +20 -0
- package/src/types/player-state.ts +36 -18
- package/src/types/plugin-state.ts +10 -3
- package/src/ui-contract.ts +253 -21
- package/src/utils/interaction-inputs.test.ts +400 -0
- package/src/utils/interaction-inputs.ts +113 -11
- package/src/utils/interaction-router.ts +200 -0
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
import type { InteractionDescriptor } from "../types/plugin-state.js";
|
|
3
3
|
import {
|
|
4
|
+
dependentInputKeys,
|
|
5
|
+
eligibleTargetsByBoardKind,
|
|
4
6
|
hasInteractionFieldErrors,
|
|
7
|
+
inputKeyForTarget,
|
|
8
|
+
resolveInteractionInputs,
|
|
5
9
|
validateInteractionInputDomains,
|
|
6
10
|
} from "./interaction-inputs.js";
|
|
11
|
+
import {
|
|
12
|
+
applyInteractionDraftMutation,
|
|
13
|
+
getInteractionDraftReadiness,
|
|
14
|
+
shouldRouteInteractionPending,
|
|
15
|
+
} from "./interaction-router.js";
|
|
7
16
|
|
|
8
17
|
function descriptor(
|
|
9
18
|
inputs: InteractionDescriptor["inputs"],
|
|
@@ -107,3 +116,394 @@ test("many target domains validate count, distinctness, and eligibility", () =>
|
|
|
107
116
|
),
|
|
108
117
|
).toBe(false);
|
|
109
118
|
});
|
|
119
|
+
|
|
120
|
+
test("dependent domains resolve from current draft and invalidate downstream keys", () => {
|
|
121
|
+
const interaction = descriptor([
|
|
122
|
+
{
|
|
123
|
+
key: "spaceId",
|
|
124
|
+
kind: "form",
|
|
125
|
+
domain: {
|
|
126
|
+
type: "choice",
|
|
127
|
+
choices: [
|
|
128
|
+
{ value: "hex-a", label: "Hex A" },
|
|
129
|
+
{ value: "hex-b", label: "Hex B" },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
key: "targetPlayerId",
|
|
135
|
+
kind: "form",
|
|
136
|
+
domain: {
|
|
137
|
+
type: "choice",
|
|
138
|
+
choices: [],
|
|
139
|
+
dependsOn: ["spaceId"],
|
|
140
|
+
dependentCases: [
|
|
141
|
+
{
|
|
142
|
+
when: { spaceId: "hex-a" },
|
|
143
|
+
domain: {
|
|
144
|
+
type: "choice",
|
|
145
|
+
choices: [{ value: "player-1", label: "Player 1" }],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
when: { spaceId: "hex-b" },
|
|
150
|
+
domain: {
|
|
151
|
+
type: "choice",
|
|
152
|
+
choices: [{ value: "player-2", label: "Player 2" }],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
const resolved = resolveInteractionInputs(interaction, { spaceId: "hex-b" });
|
|
161
|
+
expect(resolved[1]?.domain.choices).toEqual([
|
|
162
|
+
{ value: "player-2", label: "Player 2" },
|
|
163
|
+
]);
|
|
164
|
+
expect(
|
|
165
|
+
validateInteractionInputDomains(interaction, {
|
|
166
|
+
spaceId: "hex-b",
|
|
167
|
+
targetPlayerId: "player-1",
|
|
168
|
+
}),
|
|
169
|
+
).toEqual({
|
|
170
|
+
targetPlayerId: ["Selected choice is not eligible."],
|
|
171
|
+
});
|
|
172
|
+
expect(dependentInputKeys(interaction, "spaceId")).toEqual([
|
|
173
|
+
"targetPlayerId",
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("router draft mutation clears dependent inputs unless they are supplied together", () => {
|
|
178
|
+
const interaction = descriptor([
|
|
179
|
+
{
|
|
180
|
+
key: "spaceId",
|
|
181
|
+
kind: "form",
|
|
182
|
+
domain: {
|
|
183
|
+
type: "choice",
|
|
184
|
+
choices: [
|
|
185
|
+
{ value: "hex-a", label: "Hex A" },
|
|
186
|
+
{ value: "hex-b", label: "Hex B" },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
key: "targetPlayerId",
|
|
192
|
+
kind: "form",
|
|
193
|
+
domain: {
|
|
194
|
+
type: "choice",
|
|
195
|
+
choices: [],
|
|
196
|
+
dependsOn: ["spaceId"],
|
|
197
|
+
dependentCases: [
|
|
198
|
+
{
|
|
199
|
+
when: { spaceId: "hex-a" },
|
|
200
|
+
domain: {
|
|
201
|
+
type: "choice",
|
|
202
|
+
choices: [{ value: "player-1", label: "Player 1" }],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
when: { spaceId: "hex-b" },
|
|
207
|
+
domain: {
|
|
208
|
+
type: "choice",
|
|
209
|
+
choices: [{ value: "player-2", label: "Player 2" }],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
const draft: Record<string, unknown> = {
|
|
217
|
+
spaceId: "hex-a",
|
|
218
|
+
targetPlayerId: "player-1",
|
|
219
|
+
};
|
|
220
|
+
const store = {
|
|
221
|
+
getDraft: () => draft,
|
|
222
|
+
setInput: (_interactionKey: string, key: string, value: unknown) => {
|
|
223
|
+
draft[key] = value;
|
|
224
|
+
},
|
|
225
|
+
clearInput: (_interactionKey: string, key?: string) => {
|
|
226
|
+
if (key) delete draft[key];
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
expect(
|
|
231
|
+
applyInteractionDraftMutation(store, interaction, [
|
|
232
|
+
{ key: "spaceId", value: "hex-b" },
|
|
233
|
+
]),
|
|
234
|
+
).toEqual({ spaceId: "hex-b" });
|
|
235
|
+
expect(draft).toEqual({ spaceId: "hex-b" });
|
|
236
|
+
|
|
237
|
+
expect(
|
|
238
|
+
applyInteractionDraftMutation(store, interaction, [
|
|
239
|
+
{ key: "spaceId", value: "hex-a" },
|
|
240
|
+
{ key: "targetPlayerId", value: "player-1" },
|
|
241
|
+
]),
|
|
242
|
+
).toEqual({ spaceId: "hex-a", targetPlayerId: "player-1" });
|
|
243
|
+
expect(draft).toEqual({ spaceId: "hex-a", targetPlayerId: "player-1" });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("draft mutation preserves dependent target values that remain eligible", () => {
|
|
247
|
+
const interaction = descriptor([
|
|
248
|
+
{
|
|
249
|
+
key: "componentId",
|
|
250
|
+
kind: "form",
|
|
251
|
+
domain: {
|
|
252
|
+
type: "choice",
|
|
253
|
+
choices: [
|
|
254
|
+
{ value: "worker-1", label: "Worker 1" },
|
|
255
|
+
{ value: "worker-2", label: "Worker 2" },
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
key: "spaceId",
|
|
261
|
+
kind: "board-space",
|
|
262
|
+
domain: {
|
|
263
|
+
type: "target",
|
|
264
|
+
targetKind: "space",
|
|
265
|
+
eligibleTargets: ["lumberyard"],
|
|
266
|
+
dependsOn: ["componentId"],
|
|
267
|
+
dependentCases: [
|
|
268
|
+
{
|
|
269
|
+
when: { componentId: "worker-1" },
|
|
270
|
+
domain: {
|
|
271
|
+
type: "target",
|
|
272
|
+
targetKind: "space",
|
|
273
|
+
eligibleTargets: ["lumberyard"],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
when: { componentId: "worker-2" },
|
|
278
|
+
domain: {
|
|
279
|
+
type: "target",
|
|
280
|
+
targetKind: "space",
|
|
281
|
+
eligibleTargets: ["quarry"],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
]);
|
|
288
|
+
const draft: Record<string, unknown> = { spaceId: "lumberyard" };
|
|
289
|
+
const store = {
|
|
290
|
+
getDraft: () => draft,
|
|
291
|
+
setInput: (_interactionKey: string, key: string, value: unknown) => {
|
|
292
|
+
draft[key] = value;
|
|
293
|
+
},
|
|
294
|
+
clearInput: (_interactionKey: string, key?: string) => {
|
|
295
|
+
if (key) delete draft[key];
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
expect(
|
|
300
|
+
applyInteractionDraftMutation(store, interaction, [
|
|
301
|
+
{ key: "componentId", value: "worker-1" },
|
|
302
|
+
]),
|
|
303
|
+
).toEqual({ spaceId: "lumberyard", componentId: "worker-1" });
|
|
304
|
+
expect(draft).toEqual({ spaceId: "lumberyard", componentId: "worker-1" });
|
|
305
|
+
|
|
306
|
+
expect(
|
|
307
|
+
applyInteractionDraftMutation(store, interaction, [
|
|
308
|
+
{ key: "componentId", value: "worker-2" },
|
|
309
|
+
]),
|
|
310
|
+
).toEqual({ componentId: "worker-2" });
|
|
311
|
+
expect(draft).toEqual({ componentId: "worker-2" });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("router readiness includes defaults, dependent domains, and pending routing", () => {
|
|
315
|
+
const interaction = descriptor([
|
|
316
|
+
{
|
|
317
|
+
key: "spaceId",
|
|
318
|
+
kind: "form",
|
|
319
|
+
domain: {
|
|
320
|
+
type: "choice",
|
|
321
|
+
choices: [{ value: "hex-a", label: "Hex A" }],
|
|
322
|
+
},
|
|
323
|
+
defaultValue: "hex-a",
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
key: "targetPlayerId",
|
|
327
|
+
kind: "form",
|
|
328
|
+
domain: {
|
|
329
|
+
type: "choice",
|
|
330
|
+
choices: [{ value: "player-1", label: "Player 1" }],
|
|
331
|
+
dependsOn: ["spaceId"],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
]);
|
|
335
|
+
const missing = getInteractionDraftReadiness(interaction, {});
|
|
336
|
+
|
|
337
|
+
expect(missing.values).toEqual({ spaceId: "hex-a" });
|
|
338
|
+
expect(missing.missingInputs).toEqual(["targetPlayerId"]);
|
|
339
|
+
expect(missing.readyFrontier).toEqual(["targetPlayerId"]);
|
|
340
|
+
expect(missing.blockedInputs).toEqual([]);
|
|
341
|
+
expect(missing.ready).toBe(false);
|
|
342
|
+
expect(shouldRouteInteractionPending(interaction, missing)).toBe(true);
|
|
343
|
+
|
|
344
|
+
const ready = getInteractionDraftReadiness(interaction, {
|
|
345
|
+
targetPlayerId: "player-1",
|
|
346
|
+
});
|
|
347
|
+
expect(ready.ready).toBe(true);
|
|
348
|
+
expect(shouldRouteInteractionPending(interaction, ready)).toBe(true);
|
|
349
|
+
expect(
|
|
350
|
+
shouldRouteInteractionPending(
|
|
351
|
+
{ ...interaction, commit: { mode: "autoWhenReady" } },
|
|
352
|
+
ready,
|
|
353
|
+
),
|
|
354
|
+
).toBe(false);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("router readiness exposes only dependency-ready missing inputs in the frontier", () => {
|
|
358
|
+
const interaction = descriptor([
|
|
359
|
+
{
|
|
360
|
+
key: "itemId",
|
|
361
|
+
kind: "form",
|
|
362
|
+
domain: {
|
|
363
|
+
type: "choice",
|
|
364
|
+
choices: [
|
|
365
|
+
{ value: "anvil", label: "Anvil" },
|
|
366
|
+
{ value: "loom", label: "Loom" },
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
key: "cell",
|
|
372
|
+
kind: "board-space",
|
|
373
|
+
domain: {
|
|
374
|
+
type: "target",
|
|
375
|
+
targetKind: "space",
|
|
376
|
+
eligibleTargets: [],
|
|
377
|
+
dependsOn: ["itemId"],
|
|
378
|
+
dependentCases: [
|
|
379
|
+
{
|
|
380
|
+
when: { itemId: "anvil" },
|
|
381
|
+
domain: {
|
|
382
|
+
type: "target",
|
|
383
|
+
targetKind: "space",
|
|
384
|
+
eligibleTargets: ["workshop-a"],
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
when: { itemId: "loom" },
|
|
389
|
+
domain: {
|
|
390
|
+
type: "target",
|
|
391
|
+
targetKind: "space",
|
|
392
|
+
eligibleTargets: ["workshop-b"],
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
const initial = getInteractionDraftReadiness(interaction, {});
|
|
401
|
+
expect(initial.missingInputs).toEqual(["itemId", "cell"]);
|
|
402
|
+
expect(initial.readyFrontier).toEqual(["itemId"]);
|
|
403
|
+
expect(initial.blockedInputs).toEqual(["cell"]);
|
|
404
|
+
|
|
405
|
+
const afterItem = getInteractionDraftReadiness(interaction, {
|
|
406
|
+
itemId: "anvil",
|
|
407
|
+
});
|
|
408
|
+
expect(afterItem.missingInputs).toEqual(["cell"]);
|
|
409
|
+
expect(afterItem.readyFrontier).toEqual(["cell"]);
|
|
410
|
+
expect(afterItem.blockedInputs).toEqual([]);
|
|
411
|
+
expect(
|
|
412
|
+
resolveInteractionInputs(interaction, afterItem.values)[1]?.domain,
|
|
413
|
+
).toMatchObject({
|
|
414
|
+
type: "target",
|
|
415
|
+
targetKind: "space",
|
|
416
|
+
eligibleTargets: ["workshop-a"],
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("router readiness exposes all independent missing inputs in the same frontier", () => {
|
|
421
|
+
const interaction = descriptor([
|
|
422
|
+
{
|
|
423
|
+
key: "giveWood",
|
|
424
|
+
kind: "form",
|
|
425
|
+
domain: { type: "number", min: 0, max: 3 },
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
key: "giveStone",
|
|
429
|
+
kind: "form",
|
|
430
|
+
domain: { type: "number", min: 0, max: 3 },
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
key: "summary",
|
|
434
|
+
kind: "form",
|
|
435
|
+
domain: {
|
|
436
|
+
type: "choice",
|
|
437
|
+
choices: [{ value: "confirm", label: "Confirm" }],
|
|
438
|
+
dependsOn: ["giveWood", "giveStone"],
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
]);
|
|
442
|
+
|
|
443
|
+
const initial = getInteractionDraftReadiness(interaction, {});
|
|
444
|
+
expect(initial.missingInputs).toEqual(["giveWood", "giveStone", "summary"]);
|
|
445
|
+
expect(initial.readyFrontier).toEqual(["giveWood", "giveStone"]);
|
|
446
|
+
expect(initial.blockedInputs).toEqual(["summary"]);
|
|
447
|
+
|
|
448
|
+
const afterResources = getInteractionDraftReadiness(interaction, {
|
|
449
|
+
giveWood: 1,
|
|
450
|
+
giveStone: 2,
|
|
451
|
+
});
|
|
452
|
+
expect(afterResources.missingInputs).toEqual(["summary"]);
|
|
453
|
+
expect(afterResources.readyFrontier).toEqual(["summary"]);
|
|
454
|
+
expect(afterResources.blockedInputs).toEqual([]);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("target helpers resolve dependent domains against the current draft", () => {
|
|
458
|
+
const interaction = descriptor([
|
|
459
|
+
{
|
|
460
|
+
key: "itemId",
|
|
461
|
+
kind: "form",
|
|
462
|
+
domain: {
|
|
463
|
+
type: "choice",
|
|
464
|
+
choices: [{ value: "anvil", label: "Anvil" }],
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
key: "cell",
|
|
469
|
+
kind: "board-space",
|
|
470
|
+
domain: {
|
|
471
|
+
type: "target",
|
|
472
|
+
targetKind: "space",
|
|
473
|
+
eligibleTargets: [],
|
|
474
|
+
dependsOn: ["itemId"],
|
|
475
|
+
dependentCases: [
|
|
476
|
+
{
|
|
477
|
+
when: { itemId: "anvil" },
|
|
478
|
+
domain: {
|
|
479
|
+
type: "target",
|
|
480
|
+
targetKind: "space",
|
|
481
|
+
eligibleTargets: ["workshop-a"],
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
expect(interaction.inputs[1]?.domain).toMatchObject({
|
|
490
|
+
type: "target",
|
|
491
|
+
eligibleTargets: [],
|
|
492
|
+
});
|
|
493
|
+
expect(
|
|
494
|
+
resolveInteractionInputs(interaction, { itemId: "anvil" })[1]?.domain,
|
|
495
|
+
).toMatchObject({
|
|
496
|
+
type: "target",
|
|
497
|
+
eligibleTargets: ["workshop-a"],
|
|
498
|
+
});
|
|
499
|
+
expect(eligibleTargetsByBoardKind(interaction, {})).toEqual({});
|
|
500
|
+
expect(eligibleTargetsByBoardKind(interaction, { itemId: "anvil" })).toEqual({
|
|
501
|
+
space: ["workshop-a"],
|
|
502
|
+
});
|
|
503
|
+
expect(inputKeyForTarget(interaction, "space", "workshop-a", {})).toBeNull();
|
|
504
|
+
expect(
|
|
505
|
+
inputKeyForTarget(interaction, "space", "workshop-a", {
|
|
506
|
+
itemId: "anvil",
|
|
507
|
+
}),
|
|
508
|
+
).toBe("cell");
|
|
509
|
+
});
|
|
@@ -27,13 +27,70 @@ export function applyInteractionInputDefaults<
|
|
|
27
27
|
return next as Partial<Params>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function resolveInputDomain(
|
|
31
|
+
input: InteractionInputDescriptor,
|
|
32
|
+
params: Readonly<Record<string, unknown>>,
|
|
33
|
+
): InteractionInputDescriptor {
|
|
34
|
+
const dependencies = input.domain.dependsOn ?? [];
|
|
35
|
+
if (dependencies.length === 0) return input;
|
|
36
|
+
const caseMatch = input.domain.dependentCases?.find((candidate) =>
|
|
37
|
+
dependencies.every(
|
|
38
|
+
(key) =>
|
|
39
|
+
params[key] !== undefined &&
|
|
40
|
+
params[key] !== null &&
|
|
41
|
+
String(params[key]) === candidate.when[key],
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
if (!caseMatch) return input;
|
|
45
|
+
return {
|
|
46
|
+
...input,
|
|
47
|
+
domain: {
|
|
48
|
+
...caseMatch.domain,
|
|
49
|
+
dependsOn: dependencies,
|
|
50
|
+
dependentCases: input.domain.dependentCases,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveInteractionInputs(
|
|
56
|
+
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
57
|
+
params: Readonly<Record<string, unknown>>,
|
|
58
|
+
): InteractionInputDescriptor[] {
|
|
59
|
+
return descriptor.inputs.map((input) => resolveInputDomain(input, params));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function dependentInputKeys(
|
|
63
|
+
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
64
|
+
changedKey: string,
|
|
65
|
+
): string[] {
|
|
66
|
+
const dependentsByKey = new Map<string, string[]>();
|
|
67
|
+
for (const input of descriptor.inputs) {
|
|
68
|
+
for (const dependency of input.domain.dependsOn ?? []) {
|
|
69
|
+
dependentsByKey.set(dependency, [
|
|
70
|
+
...(dependentsByKey.get(dependency) ?? []),
|
|
71
|
+
input.key,
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const result: string[] = [];
|
|
76
|
+
const queue = [...(dependentsByKey.get(changedKey) ?? [])];
|
|
77
|
+
while (queue.length > 0) {
|
|
78
|
+
const key = queue.shift();
|
|
79
|
+
if (!key || result.includes(key)) continue;
|
|
80
|
+
result.push(key);
|
|
81
|
+
queue.push(...(dependentsByKey.get(key) ?? []));
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
30
86
|
export function validateInteractionInputDomains(
|
|
31
87
|
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
32
88
|
params: Readonly<Record<string, unknown>>,
|
|
33
89
|
): Partial<Record<string, readonly string[]>> {
|
|
34
90
|
const fieldErrors: Record<string, string[]> = {};
|
|
35
91
|
|
|
36
|
-
for (const
|
|
92
|
+
for (const rawInput of descriptor.inputs) {
|
|
93
|
+
const input = resolveInputDomain(rawInput, params);
|
|
37
94
|
const value = params[input.key];
|
|
38
95
|
if (value === undefined || value === null) continue;
|
|
39
96
|
|
|
@@ -67,6 +124,32 @@ export function validateInteractionInputDomains(
|
|
|
67
124
|
`Choose at most ${max} ${pluralize("option", max)}.`,
|
|
68
125
|
);
|
|
69
126
|
}
|
|
127
|
+
const allowed = new Set(
|
|
128
|
+
input.domain.choices?.map((choice) => choice.value),
|
|
129
|
+
);
|
|
130
|
+
if (
|
|
131
|
+
allowed.size > 0 &&
|
|
132
|
+
value.some((item) => !allowed.has(String(item)))
|
|
133
|
+
) {
|
|
134
|
+
pushFieldError(
|
|
135
|
+
fieldErrors,
|
|
136
|
+
input.key,
|
|
137
|
+
"Selected choice is not eligible.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (input.domain.type === "choice") {
|
|
143
|
+
const allowed = new Set(
|
|
144
|
+
input.domain.choices?.map((choice) => choice.value),
|
|
145
|
+
);
|
|
146
|
+
if (allowed.size > 0 && !allowed.has(value as string | null)) {
|
|
147
|
+
pushFieldError(
|
|
148
|
+
fieldErrors,
|
|
149
|
+
input.key,
|
|
150
|
+
"Selected choice is not eligible.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
70
153
|
}
|
|
71
154
|
|
|
72
155
|
if (input.domain.type === "target") {
|
|
@@ -176,8 +259,10 @@ export function inputByTarget(
|
|
|
176
259
|
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
177
260
|
targetKind: BoardTargetKind | "card",
|
|
178
261
|
targetId: string,
|
|
262
|
+
params: Readonly<Record<string, unknown>> = {},
|
|
179
263
|
): InteractionInputDescriptor | null {
|
|
180
|
-
for (const
|
|
264
|
+
for (const rawInput of descriptor.inputs) {
|
|
265
|
+
const input = resolveInputDomain(rawInput, params);
|
|
181
266
|
if (input.domain.type !== "target") continue;
|
|
182
267
|
if (input.domain.targetKind !== targetKind) continue;
|
|
183
268
|
if (input.domain.eligibleTargets?.includes(targetId)) return input;
|
|
@@ -188,16 +273,20 @@ export function inputByTarget(
|
|
|
188
273
|
export function eligibleTargetsForInput(
|
|
189
274
|
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
190
275
|
key: string,
|
|
276
|
+
params: Readonly<Record<string, unknown>> = {},
|
|
191
277
|
): readonly string[] | undefined {
|
|
192
|
-
const
|
|
278
|
+
const rawInput = inputByKey(descriptor, key);
|
|
279
|
+
const domain = rawInput ? resolveInputDomain(rawInput, params).domain : null;
|
|
193
280
|
return domain?.type === "target" ? domain.eligibleTargets : undefined;
|
|
194
281
|
}
|
|
195
282
|
|
|
196
283
|
export function eligibleTargetsByInput(
|
|
197
284
|
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
285
|
+
params: Readonly<Record<string, unknown>> = {},
|
|
198
286
|
): Record<string, readonly string[]> {
|
|
199
287
|
return Object.fromEntries(
|
|
200
|
-
descriptor.inputs.flatMap((
|
|
288
|
+
descriptor.inputs.flatMap((rawInput) => {
|
|
289
|
+
const input = resolveInputDomain(rawInput, params);
|
|
201
290
|
const targets =
|
|
202
291
|
input.domain.type === "target"
|
|
203
292
|
? input.domain.eligibleTargets
|
|
@@ -209,6 +298,7 @@ export function eligibleTargetsByInput(
|
|
|
209
298
|
|
|
210
299
|
export function eligibleTargetsByBoardKind(
|
|
211
300
|
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
301
|
+
params: Readonly<Record<string, unknown>> = {},
|
|
212
302
|
): Partial<Record<BoardTargetKind, readonly string[]>> {
|
|
213
303
|
const result: Record<BoardTargetKind, Set<string>> = {
|
|
214
304
|
edge: new Set<string>(),
|
|
@@ -216,7 +306,8 @@ export function eligibleTargetsByBoardKind(
|
|
|
216
306
|
space: new Set<string>(),
|
|
217
307
|
tile: new Set<string>(),
|
|
218
308
|
};
|
|
219
|
-
for (const
|
|
309
|
+
for (const rawInput of descriptor.inputs) {
|
|
310
|
+
const input = resolveInputDomain(rawInput, params);
|
|
220
311
|
if (input.domain.type !== "target") continue;
|
|
221
312
|
const targetKind = input.domain.targetKind;
|
|
222
313
|
if (!isBoardTargetKind(targetKind)) continue;
|
|
@@ -234,7 +325,7 @@ export function eligibleTargetsByBoardKind(
|
|
|
234
325
|
export function hasBoardTargetInput(
|
|
235
326
|
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
236
327
|
): boolean {
|
|
237
|
-
return
|
|
328
|
+
return boardTargetKindsOf(descriptor).length > 0;
|
|
238
329
|
}
|
|
239
330
|
|
|
240
331
|
export function hasCardTargetInput(
|
|
@@ -252,10 +343,7 @@ export function interactionArmScope(
|
|
|
252
343
|
"inputs" | "interactionKey" | "zoneId"
|
|
253
344
|
>,
|
|
254
345
|
): string {
|
|
255
|
-
const
|
|
256
|
-
const boardKinds = (Object.keys(byKind) as BoardTargetKind[]).filter(
|
|
257
|
-
(kind) => byKind[kind] !== undefined,
|
|
258
|
-
);
|
|
346
|
+
const boardKinds = boardTargetKindsOf(descriptor);
|
|
259
347
|
if (boardKinds.length === 1) return `board:${boardKinds[0]}`;
|
|
260
348
|
if (boardKinds.length > 1) return "board";
|
|
261
349
|
if (descriptor.zoneId) return `zone:${descriptor.zoneId}`;
|
|
@@ -266,8 +354,22 @@ export function inputKeyForTarget(
|
|
|
266
354
|
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
267
355
|
targetKind: BoardTargetKind | "card",
|
|
268
356
|
targetId: string,
|
|
357
|
+
params: Readonly<Record<string, unknown>> = {},
|
|
269
358
|
): string | null {
|
|
270
|
-
return inputByTarget(descriptor, targetKind, targetId)?.key ?? null;
|
|
359
|
+
return inputByTarget(descriptor, targetKind, targetId, params)?.key ?? null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function boardTargetKindsOf(
|
|
363
|
+
descriptor: Pick<InteractionDescriptor, "inputs">,
|
|
364
|
+
): BoardTargetKind[] {
|
|
365
|
+
const kinds = new Set<BoardTargetKind>();
|
|
366
|
+
for (const input of descriptor.inputs) {
|
|
367
|
+
if (input.domain.type !== "target") continue;
|
|
368
|
+
if (isBoardTargetKind(input.domain.targetKind)) {
|
|
369
|
+
kinds.add(input.domain.targetKind);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return [...kinds];
|
|
271
373
|
}
|
|
272
374
|
|
|
273
375
|
function validateInputSelection(
|