poker-bitvaljs 1.6.0 → 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 +28 -1
- package/bitval-worker.js +10 -2
- package/bitval.js +274 -51
- package/package.json +3 -2
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:**
|
|
@@ -200,6 +200,32 @@ bitval.loadPreflopTable(fs.readFileSync(require.resolve('poker-bitvaljs/bench/pr
|
|
|
200
200
|
|
|
201
201
|
Without a table loaded, preflop falls back to Monte Carlo — nothing else changes.
|
|
202
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
|
+
|
|
203
229
|
## Changes in 1.6.0
|
|
204
230
|
|
|
205
231
|
- **Flop/turn correctness fix:** exhaustive `compareRange` no longer counts
|
|
@@ -231,6 +257,7 @@ correctness with `npm test` (and `npm run test:full` for the exhaustive proofs).
|
|
|
231
257
|
- Lower `mcTargetPct` (or set `0`) for higher-precision Monte Carlo; raise it for
|
|
232
258
|
faster, looser preflop-with-dead-cards estimates.
|
|
233
259
|
- Adjust `progressInterval` to balance UI responsiveness vs. performance.
|
|
260
|
+
- For preflop with dead cards, set `bitval.mcSharedBoards = true` (see above).
|
|
234
261
|
- Web Workers load `bitval-worker.js` from next to `bitval.js` (resolved from
|
|
235
262
|
the `<script>` tag's URL). If you bundle or relocate the files, set
|
|
236
263
|
`bitval.workerUrl = '/path/to/bitval-worker.js'`. If workers can't start,
|
package/bitval-worker.js
CHANGED
|
@@ -35,10 +35,18 @@ function deserializeMatchup(matchupData) {
|
|
|
35
35
|
// Reused BitVal instance (holds the lookup tables; cheap to keep around).
|
|
36
36
|
const _bitval = new BitVal();
|
|
37
37
|
|
|
38
|
-
// Worker message handler
|
|
39
|
-
//
|
|
38
|
+
// Worker message handler: per-matchup Monte Carlo batches, or exact-kernel
|
|
39
|
+
// batches (type: 'kernel').
|
|
40
40
|
self.onmessage = async function(e) {
|
|
41
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
|
+
|
|
42
50
|
const { matchups, setupData, workerId } = e.data;
|
|
43
51
|
const setup = deserializeSetup(setupData);
|
|
44
52
|
const results = [];
|
package/bitval.js
CHANGED
|
@@ -29,6 +29,12 @@ class XorShift32 {
|
|
|
29
29
|
class BitVal {
|
|
30
30
|
constructor() {
|
|
31
31
|
this.workerUrl = _BITVAL_WORKER_URL;
|
|
32
|
+
// Opt-in Monte Carlo mode: sample boards once and evaluate every hand on
|
|
33
|
+
// each board (see _compareRangeSharedBoards). Off = per-matchup sampling.
|
|
34
|
+
this.mcSharedBoards = false;
|
|
35
|
+
// Exact flop/turn jobs at least this big (unique hands x runouts) are split
|
|
36
|
+
// across workers when workers are enabled.
|
|
37
|
+
this._EXACT_WORKER_THRESHOLD = 300000;
|
|
32
38
|
|
|
33
39
|
// Suit constants
|
|
34
40
|
this._DIAMOND = 3n;
|
|
@@ -214,7 +220,9 @@ class BitVal {
|
|
|
214
220
|
|
|
215
221
|
// Preflop exact-equity table (loaded on demand via loadPreflopTable). The 24
|
|
216
222
|
// suit permutations canonicalize a matchup for lookup (card index = rank*4+suit).
|
|
217
|
-
|
|
223
|
+
// Lookup is an open-addressing hash in typed arrays (_pfKeys/_pfSlot).
|
|
224
|
+
this._pfSlot = null; this._pfKeys = null; this._pfMask = 0; this._pfShift = 0;
|
|
225
|
+
this._pfWin = null; this._pfTie = null; this._pfTotal = 0;
|
|
218
226
|
this._S4 = (function () { const p = a => a.length <= 1 ? [a] : a.flatMap((x, i) => p(a.slice(0, i).concat(a.slice(i + 1))).map(r => [x, ...r])); return p([0, 1, 2, 3]); })();
|
|
219
227
|
}
|
|
220
228
|
|
|
@@ -337,9 +345,37 @@ class BitVal {
|
|
|
337
345
|
}
|
|
338
346
|
|
|
339
347
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
348
|
+
* All k-card runouts (k = 0..2) from the cards NOT in `exclude` (a {lo, hi}
|
|
349
|
+
* card bitset), as one flat Int32Array with 4 suit rank masks per runout
|
|
350
|
+
* (s, h, d, c) — the layout the exact kernel reads. Built from card indices
|
|
351
|
+
* directly (no BigInt).
|
|
352
|
+
* @private
|
|
353
|
+
*/
|
|
354
|
+
_runoutSuits(exclude, k) {
|
|
355
|
+
const cards = [];
|
|
356
|
+
for (let c = 0; c < 52; c++) if (!(c < 32 ? exclude.lo & (1 << c) : exclude.hi & (1 << (c - 32)))) cards.push(c);
|
|
357
|
+
const n = cards.length;
|
|
358
|
+
if (k === 0) return new Int32Array(4);
|
|
359
|
+
if (k === 1) {
|
|
360
|
+
const out = new Int32Array(n * 4);
|
|
361
|
+
for (let i = 0; i < n; i++) out[i * 4 + (cards[i] & 3)] = 1 << (cards[i] >> 2);
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
const out = new Int32Array((n * (n - 1) / 2) * 4);
|
|
365
|
+
let o = 0;
|
|
366
|
+
for (let i = 0; i < n; i++) {
|
|
367
|
+
const ci = cards[i];
|
|
368
|
+
for (let j = i + 1; j < n; j++, o += 4) {
|
|
369
|
+
const cj = cards[j];
|
|
370
|
+
out[o + (ci & 3)] |= 1 << (ci >> 2);
|
|
371
|
+
out[o + (cj & 3)] |= 1 << (cj >> 2);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Converts BigInt runout masks to the 4-suit-masks-per-runout layout.
|
|
343
379
|
* @private
|
|
344
380
|
*/
|
|
345
381
|
_buildComboSuits(comboArray) {
|
|
@@ -514,7 +550,7 @@ class BitVal {
|
|
|
514
550
|
let win = 0, tie = 0, lose = 0;
|
|
515
551
|
|
|
516
552
|
if (isExhaustive) {
|
|
517
|
-
const combos = this.
|
|
553
|
+
const combos = this._runoutSuits(this._cardsBitset(allDeadCards), numberOfCardsToDeal);
|
|
518
554
|
const n = Math.min(iterations, combos.length >> 2);
|
|
519
555
|
for (let i = 0; i < n; i++) {
|
|
520
556
|
const b = i << 2, d0 = combos[b], d1 = combos[b + 1], d2 = combos[b + 2], d3 = combos[b + 3];
|
|
@@ -573,7 +609,7 @@ class BitVal {
|
|
|
573
609
|
// Preflop exact fast path: if the preflop equity table is loaded and this is
|
|
574
610
|
// a full-board preflop query with no dead cards, look up every matchup's
|
|
575
611
|
// exact equity instead of Monte Carlo. Instant and exact.
|
|
576
|
-
if (this.
|
|
612
|
+
if (this._pfSlot && boardCards.length === 0 && deadCards.length === 0 && numberOfBoardCards === 5) {
|
|
577
613
|
const r = this._compareRangePreflopExact(heroHands, villainHands);
|
|
578
614
|
if (progressCallback) progressCallback(1, 1, 'Complete');
|
|
579
615
|
return r;
|
|
@@ -601,18 +637,34 @@ class BitVal {
|
|
|
601
637
|
if (view.getUint32(0, true) !== 0x50464551) throw new Error('invalid preflop table (bad magic)');
|
|
602
638
|
const count = view.getUint32(4, true);
|
|
603
639
|
this._pfTotal = view.getUint32(8, true);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
640
|
+
const win = new Uint32Array(count), tie = new Uint32Array(count);
|
|
641
|
+
// Open-addressing hash (linear probing, load factor <= 0.5): key -> row.
|
|
642
|
+
let bits = 1; while ((1 << bits) < count * 2) bits++;
|
|
643
|
+
const size = 1 << bits, mask = size - 1, shift = 32 - bits;
|
|
644
|
+
const keys = new Uint32Array(size), slot = new Int32Array(size).fill(-1);
|
|
607
645
|
for (let i = 0; i < count; i++) {
|
|
608
|
-
const o = 12 + i * 12;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
646
|
+
const o = 12 + i * 12, key = view.getUint32(o, true);
|
|
647
|
+
win[i] = view.getUint32(o + 4, true);
|
|
648
|
+
tie[i] = view.getUint32(o + 8, true);
|
|
649
|
+
let h = Math.imul(key, 0x9E3779B1) >>> shift;
|
|
650
|
+
while (slot[h] !== -1) h = (h + 1) & mask;
|
|
651
|
+
keys[h] = key; slot[h] = i;
|
|
652
|
+
}
|
|
653
|
+
this._pfWin = win; this._pfTie = tie;
|
|
654
|
+
this._pfKeys = keys; this._pfSlot = slot; this._pfMask = mask; this._pfShift = shift;
|
|
613
655
|
return count;
|
|
614
656
|
}
|
|
615
657
|
|
|
658
|
+
/** Row index of a canonical preflop key in the loaded table, or -1. @private */
|
|
659
|
+
_pfLookup(key) {
|
|
660
|
+
const keys = this._pfKeys, slot = this._pfSlot, mask = this._pfMask;
|
|
661
|
+
let h = Math.imul(key, 0x9E3779B1) >>> this._pfShift;
|
|
662
|
+
for (let i = slot[h]; i !== -1; h = (h + 1) & mask, i = slot[h]) {
|
|
663
|
+
if (keys[h] === key) return i;
|
|
664
|
+
}
|
|
665
|
+
return -1;
|
|
666
|
+
}
|
|
667
|
+
|
|
616
668
|
/** Card index (0..51) = rankIndex*4 + suitIndex, for a 2-char card string. @private */
|
|
617
669
|
_cardIndex(card) { return this._N_RANK[card[0]] * 4 + this._N_SUIT[card[1]]; }
|
|
618
670
|
|
|
@@ -642,14 +694,39 @@ class BitVal {
|
|
|
642
694
|
const heroC = [], villC = [];
|
|
643
695
|
for (const h of heroHands) heroC.push(this._cardIndex(h.slice(0, 2)), this._cardIndex(h.slice(2, 4)));
|
|
644
696
|
for (const v of villainHands) villC.push(this._cardIndex(v.slice(0, 2)), this._cardIndex(v.slice(2, 4)));
|
|
697
|
+
// The canonical key ((a*52+b)*52+c)*52+d is minimized over the 24 suit
|
|
698
|
+
// permutations; it's lexicographic in (hero part, villain part), so only
|
|
699
|
+
// the permutations that minimize the hero part can win (2 for offsuit or
|
|
700
|
+
// pairs, 6 for suited). Find those once per hero, then minimize the villain
|
|
701
|
+
// part over just them — same key as _preflopCanonKey, ~4-12x less work.
|
|
702
|
+
const S4 = this._S4, pWin = this._pfWin, pTie = this._pfTie;
|
|
703
|
+
const perms = new Int32Array(24 * 4); let np = 0;
|
|
645
704
|
let win = 0, tie = 0, lose = 0;
|
|
646
705
|
for (let hi = 0; hi < heroC.length; hi += 2) {
|
|
647
706
|
const h1 = heroC[hi], h2 = heroC[hi + 1];
|
|
707
|
+
let bestH = Infinity; np = 0;
|
|
708
|
+
for (let pi = 0; pi < 24; pi++) {
|
|
709
|
+
const p = S4[pi];
|
|
710
|
+
const m1 = (h1 & ~3) | p[h1 & 3], m2 = (h2 & ~3) | p[h2 & 3];
|
|
711
|
+
const hk = m1 < m2 ? m1 * 52 + m2 : m2 * 52 + m1;
|
|
712
|
+
if (hk > bestH) continue;
|
|
713
|
+
if (hk < bestH) { bestH = hk; np = 0; }
|
|
714
|
+
perms[np * 4] = p[0]; perms[np * 4 + 1] = p[1]; perms[np * 4 + 2] = p[2]; perms[np * 4 + 3] = p[3]; np++;
|
|
715
|
+
}
|
|
716
|
+
const hBase = bestH * 2704;
|
|
648
717
|
for (let vi = 0; vi < villC.length; vi += 2) {
|
|
649
718
|
const v1 = villC[vi], v2 = villC[vi + 1];
|
|
650
719
|
if (h1 === v1 || h1 === v2 || h2 === v1 || h2 === v2) continue; // conflicting hole cards
|
|
651
|
-
|
|
652
|
-
const
|
|
720
|
+
let bestV = 2704;
|
|
721
|
+
const b1 = v1 & ~3, s1 = v1 & 3, b2 = v2 & ~3, s2 = v2 & 3;
|
|
722
|
+
for (let k = 0; k < np; k++) {
|
|
723
|
+
const q = k * 4, m1 = b1 | perms[q + s1], m2 = b2 | perms[q + s2];
|
|
724
|
+
const vk = m1 < m2 ? m1 * 52 + m2 : m2 * 52 + m1;
|
|
725
|
+
if (vk < bestV) bestV = vk;
|
|
726
|
+
}
|
|
727
|
+
const idx = this._pfLookup(hBase + bestV);
|
|
728
|
+
if (idx < 0) throw new Error('preflop table is missing a matchup (corrupt or incomplete table)');
|
|
729
|
+
const w = pWin[idx], t = pTie[idx];
|
|
653
730
|
win += w; tie += t; lose += total - w - t;
|
|
654
731
|
}
|
|
655
732
|
}
|
|
@@ -982,14 +1059,11 @@ class BitVal {
|
|
|
982
1059
|
// enumerate it exactly instead of Monte-Carloing a lower requested count.
|
|
983
1060
|
// Exhaustive is exact and deterministic; the requested iteration count only
|
|
984
1061
|
// matters for larger (>=3 cards to come, e.g. preflop) scenarios.
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
// Generate combo array only if exhaustive
|
|
990
|
-
const comboArray = isExhaustive
|
|
991
|
-
? this._getCombinations(this._getAvailableCardMasksByLookUp(deadCardsMask), numberOfCardsToDeal)
|
|
1062
|
+
// Runouts as suit masks, built from card indices (no BigInt).
|
|
1063
|
+
const comboSuits = isExhaustive
|
|
1064
|
+
? this._runoutSuits(this._cardsBitset([...boardCards, ...deadCards]), numberOfCardsToDeal)
|
|
992
1065
|
: null;
|
|
1066
|
+
if (isExhaustive) iterations = comboSuits.length >> 2;
|
|
993
1067
|
|
|
994
1068
|
const self = this;
|
|
995
1069
|
return {
|
|
@@ -999,9 +1073,11 @@ class BitVal {
|
|
|
999
1073
|
deadCardsMask,
|
|
1000
1074
|
iterations,
|
|
1001
1075
|
isExhaustive,
|
|
1002
|
-
comboArray,
|
|
1076
|
+
comboArray: null, // BigInt runouts are no longer built; see _comboSuits
|
|
1077
|
+
_comboSuits: comboSuits,
|
|
1003
1078
|
numberOfCardsToDeal,
|
|
1004
1079
|
boardCards,
|
|
1080
|
+
deadCards,
|
|
1005
1081
|
numberOfBoardCards,
|
|
1006
1082
|
_validMatchups: null,
|
|
1007
1083
|
get validMatchups() {
|
|
@@ -1511,12 +1587,14 @@ class BitVal {
|
|
|
1511
1587
|
}
|
|
1512
1588
|
|
|
1513
1589
|
/**
|
|
1514
|
-
* Exact exhaustive kernel over runouts [runStart, runEnd).
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1590
|
+
* Exact exhaustive kernel over runouts [runStart, runEnd). Hands holding a
|
|
1591
|
+
* card dealt in the runout are dropped for that runout; every other unique
|
|
1592
|
+
* concrete hand is evaluated once. Matchups are then resolved by direct
|
|
1593
|
+
* comparison (small fields) or, for large fields, a radix sort of the live
|
|
1594
|
+
* hands by value followed by a linear sweep over equal-value groups that
|
|
1595
|
+
* keeps running villain counts in total and per hole card (to subtract
|
|
1596
|
+
* villains sharing a card with the hero). Byte-identical to unoptimized.
|
|
1597
|
+
* `comboSuits` holds 4 suit rank masks per runout (any number of cards).
|
|
1520
1598
|
* @private
|
|
1521
1599
|
*/
|
|
1522
1600
|
_runExhaustive(work, comboSuits, runStart, runEnd) {
|
|
@@ -1528,17 +1606,17 @@ class BitVal {
|
|
|
1528
1606
|
const cardTot = useCount ? new Int32Array(52) : null;
|
|
1529
1607
|
let win = 0, tie = 0, lose = 0;
|
|
1530
1608
|
|
|
1531
|
-
// Large-field scratch:
|
|
1532
|
-
// per-card villain counts strictly below / equal to the current value.
|
|
1533
|
-
const
|
|
1609
|
+
// Large-field scratch: live hand list, radix-sort buffers and counters,
|
|
1610
|
+
// and per-card villain counts strictly below / equal to the current value.
|
|
1611
|
+
const live = useCount ? new Int32Array(U) : null;
|
|
1612
|
+
const tmp = useCount ? new Int32Array(U) : null;
|
|
1534
1613
|
const order = useCount ? new Int32Array(U) : null;
|
|
1614
|
+
const cnt = useCount ? new Int32Array(3 * 257) : null;
|
|
1535
1615
|
const lessC = useCount ? new Int32Array(52) : null;
|
|
1536
1616
|
const eqC = useCount ? new Int32Array(52) : null;
|
|
1537
1617
|
|
|
1538
1618
|
for (let i = runStart; i < runEnd; i++) {
|
|
1539
1619
|
const b = i << 2;
|
|
1540
|
-
const d0 = bb0 | comboSuits[b], d1 = bb1 | comboSuits[b + 1], d2 = bb2 | comboSuits[b + 2], d3 = bb3 | comboSuits[b + 3];
|
|
1541
|
-
for (let u = 0; u < U; u++) { const q = u << 2; uVals[u] = this._eval7(uSuit[q] | d0, uSuit[q + 1] | d1, uSuit[q + 2] | d2, uSuit[q + 3] | d3); }
|
|
1542
1620
|
|
|
1543
1621
|
// Mark hands holding a runout card; for counting, also remove those
|
|
1544
1622
|
// villains from the field total and the per-card totals.
|
|
@@ -1558,6 +1636,14 @@ class BitVal {
|
|
|
1558
1636
|
}
|
|
1559
1637
|
}
|
|
1560
1638
|
|
|
1639
|
+
// Evaluate the live hands only.
|
|
1640
|
+
const d0 = bb0 | comboSuits[b], d1 = bb1 | comboSuits[b + 1], d2 = bb2 | comboSuits[b + 2], d3 = bb3 | comboSuits[b + 3];
|
|
1641
|
+
for (let u = 0; u < U; u++) {
|
|
1642
|
+
if (dead[u] === i) continue;
|
|
1643
|
+
const q = u << 2;
|
|
1644
|
+
uVals[u] = this._eval7(uSuit[q] | d0, uSuit[q + 1] | d1, uSuit[q + 2] | d2, uSuit[q + 3] | d3);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1561
1647
|
if (!useCount) {
|
|
1562
1648
|
for (let p = 0; p < P; p++) {
|
|
1563
1649
|
const hu = pHa[p], vu = pVa[p];
|
|
@@ -1567,24 +1653,34 @@ class BitVal {
|
|
|
1567
1653
|
continue;
|
|
1568
1654
|
}
|
|
1569
1655
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1656
|
+
// LSD radix sort of the live hands by value (values < 2^24: 3 x 8 bits).
|
|
1657
|
+
let L = 0;
|
|
1658
|
+
cnt.fill(0);
|
|
1659
|
+
for (let u = 0; u < U; u++) {
|
|
1660
|
+
if (dead[u] === i) continue;
|
|
1661
|
+
live[L++] = u;
|
|
1662
|
+
const v = uVals[u];
|
|
1663
|
+
cnt[(v & 255) + 1]++; cnt[257 + ((v >> 8) & 255) + 1]++; cnt[514 + ((v >> 16) & 255) + 1]++;
|
|
1664
|
+
}
|
|
1665
|
+
for (let o = 0; o < 771; o += 257) for (let r = 0; r < 256; r++) cnt[o + r + 1] += cnt[o + r];
|
|
1666
|
+
for (let k = 0; k < L; k++) { const u = live[k]; tmp[cnt[uVals[u] & 255]++] = u; }
|
|
1667
|
+
for (let k = 0; k < L; k++) { const u = tmp[k]; live[cnt[257 + ((uVals[u] >> 8) & 255)]++] = u; }
|
|
1668
|
+
for (let k = 0; k < L; k++) { const u = live[k]; order[cnt[514 + ((uVals[u] >> 16) & 255)]++] = u; }
|
|
1573
1669
|
lessC.fill(0);
|
|
1574
1670
|
|
|
1575
1671
|
let below = 0; // villain entries with value < current group value
|
|
1576
|
-
for (let g = 0; g <
|
|
1672
|
+
for (let g = 0; g < L;) {
|
|
1577
1673
|
const x = uVals[order[g]];
|
|
1578
|
-
let e = g + 1; while (e <
|
|
1674
|
+
let e = g + 1; while (e < L && uVals[order[e]] === x) e++;
|
|
1579
1675
|
// Villain counts at exactly x: total and per card.
|
|
1580
1676
|
let eqTotal = 0;
|
|
1581
|
-
for (let k = g; k < e; k++) { const u = order[k]
|
|
1677
|
+
for (let k = g; k < e; k++) { const u = order[k], m = vMult[u]; if (m) { eqTotal += m; eqC[uA[u]] += m; eqC[uB[u]] += m; } }
|
|
1582
1678
|
const above = nVi - below - eqTotal;
|
|
1583
1679
|
// Heroes at x. A villain holding both hero cards (the identical hand)
|
|
1584
1680
|
// is subtracted on each card, so add it back once (it ties at x).
|
|
1585
1681
|
for (let k = g; k < e; k++) {
|
|
1586
1682
|
const u = order[k], m = hMult[u];
|
|
1587
|
-
if (!m
|
|
1683
|
+
if (!m) continue;
|
|
1588
1684
|
const a = uA[u], c = uB[u];
|
|
1589
1685
|
const w = below - lessC[a] - lessC[c];
|
|
1590
1686
|
const t = eqTotal - eqC[a] - eqC[c] + vMult[u];
|
|
@@ -1592,7 +1688,7 @@ class BitVal {
|
|
|
1592
1688
|
win += w * m; tie += t * m; lose += l * m;
|
|
1593
1689
|
}
|
|
1594
1690
|
// Fold this group into the running "below" counts.
|
|
1595
|
-
for (let k = g; k < e; k++) { const u = order[k]
|
|
1691
|
+
for (let k = g; k < e; k++) { const u = order[k], m = vMult[u]; if (m) { lessC[uA[u]] += m; lessC[uB[u]] += m; eqC[uA[u]] = 0; eqC[uB[u]] = 0; } }
|
|
1596
1692
|
below += eqTotal;
|
|
1597
1693
|
g = e;
|
|
1598
1694
|
}
|
|
@@ -1606,18 +1702,36 @@ class BitVal {
|
|
|
1606
1702
|
* @private
|
|
1607
1703
|
*/
|
|
1608
1704
|
async _compareRangeOptimized(setup, progressCallback = null, progressInterval = 100, useWorkers = true) {
|
|
1609
|
-
// Exhaustive (flop/turn):
|
|
1610
|
-
//
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
// across workers was a net loss here (structured-cloning the pair/conflict
|
|
1614
|
-
// work to every worker dwarfs the compute) — true off-thread parallelism of
|
|
1615
|
-
// this path needs SharedArrayBuffer (COOP/COEP), left as a follow-up. The
|
|
1616
|
-
// reusable _runExhaustive / _buildExhaustiveWork are kept for that.
|
|
1705
|
+
// Exhaustive (flop/turn): the EXACT kernel. Large jobs are split by runout
|
|
1706
|
+
// across the worker pool (the work is a few small typed arrays, cheap to
|
|
1707
|
+
// post); small ones stay on the main thread where they're faster than the
|
|
1708
|
+
// messaging. Totals are integer sums, so both agree byte-for-byte.
|
|
1617
1709
|
if (setup.isExhaustive) {
|
|
1710
|
+
if (useWorkers && typeof Worker !== 'undefined') {
|
|
1711
|
+
const work = this._buildExhaustiveWork(setup);
|
|
1712
|
+
const comboSuits = setup._comboSuits;
|
|
1713
|
+
const nRun = comboSuits.length >> 2;
|
|
1714
|
+
if (work.U * nRun >= this._EXACT_WORKER_THRESHOLD) {
|
|
1715
|
+
const nw = (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) || 4;
|
|
1716
|
+
// Runouts cost about the same, so one chunk per worker (fewer posts).
|
|
1717
|
+
const chunk = Math.max(1, Math.ceil(nRun / nw));
|
|
1718
|
+
let next = 0, done = 0, win = 0, tie = 0, lose = 0;
|
|
1719
|
+
await this._runKernelBatches(work,
|
|
1720
|
+
() => { if (next >= nRun) return null; const a = next; next = Math.min(nRun, next + chunk); return comboSuits.slice(a * 4, next * 4); },
|
|
1721
|
+
(r, n) => { win += r.win; tie += r.tie; lose += r.lose; done += n; if (progressCallback) progressCallback(done, nRun, `Evaluating runouts ${done}/${nRun}`); },
|
|
1722
|
+
true, progressCallback);
|
|
1723
|
+
if (progressCallback) progressCallback(nRun, nRun, 'Complete');
|
|
1724
|
+
return { win, tie, lose };
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1618
1727
|
return await this._compareRangeOptimizedSequential(setup, progressCallback, progressInterval);
|
|
1619
1728
|
}
|
|
1620
1729
|
|
|
1730
|
+
// Opt-in shared-board Monte Carlo.
|
|
1731
|
+
if (this.mcSharedBoards) {
|
|
1732
|
+
return await this._compareRangeSharedBoards(setup, progressCallback, useWorkers);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1621
1735
|
// Group validMatchups by canonical key pair (only matchups that exist).
|
|
1622
1736
|
const matchupsToEvaluate = this._groupMonteCarloMatchups(setup);
|
|
1623
1737
|
const totalMatchups = matchupsToEvaluate.length;
|
|
@@ -1710,6 +1824,115 @@ class BitVal {
|
|
|
1710
1824
|
return { win, tie, lose };
|
|
1711
1825
|
}
|
|
1712
1826
|
|
|
1827
|
+
/**
|
|
1828
|
+
* Runs the exact kernel (_runExhaustive) over batches of runouts — on the
|
|
1829
|
+
* worker pool when allowed and available, otherwise on the main thread.
|
|
1830
|
+
* `nextBatch()` returns an Int32Array of runouts (4 suit masks each) or null
|
|
1831
|
+
* when there's nothing more to do (it may stop early); `onResult(r, n)` gets
|
|
1832
|
+
* each batch's { win, tie, lose } and its runout count. Batches are handed
|
|
1833
|
+
* out to whichever worker is free; a worker that fails is dropped and its
|
|
1834
|
+
* batch (and, if none are left, the rest) runs on the main thread.
|
|
1835
|
+
* @private
|
|
1836
|
+
*/
|
|
1837
|
+
async _runKernelBatches(work, nextBatch, onResult, useWorkers, progressCallback = null) {
|
|
1838
|
+
const runLocal = (b) => onResult(this._runExhaustive(work, b, 0, b.length >> 2), b.length >> 2);
|
|
1839
|
+
let workers = null;
|
|
1840
|
+
if (useWorkers && typeof Worker !== 'undefined') {
|
|
1841
|
+
const n = (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) || 4;
|
|
1842
|
+
try { workers = this._acquireWorkers(n); } catch (e) { workers = null; }
|
|
1843
|
+
}
|
|
1844
|
+
if (!workers || workers.length === 0) {
|
|
1845
|
+
let b;
|
|
1846
|
+
while ((b = nextBatch())) {
|
|
1847
|
+
runLocal(b);
|
|
1848
|
+
// Keep the page responsive between batches when someone is watching.
|
|
1849
|
+
if (progressCallback) await new Promise(resolve => setTimeout(resolve, 0));
|
|
1850
|
+
}
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
let alive = workers.length;
|
|
1854
|
+
await Promise.all(workers.map((worker) => new Promise((resolve, reject) => {
|
|
1855
|
+
let current = null;
|
|
1856
|
+
const cleanup = () => { worker.onmessage = null; worker.onerror = null; };
|
|
1857
|
+
const dispatch = () => {
|
|
1858
|
+
current = nextBatch();
|
|
1859
|
+
if (!current) { cleanup(); resolve(); return; }
|
|
1860
|
+
worker.postMessage({ type: 'kernel', work, boards: current });
|
|
1861
|
+
};
|
|
1862
|
+
worker.onmessage = (e) => {
|
|
1863
|
+
if (!e.data.success) { cleanup(); reject(new Error(e.data.error || 'Worker error')); return; }
|
|
1864
|
+
onResult(e.data.result, current.length >> 2);
|
|
1865
|
+
dispatch();
|
|
1866
|
+
};
|
|
1867
|
+
worker.onerror = (error) => {
|
|
1868
|
+
if (error && error.preventDefault) error.preventDefault();
|
|
1869
|
+
cleanup();
|
|
1870
|
+
this._discardWorker(worker);
|
|
1871
|
+
try {
|
|
1872
|
+
if (current) runLocal(current);
|
|
1873
|
+
if (--alive === 0) { let b; while ((b = nextBatch())) runLocal(b); }
|
|
1874
|
+
resolve();
|
|
1875
|
+
} catch (err) { reject(err); }
|
|
1876
|
+
};
|
|
1877
|
+
dispatch();
|
|
1878
|
+
})));
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Opt-in Monte Carlo (bitval.mcSharedBoards = true): sample boards once from
|
|
1883
|
+
* the live deck and run the exact kernel over them, so every hand is
|
|
1884
|
+
* evaluated once per board and ALL valid pairs are counted on every board.
|
|
1885
|
+
* Hands that collide with a sampled board sit that board out, which is
|
|
1886
|
+
* exactly conditional sampling per matchup, so the equity is unbiased (and
|
|
1887
|
+
* there's no canonical grouping). `iterations` caps the number of boards;
|
|
1888
|
+
* sampling stops early once the batch-means 95% CI half-width of the
|
|
1889
|
+
* aggregate equity is below `mcTarget` percentage points.
|
|
1890
|
+
* @private
|
|
1891
|
+
*/
|
|
1892
|
+
async _compareRangeSharedBoards(setup, progressCallback = null, useWorkers = true) {
|
|
1893
|
+
const work = this._buildExhaustiveWork(setup);
|
|
1894
|
+
const ex = this._cardsBitset([...setup.boardCards, ...(setup.deadCards || [])]);
|
|
1895
|
+
const deck = [];
|
|
1896
|
+
for (let c = 0; c < 52; c++) if (!(c < 32 ? ex.lo & (1 << c) : ex.hi & (1 << (c - 32)))) deck.push(c);
|
|
1897
|
+
const pool = Int32Array.from(deck), poolLen = pool.length;
|
|
1898
|
+
const k = setup.numberOfCardsToDeal, total = setup.iterations;
|
|
1899
|
+
if (k > poolLen || total <= 0) return { win: 0, tie: 0, lose: 0 };
|
|
1900
|
+
const batchSize = Math.max(64, Math.min(1024, Math.ceil(total / 16)));
|
|
1901
|
+
const target = setup.mcTarget > 0 ? setup.mcTarget / 100 : 0;
|
|
1902
|
+
let st = new XorShift32().state;
|
|
1903
|
+
let issued = 0, done = 0, stop = false, win = 0, tie = 0, lose = 0;
|
|
1904
|
+
let nb = 0, sumEq = 0, sumEq2 = 0;
|
|
1905
|
+
|
|
1906
|
+
const nextBatch = () => {
|
|
1907
|
+
if (stop || issued >= total) return null;
|
|
1908
|
+
const n = Math.min(batchSize, total - issued);
|
|
1909
|
+
issued += n;
|
|
1910
|
+
const out = new Int32Array(n * 4);
|
|
1911
|
+
for (let i = 0; i < n; i++) {
|
|
1912
|
+
for (let j = 0; j < k; j++) { // partial Fisher-Yates over the live deck
|
|
1913
|
+
st ^= st << 13; st ^= st >> 17; st ^= st << 5;
|
|
1914
|
+
const r = j + Math.abs(st) % (poolLen - j);
|
|
1915
|
+
const c = pool[r]; pool[r] = pool[j]; pool[j] = c;
|
|
1916
|
+
out[i * 4 + (c & 3)] |= 1 << (c >> 2);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return out;
|
|
1920
|
+
};
|
|
1921
|
+
const onResult = (r, n) => {
|
|
1922
|
+
win += r.win; tie += r.tie; lose += r.lose; done += n;
|
|
1923
|
+
const t = r.win + r.tie + r.lose;
|
|
1924
|
+
if (t > 0) { const e = (r.win + r.tie / 2) / t; nb++; sumEq += e; sumEq2 += e * e; }
|
|
1925
|
+
if (target && nb >= 8) {
|
|
1926
|
+
const mean = sumEq / nb, v = Math.max(0, (sumEq2 - nb * mean * mean) / (nb - 1));
|
|
1927
|
+
if (1.96 * Math.sqrt(v / nb) < target) stop = true;
|
|
1928
|
+
}
|
|
1929
|
+
if (progressCallback) progressCallback(done, total, `Sampled ${done.toLocaleString()} boards`);
|
|
1930
|
+
};
|
|
1931
|
+
await this._runKernelBatches(work, nextBatch, onResult, useWorkers, progressCallback);
|
|
1932
|
+
if (progressCallback) progressCallback(total, total, 'Complete');
|
|
1933
|
+
return { win, tie, lose };
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1713
1936
|
/**
|
|
1714
1937
|
* Returns `count` persistent workers, lazily growing the pool as needed.
|
|
1715
1938
|
* Reused across compareRange calls so thread-spawn + importScripts parse is
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poker-bitvaljs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "High-performance poker hand evaluator optimized for browser environments, using bitwise operations for rapid poker hand analysis and equity calculations.",
|
|
5
5
|
"main": "bitval.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node bench/correctness.js && node bench/determinism.js && node bench/exact-optimize.js 300 && node bench/eval-equivalence.js 1000000 && node bench/simulate-equivalence.js",
|
|
8
|
-
"test:full": "node bench/eval-equivalence.js --full && node bench/engine-differential.js && node bench/exact-optimize.js 2000",
|
|
8
|
+
"test:full": "node bench/eval-equivalence.js --full && node bench/engine-differential.js && node bench/exact-optimize.js 2000 && node bench/mc-shared-board.js",
|
|
9
9
|
"test:equiv": "node bench/eval-equivalence.js",
|
|
10
10
|
"test:exact": "node bench/exact-optimize.js",
|
|
11
11
|
"test:differential": "node bench/engine-differential.js",
|
|
12
12
|
"test:preflop": "node bench/preflop-validate.js",
|
|
13
13
|
"test:simulate": "node bench/simulate-equivalence.js",
|
|
14
|
+
"test:mc": "node bench/mc-shared-board.js",
|
|
14
15
|
"gen:preflop": "node bench/gen-preflop-table.js --out bench/preflop-table.bin",
|
|
15
16
|
"bench": "node bench/bench.js",
|
|
16
17
|
"bench:correct": "node bench/correctness.js",
|