@ankhorage/zora-tabletop 0.1.0 → 0.1.2

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 (51) hide show
  1. package/CHANGELOG.md +12 -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 +6 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/plugin.d.ts +116 -1
  24. package/dist/plugin.d.ts.map +1 -1
  25. package/dist/plugin.js +2 -1
  26. package/dist/plugin.js.map +1 -1
  27. package/dist/registry.d.ts +222 -1
  28. package/dist/registry.d.ts.map +1 -1
  29. package/dist/registry.js +11 -2
  30. package/dist/registry.js.map +1 -1
  31. package/dist/types/gamePresentation.d.ts +16 -0
  32. package/dist/types/gamePresentation.d.ts.map +1 -0
  33. package/dist/types/gamePresentation.js +2 -0
  34. package/dist/types/gamePresentation.js.map +1 -0
  35. package/dist/types/pokerTraining.d.ts +42 -0
  36. package/dist/types/pokerTraining.d.ts.map +1 -0
  37. package/dist/types/pokerTraining.js +2 -0
  38. package/dist/types/pokerTraining.js.map +1 -0
  39. package/package.json +2 -3
  40. package/src/features/game-presentations/adapters/inbound/PokerTrainingTable.tsx +41 -0
  41. package/src/features/game-presentations/adapters/inbound/createPokerTrainingTableState.test.ts +130 -0
  42. package/src/features/game-presentations/adapters/inbound/createPokerTrainingTableState.ts +133 -0
  43. package/src/features/game-presentations/adapters/inbound/pokerTrainingTableMeta.ts +80 -0
  44. package/src/features/game-presentations/domain/createTabletopGameSeats.test.ts +55 -0
  45. package/src/features/game-presentations/domain/createTabletopGameSeats.ts +43 -0
  46. package/src/index.ts +17 -0
  47. package/src/meta.test.ts +39 -0
  48. package/src/plugin.ts +2 -1
  49. package/src/registry.ts +12 -2
  50. package/src/types/gamePresentation.ts +19 -0
  51. package/src/types/pokerTraining.ts +51 -0
@@ -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,10 @@ 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';
11
15
  export { tabletopTableMeta } from './meta';
12
16
  export { ZORA_TABLETOP_COMPONENT_REGISTRY, ZORA_TABLETOP_PLUGIN } from './plugin';
13
17
  export { ZORA_PLUGIN_METADATA, ZORA_TABLETOP_COMPONENT_META } from './registry';
@@ -19,3 +23,16 @@ export type {
19
23
  TabletopSeatState,
20
24
  TabletopShape,
21
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';
package/src/meta.test.ts CHANGED
@@ -1,6 +1,9 @@
1
+ import { composeZoraPluginMetadata, ZORA_CORE_PLUGIN_METADATA } from '@ankhorage/zora/metadata';
1
2
  import { describe, expect, test } from 'bun:test';
2
3
 
4
+ import { pokerTrainingTableMeta } from './features/game-presentations/adapters/inbound/pokerTrainingTableMeta';
3
5
  import { tabletopTableMeta } from './meta';
6
+ import { ZORA_PLUGIN_METADATA, ZORA_TABLETOP_COMPONENT_META } from './registry';
4
7
 
5
8
  describe('ZORA tabletop component metadata', () => {
6
9
  test('exposes TabletopTable as a directly authorable manifest node', () => {
@@ -25,4 +28,40 @@ describe('ZORA tabletop component metadata', () => {
25
28
  expect(metadataSource).toContain('componentMeta: ZORA_TABLETOP_COMPONENT_META');
26
29
  expect(publicSource).toContain('ZORA_TABLETOP_PLUGIN');
27
30
  });
31
+
32
+ test('exposes PokerTrainingTable task data as one object binding', () => {
33
+ const taskValue = pokerTrainingTableMeta.bindings.props.task.value;
34
+
35
+ expect(taskValue.type).toBe('object');
36
+ expect(taskValue.fields.map((field) => field.path)).toEqual([
37
+ 'blinds',
38
+ 'heroPosition',
39
+ 'heroCards',
40
+ 'communityCards',
41
+ 'pot',
42
+ 'players',
43
+ ]);
44
+ });
45
+
46
+ test('registers the poker pattern with matching component metadata', async () => {
47
+ const source = await Bun.file('src/plugin.ts').text();
48
+
49
+ expect(source).toContain('{ PokerTrainingTable, TabletopTable }');
50
+ expect(Object.keys(ZORA_TABLETOP_COMPONENT_META)).toEqual([
51
+ 'PokerTrainingTable',
52
+ 'TabletopTable',
53
+ ]);
54
+ });
55
+ });
56
+
57
+ describe('ZORA 19 plugin composition', () => {
58
+ test('uses only declared core extension hosts', () => {
59
+ expect(() =>
60
+ composeZoraPluginMetadata([ZORA_CORE_PLUGIN_METADATA, ZORA_PLUGIN_METADATA]),
61
+ ).not.toThrow();
62
+ expect(ZORA_PLUGIN_METADATA.placements.map((placement) => placement.parents)).toEqual([
63
+ ['Card', 'Grid', 'Screen', 'ScreenSection', 'View'],
64
+ ['Card', 'Grid', 'Screen', 'ScreenSection', 'View'],
65
+ ]);
66
+ });
28
67
  });
package/src/plugin.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { TabletopTable } from './components/tabletop-table';
2
+ import { PokerTrainingTable } from './features/game-presentations/adapters/inbound/PokerTrainingTable';
2
3
  import { ZORA_PLUGIN_METADATA } from './registry';
3
4
 
4
- export const ZORA_TABLETOP_COMPONENT_REGISTRY = { TabletopTable } as const;
5
+ export const ZORA_TABLETOP_COMPONENT_REGISTRY = { PokerTrainingTable, TabletopTable } as const;
5
6
 
6
7
  export const ZORA_TABLETOP_PLUGIN = {
7
8
  ...ZORA_PLUGIN_METADATA,
package/src/registry.ts CHANGED
@@ -1,15 +1,25 @@
1
+ import { pokerTrainingTableMeta } from './features/game-presentations/adapters/inbound/pokerTrainingTableMeta';
1
2
  import { tabletopTableMeta } from './meta';
2
3
 
3
- export const ZORA_TABLETOP_COMPONENT_META = { TabletopTable: tabletopTableMeta } as const;
4
+ export const ZORA_TABLETOP_COMPONENT_META = {
5
+ PokerTrainingTable: pokerTrainingTableMeta,
6
+ TabletopTable: tabletopTableMeta,
7
+ } as const;
8
+
9
+ const TABLETOP_PLACEMENT_PARENTS = ['Card', 'Grid', 'Screen', 'ScreenSection', 'View'] as const;
4
10
 
5
11
  export const ZORA_PLUGIN_METADATA = {
6
12
  packageName: '@ankhorage/zora-tabletop',
7
13
  displayName: 'ZORA Tabletop',
8
14
  componentMeta: ZORA_TABLETOP_COMPONENT_META,
9
15
  placements: [
16
+ {
17
+ child: 'PokerTrainingTable',
18
+ parents: TABLETOP_PLACEMENT_PARENTS,
19
+ },
10
20
  {
11
21
  child: 'TabletopTable',
12
- parents: ['Box', 'Card', 'Container', 'Grid', 'Panel', 'Screen', 'ScreenSection', 'Stack'],
22
+ parents: TABLETOP_PLACEMENT_PARENTS,
13
23
  },
14
24
  ],
15
25
  } as const;
@@ -0,0 +1,19 @@
1
+ import type { TabletopSeatState } from '../types';
2
+
3
+ export type TabletopGameSeatPresentation = Omit<TabletopSeatState, 'id'>;
4
+
5
+ export interface TabletopGameSeatDefinition {
6
+ readonly id: string;
7
+ readonly defaultState: TabletopGameSeatPresentation;
8
+ }
9
+
10
+ export interface TabletopGameParticipantState {
11
+ readonly seatId: string;
12
+ readonly state: Partial<TabletopGameSeatPresentation>;
13
+ }
14
+
15
+ export interface CreateTabletopGameSeatsInput {
16
+ readonly seats: readonly TabletopGameSeatDefinition[];
17
+ readonly participants: readonly TabletopGameParticipantState[];
18
+ readonly missingParticipantState?: Partial<TabletopGameSeatPresentation>;
19
+ }
@@ -0,0 +1,51 @@
1
+ import type { TabletopColorOverrides } from '../colors';
2
+ import type {
3
+ PlayingCardValue,
4
+ TabletopCardSize,
5
+ TabletopSeatState,
6
+ TabletopShape,
7
+ } from '../types';
8
+
9
+ export interface PokerTrainingPlayer {
10
+ readonly position: string;
11
+ readonly stack?: number;
12
+ readonly bet?: number;
13
+ readonly cards?: readonly PlayingCardValue[];
14
+ readonly folded?: boolean;
15
+ readonly isHero?: boolean;
16
+ }
17
+
18
+ export interface PokerTrainingTaskTableData {
19
+ readonly blinds?: {
20
+ readonly small: number;
21
+ readonly big: number;
22
+ };
23
+ readonly heroPosition?: string;
24
+ readonly heroCards?: readonly PlayingCardValue[];
25
+ readonly communityCards?: readonly PlayingCardValue[];
26
+ readonly pot?: number;
27
+ readonly players?: readonly PokerTrainingPlayer[];
28
+ }
29
+
30
+ export interface CreatePokerTrainingTableStateOptions {
31
+ readonly defaultStackBigBlinds?: number;
32
+ }
33
+
34
+ export interface PokerTrainingTableState {
35
+ readonly seats: readonly TabletopSeatState[];
36
+ readonly centerCards: readonly PlayingCardValue[];
37
+ readonly centerLabel?: string;
38
+ readonly centerSublabel?: string;
39
+ readonly accessibilityLabel: string;
40
+ }
41
+
42
+ export interface PokerTrainingTableProps {
43
+ readonly task?: PokerTrainingTaskTableData;
44
+ readonly defaultStackBigBlinds?: number;
45
+ readonly shape?: TabletopShape;
46
+ readonly cardSize?: TabletopCardSize;
47
+ readonly disabled?: boolean;
48
+ readonly colorScheme?: TabletopColorOverrides;
49
+ readonly accessibilityLabel?: string;
50
+ readonly testID?: string;
51
+ }