@dreamboard-games/sdk 0.5.0-alpha.3 → 0.5.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +55 -4
  2. package/dist/board-target-B0QG6OYx.d.ts +8 -0
  3. package/dist/chunk-3JXUL4VN.js +139 -0
  4. package/dist/chunk-3JXUL4VN.js.map +1 -0
  5. package/dist/{chunk-SP234IY4.js → chunk-7RGMPJQG.js} +340 -248
  6. package/dist/chunk-7RGMPJQG.js.map +1 -0
  7. package/dist/{chunk-FMM5LVJW.js → chunk-K75F5UIJ.js} +27 -12
  8. package/dist/chunk-K75F5UIJ.js.map +1 -0
  9. package/dist/{chunk-ZPRKWPEP.js → chunk-N37D7NPB.js} +116 -29
  10. package/dist/chunk-N37D7NPB.js.map +1 -0
  11. package/dist/{chunk-LRILEVKC.js → chunk-UESVBUPU.js} +473 -496
  12. package/dist/chunk-UESVBUPU.js.map +1 -0
  13. package/dist/{diagnostics-B0lgnuZ6.d.ts → diagnostics-9gJgI85c.d.ts} +2 -2
  14. package/dist/{definition-BaHNPZol.d.ts → extract-Bjuw_gdF.d.ts} +2513 -2390
  15. package/dist/index.d.ts +524 -185
  16. package/dist/index.js +280 -226
  17. package/dist/index.js.map +1 -1
  18. package/dist/model-BCl8WsB6.d.ts +551 -0
  19. package/dist/react.d.ts +35 -7
  20. package/dist/react.js +137 -11
  21. package/dist/react.js.map +1 -1
  22. package/dist/reducer.d.ts +227 -151
  23. package/dist/reducer.js +343 -569
  24. package/dist/reducer.js.map +1 -1
  25. package/dist/testing.d.ts +54 -42
  26. package/dist/testing.js +120 -69
  27. package/dist/testing.js.map +1 -1
  28. package/dist/{types-DimqnJBq.d.ts → types-B3kIhBWb.d.ts} +41 -10
  29. package/package.json +20 -17
  30. package/dist/chunk-FMM5LVJW.js.map +0 -1
  31. package/dist/chunk-LRILEVKC.js.map +0 -1
  32. package/dist/chunk-SP234IY4.js.map +0 -1
  33. package/dist/chunk-VUWNJXLJ.js +0 -52
  34. package/dist/chunk-VUWNJXLJ.js.map +0 -1
  35. package/dist/chunk-ZPRKWPEP.js.map +0 -1
  36. package/dist/model-BHro6yXu.d.ts +0 -288
@@ -2,8 +2,72 @@ import {
2
2
  immutableCopy
3
3
  } from "./chunk-DMFVAZI2.js";
4
4
  import {
5
- inputValueInDomain
6
- } from "./chunk-VUWNJXLJ.js";
5
+ inputDomainErrors,
6
+ inputTargetInDomain,
7
+ inputValueInDomain,
8
+ inputValueKey,
9
+ requireLookup
10
+ } from "./chunk-3JXUL4VN.js";
11
+
12
+ // src/headless/runtime-features.ts
13
+ var runtimeFeatures = /* @__PURE__ */ Symbol("runtimeFeatures");
14
+
15
+ // src/headless/input-control.ts
16
+ function resourceValue(domain, value) {
17
+ const record = value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
18
+ return Object.fromEntries(
19
+ domain.resources.map((resource) => {
20
+ const amount = record[resource.resourceId];
21
+ return [resource.resourceId, typeof amount === "number" ? amount : 0];
22
+ })
23
+ );
24
+ }
25
+ function createInputControl(input, props) {
26
+ const domain = input.getDomain();
27
+ const current = input.getValue();
28
+ const common = { key: input.key, disabled: props.disabled, props };
29
+ if (domain.type === "boundedNumber") {
30
+ if (domain.selection?.mode === "many")
31
+ return {
32
+ ...common,
33
+ type: "boundedNumber",
34
+ domain,
35
+ mode: "many",
36
+ value: Array.isArray(current) ? current.filter(
37
+ (value) => typeof value === "number"
38
+ ) : [],
39
+ setValue: (value) => input.setValue(value)
40
+ };
41
+ return {
42
+ ...common,
43
+ type: "boundedNumber",
44
+ domain,
45
+ mode: "single",
46
+ value: typeof current === "number" ? current : void 0,
47
+ setValue: (value) => value === void 0 ? input.clear() : input.setValue(value)
48
+ };
49
+ }
50
+ if (domain.type === "resourceMap") {
51
+ if (domain.selection?.mode === "many")
52
+ return {
53
+ ...common,
54
+ type: "resourceMap",
55
+ domain,
56
+ mode: "many",
57
+ value: Array.isArray(current) ? current.map((value) => resourceValue(domain, value)) : [],
58
+ setValue: (value) => input.setValue(value)
59
+ };
60
+ return {
61
+ ...common,
62
+ type: "resourceMap",
63
+ domain,
64
+ mode: "single",
65
+ value: current === void 0 ? void 0 : resourceValue(domain, current),
66
+ setValue: (value) => value === void 0 ? input.clear() : input.setValue(value)
67
+ };
68
+ }
69
+ return { ...common, type: "targets", options: input.getTargetOptions() };
70
+ }
7
71
 
8
72
  // src/headless/instance.ts
9
73
  import { createStore } from "@tanstack/store";
@@ -30,68 +94,13 @@ function validateInteractionInputDomains(descriptor, params) {
30
94
  for (const rawInput of descriptor.inputs) {
31
95
  const input = rawInput;
32
96
  const value = params[input.key];
33
- if (value === void 0 || value === null) continue;
34
- const selectionErrors = validateInputSelection(input, value);
35
- for (const error of selectionErrors) {
97
+ if (value === void 0) continue;
98
+ for (const error of inputDomainErrors(
99
+ input.domain,
100
+ value,
101
+ input.domain.selection
102
+ ))
36
103
  pushFieldError(fieldErrors, input.key, error);
37
- }
38
- if (input.domain.type === "choiceList") {
39
- if (!Array.isArray(value)) {
40
- pushFieldError(fieldErrors, input.key, "Expected a list of choices.");
41
- continue;
42
- }
43
- const min = input.domain.min ?? 0;
44
- const max = input.domain.max ?? input.domain.choices?.length ?? Number.POSITIVE_INFINITY;
45
- if (value.length < min) {
46
- pushFieldError(
47
- fieldErrors,
48
- input.key,
49
- `Choose at least ${min} ${pluralize("option", min)}.`
50
- );
51
- }
52
- if (value.length > max) {
53
- pushFieldError(
54
- fieldErrors,
55
- input.key,
56
- `Choose at most ${max} ${pluralize("option", max)}.`
57
- );
58
- }
59
- const allowed = new Set(
60
- input.domain.choices?.map((choice) => choice.value)
61
- );
62
- if (allowed.size > 0 && value.some((item) => !allowed.has(String(item)))) {
63
- pushFieldError(
64
- fieldErrors,
65
- input.key,
66
- "Selected choice is not eligible."
67
- );
68
- }
69
- }
70
- if (input.domain.type === "choice") {
71
- const allowed = new Set(
72
- input.domain.choices?.map((choice) => choice.value)
73
- );
74
- if (allowed.size > 0 && !allowed.has(value)) {
75
- pushFieldError(
76
- fieldErrors,
77
- input.key,
78
- "Selected choice is not eligible."
79
- );
80
- }
81
- }
82
- if (isResolvedTargetDomain(input.domain)) {
83
- const values = valuesForSelection(input.domain.selection, value);
84
- for (const item of values) {
85
- if (!input.domain.eligibleTargets.includes(String(item))) {
86
- pushFieldError(
87
- fieldErrors,
88
- input.key,
89
- "Selected target is not eligible."
90
- );
91
- break;
92
- }
93
- }
94
- }
95
104
  }
96
105
  return fieldErrors;
97
106
  }
@@ -110,10 +119,10 @@ function isManyInput(input) {
110
119
  }
111
120
  function toggleManyValue(current, value, selection) {
112
121
  if (selection.mode !== "many") return [value];
113
- const previous = Array.isArray(current) ? current.map((item) => String(item)) : [];
114
- const existing = previous.indexOf(value);
122
+ const previous = Array.isArray(current) ? current : [];
123
+ const existing = previous.findIndex((item) => sameValue(item, value));
115
124
  if (existing >= 0) {
116
- return previous.filter((item) => item !== value);
125
+ return previous.filter((item) => !sameValue(item, value));
117
126
  }
118
127
  if (selection.max !== void 0 && previous.length >= selection.max) {
119
128
  return previous;
@@ -123,9 +132,8 @@ function toggleManyValue(current, value, selection) {
123
132
  function isManyTargetSelectable(input, current, targetId) {
124
133
  const selection = inputSelection(input);
125
134
  if (selection?.mode !== "many") return true;
126
- const currentValues = Array.isArray(current) ? current.map((item) => String(item)) : [];
127
- if (currentValues.includes(targetId)) return true;
128
- if (selection.distinct && currentValues.includes(targetId)) return false;
135
+ const currentValues = Array.isArray(current) ? current : [];
136
+ if (currentValues.some((item) => sameValue(item, targetId))) return true;
129
137
  return selection.max === void 0 || currentValues.length < selection.max;
130
138
  }
131
139
  function hasInteractionFieldErrors(fieldErrors) {
@@ -136,52 +144,21 @@ function hasInteractionFieldErrors(fieldErrors) {
136
144
  function inputByKey(descriptor, key) {
137
145
  return descriptor.inputs.find((input) => input.key === key);
138
146
  }
139
- function isTargetDomain(domain) {
140
- if (!domain) return false;
141
- return domain.type === "cardTarget" || domain.type === "boardTarget";
142
- }
143
- function isResolvedTargetDomain(domain) {
144
- return (domain.type === "cardTarget" || domain.type === "boardTarget") && domain.projection === "resolved";
145
- }
146
- function validateInputSelection(input, value) {
147
- const selection = inputSelection(input);
148
- if (selection?.mode !== "many") return [];
149
- if (!Array.isArray(value)) return ["Expected a list of values."];
150
- const errors = [];
151
- const min = selection.min;
152
- if (value.length < min) {
153
- errors.push(`Choose at least ${min} ${pluralize("value", min)}.`);
154
- }
155
- if (selection.max !== void 0 && value.length > selection.max) {
156
- errors.push(
157
- `Choose at most ${selection.max} ${pluralize("value", selection.max)}.`
158
- );
159
- }
160
- if (selection.distinct) {
161
- const seen = /* @__PURE__ */ new Set();
162
- for (const item of value) {
163
- const key = String(item);
164
- if (seen.has(key)) {
165
- errors.push("Choose each value only once.");
166
- break;
167
- }
168
- seen.add(key);
169
- }
170
- }
171
- return errors;
172
- }
173
- function valuesForSelection(selection, value) {
174
- if (selection?.mode === "many") return Array.isArray(value) ? value : [];
175
- return [value];
176
- }
177
147
  function inputSelection(input) {
178
- return "selection" in input.domain ? input.domain.selection : void 0;
148
+ if (input.domain.selection) return input.domain.selection;
149
+ if (input.domain.type === "choiceList")
150
+ return {
151
+ mode: "many",
152
+ min: input.domain.min ?? 0,
153
+ max: input.domain.max ?? input.domain.choices.length
154
+ };
155
+ return void 0;
179
156
  }
180
157
  function pushFieldError(fieldErrors, key, message) {
181
158
  fieldErrors[key] = [...fieldErrors[key] ?? [], message];
182
159
  }
183
- function pluralize(word, count) {
184
- return count === 1 ? word : `${word}s`;
160
+ function sameValue(left, right) {
161
+ return inputValueKey(left) === inputValueKey(right);
185
162
  }
186
163
 
187
164
  // src/headless/interaction-router.ts
@@ -236,8 +213,12 @@ function getInteractionDraftReadiness(descriptor, draft) {
236
213
  function routeInteractionTarget(store, descriptor, target) {
237
214
  const input = inputByKey(descriptor, target.inputKey);
238
215
  const currentDraft = store.getDraft(descriptor.interactionKey);
239
- const selection = isTargetDomain(input?.domain) ? input.domain.selection : void 0;
240
- const targetValue = selection?.mode === "many" ? toggleManyValue(currentDraft[target.inputKey], target.value, selection) : target.value;
216
+ const selection = input ? inputSelection(input) : void 0;
217
+ const targetValue = selection?.mode === "many" ? toggleManyValue(
218
+ currentDraft[target.inputKey] ?? input?.defaultValue,
219
+ target.value,
220
+ selection
221
+ ) : target.value;
241
222
  const params = applyInteractionDraftMutation(store, descriptor, [
242
223
  ...Object.entries(target.extraInputs ?? {}).map(([key, value]) => ({
243
224
  key,
@@ -258,18 +239,22 @@ function routeCardInputIntent(store, descriptor, intent) {
258
239
  });
259
240
  }
260
241
  function shouldAutoSubmitInteraction(descriptor) {
261
- return descriptor.commit.mode === "autoWhenReady" && !descriptor.inputs.some((input) => isManyInput(input));
242
+ return descriptor.commit.mode === "autoWhenReady" && !descriptor.inputs.some((input) => input.domain.selection?.mode === "many");
262
243
  }
263
244
 
264
245
  // src/headless/instance.ts
265
246
  var EMPTY = Object.freeze({});
266
247
  var EMPTY_DRAFTS = Object.freeze({});
267
- var equalValue = (a, b) => Object.is(a, b) || typeof a === "object" && typeof b === "object" && JSON.stringify(a) === JSON.stringify(b);
248
+ var equalValue = (a, b) => Object.is(a, b) || typeof a === "object" && typeof b === "object" && inputValueKey(a) === inputValueKey(b);
268
249
  function inDomain(input, value, partial = false) {
269
250
  return inputValueInDomain(input.domain, value, input.domain.selection, {
270
251
  ignoreMinimum: partial
271
252
  });
272
253
  }
254
+ function matchesBoardTarget(input, target) {
255
+ const domain = input.domain;
256
+ return domain.type === "boardTarget" && domain.valueKind === target.valueKind && (domain.targetKind === target.kind || target.kind === "space" && domain.targetKind === "tile") && domain.boardId === (target.valueKind === "board-id" ? target.boardId : target.value.boardId);
257
+ }
273
258
  function immutableValues(value) {
274
259
  return immutableCopy(value);
275
260
  }
@@ -286,7 +271,7 @@ function data(action, interaction, input, value) {
286
271
  var AmbiguousTargetError = class extends Error {
287
272
  constructor(id) {
288
273
  super(
289
- `Target '${id}' belongs to more than one interaction. Choose an interaction explicitly.`
274
+ `Target '${id}' belongs to more than one eligible input. Choose an interaction and input explicitly.`
290
275
  );
291
276
  this.name = "AmbiguousTargetError";
292
277
  }
@@ -351,19 +336,20 @@ var InteractionObject = class {
351
336
  return this.descriptor.step?.index ?? null;
352
337
  }
353
338
  getInput(key) {
339
+ return requireLookup(
340
+ this.findInput(key),
341
+ `Input in interaction ${this.key}`,
342
+ key
343
+ );
344
+ }
345
+ findInput(key) {
354
346
  return this.inputObjects.find((input) => input.key === key);
355
347
  }
356
348
  getInputs() {
357
349
  return this.inputObjects;
358
350
  }
359
351
  readiness() {
360
- const readiness = getInteractionDraftReadiness(this.descriptor, this.draft);
361
- return {
362
- ...readiness,
363
- ready: readiness.ready && this.descriptor.inputs.every(
364
- (input) => inDomain(input, readiness.values[input.key])
365
- )
366
- };
352
+ return getInteractionDraftReadiness(this.descriptor, this.draft);
367
353
  }
368
354
  getIsReady() {
369
355
  return this.readiness().ready;
@@ -392,7 +378,7 @@ var InteractionObject = class {
392
378
  getSubmitHandler() {
393
379
  return (event) => {
394
380
  event?.preventDefault();
395
- this.owner.handle(this.submit());
381
+ this.owner.handle(() => this.submit());
396
382
  };
397
383
  }
398
384
  getSubmitProps() {
@@ -422,6 +408,21 @@ var InputObject = class {
422
408
  get game() {
423
409
  return this.interaction.game;
424
410
  }
411
+ get domainType() {
412
+ return this.descriptor.domain.type;
413
+ }
414
+ get selectionMode() {
415
+ return isManyInput(this.descriptor) ? "many" : "single";
416
+ }
417
+ getTargetOptions() {
418
+ const domain = this.getDomain();
419
+ return this.getEligibleTargets().map((value) => ({
420
+ value,
421
+ label: (domain.type === "choice" || domain.type === "choiceList" ? domain.choices.find((choice) => Object.is(choice.value, value))?.label : void 0) ?? (value === null ? "None" : typeof value === "object" ? JSON.stringify(value) : String(value)),
422
+ selected: this.getIsSelected(value),
423
+ props: this.getTargetProps(value)
424
+ }));
425
+ }
425
426
  getDomain() {
426
427
  return this.descriptor.domain;
427
428
  }
@@ -443,7 +444,7 @@ var InputObject = class {
443
444
  };
444
445
  return getInteractionDraftReadiness(descriptor, {
445
446
  [this.key]: this.getValue()
446
- }).ready && inDomain(this.descriptor, this.getValue());
447
+ }).ready;
447
448
  }
448
449
  getEligibleTargets() {
449
450
  const domain = this.descriptor.domain;
@@ -455,11 +456,7 @@ var InputObject = class {
455
456
  }
456
457
  getIsEligible(value) {
457
458
  const input = this.descriptor;
458
- const selection = input.domain.selection;
459
- return inDomain(
460
- { ...input, domain: { ...input.domain, selection: void 0 } },
461
- value
462
- ) && (!selection || selection.mode !== "many" || isManyTargetSelectable(input, this.getValue(), String(value)));
459
+ return inputTargetInDomain(input.domain, value) && isManyTargetSelectable(input, this.getValue(), value);
463
460
  }
464
461
  getIsSelected(value) {
465
462
  const current = this.getValue();
@@ -485,28 +482,18 @@ var InputObject = class {
485
482
  onClick: () => this.getSelectHandler(value)()
486
483
  };
487
484
  }
488
- getFieldProps() {
485
+ getControl() {
489
486
  const disabled = !this.interaction.getIsAvailable() || this.interaction.status !== "open" || !this.interaction.connected;
490
- return {
487
+ return createInputControl(this, {
491
488
  ...data("option", this.interaction.key, this.key),
492
489
  "data-seat": this.interaction.seat,
493
- value: this.getValue() ?? "",
494
490
  disabled,
495
- "data-disabled": disabled,
496
- onChange: (event) => {
497
- if (event.currentTarget.value === "") {
498
- this.clear();
499
- return;
500
- }
501
- this.setValue(
502
- this.descriptor.domain.type === "boundedNumber" ? Number(event.currentTarget.value) : event.currentTarget.value
503
- );
504
- }
505
- };
491
+ "data-disabled": disabled
492
+ });
506
493
  }
507
494
  };
508
495
  var CardObject = class {
509
- constructor(owner, id, zone, index, encoded, routes) {
496
+ constructor(owner, id, zone, index, encoded, backImage, routes) {
510
497
  this.owner = owner;
511
498
  this.id = id;
512
499
  this.zone = zone;
@@ -518,6 +505,7 @@ var CardObject = class {
518
505
  ) ?? -1;
519
506
  this.view = encoded ? immutableCopy(JSON.parse(encoded)) : null;
520
507
  this.hidden = this.view === null;
508
+ this.backImage = backImage ?? null;
521
509
  }
522
510
  owner;
523
511
  id;
@@ -528,6 +516,7 @@ var CardObject = class {
528
516
  seat;
529
517
  view;
530
518
  hidden;
519
+ backImage;
531
520
  get game() {
532
521
  return this.owner.instance;
533
522
  }
@@ -544,16 +533,16 @@ var CardObject = class {
544
533
  )
545
534
  );
546
535
  }
547
- getCanSelect() {
536
+ getCanSelect(options) {
548
537
  return this.routes.some(
549
- (route) => route.getIsAvailable() && route.status === "open" && route.connected && route.getInputs().some(
550
- (input) => input.descriptor.domain.type === "cardTarget" && input.getIsEligible(this.id)
538
+ (route) => route.getIsAvailable() && (!options?.interaction || route.key === options.interaction) && route.status === "open" && route.connected && route.getInputs().some(
539
+ (input) => input.descriptor.domain.type === "cardTarget" && (!options?.input || input.key === options.input) && input.getIsEligible(this.id)
551
540
  )
552
541
  );
553
542
  }
554
543
  select(options) {
555
544
  if (this.epoch !== this.owner.epoch || this.owner.disposed) return;
556
- this.owner.selectCard(this.id, options?.interaction);
545
+ this.owner.selectCard(this.id, options);
557
546
  }
558
547
  getSelectHandler(options) {
559
548
  return () => this.select(options);
@@ -561,13 +550,14 @@ var CardObject = class {
561
550
  getProps(options) {
562
551
  return {
563
552
  type: "button",
564
- disabled: !this.getCanSelect(),
553
+ disabled: !this.getCanSelect(options),
565
554
  "data-action": "select",
566
555
  "data-value": this.id,
567
556
  "data-interaction": options?.interaction,
557
+ "data-input": options?.input,
568
558
  "data-eligible": this.getIsEligible(),
569
559
  "data-selected": this.getIsSelected(),
570
- "data-disabled": !this.getCanSelect(),
560
+ "data-disabled": !this.getCanSelect(options),
571
561
  "data-hidden": this.hidden,
572
562
  "data-seat": this.seat,
573
563
  onClick: this.getSelectHandler(options)
@@ -593,6 +583,9 @@ var ZoneObject = class {
593
583
  return this.count === 0;
594
584
  }
595
585
  getCard(id) {
586
+ return requireLookup(this.findCard(id), `Card in zone ${this.id}`, id);
587
+ }
588
+ findCard(id) {
596
589
  return this.cards.find((card) => card.id === id);
597
590
  }
598
591
  getCards(options) {
@@ -649,6 +642,7 @@ var Controller = class {
649
642
  };
650
643
  subscription;
651
644
  sourceState;
645
+ reportedFailures = /* @__PURE__ */ new WeakMap();
652
646
  pending = null;
653
647
  building;
654
648
  rootProperties = /* @__PURE__ */ new Map();
@@ -675,7 +669,60 @@ var Controller = class {
675
669
  this.localDrafts = options.initialState?.drafts ?? EMPTY_DRAFTS;
676
670
  this.localActive = options.initialState?.activeInteraction ?? null;
677
671
  this.sourceState = options.source.store.get();
672
+ const current = () => this.building ?? this.store.get();
678
673
  const root = {
674
+ get snapshot() {
675
+ return current().snapshot;
676
+ },
677
+ get view() {
678
+ return current().view;
679
+ },
680
+ get version() {
681
+ return current().version;
682
+ },
683
+ get connection() {
684
+ return current().connection;
685
+ },
686
+ get failure() {
687
+ return current().failure;
688
+ },
689
+ get request() {
690
+ return current().request;
691
+ },
692
+ get state() {
693
+ return current().state;
694
+ },
695
+ get phase() {
696
+ return current().phase;
697
+ },
698
+ get turn() {
699
+ return current().turn;
700
+ },
701
+ get me() {
702
+ return current().me;
703
+ },
704
+ get players() {
705
+ return current().players;
706
+ },
707
+ get interactions() {
708
+ return current().interactions;
709
+ },
710
+ get inputs() {
711
+ return current().inputs;
712
+ },
713
+ get zones() {
714
+ return current().zones;
715
+ },
716
+ get cards() {
717
+ return current().cards;
718
+ },
719
+ get events() {
720
+ return current().events;
721
+ },
722
+ store: {
723
+ get: () => this.store.get(),
724
+ subscribe: (listener) => this.store.subscribe(listener)
725
+ },
679
726
  getSnapshot: () => this.building ?? this.store.get(),
680
727
  getOptions: () => this.options,
681
728
  setOptions: (next) => this.setOptions(next),
@@ -687,34 +734,23 @@ var Controller = class {
687
734
  assertCoverage: () => this.assertCoverage(),
688
735
  inspect: () => this.store.get()
689
736
  };
690
- this.instance = root;
691
- for (const key of [
692
- "snapshot",
693
- "view",
694
- "version",
695
- "connection",
696
- "request",
697
- "state",
698
- "phase",
699
- "turn",
700
- "me",
701
- "players",
702
- "interactions",
703
- "inputs",
704
- "zones",
705
- "cards",
706
- "events"
707
- ])
708
- Object.defineProperty(root, key, {
709
- get: () => (this.building ?? this.store.get())[key]
710
- });
711
- this.store = createStore(Object.freeze(this.build()));
712
- Object.defineProperty(root, "store", {
713
- value: {
714
- get: () => this.store.get(),
715
- subscribe: (listener) => this.store.subscribe(listener)
737
+ for (const [key, descriptor] of Object.entries(
738
+ Object.getOwnPropertyDescriptors(root)
739
+ )) {
740
+ if (descriptor.get) {
741
+ Object.defineProperty(root, key, {
742
+ enumerable: false,
743
+ configurable: false
744
+ });
716
745
  }
746
+ }
747
+ Object.defineProperty(root, "store", {
748
+ enumerable: false,
749
+ configurable: false,
750
+ writable: false
717
751
  });
752
+ this.instance = root;
753
+ this.store = createStore(Object.freeze(this.build()));
718
754
  if ("apply" in options.source)
719
755
  Object.defineProperty(root, "apply", {
720
756
  value: (action) => this.options.source.apply(action)
@@ -723,18 +759,24 @@ var Controller = class {
723
759
  Object.defineProperty(root, "explore", {
724
760
  value: (...args) => this.options.source.explore(...args)
725
761
  });
726
- const context = {
727
- createBoard: (id, data2) => this.object("board", {
728
- id,
762
+ const runtime = {
763
+ game: this.instance,
764
+ getBoards: () => {
765
+ const view = this.instance.view;
766
+ return view?.boards?.byId ?? {};
767
+ },
768
+ createBoard: (data2) => this.object("board", {
769
+ id: data2.id,
729
770
  data: immutableCopy(data2),
730
771
  game: this.instance
731
772
  }),
732
- routeTarget: (kind, id, options2) => this.routeTarget(kind, id, options2?.interaction, options2?.boardId),
773
+ routeTarget: (target, options2) => this.routeTarget(target, options2),
733
774
  routeCardDrop: (cardId, target) => this.routeCardDrop(cardId, target),
734
775
  invalidate: () => {
735
776
  if (!this.disposed) this.refresh();
736
777
  }
737
778
  };
779
+ const context = { ...runtime, [runtimeFeatures]: runtime };
738
780
  for (const feature of Object.values(
739
781
  features?.(this.instance, context) ?? {}
740
782
  )) {
@@ -762,6 +804,7 @@ var Controller = class {
762
804
  }
763
805
  this.refresh();
764
806
  this.subscribeSource();
807
+ this.reportSourceFailure();
765
808
  }
766
809
  install(target, extension) {
767
810
  if (!extension) return;
@@ -796,7 +839,8 @@ var Controller = class {
796
839
  );
797
840
  }
798
841
  object(hook, value) {
799
- return Object.freeze(Object.setPrototypeOf(value, this.prototypes[hook]));
842
+ Object.setPrototypeOf(value, this.prototypes[hook]);
843
+ return Object.freeze(value);
800
844
  }
801
845
  drafts() {
802
846
  if (this.options.state && this.options.state === this.invalidControlledState)
@@ -843,7 +887,7 @@ var Controller = class {
843
887
  return base;
844
888
  }
845
889
  current(key) {
846
- return this.store.get().interactions.get(key);
890
+ return this.store.get().interactions.find(key);
847
891
  }
848
892
  editable(key) {
849
893
  const interaction = this.current(key);
@@ -851,11 +895,11 @@ var Controller = class {
851
895
  }
852
896
  setInput(key, inputKey, value) {
853
897
  const interaction = this.editable(key);
854
- if (!interaction?.getInput(inputKey)) return;
898
+ if (!interaction?.findInput(inputKey)) return;
855
899
  this.writeDraft(key, { ...this.drafts()[key], [inputKey]: value });
856
900
  }
857
901
  clearInput(key, inputKey) {
858
- if (!this.editable(key)?.getInput(inputKey)) return;
902
+ if (!this.editable(key)?.findInput(inputKey)) return;
859
903
  const value = { ...this.drafts()[key] };
860
904
  delete value[inputKey];
861
905
  this.writeDraft(key, value);
@@ -869,9 +913,9 @@ var Controller = class {
869
913
  const interaction = route ?? this.editable(key);
870
914
  if (route && (!route.currentLifetime() || !route.getIsAvailable() || this.sourceState.connection !== "ready" || this.sourceState.request || this.pending))
871
915
  return;
872
- const input = interaction?.getInput(inputKey);
916
+ const input = interaction?.findInput(inputKey);
873
917
  if (!interaction || !input?.getIsEligible(value)) return;
874
- if (typeof value === "string" && (input.descriptor.domain.type === "cardTarget" || input.descriptor.domain.type === "boardTarget")) {
918
+ if (isManyInput(input.descriptor) || typeof value === "string" && (input.descriptor.domain.type === "cardTarget" || input.descriptor.domain.type === "boardTarget")) {
875
919
  let next = { ...this.drafts()[key] };
876
920
  routeInteractionTarget(
877
921
  {
@@ -891,33 +935,45 @@ var Controller = class {
891
935
  this.activate(key);
892
936
  const current = this.current(key);
893
937
  if (current && Object.hasOwn(this.drafts()[key] ?? EMPTY, inputKey) && shouldAutoSubmitInteraction(current.descriptor) && current.getIsReady())
894
- this.handle(this.submit(key, false));
895
- }
896
- selectCard(id, explicit) {
897
- const card = this.store.get().cards.get(id);
898
- if (!card) return;
899
- let routes = card.routes.filter(
900
- (route2) => route2.getIsAvailable() && route2.currentLifetime() && !this.sourceState.request && this.sourceState.connection === "ready" && route2.getInputs().some(
901
- (input2) => input2.descriptor.domain.type === "cardTarget" && input2.getIsEligible(id)
902
- )
903
- );
904
- if (explicit) routes = routes.filter((route2) => route2.key === explicit);
905
- if (routes.length > 1) throw new AmbiguousTargetError(id);
906
- const route = routes[0];
907
- const input = route?.getInputs().find(
908
- (input2) => input2.descriptor.domain.type === "cardTarget" && input2.getIsEligible(id)
909
- );
910
- if (route && input) this.select(route.key, input.key, id, route);
938
+ this.handle(() => this.submit(key, false));
939
+ }
940
+ selectCard(id, options) {
941
+ const card = this.store.get().cards.find(id);
942
+ if (!card || this.sourceState.request || this.sourceState.connection !== "ready")
943
+ return;
944
+ const candidates = card.routes.flatMap((interaction) => {
945
+ if (!interaction.getIsAvailable() || !interaction.currentLifetime() || options?.interaction && interaction.key !== options.interaction)
946
+ return [];
947
+ return interaction.getInputs().filter(
948
+ (input) => input.descriptor.domain.type === "cardTarget" && (!options?.input || input.key === options.input) && input.getIsEligible(id)
949
+ ).map((input) => ({ interaction, input }));
950
+ });
951
+ if (candidates.length > 1) throw new AmbiguousTargetError(id);
952
+ const candidate = candidates[0];
953
+ if (candidate)
954
+ this.select(
955
+ candidate.interaction.key,
956
+ candidate.input.key,
957
+ id,
958
+ candidate.interaction
959
+ );
911
960
  }
912
- handle(promise) {
913
- void promise.then(
961
+ handle(submit) {
962
+ const source = this.options.source;
963
+ const active = () => !this.disposed && this.options.source === source;
964
+ void submit().then(
914
965
  (result) => {
915
- if (!result.accepted)
966
+ if (active() && !result.accepted)
916
967
  this.options.onError?.(
917
968
  new Error(result.message ?? result.errorCode, { cause: result })
918
969
  );
919
970
  },
920
- (error) => this.options.onError?.(error)
971
+ (error) => {
972
+ if (!active()) return;
973
+ const state = source.store.get();
974
+ if (state.failure === error || state.connection === "closed") return;
975
+ this.options.onError?.(error);
976
+ }
921
977
  );
922
978
  }
923
979
  async submit(key, cancel) {
@@ -968,7 +1024,7 @@ var Controller = class {
968
1024
  const operation = this.pending;
969
1025
  if (!operation?.accepted || this.sourceState.request || operation.source !== this.options.source || operation.epoch !== this.epoch)
970
1026
  return;
971
- if (this.sourceState.connection === "closed") {
1027
+ if (this.sourceState.connection === "closed" || this.sourceState.connection === "failed") {
972
1028
  this.pending = null;
973
1029
  this.refresh();
974
1030
  return;
@@ -980,6 +1036,13 @@ var Controller = class {
980
1036
  this.reset(operation.key);
981
1037
  else this.refresh();
982
1038
  }
1039
+ reportSourceFailure() {
1040
+ const failure = this.sourceState.failure;
1041
+ if (!failure || !this.options.onError || this.reportedFailures.get(this.options.source) === failure)
1042
+ return;
1043
+ this.reportedFailures.set(this.options.source, failure);
1044
+ this.options.onError?.(failure);
1045
+ }
983
1046
  subscribeSource() {
984
1047
  const source = this.options.source;
985
1048
  this.subscription = source.store.subscribe(() => {
@@ -999,6 +1062,7 @@ var Controller = class {
999
1062
  this.reconcile();
1000
1063
  this.refresh();
1001
1064
  this.finishBarrier();
1065
+ this.reportSourceFailure();
1002
1066
  });
1003
1067
  }
1004
1068
  setOptions(next) {
@@ -1022,6 +1086,7 @@ var Controller = class {
1022
1086
  }
1023
1087
  this.reconcile();
1024
1088
  this.refresh();
1089
+ this.reportSourceFailure();
1025
1090
  }
1026
1091
  reconcile() {
1027
1092
  const snapshot = this.sourceState.snapshot;
@@ -1047,11 +1112,12 @@ var Controller = class {
1047
1112
  for (const [name, value] of Object.entries(values)) {
1048
1113
  const input = descriptor.inputs.find((input2) => input2.key === name);
1049
1114
  if (!input && this.pending?.key === key) continue;
1050
- if (input?.domain.selection?.mode === "many" && Array.isArray(value)) {
1115
+ const selection = input && inputSelection(input);
1116
+ if (input && selection?.mode === "many" && Array.isArray(value)) {
1051
1117
  const eligible = value.filter(
1052
- (item) => inputValueInDomain(input.domain, item)
1118
+ (item) => inputTargetInDomain(input.domain, item)
1053
1119
  );
1054
- const max = input.domain.selection.max;
1120
+ const max = selection.max;
1055
1121
  const retained = max === void 0 ? eligible : eligible.slice(0, max);
1056
1122
  if (retained.length !== value.length) {
1057
1123
  kept[name] = retained;
@@ -1082,11 +1148,7 @@ var Controller = class {
1082
1148
  this.subscription?.unsubscribe();
1083
1149
  this.pending = null;
1084
1150
  this.options.source.dispose();
1085
- this.sourceState = {
1086
- ...this.sourceState,
1087
- connection: "closed",
1088
- request: null
1089
- };
1151
+ this.sourceState = this.options.source.store.get();
1090
1152
  this.refresh();
1091
1153
  }
1092
1154
  assertCoverage() {
@@ -1096,40 +1158,35 @@ var Controller = class {
1096
1158
  if (missing.length)
1097
1159
  throw new Error(`Interactions not read: ${missing.join(", ")}`);
1098
1160
  }
1099
- routeTarget(kind, id, explicit, boardId) {
1100
- if (kind === "card") {
1101
- this.selectCard(id, explicit);
1161
+ routeTarget(target, options) {
1162
+ if (target.kind === "card") {
1163
+ this.selectCard(target.value, options);
1102
1164
  return;
1103
1165
  }
1104
1166
  const candidates = this.lastInteractions.flatMap(
1105
1167
  (interaction) => interaction.getInputs().filter(
1106
- (input) => input.descriptor.domain.type === "boardTarget" && (input.descriptor.domain.targetKind === kind || kind === "space" && input.descriptor.domain.targetKind === "tile") && (!boardId || input.descriptor.domain.boardId === boardId) && input.getIsEligible(id) && interaction.getIsAvailable() && (!explicit || interaction.key === explicit)
1168
+ (input) => matchesBoardTarget(input.descriptor, target) && input.getIsEligible(target.value) && interaction.getIsAvailable() && (!options?.interaction || interaction.key === options.interaction) && (!options?.input || input.key === options.input)
1107
1169
  ).map((input) => ({ interaction, input }))
1108
1170
  );
1109
- if (candidates.length > 1) throw new AmbiguousTargetError(id);
1171
+ if (candidates.length > 1)
1172
+ throw new AmbiguousTargetError(JSON.stringify(target.value));
1110
1173
  const match = candidates[0];
1111
- if (match) this.select(match.interaction.key, match.input.key, id);
1174
+ if (match)
1175
+ this.select(match.interaction.key, match.input.key, target.value);
1112
1176
  }
1113
1177
  routeCardDrop(cardId, target) {
1114
1178
  if (this.disposed || this.sourceState.request || this.pending || this.sourceState.connection !== "ready")
1115
1179
  return;
1116
- const card = this.store.get().cards.get(cardId);
1117
- if (!card) return;
1118
- const candidates = card.routes.flatMap((interaction2) => {
1119
- if (!interaction2.getIsAvailable() || target.interaction && interaction2.key !== target.interaction)
1120
- return [];
1121
- const cardInput2 = interaction2.getInputs().find(
1122
- (input2) => input2.descriptor.domain.type === "cardTarget" && input2.getIsEligible(cardId)
1123
- );
1124
- if (!cardInput2) return [];
1125
- return interaction2.getInputs().filter(
1126
- (input2) => input2.descriptor.domain.type === "boardTarget" && (input2.descriptor.domain.targetKind === target.kind || target.kind === "space" && input2.descriptor.domain.targetKind === "tile") && (!target.boardId || input2.descriptor.domain.boardId === target.boardId) && input2.getIsEligible(target.id)
1127
- ).map((input2) => ({ interaction: interaction2, cardInput: cardInput2, input: input2 }));
1128
- });
1129
- if (candidates.length > 1) throw new AmbiguousTargetError(target.id);
1130
- const candidate = candidates[0];
1131
- if (!candidate) return;
1132
- const { interaction, cardInput, input } = candidate;
1180
+ const card = this.store.get().cards.find(cardId);
1181
+ const interaction = card?.routes.find(
1182
+ (route) => route.key === target.interactionKey
1183
+ );
1184
+ if (!interaction?.currentLifetime() || !interaction.getIsAvailable())
1185
+ return;
1186
+ const cardInput = interaction.findInput(target.cardInputKey);
1187
+ const input = interaction.findInput(target.inputKey);
1188
+ if (!cardInput || cardInput.descriptor.domain.type !== "cardTarget" || !cardInput.getIsEligible(cardId) || !input || !matchesBoardTarget(input.descriptor, target) || !input.getIsEligible(target.value))
1189
+ return;
1133
1190
  let next = { ...this.drafts()[interaction.key] };
1134
1191
  routeCardInputIntent(
1135
1192
  {
@@ -1145,14 +1202,14 @@ var Controller = class {
1145
1202
  {
1146
1203
  cardId,
1147
1204
  cardInputKey: cardInput.key,
1148
- dropTarget: { inputKey: input.key, value: target.id }
1205
+ dropTarget: { inputKey: input.key, value: target.value }
1149
1206
  }
1150
1207
  );
1151
1208
  this.writeDraft(interaction.key, next);
1152
1209
  this.activate(interaction.key);
1153
1210
  const current = this.current(interaction.key);
1154
1211
  if (current && shouldAutoSubmitInteraction(current.descriptor) && current.getIsReady())
1155
- this.handle(current.submit());
1212
+ this.handle(() => current.submit());
1156
1213
  }
1157
1214
  warnUnread() {
1158
1215
  const environment = import.meta.env?.DEV ?? globalThis.process?.env?.NODE_ENV === "development";
@@ -1170,7 +1227,7 @@ var Controller = class {
1170
1227
  }
1171
1228
  build(previous) {
1172
1229
  this.trackDrafts();
1173
- const { snapshot, connection, request } = this.sourceState;
1230
+ const { snapshot, connection, request, failure } = this.sourceState;
1174
1231
  const old = previous?.snapshot;
1175
1232
  const drafts = this.drafts();
1176
1233
  const active = this.active();
@@ -1193,11 +1250,20 @@ var Controller = class {
1193
1250
  );
1194
1251
  const players = sameActors && previous ? previous.players : {
1195
1252
  order: Object.freeze(playerObjects.map((p) => p.id)),
1196
- get: (id) => playerObjects.find((p) => p.id === id),
1253
+ get: (id) => requireLookup(
1254
+ playerObjects.find((p) => p.id === id),
1255
+ "Player",
1256
+ id
1257
+ ),
1258
+ find: (id) => playerObjects.find((p) => p.id === id),
1197
1259
  getAll: () => playerObjects,
1198
1260
  next: (id) => {
1199
1261
  const index = playerObjects.findIndex((p) => p.id === id);
1200
- return index < 0 ? void 0 : playerObjects[(index + 1) % playerObjects.length];
1262
+ return requireLookup(
1263
+ index < 0 ? void 0 : playerObjects[(index + 1) % playerObjects.length],
1264
+ "Next player after",
1265
+ id
1266
+ );
1201
1267
  }
1202
1268
  };
1203
1269
  const me = sameActors && previous ? previous.me : snapshot ? {
@@ -1233,6 +1299,14 @@ var Controller = class {
1233
1299
  this.lastInteractions = Object.freeze(interactionObjects);
1234
1300
  const interactions = interactionsSame ? previous.interactions : {
1235
1301
  get: (key) => {
1302
+ this.observed.add(key);
1303
+ return requireLookup(
1304
+ interactionObjects.find((value) => value.key === key),
1305
+ "Interaction",
1306
+ key
1307
+ );
1308
+ },
1309
+ find: (key) => {
1236
1310
  this.observed.add(key);
1237
1311
  return interactionObjects.find((value) => value.key === key);
1238
1312
  },
@@ -1249,7 +1323,12 @@ var Controller = class {
1249
1323
  }
1250
1324
  };
1251
1325
  const inputs = interactionsSame && previous ? previous.inputs : {
1252
- get: (key, name) => interactions.get(key)?.getInput(name)
1326
+ get: (key, name) => requireLookup(
1327
+ interactions.find(key)?.findInput(name),
1328
+ `Input in interaction ${key}`,
1329
+ name
1330
+ ),
1331
+ find: (key, name) => interactions.find(key)?.findInput(name)
1253
1332
  };
1254
1333
  const zonesSame = sameSnapshot && interactionsSame && previous;
1255
1334
  const zones = zonesSame ? previous.zones : (() => {
@@ -1269,6 +1348,7 @@ var Controller = class {
1269
1348
  id,
1270
1349
  index,
1271
1350
  zone.cardViewsById[cardId],
1351
+ zone.cardBacksById[cardId],
1272
1352
  Object.freeze(
1273
1353
  (zone.playableByCardId[cardId] ?? []).map(
1274
1354
  (descriptor) => this.object(
@@ -1291,12 +1371,22 @@ var Controller = class {
1291
1371
  )
1292
1372
  );
1293
1373
  return {
1294
- get: (id) => objects.find((zone) => zone.id === id),
1374
+ get: (id) => requireLookup(
1375
+ objects.find((zone) => zone.id === id),
1376
+ "Zone",
1377
+ id
1378
+ ),
1379
+ find: (id) => objects.find((zone) => zone.id === id),
1295
1380
  getAll: () => Object.freeze(objects)
1296
1381
  };
1297
1382
  })();
1298
1383
  const cards = zonesSame ? previous.cards : {
1299
- get: (id) => zones.getAll().map((zone) => zone.getCard(id)).find((card) => card !== void 0)
1384
+ get: (id) => requireLookup(
1385
+ zones.getAll().map((zone) => zone.findCard(id)).find((card) => card !== void 0),
1386
+ "Card",
1387
+ id
1388
+ ),
1389
+ find: (id) => zones.getAll().map((zone) => zone.findCard(id)).find((card) => card !== void 0)
1300
1390
  };
1301
1391
  const capturedState = this.lastSnapshotDrafts === drafts && previous?.state.activeInteraction === active ? previous.state : immutableCopy({ drafts, activeInteraction: active });
1302
1392
  this.lastSnapshotDrafts = drafts;
@@ -1308,6 +1398,7 @@ var Controller = class {
1308
1398
  version: snapshot?.version ?? null,
1309
1399
  connection,
1310
1400
  request,
1401
+ failure,
1311
1402
  state: capturedState,
1312
1403
  phase: sameSnapshot && previous ? previous.phase : Object.freeze(
1313
1404
  new PhaseObject(snapshot?.frame.flow.currentPhase ?? null)
@@ -1357,7 +1448,8 @@ function createGameInstance() {
1357
1448
  }
1358
1449
 
1359
1450
  export {
1451
+ runtimeFeatures,
1360
1452
  AmbiguousTargetError,
1361
1453
  createGameInstance
1362
1454
  };
1363
- //# sourceMappingURL=chunk-SP234IY4.js.map
1455
+ //# sourceMappingURL=chunk-7RGMPJQG.js.map