@ankhorage/zora-tabletop 0.0.9 → 0.1.1

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 (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +45 -1
  3. package/dist/features/game-presentations/adapters/inbound/PokerTrainingTable.d.ts +16 -0
  4. package/dist/features/game-presentations/adapters/inbound/PokerTrainingTable.d.ts.map +1 -0
  5. package/dist/features/game-presentations/adapters/inbound/PokerTrainingTable.js +20 -0
  6. package/dist/features/game-presentations/adapters/inbound/PokerTrainingTable.js.map +1 -0
  7. package/dist/features/game-presentations/adapters/inbound/createPokerTrainingTableState.d.ts +13 -0
  8. package/dist/features/game-presentations/adapters/inbound/createPokerTrainingTableState.d.ts.map +1 -0
  9. package/dist/features/game-presentations/adapters/inbound/createPokerTrainingTableState.js +98 -0
  10. package/dist/features/game-presentations/adapters/inbound/createPokerTrainingTableState.js.map +1 -0
  11. package/dist/features/game-presentations/adapters/inbound/pokerTrainingTableMeta.d.ts +110 -0
  12. package/dist/features/game-presentations/adapters/inbound/pokerTrainingTableMeta.d.ts.map +1 -0
  13. package/dist/features/game-presentations/adapters/inbound/pokerTrainingTableMeta.js +81 -0
  14. package/dist/features/game-presentations/adapters/inbound/pokerTrainingTableMeta.js.map +1 -0
  15. package/dist/features/game-presentations/domain/createTabletopGameSeats.d.ts +25 -0
  16. package/dist/features/game-presentations/domain/createTabletopGameSeats.d.ts.map +1 -0
  17. package/dist/features/game-presentations/domain/createTabletopGameSeats.js +33 -0
  18. package/dist/features/game-presentations/domain/createTabletopGameSeats.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +7 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/meta.d.ts +145 -0
  24. package/dist/meta.d.ts.map +1 -0
  25. package/dist/meta.js +115 -0
  26. package/dist/meta.js.map +1 -0
  27. package/dist/plugin.d.ts +277 -0
  28. package/dist/plugin.d.ts.map +1 -0
  29. package/dist/plugin.js +9 -0
  30. package/dist/plugin.js.map +1 -0
  31. package/dist/registry.d.ts +522 -0
  32. package/dist/registry.d.ts.map +1 -0
  33. package/dist/registry.js +22 -0
  34. package/dist/registry.js.map +1 -0
  35. package/dist/types/gamePresentation.d.ts +16 -0
  36. package/dist/types/gamePresentation.d.ts.map +1 -0
  37. package/dist/types/gamePresentation.js +2 -0
  38. package/dist/types/gamePresentation.js.map +1 -0
  39. package/dist/types/pokerTraining.d.ts +42 -0
  40. package/dist/types/pokerTraining.d.ts.map +1 -0
  41. package/dist/types/pokerTraining.js +2 -0
  42. package/dist/types/pokerTraining.js.map +1 -0
  43. package/package.json +12 -5
  44. package/src/features/game-presentations/adapters/inbound/PokerTrainingTable.tsx +41 -0
  45. package/src/features/game-presentations/adapters/inbound/createPokerTrainingTableState.test.ts +130 -0
  46. package/src/features/game-presentations/adapters/inbound/createPokerTrainingTableState.ts +133 -0
  47. package/src/features/game-presentations/adapters/inbound/pokerTrainingTableMeta.ts +80 -0
  48. package/src/features/game-presentations/domain/createTabletopGameSeats.test.ts +55 -0
  49. package/src/features/game-presentations/domain/createTabletopGameSeats.ts +43 -0
  50. package/src/index.ts +20 -0
  51. package/src/meta.test.ts +54 -0
  52. package/src/meta.ts +114 -0
  53. package/src/plugin.ts +10 -0
  54. package/src/registry.ts +23 -0
  55. package/src/types/gamePresentation.ts +19 -0
  56. package/src/types/pokerTraining.ts +51 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/zora-tabletop",
3
3
  "type": "module",
4
- "version": "0.0.9",
4
+ "version": "0.1.1",
5
5
  "description": "Tabletop and card-game UI primitives for React Native and React Native Web, built on ZORA.",
6
6
  "homepage": "https://github.com/ankhorage/zora-tabletop#readme",
7
7
  "bugs": {
@@ -41,6 +41,13 @@
41
41
  "import": "./dist/index.js",
42
42
  "default": "./dist/index.js"
43
43
  },
44
+ "./metadata": {
45
+ "react-native": "./src/registry.ts",
46
+ "browser": "./src/registry.ts",
47
+ "types": "./dist/registry.d.ts",
48
+ "import": "./dist/registry.js",
49
+ "default": "./dist/registry.js"
50
+ },
44
51
  "./package.json": "./package.json"
45
52
  },
46
53
  "publishConfig": {
@@ -62,14 +69,14 @@
62
69
  "knip:check": "ankhorage-knip"
63
70
  },
64
71
  "peerDependencies": {
65
- "@ankhorage/zora": ">=4.2.0",
72
+ "@ankhorage/zora": ">=4.2.2",
66
73
  "react": ">=18.2.0",
67
74
  "react-native": ">=0.72.0"
68
75
  },
69
76
  "devDependencies": {
70
- "@ankhorage/devtools": "^1.11.0",
77
+ "@ankhorage/devtools": "^1.11.11",
71
78
  "@ankhorage/paradox": "^0.1.24",
72
- "@ankhorage/zora": "^4.2.0",
79
+ "@ankhorage/zora": "^4.2.2",
73
80
  "@react-native-picker/picker": "2.11.4",
74
81
  "@react-native-vector-icons/fontawesome": "^13.1.3",
75
82
  "@react-native-vector-icons/fontawesome5": "^13.1.3",
@@ -86,5 +93,5 @@
86
93
  "react-native-web": "~0.21.0",
87
94
  "typescript": "~6.0.3"
88
95
  },
89
- "packageManager": "bun@1.4.1"
96
+ "packageManager": "bun@1.4.2"
90
97
  }
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+
3
+ import { TabletopTable } from '../../../../components/tabletop-table';
4
+ import type { PokerTrainingTableProps } from '../../../../types/pokerTraining';
5
+ import { createPokerTrainingTableState } from './createPokerTrainingTableState';
6
+
7
+ /***
8
+ * Manifest-ready poker training pattern that reconstructs a complete table from task data.
9
+ *
10
+ * Bind the raw training task object to `task`; the pattern maps sparse active-player data to a
11
+ * full nine-seat presentation while leaving API execution in the generated application.
12
+ *
13
+ * @readme
14
+ * @example Training task binding
15
+ * ```tsx
16
+ * <PokerTrainingTable task={task} />
17
+ * ```
18
+ */
19
+ export function PokerTrainingTable({
20
+ task = {},
21
+ defaultStackBigBlinds = 100,
22
+ accessibilityLabel,
23
+ ...tableProps
24
+ }: PokerTrainingTableProps) {
25
+ const state = React.useMemo(
26
+ () => createPokerTrainingTableState(task, { defaultStackBigBlinds }),
27
+ [defaultStackBigBlinds, task],
28
+ );
29
+
30
+ return (
31
+ <TabletopTable
32
+ {...tableProps}
33
+ accessibilityLabel={accessibilityLabel ?? state.accessibilityLabel}
34
+ centerCards={state.centerCards}
35
+ centerLabel={state.centerLabel}
36
+ centerSublabel={state.centerSublabel}
37
+ seatCount={9}
38
+ seats={state.seats}
39
+ />
40
+ );
41
+ }
@@ -0,0 +1,130 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import type { PokerTrainingTaskTableData } from '../../../../types/pokerTraining';
4
+ import { createPokerTrainingTableState } from './createPokerTrainingTableState';
5
+
6
+ const liveTask: PokerTrainingTaskTableData = {
7
+ blinds: { small: 50, big: 100 },
8
+ heroPosition: 'BTN',
9
+ heroCards: [
10
+ { rank: 'K', suit: 'hearts' },
11
+ { rank: 'Q', suit: 'hearts' },
12
+ ],
13
+ communityCards: [
14
+ { rank: 'A', suit: 'clubs' },
15
+ { rank: '7', suit: 'hearts' },
16
+ { rank: '2', suit: 'spades' },
17
+ { rank: 'J', suit: 'diamonds' },
18
+ ],
19
+ pot: 1200,
20
+ players: [
21
+ { position: 'BB', stack: 10000 },
22
+ { position: 'BTN', stack: 10000, isHero: true },
23
+ ],
24
+ };
25
+
26
+ test('createPokerTrainingTableState reconstructs a sparse task as nine seats', () => {
27
+ const state = createPokerTrainingTableState(liveTask);
28
+
29
+ expect(state.seats).toHaveLength(9);
30
+ expect(state.seats.map((seat) => seat.id)).toEqual([
31
+ 'BTN',
32
+ 'SB',
33
+ 'BB',
34
+ 'UTG',
35
+ 'UTG+1',
36
+ 'MP',
37
+ 'MP+1',
38
+ 'HJ',
39
+ 'CO',
40
+ ]);
41
+ expect(state.seats[0]).toMatchObject({
42
+ id: 'BTN',
43
+ cards: liveTask.heroCards,
44
+ selected: true,
45
+ sublabel: '100 BB',
46
+ tokenLabel: 'D',
47
+ });
48
+ expect(state.seats[2]).toMatchObject({
49
+ id: 'BB',
50
+ faceDownCards: 2,
51
+ sublabel: '100 BB',
52
+ });
53
+ expect(state.seats[1]).toMatchObject({
54
+ id: 'SB',
55
+ disabled: true,
56
+ muted: true,
57
+ sublabel: '100 BB · Folded',
58
+ });
59
+ });
60
+
61
+ test('createPokerTrainingTableState maps shared live task state', () => {
62
+ const state = createPokerTrainingTableState(liveTask);
63
+
64
+ expect(state.centerCards).toEqual(liveTask.communityCards);
65
+ expect(state.centerLabel).toBe('Pot 1,200');
66
+ expect(state.centerSublabel).toBe('Blinds 50 / 100');
67
+ expect(state.accessibilityLabel).toBe(
68
+ 'Nine-player poker table. Hero BTN. Pot 1,200. Blinds 50 / 100.',
69
+ );
70
+ });
71
+
72
+ test('createPokerTrainingTableState rotates another hero to the bottom seat', () => {
73
+ const state = createPokerTrainingTableState({
74
+ ...liveTask,
75
+ heroPosition: 'CO',
76
+ players: [{ position: 'CO', stack: 5000, bet: 250, isHero: true }],
77
+ });
78
+
79
+ expect(state.seats[0]).toMatchObject({
80
+ id: 'CO',
81
+ selected: true,
82
+ sublabel: '50 BB · Bet 2.5 BB',
83
+ });
84
+ expect(state.seats.map((seat) => seat.id)).toEqual([
85
+ 'CO',
86
+ 'BTN',
87
+ 'SB',
88
+ 'BB',
89
+ 'UTG',
90
+ 'UTG+1',
91
+ 'MP',
92
+ 'MP+1',
93
+ 'HJ',
94
+ ]);
95
+ });
96
+
97
+ test('createPokerTrainingTableState maps folded and visible opponent state', () => {
98
+ const visibleCards = [
99
+ { rank: 'A', suit: 'spades' },
100
+ { rank: 'A', suit: 'diamonds' },
101
+ ] as const;
102
+ const state = createPokerTrainingTableState({
103
+ ...liveTask,
104
+ players: [
105
+ { position: 'BB', stack: 9000, bet: 100, folded: true },
106
+ { position: 'HJ', stack: 8000, cards: visibleCards },
107
+ ],
108
+ });
109
+
110
+ const foldedSeat = state.seats.find((seat) => seat.id === 'BB');
111
+
112
+ expect(foldedSeat).toMatchObject({
113
+ disabled: true,
114
+ muted: true,
115
+ sublabel: '90 BB · Bet 1 BB · Folded',
116
+ });
117
+ expect(foldedSeat?.faceDownCards).toBeUndefined();
118
+ expect(state.seats.find((seat) => seat.id === 'HJ')).toMatchObject({
119
+ cards: visibleCards,
120
+ sublabel: '80 BB',
121
+ });
122
+ });
123
+
124
+ test('createPokerTrainingTableState uses a configurable folded stack', () => {
125
+ const state = createPokerTrainingTableState({}, { defaultStackBigBlinds: 75 });
126
+
127
+ expect(state.seats[0]?.sublabel).toBe('75 BB · Folded');
128
+ expect(state.centerLabel).toBeUndefined();
129
+ expect(state.centerSublabel).toBeUndefined();
130
+ });
@@ -0,0 +1,133 @@
1
+ import type { TabletopGameParticipantState } from '../../../../types/gamePresentation';
2
+ import type {
3
+ CreatePokerTrainingTableStateOptions,
4
+ PokerTrainingPlayer,
5
+ PokerTrainingTableState,
6
+ PokerTrainingTaskTableData,
7
+ } from '../../../../types/pokerTraining';
8
+ import { createTabletopGameSeats } from '../../domain/createTabletopGameSeats';
9
+
10
+ /***
11
+ * Adapts the table-relevant subset of a poker training task to generic tabletop presentation state.
12
+ *
13
+ * The canonical poker ring is rotated so the hero occupies the first (bottom) tabletop seat.
14
+ * Players omitted by the task are reconstructed as folded with the configured default stack.
15
+ *
16
+ * @param task - Serializable poker training task data received by the generated application.
17
+ * @param options - Presentation defaults for task fields that are intentionally omitted.
18
+ * @returns Seats and shared-table content ready for `TabletopTable`.
19
+ */
20
+ export function createPokerTrainingTableState(
21
+ task: PokerTrainingTaskTableData,
22
+ { defaultStackBigBlinds = 100 }: CreatePokerTrainingTableStateOptions = {},
23
+ ): PokerTrainingTableState {
24
+ const bigBlind = task.blinds?.big;
25
+ const heroPosition = task.heroPosition ?? task.players?.find((player) => player.isHero)?.position;
26
+ const orderedPositions = rotatePokerPositions(heroPosition);
27
+ const defaultStack = `${formatAmount(defaultStackBigBlinds)} BB`;
28
+ const participants = (task.players ?? []).map((player) =>
29
+ createPokerParticipantState(player, task, bigBlind),
30
+ );
31
+ const seats = createTabletopGameSeats({
32
+ seats: orderedPositions.map((position) => ({
33
+ id: position,
34
+ defaultState: {
35
+ label: position,
36
+ tokenLabel: position === 'BTN' ? 'D' : undefined,
37
+ },
38
+ })),
39
+ participants,
40
+ missingParticipantState: {
41
+ disabled: true,
42
+ muted: true,
43
+ sublabel: `${defaultStack} · Folded`,
44
+ },
45
+ });
46
+
47
+ return {
48
+ seats,
49
+ centerCards: task.communityCards ?? [],
50
+ centerLabel: task.pot === undefined ? undefined : `Pot ${formatAmount(task.pot)}`,
51
+ centerSublabel:
52
+ task.blinds === undefined
53
+ ? undefined
54
+ : `Blinds ${formatAmount(task.blinds.small)} / ${formatAmount(task.blinds.big)}`,
55
+ accessibilityLabel: createPokerAccessibilityLabel(heroPosition, task),
56
+ };
57
+ }
58
+
59
+ const pokerPositions = ['BTN', 'SB', 'BB', 'UTG', 'UTG+1', 'MP', 'MP+1', 'HJ', 'CO'] as const;
60
+
61
+ /*** Rotates the canonical nine-seat poker ring to place the hero at the bottom seat. */
62
+ function rotatePokerPositions(heroPosition: string | undefined): readonly string[] {
63
+ const heroIndex = pokerPositions.findIndex((position) => position === heroPosition);
64
+ if (heroIndex <= 0) return pokerPositions;
65
+
66
+ return [...pokerPositions.slice(heroIndex), ...pokerPositions.slice(0, heroIndex)];
67
+ }
68
+
69
+ /*** Converts one poker participant record into a generic seat-state override. */
70
+ function createPokerParticipantState(
71
+ player: PokerTrainingPlayer,
72
+ task: PokerTrainingTaskTableData,
73
+ bigBlind: number | undefined,
74
+ ): TabletopGameParticipantState {
75
+ const isHero = player.isHero === true || player.position === task.heroPosition;
76
+ const cards = player.cards ?? (isHero ? task.heroCards : undefined);
77
+ const folded = player.folded === true;
78
+
79
+ return {
80
+ seatId: player.position,
81
+ state: {
82
+ cards,
83
+ disabled: folded,
84
+ faceDownCards: cards === undefined && !folded && !isHero ? 2 : undefined,
85
+ muted: folded,
86
+ selected: isHero,
87
+ sublabel: formatPlayerSublabel(player, bigBlind),
88
+ },
89
+ };
90
+ }
91
+
92
+ /*** Formats stack and optional committed chips as a compact seat sublabel. */
93
+ function formatPlayerSublabel(
94
+ player: PokerTrainingPlayer,
95
+ bigBlind: number | undefined,
96
+ ): string | undefined {
97
+ const stack = player.stack === undefined ? undefined : formatChipStack(player.stack, bigBlind);
98
+ const bet = player.bet === undefined ? undefined : `Bet ${formatChipStack(player.bet, bigBlind)}`;
99
+ const parts = [stack, bet, player.folded === true ? 'Folded' : undefined].filter(
100
+ (part): part is string => part !== undefined,
101
+ );
102
+
103
+ return parts.length === 0 ? undefined : parts.join(' · ');
104
+ }
105
+
106
+ /*** Formats chips as big blinds when a positive blind is known, otherwise as an amount. */
107
+ function formatChipStack(amount: number, bigBlind: number | undefined): string {
108
+ if (bigBlind !== undefined && bigBlind > 0) {
109
+ return `${formatAmount(amount / bigBlind)} BB`;
110
+ }
111
+
112
+ return `${formatAmount(amount)} chips`;
113
+ }
114
+
115
+ /*** Formats a numeric poker amount consistently across native and web runtimes. */
116
+ function formatAmount(amount: number): string {
117
+ return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(amount);
118
+ }
119
+
120
+ /*** Summarizes the reconstructed table for assistive technologies. */
121
+ function createPokerAccessibilityLabel(
122
+ heroPosition: string | undefined,
123
+ task: PokerTrainingTaskTableData,
124
+ ): string {
125
+ const parts = ['Nine-player poker table'];
126
+ if (heroPosition !== undefined) parts.push(`Hero ${heroPosition}`);
127
+ if (task.pot !== undefined) parts.push(`Pot ${formatAmount(task.pot)}`);
128
+ if (task.blinds !== undefined) {
129
+ parts.push(`Blinds ${formatAmount(task.blinds.small)} / ${formatAmount(task.blinds.big)}`);
130
+ }
131
+
132
+ return `${parts.join('. ')}.`;
133
+ }
@@ -0,0 +1,80 @@
1
+ export const pokerTrainingTableMeta = {
2
+ name: 'PokerTrainingTable',
3
+ category: 'pattern',
4
+ description: 'Reconstructs a complete nine-seat poker table from sparse training task data.',
5
+ directManifestNode: true,
6
+ allowedChildren: [],
7
+ blueprint: {
8
+ label: 'Poker training table',
9
+ defaultProps: {
10
+ task: {},
11
+ defaultStackBigBlinds: 100,
12
+ shape: 'oval',
13
+ cardSize: 'small',
14
+ },
15
+ },
16
+ bindings: {
17
+ props: {
18
+ task: {
19
+ label: 'Poker training task',
20
+ description: 'Table-relevant fields from a poker training task.',
21
+ value: {
22
+ type: 'object',
23
+ fields: [
24
+ { path: 'blinds', type: 'object', label: 'Blinds' },
25
+ { path: 'heroPosition', type: 'string', label: 'Hero position' },
26
+ { path: 'heroCards', type: 'array', label: 'Hero cards' },
27
+ { path: 'communityCards', type: 'array', label: 'Community cards' },
28
+ { path: 'pot', type: 'number', label: 'Pot' },
29
+ { path: 'players', type: 'array', label: 'Players' },
30
+ ],
31
+ },
32
+ acceptsFallback: true,
33
+ },
34
+ disabled: {
35
+ label: 'Disabled',
36
+ value: { type: 'boolean' },
37
+ acceptsFallback: true,
38
+ acceptsTransforms: true,
39
+ },
40
+ },
41
+ },
42
+ props: {
43
+ defaultStackBigBlinds: {
44
+ type: 'number',
45
+ category: 'Players',
46
+ label: 'Default stack in big blinds',
47
+ default: 100,
48
+ authoring: { authority: 'instance' },
49
+ },
50
+ shape: {
51
+ type: 'enum',
52
+ category: 'Table',
53
+ label: 'Shape',
54
+ enum: ['circle', 'oval', 'rounded'],
55
+ default: 'oval',
56
+ authoring: { authority: 'instance' },
57
+ },
58
+ cardSize: {
59
+ type: 'enum',
60
+ category: 'Cards',
61
+ label: 'Card size',
62
+ enum: ['small', 'medium', 'large'],
63
+ default: 'small',
64
+ authoring: { authority: 'instance' },
65
+ },
66
+ disabled: {
67
+ type: 'boolean',
68
+ category: 'State',
69
+ label: 'Disabled',
70
+ default: false,
71
+ authoring: { authority: 'instance' },
72
+ },
73
+ accessibilityLabel: {
74
+ type: 'string',
75
+ category: 'Accessibility',
76
+ label: 'Accessibility label',
77
+ authoring: { authority: 'instance' },
78
+ },
79
+ },
80
+ } as const;
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { createTabletopGameSeats } from './createTabletopGameSeats';
4
+
5
+ describe('createTabletopGameSeats', () => {
6
+ test('maps current participants without embedding a specific card game', () => {
7
+ const seats = createTabletopGameSeats({
8
+ seats: [
9
+ { id: 'north', defaultState: { label: 'North', tokenLabel: 'Lead' } },
10
+ { id: 'east', defaultState: { label: 'East' } },
11
+ { id: 'south', defaultState: { label: 'South' } },
12
+ { id: 'west', defaultState: { label: 'West' } },
13
+ ],
14
+ participants: [
15
+ {
16
+ seatId: 'east',
17
+ state: {
18
+ cards: [{ rank: '7', suit: 'diamonds' }],
19
+ sublabel: 'Ready',
20
+ },
21
+ },
22
+ ],
23
+ missingParticipantState: { disabled: true, muted: true, sublabel: 'Out' },
24
+ });
25
+
26
+ expect(seats).toHaveLength(4);
27
+ expect(seats[0]).toMatchObject({
28
+ id: 'north',
29
+ label: 'North',
30
+ tokenLabel: 'Lead',
31
+ disabled: true,
32
+ muted: true,
33
+ sublabel: 'Out',
34
+ });
35
+ expect(seats[1]).toMatchObject({
36
+ id: 'east',
37
+ label: 'East',
38
+ cards: [{ rank: '7', suit: 'diamonds' }],
39
+ sublabel: 'Ready',
40
+ });
41
+ });
42
+
43
+ test('uses the last participant update and ignores seats outside the layout', () => {
44
+ const seats = createTabletopGameSeats({
45
+ seats: [{ id: 'one', defaultState: { label: 'One' } }],
46
+ participants: [
47
+ { seatId: 'one', state: { sublabel: 'Waiting' } },
48
+ { seatId: 'unknown', state: { sublabel: 'Ignored' } },
49
+ { seatId: 'one', state: { sublabel: 'Playing' } },
50
+ ],
51
+ });
52
+
53
+ expect(seats).toEqual([{ id: 'one', label: 'One', sublabel: 'Playing' }]);
54
+ });
55
+ });
@@ -0,0 +1,43 @@
1
+ import type { TabletopSeatState } from '../../../types';
2
+ import type { CreateTabletopGameSeatsInput } from '../../../types/gamePresentation';
3
+
4
+ /***
5
+ * Joins an ordered game layout with the participant states currently present in a scene.
6
+ *
7
+ * Missing participants retain their seat definition and receive the configured fallback state,
8
+ * which lets card games represent folded, eliminated, disconnected, or otherwise inactive seats
9
+ * without embedding those game rules in the tabletop renderer.
10
+ *
11
+ * @param input - Ordered seats, participant overrides, and the fallback for missing participants.
12
+ * @returns Normalized tabletop seats in the same order as the supplied seat definitions.
13
+ * @example Generic four-seat game
14
+ * ```ts
15
+ * const seats = createTabletopGameSeats({
16
+ * seats: [
17
+ * { id: 'north', defaultState: { label: 'North' } },
18
+ * { id: 'east', defaultState: { label: 'East' } },
19
+ * ],
20
+ * participants: [{ seatId: 'east', state: { sublabel: 'Ready' } }],
21
+ * missingParticipantState: { muted: true },
22
+ * });
23
+ * ```
24
+ */
25
+ export function createTabletopGameSeats({
26
+ seats,
27
+ participants,
28
+ missingParticipantState = { disabled: true, muted: true },
29
+ }: CreateTabletopGameSeatsInput): readonly TabletopSeatState[] {
30
+ const participantsBySeat = new Map(
31
+ participants.map((participant) => [participant.seatId, participant] as const),
32
+ );
33
+
34
+ return seats.map((seat) => {
35
+ const participant = participantsBySeat.get(seat.id);
36
+
37
+ return {
38
+ id: seat.id,
39
+ ...seat.defaultState,
40
+ ...(participant === undefined ? missingParticipantState : participant.state),
41
+ };
42
+ });
43
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,13 @@ export type { PlayingCardProps } from './components/playing-card';
8
8
  export { PlayingCard } from './components/playing-card';
9
9
  export type { TabletopTableProps } from './components/tabletop-table';
10
10
  export { TabletopTable } from './components/tabletop-table';
11
+ export { createPokerTrainingTableState } from './features/game-presentations/adapters/inbound/createPokerTrainingTableState';
12
+ export { PokerTrainingTable } from './features/game-presentations/adapters/inbound/PokerTrainingTable';
13
+ export { pokerTrainingTableMeta } from './features/game-presentations/adapters/inbound/pokerTrainingTableMeta';
14
+ export { createTabletopGameSeats } from './features/game-presentations/domain/createTabletopGameSeats';
15
+ export { tabletopTableMeta } from './meta';
16
+ export { ZORA_TABLETOP_COMPONENT_REGISTRY, ZORA_TABLETOP_PLUGIN } from './plugin';
17
+ export { ZORA_PLUGIN_METADATA, ZORA_TABLETOP_COMPONENT_META } from './registry';
11
18
  export type {
12
19
  PlayingCardSuit,
13
20
  PlayingCardValue,
@@ -16,3 +23,16 @@ export type {
16
23
  TabletopSeatState,
17
24
  TabletopShape,
18
25
  } from './types';
26
+ export type {
27
+ CreateTabletopGameSeatsInput,
28
+ TabletopGameParticipantState,
29
+ TabletopGameSeatDefinition,
30
+ TabletopGameSeatPresentation,
31
+ } from './types/gamePresentation';
32
+ export type {
33
+ CreatePokerTrainingTableStateOptions,
34
+ PokerTrainingPlayer,
35
+ PokerTrainingTableProps,
36
+ PokerTrainingTableState,
37
+ PokerTrainingTaskTableData,
38
+ } from './types/pokerTraining';
@@ -0,0 +1,54 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { pokerTrainingTableMeta } from './features/game-presentations/adapters/inbound/pokerTrainingTableMeta';
4
+ import { tabletopTableMeta } from './meta';
5
+ import { ZORA_TABLETOP_COMPONENT_META } from './registry';
6
+
7
+ describe('ZORA tabletop component metadata', () => {
8
+ test('exposes TabletopTable as a directly authorable manifest node', () => {
9
+ expect(tabletopTableMeta.directManifestNode).toBe(true);
10
+ expect(tabletopTableMeta.blueprint.defaultProps).toMatchObject({
11
+ seats: [],
12
+ shape: 'oval',
13
+ });
14
+ expect(tabletopTableMeta.bindings.props.seats.value).toEqual({
15
+ type: 'array',
16
+ itemType: 'object',
17
+ });
18
+ });
19
+
20
+ test('exports one canonical plugin descriptor from the public API', async () => {
21
+ const source = await Bun.file('src/plugin.ts').text();
22
+ const metadataSource = await Bun.file('src/registry.ts').text();
23
+ const publicSource = await Bun.file('src/index.ts').text();
24
+
25
+ expect(metadataSource).toContain("packageName: '@ankhorage/zora-tabletop'");
26
+ expect(source).toContain('componentRegistry: ZORA_TABLETOP_COMPONENT_REGISTRY');
27
+ expect(metadataSource).toContain('componentMeta: ZORA_TABLETOP_COMPONENT_META');
28
+ expect(publicSource).toContain('ZORA_TABLETOP_PLUGIN');
29
+ });
30
+
31
+ test('exposes PokerTrainingTable task data as one object binding', () => {
32
+ const taskValue = pokerTrainingTableMeta.bindings.props.task.value;
33
+
34
+ expect(taskValue.type).toBe('object');
35
+ expect(taskValue.fields.map((field) => field.path)).toEqual([
36
+ 'blinds',
37
+ 'heroPosition',
38
+ 'heroCards',
39
+ 'communityCards',
40
+ 'pot',
41
+ 'players',
42
+ ]);
43
+ });
44
+
45
+ test('registers the poker pattern with matching component metadata', async () => {
46
+ const source = await Bun.file('src/plugin.ts').text();
47
+
48
+ expect(source).toContain('{ PokerTrainingTable, TabletopTable }');
49
+ expect(Object.keys(ZORA_TABLETOP_COMPONENT_META)).toEqual([
50
+ 'PokerTrainingTable',
51
+ 'TabletopTable',
52
+ ]);
53
+ });
54
+ });