poker-bitvaljs 1.5.2 → 1.7.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 +45 -5
- package/bench/preflop-table.bin +0 -0
- package/bitval-worker.js +12 -84
- package/bitval.js +739 -370
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ Compares two ranges of hands.
|
|
|
175
175
|
- `optimize` (Boolean): Use the optimized path (default: `true`). On the flop/turn this is **exact** (byte-identical to `optimize: false`), just much faster.
|
|
176
176
|
- `progressCallback` (Function): Optional callback `(current, total, message) => {}` (default: `null`)
|
|
177
177
|
- `progressInterval` (Number): Update progress callback every N matchups (default: `100`)
|
|
178
|
-
- `useWorkers` (Boolean): Use Web Workers
|
|
178
|
+
- `useWorkers` (Boolean): Use Web Workers (default: `true`) for Monte Carlo and for large exact flop/turn queries; small exact queries stay on the main thread.
|
|
179
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.
|
|
180
180
|
|
|
181
181
|
**Returns:**
|
|
@@ -185,20 +185,55 @@ Compares two ranges of hands.
|
|
|
185
185
|
|
|
186
186
|
Loads the precomputed exact preflop equity table and enables the preflop fast
|
|
187
187
|
path in `compareRange` (empty board, no dead cards). Accepts an `ArrayBuffer`
|
|
188
|
-
(browser) or Node `Buffer`.
|
|
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`.
|
|
189
190
|
|
|
190
191
|
```javascript
|
|
191
|
-
// Browser
|
|
192
|
-
const buf = await (await fetch('./bench/preflop-table.bin')).arrayBuffer();
|
|
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();
|
|
193
194
|
bitval.loadPreflopTable(buf);
|
|
194
195
|
|
|
195
196
|
// Node
|
|
196
197
|
const fs = require('fs');
|
|
197
|
-
bitval.loadPreflopTable(fs.readFileSync('bench/preflop-table.bin'));
|
|
198
|
+
bitval.loadPreflopTable(fs.readFileSync(require.resolve('poker-bitvaljs/bench/preflop-table.bin')));
|
|
198
199
|
```
|
|
199
200
|
|
|
200
201
|
Without a table loaded, preflop falls back to Monte Carlo — nothing else changes.
|
|
201
202
|
|
|
203
|
+
### Shared-board Monte Carlo (opt-in)
|
|
204
|
+
|
|
205
|
+
Monte Carlo is only used when the preflop table can't be (preflop with dead
|
|
206
|
+
cards, or no table loaded). By default each matchup samples its own boards.
|
|
207
|
+
Setting `mcSharedBoards` samples boards once and evaluates every hand on each
|
|
208
|
+
board, counting all valid pairs at once — typically 25–100× faster for
|
|
209
|
+
range-vs-range, with no canonical-grouping approximation:
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
const bitval = new BitVal();
|
|
213
|
+
bitval.mcSharedBoards = true;
|
|
214
|
+
const r = await bitval.compareRange(heroRange, villainRange, [], ['2c'], 5, 100000);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
In this mode `iterations` is the maximum number of **boards** sampled (shared by
|
|
218
|
+
all matchups), `mcTargetPct` applies to the aggregate equity, and the returned
|
|
219
|
+
`win`/`tie`/`lose` are summed over valid pairs × boards. Equity percentages are
|
|
220
|
+
directly comparable to the default mode; raw counts are not.
|
|
221
|
+
|
|
222
|
+
## Changes in 1.7.0
|
|
223
|
+
|
|
224
|
+
- Opt-in shared-board Monte Carlo (`mcSharedBoards`, above).
|
|
225
|
+
- Large exact flop/turn queries are split across Web Workers.
|
|
226
|
+
- Faster exact flop/turn kernel (radix sort), faster preflop table load and
|
|
227
|
+
lookup (~4.5× for full range vs full range). Results are unchanged.
|
|
228
|
+
|
|
229
|
+
## Changes in 1.6.0
|
|
230
|
+
|
|
231
|
+
- **Flop/turn correctness fix:** exhaustive `compareRange` no longer counts
|
|
232
|
+
runouts that deal a player's own hole card. Flop/turn equities from earlier
|
|
233
|
+
versions were slightly off (typically 0–3 percentage points) and will change.
|
|
234
|
+
- Large range-vs-range flop/turn is ~10–100× faster; `simulate()` is ~45×
|
|
235
|
+
faster; the preflop table ships with the package.
|
|
236
|
+
|
|
202
237
|
## Performance
|
|
203
238
|
|
|
204
239
|
- **No BigInt in the hot path** — cards are four 13-bit suit lanes evaluated with
|
|
@@ -222,6 +257,11 @@ correctness with `npm test` (and `npm run test:full` for the exhaustive proofs).
|
|
|
222
257
|
- Lower `mcTargetPct` (or set `0`) for higher-precision Monte Carlo; raise it for
|
|
223
258
|
faster, looser preflop-with-dead-cards estimates.
|
|
224
259
|
- Adjust `progressInterval` to balance UI responsiveness vs. performance.
|
|
260
|
+
- For preflop with dead cards, set `bitval.mcSharedBoards = true` (see above).
|
|
261
|
+
- Web Workers load `bitval-worker.js` from next to `bitval.js` (resolved from
|
|
262
|
+
the `<script>` tag's URL). If you bundle or relocate the files, set
|
|
263
|
+
`bitval.workerUrl = '/path/to/bitval-worker.js'`. If workers can't start,
|
|
264
|
+
`compareRange` falls back to the main thread.
|
|
225
265
|
|
|
226
266
|
## Testing Locally
|
|
227
267
|
|
|
Binary file
|
package/bitval-worker.js
CHANGED
|
@@ -32,101 +32,29 @@ function deserializeMatchup(matchupData) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Evaluates a single matchup in the worker context
|
|
37
|
-
* Similar to _evaluateMatchup but without progress callbacks
|
|
38
|
-
*/
|
|
39
|
-
async function evaluateMatchupInWorker(heroMask, villainMask, setup, bitval) {
|
|
40
|
-
let win = 0, tie = 0, lose = 0;
|
|
41
|
-
const deadMask = heroMask | villainMask | setup.deadCardsMask;
|
|
42
|
-
|
|
43
|
-
// Initialize random number generator for Monte Carlo (not needed for exhaustive)
|
|
44
|
-
if (!setup.isExhaustive) {
|
|
45
|
-
bitval.xorShift = new XorShift32();
|
|
46
|
-
}
|
|
47
|
-
|
|
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.
|
|
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;
|
|
61
|
-
if (setup.isExhaustive) {
|
|
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;
|
|
71
|
-
}
|
|
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
|
-
|
|
78
|
-
// Main evaluation loop
|
|
79
|
-
for (let i = 0; i < iterations; i++) {
|
|
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];
|
|
84
|
-
} else {
|
|
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
|
-
}
|
|
95
|
-
}
|
|
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; }
|
|
105
|
-
}
|
|
106
|
-
}
|
|
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
|
-
|
|
113
|
-
return { matchupWin: win, matchupTie: tie, matchupLose: lose };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
35
|
// Reused BitVal instance (holds the lookup tables; cheap to keep around).
|
|
117
36
|
const _bitval = new BitVal();
|
|
118
37
|
|
|
119
|
-
// Worker message handler
|
|
120
|
-
//
|
|
38
|
+
// Worker message handler: per-matchup Monte Carlo batches, or exact-kernel
|
|
39
|
+
// batches (type: 'kernel').
|
|
121
40
|
self.onmessage = async function(e) {
|
|
122
41
|
try {
|
|
42
|
+
// Exact kernel over a batch of runouts / sampled boards
|
|
43
|
+
// (BitVal._runKernelBatches): exact flop/turn and shared-board Monte Carlo.
|
|
44
|
+
if (e.data.type === 'kernel') {
|
|
45
|
+
const { work, boards } = e.data;
|
|
46
|
+
self.postMessage({ success: true, result: _bitval._runExhaustive(work, boards, 0, boards.length >> 2) });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
123
50
|
const { matchups, setupData, workerId } = e.data;
|
|
124
51
|
const setup = deserializeSetup(setupData);
|
|
125
52
|
const results = [];
|
|
126
53
|
|
|
127
54
|
for (const matchupData of matchups) {
|
|
128
55
|
const matchup = deserializeMatchup(matchupData);
|
|
129
|
-
|
|
56
|
+
// Same synchronous loop as the main thread (BitVal._evaluateMatchupSync).
|
|
57
|
+
const result = _bitval._evaluateMatchupSync(matchup.heroMask, matchup.villainMask, setup);
|
|
130
58
|
results.push({ key: matchup.key, validCount: matchup.validCount, ...result });
|
|
131
59
|
}
|
|
132
60
|
|