@pokertools/types 1.0.1 → 1.0.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
@@ -1,86 +1,1288 @@
1
- # @pokertools/types
1
+ # 🃏 @pokertools/types
2
2
 
3
- Pure TypeScript type definitions for poker game engine. This package contains zero runtime code - only type definitions.
3
+ > **TypeScript type definitions and Zod schemas for the PokerTools ecosystem**
4
4
 
5
- ## Installation
5
+ [![npm version](https://img.shields.io/npm/v/@pokertools/types.svg)](https://www.npmjs.com/package/@pokertools/types)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ This package provides the **single source of truth** for all type definitions used across the PokerTools monorepo. It includes TypeScript interfaces, enums, and Zod validation schemas that ensure type safety from the game engine to the API to the client SDK.
10
+
11
+ ---
12
+
13
+ ## 📦 Installation
6
14
 
7
15
  ```bash
8
16
  npm install @pokertools/types
9
17
  ```
10
18
 
11
- ## Usage
19
+ ```bash
20
+ yarn add @pokertools/types
21
+ ```
22
+
23
+ ```bash
24
+ pnpm add @pokertools/types
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 🏗️ Architecture Overview
30
+
31
+ ```
32
+ ┌─────────────────────────────────────────────────────────────────────────────┐
33
+ │ @pokertools/types │
34
+ │ Single Source of Truth │
35
+ └─────────────────────────────────────────────────────────────────────────────┘
36
+
37
+ ┌───────────────────────────┼───────────────────────────┐
38
+ │ │ │
39
+ ▼ ▼ ▼
40
+ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
41
+ │ Engine │ │ API │ │ Client SDK │
42
+ │ @pokertools/ │ │ @pokertools/ │ │ Frontend │
43
+ │ engine │ │ api │ │ Application │
44
+ └───────────────┘ └───────────────┘ └───────────────┘
45
+ │ │ │
46
+ └─────────────────────────┴─────────────────────────┘
47
+
48
+ ┌─────────────┴─────────────┐
49
+ │ Shared Contracts │
50
+ │ • Type Safety │
51
+ │ • Runtime Validation │
52
+ │ • Consistent Behavior │
53
+ └───────────────────────────┘
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 📚 Type Categories
59
+
60
+ | Category | Description | Files |
61
+ | ----------------- | ------------------------- | --------------------------------- |
62
+ | 🎮 **Game State** | Core game state types | `GameState.ts`, `PublicState.ts` |
63
+ | 👤 **Player** | Player model and status | `Player.ts` |
64
+ | 🎯 **Actions** | All game actions | `Action.ts`, `ActionWhitelist.ts` |
65
+ | 💰 **Pot** | Pot management | `Pot.ts` |
66
+ | ⚙️ **Config** | Table configuration | `Config.ts` |
67
+ | 📜 **History** | Hand history records | `HandHistory.ts` |
68
+ | 🔌 **WebSocket** | Real-time protocol | `WebSocketMessages.ts` |
69
+ | ❌ **Errors** | Error codes and responses | `ErrorCodes.ts` |
70
+ | ✅ **Schemas** | Zod validation schemas | `schemas.ts` |
71
+ | 🌐 **API** | REST API DTOs | `api/*.ts` |
72
+
73
+ ---
74
+
75
+ ## 🎮 Game State Types
76
+
77
+ ### GameState
78
+
79
+ The central immutable game state containing all information about the current hand.
80
+
81
+ ```typescript
82
+ import { GameState, Street, Winner } from "@pokertools/types";
83
+
84
+ // GameState structure
85
+ interface GameState {
86
+ // Table Configuration
87
+ config: TableConfig;
88
+ players: ReadonlyArray<Player | null>; // Indexed by seat (0-9)
89
+ maxPlayers: number;
90
+
91
+ // Hand State
92
+ handNumber: number;
93
+ buttonSeat: number | null;
94
+ deck: readonly number[]; // Remaining cards (integer codes)
95
+ board: readonly string[]; // Community cards ["As", "Kd", ...]
96
+ street: Street;
97
+
98
+ // Betting State
99
+ pots: readonly Pot[];
100
+ currentBets: ReadonlyMap<number, number>; // Seat -> bet amount
101
+ minRaise: number;
102
+ lastRaiseAmount: number;
103
+ actionTo: number | null; // Current actor's seat
104
+ lastAggressorSeat: number | null;
105
+
106
+ // Hand Progress
107
+ activePlayers: readonly number[]; // Non-folded seats
108
+ winners: readonly Winner[] | null;
109
+ rakeThisHand: number;
110
+
111
+ // Blind Tracking
112
+ smallBlind: number;
113
+ bigBlind: number;
114
+ ante: number;
115
+ blindLevel: number; // Tournament level index
116
+
117
+ // Time Bank
118
+ timeBanks: ReadonlyMap<number, number>;
119
+ timeBankActiveSeat: number | null;
120
+
121
+ // History
122
+ actionHistory: readonly ActionRecord[];
123
+ previousStates: readonly GameState[];
124
+
125
+ // Metadata
126
+ timestamp: number;
127
+ handId: string;
128
+ }
129
+ ```
130
+
131
+ ### Street Enum
132
+
133
+ ```typescript
134
+ import { Street } from "@pokertools/types";
135
+
136
+ const enum Street {
137
+ PREFLOP = "PREFLOP",
138
+ FLOP = "FLOP",
139
+ TURN = "TURN",
140
+ RIVER = "RIVER",
141
+ SHOWDOWN = "SHOWDOWN",
142
+ }
143
+
144
+ // Usage
145
+ if (state.street === Street.FLOP) {
146
+ console.log("Flop cards:", state.board.slice(0, 3));
147
+ }
148
+ ```
149
+
150
+ ### Winner Type
151
+
152
+ ```typescript
153
+ import { Winner } from "@pokertools/types";
154
+
155
+ interface Winner {
156
+ seat: number;
157
+ amount: number;
158
+ hand: readonly string[] | null; // Best 5 cards or null if uncontested
159
+ handRank: string | null; // "Full House, Aces full of Kings"
160
+ }
161
+ ```
162
+
163
+ ### PublicState
164
+
165
+ Sanitized game state for client consumption with hidden information masked.
166
+
167
+ ```typescript
168
+ import { PublicState, PublicPlayer } from "@pokertools/types";
169
+
170
+ interface PublicState extends Omit<GameState, "deck" | "players"> {
171
+ deck: readonly number[]; // Always empty
172
+ players: ReadonlyArray<PublicPlayer | null>;
173
+ viewingPlayerId: string | null; // null = spectator
174
+ version: number; // For state sync
175
+ }
176
+
177
+ // PublicPlayer has masked cards
178
+ interface PublicPlayer extends Omit<Player, "hand"> {
179
+ hand: ReadonlyArray<string | null> | null;
180
+ // Examples:
181
+ // ["As", "Kd"] - both visible
182
+ // [null, null] - both hidden
183
+ // ["As", null] - left card visible only
184
+ // null - no cards (mucked/folded)
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## 👤 Player Types
191
+
192
+ ### Player Interface
193
+
194
+ ```typescript
195
+ import { Player, PlayerStatus, SitInOption } from "@pokertools/types";
196
+
197
+ interface Player {
198
+ id: string; // Unique player ID
199
+ name: string; // Display name
200
+ seat: number; // 0-9 seat index
201
+ stack: number; // Current chips (integer)
202
+ hand: ReadonlyArray<string | null> | null; // Hole cards
203
+ shownCards: readonly number[] | null; // Indices shown at showdown
204
+ status: PlayerStatus;
205
+ betThisStreet: number;
206
+ totalInvestedThisHand: number;
207
+ isSittingOut: boolean;
208
+ timeBank: number; // Seconds remaining
209
+ pendingAddOn: number; // Chips waiting for next hand
210
+ sitInOption: SitInOption;
211
+ reservationExpiry: number | null;
212
+ }
213
+ ```
214
+
215
+ ### PlayerStatus Enum
216
+
217
+ ```typescript
218
+ import { PlayerStatus } from "@pokertools/types";
219
+
220
+ const enum PlayerStatus {
221
+ ACTIVE = "ACTIVE", // In hand, can act
222
+ FOLDED = "FOLDED", // Folded this hand
223
+ ALL_IN = "ALL_IN", // No chips left to bet
224
+ SITTING_OUT = "SITTING_OUT", // Not playing
225
+ WAITING = "WAITING", // At table, not in hand yet
226
+ BUSTED = "BUSTED", // Stack = 0
227
+ RESERVED = "RESERVED", // Seat reserved, awaiting payment
228
+ }
229
+
230
+ // Status flow diagram:
231
+ //
232
+ // RESERVED ──► WAITING ──► ACTIVE ──┬──► FOLDED
233
+ // ▲ │
234
+ // │ ├──► ALL_IN
235
+ // │ │
236
+ // └────────────────┴──► BUSTED
237
+ // │
238
+ // └──► SITTING_OUT
239
+ ```
240
+
241
+ ### SitInOption Enum
242
+
243
+ ```typescript
244
+ import { SitInOption } from "@pokertools/types";
245
+
246
+ const enum SitInOption {
247
+ IMMEDIATE = "IMMEDIATE", // Sit in right away
248
+ WAIT_FOR_BB = "WAIT_FOR_BB", // Wait for big blind position
249
+ }
250
+
251
+ // Usage in cash games
252
+ const sitAction: SitAction = {
253
+ type: ActionType.SIT,
254
+ playerId: "user123",
255
+ playerName: "Alice",
256
+ seat: 3,
257
+ stack: 1000,
258
+ sitInOption: SitInOption.WAIT_FOR_BB,
259
+ };
260
+ ```
261
+
262
+ ---
263
+
264
+ ## 🎯 Action Types
265
+
266
+ ### ActionType Enum
267
+
268
+ ```typescript
269
+ import { ActionType } from "@pokertools/types";
270
+
271
+ const enum ActionType {
272
+ // Management
273
+ SIT = "SIT",
274
+ STAND = "STAND",
275
+ ADD_CHIPS = "ADD_CHIPS",
276
+ RESERVE_SEAT = "RESERVE_SEAT",
277
+
278
+ // Dealing
279
+ DEAL = "DEAL",
280
+
281
+ // Betting
282
+ FOLD = "FOLD",
283
+ CHECK = "CHECK",
284
+ CALL = "CALL",
285
+ BET = "BET",
286
+ RAISE = "RAISE",
287
+
288
+ // Showdown
289
+ SHOW = "SHOW",
290
+ MUCK = "MUCK",
291
+
292
+ // Special
293
+ TIMEOUT = "TIMEOUT",
294
+ TIME_BANK = "TIME_BANK",
295
+ UNCALLED_BET_RETURNED = "UNCALLED_BET_RETURNED",
296
+
297
+ // Tournament
298
+ NEXT_BLIND_LEVEL = "NEXT_BLIND_LEVEL",
299
+ }
300
+ ```
301
+
302
+ ### Action Interfaces
12
303
 
13
304
  ```typescript
14
305
  import {
15
306
  Action,
16
- GameState,
17
- Player,
18
- Pot,
19
- ActionType,
20
- PlayerStatus,
21
- Street,
307
+ SitAction,
308
+ StandAction,
309
+ DealAction,
310
+ FoldAction,
311
+ CheckAction,
312
+ CallAction,
313
+ BetAction,
314
+ RaiseAction,
315
+ ShowAction,
316
+ MuckAction,
317
+ AddChipsAction,
318
+ ReserveSeatAction,
319
+ TimeoutAction,
320
+ TimeBankAction,
321
+ UncalledBetReturnedAction,
322
+ NextBlindLevelAction,
22
323
  } from "@pokertools/types";
23
324
 
24
- // Use types in your application
25
- const player: Player = {
26
- id: "player1",
27
- name: "Alice",
325
+ // SIT - Join a table
326
+ const sit: SitAction = {
327
+ type: ActionType.SIT,
328
+ playerId: "user123",
329
+ playerName: "Alice",
28
330
  seat: 0,
29
331
  stack: 1000,
30
- hand: null,
31
- shownCards: null, // New: tracks which cards are visible at showdown
32
- status: PlayerStatus.WAITING,
33
- betThisStreet: 0,
34
- totalInvestedThisHand: 0,
35
- isSittingOut: false,
36
- timeBank: 30,
332
+ sitInOption: SitInOption.IMMEDIATE, // optional
333
+ };
334
+
335
+ // STAND - Leave table
336
+ const stand: StandAction = {
337
+ type: ActionType.STAND,
338
+ playerId: "user123",
339
+ };
340
+
341
+ // DEAL - Start new hand
342
+ const deal: DealAction = {
343
+ type: ActionType.DEAL,
344
+ };
345
+
346
+ // FOLD - Forfeit hand
347
+ const fold: FoldAction = {
348
+ type: ActionType.FOLD,
349
+ playerId: "user123",
350
+ };
351
+
352
+ // CHECK - Pass (no bet to call)
353
+ const check: CheckAction = {
354
+ type: ActionType.CHECK,
355
+ playerId: "user123",
356
+ };
357
+
358
+ // CALL - Match current bet
359
+ const call: CallAction = {
360
+ type: ActionType.CALL,
361
+ playerId: "user123",
362
+ amount: 50, // optional, for history tracking
363
+ };
364
+
365
+ // BET - Opening bet
366
+ const bet: BetAction = {
367
+ type: ActionType.BET,
368
+ playerId: "user123",
369
+ amount: 100, // Total bet size
37
370
  };
38
371
 
39
- const action: Action = {
372
+ // RAISE - Raise existing bet
373
+ const raise: RaiseAction = {
40
374
  type: ActionType.RAISE,
41
- playerId: "player1",
42
- amount: 100,
375
+ playerId: "user123",
376
+ amount: 300, // Total raise amount
377
+ };
378
+
379
+ // SHOW - Show cards at showdown
380
+ const show: ShowAction = {
381
+ type: ActionType.SHOW,
382
+ playerId: "user123",
383
+ cardIndices: [0, 1], // optional: [0], [1], [0,1], or omit for all
384
+ };
385
+
386
+ // MUCK - Hide cards at showdown
387
+ const muck: MuckAction = {
388
+ type: ActionType.MUCK,
389
+ playerId: "user123",
390
+ };
391
+
392
+ // ADD_CHIPS - Rebuy/top-up (applied next hand)
393
+ const addChips: AddChipsAction = {
394
+ type: ActionType.ADD_CHIPS,
395
+ playerId: "user123",
396
+ amount: 500,
397
+ };
398
+
399
+ // RESERVE_SEAT - Lock seat while processing payment
400
+ const reserve: ReserveSeatAction = {
401
+ type: ActionType.RESERVE_SEAT,
402
+ playerId: "user123",
403
+ playerName: "Alice",
404
+ seat: 0,
405
+ expiryTimestamp: Date.now() + 30000, // 30 seconds
406
+ };
407
+
408
+ // TIMEOUT - Player ran out of time
409
+ const timeout: TimeoutAction = {
410
+ type: ActionType.TIMEOUT,
411
+ playerId: "user123",
412
+ timestamp: Date.now(), // optional
413
+ };
414
+
415
+ // TIME_BANK - Activate time bank
416
+ const timeBank: TimeBankAction = {
417
+ type: ActionType.TIME_BANK,
418
+ playerId: "user123",
419
+ };
420
+
421
+ // UNCALLED_BET_RETURNED - Internal engine action
422
+ const uncalled: UncalledBetReturnedAction = {
423
+ type: ActionType.UNCALLED_BET_RETURNED,
424
+ playerId: "user123",
425
+ amount: 50,
426
+ };
427
+
428
+ // NEXT_BLIND_LEVEL - Tournament blind increase
429
+ const nextLevel: NextBlindLevelAction = {
430
+ type: ActionType.NEXT_BLIND_LEVEL,
431
+ };
432
+ ```
433
+
434
+ ### Action Union Type
435
+
436
+ ```typescript
437
+ import { Action } from "@pokertools/types";
438
+
439
+ // Action is a discriminated union of all action types
440
+ type Action =
441
+ | SitAction
442
+ | StandAction
443
+ | AddChipsAction
444
+ | ReserveSeatAction
445
+ | DealAction
446
+ | FoldAction
447
+ | CheckAction
448
+ | CallAction
449
+ | BetAction
450
+ | RaiseAction
451
+ | ShowAction
452
+ | MuckAction
453
+ | TimeoutAction
454
+ | TimeBankAction
455
+ | UncalledBetReturnedAction
456
+ | NextBlindLevelAction;
457
+
458
+ // Type narrowing with discriminated unions
459
+ function handleAction(action: Action): void {
460
+ switch (action.type) {
461
+ case ActionType.BET:
462
+ console.log(`Bet: ${action.amount}`);
463
+ break;
464
+ case ActionType.RAISE:
465
+ console.log(`Raise to: ${action.amount}`);
466
+ break;
467
+ case ActionType.FOLD:
468
+ console.log(`${action.playerId} folded`);
469
+ break;
470
+ // ... handle other actions
471
+ }
472
+ }
473
+ ```
474
+
475
+ ### Action Whitelist
476
+
477
+ ```typescript
478
+ import { ALLOWED_GAMEPLAY_ACTIONS, isAllowedGameplayAction } from "@pokertools/types";
479
+
480
+ // Actions allowed through the API gameplay endpoint
481
+ // Management actions (SIT, ADD_CHIPS, RESERVE_SEAT) require dedicated endpoints
482
+ const ALLOWED_GAMEPLAY_ACTIONS: readonly ActionType[] = [
483
+ ActionType.DEAL,
484
+ ActionType.CHECK,
485
+ ActionType.CALL,
486
+ ActionType.RAISE,
487
+ ActionType.BET,
488
+ ActionType.FOLD,
489
+ ActionType.SHOW,
490
+ ActionType.MUCK,
491
+ ActionType.TIME_BANK,
492
+ ActionType.STAND,
493
+ ActionType.NEXT_BLIND_LEVEL,
494
+ ];
495
+
496
+ // Type guard
497
+ if (isAllowedGameplayAction(action.type)) {
498
+ // Safe to process through gameplay endpoint
499
+ }
500
+ ```
501
+
502
+ ### ActionRecord
503
+
504
+ ```typescript
505
+ import { ActionRecord } from "@pokertools/types";
506
+
507
+ interface ActionRecord {
508
+ action: Action;
509
+ seat: number | null; // null for table-level actions
510
+ resultingPot: number;
511
+ resultingStack: number;
512
+ street?: string;
513
+ }
514
+ ```
515
+
516
+ ---
517
+
518
+ ## 💰 Pot Types
519
+
520
+ ```typescript
521
+ import { Pot, PotType } from "@pokertools/types";
522
+
523
+ type PotType = "MAIN" | "SIDE";
524
+
525
+ interface Pot {
526
+ amount: number; // Total chips
527
+ eligibleSeats: readonly number[]; // Who can win
528
+ type: PotType;
529
+ capPerPlayer: number; // Max contribution
530
+ }
531
+
532
+ // Pot diagram for side pots:
533
+ //
534
+ // Player A: 100 chips (all-in)
535
+ // Player B: 300 chips (all-in)
536
+ // Player C: 500 chips (active)
537
+ //
538
+ // ┌─────────────────────────────────────────┐
539
+ // │ MAIN POT: 300 (100 × 3) │
540
+ // │ Eligible: [A, B, C] │
541
+ // ├─────────────────────────────────────────┤
542
+ // │ SIDE POT 1: 400 (200 × 2) │
543
+ // │ Eligible: [B, C] │
544
+ // ├─────────────────────────────────────────┤
545
+ // │ SIDE POT 2: 200 (uncalled portion) │
546
+ // │ Returned to: C │
547
+ // └─────────────────────────────────────────┘
548
+ ```
549
+
550
+ ---
551
+
552
+ ## ⚙️ Configuration Types
553
+
554
+ ### TableConfig
555
+
556
+ ```typescript
557
+ import { TableConfig, BlindLevel } from "@pokertools/types";
558
+
559
+ interface TableConfig {
560
+ smallBlind: number;
561
+ bigBlind: number;
562
+ ante?: number; // Default: 0
563
+ maxPlayers?: number; // 2-10, default: 9
564
+ initialStack?: number; // Tournament starting stack
565
+ blindStructure?: readonly BlindLevel[]; // Tournament schedule
566
+ timeBankSeconds?: number; // Default: 30
567
+ timeBankDeductionSeconds?: number; // Default: 10
568
+ randomProvider?: () => number; // Default: Math.random
569
+ rakePercent?: number; // 0-100, cash games
570
+ rakeCap?: number; // Max rake per pot
571
+ noFlopNoDrop?: boolean; // No rake if ends preflop, default: true
572
+ validateIntegrity?: boolean; // Chip conservation checks, default: true
573
+ isClient?: boolean; // Client mode (masked cards)
574
+ }
575
+
576
+ interface BlindLevel {
577
+ smallBlind: number;
578
+ bigBlind: number;
579
+ ante: number;
580
+ }
581
+
582
+ // Example: Cash game config
583
+ const cashConfig: TableConfig = {
584
+ smallBlind: 1,
585
+ bigBlind: 2,
586
+ maxPlayers: 9,
587
+ rakePercent: 5,
588
+ rakeCap: 10,
589
+ };
590
+
591
+ // Example: Tournament config
592
+ const tournamentConfig: TableConfig = {
593
+ smallBlind: 25,
594
+ bigBlind: 50,
595
+ ante: 5,
596
+ maxPlayers: 9,
597
+ initialStack: 10000,
598
+ blindStructure: [
599
+ { smallBlind: 25, bigBlind: 50, ante: 5 },
600
+ { smallBlind: 50, bigBlind: 100, ante: 10 },
601
+ { smallBlind: 75, bigBlind: 150, ante: 15 },
602
+ { smallBlind: 100, bigBlind: 200, ante: 25 },
603
+ ],
604
+ };
605
+ ```
606
+
607
+ ---
608
+
609
+ ## 📜 Hand History Types
610
+
611
+ ```typescript
612
+ import {
613
+ HandHistory,
614
+ HandHistoryPlayer,
615
+ StreetHistory,
616
+ HandHistoryActionRecord,
617
+ WinnerRecord,
618
+ ExportOptions,
619
+ } from "@pokertools/types";
620
+
621
+ interface HandHistory {
622
+ handId: string;
623
+ timestamp: number;
624
+ tableName: string;
625
+ gameType: "Cash" | "Tournament";
626
+ stakes: {
627
+ smallBlind: number;
628
+ bigBlind: number;
629
+ ante: number;
630
+ };
631
+ maxPlayers: number;
632
+ buttonSeat: number;
633
+ players: readonly HandHistoryPlayer[];
634
+ streets: readonly StreetHistory[];
635
+ winners: readonly WinnerRecord[];
636
+ totalPot: number;
637
+ }
638
+
639
+ interface HandHistoryPlayer {
640
+ seat: number;
641
+ name: string;
642
+ startingStack: number;
643
+ endingStack: number;
644
+ cards?: readonly string[]; // If shown
645
+ }
646
+
647
+ interface StreetHistory {
648
+ street: Street;
649
+ board: readonly string[];
650
+ actions: readonly HandHistoryActionRecord[];
651
+ pot: number;
652
+ }
653
+
654
+ interface HandHistoryActionRecord {
655
+ seat: number;
656
+ playerName: string;
657
+ action: Action;
658
+ amount?: number;
659
+ isAllIn?: boolean;
660
+ timestamp: number;
661
+ }
662
+
663
+ interface WinnerRecord {
664
+ seat: number;
665
+ playerName: string;
666
+ amount: number;
667
+ hand?: readonly string[];
668
+ handRank?: string;
669
+ }
670
+
671
+ interface ExportOptions {
672
+ format: "pokerstars" | "json" | "compact";
673
+ includeHoleCards?: boolean;
674
+ timezone?: string;
675
+ }
676
+ ```
677
+
678
+ ---
679
+
680
+ ## 🔌 WebSocket Protocol
681
+
682
+ ### Client → Server Messages
683
+
684
+ ```typescript
685
+ import { ClientMessage, JoinTableMessage, LeaveTableMessage, PingMessage } from "@pokertools/types";
686
+
687
+ // JOIN - Subscribe to table updates
688
+ const join: JoinTableMessage = {
689
+ type: "JOIN",
690
+ tableId: "table123",
691
+ requestId: "req-001", // optional correlation ID
692
+ };
693
+
694
+ // LEAVE - Unsubscribe from table
695
+ const leave: LeaveTableMessage = {
696
+ type: "LEAVE",
697
+ tableId: "table123",
698
+ requestId: "req-002",
699
+ };
700
+
701
+ // PING - Application-level heartbeat
702
+ const ping: PingMessage = {
703
+ type: "PING",
704
+ requestId: "ping-001",
705
+ timestamp: Date.now(), // optional
706
+ };
707
+
708
+ // Union type
709
+ type ClientMessage = JoinTableMessage | LeaveTableMessage | PingMessage;
710
+ ```
711
+
712
+ ### Server → Client Messages
713
+
714
+ ```typescript
715
+ import {
716
+ ServerMessage,
717
+ SnapshotMessage,
718
+ StateUpdateMessage,
719
+ ErrorMessage,
720
+ AckMessage,
721
+ PongMessage,
722
+ ActionNotificationMessage,
723
+ } from "@pokertools/types";
724
+
725
+ // SNAPSHOT - Full state when joining
726
+ const snapshot: SnapshotMessage = {
727
+ type: "SNAPSHOT",
728
+ tableId: "table123",
729
+ state: publicState, // PublicState
730
+ timestamp: Date.now(),
731
+ };
732
+
733
+ // STATE_UPDATE - Lightweight change notification
734
+ const update: StateUpdateMessage = {
735
+ type: "STATE_UPDATE",
736
+ tableId: "table123",
737
+ version: 42,
738
+ timestamp: Date.now(),
739
+ };
740
+
741
+ // ERROR - Error response
742
+ const error: ErrorMessage = {
743
+ type: "ERROR",
744
+ code: "INVALID_ACTION",
745
+ message: "Cannot check when there's a bet to call",
746
+ requestId: "req-001",
747
+ context: { currentBet: 100 },
748
+ };
749
+
750
+ // ACK - Success acknowledgment
751
+ const ack: AckMessage = {
752
+ type: "ACK",
753
+ requestId: "req-001",
754
+ message: "Joined table successfully",
755
+ };
756
+
757
+ // PONG - Response to PING
758
+ const pong: PongMessage = {
759
+ type: "PONG",
760
+ requestId: "ping-001",
761
+ timestamp: Date.now(),
762
+ };
763
+
764
+ // ACTION - Player action notification (for UX)
765
+ const actionNotif: ActionNotificationMessage = {
766
+ type: "ACTION",
767
+ tableId: "table123",
768
+ playerId: "user123",
769
+ actionType: "RAISE",
770
+ amount: 300,
43
771
  timestamp: Date.now(),
44
772
  };
45
773
  ```
46
774
 
47
- ## Type Exports
775
+ ### Type Guards
48
776
 
49
- ### Core Types
777
+ ```typescript
778
+ import {
779
+ isClientMessage,
780
+ isJoinMessage,
781
+ isLeaveMessage,
782
+ isPingMessage,
783
+ isServerMessage,
784
+ } from "@pokertools/types";
50
785
 
51
- - `Action` - Player actions (fold, check, call, bet, raise, etc.)
52
- - `GameState` - Complete game state
53
- - `Player` - Player information
54
- - `Pot` - Pot information (main and side pots)
55
- - `Config` - Game configuration
56
- - `PublicState` - Masked state for public view
57
- - `HandHistory` - Hand history information
786
+ // Usage
787
+ function handleMessage(data: unknown): void {
788
+ if (isClientMessage(data)) {
789
+ if (isJoinMessage(data)) {
790
+ subscribeToTable(data.tableId);
791
+ } else if (isLeaveMessage(data)) {
792
+ unsubscribeFromTable(data.tableId);
793
+ } else if (isPingMessage(data)) {
794
+ sendPong(data.requestId);
795
+ }
796
+ }
797
+ }
798
+ ```
799
+
800
+ ### Zod Schemas for Runtime Validation
801
+
802
+ ```typescript
803
+ import { ClientMessageSchema, parseClientMessage, safeParseClientMessage } from "@pokertools/types";
804
+
805
+ // Throws on invalid input
806
+ const message = parseClientMessage(jsonData);
58
807
 
59
- ### Enums
808
+ // Returns result object
809
+ const result = safeParseClientMessage(jsonData);
810
+ if (result.success) {
811
+ handleMessage(result.data);
812
+ } else {
813
+ console.error("Invalid message:", result.error);
814
+ }
815
+ ```
816
+
817
+ ---
818
+
819
+ ## ❌ Error Codes
60
820
 
61
- - `ActionType` - All possible action types
62
- - `PlayerStatus` - Player statuses (active, folded, all-in, etc.)
63
- - `Street` - Betting rounds (preflop, flop, turn, river, showdown)
821
+ ### ErrorCodes Object
822
+
823
+ ```typescript
824
+ import { ErrorCodes, ErrorCode } from "@pokertools/types";
64
825
 
65
- ### Interfaces
826
+ const ErrorCodes = {
827
+ // Generic
828
+ INVALID_ACTION: "INVALID_ACTION",
66
829
 
67
- All types are readonly and immutable by design.
830
+ // Player errors
831
+ PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND",
832
+ PLAYER_NOT_ACTIVE: "PLAYER_NOT_ACTIVE",
833
+ NOT_YOUR_TURN: "NOT_YOUR_TURN",
834
+ NO_CHIPS: "NO_CHIPS",
835
+ NOT_SEATED: "NOT_SEATED",
836
+
837
+ // Betting errors
838
+ CANNOT_CHECK: "CANNOT_CHECK",
839
+ NOTHING_TO_CALL: "NOTHING_TO_CALL",
840
+ CANNOT_BET: "CANNOT_BET",
841
+ BET_TOO_SMALL: "BET_TOO_SMALL",
842
+ CANNOT_RAISE: "CANNOT_RAISE",
843
+ CANNOT_RERAISE: "CANNOT_RERAISE",
844
+ RAISE_TOO_SMALL: "RAISE_TOO_SMALL",
845
+
846
+ // Deal errors
847
+ CANNOT_DEAL: "CANNOT_DEAL",
848
+ NOT_ENOUGH_PLAYERS: "NOT_ENOUGH_PLAYERS",
849
+
850
+ // Seat errors
851
+ INVALID_SEAT: "INVALID_SEAT",
852
+ SEAT_OCCUPIED: "SEAT_OCCUPIED",
853
+ INVALID_STACK: "INVALID_STACK",
854
+
855
+ // Validation errors
856
+ INVALID_AMOUNT: "INVALID_AMOUNT",
857
+ INVALID_TIMESTAMP: "INVALID_TIMESTAMP",
858
+
859
+ // Financial errors
860
+ INSUFFICIENT_FUNDS: "INSUFFICIENT_FUNDS",
861
+ INVALID_BUY_IN: "INVALID_BUY_IN",
862
+
863
+ // Auth errors
864
+ UNAUTHORIZED: "UNAUTHORIZED",
865
+ FORBIDDEN: "FORBIDDEN",
866
+
867
+ // Resource errors
868
+ NOT_FOUND: "NOT_FOUND",
869
+ TABLE_NOT_FOUND: "TABLE_NOT_FOUND",
870
+
871
+ // Rate limiting
872
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
873
+
874
+ // Server errors
875
+ INTERNAL_ERROR: "INTERNAL_ERROR",
876
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
877
+ } as const;
878
+
879
+ // Type for all error codes
880
+ type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
881
+ ```
882
+
883
+ ### HTTP Status Mapping
884
+
885
+ ```typescript
886
+ import { ERROR_STATUS_MAP, getStatusCodeForError } from "@pokertools/types";
887
+
888
+ // Get HTTP status for an error code
889
+ const status = getStatusCodeForError(ErrorCodes.NOT_YOUR_TURN); // 403
890
+
891
+ // Full mapping
892
+ const ERROR_STATUS_MAP: Record<ErrorCode, number> = {
893
+ [ErrorCodes.INVALID_ACTION]: 400,
894
+ [ErrorCodes.UNAUTHORIZED]: 401,
895
+ [ErrorCodes.FORBIDDEN]: 403,
896
+ [ErrorCodes.NOT_YOUR_TURN]: 403,
897
+ [ErrorCodes.NOT_FOUND]: 404,
898
+ [ErrorCodes.TABLE_NOT_FOUND]: 404,
899
+ [ErrorCodes.PLAYER_NOT_FOUND]: 404,
900
+ [ErrorCodes.RATE_LIMIT_EXCEEDED]: 429,
901
+ [ErrorCodes.INTERNAL_ERROR]: 500,
902
+ [ErrorCodes.SERVICE_UNAVAILABLE]: 503,
903
+ // ... see ErrorCodes.ts for full mapping
904
+ };
905
+ ```
906
+
907
+ ### Error Response Interface
908
+
909
+ ```typescript
910
+ import { ErrorResponse, createErrorResponse, hasErrorCode } from "@pokertools/types";
911
+
912
+ interface ErrorResponse {
913
+ error: ErrorCode;
914
+ message: string;
915
+ context?: Record<string, unknown>;
916
+ statusCode?: number;
917
+ }
918
+
919
+ // Create standardized error response
920
+ const response = createErrorResponse(
921
+ ErrorCodes.BET_TOO_SMALL,
922
+ "Bet must be at least 10",
923
+ { minBet: 10, attemptedBet: 5 },
924
+ 400
925
+ );
926
+
927
+ // Check if error contains a specific code
928
+ try {
929
+ engine.applyAction(action);
930
+ } catch (error) {
931
+ if (hasErrorCode(error, ErrorCodes.NOT_YOUR_TURN)) {
932
+ showNotYourTurnMessage();
933
+ }
934
+ }
935
+ ```
936
+
937
+ ---
938
+
939
+ ## ✅ Zod Validation Schemas
940
+
941
+ All schemas provide **runtime validation** that mirrors TypeScript types.
942
+
943
+ ### Action Schemas
944
+
945
+ ```typescript
946
+ import {
947
+ ActionSchema,
948
+ SitActionSchema,
949
+ StandActionSchema,
950
+ BetActionSchema,
951
+ RaiseActionSchema,
952
+ CallActionSchema,
953
+ CheckActionSchema,
954
+ FoldActionSchema,
955
+ DealActionSchema,
956
+ AddChipsActionSchema,
957
+ ReserveSeatActionSchema,
958
+ ShowActionSchema,
959
+ MuckActionSchema,
960
+ TimeBankActionSchema,
961
+ TimeoutActionSchema,
962
+ NextBlindLevelActionSchema,
963
+ UncalledBetReturnedActionSchema,
964
+ ValidatedAction,
965
+ } from "@pokertools/types";
966
+
967
+ // Validate any action
968
+ const result = ActionSchema.safeParse(userInput);
969
+ if (result.success) {
970
+ const action: ValidatedAction = result.data;
971
+ engine.applyAction(action);
972
+ } else {
973
+ console.error(result.error.issues);
974
+ }
975
+
976
+ // Validate specific action type
977
+ const sitResult = SitActionSchema.safeParse({
978
+ type: "SIT",
979
+ playerId: "user123",
980
+ playerName: "Alice",
981
+ seat: 0,
982
+ stack: 1000,
983
+ sitInOption: "WAIT_FOR_BB",
984
+ });
985
+ ```
986
+
987
+ ### Configuration Schemas
988
+
989
+ ```typescript
990
+ import {
991
+ TableConfigSchema,
992
+ BlindLevelSchema,
993
+ CreateTableSchema,
994
+ ValidatedTableConfig,
995
+ ValidatedBlindLevel,
996
+ CreateTableRequest,
997
+ } from "@pokertools/types";
998
+
999
+ // Validate table config
1000
+ const configResult = TableConfigSchema.safeParse({
1001
+ smallBlind: 5,
1002
+ bigBlind: 10,
1003
+ maxPlayers: 9,
1004
+ rakePercent: 5,
1005
+ });
1006
+
1007
+ // Custom refinements
1008
+ // - bigBlind must be > smallBlind
1009
+ // - maxPlayers must be 2-10
1010
+ // - maxBuyIn >= minBuyIn (if both provided)
1011
+
1012
+ // Create table request
1013
+ const createResult = CreateTableSchema.safeParse({
1014
+ name: "High Stakes",
1015
+ mode: "CASH",
1016
+ smallBlind: 25,
1017
+ bigBlind: 50,
1018
+ maxPlayers: 6, // defaults to 9 if omitted
1019
+ minBuyIn: 1000,
1020
+ maxBuyIn: 5000,
1021
+ });
1022
+ ```
1023
+
1024
+ ### API Request Schemas
1025
+
1026
+ ```typescript
1027
+ import {
1028
+ BuyInRequestSchema,
1029
+ AddChipsRequestSchema,
1030
+ GameActionRequestSchema,
1031
+ BuyInRequest,
1032
+ AddChipsRequest,
1033
+ GameActionRequest,
1034
+ } from "@pokertools/types";
1035
+
1036
+ // Buy-in request validation
1037
+ const buyInResult = BuyInRequestSchema.safeParse({
1038
+ amount: 1000,
1039
+ seat: 0,
1040
+ idempotencyKey: "unique-key-123",
1041
+ sitInOption: "WAIT_FOR_BB",
1042
+ });
1043
+
1044
+ // Add chips request validation
1045
+ const addChipsResult = AddChipsRequestSchema.safeParse({
1046
+ amount: 500,
1047
+ idempotencyKey: "unique-key-456",
1048
+ });
1049
+
1050
+ // Game action request validation
1051
+ const gameActionResult = GameActionRequestSchema.safeParse({
1052
+ type: "RAISE",
1053
+ amount: 200,
1054
+ });
1055
+ ```
1056
+
1057
+ ### Validation Rules Summary
1058
+
1059
+ | Schema | Key Validations |
1060
+ | ------------------------- | ------------------------------------------------ |
1061
+ | `SitActionSchema` | seat: 0-9, stack: positive int, name: 1-50 chars |
1062
+ | `ReserveSeatActionSchema` | seat: 0-9, expiryTimestamp: positive int |
1063
+ | `BetActionSchema` | amount: positive int |
1064
+ | `RaiseActionSchema` | amount: positive int |
1065
+ | `TableConfigSchema` | bigBlind > smallBlind, maxPlayers: 2-10 |
1066
+ | `CreateTableSchema` | mode: CASH\|TOURNAMENT, maxBuyIn >= minBuyIn |
1067
+ | `BuyInRequestSchema` | amount: positive int, idempotencyKey: required |
1068
+
1069
+ ---
1070
+
1071
+ ## 🌐 API DTOs
1072
+
1073
+ ### Auth Types
1074
+
1075
+ ```typescript
1076
+ import { LoginRequest, LoginResponse, NonceResponse } from "@pokertools/types";
1077
+
1078
+ interface LoginRequest {
1079
+ message: string;
1080
+ signature: `0x${string}`; // Ethereum signature format
1081
+ }
1082
+
1083
+ interface LoginResponse {
1084
+ token: string;
1085
+ user: {
1086
+ id: string;
1087
+ username: string;
1088
+ };
1089
+ }
1090
+
1091
+ interface NonceResponse {
1092
+ nonce: string;
1093
+ }
1094
+ ```
1095
+
1096
+ ### Table Types
1097
+
1098
+ ```typescript
1099
+ import {
1100
+ TableListItem,
1101
+ GetTablesResponse,
1102
+ StandRequest,
1103
+ TableStateResponse,
1104
+ } from "@pokertools/types";
1105
+
1106
+ interface TableListItem {
1107
+ id: string;
1108
+ name: string;
1109
+ config: TableConfig;
1110
+ status: TableStatus;
1111
+ }
1112
+
1113
+ interface GetTablesResponse {
1114
+ tables: TableListItem[];
1115
+ }
1116
+
1117
+ interface TableStateResponse {
1118
+ state: PublicState;
1119
+ }
1120
+ ```
1121
+
1122
+ ### Common Types
1123
+
1124
+ ```typescript
1125
+ import { ApiErrorResponse, SuccessResponse, GameMode, TableStatus } from "@pokertools/types";
1126
+
1127
+ interface ApiErrorResponse {
1128
+ error: string;
1129
+ message?: string;
1130
+ code?: string;
1131
+ }
1132
+
1133
+ interface SuccessResponse {
1134
+ success: true;
1135
+ }
1136
+
1137
+ type GameMode = "CASH" | "TOURNAMENT";
1138
+ type TableStatus = "WAITING" | "ACTIVE" | "FINISHED";
1139
+ ```
1140
+
1141
+ ---
1142
+
1143
+ ## 🎴 Card Representation
1144
+
1145
+ Cards are represented as **2-character strings**:
1146
+
1147
+ ```
1148
+ ┌─────────────────────────────────────────────────────────────┐
1149
+ │ CARD FORMAT: [Rank][Suit] │
1150
+ ├─────────────────────────────────────────────────────────────┤
1151
+ │ Ranks: 2, 3, 4, 5, 6, 7, 8, 9, T (10), J, Q, K, A │
1152
+ │ Suits: s (♠), h (♥), d (♦), c (♣) │
1153
+ ├─────────────────────────────────────────────────────────────┤
1154
+ │ Examples: │
1155
+ │ "As" = Ace of Spades ♠ │
1156
+ │ "Kh" = King of Hearts ♥ │
1157
+ │ "Td" = Ten of Diamonds ♦ │
1158
+ │ "2c" = Two of Clubs ♣ │
1159
+ └─────────────────────────────────────────────────────────────┘
1160
+ ```
1161
+
1162
+ ### Usage in Types
1163
+
1164
+ ```typescript
1165
+ // Hole cards
1166
+ const hand: string[] = ["As", "Kh"];
1167
+
1168
+ // Community board
1169
+ const board: string[] = ["Td", "Jc", "Qs"];
1170
+
1171
+ // Masked cards in PublicPlayer
1172
+ const maskedHand: (string | null)[] = [null, null]; // Both hidden
1173
+ const partialHand: (string | null)[] = ["As", null]; // One shown
1174
+
1175
+ // Deck uses integer codes internally
1176
+ const deck: number[] = [0, 1, 2, ...]; // Engine internal use
1177
+ ```
1178
+
1179
+ ---
1180
+
1181
+ ## 📖 Complete Example
1182
+
1183
+ ```typescript
1184
+ import {
1185
+ GameState,
1186
+ PublicState,
1187
+ Action,
1188
+ ActionType,
1189
+ PlayerStatus,
1190
+ Street,
1191
+ ActionSchema,
1192
+ ErrorCodes,
1193
+ } from "@pokertools/types";
1194
+
1195
+ // Validate incoming action from client
1196
+ function processPlayerAction(rawAction: unknown, state: PublicState): void {
1197
+ // Runtime validation
1198
+ const result = ActionSchema.safeParse(rawAction);
1199
+
1200
+ if (!result.success) {
1201
+ throw new Error(`Invalid action: ${result.error.message}`);
1202
+ }
1203
+
1204
+ const action = result.data;
1205
+
1206
+ // Type narrowing with discriminated union
1207
+ switch (action.type) {
1208
+ case ActionType.FOLD:
1209
+ console.log(`Player ${action.playerId} folds`);
1210
+ break;
1211
+
1212
+ case ActionType.BET:
1213
+ console.log(`Player ${action.playerId} bets ${action.amount}`);
1214
+ break;
1215
+
1216
+ case ActionType.RAISE:
1217
+ console.log(`Player ${action.playerId} raises to ${action.amount}`);
1218
+ break;
1219
+
1220
+ case ActionType.CALL:
1221
+ console.log(`Player ${action.playerId} calls`);
1222
+ break;
1223
+
1224
+ case ActionType.CHECK:
1225
+ console.log(`Player ${action.playerId} checks`);
1226
+ break;
1227
+
1228
+ default:
1229
+ // TypeScript knows all cases are handled
1230
+ const _exhaustive: never = action;
1231
+ }
1232
+ }
1233
+
1234
+ // Work with public state
1235
+ function renderTable(state: PublicState): void {
1236
+ console.log(`Hand #${state.handNumber} - ${state.street}`);
1237
+ console.log(`Board: ${state.board.join(" ") || "(no cards)"}`);
1238
+ console.log(`Pot: ${state.pots.reduce((sum, p) => sum + p.amount, 0)}`);
1239
+
1240
+ for (const player of state.players) {
1241
+ if (player) {
1242
+ const cards = player.hand ? player.hand.map((c) => c ?? "??").join(" ") : "folded";
1243
+ console.log(
1244
+ `Seat ${player.seat}: ${player.name} (${player.stack}) [${cards}] - ${player.status}`
1245
+ );
1246
+ }
1247
+ }
1248
+
1249
+ if (state.actionTo !== null) {
1250
+ const actor = state.players[state.actionTo];
1251
+ console.log(`Action to: ${actor?.name}`);
1252
+ }
1253
+ }
1254
+ ```
1255
+
1256
+ ---
1257
+
1258
+ ## 🔧 TypeScript Configuration
1259
+
1260
+ For optimal type checking, use these compiler options:
1261
+
1262
+ ```json
1263
+ {
1264
+ "compilerOptions": {
1265
+ "strict": true,
1266
+ "preserveConstEnums": true,
1267
+ "esModuleInterop": true
1268
+ }
1269
+ }
1270
+ ```
68
1271
 
69
- ## Philosophy
1272
+ **Note:** `preserveConstEnums` is recommended for `const enum` types like `ActionType`, `PlayerStatus`, and `Street`.
70
1273
 
71
- This package follows these principles:
1274
+ ---
72
1275
 
73
- 1. **Pure Types Only** - No runtime code, no validation, no logic
74
- 2. **Immutable by Design** - All fields are readonly
75
- 3. **Zero Dependencies** - Lightweight for frontend use
76
- 4. **Single Source of Truth** - Used by engine, API, and SDK
1276
+ ## 📄 License
77
1277
 
78
- ## Related Packages
1278
+ MIT © A.Aurelius
79
1279
 
80
- - `@pokertools/engine` - Game engine (depends on this package)
81
- - `@pokertools/api` - REST/WebSocket API (depends on this package)
82
- - `@pokertools/sdk` - Frontend SDK (depends on this package)
1280
+ ---
83
1281
 
84
- ## License
1282
+ ## 🔗 Related Packages
85
1283
 
86
- MIT
1284
+ | Package | Description |
1285
+ | ------------------------------------- | ------------------ |
1286
+ | [@pokertools/engine](../engine) | Game state machine |
1287
+ | [@pokertools/evaluator](../evaluator) | Hand evaluation |
1288
+ | [@pokertools/api](../api) | REST/WebSocket API |