@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 +1253 -51
- package/dist/.tsbuildinfo +1 -0
- package/dist/Action.d.ts +1 -0
- package/dist/ActionWhitelist.d.ts +15 -0
- package/dist/ActionWhitelist.js +32 -0
- package/dist/Config.d.ts +1 -0
- package/dist/ErrorCodes.d.ts +74 -0
- package/dist/ErrorCodes.js +128 -0
- package/dist/GameState.d.ts +1 -0
- package/dist/HandHistory.d.ts +63 -17
- package/dist/Player.d.ts +1 -1
- package/dist/PublicState.d.ts +1 -0
- package/dist/WebSocketMessages.d.ts +232 -0
- package/dist/WebSocketMessages.js +146 -0
- package/dist/api/auth.d.ts +14 -0
- package/dist/api/auth.js +2 -0
- package/dist/api/common.d.ts +10 -0
- package/dist/api/common.js +2 -0
- package/dist/api/tables.d.ts +17 -0
- package/dist/api/tables.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/schemas.d.ts +289 -0
- package/dist/schemas.js +259 -0
- package/package.json +20 -8
- package/dist/tsconfig.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -1,86 +1,1288 @@
|
|
|
1
|
-
# @pokertools/types
|
|
1
|
+
# 🃏 @pokertools/types
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **TypeScript type definitions and Zod schemas for the PokerTools ecosystem**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@pokertools/types)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
372
|
+
// RAISE - Raise existing bet
|
|
373
|
+
const raise: RaiseAction = {
|
|
40
374
|
type: ActionType.RAISE,
|
|
41
|
-
playerId: "
|
|
42
|
-
amount:
|
|
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
|
-
|
|
775
|
+
### Type Guards
|
|
48
776
|
|
|
49
|
-
|
|
777
|
+
```typescript
|
|
778
|
+
import {
|
|
779
|
+
isClientMessage,
|
|
780
|
+
isJoinMessage,
|
|
781
|
+
isLeaveMessage,
|
|
782
|
+
isPingMessage,
|
|
783
|
+
isServerMessage,
|
|
784
|
+
} from "@pokertools/types";
|
|
50
785
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
821
|
+
### ErrorCodes Object
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
import { ErrorCodes, ErrorCode } from "@pokertools/types";
|
|
64
825
|
|
|
65
|
-
|
|
826
|
+
const ErrorCodes = {
|
|
827
|
+
// Generic
|
|
828
|
+
INVALID_ACTION: "INVALID_ACTION",
|
|
66
829
|
|
|
67
|
-
|
|
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
|
-
|
|
1272
|
+
**Note:** `preserveConstEnums` is recommended for `const enum` types like `ActionType`, `PlayerStatus`, and `Street`.
|
|
70
1273
|
|
|
71
|
-
|
|
1274
|
+
---
|
|
72
1275
|
|
|
73
|
-
|
|
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
|
-
|
|
1278
|
+
MIT © A.Aurelius
|
|
79
1279
|
|
|
80
|
-
|
|
81
|
-
- `@pokertools/api` - REST/WebSocket API (depends on this package)
|
|
82
|
-
- `@pokertools/sdk` - Frontend SDK (depends on this package)
|
|
1280
|
+
---
|
|
83
1281
|
|
|
84
|
-
##
|
|
1282
|
+
## 🔗 Related Packages
|
|
85
1283
|
|
|
86
|
-
|
|
1284
|
+
| Package | Description |
|
|
1285
|
+
| ------------------------------------- | ------------------ |
|
|
1286
|
+
| [@pokertools/engine](../engine) | Game state machine |
|
|
1287
|
+
| [@pokertools/evaluator](../evaluator) | Hand evaluation |
|
|
1288
|
+
| [@pokertools/api](../api) | REST/WebSocket API |
|