poker-bitvaljs 1.2.0 → 1.6.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.
- package/README.md +61 -27
- package/bench/preflop-table.bin +0 -0
- package/bitval-worker.js +8 -93
- package/bitval.js +950 -459
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -6,11 +6,15 @@ High-performance poker hand evaluator optimized for browser environments, using
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- **Fast hand evaluation** using
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
-
|
|
13
|
-
- **
|
|
9
|
+
- **Fast hand evaluation** using four 13-bit suit lanes and plain 32-bit integers
|
|
10
|
+
(no BigInt in the hot path) — ~28–70× faster than the previous BigInt engine
|
|
11
|
+
- **Exact range vs range equity** on the flop and turn via evaluate-once with
|
|
12
|
+
counting (byte-identical to brute force, no approximation)
|
|
13
|
+
- **Exact, instant preflop** via an optional precomputed equity table
|
|
14
|
+
(`loadPreflopTable`) — replaces Monte Carlo entirely for preflop
|
|
15
|
+
- **Monte Carlo** with confidence-interval early-stop for the remaining sampled
|
|
16
|
+
case (preflop with dead cards)
|
|
17
|
+
- **Web Workers** for the Monte Carlo path
|
|
14
18
|
- **Progress callbacks** with configurable update intervals
|
|
15
19
|
- **Browser and Node.js compatible**
|
|
16
20
|
|
|
@@ -157,9 +161,9 @@ Simulates a single hand vs hand matchup.
|
|
|
157
161
|
**Returns:**
|
|
158
162
|
- `{ win, tie, lose }`: Results object
|
|
159
163
|
|
|
160
|
-
### `compareRange(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize, progressCallback, progressInterval, useWorkers)`
|
|
164
|
+
### `compareRange(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize, progressCallback, progressInterval, useWorkers, mcTargetPct)`
|
|
161
165
|
|
|
162
|
-
Compares two ranges of hands
|
|
166
|
+
Compares two ranges of hands.
|
|
163
167
|
|
|
164
168
|
**Parameters:**
|
|
165
169
|
- `heroHands` (Array): Array of hero hand strings (e.g., `['AsAh', 'AsAd']`)
|
|
@@ -167,40 +171,70 @@ Compares two ranges of hands with optional optimization and Web Workers parallel
|
|
|
167
171
|
- `boardCards` (Array): Board cards as strings (default: `[]`)
|
|
168
172
|
- `deadCards` (Array): Dead cards as strings (default: `[]`)
|
|
169
173
|
- `numberOfBoardCards` (Number): Total board cards (default: `5`)
|
|
170
|
-
- `iterations` (Number):
|
|
171
|
-
- `optimize` (Boolean): Use
|
|
174
|
+
- `iterations` (Number): Monte Carlo samples per matchup (default: `10000`). Ignored on the flop/turn (exhaustive) and preflop-table paths, which are exact.
|
|
175
|
+
- `optimize` (Boolean): Use the optimized path (default: `true`). On the flop/turn this is **exact** (byte-identical to `optimize: false`), just much faster.
|
|
172
176
|
- `progressCallback` (Function): Optional callback `(current, total, message) => {}` (default: `null`)
|
|
173
177
|
- `progressInterval` (Number): Update progress callback every N matchups (default: `100`)
|
|
174
|
-
- `useWorkers` (Boolean): Use Web Workers for
|
|
178
|
+
- `useWorkers` (Boolean): Use Web Workers for the Monte Carlo path (default: `true`). The exact flop/turn path runs on the main thread.
|
|
179
|
+
- `mcTargetPct` (Number): Monte Carlo early-stop target — stop sampling a matchup once its 95% confidence-interval half-width is within this many equity percentage points (default: `0.3`). Pass `0` to force the full `iterations`. Only affects the Monte Carlo (preflop) path.
|
|
175
180
|
|
|
176
181
|
**Returns:**
|
|
177
182
|
- `Promise<{ win, tie, lose }>`: Results object
|
|
178
183
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
### `loadPreflopTable(buffer)`
|
|
185
|
+
|
|
186
|
+
Loads the precomputed exact preflop equity table and enables the preflop fast
|
|
187
|
+
path in `compareRange` (empty board, no dead cards). Accepts an `ArrayBuffer`
|
|
188
|
+
(browser) or Node `Buffer`. The table (~1 MB) ships with the npm package at
|
|
189
|
+
`poker-bitvaljs/bench/preflop-table.bin`; regenerate it with `npm run gen:preflop`.
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
// Browser (serve the file from wherever you host the package)
|
|
193
|
+
const buf = await (await fetch('./node_modules/poker-bitvaljs/bench/preflop-table.bin')).arrayBuffer();
|
|
194
|
+
bitval.loadPreflopTable(buf);
|
|
195
|
+
|
|
196
|
+
// Node
|
|
197
|
+
const fs = require('fs');
|
|
198
|
+
bitval.loadPreflopTable(fs.readFileSync(require.resolve('poker-bitvaljs/bench/preflop-table.bin')));
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Without a table loaded, preflop falls back to Monte Carlo — nothing else changes.
|
|
202
|
+
|
|
203
|
+
## Changes in 1.6.0
|
|
204
|
+
|
|
205
|
+
- **Flop/turn correctness fix:** exhaustive `compareRange` no longer counts
|
|
206
|
+
runouts that deal a player's own hole card. Flop/turn equities from earlier
|
|
207
|
+
versions were slightly off (typically 0–3 percentage points) and will change.
|
|
208
|
+
- Large range-vs-range flop/turn is ~10–100× faster; `simulate()` is ~45×
|
|
209
|
+
faster; the preflop table ships with the package.
|
|
186
210
|
|
|
187
211
|
## Performance
|
|
188
212
|
|
|
189
|
-
- **
|
|
190
|
-
-
|
|
191
|
-
-
|
|
192
|
-
|
|
193
|
-
-
|
|
194
|
-
|
|
213
|
+
- **No BigInt in the hot path** — cards are four 13-bit suit lanes evaluated with
|
|
214
|
+
plain 32-bit integer ops. ~28–70× faster than the previous BigInt engine.
|
|
215
|
+
- **Flop/turn: exact and fast.** Each unique concrete hand is evaluated once per
|
|
216
|
+
runout; wide range-vs-range is resolved by counting. No ±0.5% approximation.
|
|
217
|
+
- **Preflop: exact table lookup** (with `loadPreflopTable`) instead of Monte
|
|
218
|
+
Carlo — effectively instant.
|
|
219
|
+
- **Monte Carlo** (only preflop with dead cards) uses confidence-interval
|
|
220
|
+
early-stop and Web Workers.
|
|
221
|
+
|
|
222
|
+
Reproduce the benchmarks with `node bench/pre-vs-post.js` and `npm run bench`;
|
|
223
|
+
correctness with `npm test` (and `npm run test:full` for the exhaustive proofs).
|
|
195
224
|
|
|
196
225
|
**Test performance and benchmark online:** [https://darraghgeo.github.io/poker-bitvaljs/](https://darraghgeo.github.io/poker-bitvaljs/)
|
|
197
226
|
|
|
198
227
|
### Performance Tips
|
|
199
228
|
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
|
|
229
|
+
- Call `loadPreflopTable` once at startup for exact, instant preflop equity.
|
|
230
|
+
- Keep `optimize: true` (default) — on the flop/turn it is exact and much faster.
|
|
231
|
+
- Lower `mcTargetPct` (or set `0`) for higher-precision Monte Carlo; raise it for
|
|
232
|
+
faster, looser preflop-with-dead-cards estimates.
|
|
233
|
+
- Adjust `progressInterval` to balance UI responsiveness vs. performance.
|
|
234
|
+
- Web Workers load `bitval-worker.js` from next to `bitval.js` (resolved from
|
|
235
|
+
the `<script>` tag's URL). If you bundle or relocate the files, set
|
|
236
|
+
`bitval.workerUrl = '/path/to/bitval-worker.js'`. If workers can't start,
|
|
237
|
+
`compareRange` falls back to the main thread.
|
|
204
238
|
|
|
205
239
|
## Testing Locally
|
|
206
240
|
|
|
Binary file
|
package/bitval-worker.js
CHANGED
|
@@ -32,107 +32,22 @@ function deserializeMatchup(matchupData) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* Similar to _evaluateMatchup but without progress callbacks
|
|
38
|
-
*/
|
|
39
|
-
async function evaluateMatchupInWorker(heroMask, villainMask, setup, evalCache, cacheInfo, bitval) {
|
|
40
|
-
let win = 0, tie = 0, lose = 0;
|
|
41
|
-
const deadMask = heroMask | villainMask | setup.deadCardsMask;
|
|
42
|
-
const matchupDeadMask = heroMask | villainMask; // Only matchup-level dead cards (excludes setup.deadCardsMask)
|
|
43
|
-
|
|
44
|
-
// Initialize random number generator for Monte Carlo (not needed for exhaustive)
|
|
45
|
-
if (!setup.isExhaustive) {
|
|
46
|
-
bitval.xorShift = new XorShift32();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Prepare for exhaustive enumeration if applicable
|
|
50
|
-
let comboArray = null;
|
|
51
|
-
let iterations = setup.iterations;
|
|
52
|
-
if (setup.isExhaustive) {
|
|
53
|
-
// Use comboArray from setup if available, otherwise compute it
|
|
54
|
-
if (setup.comboArray && setup.comboArray.length > 0) {
|
|
55
|
-
comboArray = setup.comboArray;
|
|
56
|
-
iterations = comboArray.length;
|
|
57
|
-
} else {
|
|
58
|
-
// Compute comboArray if not provided
|
|
59
|
-
const availableMasks = bitval._getAvailableCardMasksByLookUp(deadMask);
|
|
60
|
-
comboArray = bitval._getCombinations(availableMasks, setup.numberOfCardsToDeal);
|
|
61
|
-
iterations = comboArray.length;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Main evaluation loop
|
|
66
|
-
for (let i = 0; i < iterations; i++) {
|
|
67
|
-
// Generate board: use pre-computed combo for exhaustive, or deal randomly
|
|
68
|
-
const board = setup.isExhaustive
|
|
69
|
-
? setup.boardMask | comboArray[i]
|
|
70
|
-
: bitval.deal(setup.boardMask, deadMask, setup.numberOfCardsToDeal) | setup.boardMask;
|
|
71
|
-
|
|
72
|
-
// Evaluate both hands
|
|
73
|
-
let hEval, hKick, vEval, vKick;
|
|
74
|
-
if (cacheInfo && evalCache) {
|
|
75
|
-
[hEval, hKick] = bitval._getCachedEvaluation(cacheInfo.heroKey, cacheInfo.heroHand, board, evalCache, matchupDeadMask);
|
|
76
|
-
[vEval, vKick] = bitval._getCachedEvaluation(cacheInfo.villainKey, cacheInfo.villainHand, board, evalCache, matchupDeadMask);
|
|
77
|
-
} else {
|
|
78
|
-
[hEval, hKick] = bitval.evaluate(heroMask | board);
|
|
79
|
-
[vEval, vKick] = bitval.evaluate(villainMask | board);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Compare and accumulate results
|
|
83
|
-
if (hEval > vEval || (hEval === vEval && hKick > vKick)) {
|
|
84
|
-
win++;
|
|
85
|
-
} else if (vEval > hEval || (vEval === hEval && vKick > hKick)) {
|
|
86
|
-
lose++;
|
|
87
|
-
} else {
|
|
88
|
-
tie++;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { matchupWin: win, matchupTie: tie, matchupLose: lose };
|
|
93
|
-
}
|
|
35
|
+
// Reused BitVal instance (holds the lookup tables; cheap to keep around).
|
|
36
|
+
const _bitval = new BitVal();
|
|
94
37
|
|
|
95
|
-
// Worker message handler
|
|
38
|
+
// Worker message handler. Workers handle the Monte Carlo path only; the
|
|
39
|
+
// exhaustive exact path runs on the main thread (see BitVal._compareRangeOptimized).
|
|
96
40
|
self.onmessage = async function(e) {
|
|
97
41
|
try {
|
|
98
42
|
const { matchups, setupData, workerId } = e.data;
|
|
99
|
-
|
|
100
|
-
// Deserialize setup
|
|
101
43
|
const setup = deserializeSetup(setupData);
|
|
102
|
-
|
|
103
|
-
// Create BitVal instance and cache for this worker (reused for all matchups)
|
|
104
|
-
const bitval = new BitVal();
|
|
105
|
-
const evalCache = new FastestAutoClearingCache(16000000);
|
|
106
|
-
|
|
107
44
|
const results = [];
|
|
108
|
-
|
|
109
|
-
// Evaluate each matchup in the batch
|
|
45
|
+
|
|
110
46
|
for (const matchupData of matchups) {
|
|
111
47
|
const matchup = deserializeMatchup(matchupData);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
heroKey: matchup.heroKey,
|
|
116
|
-
heroHand: matchup.heroHand,
|
|
117
|
-
villainKey: matchup.villainKey,
|
|
118
|
-
villainHand: matchup.villainHand
|
|
119
|
-
} : null;
|
|
120
|
-
|
|
121
|
-
// Evaluate matchup
|
|
122
|
-
const result = await evaluateMatchupInWorker(
|
|
123
|
-
matchup.heroMask,
|
|
124
|
-
matchup.villainMask,
|
|
125
|
-
setup,
|
|
126
|
-
evalCache,
|
|
127
|
-
cacheInfo,
|
|
128
|
-
bitval
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
results.push({
|
|
132
|
-
key: matchup.key,
|
|
133
|
-
validCount: matchup.validCount,
|
|
134
|
-
...result
|
|
135
|
-
});
|
|
48
|
+
// Same synchronous loop as the main thread (BitVal._evaluateMatchupSync).
|
|
49
|
+
const result = _bitval._evaluateMatchupSync(matchup.heroMask, matchup.villainMask, setup);
|
|
50
|
+
results.push({ key: matchup.key, validCount: matchup.validCount, ...result });
|
|
136
51
|
}
|
|
137
52
|
|
|
138
53
|
// Send results back to main thread
|