poker-bitvaljs 1.1.2 → 1.5.2
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 +48 -27
- package/bitval-worker.js +66 -71
- package/bitval.js +574 -284
- package/package.json +11 -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,57 @@ 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`. Generate the table with `npm run gen:preflop`.
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
// Browser
|
|
192
|
+
const buf = await (await fetch('./bench/preflop-table.bin')).arrayBuffer();
|
|
193
|
+
bitval.loadPreflopTable(buf);
|
|
194
|
+
|
|
195
|
+
// Node
|
|
196
|
+
const fs = require('fs');
|
|
197
|
+
bitval.loadPreflopTable(fs.readFileSync('bench/preflop-table.bin'));
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Without a table loaded, preflop falls back to Monte Carlo — nothing else changes.
|
|
186
201
|
|
|
187
202
|
## Performance
|
|
188
203
|
|
|
189
|
-
- **
|
|
190
|
-
-
|
|
191
|
-
-
|
|
192
|
-
|
|
193
|
-
-
|
|
194
|
-
|
|
204
|
+
- **No BigInt in the hot path** — cards are four 13-bit suit lanes evaluated with
|
|
205
|
+
plain 32-bit integer ops. ~28–70× faster than the previous BigInt engine.
|
|
206
|
+
- **Flop/turn: exact and fast.** Each unique concrete hand is evaluated once per
|
|
207
|
+
runout; wide range-vs-range is resolved by counting. No ±0.5% approximation.
|
|
208
|
+
- **Preflop: exact table lookup** (with `loadPreflopTable`) instead of Monte
|
|
209
|
+
Carlo — effectively instant.
|
|
210
|
+
- **Monte Carlo** (only preflop with dead cards) uses confidence-interval
|
|
211
|
+
early-stop and Web Workers.
|
|
212
|
+
|
|
213
|
+
Reproduce the benchmarks with `node bench/pre-vs-post.js` and `npm run bench`;
|
|
214
|
+
correctness with `npm test` (and `npm run test:full` for the exhaustive proofs).
|
|
195
215
|
|
|
196
216
|
**Test performance and benchmark online:** [https://darraghgeo.github.io/poker-bitvaljs/](https://darraghgeo.github.io/poker-bitvaljs/)
|
|
197
217
|
|
|
198
218
|
### Performance Tips
|
|
199
219
|
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
|
|
220
|
+
- Call `loadPreflopTable` once at startup for exact, instant preflop equity.
|
|
221
|
+
- Keep `optimize: true` (default) — on the flop/turn it is exact and much faster.
|
|
222
|
+
- Lower `mcTargetPct` (or set `0`) for higher-precision Monte Carlo; raise it for
|
|
223
|
+
faster, looser preflop-with-dead-cards estimates.
|
|
224
|
+
- Adjust `progressInterval` to balance UI responsiveness vs. performance.
|
|
204
225
|
|
|
205
226
|
## Testing Locally
|
|
206
227
|
|
package/bitval-worker.js
CHANGED
|
@@ -36,103 +36,98 @@ function deserializeMatchup(matchupData) {
|
|
|
36
36
|
* Evaluates a single matchup in the worker context
|
|
37
37
|
* Similar to _evaluateMatchup but without progress callbacks
|
|
38
38
|
*/
|
|
39
|
-
async function evaluateMatchupInWorker(heroMask, villainMask, setup,
|
|
39
|
+
async function evaluateMatchupInWorker(heroMask, villainMask, setup, bitval) {
|
|
40
40
|
let win = 0, tie = 0, lose = 0;
|
|
41
41
|
const deadMask = heroMask | villainMask | setup.deadCardsMask;
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
|
|
44
43
|
// Initialize random number generator for Monte Carlo (not needed for exhaustive)
|
|
45
44
|
if (!setup.isExhaustive) {
|
|
46
45
|
bitval.xorShift = new XorShift32();
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
//
|
|
50
|
-
|
|
48
|
+
// BigInt-free (Number) hot loop, mirroring BitVal._evaluateMatchup. Cards are
|
|
49
|
+
// four 13-bit suit rank masks; bitval._eval7 does the evaluation with no BigInt.
|
|
51
50
|
let iterations = setup.iterations;
|
|
51
|
+
const heroSuits = bitval._suitsFromBigMask(heroMask);
|
|
52
|
+
const villainSuits = bitval._suitsFromBigMask(villainMask);
|
|
53
|
+
const boardBaseSuits = bitval._suitsFromBigMask(setup.boardMask);
|
|
54
|
+
const hs0 = heroSuits[0], hs1 = heroSuits[1], hs2 = heroSuits[2], hs3 = heroSuits[3];
|
|
55
|
+
const vs0 = villainSuits[0], vs1 = villainSuits[1], vs2 = villainSuits[2], vs3 = villainSuits[3];
|
|
56
|
+
const bb0 = boardBaseSuits[0], bb1 = boardBaseSuits[1], bb2 = boardBaseSuits[2], bb3 = boardBaseSuits[3];
|
|
57
|
+
|
|
58
|
+
let comboSuits = null; // flat Int32Array, 4 entries per combo
|
|
59
|
+
let pool = null, poolLen = 0; // packed Int32Array of available cards
|
|
60
|
+
const nDeal = setup.numberOfCardsToDeal;
|
|
52
61
|
if (setup.isExhaustive) {
|
|
53
|
-
// Use comboArray from setup if available, otherwise compute it
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
62
|
+
// Use comboArray from setup if available, otherwise compute it.
|
|
63
|
+
let comboArray = (setup.comboArray && setup.comboArray.length > 0)
|
|
64
|
+
? setup.comboArray
|
|
65
|
+
: bitval._getCombinations(bitval._getAvailableCardMasksByLookUp(deadMask), nDeal);
|
|
66
|
+
comboSuits = bitval._buildComboSuits(comboArray);
|
|
67
|
+
iterations = comboSuits.length >> 2;
|
|
68
|
+
} else {
|
|
69
|
+
pool = bitval._availableSuitCards(deadMask);
|
|
70
|
+
poolLen = pool.length;
|
|
63
71
|
}
|
|
64
|
-
|
|
72
|
+
|
|
73
|
+
// Monte Carlo early-stop (mirrors BitVal._evaluateMatchup).
|
|
74
|
+
const mcTarget = setup.isExhaustive ? 0 : (setup.mcTarget > 0 ? setup.mcTarget / 100 : 0);
|
|
75
|
+
const reqIters = iterations;
|
|
76
|
+
let ran = iterations;
|
|
77
|
+
|
|
65
78
|
// Main evaluation loop
|
|
66
79
|
for (let i = 0; i < iterations; i++) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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);
|
|
80
|
+
let d0, d1, d2, d3;
|
|
81
|
+
if (comboSuits) {
|
|
82
|
+
const b = i << 2;
|
|
83
|
+
d0 = bb0 | comboSuits[b]; d1 = bb1 | comboSuits[b + 1]; d2 = bb2 | comboSuits[b + 2]; d3 = bb3 | comboSuits[b + 3];
|
|
77
84
|
} else {
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
d0 = bb0; d1 = bb1; d2 = bb2; d3 = bb3;
|
|
86
|
+
for (let kk = 0; kk < nDeal; kk++) {
|
|
87
|
+
const j = kk + bitval.xorShift.next(poolLen - kk);
|
|
88
|
+
const tmp = pool[kk]; pool[kk] = pool[j]; pool[j] = tmp;
|
|
89
|
+
const p = pool[kk], bit = p & 0xFFFF, suit = p >> 16;
|
|
90
|
+
if (suit === 0) d0 |= bit;
|
|
91
|
+
else if (suit === 1) d1 |= bit;
|
|
92
|
+
else if (suit === 2) d2 |= bit;
|
|
93
|
+
else d3 |= bit;
|
|
94
|
+
}
|
|
80
95
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
tie
|
|
96
|
+
const hE = bitval._eval7(hs0 | d0, hs1 | d1, hs2 | d2, hs3 | d3);
|
|
97
|
+
const vE = bitval._eval7(vs0 | d0, vs1 | d1, vs2 | d2, vs3 | d3);
|
|
98
|
+
if (hE > vE) win++;
|
|
99
|
+
else if (vE > hE) lose++;
|
|
100
|
+
else tie++;
|
|
101
|
+
|
|
102
|
+
if (mcTarget && (i & 4095) === 4095 && i >= 8191) {
|
|
103
|
+
const n = i + 1, p = (win + tie * 0.5) / n;
|
|
104
|
+
if (1.96 * Math.sqrt(p * (1 - p) / n) < mcTarget) { ran = n; break; }
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
|
-
|
|
107
|
+
|
|
108
|
+
if (ran < reqIters && ran > 0) {
|
|
109
|
+
const s = reqIters / ran;
|
|
110
|
+
win = Math.round(win * s); tie = Math.round(tie * s); lose = Math.round(lose * s);
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
return { matchupWin: win, matchupTie: tie, matchupLose: lose };
|
|
93
114
|
}
|
|
94
115
|
|
|
95
|
-
//
|
|
116
|
+
// Reused BitVal instance (holds the lookup tables; cheap to keep around).
|
|
117
|
+
const _bitval = new BitVal();
|
|
118
|
+
|
|
119
|
+
// Worker message handler. Workers handle the Monte Carlo path only; the
|
|
120
|
+
// exhaustive exact path runs on the main thread (see BitVal._compareRangeOptimized).
|
|
96
121
|
self.onmessage = async function(e) {
|
|
97
122
|
try {
|
|
98
123
|
const { matchups, setupData, workerId } = e.data;
|
|
99
|
-
|
|
100
|
-
// Deserialize setup
|
|
101
124
|
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
125
|
const results = [];
|
|
108
|
-
|
|
109
|
-
// Evaluate each matchup in the batch
|
|
126
|
+
|
|
110
127
|
for (const matchupData of matchups) {
|
|
111
128
|
const matchup = deserializeMatchup(matchupData);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const cacheInfo = matchup.heroKey ? {
|
|
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
|
-
});
|
|
129
|
+
const result = await evaluateMatchupInWorker(matchup.heroMask, matchup.villainMask, setup, _bitval);
|
|
130
|
+
results.push({ key: matchup.key, validCount: matchup.validCount, ...result });
|
|
136
131
|
}
|
|
137
132
|
|
|
138
133
|
// Send results back to main thread
|