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

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 (40) hide show
  1. package/README.md +344 -21
  2. package/dist/board-element-DXLPBVk-.d.ts +13 -0
  3. package/dist/{chunk-LRILEVKC.js → chunk-63GWNBZX.js} +2763 -2327
  4. package/dist/chunk-63GWNBZX.js.map +1 -0
  5. package/dist/{chunk-SP234IY4.js → chunk-BVWJS3AB.js} +752 -343
  6. package/dist/chunk-BVWJS3AB.js.map +1 -0
  7. package/dist/{chunk-ZPRKWPEP.js → chunk-CWANHA4C.js} +284 -138
  8. package/dist/chunk-CWANHA4C.js.map +1 -0
  9. package/dist/chunk-CZRPUWCT.js +1602 -0
  10. package/dist/chunk-CZRPUWCT.js.map +1 -0
  11. package/dist/{chunk-FMM5LVJW.js → chunk-IEMZ26FI.js} +192 -383
  12. package/dist/chunk-IEMZ26FI.js.map +1 -0
  13. package/dist/{diagnostics-B0lgnuZ6.d.ts → diagnostics-D3mzqjxL.d.ts} +2 -3
  14. package/dist/extract-Bi6_tzUr.d.ts +6899 -0
  15. package/dist/index.d.ts +2058 -385
  16. package/dist/index.js +1260 -288
  17. package/dist/index.js.map +1 -1
  18. package/dist/react.d.ts +153 -8
  19. package/dist/react.js +1396 -30
  20. package/dist/react.js.map +1 -1
  21. package/dist/reducer.d.ts +592 -776
  22. package/dist/reducer.js +4617 -4055
  23. package/dist/reducer.js.map +1 -1
  24. package/dist/shortcuts-CuQXWAMv.d.ts +824 -0
  25. package/dist/testing.d.ts +59 -53
  26. package/dist/testing.js +335 -190
  27. package/dist/testing.js.map +1 -1
  28. package/dist/types-BlvVQ9WL.d.ts +125 -0
  29. package/package.json +17 -17
  30. package/dist/chunk-DMFVAZI2.js +0 -16
  31. package/dist/chunk-DMFVAZI2.js.map +0 -1
  32. package/dist/chunk-FMM5LVJW.js.map +0 -1
  33. package/dist/chunk-LRILEVKC.js.map +0 -1
  34. package/dist/chunk-SP234IY4.js.map +0 -1
  35. package/dist/chunk-VUWNJXLJ.js +0 -52
  36. package/dist/chunk-VUWNJXLJ.js.map +0 -1
  37. package/dist/chunk-ZPRKWPEP.js.map +0 -1
  38. package/dist/definition-BaHNPZol.d.ts +0 -4639
  39. package/dist/model-BHro6yXu.d.ts +0 -288
  40. package/dist/types-DimqnJBq.d.ts +0 -86
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, selector hook and subscription component.
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
 
@@ -82,7 +82,7 @@ const ownCard = play.inputs.card({
82
82
  id: "own-card",
83
83
  errorCode: "NOT_YOUR_CARD",
84
84
  test: ({ q, playerId, targetId }) =>
85
- q.zone.playerCards(playerId, "hand").includes(targetId),
85
+ q.zone("hand", playerId).includes(targetId),
86
86
  },
87
87
  });
88
88
 
@@ -95,11 +95,10 @@ export default play.define({
95
95
  inputs: { cardId: ownCard },
96
96
  reduce({ tx, input, q }) {
97
97
  // tx.state.phase.leadCardId and input.params.cardId are inferred.
98
- tx.moveCardFromPlayerZoneToSharedZone({
99
- playerId: input.playerId,
100
- fromZoneId: "hand",
101
- toZoneId: "trick",
102
- cardId: input.params.cardId,
98
+ tx.moveComponentToZone({
99
+ componentId: input.params.cardId,
100
+ to: { zoneId: "trick" },
101
+ playedBy: input.playerId,
103
102
  });
104
103
  tx.patchPhaseState({ leadCardId: input.params.cardId });
105
104
  const next = q.player.nextInOrder(input.playerId);
@@ -107,7 +106,7 @@ export default play.define({
107
106
  tx.patchPublicState({ currentPlayerId: next });
108
107
  tx.setActivePlayers([next]);
109
108
  }
110
- if (tx.q.zone.playerCards(input.playerId, "hand").length === 0) {
109
+ if (tx.q.zone("hand", input.playerId).length === 0) {
111
110
  return tx.transition("setup");
112
111
  }
113
112
  },
@@ -168,7 +167,7 @@ export default game.assemble({
168
167
  phases: { setup, play },
169
168
  view: game.view(({ state, playerId, q }) => ({
170
169
  me: playerId,
171
- hand: q.zone.playerCards(playerId, "hand"),
170
+ hand: q.zone("hand", playerId),
172
171
  current: state.publicState.currentPlayerId,
173
172
  })),
174
173
  });
@@ -182,24 +181,119 @@ imports the assembled game, so there is no import cycle.
182
181
  New workspaces keep authored starter code in `app/game.ts` and `ui/App.tsx`.
183
182
  Import the manifest directly. `compileManifest(manifest)` provides inferred ID schemas,
184
183
  table schemas, fresh initial tables, and board metadata in memory. `createGame`
185
- also accepts the authored manifest directly. Bind a typed React hook with
186
- `createGameHook<Game>()({ features, coverage })` from `@dreamboard-games/sdk/react`,
184
+ requires that compiled manifest when binding state schemas. Bind a typed React hook with
185
+ `createGameHook<Game>()({ features })` from `@dreamboard-games/sdk/react`,
187
186
  and pass a source to its `GameProvider`. The hosted UI imports `Game` only as a type;
188
187
  `iframeSource()` supplies authoritative frames and handles commands.
189
188
  No authoring generation step or shared workspace files are needed.
190
189
 
190
+ `createInitialTable({ playerIds })` takes the actual roster explicitly. Cards,
191
+ piece, die and tile seeds can use `scope: "perPlayer"` to replicate for those
192
+ seats, with initial ownership derived from the replication origin. Ownership
193
+ changes do not change instance IDs. Use `perPlayerInstanceId` when constructing
194
+ a reference from a known family, expanded base and seat; decoding validates
195
+ syntax only, while table admission checks live membership. UI consumers should
196
+ use the canonical IDs supplied by projected boards and eligible targets.
197
+ Instance record factories likewise require `{ playerIds }`, for example
198
+ `manifest.records.pieceIds(0, { playerIds })`. Player IDs are nonempty and unique;
199
+ `__proto__` is reserved to preserve roster keys through record parsing.
200
+
201
+ Zones declare either `scope` or `attachedTo` a board, generic board space, tile
202
+ cell, piece type, or die type. Query and move through the same zone API with the concrete host ID;
203
+ use `boardSpaceHostId(boardId, spaceId)` for a space attachment. Component-hosted
204
+ access follows the host's current owner, independently of contained ownership
205
+ and card face state. Initialization, movement and restore reject containment
206
+ cycles. Containers, slots and their location variants are removed.
207
+
208
+ `tileTypes` own immutable hex/square cells, annotations and rule fields;
209
+ `tileSeeds` create game-owned instances with independent ownership and mutable
210
+ properties. `q.tile(id)` addresses that inventory, including detached tiles.
211
+ Canonical `OnBoard` locations own placement. Hex/square topology is derived from
212
+ those locations and immutable definitions; runtime boards store only `baseId`
213
+ and explicit session relations. Cell identity follows the tile instance and
214
+ local cell, while world edges and vertices belong to the exact board instance.
215
+ The flat seat `boards` projection is the sole client topology source.
216
+
217
+ Private tile destinations remain rejected in this layer. Generic component
218
+ moves cannot remove a placed tile or place tiles on component spaces, edges or
219
+ vertices. Tile-cell zone hosts must remain empty while their tile is unplaced.
220
+ The existing UI zone facade still presents cards.
221
+
222
+ Manifest card counts and explicit piece/die/tile seed counts must be positive safe
223
+ integers. Omitted seed counts mean one copy. Zero, negative, fractional, non-finite,
224
+ and unsafe counts fail before ID expansion; negative, zero, and fractional literals
225
+ also fail typechecking. Widened `number` values still require runtime validation.
226
+ Player bounds must be positive safe integers with `minPlayers <= maxPlayers`.
227
+
228
+ `defineTopologyManifest` returns `ValidatedManifest<M>` after semantic validation.
229
+ `compileManifest` returns a branded `CompiledManifest<M>` after validation and table
230
+ materialization. Both snapshot the authored input so later source edits do not
231
+ change the validated value or future initial tables. Literal IDs remain inferred.
232
+
233
+ `ReducerGameDefinitionInput` describes the authored assembly fields. The assembled
234
+ `ReducerGameDefinition` adds the bound contract and validation brand; authors never
235
+ supply those output-only fields.
236
+
237
+ `game.assemble` checks phase names, the initial phase, interaction declarations,
238
+ and simultaneous-phase requirements, then returns a branded `ReducerGameDefinition`.
239
+ Runtime consumers such as `createReducerBundle` require that assembled type;
240
+ handwritten structural lookalikes do not satisfy it. These brands record SDK
241
+ construction checks, not correctness of arbitrary reducer callbacks, and do not
242
+ replace validation of runtime commands or state. As with other TypeScript types,
243
+ explicit assertions can bypass them; treat constructed definitions as immutable.
244
+ Each card in a manual card set requires `id` for its definition and `cardType`
245
+ for its category:
246
+
247
+ ```ts
248
+ cards: [
249
+ { id: "ace", cardType: "ranked", name: "Ace", count: 2, properties: {} },
250
+ { id: "king", cardType: "ranked", name: "King", count: 1, properties: {} },
251
+ ];
252
+ ```
253
+
254
+ These definitions create runtime IDs `ace-1`, `ace-2`, and `king`, all in the
255
+ `ranked` category. Component IDs cannot start with `card-ref:`; that prefix is reserved
256
+ for opaque concealed-card references. These references belong to one seat and
257
+ authority version and expire after commits or restores. When the card schema has `byCardType`, every `cardType` must
258
+ name one of its schemas; `compileManifest` rejects unknown categories before `createGame` binds the result.
259
+ The inferred table narrows a card lookup by its runtime ID to that definition's
260
+ card set, category, and properties. Author field schemas with `z.object`. For card categories, use
261
+ `cardSchema: { byCardType: { ranked: base.extend({ points: z.number().int() }) } }`
262
+ to reuse shared properties. Zod `.extend` defines property overrides in both
263
+ the runtime schema and inferred type. Fields are required unless authored with
264
+ `.optional()` or an explicit `.default(...)`; primitive defaults are not inferred.
265
+ Use `ref.cardId()`, `ref.pieceId()`, and the other `ref` markers for manifest
266
+ references. `defineTopologyManifest` exports portable JSON Schema while
267
+ retaining the authored types; refinements, transforms, and unsupported Zod
268
+ constructs fail at definition time.
269
+ For external JSON documents, call `parseTopologyManifestJson(value)` before
270
+ `compileManifest`. The parser checks structural keys, field schemas and manifest
271
+ references; transport services can preserve the document without duplicating
272
+ the SDK's model.
273
+ To migrate an older manifest, replace each card's `type` with `id` and set
274
+ `cardType` explicitly (often to the former `type` value).
275
+
191
276
  ## Reducer runner contract
192
277
 
193
- `createReducerBundle(game)` returns exactly the contract version and four
194
- operations: `boardStatic()`, `initialize(input)`, `dispatch({ state, input })`,
195
- and `project({ state, playerIds })`. The runner contract is `0.6.0`; hosts must
196
- require that exact version. Dispatch includes validation, direct transaction mutations, and phase entry.
278
+ `createReducerBundle(game)` returns exactly the contract version and three
279
+ operations: `initialize(input)`, `dispatch({ state, input, referenceBasis })`,
280
+ and `project({ state, playerIds, referenceBasis })`. The runner contract is `0.11.0`; hosts must
281
+ require that exact version. The host supplies `referenceBasis: { sessionId, version }`:
282
+ use a fresh session ID for each authority lifetime, increment version exactly once
283
+ for every accepted input, and advance it on restore without restoring an old
284
+ version from checkpoint data. Never change authoritative state under the same
285
+ basis. Submitted inputs carry the complete issuing frame basis; the host checks
286
+ its action-set version before dispatch. Dispatch includes validation, direct
287
+ transaction mutations, and phase entry.
197
288
  Initialization returns
198
289
  `{ state, terminal?, events? }`, preserving outcomes and events from initial
199
290
  phase entry and returned transitions.
200
291
 
201
- Mutation callbacks use `tx.roll(dieId)`, `tx.shuffle({ zoneId, playerId? })`, and
202
- `tx.deal({ fromZoneId, toZoneId, playerId, count })` directly. Return
292
+ Mutation callbacks use `tx.roll(dieId)`, `tx.shuffle({ zone: { zoneId, hostId } })`,
293
+ `tx.deal({ from, to, count })` and
294
+ `tx.flipCard({ cardId, faceUp })` directly. Like events, the zones the latest
295
+ accepted operation shuffled are recorded in the runtime state; every seat that
296
+ sees such a zone learns that it was shuffled, never its new order. Return
203
297
  `tx.transition(phaseName)` to enter a phase, including reentering the current
204
298
  phase. An unreturned outcome schedules no work. Entry chains are bounded to
205
299
  1,000 entries per dispatch. `tx.endGame(outcome, { transition })` enters the
@@ -234,7 +328,8 @@ implicit costs, guidance metadata, and phase zone declarations are removed.
234
328
 
235
329
  A game authors one `view` for each requested seat. Public and private fields
236
330
  compose in that function; the transport never uses a seat view as a spectator
237
- payload. Static boards come directly from the compiled manifest.
331
+ payload. Shared static boards come from compiled definitions; per-player boards
332
+ are instantiated for the actual roster.
238
333
 
239
334
  Use `memoize((input: SomeImmutableObject) => result)` for shared pure calculations.
240
335
  It caches by object identity with a WeakMap, including `undefined` results. Pass
@@ -255,6 +350,149 @@ pnpm add @dreamboard-games/sdk react@^19 react-dom@^19 @tanstack/react-store@0.1
255
350
  install the React adapter. The `/react` entry delegates selectors to that package;
256
351
  the application bundler resolves its supported React subscription dependencies.
257
352
 
353
+ `/react` classifies each card press as a tap, hold, drag or browse. Enable
354
+ `dragFeature`, export `useCardGesture`, `useCardRow`, `useActiveCard`, `useDropArea` and `useDragOverlay` from the
355
+ binding, and use the copied Hand and BoardTargets components or your own renderer.
356
+ The SDK prescribes no layout. Headless `game.drag` remains browser-free and handles
357
+ atomic domain routing. A drop area can bind typed inputs with
358
+ `{ interaction: "play.move", params: { destination: "discard" } }`; the SDK
359
+ adds the dragged card and admits the complete draft against current domains.
360
+ A ready bound single-card drop submits the action even when its commit mode is
361
+ manual; unbound drops and many-card drafts keep their normal commit behavior.
362
+ `game.drag.getIsDropTarget(target)` uses the same admission as dropping.
363
+ Interaction `getSubmitProps(params)` and `getSubmitHandler(params)` accept the
364
+ same explicit params as `submit(params)`, retaining pending and connection gates.
365
+ `fanLayout` computes arc positions that always fit a hand,
366
+ `handFan` adds a focused, readable card with configurable presets and timing,
367
+ and
368
+ `originsFeature` adds `card.getOrigin()` so a newly shown card can animate from
369
+ the zone or player it came from, and `zone.getShuffle()` so a shuffled pile can
370
+ riffle.
371
+
372
+ ## Local hand sorting
373
+
374
+ Install `handFeature(core, context, { zones })` with named sorts for each zone:
375
+ `{ hand: { defaultSort: "rank", sorts: { rank: { compare } } } }`.
376
+ Comparators receive the seat's projected `Card` objects; narrow `card.hidden`
377
+ before reading visible card data. Equal comparisons retain source order.
378
+
379
+ `game.hand.getSortModes(zone)` returns typed mode IDs in declaration order.
380
+ Read `getSortMode(zone)` and `getSortedCardIds(zone)`, then call
381
+ `setSortMode(zone, mode)` from your UI. The zone carries its canonical host;
382
+ each host's choice is independent. Labels, buttons and cycling are authored UI.
383
+ Without a selected or default mode, the order comes directly from the source.
384
+ The feature retains the zone's selected/selectable card ID helpers.
385
+
386
+ Sort changes notify subscribers and preserve drafts and card selection without
387
+ submitting. Choices survive frames and reset on source or seat changes. Captured
388
+ `game.hand` branches retain their modes, and sorted IDs are derived from the
389
+ projected zone passed to that branch. Old lifetime setters and disposed setters
390
+ do nothing. See [Optional features](../../docs/api/features.md).
391
+
392
+ ## Opt-in game shortcuts
393
+
394
+ Install `shortcutsFeature` in the same `createGameHook<Definition>()` binding as
395
+ other features, and export its inferred `useGameShortcuts` and
396
+ `useShortcutTarget` and `useShortcutHints` hooks. A child inside the bound `GameProvider` installs the
397
+ app's bindings once:
398
+
399
+ ```tsx
400
+ function AppShortcuts() {
401
+ useGameShortcuts({
402
+ bindings: [
403
+ {
404
+ kind: "interaction",
405
+ keys: ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
406
+ label: "Draw cards",
407
+ target: "zone",
408
+ zoneId: "deck",
409
+ interaction: "play.draw",
410
+ inputs: ({ key }) => ({ count: Number(key) }),
411
+ },
412
+ {
413
+ kind: "interaction",
414
+ keys: ["f"],
415
+ label: "Flip card",
416
+ target: "card",
417
+ input: "card",
418
+ interaction: "play.flip",
419
+ inputs: ({ target }) => ({ card: target.value }),
420
+ },
421
+ {
422
+ kind: "local",
423
+ keys: ["s"],
424
+ label: "Change hand order",
425
+ target: "zone",
426
+ zoneId: "hand",
427
+ getIsAvailable: ({ target }) => target.hostId === playerId,
428
+ run: ({ target }) => changeAuthoredOrder(target.hostId),
429
+ },
430
+ ],
431
+ });
432
+ return null;
433
+ }
434
+ ```
435
+
436
+ These interaction and input names are examples from an authored reducer;
437
+ `playerId` and `changeAuthoredOrder` are supplied by the game. The
438
+ binding's discriminated types derive from that reducer: interaction names,
439
+ input keys, target kinds, and every required payload field must match. A game
440
+ that exposes a counted draw submits one `count` payload; shortcuts never repeat
441
+ a draw-one action. No actions or keys are supplied by default.
442
+
443
+ Local bindings run game-authored presentation callbacks for a focused zone
444
+ control or a focused card in that zone. A focused card resolves its current
445
+ containing zone and actual host, including opaque hidden card references. Local
446
+ bindings always use real DOM focus, including focus acquired by clicking; hover
447
+ alone never activates them. Mouse browsing can change the interaction target
448
+ while local actions retain the focused control's zone. They are unavailable
449
+ during a card pointer gesture or its settling drag overlay. Games own any
450
+ further restrictions through the optional pure `getIsAvailable` predicate.
451
+ They do not submit source commands, alter interaction drafts, or require the
452
+ player's reducer turn; an interaction request does not block a local action.
453
+ `run` returns `void` or `Promise<void>` and is never evaluated for hints.
454
+ The SDK supplies no sorting modes or cycling policy.
455
+
456
+ Card controls using `useCardGesture` share its canonical pointer and keyboard
457
+ focus. Zone controls spread
458
+ `useShortcutTarget({ kind: "zone", zoneId: "deck", hostId: "table" }).props`;
459
+ `hostId` must be the actual host allowed by that zone's attachment. Selectable
460
+ board and tile controls can use the same hook with their canonical
461
+ `SelectionTarget`. Hidden cards retain their opaque seat references.
462
+
463
+ Keys are exact browser `event.key` strings. The adapter ignores held repeats,
464
+ composition, editing controls, modifier combinations, and open modal or menu
465
+ surfaces. Native radio inputs can carry a zone target for authored shortcuts;
466
+ their ordinary arrow and space keys remain native unless explicitly bound.
467
+ It prevents the browser default only when a single eligible binding
468
+ handles the key. Source and seat changes clear the gesture target; unmounting
469
+ removes the listener and binding hints.
470
+
471
+ Input factories must be pure: eligibility and hints evaluate them against the
472
+ current projected domains. Availability predicates for local bindings must
473
+ also be pure. `game.shortcuts.getHints(target, activity)` returns only
474
+ currently usable, unambiguous keys with their labels and `kind`. Interaction
475
+ hints also carry their typed interaction name; local hints do not. Both variants
476
+ share key collision handling. Headless callers supply local focus explicitly as
477
+ `activity: { focusedTarget, pointerActive, touch }`; omitting activity admits
478
+ only interaction bindings. Hints are empty while `touch` is true. The mounted
479
+ hooks set it when the latest press was a finger and clear it on the next key or
480
+ mouse press; the keys themselves keep working. `useShortcutHints(target)` reads
481
+ those hints reactively and returns no hints when the feature is absent. The mounted
482
+ `useGameShortcuts` hook returns `{ target, hints }` for a reserved help/status
483
+ area. Rejected actions and transport failures use the binding's existing
484
+ `onError` handler; callbacks from an old source, seat, or unmounted child are
485
+ ignored. `game.shortcuts.handle(key, target, activity)` returns `null` for an
486
+ unhandled
487
+ key, or a promise of `{ kind: "local" }` or
488
+ `{ kind: "interaction", result: SubmitResult }`. Callback failures reject that
489
+ promise and use the same `onError` path. Interaction bindings use the same
490
+ atomic `interaction.submit(params)` path available to authored action buttons.
491
+ Explicit params are validated against the current projected descriptor without
492
+ mutating drafts. Existing `submit()` continues to submit the interaction draft
493
+ and defaults; both forms share availability and in-flight lifecycle barriers.
494
+ The headless feature installs no DOM listeners.
495
+
258
496
  ## Local development and tests
259
497
 
260
498
  `localSource(game, { players, seed, as, options })` executes the production reducer
@@ -264,12 +502,97 @@ the hosted entry uses only `iframeSource()` and type imports.
264
502
 
265
503
  Local sources expose `inspect`, bounded `explore`, typed explicit-actor `apply`,
266
504
  `switchSeat`, `checkpoint`, and validated `restore`. A JSON checkpoint preserves
267
- pending selections and terminal state; restoring does not replay commands.
505
+ visible pending selections and terminal state; restoring does not replay commands.
506
+ Concealed selections expire after any other accepted input or restore. Each
507
+ accepted extension of that same unfinished selection issues a new validity stamp
508
+ for the immediately following frame. This prevents drafts from tracking hidden
509
+ tiles or cards through a shuffle, including a shuffle that leaves their position
510
+ unchanged.
268
511
  `createTestSource(snapshot)` supplies controlled frames and acknowledgements for
269
512
  instance and React tests. Static and hosted sources do not expose `apply`.
270
513
 
271
514
  Hosts import `assertReducerBundleContract`, `REDUCER_CONTRACT_VERSION`,
272
515
  `ReducerWire` types and `ReducerWireZod` schemas from `/reducer`. Canonical iframe
273
516
  and gameplay websocket schemas plus `materializePluginGameplayFrame` live at the
274
- root. Materialize the seat projection with static board data before publishing it;
275
- sources publish the canonical seat view and keep command bases private.
517
+ root. Materialize the requested seat directly from its projection before publishing it;
518
+ sources publish the canonical seat view and submit the exact frame basis.
519
+
520
+ Visible cards share one complete `ViewCard` shape: `id`, `cardType`, and JSON
521
+ `properties`, plus optional `name`, `text`, `frontImage`, and `backImage`.
522
+ `ViewCardSchema` validates this shape. Reducer and gameplay `cardViewsById` maps
523
+ contain these objects directly. Headless `card.view` preserves the manifest's
524
+ card identity, category, and property inference and is deeply readonly; table-only
525
+ `cardSetId` and `componentType` are absent. Concealed cards have a positional
526
+ opaque seat identity, no view, and an optional separate `cardBacksById` entry.
527
+ This wire format requires plugin protocol version 11 and reducer contract `0.11.0`.
528
+
529
+ Tiles use opaque `SeatTileRef` values even when visible. A projected tile is either
530
+ `{ disclosure: "visible", ref, tileTypeId, name, ownerId, fields, properties, frontImage? }` or
531
+ `{ disclosure: "concealed", ref, appearance }`; an omitted tile contributes no
532
+ entry or count. Public appearances are authored independently of secret faces.
533
+ Boards and zones each carry their own projected tiles. Only visible tiles
534
+ contribute cells, edges, vertices, and relations to the seat's board topology.
535
+ Use `phase.inputs.tile({ from: [zoneId] })` or
536
+ `phase.inputs.tile({ boards: [boardId] })` to select a tile. Reducers receive
537
+ its authoritative identity after the SDK validates and resolves the reference.
538
+ `tx.setTileDisclosure` controls face audiences and public appearance;
539
+ `tx.setBoardVisibility` controls the board's audience. Location visibility always
540
+ caps tile disclosure. Authored defaults cannot identify concealed tiles or cards.
541
+ Game-owned view fields can publish visible references through `references.tile`
542
+ and `references.space`; arbitrary authored text and JSON remain explicit publication.
543
+
544
+ Every `tx.emit` event declares `audience: { kind: "public" }` or
545
+ `{ kind: "seats", playerIds }`. A typed detail `{ kind: "tile", tileId }` is
546
+ projected only when that tile is visible to the audience. Identity-specific event
547
+ details and view references never track a concealed tile. Clients receive only
548
+ their own events and admitted tile references.
549
+
550
+ Card sets contain their authored `cards`, `cardSchema`, and `defaultHome` directly.
551
+ Standard playing cards are game-owned definitions with ordinary suit/rank
552
+ properties; the SDK does not synthesize inventories or assign built-in rules.
553
+
554
+ The exported `createGestureRecognizer(down, callbacks, options)` defaults to
555
+ vertical touch dragging, up or down, so card containers retain sideways
556
+ scrolling. A row that fits uses `{ scrub: true }`: a sliding finger reports each
557
+ move through `browse`, and turning up or down still drags. Pile controls can use
558
+ `{ dragDirection: "any" }` with `touch-action: none` to drag toward a hand
559
+ beside them. This classifies pointer intent only; submit an authored interaction
560
+ through the bound game to enforce its rules, without selecting a hidden card ID.
561
+
562
+ ### Local card groups and marquee selection
563
+
564
+ `cardSelectionFeature` holds a local list of projected card IDs, separate from
565
+ interaction drafts. Authors decide which zones allow selection and how to draw
566
+ it. Selection clears on an authoritative frame or source/perspective change.
567
+
568
+ ```ts
569
+ features(core, context) {
570
+ const selection = cardSelectionFeature(core, context);
571
+ return {
572
+ selection,
573
+ drag: dragFeature(core, context, {
574
+ getSelection(cardId) {
575
+ const ids = selection.root.cardSelection.cardIds;
576
+ return ids.includes(cardId) ? ids : undefined;
577
+ },
578
+ }),
579
+ };
580
+ }
581
+ ```
582
+
583
+ Use `game.cardSelection.set(ids)`, `.toggle(id)` and `.clear()` for authored
584
+ controls. A selected group can drop only onto an interaction with a `many(...)`
585
+ card input that admits the entire group. Dropping submits that input once in
586
+ selection order, replacing its draft; it never submits a sequence of single-card
587
+ moves. The reducer remains responsible for validating and applying the group
588
+ atomically. An unselected card retains ordinary single-card draft behavior.
589
+ `useDragOverlay().cardIds` identifies all lifted cards; `cardId` is the anchor.
590
+
591
+ `useMarqueeSelection` from `/react` is an opt-in mouse/pen background gesture.
592
+ Pass `enabled`, `getCards: () => [{ id, element }]`, and
593
+ `onSelect: (ids, additive) => ...`; spread its `props` on the selectable area.
594
+ It reports intersecting cards on release and viewport `bounds` for an authored
595
+ fixed-position overlay. Shift requests additive selection; Escape, pointer
596
+ cancellation, blur or disabling the area cancels. Buttons, inputs and card
597
+ controls keep their own gestures, and touch retains scrolling. A background
598
+ click reports an empty selection. The hook supplies no styling or bulk-action UI.
@@ -0,0 +1,13 @@
1
+ import { f as BoardEdgeId, n as BoardVertexId } from './extract-Bi6_tzUr.js';
2
+
3
+ declare function boardEdgeId<BoardId extends string>(layout: "hex" | "square", boardId: BoardId, latticeId: string): BoardEdgeId<BoardId>;
4
+ declare function boardVertexId<BoardId extends string>(layout: "hex" | "square", boardId: BoardId, latticeId: string): BoardVertexId<BoardId>;
5
+ /** Syntax proves namespace only; current placement owns element membership. */
6
+ declare function parseBoardElementId(value: unknown): {
7
+ kind: "edge" | "vertex";
8
+ layout: "hex" | "square";
9
+ boardId: string;
10
+ latticeId: string;
11
+ } | null;
12
+
13
+ export { boardVertexId as a, boardEdgeId as b, parseBoardElementId as p };