@dreamboard-games/sdk 0.5.0-alpha.4 → 0.5.0-alpha.6

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/README.md CHANGED
@@ -13,7 +13,7 @@ pnpm add @dreamboard-games/sdk
13
13
  The package declarations and export map are the API authority. There are four entry points:
14
14
 
15
15
  - `@dreamboard-games/sdk`: framework-free instances, sources, features and canonical host protocol schemas.
16
- - `@dreamboard-games/sdk/react`: typed React provider, selectors, subscriptions, and dnd-kit card/drop hooks.
16
+ - `@dreamboard-games/sdk/react`: typed React provider, selectors, subscriptions, and card gesture and drop-area hooks.
17
17
  - `@dreamboard-games/sdk/reducer`: game authoring, manifest compilation, execution and trusted worker admission.
18
18
  - `@dreamboard-games/sdk/testing`: browser-safe local/scenario sources, replay, inspection and bounded exploration.
19
19
 
@@ -221,7 +221,8 @@ cards: [
221
221
  ```
222
222
 
223
223
  These definitions create runtime IDs `ace-1`, `ace-2`, and `king`, all in the
224
- `ranked` category. When the card schema has `variants`, every `cardType` must
224
+ `ranked` category. Card IDs cannot start with `hidden:`; that prefix is reserved
225
+ for the positions a seat uses to address concealed cards. When the card schema has `variants`, every `cardType` must
225
226
  name one of them; `compileManifest` and `createGame` reject unknown categories.
226
227
  The inferred table narrows a card lookup by its runtime ID to that definition's
227
228
  card set, category, and properties. A variant property overrides a shared
@@ -233,14 +234,15 @@ To migrate an older manifest, replace each card's `type` with `id` and set
233
234
 
234
235
  `createReducerBundle(game)` returns exactly the contract version and four
235
236
  operations: `boardStatic()`, `initialize(input)`, `dispatch({ state, input })`,
236
- and `project({ state, playerIds })`. The runner contract is `0.7.0`; hosts must
237
+ and `project({ state, playerIds })`. The runner contract is `0.8.0`; hosts must
237
238
  require that exact version. Dispatch includes validation, direct transaction mutations, and phase entry.
238
239
  Initialization returns
239
240
  `{ state, terminal?, events? }`, preserving outcomes and events from initial
240
241
  phase entry and returned transitions.
241
242
 
242
- Mutation callbacks use `tx.roll(dieId)`, `tx.shuffle({ zoneId, playerId? })`, and
243
- `tx.deal({ fromZoneId, toZoneId, playerId, count })` directly. Return
243
+ Mutation callbacks use `tx.roll(dieId)`, `tx.shuffle({ zoneId, playerId? })`,
244
+ `tx.deal({ fromZoneId, toZoneId, playerId, count })` and
245
+ `tx.flipCard({ cardId, faceUp })` directly. Return
244
246
  `tx.transition(phaseName)` to enter a phase, including reentering the current
245
247
  phase. An unreturned outcome schedules no work. Entry chains are bounded to
246
248
  1,000 entries per dispatch. `tx.endGame(outcome, { transition })` enters the
@@ -296,10 +298,13 @@ pnpm add @dreamboard-games/sdk react@^19 react-dom@^19 @tanstack/react-store@0.1
296
298
  install the React adapter. The `/react` entry delegates selectors to that package;
297
299
  the application bundler resolves its supported React subscription dependencies.
298
300
 
299
- The SDK includes dnd-kit for `/react` drag and drop. Enable `dragFeature`, export
300
- `useCardDrag` and `useBoardDrop` from the binding, and use the copied Hand and
301
- BoardTargets components or your own renderer. GameProvider owns the drag provider.
302
- Headless `game.drag` remains browser-free and handles atomic domain routing.
301
+ `/react` classifies each card press as a tap, hold, drag or browse. Enable
302
+ `dragFeature`, export `useCardGesture`, `useDropArea` and `useDragOverlay` from the
303
+ binding, and use the copied Hand and BoardTargets components or your own renderer.
304
+ The SDK prescribes no layout. Headless `game.drag` remains browser-free and handles
305
+ atomic domain routing. `fanLayout` computes arc positions for a hand, and
306
+ `originsFeature` adds `card.getOrigin()` so a newly shown card can animate from
307
+ the zone or player it came from.
303
308
 
304
309
  ## Local development and tests
305
310
 
@@ -9,6 +9,66 @@ import {
9
9
  requireLookup
10
10
  } from "./chunk-3JXUL4VN.js";
11
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
+ }
71
+
12
72
  // src/headless/instance.ts
13
73
  import { createStore } from "@tanstack/store";
14
74
 
@@ -348,6 +408,21 @@ var InputObject = class {
348
408
  get game() {
349
409
  return this.interaction.game;
350
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
+ }
351
426
  getDomain() {
352
427
  return this.descriptor.domain;
353
428
  }
@@ -407,28 +482,18 @@ var InputObject = class {
407
482
  onClick: () => this.getSelectHandler(value)()
408
483
  };
409
484
  }
410
- getFieldProps() {
485
+ getControl() {
411
486
  const disabled = !this.interaction.getIsAvailable() || this.interaction.status !== "open" || !this.interaction.connected;
412
- return {
487
+ return createInputControl(this, {
413
488
  ...data("option", this.interaction.key, this.key),
414
489
  "data-seat": this.interaction.seat,
415
- value: this.getValue() ?? "",
416
490
  disabled,
417
- "data-disabled": disabled,
418
- onChange: (event) => {
419
- if (event.currentTarget.value === "") {
420
- this.clear();
421
- return;
422
- }
423
- this.setValue(
424
- this.descriptor.domain.type === "boundedNumber" ? Number(event.currentTarget.value) : event.currentTarget.value
425
- );
426
- }
427
- };
491
+ "data-disabled": disabled
492
+ });
428
493
  }
429
494
  };
430
495
  var CardObject = class {
431
- constructor(owner, id, zone, index, encoded, routes) {
496
+ constructor(owner, id, zone, index, encoded, backImage, routes) {
432
497
  this.owner = owner;
433
498
  this.id = id;
434
499
  this.zone = zone;
@@ -440,6 +505,7 @@ var CardObject = class {
440
505
  ) ?? -1;
441
506
  this.view = encoded ? immutableCopy(JSON.parse(encoded)) : null;
442
507
  this.hidden = this.view === null;
508
+ this.backImage = backImage ?? null;
443
509
  }
444
510
  owner;
445
511
  id;
@@ -450,6 +516,7 @@ var CardObject = class {
450
516
  seat;
451
517
  view;
452
518
  hidden;
519
+ backImage;
453
520
  get game() {
454
521
  return this.owner.instance;
455
522
  }
@@ -602,7 +669,60 @@ var Controller = class {
602
669
  this.localDrafts = options.initialState?.drafts ?? EMPTY_DRAFTS;
603
670
  this.localActive = options.initialState?.activeInteraction ?? null;
604
671
  this.sourceState = options.source.store.get();
672
+ const current = () => this.building ?? this.store.get();
605
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
+ },
606
726
  getSnapshot: () => this.building ?? this.store.get(),
607
727
  getOptions: () => this.options,
608
728
  setOptions: (next) => this.setOptions(next),
@@ -614,35 +734,23 @@ var Controller = class {
614
734
  assertCoverage: () => this.assertCoverage(),
615
735
  inspect: () => this.store.get()
616
736
  };
617
- this.instance = root;
618
- for (const key of [
619
- "snapshot",
620
- "view",
621
- "version",
622
- "connection",
623
- "failure",
624
- "request",
625
- "state",
626
- "phase",
627
- "turn",
628
- "me",
629
- "players",
630
- "interactions",
631
- "inputs",
632
- "zones",
633
- "cards",
634
- "events"
635
- ])
636
- Object.defineProperty(root, key, {
637
- get: () => (this.building ?? this.store.get())[key]
638
- });
639
- this.store = createStore(Object.freeze(this.build()));
640
- Object.defineProperty(root, "store", {
641
- value: {
642
- get: () => this.store.get(),
643
- 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
+ });
644
745
  }
746
+ }
747
+ Object.defineProperty(root, "store", {
748
+ enumerable: false,
749
+ configurable: false,
750
+ writable: false
645
751
  });
752
+ this.instance = root;
753
+ this.store = createStore(Object.freeze(this.build()));
646
754
  if ("apply" in options.source)
647
755
  Object.defineProperty(root, "apply", {
648
756
  value: (action) => this.options.source.apply(action)
@@ -651,9 +759,14 @@ var Controller = class {
651
759
  Object.defineProperty(root, "explore", {
652
760
  value: (...args) => this.options.source.explore(...args)
653
761
  });
654
- const context = {
655
- createBoard: (id, data2) => this.object("board", {
656
- 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,
657
770
  data: immutableCopy(data2),
658
771
  game: this.instance
659
772
  }),
@@ -663,6 +776,7 @@ var Controller = class {
663
776
  if (!this.disposed) this.refresh();
664
777
  }
665
778
  };
779
+ const context = { ...runtime, [runtimeFeatures]: runtime };
666
780
  for (const feature of Object.values(
667
781
  features?.(this.instance, context) ?? {}
668
782
  )) {
@@ -725,7 +839,8 @@ var Controller = class {
725
839
  );
726
840
  }
727
841
  object(hook, value) {
728
- return Object.freeze(Object.setPrototypeOf(value, this.prototypes[hook]));
842
+ Object.setPrototypeOf(value, this.prototypes[hook]);
843
+ return Object.freeze(value);
729
844
  }
730
845
  drafts() {
731
846
  if (this.options.state && this.options.state === this.invalidControlledState)
@@ -1069,10 +1184,17 @@ var Controller = class {
1069
1184
  if (!interaction?.currentLifetime() || !interaction.getIsAvailable())
1070
1185
  return;
1071
1186
  const cardInput = interaction.findInput(target.cardInputKey);
1072
- const input = interaction.findInput(target.inputKey);
1073
- if (!cardInput || cardInput.descriptor.domain.type !== "cardTarget" || !cardInput.getIsEligible(cardId) || !input || !matchesBoardTarget(input.descriptor, target) || !input.getIsEligible(target.value))
1187
+ if (!cardInput || cardInput.descriptor.domain.type !== "cardTarget" || !cardInput.getIsEligible(cardId))
1188
+ return;
1189
+ const input = target.kind === "interaction" ? void 0 : interaction.findInput(target.inputKey);
1190
+ if (target.kind !== "interaction" && (!input || !matchesBoardTarget(input.descriptor, target) || !input.getIsEligible(target.value)))
1074
1191
  return;
1075
1192
  let next = { ...this.drafts()[interaction.key] };
1193
+ const chosen = next[cardInput.key];
1194
+ if (!input && isManyInput(cardInput.descriptor) && Array.isArray(chosen) && chosen.includes(cardId)) {
1195
+ this.activate(interaction.key);
1196
+ return;
1197
+ }
1076
1198
  routeCardInputIntent(
1077
1199
  {
1078
1200
  getDraft: () => next,
@@ -1087,7 +1209,7 @@ var Controller = class {
1087
1209
  {
1088
1210
  cardId,
1089
1211
  cardInputKey: cardInput.key,
1090
- dropTarget: { inputKey: input.key, value: target.value }
1212
+ dropTarget: input && target.kind !== "interaction" ? { inputKey: input.key, value: target.value } : void 0
1091
1213
  }
1092
1214
  );
1093
1215
  this.writeDraft(interaction.key, next);
@@ -1233,6 +1355,7 @@ var Controller = class {
1233
1355
  id,
1234
1356
  index,
1235
1357
  zone.cardViewsById[cardId],
1358
+ zone.cardBacksById[cardId],
1236
1359
  Object.freeze(
1237
1360
  (zone.playableByCardId[cardId] ?? []).map(
1238
1361
  (descriptor) => this.object(
@@ -1331,8 +1454,78 @@ function createGameInstance() {
1331
1454
  };
1332
1455
  }
1333
1456
 
1457
+ // src/headless/gesture.ts
1458
+ var GESTURE_THRESHOLDS = Object.freeze({
1459
+ /** Movement, in CSS pixels, before a press becomes a drag or a browse. */
1460
+ slop: 8,
1461
+ /** A touch or pen held this long without moving inspects the card. */
1462
+ holdMs: 350,
1463
+ /** A mouse resting on a card this long inspects it. */
1464
+ hoverMs: 250,
1465
+ /** A finger drags when upward travel exceeds this multiple of sideways travel. */
1466
+ axisBias: 1.25
1467
+ });
1468
+ function createGestureRecognizer(down, callbacks) {
1469
+ const origin = { x: down.clientX, y: down.clientY };
1470
+ const mouse = down.pointerType === "mouse";
1471
+ let kind = "tap";
1472
+ let finished = false;
1473
+ const holdTimer = mouse ? void 0 : setTimeout(() => {
1474
+ kind = "hold";
1475
+ callbacks.hold();
1476
+ }, GESTURE_THRESHOLDS.holdMs);
1477
+ const point = (event) => ({
1478
+ x: event.clientX,
1479
+ y: event.clientY
1480
+ });
1481
+ const ours = (event) => !finished && (!event || event.pointerId === down.pointerId);
1482
+ function finish() {
1483
+ finished = true;
1484
+ clearTimeout(holdTimer);
1485
+ }
1486
+ return {
1487
+ move(event) {
1488
+ if (!ours(event)) return;
1489
+ if (kind === "drag") return callbacks.dragMove(point(event));
1490
+ const dx = event.clientX - origin.x;
1491
+ const dy = event.clientY - origin.y;
1492
+ if (kind !== "tap" || Math.hypot(dx, dy) < GESTURE_THRESHOLDS.slop)
1493
+ return;
1494
+ clearTimeout(holdTimer);
1495
+ const lifts = mouse || -dy > GESTURE_THRESHOLDS.axisBias * Math.abs(dx);
1496
+ if (lifts && callbacks.dragStart(origin)) {
1497
+ kind = "drag";
1498
+ callbacks.dragMove(point(event));
1499
+ } else kind = "browse";
1500
+ },
1501
+ up(event) {
1502
+ if (!ours(event)) return;
1503
+ finish();
1504
+ callbacks.end(kind, point(event));
1505
+ },
1506
+ cancel(event) {
1507
+ if (!ours(event)) return;
1508
+ finish();
1509
+ callbacks.cancel(kind);
1510
+ }
1511
+ };
1512
+ }
1513
+
1514
+ // src/headless/drop-targets.ts
1515
+ function isSameDropTarget(left, right) {
1516
+ if (left.interactionKey !== right.interactionKey || left.cardInputKey !== right.cardInputKey)
1517
+ return false;
1518
+ if (left.kind === "interaction" || right.kind === "interaction")
1519
+ return left.kind === right.kind;
1520
+ return left.kind === right.kind && left.valueKind === right.valueKind && inputValueKey(left.value) === inputValueKey(right.value) && (left.valueKind !== "board-id" || right.valueKind === "board-id" && left.boardId === right.boardId) && left.inputKey === right.inputKey;
1521
+ }
1522
+
1334
1523
  export {
1524
+ runtimeFeatures,
1335
1525
  AmbiguousTargetError,
1336
- createGameInstance
1526
+ createGameInstance,
1527
+ isSameDropTarget,
1528
+ GESTURE_THRESHOLDS,
1529
+ createGestureRecognizer
1337
1530
  };
1338
- //# sourceMappingURL=chunk-OOFMBCFF.js.map
1531
+ //# sourceMappingURL=chunk-CSHKCPBY.js.map