@pokertools/engine 1.0.0

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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +607 -0
  3. package/dist/actions/betting.d.ts +21 -0
  4. package/dist/actions/betting.js +410 -0
  5. package/dist/actions/dealing.d.ts +9 -0
  6. package/dist/actions/dealing.js +206 -0
  7. package/dist/actions/management.d.ts +9 -0
  8. package/dist/actions/management.js +58 -0
  9. package/dist/actions/showdownActions.d.ts +9 -0
  10. package/dist/actions/showdownActions.js +119 -0
  11. package/dist/actions/special.d.ts +14 -0
  12. package/dist/actions/special.js +98 -0
  13. package/dist/actions/streetProgression.d.ts +13 -0
  14. package/dist/actions/streetProgression.js +157 -0
  15. package/dist/actions/tournament.d.ts +5 -0
  16. package/dist/actions/tournament.js +38 -0
  17. package/dist/actions/validation.d.ts +6 -0
  18. package/dist/actions/validation.js +182 -0
  19. package/dist/engine/PokerEngine.d.ts +92 -0
  20. package/dist/engine/PokerEngine.js +246 -0
  21. package/dist/engine/gameReducer.d.ts +10 -0
  22. package/dist/engine/gameReducer.js +135 -0
  23. package/dist/errors/ConfigError.d.ts +8 -0
  24. package/dist/errors/ConfigError.js +15 -0
  25. package/dist/errors/CriticalStateError.d.ts +8 -0
  26. package/dist/errors/CriticalStateError.js +15 -0
  27. package/dist/errors/ErrorCodes.d.ts +38 -0
  28. package/dist/errors/ErrorCodes.js +46 -0
  29. package/dist/errors/IllegalActionError.d.ts +9 -0
  30. package/dist/errors/IllegalActionError.js +15 -0
  31. package/dist/errors/PokerEngineError.d.ts +8 -0
  32. package/dist/errors/PokerEngineError.js +19 -0
  33. package/dist/errors/index.d.ts +5 -0
  34. package/dist/errors/index.js +22 -0
  35. package/dist/history/exporter.d.ts +28 -0
  36. package/dist/history/exporter.js +60 -0
  37. package/dist/history/formats/json.d.ts +14 -0
  38. package/dist/history/formats/json.js +46 -0
  39. package/dist/history/formats/pokerstars.d.ts +10 -0
  40. package/dist/history/formats/pokerstars.js +188 -0
  41. package/dist/history/handHistoryBuilder.d.ts +10 -0
  42. package/dist/history/handHistoryBuilder.js +179 -0
  43. package/dist/history/types.d.ts +73 -0
  44. package/dist/history/types.js +5 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +38 -0
  47. package/dist/rules/actionOrder.d.ts +14 -0
  48. package/dist/rules/actionOrder.js +211 -0
  49. package/dist/rules/blinds.d.ts +24 -0
  50. package/dist/rules/blinds.js +64 -0
  51. package/dist/rules/headsUp.d.ts +15 -0
  52. package/dist/rules/headsUp.js +44 -0
  53. package/dist/rules/showdown.d.ts +9 -0
  54. package/dist/rules/showdown.js +164 -0
  55. package/dist/rules/sidePots.d.ts +32 -0
  56. package/dist/rules/sidePots.js +173 -0
  57. package/dist/tsconfig.tsbuildinfo +1 -0
  58. package/dist/utils/cardUtils.d.ts +12 -0
  59. package/dist/utils/cardUtils.js +30 -0
  60. package/dist/utils/constants.d.ts +38 -0
  61. package/dist/utils/constants.js +41 -0
  62. package/dist/utils/deck.d.ts +46 -0
  63. package/dist/utils/deck.js +126 -0
  64. package/dist/utils/invariants.d.ts +39 -0
  65. package/dist/utils/invariants.js +163 -0
  66. package/dist/utils/positioning.d.ts +36 -0
  67. package/dist/utils/positioning.js +97 -0
  68. package/dist/utils/rake.d.ts +13 -0
  69. package/dist/utils/rake.js +45 -0
  70. package/dist/utils/serialization.d.ts +53 -0
  71. package/dist/utils/serialization.js +106 -0
  72. package/dist/utils/validation.d.ts +20 -0
  73. package/dist/utils/validation.js +52 -0
  74. package/dist/utils/viewMasking.d.ts +20 -0
  75. package/dist/utils/viewMasking.js +90 -0
  76. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 PokerTools
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,607 @@
1
+ # @pokertools/engine
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@pokertools/engine.svg)](https://www.npmjs.com/package/@pokertools/engine)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/pokertools/engine/main.yml?branch=main)](https://github.com/pokertools/engine/actions)
6
+ [![Coverage](https://img.shields.io/codecov/c/github/pokertools/engine)](https://codecov.io/gh/pokertools/engine)
7
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@pokertools/engine)](https://bundlephobia.com/package/@pokertools/engine)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue.svg)](https://www.typescriptlang.org/)
9
+
10
+ An enterprise-grade, **deterministic, immutable, and high-performance** Texas Hold'em poker engine.
11
+
12
+ Built on the **Redux design pattern**, this engine treats the poker game as a finite state machine. It accepts a `GameState` and an `Action`, and returns a new `GameState`. This architecture makes it uniquely suited for:
13
+
14
+ - **Multiplayer Servers:** Easy synchronization, crash recovery, and concurrency.
15
+ - **AI Training:** Fast simulations for Monte Carlo / Reinforcement Learning (17m hands/sec using `@pokertools/evaluator`).
16
+ - **Real Money Gaming:** Auditable RNG, integer-only arithmetic, and strict invariant checking.
17
+ - **Solvers:** Correct handling of complex side-pots, split-pots, and heads-up positioning.
18
+
19
+ ## Table of Contents
20
+
21
+ - [Features](#features)
22
+ - [Installation](#installation)
23
+ - [Quick Start](#quick-start)
24
+ - [Architecture](#architecture)
25
+ - [Project Structure](#project-structure)
26
+ - [Advanced Usage](#advanced-usage)
27
+ - [Provable Fairness (Custom RNG)](#provable-fairness-custom-rng)
28
+ - [Crash Recovery (Snapshots)](#crash-recovery-snapshots)
29
+ - [Event Subscription & Middleware](#event-subscription--middleware)
30
+ - [Hand History Export](#hand-history-export)
31
+ - [Time Banks & Timeout Logic](#time-banks--timeout-logic)
32
+ - [Undo & Rollback](#undo--rollback)
33
+ - [Tournament Blind Schedules](#tournament-blind-schedules)
34
+ - [Scalability & Worker Threads](#scalability--worker-threads)
35
+ - [Security & Integrity](#security--integrity)
36
+ - [View Masking (Anti-Cheat)](#view-masking-anti-cheat)
37
+ - [Invariant Auditing](#invariant-auditing)
38
+ - [Integer Arithmetic](#integer-arithmetic)
39
+ - [Rule Logic & Edge Cases](#rule-logic--edge-cases)
40
+ - [API Reference](#api-reference)
41
+ - [Error Handling](#error-handling)
42
+ - [Contributing](#contributing)
43
+ - [License](#license)
44
+
45
+ ## Features
46
+
47
+ - **Pure State Machine:** Zero internal mutation. `f(state, action) => newState`.
48
+ - **Provably Fair:** Inject your own RNG (e.g., CSPRNG or hardware RNG) for completely auditable shuffling.
49
+ - **Crash Resilient:** Export lightweight JSON snapshots and restore game state instantly from a database.
50
+ - **Event Driven:** Subscribe to state changes to trigger UI sounds, animations, or analytics.
51
+ - **Complex Logic Solved:**
52
+ - **Side Pots:** Handles multi-way all-ins with mathematically correct pot segregation using the iterative subtraction method.
53
+ - **Split Pots:** Distributes odd chips by position (closest to left of button) or suit automatically.
54
+ - **Heads-Up Rules:** Automatically switches Button/SB positioning logic when only 2 players remain.
55
+ - **Auto-Runout:** Automatically deals remaining streets and calculates winners when all active players are all-in.
56
+ - **Rake Support:** Configurable rake percentage and cap for cash games (tournaments automatically excluded).
57
+ - **Granular Card Visibility:** Players can selectively show cards at showdown (e.g., show only one card to prove a bluff).
58
+ - **Time Bank Support:** Native support for "Time Bank" resource management and explicit timeout resolutions.
59
+ - **Hand History:** Native export to standard PokerStars/PHH text formats for compatibility with tracking software (PokerTracker 4, HM3).
60
+ - **Strict Typing:** Written in TypeScript with exhaustive definitions for every state transition.
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ # npm
66
+ npm install @pokertools/engine
67
+
68
+ # yarn
69
+ yarn add @pokertools/engine
70
+
71
+ # pnpm
72
+ pnpm add @pokertools/engine
73
+ ```
74
+
75
+ ## Quick Start
76
+
77
+ This example demonstrates a simple Pre-flop to Flop sequence.
78
+
79
+ ```typescript
80
+ import { PokerEngine, ActionType } from "@pokertools/engine";
81
+
82
+ // 1. Setup Table
83
+ const engine = new PokerEngine({ smallBlind: 10, bigBlind: 20 });
84
+
85
+ // 2. Event Listener (Logging)
86
+ // Subscribe before playing to capture all events
87
+ engine.on((action, oldState, newState) => {
88
+ console.log(`[${action.type}] ${newState.street} - Pot: ${newState.pot}`);
89
+ });
90
+
91
+ // 3. Manage Players
92
+ engine.sit(0, "p1", "Alice", 1000);
93
+ engine.sit(1, "p2", "Bob", 1000);
94
+ engine.sit(2, "p3", "Tom", 100);
95
+
96
+ engine.stand("p3"); // Tom leaves
97
+ engine.sit(2, "p4", "Charlie", 500); // Charlie takes the seat
98
+
99
+ // 4. Start Hand
100
+ // Button: Alice (Seat 0), SB: Bob (Seat 1), BB: Charlie (Seat 2)
101
+ engine.deal();
102
+
103
+ // 5. Pre-Flop Action
104
+ // Action starts with Button (Alice) in 3-handed play
105
+ engine.act({ type: ActionType.FOLD, playerId: "p1" }); // Alice folds
106
+ engine.act({ type: ActionType.CALL, playerId: "p2" }); // Bob completes SB to 20 (posts 10 more)
107
+ engine.act({ type: ActionType.CHECK, playerId: "p4" }); // Charlie checks option (already posted BB)
108
+
109
+ // 6. Flop Action (Pot: 40)
110
+ // Engine auto-deals Flop. Bob acts first (first active player left of button).
111
+ engine.act({ type: ActionType.CHECK, playerId: "p2" });
112
+ engine.act({ type: ActionType.BET, playerId: "p4", amount: 40 }); // Charlie bets 40
113
+ engine.act({ type: ActionType.CALL, playerId: "p2" }); // Bob calls 40
114
+
115
+ // 7. Inspect Data
116
+ // Global state (Admin/Server only - reveals all cards)
117
+ const globalState = engine.state;
118
+
119
+ // Player view (Safe for Client - masks opponents' cards)
120
+ const bobView = engine.view("p2");
121
+
122
+ console.log(`Board: ${globalState.board}`); // e.g., ["As", "Kd", "2c"]
123
+ console.log(`Bob's Hand: ${bobView.players[1].hand}`); // e.g., ["Ah", "Kh"]
124
+ console.log(`Charlie's Hand: ${bobView.players[2].hand ?? "Hidden"}`); // "Hidden" (masked)
125
+ ```
126
+
127
+ ### Configuration Examples
128
+
129
+ #### Cash Game with Rake
130
+
131
+ ```typescript
132
+ const cashEngine = new PokerEngine({
133
+ smallBlind: 5,
134
+ bigBlind: 10,
135
+ rakePercent: 5, // 5% rake
136
+ rakeCap: 10, // Max 10 chips per pot
137
+ });
138
+
139
+ // After showdown
140
+ console.log(cashEngine.state.rakeThisHand); // e.g., 5 (rake collected)
141
+ // Chip conservation automatically accounts for rake
142
+ ```
143
+
144
+ #### Tournament (No Rake)
145
+
146
+ ```typescript
147
+ const tournamentEngine = new PokerEngine({
148
+ smallBlind: 25,
149
+ bigBlind: 50,
150
+ ante: 5,
151
+ blindStructure: [
152
+ { smallBlind: 25, bigBlind: 50, ante: 5 },
153
+ { smallBlind: 50, bigBlind: 100, ante: 10 },
154
+ { smallBlind: 100, bigBlind: 200, ante: 25 },
155
+ ],
156
+ });
157
+
158
+ // Rake is automatically disabled for tournaments
159
+ console.log(tournamentEngine.state.rakeThisHand); // Always 0
160
+ ```
161
+
162
+ ## Architecture
163
+
164
+ Unlike traditional object-oriented poker engines where `player.bet()` mutates the player object in place, `@pokertools/engine` uses a **Reducer Pattern**.
165
+
166
+ ```typescript
167
+ function gameReducer(state: GameState, action: Action): GameState;
168
+ ```
169
+
170
+ This design enables:
171
+
172
+ 1. **Time Travel:** You can save an array of `Action` objects and replay an entire hand perfectly to debug issues.
173
+ 2. **Concurrency:** Since the state is immutable, you can safely read the state in one thread (e.g., sending updates to clients) while calculating the next state in another.
174
+ 3. **Testability:** Testing becomes a matter of input vs. output, without complex setup/teardown of class instances.
175
+
176
+ ## Project Structure
177
+
178
+ The recommended file structure for this library follows separation-of-concerns principles:
179
+
180
+ ```
181
+ @pokertools/engine/
182
+ .
183
+ ├── LICENSE
184
+ ├── README.md
185
+ ├── jest.config.js
186
+ ├── package.json
187
+ ├── src
188
+ │   ├── actions
189
+ │   │   ├── betting.ts
190
+ │   │   ├── dealing.ts
191
+ │   │   ├── management.ts
192
+ │   │   ├── showdownActions.ts
193
+ │   │   ├── special.ts
194
+ │   │   ├── streetProgression.ts
195
+ │   │   ├── tournament.ts
196
+ │   │   └── validation.ts
197
+ │   ├── engine
198
+ │   │   ├── PokerEngine.ts
199
+ │   │   └── gameReducer.ts
200
+ │   ├── errors
201
+ │   │   ├── ConfigError.ts
202
+ │   │   ├── CriticalStateError.ts
203
+ │   │   ├── ErrorCodes.ts
204
+ │   │   ├── IllegalActionError.ts
205
+ │   │   ├── PokerEngineError.ts
206
+ │   │   └── index.ts
207
+ │   ├── history
208
+ │   │   ├── exporter.ts
209
+ │   │   ├── formats
210
+ │   │   │   ├── json.ts
211
+ │   │   │   └── pokerstars.ts
212
+ │   │   ├── handHistoryBuilder.ts
213
+ │   │   └── types.ts
214
+ │   ├── index.ts
215
+ │   ├── rules
216
+ │   │   ├── actionOrder.ts
217
+ │   │   ├── blinds.ts
218
+ │   │   ├── headsUp.ts
219
+ │   │   ├── showdown.ts
220
+ │   │   └── sidePots.ts
221
+ │   └── utils
222
+ │   ├── cardUtils.ts
223
+ │   ├── constants.ts
224
+ │   ├── deck.ts
225
+ │   ├── invariants.ts
226
+ │   ├── positioning.ts
227
+ │   ├── rake.ts
228
+ │   ├── serialization.ts
229
+ │   ├── validation.ts
230
+ │   └── viewMasking.ts
231
+ ├── tests
232
+ │   ├── bugs
233
+ │   ├── debug
234
+ │   ├── integration
235
+ │   ├── property
236
+ │   ├── security
237
+ │   └── unit
238
+ └── tsconfig.json
239
+ ```
240
+
241
+ ## Advanced Usage
242
+
243
+ ### Provable Fairness (Custom RNG)
244
+
245
+ By default, the engine uses `Math.random()`. For real-money gaming, tournaments, or replayable simulations, you **must** inject a seeded or crypto-secure generator. This allows you to prove to players that the deck was shuffled fairly.
246
+
247
+ ```typescript
248
+ import seedrandom from "seedrandom";
249
+
250
+ // Create a seeded generator (or use a Crypto API)
251
+ const rng = seedrandom("championship-final-table-seed-12345");
252
+
253
+ const engine = new PokerEngine({
254
+ smallBlind: 10,
255
+ bigBlind: 20,
256
+ // The engine will use this function for all shuffling and random decisions
257
+ randomProvider: () => rng.quick(),
258
+ });
259
+ ```
260
+
261
+ ### Crash Recovery (Snapshots)
262
+
263
+ Because the state is immutable and serializable, you can save the game state to a persistent store (Redis, Postgres, File System) after every move. If your Node.js process crashes, you can restore the table instantly.
264
+
265
+ ```typescript
266
+ // --- 1. SAVING ---
267
+ // Get a lightweight, serializable JSON object
268
+ const snapshot = engine.snapshot;
269
+ // Save to database (e.g., Redis key "table:101")
270
+ await db.save("table:101", JSON.stringify(snapshot));
271
+
272
+ // ... Server Crashes or Restarts ...
273
+
274
+ // --- 2. RESTORING ---
275
+ const savedJson = await db.get("table:101");
276
+ const snapshot = JSON.parse(savedJson);
277
+
278
+ // Create a new engine instance pre-loaded with the exact previous state
279
+ const engine = PokerEngine.restore(snapshot);
280
+
281
+ // Resume play immediately - players won't even notice the restart
282
+ console.log(engine.state.actionTo);
283
+ ```
284
+
285
+ ### Event Subscription & Middleware
286
+
287
+ Since the engine is pure, you need a way to know "What just happened?" to trigger side effects like playing sounds, updating the UI, or logging to an analytics server.
288
+
289
+ The engine provides a subscription model that receives the `Action`, the `OldState`, and the `NewState`.
290
+
291
+ ```typescript
292
+ engine.on((action, oldState, newState) => {
293
+ // 1. Detect Phase Changes (e.g., Preflop -> Flop)
294
+ if (newState.street !== oldState.street) {
295
+ console.log(`[EVENT] Dealing ${newState.street}: ${newState.board}`);
296
+ socket.emit("playSound", "deal_cards");
297
+ }
298
+
299
+ // 2. Detect Player Actions
300
+ if (action.type === ActionType.FOLD) {
301
+ console.log(`[EVENT] Player ${action.playerId} folded.`);
302
+ socket.emit("animation", { type: "fold", seat: action.playerId });
303
+ }
304
+
305
+ // 3. Detect Winners
306
+ if (newState.winners && !oldState.winners) {
307
+ console.log(`[EVENT] Winners:`, newState.winners);
308
+ // Trigger chip gathering animation
309
+ }
310
+ });
311
+ ```
312
+
313
+ ### Hand History Export
314
+
315
+ Serious players require Hand Histories to analyze their gameplay in tools like **PokerTracker 4**, **Holdem Manager 3**, or **GTO Wizard**. The engine can export the current hand in a standardized text format.
316
+
317
+ ```typescript
318
+ // Call this at the end of a hand (Street = SHOWDOWN)
319
+ const historyText = engine.history();
320
+
321
+ console.log(historyText);
322
+
323
+ /* Output Example:
324
+ PokerStars Hand #23948239048: Hold'em No Limit ($10/$20 USD) - YYYY/MM/DD
325
+ Table 'Alpha' 6-max Seat #1 is the button
326
+ Seat 1: Alice ($1000 in chips)
327
+ Seat 2: Bob ($1000 in chips)
328
+ Bob: posts small blind $10
329
+ Alice: posts big blind $20
330
+ *** HOLE CARDS ***
331
+ Dealt to Bob [Ah Kh]
332
+ Bob: raises $40 to $60
333
+ ...
334
+ */
335
+ ```
336
+
337
+ ### Time Banks & Timeout Logic
338
+
339
+ The engine follows a strict separation of concerns:
340
+
341
+ - **The Server** manages the clock (Real-time).
342
+ - **The Engine** manages the Time Bank (Resource).
343
+
344
+ The engine does not include a `setTimeout`. Instead, you dispatch explicit actions when the server determines time has expired.
345
+
346
+ ```typescript
347
+ // Scenario: Server timer hits 0.0s for Player P1
348
+
349
+ // 1. Check if player has Time Bank remaining
350
+ if (engine.canUseTimeBank("p1")) {
351
+ // Auto-activate time bank: Deducts time tokens and keeps action on P1
352
+ engine.act({ type: ActionType.TIME_BANK, playerId: "p1" });
353
+ console.log("Time Bank activated!");
354
+ } else {
355
+ // 2. No time left: Force a Fold (or Check if allowed)
356
+ // This action will also mark the player as "Sitting Out"
357
+ engine.act({ type: ActionType.TIMEOUT, playerId: "p1" });
358
+ console.log("Player timed out and folded.");
359
+ }
360
+ ```
361
+
362
+ ### Undo & Rollback
363
+
364
+ Essential for admin tools, friendly games, or correcting misclicks. Since the engine uses a persistent data structure, rolling back to a previous state is computationally cheap and instant.
365
+
366
+ ```typescript
367
+ // 1. Player misclicks Fold
368
+ engine.act({ type: ActionType.FOLD, playerId: "p1" });
369
+
370
+ // 2. Admin intervenes
371
+ engine.undo();
372
+
373
+ // 3. State is now exactly as it was before the fold
374
+ // The actionTo pointer is back on p1
375
+ ```
376
+
377
+ ### Tournament Blind Schedules
378
+
379
+ For tournaments, you can define a blind structure. The engine does not auto-increment automatically (as that is often time-based), but provides a simple API to advance levels.
380
+
381
+ ```typescript
382
+ const engine = new PokerEngine({
383
+ initialStack: 1500,
384
+ blindStructure: [
385
+ { smallBlind: 10, bigBlind: 20, ante: 0 }, // Level 1
386
+ { smallBlind: 20, bigBlind: 40, ante: 0 }, // Level 2
387
+ { smallBlind: 50, bigBlind: 100, ante: 10 }, // Level 3
388
+ ],
389
+ });
390
+
391
+ // ... Play some hands ...
392
+
393
+ // Advance to Level 2
394
+ engine.nextBlindLevel();
395
+ console.log(engine.blinds); // { smallBlind: 20, bigBlind: 40 }
396
+ ```
397
+
398
+ ### Scalability & Worker Threads
399
+
400
+ For high-volume applications (e.g., 10,000 concurrent tables), running poker logic on the main Node.js event loop can block I/O. Because the engine is state-in/state-out, it is trivial to offload to **Worker Threads**.
401
+
402
+ ```typescript
403
+ // worker.ts
404
+ import { parentPort } from "worker_threads";
405
+ import { PokerEngine } from "@pokertools/engine";
406
+
407
+ parentPort?.on("message", (msg) => {
408
+ if (msg.type === "PROCESS_ACTION") {
409
+ // 1. Rehydrate engine from snapshot
410
+ const engine = PokerEngine.restore(msg.snapshot);
411
+ // 2. Run logic
412
+ const newState = engine.act(msg.action);
413
+ // 3. Send result back to Main Thread
414
+ parentPort?.postMessage({ status: "success", state: newState });
415
+ }
416
+ });
417
+ ```
418
+
419
+ ## Security & Integrity
420
+
421
+ ### View Masking (Anti-Cheat)
422
+
423
+ **Critical:** Never send the full `GameState` result from `state` to a client. It contains the Deck and Opponent Hole Cards.
424
+
425
+ Use the built-in view generator to create a sanitized version for specific players.
426
+
427
+ ```typescript
428
+ // Server Code
429
+ const globalState = engine.state;
430
+
431
+ // Send to Alice (Seat 1)
432
+ // - Hides Bob's cards
433
+ // - Hides the Deck
434
+ // - Respects shownCards for granular visibility
435
+ const aliceView = engine.view("p1");
436
+ socket.to("p1").emit("gameState", aliceView);
437
+
438
+ // Send to Bob (Seat 2)
439
+ const bobView = engine.view("p2");
440
+ socket.to("p2").emit("gameState", bobView);
441
+ ```
442
+
443
+ #### Granular Card Visibility (New Feature)
444
+
445
+ At showdown, players can control which cards are revealed using the `shownCards` field:
446
+
447
+ ```typescript
448
+ // Player state after showdown
449
+ player.hand = ["As", "Kd"]; // Actual cards (always preserved)
450
+ player.shownCards = [0, 1]; // Both cards shown (winner)
451
+
452
+ // Or for selective showing
453
+ player.shownCards = [0]; // Only left card shown
454
+ player.shownCards = null; // Mucked (no cards shown)
455
+
456
+ // Public view respects shownCards and preserves positional context:
457
+ // - shownCards: [0, 1] → hand: ["As", "Kd"] (both visible)
458
+ // - shownCards: [0] → hand: ["As", null] (left visible, right hidden)
459
+ // - shownCards: [1] → hand: [null, "Kd"] (left hidden, right visible)
460
+ // - shownCards: null → hand: null (completely mucked)
461
+ ```
462
+
463
+ **Important:** The view masking preserves positional context by using `null` for hidden cards. This ensures clients know which card is being shown (left vs right).
464
+
465
+ #### Optional Show Actions
466
+
467
+ Players can reveal their cards after showdown:
468
+
469
+ ```typescript
470
+ // Loser reveals both cards to show a bluff
471
+ engine.act({
472
+ type: ActionType.SHOW,
473
+ playerId: "loser123",
474
+ // cardIndices optional - defaults to all cards [0, 1]
475
+ });
476
+
477
+ // Or show only specific cards
478
+ engine.act({
479
+ type: ActionType.SHOW,
480
+ playerId: "player456",
481
+ cardIndices: [0], // Show only left card
482
+ });
483
+
484
+ // Winner can also explicitly show (already shown by default)
485
+ engine.act({
486
+ type: ActionType.SHOW,
487
+ playerId: "winner789",
488
+ });
489
+ ```
490
+
491
+ ### Invariant Auditing
492
+
493
+ The engine implements strict accounting logic. After every single action, it runs an internal audit to ensure **Conservation of Chips**.
494
+
495
+ $$\sum(\text{PlayerStacks}) + \sum(\text{Pots}) + \sum(\text{CurrentBets}) = \text{InitialChips}$$
496
+
497
+ If a logic bug ever causes a chip to duplicate or vanish, the engine throws a `CriticalStateError`.
498
+
499
+ **Recommendation:** Wrap your `act` calls in a try/catch. If this error occurs, **freeze the table** immediately. It indicates a serious data integrity issue.
500
+
501
+ ### Integer Arithmetic
502
+
503
+ To ensure financial accuracy, the engine strictly forbids floating-point numbers.
504
+
505
+ - **Input:** All stacks, bets, and blinds must be Integers (representing cents or the smallest chip unit).
506
+ - **Internal:** Pot divisions use integer division with deterministic remainder distribution (by position).
507
+ - **Safety:** This prevents "Penny Drift" exploits common in JavaScript floating-point math.
508
+
509
+ ## Rule Logic & Edge Cases
510
+
511
+ This engine isn't just a loop; it is a strict implementation of standard TDA (Tournament Directors Association) rules.
512
+
513
+ ### 1. Heads-Up Positioning
514
+
515
+ In a standard game (3+ players), the Small Blind is to the left of the Button.
516
+
517
+ - **The Trap:** In Heads-Up (2 players), the **Button IS the Small Blind**.
518
+ - **The Rule:** The Button acts **first** Pre-Flop, and acts **last** Post-Flop.
519
+ - **Implementation:** The engine detects when exactly 2 players are active (not folded/busted) and automatically swaps the blind posting order and action order to comply with this rule.
520
+
521
+ ### 2. The "Incomplete Raise"
522
+
523
+ - **Scenario:** Player A bets 100. Player B is All-In for 120 (Raise of 20). Min raise is 100.
524
+ - **The Rule:** Since the raise (20) is less than 50% (or 100% depending on ruleset) of the min-raise, the betting is **NOT** re-opened for Player A. Player A can only CALL or FOLD. They cannot re-raise.
525
+ - **Implementation:** The engine tracks `legalActions` and will throw `ILLEGAL_ACTION` if Player A attempts to raise in this spot.
526
+
527
+ ### 3. Split Pot "Odd Chip" Resolution
528
+
529
+ In split pots (e.g., High-Low or Tie), chip counts often result in decimals (e.g., 25 chips / 2 players = 12.5).
530
+
531
+ - **The Rule:** The odd chip goes to the player in the **worst position** (closest to the left of the button). In High-Low games, the odd chip goes to the High hand.
532
+ - **Implementation:** The engine resolves this deterministically using seat indexes relative to the dealer button.
533
+
534
+ ### 4. Side Pots (Iterative Subtraction)
535
+
536
+ The engine handles complex multi-way all-ins.
537
+
538
+ - **Scenario:** A (100 chips), B (500 chips), C (1000 chips) all go All-In.
539
+ - **Pot 1 (Main):** 300 chips (100 each from A, B, C). A, B, and C contest this.
540
+ - **Pot 2 (Side):** 800 chips (400 each from B and C). Only B and C contest this.
541
+ - **Remaining:** C's extra 500 chips are returned (no one to match).
542
+ - **Resolution:** The engine evaluates hands for Pot 2 first, awards it, then evaluates Pot 1.
543
+
544
+ ## API Reference
545
+
546
+ ### `PokerEngine` Class
547
+
548
+ | Method | Arguments | Returns | Description |
549
+ | ------------- | ----------------------- | ------------- | -------------------------------------------------- |
550
+ | `constructor` | `config` | `PokerEngine` | Creates a new table instance. |
551
+ | `sit` | `seat, id, name, stack` | `void` | Adds a player to a specific seat. |
552
+ | `stand` | `id` | `void` | Removes a player. |
553
+ | `deal` | `none` | `void` | Starts the hand (Shuffles/Posts Blinds). |
554
+ | `act` | `action` | `GameState` | Executes a move. |
555
+ | `undo` | `none` | `boolean` | Reverts state to previous step. |
556
+ | `state` | _(Getter)_ | `GameState` | The full, internal, unmasked state. |
557
+ | `view` | `id?` | `PublicState` | Masked state for a player (or spectator if no ID). |
558
+ | `snapshot` | _(Getter)_ | `object` | Serializable JSON for database storage. |
559
+ | `restore` | `snapshot` | `PokerEngine` | **Static.** Recreates engine from backup. |
560
+ | `history` | `none` | `string` | Generates the text log (PHH/PokerStars). |
561
+ | `on` | `fn(act, old, new)` | `unsub` | Subscribe to state changes. |
562
+
563
+ ### `Action` Types
564
+
565
+ ```typescript
566
+ interface Action {
567
+ type: ActionType;
568
+ playerId: string;
569
+ amount?: number; // Required for BET and RAISE
570
+ }
571
+
572
+ enum ActionType {
573
+ FOLD = "FOLD",
574
+ CHECK = "CHECK",
575
+ CALL = "CALL",
576
+ BET = "BET", // Opening a bet
577
+ RAISE = "RAISE", // Increasing an existing bet
578
+ TIMEOUT = "TIMEOUT",
579
+ TIME_BANK = "TIME_BANK", // Extends turn using time bank
580
+ }
581
+ ```
582
+
583
+ ## Error Handling
584
+
585
+ The engine throws typed errors. You should wrap `act` calls in a `try/catch` block.
586
+
587
+ | Error Code | Description | Recommended Action |
588
+ | ---------------------------- | ---------------------------------- | ------------------------------------ |
589
+ | `CRITICAL_INVARIANT_FAILURE` | Chips disappeared/duplicated. | **FREEZE GAME**. Contact Support. |
590
+ | `NOT_YOUR_TURN` | Player acted out of order. | Ignore or warn client. |
591
+ | `INVALID_AMOUNT` | Bet is below min-raise or > stack. | Reject action, ask for valid amount. |
592
+ | `ILLEGAL_ACTION` | Tried to Check when facing a bet. | Reject action. |
593
+ | `STALE_STATE` | Optimistic UI mismatch. | Send fresh `view()` to client. |
594
+
595
+ ## Contributing
596
+
597
+ This project is part of the `@pokertools` monorepo.
598
+
599
+ 1. Clone the repository.
600
+ 2. Run `npm install`.
601
+ 3. Run `npm test`.
602
+
603
+ **Note:** The test suite includes over 500 edge-case scenarios including split pots, kickers, and side-pot math. Please ensure all pass before submitting a PR.
604
+
605
+ ## License
606
+
607
+ MIT
@@ -0,0 +1,21 @@
1
+ import { GameState, FoldAction, CheckAction, CallAction, BetAction, RaiseAction } from "@pokertools/types";
2
+ /**
3
+ * Handle FOLD action
4
+ */
5
+ export declare function handleFold(state: GameState, action: FoldAction): GameState;
6
+ /**
7
+ * Handle CHECK action
8
+ */
9
+ export declare function handleCheck(state: GameState, action: CheckAction): GameState;
10
+ /**
11
+ * Handle CALL action
12
+ */
13
+ export declare function handleCall(state: GameState, action: CallAction): GameState;
14
+ /**
15
+ * Handle BET action
16
+ */
17
+ export declare function handleBet(state: GameState, action: BetAction): GameState;
18
+ /**
19
+ * Handle RAISE action
20
+ */
21
+ export declare function handleRaise(state: GameState, action: RaiseAction): GameState;