@pokertools/engine 1.0.1 โ†’ 1.0.4

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 (38) hide show
  1. package/README.md +591 -445
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/actions/betting.js +7 -2
  4. package/dist/actions/dealing.js +46 -20
  5. package/dist/actions/management.js +26 -5
  6. package/dist/actions/special.d.ts +18 -0
  7. package/dist/actions/special.js +20 -0
  8. package/dist/browser.d.ts +27 -0
  9. package/dist/browser.js +73 -0
  10. package/dist/engine/PokerEngine.d.ts +23 -2
  11. package/dist/engine/PokerEngine.js +54 -2
  12. package/dist/errors/ErrorCodes.d.ts +4 -35
  13. package/dist/errors/ErrorCodes.js +7 -41
  14. package/dist/errors/index.d.ts +0 -1
  15. package/dist/errors/index.js +1 -1
  16. package/dist/history/exporter.d.ts +1 -2
  17. package/dist/history/formats/json.d.ts +1 -1
  18. package/dist/history/formats/pokerstars.d.ts +1 -1
  19. package/dist/history/handHistoryBuilder.d.ts +1 -2
  20. package/dist/history/handHistoryBuilder.js +4 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/rules/actionOrder.js +4 -4
  23. package/dist/rules/blinds.d.ts +2 -0
  24. package/dist/rules/blinds.js +27 -3
  25. package/dist/rules/headsUp.js +18 -0
  26. package/dist/rules/showdown.js +10 -0
  27. package/dist/utils/cardUtils.d.ts +2 -1
  28. package/dist/utils/cardUtils.js +2 -1
  29. package/dist/utils/invariants.js +4 -0
  30. package/dist/utils/positioning.js +2 -2
  31. package/dist/utils/serialization.d.ts +1 -0
  32. package/dist/utils/serialization.js +2 -0
  33. package/dist/utils/viewMasking.d.ts +2 -1
  34. package/dist/utils/viewMasking.js +9 -1
  35. package/package.json +31 -5
  36. package/dist/history/types.d.ts +0 -73
  37. package/dist/history/types.js +0 -5
  38. package/dist/tsconfig.tsbuildinfo +0 -1
package/README.md CHANGED
@@ -1,607 +1,753 @@
1
- # @pokertools/engine
1
+ # ๐Ÿƒ @pokertools/engine
2
+
3
+ > **Enterprise-grade Texas Hold'em poker game engine**
2
4
 
3
5
  [![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-blue.svg)](https://opensource.org/licenses/MIT)
5
- [![Build Status](https://img.shields.io/github/actions/workflow/status/aaurelions/pokertools/ci.yml?branch=main)](https://github.com/aaurelions/pokertools/actions)
6
- [![Coverage](https://img.shields.io/codecov/c/github/aaurelions/pokertools)](https://codecov.io/gh/aaurelions/pokertools)
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
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
+ [![Tests](https://img.shields.io/badge/tests-320%20passed-brightgreen.svg)]()
9
+
10
+ A **production-ready** poker game engine featuring immutable state management, chip conservation auditing, side pot calculation, rake handling, tournament support, and comprehensive rule enforcement.
11
+
12
+ ---
13
+
14
+ ## โœจ Features
15
+
16
+ ```
17
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
18
+ โ”‚ ENGINE FEATURES โ”‚
19
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
20
+ โ”‚ ๐ŸŽฐ Complete Texas Hold'em Implementation โ”‚
21
+ โ”‚ โ™ป๏ธ Immutable State Machine (Redux-style) โ”‚
22
+ โ”‚ ๐Ÿ’ฐ Chip Conservation Auditing โ”‚
23
+ โ”‚ ๐Ÿฆ Side Pot Calculation โ”‚
24
+ โ”‚ ๐Ÿ“Š Rake Support (% + cap + noFlopNoDrop) โ”‚
25
+ โ”‚ ๐Ÿ† Tournament Mode (blind structure) โ”‚
26
+ โ”‚ ๐Ÿ‘€ View Masking (anti-cheat) โ”‚
27
+ โ”‚ ๐Ÿ’พ Snapshot Serialization โ”‚
28
+ โ”‚ โ†ฉ๏ธ Undo Support โ”‚
29
+ โ”‚ ๐Ÿ“œ Hand History Export (JSON, PokerStars) โ”‚
30
+ โ”‚ ๐ŸŒ Browser Support (Web Crypto RNG) โ”‚
31
+ โ”‚ ๐Ÿ”’ Type-safe Error Handling โ”‚
32
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
33
+ ```
34
+
35
+ ---
36
+
37
+ ## ๐Ÿ“ฆ Installation
63
38
 
64
39
  ```bash
65
- # npm
66
40
  npm install @pokertools/engine
41
+ ```
67
42
 
68
- # yarn
43
+ ```bash
69
44
  yarn add @pokertools/engine
45
+ ```
70
46
 
71
- # pnpm
47
+ ```bash
72
48
  pnpm add @pokertools/engine
73
49
  ```
74
50
 
75
- ## Quick Start
51
+ ---
76
52
 
77
- This example demonstrates a simple Pre-flop to Flop sequence.
53
+ ## ๐Ÿš€ Quick Start
78
54
 
79
55
  ```typescript
80
56
  import { PokerEngine, ActionType } from "@pokertools/engine";
81
57
 
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}`);
58
+ // Create a cash game table
59
+ const engine = new PokerEngine({
60
+ smallBlind: 1,
61
+ bigBlind: 2,
62
+ maxPlayers: 6,
89
63
  });
90
64
 
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);
65
+ // Seat players
66
+ engine.sit(0, "alice", "Alice", 200);
67
+ engine.sit(1, "bob", "Bob", 200);
95
68
 
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)
69
+ // Deal a hand
101
70
  engine.deal();
102
71
 
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)
72
+ // Get current state
73
+ console.log(engine.state.street); // "PREFLOP"
74
+ console.log(engine.state.actionTo); // 0 (Alice's turn)
75
+
76
+ // Execute actions
77
+ engine.act({ type: ActionType.CALL, playerId: "alice" });
78
+ engine.act({ type: ActionType.CHECK, playerId: "bob" });
108
79
 
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
80
+ // Get player view (masked for opponents)
81
+ const aliceView = engine.view("alice");
82
+ ```
114
83
 
115
- // 7. Inspect Data
116
- // Global state (Admin/Server only - reveals all cards)
117
- const globalState = engine.state;
84
+ ---
118
85
 
119
- // Player view (Safe for Client - masks opponents' cards)
120
- const bobView = engine.view("p2");
86
+ ## ๐Ÿ—๏ธ Architecture
121
87
 
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)
88
+ ```
89
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
90
+ โ”‚ ARCHITECTURE โ”‚
91
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
92
+
93
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
94
+ โ”‚ PokerEngine โ”‚
95
+ โ”‚ (Stateful API Wrapper) โ”‚
96
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
97
+ โ”‚
98
+ โ–ผ
99
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
100
+ โ”‚ gameReducer โ”‚
101
+ โ”‚ f(state, action) => state โ”‚
102
+ โ”‚ (Pure Function) โ”‚
103
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
104
+ โ”‚
105
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
106
+ โ”‚ โ”‚ โ”‚
107
+ โ–ผ โ–ผ โ–ผ
108
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
109
+ โ”‚ Actions โ”‚ โ”‚ Rules โ”‚ โ”‚ Utilities โ”‚
110
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
111
+ โ”‚ โ€ข betting.ts โ”‚ โ”‚ โ€ข actionOrder โ”‚ โ”‚ โ€ข viewMasking โ”‚
112
+ โ”‚ โ€ข dealing.ts โ”‚ โ”‚ โ€ข blinds โ”‚ โ”‚ โ€ข serialization โ”‚
113
+ โ”‚ โ€ข management.ts โ”‚ โ”‚ โ€ข headsUp โ”‚ โ”‚ โ€ข invariants โ”‚
114
+ โ”‚ โ€ข showdown.ts โ”‚ โ”‚ โ€ข showdown โ”‚ โ”‚ โ€ข rake โ”‚
115
+ โ”‚ โ€ข special.ts โ”‚ โ”‚ โ€ข sidePots โ”‚ โ”‚ โ€ข deck โ”‚
116
+ โ”‚ โ€ข tournament.ts โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ€ข cardUtils โ”‚
117
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
125
118
  ```
126
119
 
127
- ### Configuration Examples
120
+ ### State Flow
128
121
 
129
- #### Cash Game with Rake
122
+ ```
123
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
124
+ โ”‚ PREFLOP โ”‚ โ”€โ”€โ–ถ โ”‚ FLOP โ”‚ โ”€โ”€โ–ถ โ”‚ TURN โ”‚ โ”€โ”€โ–ถ โ”‚ RIVER โ”‚ โ”€โ”€โ–ถ โ”‚ SHOWDOWN โ”‚
125
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
126
+ โ”‚ โ”‚
127
+ โ”‚ All but one fold โ”‚
128
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
129
+ (Award pot)
130
+ ```
130
131
 
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
- });
132
+ ---
138
133
 
139
- // After showdown
140
- console.log(cashEngine.state.rakeThisHand); // e.g., 5 (rake collected)
141
- // Chip conservation automatically accounts for rake
142
- ```
134
+ ## ๐Ÿ“– API Reference
135
+
136
+ ### PokerEngine Class
143
137
 
144
- #### Tournament (No Rake)
138
+ #### Constructor
145
139
 
146
140
  ```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
- });
141
+ import { PokerEngine } from "@pokertools/engine";
157
142
 
158
- // Rake is automatically disabled for tournaments
159
- console.log(tournamentEngine.state.rakeThisHand); // Always 0
143
+ const engine = new PokerEngine(config: TableConfig, timeProvider?: () => number);
160
144
  ```
161
145
 
162
- ## Architecture
146
+ **TableConfig Options:**
163
147
 
164
- Unlike traditional object-oriented poker engines where `player.bet()` mutates the player object in place, `@pokertools/engine` uses a **Reducer Pattern**.
148
+ | Option | Type | Default | Description |
149
+ | ------------------- | -------------- | ------------- | ---------------------------- |
150
+ | `smallBlind` | `number` | required | Small blind amount |
151
+ | `bigBlind` | `number` | required | Big blind amount |
152
+ | `ante` | `number` | `0` | Ante per player |
153
+ | `maxPlayers` | `number` | `9` | Maximum seats (2-10) |
154
+ | `blindStructure` | `BlindLevel[]` | - | Tournament blind levels |
155
+ | `timeBankSeconds` | `number` | `30` | Time bank per player |
156
+ | `rakePercent` | `number` | `0` | Rake percentage (0-100) |
157
+ | `rakeCap` | `number` | - | Maximum rake per pot |
158
+ | `noFlopNoDrop` | `boolean` | `true` | No rake if hand ends preflop |
159
+ | `randomProvider` | `() => number` | `Math.random` | RNG function |
160
+ | `validateIntegrity` | `boolean` | `true` | Enable chip auditing |
161
+ | `isClient` | `boolean` | `false` | Client/optimistic mode |
162
+
163
+ ---
164
+
165
+ #### Core Methods
166
+
167
+ ##### `sit(seat, id, name, stack)`
168
+
169
+ Add a player to the table.
165
170
 
166
171
  ```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.
172
+ engine.sit(0, "user123", "Alice", 1000);
173
+ ```
174
+
175
+ ##### `stand(id)`
176
+
177
+ Remove a player from the table.
246
178
 
247
179
  ```typescript
248
- import seedrandom from "seedrandom";
180
+ engine.stand("user123");
181
+ ```
249
182
 
250
- // Create a seeded generator (or use a Crypto API)
251
- const rng = seedrandom("championship-final-table-seed-12345");
183
+ ##### `deal()`
252
184
 
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
- });
185
+ Deal a new hand.
186
+
187
+ ```typescript
188
+ engine.deal();
259
189
  ```
260
190
 
261
- ### Crash Recovery (Snapshots)
191
+ ##### `act(action)`
262
192
 
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.
193
+ Execute a game action.
264
194
 
265
195
  ```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));
196
+ // Fold
197
+ engine.act({ type: ActionType.FOLD, playerId: "user123" });
271
198
 
272
- // ... Server Crashes or Restarts ...
199
+ // Check
200
+ engine.act({ type: ActionType.CHECK, playerId: "user123" });
273
201
 
274
- // --- 2. RESTORING ---
275
- const savedJson = await db.get("table:101");
276
- const snapshot = JSON.parse(savedJson);
202
+ // Call
203
+ engine.act({ type: ActionType.CALL, playerId: "user123" });
277
204
 
278
- // Create a new engine instance pre-loaded with the exact previous state
279
- const engine = PokerEngine.restore(snapshot);
205
+ // Bet (opening bet)
206
+ engine.act({ type: ActionType.BET, playerId: "user123", amount: 100 });
280
207
 
281
- // Resume play immediately - players won't even notice the restart
282
- console.log(engine.state.actionTo);
283
- ```
208
+ // Raise
209
+ engine.act({ type: ActionType.RAISE, playerId: "user123", amount: 200 });
284
210
 
285
- ### Event Subscription & Middleware
211
+ // Show cards at showdown
212
+ engine.act({ type: ActionType.SHOW, playerId: "user123", cardIndices: [0, 1] });
286
213
 
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.
214
+ // Muck cards at showdown
215
+ engine.act({ type: ActionType.MUCK, playerId: "user123" });
288
216
 
289
- The engine provides a subscription model that receives the `Action`, the `OldState`, and the `NewState`.
217
+ // Activate time bank
218
+ engine.act({ type: ActionType.TIME_BANK, playerId: "user123" });
219
+ ```
290
220
 
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
- }
221
+ ---
298
222
 
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
- }
223
+ #### State Access
304
224
 
305
- // 3. Detect Winners
306
- if (newState.winners && !oldState.winners) {
307
- console.log(`[EVENT] Winners:`, newState.winners);
308
- // Trigger chip gathering animation
309
- }
310
- });
225
+ ##### `state` (getter)
226
+
227
+ Get full unmasked game state.
228
+
229
+ ```typescript
230
+ const state = engine.state;
231
+ console.log(state.street); // "PREFLOP" | "FLOP" | "TURN" | "RIVER" | "SHOWDOWN"
232
+ console.log(state.actionTo); // Seat number of current actor
233
+ console.log(state.board); // Community cards ["As", "Kd", "Qh"]
234
+ console.log(state.pots); // Array of pot objects
235
+ console.log(state.winners); // null or Winner[] after showdown
311
236
  ```
312
237
 
313
- ### Hand History Export
238
+ ##### `view(playerId?, version?)`
314
239
 
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.
240
+ Get player-specific view with opponent cards masked.
316
241
 
317
242
  ```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
- */
243
+ // Player view (sees own cards)
244
+ const aliceView = engine.view("alice");
245
+
246
+ // Spectator view (all hole cards hidden)
247
+ const spectatorView = engine.view();
248
+
249
+ // With version number for sync
250
+ const versioned = engine.view("alice", 42);
335
251
  ```
336
252
 
337
- ### Time Banks & Timeout Logic
253
+ ---
338
254
 
339
- The engine follows a strict separation of concerns:
255
+ #### Validation
340
256
 
341
- - **The Server** manages the clock (Real-time).
342
- - **The Engine** manages the Time Bank (Resource).
257
+ ##### `validate(action)`
343
258
 
344
- The engine does not include a `setTimeout`. Instead, you dispatch explicit actions when the server determines time has expired.
259
+ Check if action is valid without executing.
345
260
 
346
261
  ```typescript
347
- // Scenario: Server timer hits 0.0s for Player P1
262
+ const result = engine.validate({
263
+ type: ActionType.BET,
264
+ playerId: "alice",
265
+ amount: 100,
266
+ });
348
267
 
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!");
268
+ if (result.valid) {
269
+ // Action can be executed
354
270
  } 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.");
271
+ console.log(result.error); // "Cannot bet, there's already a bet to call"
272
+ console.log(result.code); // "CANNOT_BET"
359
273
  }
360
274
  ```
361
275
 
362
- ### Undo & Rollback
276
+ ---
363
277
 
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.
278
+ #### Serialization
365
279
 
366
- ```typescript
367
- // 1. Player misclicks Fold
368
- engine.act({ type: ActionType.FOLD, playerId: "p1" });
280
+ ##### `snapshot` (getter)
369
281
 
370
- // 2. Admin intervenes
371
- engine.undo();
282
+ Get serializable snapshot.
372
283
 
373
- // 3. State is now exactly as it was before the fold
374
- // The actionTo pointer is back on p1
284
+ ```typescript
285
+ const snapshot = engine.snapshot;
286
+ localStorage.setItem("game", JSON.stringify(snapshot));
375
287
  ```
376
288
 
377
- ### Tournament Blind Schedules
289
+ ##### `PokerEngine.restore(snapshot)`
378
290
 
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.
291
+ Restore from snapshot.
380
292
 
381
293
  ```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
- });
294
+ const saved = JSON.parse(localStorage.getItem("game")!);
295
+ const engine = PokerEngine.restore(saved);
296
+ ```
390
297
 
391
- // ... Play some hands ...
298
+ ---
392
299
 
393
- // Advance to Level 2
394
- engine.nextBlindLevel();
395
- console.log(engine.blinds); // { smallBlind: 20, bigBlind: 40 }
396
- ```
300
+ #### Event Handling
397
301
 
398
- ### Scalability & Worker Threads
302
+ ##### `on(callback)`
399
303
 
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**.
304
+ Subscribe to state changes.
401
305
 
402
306
  ```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
- }
307
+ const unsubscribe = engine.on((action, oldState, newState) => {
308
+ console.log(`Action: ${action.type}`);
309
+ console.log(`Street: ${oldState.street} -> ${newState.street}`);
416
310
  });
311
+
312
+ // Later: unsubscribe
313
+ unsubscribe();
417
314
  ```
418
315
 
419
- ## Security & Integrity
316
+ ---
420
317
 
421
- ### View Masking (Anti-Cheat)
318
+ #### Undo
422
319
 
423
- **Critical:** Never send the full `GameState` result from `state` to a client. It contains the Deck and Opponent Hole Cards.
320
+ ##### `undo()`
424
321
 
425
- Use the built-in view generator to create a sanitized version for specific players.
322
+ Undo last action.
426
323
 
427
324
  ```typescript
428
- // Server Code
429
- const globalState = engine.state;
325
+ const success = engine.undo();
326
+ if (success) {
327
+ console.log("Action undone");
328
+ }
329
+ ```
330
+
331
+ ---
332
+
333
+ #### Tournament
430
334
 
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);
335
+ ##### `nextBlindLevel()`
437
336
 
438
- // Send to Bob (Seat 2)
439
- const bobView = engine.view("p2");
440
- socket.to("p2").emit("gameState", bobView);
337
+ Advance to next blind level.
338
+
339
+ ```typescript
340
+ engine.nextBlindLevel();
341
+ console.log(engine.state.blindLevel); // 1
342
+ console.log(engine.state.bigBlind); // Updated
441
343
  ```
442
344
 
443
- #### Granular Card Visibility (New Feature)
345
+ ---
444
346
 
445
- At showdown, players can control which cards are revealed using the `shownCards` field:
347
+ #### Hand History
348
+
349
+ ##### `history(options?)`
350
+
351
+ Export hand history in various formats.
446
352
 
447
353
  ```typescript
448
- // Player state after showdown
449
- player.hand = ["As", "Kd"]; // Actual cards (always preserved)
450
- player.shownCards = [0, 1]; // Both cards shown (winner)
354
+ // JSON format (default)
355
+ const json = engine.history();
451
356
 
452
- // Or for selective showing
453
- player.shownCards = [0]; // Only left card shown
454
- player.shownCards = null; // Mucked (no cards shown)
357
+ // PokerStars format
358
+ const ps = engine.history({ format: "pokerstars" });
455
359
 
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)
360
+ // Compact JSON
361
+ const compact = engine.history({ format: "compact" });
461
362
  ```
462
363
 
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).
364
+ ##### `getHandHistory()`
365
+
366
+ Get structured history object.
367
+
368
+ ```typescript
369
+ const history = engine.getHandHistory();
370
+ console.log(history.handId);
371
+ console.log(history.winners);
372
+ console.log(history.streets);
373
+ ```
374
+
375
+ ---
376
+
377
+ #### Optimistic Updates
464
378
 
465
- #### Optional Show Actions
379
+ ##### `optimisticAct(action)`
466
380
 
467
- Players can reveal their cards after showdown:
381
+ Preview action result without modifying state.
468
382
 
469
383
  ```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]
384
+ const preview = engine.optimisticAct({
385
+ type: ActionType.BET,
386
+ playerId: "alice",
387
+ amount: 100,
475
388
  });
476
389
 
477
- // Or show only specific cards
478
- engine.act({
479
- type: ActionType.SHOW,
480
- playerId: "player456",
481
- cardIndices: [0], // Show only left card
482
- });
390
+ // preview contains new state
391
+ // engine.state is unchanged
392
+ ```
483
393
 
484
- // Winner can also explicitly show (already shown by default)
485
- engine.act({
486
- type: ActionType.SHOW,
487
- playerId: "winner789",
488
- });
394
+ ##### `reconcile(serverState)`
395
+
396
+ Merge server state into client engine.
397
+
398
+ ```typescript
399
+ // After receiving state from server
400
+ engine.reconcile(serverState);
489
401
  ```
490
402
 
491
- ### Invariant Auditing
403
+ ---
492
404
 
493
- The engine implements strict accounting logic. After every single action, it runs an internal audit to ensure **Conservation of Chips**.
405
+ ## ๐Ÿ’ฐ Money Handling
494
406
 
495
- $$\sum(\text{PlayerStacks}) + \sum(\text{Pots}) + \sum(\text{CurrentBets}) = \text{InitialChips}$$
407
+ ### Chip Conservation
496
408
 
497
- If a logic bug ever causes a chip to duplicate or vanish, the engine throws a `CriticalStateError`.
409
+ The engine enforces strict chip conservation:
498
410
 
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.
411
+ ```
412
+ โˆ‘(player.stack) + โˆ‘(pot.amount) + โˆ‘(currentBets) + rake = constant
413
+ ```
500
414
 
501
- ### Integer Arithmetic
415
+ Any violation throws `CriticalStateError`.
502
416
 
503
- To ensure financial accuracy, the engine strictly forbids floating-point numbers.
417
+ ### Side Pots
504
418
 
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.
419
+ Automatic side pot calculation for all-in scenarios:
508
420
 
509
- ## Rule Logic & Edge Cases
421
+ ```typescript
422
+ // Example: 3 players with different stacks
423
+ // Alice: 100 (all-in)
424
+ // Bob: 300 (all-in)
425
+ // Charlie: 500 (active)
426
+
427
+ // Results in:
428
+ // Main Pot: 300 (100 ร— 3) - Alice, Bob, Charlie eligible
429
+ // Side Pot: 400 (200 ร— 2) - Bob, Charlie eligible
430
+ // Uncalled: 200 - returned to Charlie
431
+ ```
510
432
 
511
- This engine isn't just a loop; it is a strict implementation of standard TDA (Tournament Directors Association) rules.
433
+ ### Rake
512
434
 
513
- ### 1. Heads-Up Positioning
435
+ ```typescript
436
+ const engine = new PokerEngine({
437
+ smallBlind: 1,
438
+ bigBlind: 2,
439
+ rakePercent: 5, // 5% rake
440
+ rakeCap: 10, // Max $10 per pot
441
+ noFlopNoDrop: true, // No rake if ends preflop
442
+ });
443
+ ```
514
444
 
515
- In a standard game (3+ players), the Small Blind is to the left of the Button.
445
+ ---
516
446
 
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.
447
+ ## ๐Ÿ† Tournament Mode
520
448
 
521
- ### 2. The "Incomplete Raise"
449
+ ```typescript
450
+ const tournament = new PokerEngine({
451
+ smallBlind: 25,
452
+ bigBlind: 50,
453
+ ante: 5,
454
+ maxPlayers: 9,
455
+ initialStack: 10000,
456
+ blindStructure: [
457
+ { smallBlind: 25, bigBlind: 50, ante: 5 },
458
+ { smallBlind: 50, bigBlind: 100, ante: 10 },
459
+ { smallBlind: 75, bigBlind: 150, ante: 15 },
460
+ { smallBlind: 100, bigBlind: 200, ante: 25 },
461
+ { smallBlind: 150, bigBlind: 300, ante: 50 },
462
+ ],
463
+ });
464
+
465
+ // Advance blinds (e.g., on timer)
466
+ tournament.nextBlindLevel();
467
+ ```
522
468
 
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.
469
+ **Tournament-specific rules:**
526
470
 
527
- ### 3. Split Pot "Odd Chip" Resolution
471
+ - Sitting-out players must post blinds/antes
472
+ - Dead button rule for empty seats
473
+ - No rake
528
474
 
529
- In split pots (e.g., High-Low or Tie), chip counts often result in decimals (e.g., 25 chips / 2 players = 12.5).
475
+ ---
530
476
 
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.
477
+ ## ๐ŸŒ Browser Usage
533
478
 
534
- ### 4. Side Pots (Iterative Subtraction)
479
+ ```typescript
480
+ import { createBrowserEngine } from "@pokertools/engine/browser";
535
481
 
536
- The engine handles complex multi-way all-ins.
482
+ // Uses Web Crypto API for secure RNG
483
+ const engine = createBrowserEngine({
484
+ smallBlind: 1,
485
+ bigBlind: 2,
486
+ });
487
+ ```
537
488
 
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.
489
+ ---
543
490
 
544
- ## API Reference
491
+ ## โŒ Error Handling
545
492
 
546
- ### `PokerEngine` Class
493
+ ### Error Types
547
494
 
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. |
495
+ | Error | Description |
496
+ | -------------------- | ----------------------------------------------- |
497
+ | `IllegalActionError` | Invalid game action (send to client) |
498
+ | `CriticalStateError` | Engine invariant violated (should never happen) |
499
+ | `ConfigError` | Invalid configuration |
562
500
 
563
- ### `Action` Types
501
+ ### Error Codes
564
502
 
565
503
  ```typescript
566
- interface Action {
567
- type: ActionType;
568
- playerId: string;
569
- amount?: number; // Required for BET and RAISE
504
+ import { ErrorCodes } from "@pokertools/engine";
505
+
506
+ try {
507
+ engine.act({ type: ActionType.CHECK, playerId: "alice" });
508
+ } catch (err) {
509
+ if (err instanceof IllegalActionError) {
510
+ switch (err.code) {
511
+ case ErrorCodes.NOT_YOUR_TURN:
512
+ showMessage("Wait for your turn");
513
+ break;
514
+ case ErrorCodes.CANNOT_CHECK:
515
+ showMessage("You must call or fold");
516
+ break;
517
+ case ErrorCodes.BET_TOO_SMALL:
518
+ showMessage(`Minimum bet is ${err.context.minBet}`);
519
+ break;
520
+ }
521
+ }
570
522
  }
523
+ ```
524
+
525
+ **Available Error Codes:**
526
+
527
+ | Category | Codes |
528
+ | -------- | --------------------------------------------------------------------- |
529
+ | Player | `PLAYER_NOT_FOUND`, `NOT_YOUR_TURN`, `NOT_SEATED`, `NO_CHIPS` |
530
+ | Betting | `CANNOT_CHECK`, `NOTHING_TO_CALL`, `BET_TOO_SMALL`, `RAISE_TOO_SMALL` |
531
+ | Deal | `CANNOT_DEAL`, `NOT_ENOUGH_PLAYERS` |
532
+ | Seat | `INVALID_SEAT`, `SEAT_OCCUPIED`, `INVALID_STACK` |
533
+
534
+ ---
535
+
536
+ ## ๐ŸŽด Action Types
537
+
538
+ ```typescript
539
+ import { ActionType } from "@pokertools/engine";
571
540
 
572
541
  enum ActionType {
542
+ // Management
543
+ SIT = "SIT",
544
+ STAND = "STAND",
545
+ ADD_CHIPS = "ADD_CHIPS",
546
+ RESERVE_SEAT = "RESERVE_SEAT",
547
+
548
+ // Dealing
549
+ DEAL = "DEAL",
550
+
551
+ // Betting
573
552
  FOLD = "FOLD",
574
553
  CHECK = "CHECK",
575
554
  CALL = "CALL",
576
- BET = "BET", // Opening a bet
577
- RAISE = "RAISE", // Increasing an existing bet
555
+ BET = "BET",
556
+ RAISE = "RAISE",
557
+
558
+ // Showdown
559
+ SHOW = "SHOW",
560
+ MUCK = "MUCK",
561
+
562
+ // Special
578
563
  TIMEOUT = "TIMEOUT",
579
- TIME_BANK = "TIME_BANK", // Extends turn using time bank
564
+ TIME_BANK = "TIME_BANK",
565
+
566
+ // Tournament
567
+ NEXT_BLIND_LEVEL = "NEXT_BLIND_LEVEL",
568
+ }
569
+ ```
570
+
571
+ ---
572
+
573
+ ## ๐Ÿ“œ Hand History Export
574
+
575
+ ### JSON Format
576
+
577
+ ```typescript
578
+ const history = engine.history({ format: "json" });
579
+ ```
580
+
581
+ ```json
582
+ {
583
+ "handId": "hand-1734012345678-123456",
584
+ "timestamp": 1734012345678,
585
+ "tableName": "Table 1",
586
+ "gameType": "Cash",
587
+ "stakes": { "smallBlind": 1, "bigBlind": 2, "ante": 0 },
588
+ "buttonSeat": 0,
589
+ "players": [
590
+ { "seat": 0, "name": "Alice", "startingStack": 200, "endingStack": 220 },
591
+ { "seat": 1, "name": "Bob", "startingStack": 200, "endingStack": 180 }
592
+ ],
593
+ "streets": [
594
+ {
595
+ "street": "PREFLOP",
596
+ "board": [],
597
+ "actions": [...]
598
+ }
599
+ ],
600
+ "winners": [
601
+ { "seat": 0, "playerName": "Alice", "amount": 20, "hand": ["As", "Kd"], "handRank": "Two Pair" }
602
+ ],
603
+ "totalPot": 40
580
604
  }
581
605
  ```
582
606
 
583
- ## Error Handling
607
+ ### PokerStars Format
608
+
609
+ ```typescript
610
+ const history = engine.history({ format: "pokerstars" });
611
+ ```
612
+
613
+ ```
614
+ PokerStars Hand #hand-1734012345678: Hold'em No Limit ($1/$2 USD)
615
+ Table 'Table 1' 6-max Seat #1 is the button
616
+ Seat 1: Alice ($200 in chips)
617
+ Seat 2: Bob ($200 in chips)
618
+ Alice: posts small blind $1
619
+ Bob: posts big blind $2
620
+ *** HOLE CARDS ***
621
+ Dealt to Alice [As Kd]
622
+ Alice: calls $1
623
+ Bob: checks
624
+ *** FLOP *** [Qh Jc Ts]
625
+ ...
626
+ ```
627
+
628
+ ---
629
+
630
+ ## ๐Ÿ”ง Utilities
631
+
632
+ ### View Masking
633
+
634
+ ```typescript
635
+ import { createPublicView } from "@pokertools/engine";
636
+
637
+ // Create masked view for specific player
638
+ const aliceView = createPublicView(state, "alice");
639
+
640
+ // Spectator view (all cards hidden)
641
+ const spectatorView = createPublicView(state, null);
642
+ ```
584
643
 
585
- The engine throws typed errors. You should wrap `act` calls in a `try/catch` block.
644
+ ### Chip Auditing
586
645
 
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. |
646
+ ```typescript
647
+ import { calculateTotalChips, auditChipConservation } from "@pokertools/engine";
648
+
649
+ // Get total chips in game
650
+ const total = calculateTotalChips(state);
651
+
652
+ // Verify chip conservation (throws on failure)
653
+ auditChipConservation(state, expectedTotal);
654
+ ```
655
+
656
+ ### Snapshot
657
+
658
+ ```typescript
659
+ import { createSnapshot, restoreFromSnapshot } from "@pokertools/engine";
660
+
661
+ // Serialize
662
+ const snapshot = createSnapshot(state);
663
+ const json = JSON.stringify(snapshot);
664
+
665
+ // Deserialize
666
+ const restored = restoreFromSnapshot(JSON.parse(json));
667
+ ```
668
+
669
+ ---
670
+
671
+ ## ๐Ÿงช Testing
672
+
673
+ The engine includes 320 tests across multiple categories:
674
+
675
+ | Category | Files | Description |
676
+ | -------------- | ----- | ---------------------------- |
677
+ | Unit | 24 | Individual component tests |
678
+ | Integration | 4 | Full game flow tests |
679
+ | Property | 3 | Randomized invariant testing |
680
+ | Bug Regression | 2 | Fixed bug verification |
681
+ | Security | 1 | Anti-cheat/exploit tests |
682
+ | Debug | 4 | Detailed trace tests |
683
+
684
+ ```bash
685
+ npm test -w @pokertools/engine
686
+ ```
687
+
688
+ ---
689
+
690
+ ## ๐Ÿ“Š State Structure
691
+
692
+ ```typescript
693
+ interface GameState {
694
+ // Configuration
695
+ config: TableConfig;
696
+ players: (Player | null)[];
697
+ maxPlayers: number;
698
+
699
+ // Hand State
700
+ handNumber: number;
701
+ buttonSeat: number | null;
702
+ deck: number[]; // Card codes (server only)
703
+ board: string[]; // Community cards
704
+ street: Street;
705
+
706
+ // Betting
707
+ pots: Pot[];
708
+ currentBets: Map<number, number>;
709
+ minRaise: number;
710
+ lastRaiseAmount: number;
711
+ actionTo: number | null;
712
+ lastAggressorSeat: number | null;
713
+
714
+ // Progress
715
+ activePlayers: number[];
716
+ winners: Winner[] | null;
717
+ rakeThisHand: number;
718
+
719
+ // Blinds
720
+ smallBlind: number;
721
+ bigBlind: number;
722
+ ante: number;
723
+ blindLevel: number;
724
+
725
+ // Time Bank
726
+ timeBanks: Map<number, number>;
727
+ timeBankActiveSeat: number | null;
728
+
729
+ // History
730
+ actionHistory: ActionRecord[];
731
+ previousStates: GameState[]; // For undo
732
+
733
+ // Metadata
734
+ timestamp: number;
735
+ handId: string;
736
+ }
737
+ ```
594
738
 
595
- ## Contributing
739
+ ---
596
740
 
597
- This project is part of the `@pokertools` monorepo.
741
+ ## ๐Ÿ”— Related Packages
598
742
 
599
- 1. Clone the repository.
600
- 2. Run `npm install`.
601
- 3. Run `npm test`.
743
+ | Package | Description |
744
+ | ------------------------------------- | ------------------ |
745
+ | [@pokertools/types](../types) | Type definitions |
746
+ | [@pokertools/evaluator](../evaluator) | Hand evaluation |
747
+ | [@pokertools/api](../api) | REST/WebSocket API |
602
748
 
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.
749
+ ---
604
750
 
605
- ## License
751
+ ## ๐Ÿ“„ License
606
752
 
607
- MIT
753
+ MIT ยฉ A.Aurelius