@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.
- package/README.md +591 -445
- package/dist/.tsbuildinfo +1 -0
- package/dist/actions/betting.js +7 -2
- package/dist/actions/dealing.js +46 -20
- package/dist/actions/management.js +26 -5
- package/dist/actions/special.d.ts +18 -0
- package/dist/actions/special.js +20 -0
- package/dist/browser.d.ts +27 -0
- package/dist/browser.js +73 -0
- package/dist/engine/PokerEngine.d.ts +23 -2
- package/dist/engine/PokerEngine.js +54 -2
- package/dist/errors/ErrorCodes.d.ts +4 -35
- package/dist/errors/ErrorCodes.js +7 -41
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +1 -1
- package/dist/history/exporter.d.ts +1 -2
- package/dist/history/formats/json.d.ts +1 -1
- package/dist/history/formats/pokerstars.d.ts +1 -1
- package/dist/history/handHistoryBuilder.d.ts +1 -2
- package/dist/history/handHistoryBuilder.js +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/rules/actionOrder.js +4 -4
- package/dist/rules/blinds.d.ts +2 -0
- package/dist/rules/blinds.js +27 -3
- package/dist/rules/headsUp.js +18 -0
- package/dist/rules/showdown.js +10 -0
- package/dist/utils/cardUtils.d.ts +2 -1
- package/dist/utils/cardUtils.js +2 -1
- package/dist/utils/invariants.js +4 -0
- package/dist/utils/positioning.js +2 -2
- package/dist/utils/serialization.d.ts +1 -0
- package/dist/utils/serialization.js +2 -0
- package/dist/utils/viewMasking.d.ts +2 -1
- package/dist/utils/viewMasking.js +9 -1
- package/package.json +31 -5
- package/dist/history/types.d.ts +0 -73
- package/dist/history/types.js +0 -5
- 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
|
[](https://www.npmjs.com/package/@pokertools/engine)
|
|
4
|
-
[](#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
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[]()
|
|
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
|
-
|
|
43
|
+
```bash
|
|
69
44
|
yarn add @pokertools/engine
|
|
45
|
+
```
|
|
70
46
|
|
|
71
|
-
|
|
47
|
+
```bash
|
|
72
48
|
pnpm add @pokertools/engine
|
|
73
49
|
```
|
|
74
50
|
|
|
75
|
-
|
|
51
|
+
---
|
|
76
52
|
|
|
77
|
-
|
|
53
|
+
## ๐ Quick Start
|
|
78
54
|
|
|
79
55
|
```typescript
|
|
80
56
|
import { PokerEngine, ActionType } from "@pokertools/engine";
|
|
81
57
|
|
|
82
|
-
//
|
|
83
|
-
const engine = new PokerEngine({
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
//
|
|
92
|
-
engine.sit(0, "
|
|
93
|
-
engine.sit(1, "
|
|
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
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
// Global state (Admin/Server only - reveals all cards)
|
|
117
|
-
const globalState = engine.state;
|
|
84
|
+
---
|
|
118
85
|
|
|
119
|
-
|
|
120
|
-
const bobView = engine.view("p2");
|
|
86
|
+
## ๐๏ธ Architecture
|
|
121
87
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
###
|
|
120
|
+
### State Flow
|
|
128
121
|
|
|
129
|
-
|
|
122
|
+
```
|
|
123
|
+
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโ
|
|
124
|
+
โ PREFLOP โ โโโถ โ FLOP โ โโโถ โ TURN โ โโโถ โ RIVER โ โโโถ โ SHOWDOWN โ
|
|
125
|
+
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโ
|
|
126
|
+
โ โ
|
|
127
|
+
โ All but one fold โ
|
|
128
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
129
|
+
(Award pot)
|
|
130
|
+
```
|
|
130
131
|
|
|
131
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```
|
|
134
|
+
## ๐ API Reference
|
|
135
|
+
|
|
136
|
+
### PokerEngine Class
|
|
143
137
|
|
|
144
|
-
####
|
|
138
|
+
#### Constructor
|
|
145
139
|
|
|
146
140
|
```typescript
|
|
147
|
-
|
|
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
|
-
|
|
159
|
-
console.log(tournamentEngine.state.rakeThisHand); // Always 0
|
|
143
|
+
const engine = new PokerEngine(config: TableConfig, timeProvider?: () => number);
|
|
160
144
|
```
|
|
161
145
|
|
|
162
|
-
|
|
146
|
+
**TableConfig Options:**
|
|
163
147
|
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
180
|
+
engine.stand("user123");
|
|
181
|
+
```
|
|
249
182
|
|
|
250
|
-
|
|
251
|
-
const rng = seedrandom("championship-final-table-seed-12345");
|
|
183
|
+
##### `deal()`
|
|
252
184
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
randomProvider: () => rng.quick(),
|
|
258
|
-
});
|
|
185
|
+
Deal a new hand.
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
engine.deal();
|
|
259
189
|
```
|
|
260
190
|
|
|
261
|
-
|
|
191
|
+
##### `act(action)`
|
|
262
192
|
|
|
263
|
-
|
|
193
|
+
Execute a game action.
|
|
264
194
|
|
|
265
195
|
```typescript
|
|
266
|
-
//
|
|
267
|
-
|
|
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
|
-
//
|
|
199
|
+
// Check
|
|
200
|
+
engine.act({ type: ActionType.CHECK, playerId: "user123" });
|
|
273
201
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
const snapshot = JSON.parse(savedJson);
|
|
202
|
+
// Call
|
|
203
|
+
engine.act({ type: ActionType.CALL, playerId: "user123" });
|
|
277
204
|
|
|
278
|
-
//
|
|
279
|
-
|
|
205
|
+
// Bet (opening bet)
|
|
206
|
+
engine.act({ type: ActionType.BET, playerId: "user123", amount: 100 });
|
|
280
207
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
```
|
|
208
|
+
// Raise
|
|
209
|
+
engine.act({ type: ActionType.RAISE, playerId: "user123", amount: 200 });
|
|
284
210
|
|
|
285
|
-
|
|
211
|
+
// Show cards at showdown
|
|
212
|
+
engine.act({ type: ActionType.SHOW, playerId: "user123", cardIndices: [0, 1] });
|
|
286
213
|
|
|
287
|
-
|
|
214
|
+
// Muck cards at showdown
|
|
215
|
+
engine.act({ type: ActionType.MUCK, playerId: "user123" });
|
|
288
216
|
|
|
289
|
-
|
|
217
|
+
// Activate time bank
|
|
218
|
+
engine.act({ type: ActionType.TIME_BANK, playerId: "user123" });
|
|
219
|
+
```
|
|
290
220
|
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
238
|
+
##### `view(playerId?, version?)`
|
|
314
239
|
|
|
315
|
-
|
|
240
|
+
Get player-specific view with opponent cards masked.
|
|
316
241
|
|
|
317
242
|
```typescript
|
|
318
|
-
//
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
253
|
+
---
|
|
338
254
|
|
|
339
|
-
|
|
255
|
+
#### Validation
|
|
340
256
|
|
|
341
|
-
|
|
342
|
-
- **The Engine** manages the Time Bank (Resource).
|
|
257
|
+
##### `validate(action)`
|
|
343
258
|
|
|
344
|
-
|
|
259
|
+
Check if action is valid without executing.
|
|
345
260
|
|
|
346
261
|
```typescript
|
|
347
|
-
|
|
262
|
+
const result = engine.validate({
|
|
263
|
+
type: ActionType.BET,
|
|
264
|
+
playerId: "alice",
|
|
265
|
+
amount: 100,
|
|
266
|
+
});
|
|
348
267
|
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
//
|
|
356
|
-
//
|
|
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
|
-
|
|
276
|
+
---
|
|
363
277
|
|
|
364
|
-
|
|
278
|
+
#### Serialization
|
|
365
279
|
|
|
366
|
-
|
|
367
|
-
// 1. Player misclicks Fold
|
|
368
|
-
engine.act({ type: ActionType.FOLD, playerId: "p1" });
|
|
280
|
+
##### `snapshot` (getter)
|
|
369
281
|
|
|
370
|
-
|
|
371
|
-
engine.undo();
|
|
282
|
+
Get serializable snapshot.
|
|
372
283
|
|
|
373
|
-
|
|
374
|
-
|
|
284
|
+
```typescript
|
|
285
|
+
const snapshot = engine.snapshot;
|
|
286
|
+
localStorage.setItem("game", JSON.stringify(snapshot));
|
|
375
287
|
```
|
|
376
288
|
|
|
377
|
-
|
|
289
|
+
##### `PokerEngine.restore(snapshot)`
|
|
378
290
|
|
|
379
|
-
|
|
291
|
+
Restore from snapshot.
|
|
380
292
|
|
|
381
293
|
```typescript
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
298
|
+
---
|
|
392
299
|
|
|
393
|
-
|
|
394
|
-
engine.nextBlindLevel();
|
|
395
|
-
console.log(engine.blinds); // { smallBlind: 20, bigBlind: 40 }
|
|
396
|
-
```
|
|
300
|
+
#### Event Handling
|
|
397
301
|
|
|
398
|
-
|
|
302
|
+
##### `on(callback)`
|
|
399
303
|
|
|
400
|
-
|
|
304
|
+
Subscribe to state changes.
|
|
401
305
|
|
|
402
306
|
```typescript
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
316
|
+
---
|
|
420
317
|
|
|
421
|
-
|
|
318
|
+
#### Undo
|
|
422
319
|
|
|
423
|
-
|
|
320
|
+
##### `undo()`
|
|
424
321
|
|
|
425
|
-
|
|
322
|
+
Undo last action.
|
|
426
323
|
|
|
427
324
|
```typescript
|
|
428
|
-
|
|
429
|
-
|
|
325
|
+
const success = engine.undo();
|
|
326
|
+
if (success) {
|
|
327
|
+
console.log("Action undone");
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
#### Tournament
|
|
430
334
|
|
|
431
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
345
|
+
---
|
|
444
346
|
|
|
445
|
-
|
|
347
|
+
#### Hand History
|
|
348
|
+
|
|
349
|
+
##### `history(options?)`
|
|
350
|
+
|
|
351
|
+
Export hand history in various formats.
|
|
446
352
|
|
|
447
353
|
```typescript
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
player.shownCards = [0, 1]; // Both cards shown (winner)
|
|
354
|
+
// JSON format (default)
|
|
355
|
+
const json = engine.history();
|
|
451
356
|
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
player.shownCards = null; // Mucked (no cards shown)
|
|
357
|
+
// PokerStars format
|
|
358
|
+
const ps = engine.history({ format: "pokerstars" });
|
|
455
359
|
|
|
456
|
-
//
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
+
##### `optimisticAct(action)`
|
|
466
380
|
|
|
467
|
-
|
|
381
|
+
Preview action result without modifying state.
|
|
468
382
|
|
|
469
383
|
```typescript
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
//
|
|
478
|
-
engine.
|
|
479
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
403
|
+
---
|
|
492
404
|
|
|
493
|
-
|
|
405
|
+
## ๐ฐ Money Handling
|
|
494
406
|
|
|
495
|
-
|
|
407
|
+
### Chip Conservation
|
|
496
408
|
|
|
497
|
-
|
|
409
|
+
The engine enforces strict chip conservation:
|
|
498
410
|
|
|
499
|
-
|
|
411
|
+
```
|
|
412
|
+
โ(player.stack) + โ(pot.amount) + โ(currentBets) + rake = constant
|
|
413
|
+
```
|
|
500
414
|
|
|
501
|
-
|
|
415
|
+
Any violation throws `CriticalStateError`.
|
|
502
416
|
|
|
503
|
-
|
|
417
|
+
### Side Pots
|
|
504
418
|
|
|
505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
433
|
+
### Rake
|
|
512
434
|
|
|
513
|
-
|
|
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
|
-
|
|
445
|
+
---
|
|
516
446
|
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
471
|
+
- Sitting-out players must post blinds/antes
|
|
472
|
+
- Dead button rule for empty seats
|
|
473
|
+
- No rake
|
|
528
474
|
|
|
529
|
-
|
|
475
|
+
---
|
|
530
476
|
|
|
531
|
-
|
|
532
|
-
- **Implementation:** The engine resolves this deterministically using seat indexes relative to the dealer button.
|
|
477
|
+
## ๐ Browser Usage
|
|
533
478
|
|
|
534
|
-
|
|
479
|
+
```typescript
|
|
480
|
+
import { createBrowserEngine } from "@pokertools/engine/browser";
|
|
535
481
|
|
|
536
|
-
|
|
482
|
+
// Uses Web Crypto API for secure RNG
|
|
483
|
+
const engine = createBrowserEngine({
|
|
484
|
+
smallBlind: 1,
|
|
485
|
+
bigBlind: 2,
|
|
486
|
+
});
|
|
487
|
+
```
|
|
537
488
|
|
|
538
|
-
|
|
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
|
-
##
|
|
491
|
+
## โ Error Handling
|
|
545
492
|
|
|
546
|
-
###
|
|
493
|
+
### Error Types
|
|
547
494
|
|
|
548
|
-
|
|
|
549
|
-
|
|
|
550
|
-
| `
|
|
551
|
-
| `
|
|
552
|
-
| `
|
|
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
|
-
###
|
|
501
|
+
### Error Codes
|
|
564
502
|
|
|
565
503
|
```typescript
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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",
|
|
577
|
-
RAISE = "RAISE",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
644
|
+
### Chip Auditing
|
|
586
645
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
739
|
+
---
|
|
596
740
|
|
|
597
|
-
|
|
741
|
+
## ๐ Related Packages
|
|
598
742
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
749
|
+
---
|
|
604
750
|
|
|
605
|
-
## License
|
|
751
|
+
## ๐ License
|
|
606
752
|
|
|
607
|
-
MIT
|
|
753
|
+
MIT ยฉ A.Aurelius
|