poker-bitvaljs 1.5.2 → 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 +17 -4
- package/bench/preflop-table.bin +0 -0
- package/bitval-worker.js +2 -82
- package/bitval.js +492 -346
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -185,20 +185,29 @@ 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
|
+
## 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.
|
|
210
|
+
|
|
202
211
|
## Performance
|
|
203
212
|
|
|
204
213
|
- **No BigInt in the hot path** — cards are four 13-bit suit lanes evaluated with
|
|
@@ -222,6 +231,10 @@ correctness with `npm test` (and `npm run test:full` for the exhaustive proofs).
|
|
|
222
231
|
- Lower `mcTargetPct` (or set `0`) for higher-precision Monte Carlo; raise it for
|
|
223
232
|
faster, looser preflop-with-dead-cards estimates.
|
|
224
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.
|
|
225
238
|
|
|
226
239
|
## Testing Locally
|
|
227
240
|
|
|
Binary file
|
package/bitval-worker.js
CHANGED
|
@@ -32,87 +32,6 @@ 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
|
|
|
@@ -126,7 +45,8 @@ self.onmessage = async function(e) {
|
|
|
126
45
|
|
|
127
46
|
for (const matchupData of matchups) {
|
|
128
47
|
const matchup = deserializeMatchup(matchupData);
|
|
129
|
-
|
|
48
|
+
// Same synchronous loop as the main thread (BitVal._evaluateMatchupSync).
|
|
49
|
+
const result = _bitval._evaluateMatchupSync(matchup.heroMask, matchup.villainMask, setup);
|
|
130
50
|
results.push({ key: matchup.key, validCount: matchup.validCount, ...result });
|
|
131
51
|
}
|
|
132
52
|
|
package/bitval.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
// Where bitval-worker.js lives. Resolved against this script's own URL when it
|
|
2
|
+
// is loaded with a <script> tag (so it works from node_modules / a CDN path,
|
|
3
|
+
// not just next to the page); otherwise relative to the page. Override per
|
|
4
|
+
// instance with `bitval.workerUrl = '...'` (e.g. when bundling).
|
|
5
|
+
const _BITVAL_WORKER_URL = (function () {
|
|
6
|
+
try {
|
|
7
|
+
if (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) {
|
|
8
|
+
return new URL('bitval-worker.js', document.currentScript.src).href;
|
|
9
|
+
}
|
|
10
|
+
} catch (e) { /* fall through */ }
|
|
11
|
+
return './bitval-worker.js';
|
|
12
|
+
})();
|
|
13
|
+
|
|
1
14
|
class XorShift32 {
|
|
2
15
|
constructor(seed = null) {
|
|
3
16
|
this.state = seed !== null ? seed : Math.floor(Math.random() * 2147483647) || 1;
|
|
@@ -15,6 +28,8 @@ class XorShift32 {
|
|
|
15
28
|
|
|
16
29
|
class BitVal {
|
|
17
30
|
constructor() {
|
|
31
|
+
this.workerUrl = _BITVAL_WORKER_URL;
|
|
32
|
+
|
|
18
33
|
// Suit constants
|
|
19
34
|
this._DIAMOND = 3n;
|
|
20
35
|
this._HEART = 2n;
|
|
@@ -175,6 +190,27 @@ class BitVal {
|
|
|
175
190
|
if (top < 0 && (m & (1 << 12)) && (m & 0b1111) === 0b1111) top = 3; // wheel
|
|
176
191
|
this._STRAIGHT13[m] = top;
|
|
177
192
|
}
|
|
193
|
+
// Kicker tables (replace the per-eval "peel the highest bit" loops):
|
|
194
|
+
// _TOP5V13[m] = top five ranks of m packed as r1*65536+r2*4096+r3*256+r4*16+r5
|
|
195
|
+
// (flush / high card value; fewer ranks just sum fewer terms).
|
|
196
|
+
// _KEEP2_13[m] / _KEEP3_13[m] = m with only its top 2 / top 3 bits kept, so
|
|
197
|
+
// _TOP5V13[_KEEP3_13[m]] >> 4 = k1*4096+k2*256+k3*16 (pair kickers).
|
|
198
|
+
this._TOP5V13 = new Int32Array(8192);
|
|
199
|
+
this._KEEP2_13 = new Uint16Array(8192);
|
|
200
|
+
this._KEEP3_13 = new Uint16Array(8192);
|
|
201
|
+
for (let m = 0; m < 8192; m++) {
|
|
202
|
+
let v = 0, shift = 65536, n = 0, keep = 0;
|
|
203
|
+
for (let r = 12; r >= 0 && n < 5; r--) {
|
|
204
|
+
if (m & (1 << r)) {
|
|
205
|
+
v += r * shift; shift >>= 4; n++; keep |= 1 << r;
|
|
206
|
+
if (n === 2) this._KEEP2_13[m] = keep;
|
|
207
|
+
if (n === 3) this._KEEP3_13[m] = keep;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (n < 2) this._KEEP2_13[m] = keep;
|
|
211
|
+
if (n < 3) this._KEEP3_13[m] = keep;
|
|
212
|
+
this._TOP5V13[m] = v;
|
|
213
|
+
}
|
|
178
214
|
|
|
179
215
|
// Preflop exact-equity table (loaded on demand via loadPreflopTable). The 24
|
|
180
216
|
// suit permutations canonicalize a matchup for lookup (card index = rank*4+suit).
|
|
@@ -362,11 +398,7 @@ class BitVal {
|
|
|
362
398
|
}
|
|
363
399
|
|
|
364
400
|
// Flush
|
|
365
|
-
if (fm)
|
|
366
|
-
let m = fm, v = 5 * 1048576, shift = 65536;
|
|
367
|
-
for (let n = 0; n < 5; n++) { const r = this._high(m); v += r * shift; m &= ~(1 << r); shift >>= 4; }
|
|
368
|
-
return v;
|
|
369
|
-
}
|
|
401
|
+
if (fm) return 5 * 1048576 + this._TOP5V13[fm];
|
|
370
402
|
|
|
371
403
|
// Straight
|
|
372
404
|
{
|
|
@@ -377,10 +409,7 @@ class BitVal {
|
|
|
377
409
|
// Trips
|
|
378
410
|
if (trips) {
|
|
379
411
|
const t = this._high(trips);
|
|
380
|
-
|
|
381
|
-
const k1 = this._high(rest); rest &= ~(1 << k1);
|
|
382
|
-
const k2 = this._high(rest);
|
|
383
|
-
return 3 * 1048576 + t * 65536 + k1 * 4096 + k2 * 256;
|
|
412
|
+
return 3 * 1048576 + t * 65536 + (this._TOP5V13[this._KEEP2_13[all & ~(1 << t)]] >> 4);
|
|
384
413
|
}
|
|
385
414
|
|
|
386
415
|
// Two pair
|
|
@@ -394,19 +423,11 @@ class BitVal {
|
|
|
394
423
|
// One pair
|
|
395
424
|
if (pairs) {
|
|
396
425
|
const p = this._high(pairs);
|
|
397
|
-
|
|
398
|
-
const k1 = this._high(rest); rest &= ~(1 << k1);
|
|
399
|
-
const k2 = this._high(rest); rest &= ~(1 << k2);
|
|
400
|
-
const k3 = this._high(rest);
|
|
401
|
-
return 1 * 1048576 + p * 65536 + k1 * 4096 + k2 * 256 + k3 * 16;
|
|
426
|
+
return 1 * 1048576 + p * 65536 + (this._TOP5V13[this._KEEP3_13[all & ~(1 << p)]] >> 4);
|
|
402
427
|
}
|
|
403
428
|
|
|
404
429
|
// High card
|
|
405
|
-
|
|
406
|
-
let m = all, v = 0, shift = 65536;
|
|
407
|
-
for (let n = 0; n < 5; n++) { const r = this._high(m); v += r * shift; m &= ~(1 << r); shift >>= 4; }
|
|
408
|
-
return v;
|
|
409
|
-
}
|
|
430
|
+
return this._TOP5V13[all];
|
|
410
431
|
}
|
|
411
432
|
|
|
412
433
|
/**
|
|
@@ -453,15 +474,11 @@ class BitVal {
|
|
|
453
474
|
* @returns {Object} { win, tie, lose, ...handStrengths } - Results object
|
|
454
475
|
*/
|
|
455
476
|
simulate(iterations, numberOfBoardCards = 5, hero = [], villain = [], board = [], deadCards = []) {
|
|
456
|
-
|
|
457
|
-
this.xorShift = new XorShift32();
|
|
477
|
+
const numberOfCardsToDeal = numberOfBoardCards - board.length;
|
|
478
|
+
const xorShift = this.xorShift = new XorShift32();
|
|
458
479
|
|
|
459
480
|
const allDeadCards = [...hero, ...villain, ...board, ...deadCards];
|
|
460
|
-
|
|
461
|
-
numberOfCardsToDeal = numberOfBoardCards - board.length;
|
|
462
|
-
hero = this.getBitMasked(hero);
|
|
463
|
-
villain = this.getBitMasked(villain);
|
|
464
|
-
board = this.getBitMasked(board);
|
|
481
|
+
const deadMask = this.getBitMasked(allDeadCards);
|
|
465
482
|
|
|
466
483
|
const availableCards = 52 - new Set(allDeadCards).size;
|
|
467
484
|
const exhaustiveCombinations = this.combinations(availableCards, numberOfCardsToDeal);
|
|
@@ -473,13 +490,8 @@ class BitVal {
|
|
|
473
490
|
// for larger (>=3 cards to come) scenarios.
|
|
474
491
|
const isExhaustive = numberOfCardsToDeal >= 0 && numberOfCardsToDeal <= 2;
|
|
475
492
|
iterations = isExhaustive ? exhaustiveCombinations : Math.min(iterations, exhaustiveCombinations);
|
|
476
|
-
let comboArray = null;
|
|
477
|
-
if (isExhaustive) {
|
|
478
|
-
const availableMasks = this._getAvailableCardMasksByLookUp(deadCards);
|
|
479
|
-
comboArray = this._getCombinations(availableMasks, numberOfCardsToDeal);
|
|
480
|
-
}
|
|
481
493
|
|
|
482
|
-
let result = {
|
|
494
|
+
let result = {
|
|
483
495
|
"win": 0,
|
|
484
496
|
"tie": 0,
|
|
485
497
|
"lose": 0,
|
|
@@ -494,34 +506,44 @@ class BitVal {
|
|
|
494
506
|
"Straight Flush": 0
|
|
495
507
|
};
|
|
496
508
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
let [villain_eval, villain_tiebreaker] = this.evaluate(villain | _board);
|
|
509
|
+
// BigInt-free hot loop (same engine as compareRange): cards are four 13-bit
|
|
510
|
+
// suit rank masks and _eval7 orders hands identically to evaluate().
|
|
511
|
+
const hs = this._suitsOfCards(hero), vs = this._suitsOfCards(villain), bs = this._suitsOfCards(board);
|
|
512
|
+
const hs0 = hs[0] | bs[0], hs1 = hs[1] | bs[1], hs2 = hs[2] | bs[2], hs3 = hs[3] | bs[3];
|
|
513
|
+
const vs0 = vs[0] | bs[0], vs1 = vs[1] | bs[1], vs2 = vs[2] | bs[2], vs3 = vs[3] | bs[3];
|
|
514
|
+
let win = 0, tie = 0, lose = 0;
|
|
504
515
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (hero_tiebreaker > villain_tiebreaker) {
|
|
514
|
-
result["win"]++;
|
|
515
|
-
continue;
|
|
516
|
+
if (isExhaustive) {
|
|
517
|
+
const combos = this._buildComboSuits(this._getCombinations(this._getAvailableCardMasksByLookUp(deadMask), numberOfCardsToDeal));
|
|
518
|
+
const n = Math.min(iterations, combos.length >> 2);
|
|
519
|
+
for (let i = 0; i < n; i++) {
|
|
520
|
+
const b = i << 2, d0 = combos[b], d1 = combos[b + 1], d2 = combos[b + 2], d3 = combos[b + 3];
|
|
521
|
+
const hE = this._eval7(hs0 | d0, hs1 | d1, hs2 | d2, hs3 | d3);
|
|
522
|
+
const vE = this._eval7(vs0 | d0, vs1 | d1, vs2 | d2, vs3 | d3);
|
|
523
|
+
if (hE > vE) win++; else if (vE > hE) lose++; else tie++;
|
|
516
524
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
525
|
+
} else {
|
|
526
|
+
// Partial Fisher-Yates draw from the packed pool of live cards.
|
|
527
|
+
const pool = this._availableSuitCards(deadMask), poolLen = pool.length;
|
|
528
|
+
const nDeal = poolLen < numberOfCardsToDeal ? 0 : numberOfCardsToDeal;
|
|
529
|
+
let st = xorShift.state;
|
|
530
|
+
for (let i = 0; i < iterations; i++) {
|
|
531
|
+
let d0 = 0, d1 = 0, d2 = 0, d3 = 0;
|
|
532
|
+
for (let k = 0; k < nDeal; k++) {
|
|
533
|
+
st ^= st << 13; st ^= st >> 17; st ^= st << 5;
|
|
534
|
+
const j = k + Math.abs(st) % (poolLen - k);
|
|
535
|
+
const card = pool[j]; pool[j] = pool[k]; pool[k] = card;
|
|
536
|
+
const bit = card & 0xFFFF, suit = card >> 16;
|
|
537
|
+
if (suit === 0) d0 |= bit; else if (suit === 1) d1 |= bit; else if (suit === 2) d2 |= bit; else d3 |= bit;
|
|
538
|
+
}
|
|
539
|
+
const hE = this._eval7(hs0 | d0, hs1 | d1, hs2 | d2, hs3 | d3);
|
|
540
|
+
const vE = this._eval7(vs0 | d0, vs1 | d1, vs2 | d2, vs3 | d3);
|
|
541
|
+
if (hE > vE) win++; else if (vE > hE) lose++; else tie++;
|
|
520
542
|
}
|
|
521
|
-
|
|
522
|
-
result["tie"]++
|
|
543
|
+
xorShift.state = st;
|
|
523
544
|
}
|
|
524
|
-
|
|
545
|
+
|
|
546
|
+
result.win = win; result.tie = tie; result.lose = lose;
|
|
525
547
|
return result;
|
|
526
548
|
}
|
|
527
549
|
|
|
@@ -888,100 +910,103 @@ class BitVal {
|
|
|
888
910
|
// PRIVATE - Range Comparison Helpers
|
|
889
911
|
// ============================================
|
|
890
912
|
|
|
913
|
+
/**
|
|
914
|
+
* Parses a 2-card hand string into card indices (0..51, rank*4+suit) and a
|
|
915
|
+
* split 64-bit card bitset (lo = cards 0..31, hi = cards 32..51) so overlap
|
|
916
|
+
* tests are plain Number ANDs instead of BigInt work.
|
|
917
|
+
* @private
|
|
918
|
+
*/
|
|
919
|
+
_parseHand(hand) {
|
|
920
|
+
const a = this._cardIndex(hand), b = this._cardIndex(hand.substring(2, 4));
|
|
921
|
+
const lo = (a < 32 ? 1 << a : 0) | (b < 32 ? 1 << b : 0);
|
|
922
|
+
const hi = (a >= 32 ? 1 << (a - 32) : 0) | (b >= 32 ? 1 << (b - 32) : 0);
|
|
923
|
+
return { a, b, lo, hi };
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Card bitset (lo/hi halves) for an array of card strings.
|
|
928
|
+
* @private
|
|
929
|
+
*/
|
|
930
|
+
_cardsBitset(cards) {
|
|
931
|
+
let lo = 0, hi = 0;
|
|
932
|
+
for (const c of cards) { const i = this._cardIndex(c); if (i < 32) lo |= 1 << i; else hi |= 1 << (i - 32); }
|
|
933
|
+
return { lo, hi };
|
|
934
|
+
}
|
|
935
|
+
|
|
891
936
|
/**
|
|
892
937
|
* Generates all valid matchups between hero and villain hands, excluding overlaps.
|
|
938
|
+
* Each hand is parsed once; the O(H*V) overlap test runs on Number bitsets.
|
|
893
939
|
* @private
|
|
894
940
|
*/
|
|
895
941
|
_generateValidMatchups(heroHands, villainHands, boardCards = []) {
|
|
896
942
|
const valid = [];
|
|
897
|
-
const
|
|
943
|
+
const board = this._cardsBitset(boardCards);
|
|
944
|
+
const nV = villainHands.length;
|
|
945
|
+
const vLo = new Int32Array(nV), vHi = new Int32Array(nV), vOk = new Uint8Array(nV), vMask = new Array(nV);
|
|
946
|
+
for (let j = 0; j < nV; j++) {
|
|
947
|
+
const p = this._parseHand(villainHands[j]);
|
|
948
|
+
vLo[j] = p.lo; vHi[j] = p.hi;
|
|
949
|
+
vOk[j] = ((p.lo & board.lo) | (p.hi & board.hi)) === 0 ? 1 : 0;
|
|
950
|
+
vMask[j] = vOk[j] ? this.getBitMasked(this._handStringToCards(villainHands[j])) : 0n;
|
|
951
|
+
}
|
|
898
952
|
for (const h of heroHands) {
|
|
953
|
+
const p = this._parseHand(h);
|
|
954
|
+
if ((p.lo & board.lo) | (p.hi & board.hi)) continue;
|
|
899
955
|
const hMask = this.getBitMasked(this._handStringToCards(h));
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if ((vMask & boardMask) !== 0n || (hMask & vMask) !== 0n) continue;
|
|
904
|
-
valid.push({ heroHand: h, villainHand: v, heroMask: hMask, villainMask: vMask });
|
|
956
|
+
for (let j = 0; j < nV; j++) {
|
|
957
|
+
if (!vOk[j] || (p.lo & vLo[j]) | (p.hi & vHi[j])) continue;
|
|
958
|
+
valid.push({ heroHand: h, villainHand: villainHands[j], heroMask: hMask, villainMask: vMask[j] });
|
|
905
959
|
}
|
|
906
960
|
}
|
|
907
961
|
return valid;
|
|
908
962
|
}
|
|
909
963
|
|
|
910
964
|
/**
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const counts = new Map();
|
|
916
|
-
const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
|
|
917
|
-
for (const { heroHand, villainHand } of validMatchups) {
|
|
918
|
-
const hKey = this._getCanonicalKey(heroHand, boardCards, numberOfBoardCards, boardSuitCounts);
|
|
919
|
-
const vKey = this._getCanonicalKey(villainHand, boardCards, numberOfBoardCards, boardSuitCounts);
|
|
920
|
-
const key = `${hKey}:${vKey}`;
|
|
921
|
-
counts.set(key, (counts.get(key) || 0) + 1);
|
|
922
|
-
}
|
|
923
|
-
return counts;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Sets up range comparison by generating valid matchups and canonical mappings.
|
|
965
|
+
* Sets up range comparison. The per-pair matchup list is only materialized
|
|
966
|
+
* (lazily, via the `validMatchups` getter) by the paths that need it — the
|
|
967
|
+
* Monte Carlo and unoptimized paths. The exact exhaustive path works straight
|
|
968
|
+
* from the hand strings (see _buildExhaustiveWork).
|
|
928
969
|
* @private
|
|
929
970
|
*/
|
|
930
971
|
_compareRangeSetup(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize) {
|
|
931
|
-
// Generate valid matchups (always needed)
|
|
932
|
-
const validMatchups = this._generateValidMatchups(heroHands, villainHands, boardCards);
|
|
933
|
-
|
|
934
|
-
// Calculate canonical mappings only if optimization is enabled
|
|
935
|
-
let hCanon = null;
|
|
936
|
-
let vCanon = null;
|
|
937
|
-
let validCounts = null;
|
|
938
|
-
if (optimize) {
|
|
939
|
-
hCanon = this._flattenHandsToCanonical(heroHands, boardCards, numberOfBoardCards);
|
|
940
|
-
vCanon = this._flattenHandsToCanonical(villainHands, boardCards, numberOfBoardCards);
|
|
941
|
-
validCounts = this._calculateValidCounts(validMatchups, hCanon, vCanon, boardCards, numberOfBoardCards);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
972
|
// Calculate bitmasks
|
|
945
973
|
const boardMask = this.getBitMasked(boardCards);
|
|
946
974
|
const deadCardsMask = this.getBitMasked([...boardCards, ...deadCards]);
|
|
947
|
-
|
|
975
|
+
|
|
948
976
|
// Calculate cards to deal and determine if exhaustive enumeration is possible.
|
|
949
977
|
// 0 cards to come (a complete board) is exhaustive too: exactly one runout.
|
|
950
978
|
const numberOfCardsToDeal = numberOfBoardCards - boardCards.length;
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
// Calculate available cards using bitmask (more efficient than Set)
|
|
954
|
-
const availableCards = 52 - this.countBits(deadCardsMask);
|
|
955
|
-
|
|
956
|
-
// Determine if we should use exhaustive enumeration
|
|
957
|
-
const exhaustiveCombinations = canBeExhaustive
|
|
958
|
-
? this.combinations(availableCards, numberOfCardsToDeal)
|
|
959
|
-
: Infinity;
|
|
979
|
+
const isExhaustive = numberOfCardsToDeal >= 0 && numberOfCardsToDeal <= 2;
|
|
980
|
+
|
|
960
981
|
// For <=2 cards to come the full runout space is tiny (<=1326), so always
|
|
961
982
|
// enumerate it exactly instead of Monte-Carloing a lower requested count.
|
|
962
983
|
// Exhaustive is exact and deterministic; the requested iteration count only
|
|
963
984
|
// matters for larger (>=3 cards to come, e.g. preflop) scenarios.
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
985
|
+
if (isExhaustive) {
|
|
986
|
+
iterations = this.combinations(52 - this.countBits(deadCardsMask), numberOfCardsToDeal);
|
|
987
|
+
}
|
|
988
|
+
|
|
967
989
|
// Generate combo array only if exhaustive
|
|
968
|
-
const comboArray = isExhaustive
|
|
969
|
-
? this._getCombinations(this._getAvailableCardMasksByLookUp(deadCardsMask), numberOfCardsToDeal)
|
|
990
|
+
const comboArray = isExhaustive
|
|
991
|
+
? this._getCombinations(this._getAvailableCardMasksByLookUp(deadCardsMask), numberOfCardsToDeal)
|
|
970
992
|
: null;
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
993
|
+
|
|
994
|
+
const self = this;
|
|
995
|
+
return {
|
|
996
|
+
heroHands,
|
|
997
|
+
villainHands,
|
|
998
|
+
boardMask,
|
|
999
|
+
deadCardsMask,
|
|
1000
|
+
iterations,
|
|
1001
|
+
isExhaustive,
|
|
1002
|
+
comboArray,
|
|
1003
|
+
numberOfCardsToDeal,
|
|
1004
|
+
boardCards,
|
|
1005
|
+
numberOfBoardCards,
|
|
1006
|
+
_validMatchups: null,
|
|
1007
|
+
get validMatchups() {
|
|
1008
|
+
return this._validMatchups || (this._validMatchups = self._generateValidMatchups(heroHands, villainHands, boardCards));
|
|
1009
|
+
}
|
|
985
1010
|
};
|
|
986
1011
|
}
|
|
987
1012
|
|
|
@@ -1001,8 +1026,8 @@ class BitVal {
|
|
|
1001
1026
|
isExhaustive: setup.isExhaustive,
|
|
1002
1027
|
numberOfCardsToDeal: setup.numberOfCardsToDeal,
|
|
1003
1028
|
mcTarget: setup.mcTarget
|
|
1004
|
-
// Note:
|
|
1005
|
-
//
|
|
1029
|
+
// Note: validMatchups is not needed in workers; matchups are
|
|
1030
|
+
// pre-serialized and sent separately
|
|
1006
1031
|
};
|
|
1007
1032
|
}
|
|
1008
1033
|
|
|
@@ -1048,7 +1073,76 @@ class BitVal {
|
|
|
1048
1073
|
* Optimized for browser performance with adaptive yielding and inlined hot loop.
|
|
1049
1074
|
* @private
|
|
1050
1075
|
*/
|
|
1076
|
+
/**
|
|
1077
|
+
* Synchronous single-matchup evaluation (no progress / yielding). Used by the
|
|
1078
|
+
* sequential path when there's no progress callback and by the Web Worker,
|
|
1079
|
+
* so both run the same tight loop with the RNG state held in a local.
|
|
1080
|
+
* @private
|
|
1081
|
+
*/
|
|
1082
|
+
_evaluateMatchupSync(heroMask, villainMask, setup) {
|
|
1083
|
+
let win = 0, tie = 0, lose = 0;
|
|
1084
|
+
const hs = this._suitsFromBigMask(heroMask), vs = this._suitsFromBigMask(villainMask);
|
|
1085
|
+
const bb = setup._boardSuits || (setup._boardSuits = this._suitsFromBigMask(setup.boardMask));
|
|
1086
|
+
const hs0 = hs[0], hs1 = hs[1], hs2 = hs[2], hs3 = hs[3];
|
|
1087
|
+
const vs0 = vs[0], vs1 = vs[1], vs2 = vs[2], vs3 = vs[3];
|
|
1088
|
+
const bb0 = bb[0], bb1 = bb[1], bb2 = bb[2], bb3 = bb[3];
|
|
1089
|
+
|
|
1090
|
+
if (setup.isExhaustive) {
|
|
1091
|
+
const comboSuits = setup._comboSuits || (setup._comboSuits = this._buildComboSuits(setup.comboArray));
|
|
1092
|
+
const n = comboSuits.length >> 2;
|
|
1093
|
+
const m0 = hs0 | vs0, m1 = hs1 | vs1, m2 = hs2 | vs2, m3 = hs3 | vs3;
|
|
1094
|
+
for (let i = 0; i < n; i++) {
|
|
1095
|
+
const b = i << 2;
|
|
1096
|
+
const c0 = comboSuits[b], c1 = comboSuits[b + 1], c2 = comboSuits[b + 2], c3 = comboSuits[b + 3];
|
|
1097
|
+
if ((c0 & m0) | (c1 & m1) | (c2 & m2) | (c3 & m3)) continue; // runout uses a hole card
|
|
1098
|
+
const d0 = bb0 | c0, d1 = bb1 | c1, d2 = bb2 | c2, d3 = bb3 | c3;
|
|
1099
|
+
const hE = this._eval7(hs0 | d0, hs1 | d1, hs2 | d2, hs3 | d3);
|
|
1100
|
+
const vE = this._eval7(vs0 | d0, vs1 | d1, vs2 | d2, vs3 | d3);
|
|
1101
|
+
if (hE > vE) win++; else if (vE > hE) lose++; else tie++;
|
|
1102
|
+
}
|
|
1103
|
+
return { matchupWin: win, matchupTie: tie, matchupLose: lose };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const xorShift = this.xorShift = new XorShift32();
|
|
1107
|
+
const pool = this._availableSuitCards(heroMask | villainMask | setup.deadCardsMask), poolLen = pool.length;
|
|
1108
|
+
const nDeal = setup.numberOfCardsToDeal;
|
|
1109
|
+
const iterations = setup.iterations;
|
|
1110
|
+
const mcTarget = setup.mcTarget > 0 ? setup.mcTarget / 100 : 0;
|
|
1111
|
+
let ran = iterations, st = xorShift.state;
|
|
1112
|
+
for (let i = 0; i < iterations; i++) {
|
|
1113
|
+
let d0 = bb0, d1 = bb1, d2 = bb2, d3 = bb3;
|
|
1114
|
+
for (let k = 0; k < nDeal; k++) {
|
|
1115
|
+
st ^= st << 13; st ^= st >> 17; st ^= st << 5;
|
|
1116
|
+
const j = k + Math.abs(st) % (poolLen - k);
|
|
1117
|
+
const card = pool[j]; pool[j] = pool[k]; pool[k] = card;
|
|
1118
|
+
const bit = card & 0xFFFF, suit = card >> 16;
|
|
1119
|
+
if (suit === 0) d0 |= bit; else if (suit === 1) d1 |= bit; else if (suit === 2) d2 |= bit; else d3 |= bit;
|
|
1120
|
+
}
|
|
1121
|
+
const hE = this._eval7(hs0 | d0, hs1 | d1, hs2 | d2, hs3 | d3);
|
|
1122
|
+
const vE = this._eval7(vs0 | d0, vs1 | d1, vs2 | d2, vs3 | d3);
|
|
1123
|
+
if (hE > vE) win++; else if (vE > hE) lose++; else tie++;
|
|
1124
|
+
|
|
1125
|
+
// Monte Carlo early-stop check (every 4096 draws, past a warm-up).
|
|
1126
|
+
if (mcTarget && (i & 4095) === 4095 && i >= 8191) {
|
|
1127
|
+
const n = i + 1, p = (win + tie * 0.5) / n;
|
|
1128
|
+
if (1.96 * Math.sqrt(p * (1 - p) / n) < mcTarget) { ran = n; break; }
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
xorShift.state = st;
|
|
1132
|
+
|
|
1133
|
+
// If early-stopped, scale counts back to the requested iteration count so
|
|
1134
|
+
// every matchup is weighted equally in the aggregate.
|
|
1135
|
+
if (ran < iterations && ran > 0) {
|
|
1136
|
+
const s = iterations / ran;
|
|
1137
|
+
win = Math.round(win * s); tie = Math.round(tie * s); lose = Math.round(lose * s);
|
|
1138
|
+
}
|
|
1139
|
+
return { matchupWin: win, matchupTie: tie, matchupLose: lose };
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1051
1142
|
async _evaluateMatchup(heroMask, villainMask, setup, progressCallback = null, matchupIndex = 0, totalMatchups = 1) {
|
|
1143
|
+
// No progress reporting: run the tight synchronous loop (no per-iteration
|
|
1144
|
+
// yield checks, RNG in a local).
|
|
1145
|
+
if (!progressCallback) return this._evaluateMatchupSync(heroMask, villainMask, setup);
|
|
1052
1146
|
let win = 0, tie = 0, lose = 0;
|
|
1053
1147
|
const deadMask = heroMask | villainMask | setup.deadCardsMask;
|
|
1054
1148
|
|
|
@@ -1097,10 +1191,13 @@ class BitVal {
|
|
|
1097
1191
|
for (let i = 0; i < iterations; i++) {
|
|
1098
1192
|
// Build the runout as suit rank-masks: precomputed combo (exhaustive) or
|
|
1099
1193
|
// a Fisher-Yates draw from the available pool (Monte Carlo).
|
|
1100
|
-
let d0, d1, d2, d3;
|
|
1194
|
+
let d0, d1, d2, d3, live = true;
|
|
1101
1195
|
if (comboSuits) {
|
|
1102
1196
|
const b = i << 2;
|
|
1103
|
-
|
|
1197
|
+
const c0 = comboSuits[b], c1 = comboSuits[b + 1], c2 = comboSuits[b + 2], c3 = comboSuits[b + 3];
|
|
1198
|
+
// Skip runouts that use a hole card (impossible deals).
|
|
1199
|
+
live = ((c0 & (hs0 | vs0)) | (c1 & (hs1 | vs1)) | (c2 & (hs2 | vs2)) | (c3 & (hs3 | vs3))) === 0;
|
|
1200
|
+
d0 = bb0 | c0; d1 = bb1 | c1; d2 = bb2 | c2; d3 = bb3 | c3;
|
|
1104
1201
|
} else {
|
|
1105
1202
|
d0 = bb0; d1 = bb1; d2 = bb2; d3 = bb3;
|
|
1106
1203
|
for (let k = 0; k < nDeal; k++) {
|
|
@@ -1114,11 +1211,13 @@ class BitVal {
|
|
|
1114
1211
|
}
|
|
1115
1212
|
}
|
|
1116
1213
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1214
|
+
if (live) {
|
|
1215
|
+
const hE = this._eval7(hs0 | d0, hs1 | d1, hs2 | d2, hs3 | d3);
|
|
1216
|
+
const vE = this._eval7(vs0 | d0, vs1 | d1, vs2 | d2, vs3 | d3);
|
|
1217
|
+
if (hE > vE) win++;
|
|
1218
|
+
else if (vE > hE) lose++;
|
|
1219
|
+
else tie++;
|
|
1220
|
+
}
|
|
1122
1221
|
|
|
1123
1222
|
// Monte Carlo early-stop check (every 4096 draws, past a warm-up).
|
|
1124
1223
|
if (mcTarget && (i & 4095) === 4095 && i >= 8191) {
|
|
@@ -1228,6 +1327,62 @@ class BitVal {
|
|
|
1228
1327
|
return { win, tie, lose };
|
|
1229
1328
|
}
|
|
1230
1329
|
|
|
1330
|
+
/**
|
|
1331
|
+
* Groups the valid (hero, villain) matchups by canonical key pair for the
|
|
1332
|
+
* Monte Carlo path. Canonical keys depend only on the hand (and board), so
|
|
1333
|
+
* they're computed once per hand string, and pairs are enumerated with Number
|
|
1334
|
+
* card bitsets — no per-pair objects or string keys. Each group keeps the
|
|
1335
|
+
* first matchup (hero-major order) as its representative. Cached on `setup`.
|
|
1336
|
+
* @private
|
|
1337
|
+
*/
|
|
1338
|
+
_groupMonteCarloMatchups(setup) {
|
|
1339
|
+
if (setup._mcGroups) return setup._mcGroups;
|
|
1340
|
+
const { heroHands, villainHands, boardCards, numberOfBoardCards } = setup;
|
|
1341
|
+
const board = this._cardsBitset(boardCards);
|
|
1342
|
+
const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
|
|
1343
|
+
const keyIds = new Map(), keyNames = [];
|
|
1344
|
+
const keyId = (hand) => {
|
|
1345
|
+
const k = this._getCanonicalKey(hand, boardCards, numberOfBoardCards, boardSuitCounts);
|
|
1346
|
+
let id = keyIds.get(k);
|
|
1347
|
+
if (id === undefined) { id = keyNames.length; keyIds.set(k, id); keyNames.push(k); }
|
|
1348
|
+
return id;
|
|
1349
|
+
};
|
|
1350
|
+
const parseSide = (hands) => {
|
|
1351
|
+
const n = hands.length, lo = new Int32Array(n), hi = new Int32Array(n), id = new Int32Array(n), ok = new Uint8Array(n);
|
|
1352
|
+
for (let i = 0; i < n; i++) {
|
|
1353
|
+
const p = this._parseHand(hands[i]);
|
|
1354
|
+
lo[i] = p.lo; hi[i] = p.hi;
|
|
1355
|
+
ok[i] = ((p.lo & board.lo) | (p.hi & board.hi)) === 0 ? 1 : 0;
|
|
1356
|
+
if (ok[i]) id[i] = keyId(hands[i]);
|
|
1357
|
+
}
|
|
1358
|
+
return { lo, hi, id, ok, n };
|
|
1359
|
+
};
|
|
1360
|
+
const H = parseSide(heroHands), V = parseSide(villainHands);
|
|
1361
|
+
const nK = keyNames.length;
|
|
1362
|
+
const groupOf = new Map(), groups = [];
|
|
1363
|
+
for (let h = 0; h < H.n; h++) {
|
|
1364
|
+
if (!H.ok[h]) continue;
|
|
1365
|
+
const hl = H.lo[h], hh = H.hi[h], hk = H.id[h] * nK;
|
|
1366
|
+
for (let v = 0; v < V.n; v++) {
|
|
1367
|
+
if (!V.ok[v] || (hl & V.lo[v]) | (hh & V.hi[v])) continue;
|
|
1368
|
+
const gk = hk + V.id[v];
|
|
1369
|
+
let g = groupOf.get(gk);
|
|
1370
|
+
if (g === undefined) {
|
|
1371
|
+
const heroHand = heroHands[h], villainHand = villainHands[v];
|
|
1372
|
+
const heroKey = keyNames[H.id[h]], villainKey = keyNames[V.id[v]];
|
|
1373
|
+
g = {
|
|
1374
|
+
key: `${heroKey}:${villainKey}`, validCount: 0, heroKey, villainKey, heroHand, villainHand,
|
|
1375
|
+
heroMask: this.getBitMasked(this._handStringToCards(heroHand)),
|
|
1376
|
+
villainMask: this.getBitMasked(this._handStringToCards(villainHand))
|
|
1377
|
+
};
|
|
1378
|
+
groupOf.set(gk, g); groups.push(g);
|
|
1379
|
+
}
|
|
1380
|
+
g.validCount++;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return (setup._mcGroups = groups);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1231
1386
|
/**
|
|
1232
1387
|
* Compares ranges with canonical key caching optimization (sequential version).
|
|
1233
1388
|
* @private
|
|
@@ -1237,29 +1392,16 @@ class BitVal {
|
|
|
1237
1392
|
let lastProgressTime = 0;
|
|
1238
1393
|
const progressUpdateInterval = 100; // Update progress at most every 100ms
|
|
1239
1394
|
|
|
1240
|
-
// Pre-calculate board suit counts once (constant for all matchups)
|
|
1241
|
-
const boardSuitCounts = setup.boardCards.length > 0
|
|
1242
|
-
? this._getBoardSuitCounts(setup.boardCards)
|
|
1243
|
-
: null;
|
|
1244
|
-
|
|
1245
1395
|
// Monte Carlo (>=3 cards to come): canonical grouping + independent
|
|
1246
1396
|
// per-matchup sampling (each matchup deals its own boards, so runouts can't
|
|
1247
1397
|
// be shared). Grouping only pays off here, where a matchup is expensive.
|
|
1248
1398
|
if (!setup.isExhaustive) {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
const mHKey = this._getCanonicalKey(matchup.heroHand, setup.boardCards, setup.numberOfBoardCards, boardSuitCounts);
|
|
1252
|
-
const mVKey = this._getCanonicalKey(matchup.villainHand, setup.boardCards, setup.numberOfBoardCards, boardSuitCounts);
|
|
1253
|
-
const key = `${mHKey}:${mVKey}`;
|
|
1254
|
-
let g = matchupGroups.get(key);
|
|
1255
|
-
if (!g) { g = { heroKey: mHKey, villainKey: mVKey, representative: matchup, count: 0 }; matchupGroups.set(key, g); }
|
|
1256
|
-
g.count++;
|
|
1257
|
-
}
|
|
1258
|
-
const totalMatchups = matchupGroups.size;
|
|
1399
|
+
const groups = this._groupMonteCarloMatchups(setup);
|
|
1400
|
+
const totalMatchups = groups.length;
|
|
1259
1401
|
let matchupIndex = 0;
|
|
1260
|
-
for (const
|
|
1261
|
-
const validCount = group.
|
|
1262
|
-
const validPair = group
|
|
1402
|
+
for (const group of groups) {
|
|
1403
|
+
const validCount = group.validCount;
|
|
1404
|
+
const validPair = group;
|
|
1263
1405
|
const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(
|
|
1264
1406
|
validPair.heroMask, validPair.villainMask, setup, progressCallback, matchupIndex, totalMatchups
|
|
1265
1407
|
);
|
|
@@ -1285,9 +1427,8 @@ class BitVal {
|
|
|
1285
1427
|
}
|
|
1286
1428
|
|
|
1287
1429
|
// Exhaustive: EXACT evaluate-once. Build the shared work structure and run
|
|
1288
|
-
// the kernel over all runouts
|
|
1289
|
-
//
|
|
1290
|
-
// byte-for-byte.
|
|
1430
|
+
// the kernel over all runouts (on the main thread for both the worker and
|
|
1431
|
+
// non-worker paths, so they agree byte-for-byte).
|
|
1291
1432
|
const work = this._buildExhaustiveWork(setup);
|
|
1292
1433
|
const comboSuits = setup._comboSuits || (setup._comboSuits = this._buildComboSuits(setup.comboArray));
|
|
1293
1434
|
const nRun = comboSuits.length >> 2;
|
|
@@ -1297,72 +1438,163 @@ class BitVal {
|
|
|
1297
1438
|
}
|
|
1298
1439
|
|
|
1299
1440
|
/**
|
|
1300
|
-
* Builds the reusable per-run work for the exact exhaustive kernel
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1303
|
-
* it
|
|
1441
|
+
* Builds the reusable per-run work for the exact exhaustive kernel straight
|
|
1442
|
+
* from the hand strings (no per-pair objects, no BigInt). Hands are
|
|
1443
|
+
* deduplicated into a UNION of hero+villain concrete hands — each evaluated
|
|
1444
|
+
* once per runout even when it appears in both ranges — with per-hand hero
|
|
1445
|
+
* and villain multiplicities, so duplicates count exactly as they do in the
|
|
1446
|
+
* per-pair unoptimized computation. Small fields get an explicit valid-pair
|
|
1447
|
+
* list; large fields are resolved by the counting sweep in _runExhaustive.
|
|
1448
|
+
* Everything is typed arrays so it could be shipped to workers unchanged.
|
|
1304
1449
|
* @private
|
|
1305
1450
|
*/
|
|
1306
1451
|
_buildExhaustiveWork(setup) {
|
|
1307
|
-
const
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1452
|
+
const board = this._cardsBitset(setup.boardCards);
|
|
1453
|
+
const uIndex = new Int32Array(52 * 52).fill(-1);
|
|
1454
|
+
const uSuitArr = [], uAArr = [], uBArr = [], uLoArr = [], uHiArr = [];
|
|
1455
|
+
const addSide = (hands) => {
|
|
1456
|
+
const u = [];
|
|
1457
|
+
for (const hand of hands) {
|
|
1458
|
+
const p = this._parseHand(hand);
|
|
1459
|
+
if ((p.lo & board.lo) | (p.hi & board.hi)) continue; // conflicts with board
|
|
1460
|
+
const key = p.a < p.b ? p.a * 52 + p.b : p.b * 52 + p.a;
|
|
1461
|
+
let ui = uIndex[key];
|
|
1462
|
+
if (ui < 0) {
|
|
1463
|
+
ui = uIndex[key] = uAArr.length;
|
|
1464
|
+
const s = this._suitsOfCards([hand.substring(0, 2), hand.substring(2, 4)]);
|
|
1465
|
+
uSuitArr.push(s[0], s[1], s[2], s[3]);
|
|
1466
|
+
uAArr.push(p.a); uBArr.push(p.b); uLoArr.push(p.lo); uHiArr.push(p.hi);
|
|
1467
|
+
}
|
|
1468
|
+
u.push(ui);
|
|
1469
|
+
}
|
|
1470
|
+
return u;
|
|
1471
|
+
};
|
|
1472
|
+
const heroU = addSide(setup.heroHands);
|
|
1473
|
+
const villU = addSide(setup.villainHands);
|
|
1474
|
+
const U = uAArr.length;
|
|
1475
|
+
const uSuit = Int32Array.from(uSuitArr), uA = Int32Array.from(uAArr), uB = Int32Array.from(uBArr);
|
|
1476
|
+
const hMult = new Int32Array(U), vMult = new Int32Array(U);
|
|
1477
|
+
for (const u of heroU) hMult[u]++;
|
|
1478
|
+
for (const u of villU) vMult[u]++;
|
|
1479
|
+
const nH = heroU.length, nV = villU.length;
|
|
1319
1480
|
const bb = setup._boardSuits || (setup._boardSuits = this._suitsFromBigMask(setup.boardMask));
|
|
1320
1481
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1482
|
+
// cardU[cardUOff[c]..cardUOff[c+1]) = union hands holding card c, used to
|
|
1483
|
+
// drop hands whose hole cards appear in a runout (that deal is impossible).
|
|
1484
|
+
const cardUOff = new Int32Array(53);
|
|
1485
|
+
for (let u = 0; u < U; u++) { cardUOff[uA[u] + 1]++; cardUOff[uB[u] + 1]++; }
|
|
1486
|
+
for (let c = 0; c < 52; c++) cardUOff[c + 1] += cardUOff[c];
|
|
1487
|
+
const cardU = new Int32Array(U * 2), cfill = cardUOff.slice(0, 52);
|
|
1488
|
+
for (let u = 0; u < U; u++) { cardU[cfill[uA[u]]++] = u; cardU[cfill[uB[u]]++] = u; }
|
|
1489
|
+
|
|
1490
|
+
const useCount = nH >= 16 && nV >= 16;
|
|
1491
|
+
let pHa = null, pVa = null, P = 0, cardTotal = null;
|
|
1323
1492
|
if (useCount) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
for (let
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1493
|
+
// cardTotal[c] = number of villain entries holding card c.
|
|
1494
|
+
cardTotal = new Int32Array(52);
|
|
1495
|
+
for (let u = 0; u < U; u++) { cardTotal[uA[u]] += vMult[u]; cardTotal[uB[u]] += vMult[u]; }
|
|
1496
|
+
} else {
|
|
1497
|
+
const ph = [], pv = [];
|
|
1498
|
+
for (const h of heroU) {
|
|
1499
|
+
const hl = uLoArr[h], hh = uHiArr[h];
|
|
1500
|
+
for (const v of villU) {
|
|
1501
|
+
if ((hl & uLoArr[v]) | (hh & uHiArr[v])) continue;
|
|
1502
|
+
ph.push(h); pv.push(v);
|
|
1503
|
+
}
|
|
1330
1504
|
}
|
|
1331
|
-
|
|
1505
|
+
pHa = Int32Array.from(ph); pVa = Int32Array.from(pv); P = pHa.length;
|
|
1332
1506
|
}
|
|
1333
|
-
return {
|
|
1507
|
+
return {
|
|
1508
|
+
uSuit, uA, uB, hMult, vMult, U, nH, nV, useCount, pHa, pVa, P, cardTotal, cardUOff, cardU,
|
|
1509
|
+
bb0: bb[0], bb1: bb[1], bb2: bb[2], bb3: bb[3]
|
|
1510
|
+
};
|
|
1334
1511
|
}
|
|
1335
1512
|
|
|
1336
1513
|
/**
|
|
1337
1514
|
* Exact exhaustive kernel over runouts [runStart, runEnd). Evaluates each
|
|
1338
1515
|
* unique concrete hand once per runout, then resolves matchups by direct
|
|
1339
|
-
* comparison (small fields) or
|
|
1340
|
-
*
|
|
1516
|
+
* comparison (small fields) or, for large fields, a single sort of the
|
|
1517
|
+
* unique hands followed by a linear sweep over equal-value groups that keeps
|
|
1518
|
+
* running villain counts in total and per hole card (to subtract villains
|
|
1519
|
+
* sharing a card with the hero). Byte-identical to unoptimized.
|
|
1341
1520
|
* @private
|
|
1342
1521
|
*/
|
|
1343
1522
|
_runExhaustive(work, comboSuits, runStart, runEnd) {
|
|
1344
|
-
const {
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1523
|
+
const { uSuit, uA, uB, hMult, vMult, U, nV, useCount, pHa, pVa, P, cardTotal, cardUOff, cardU, bb0, bb1, bb2, bb3 } = work;
|
|
1524
|
+
const uVals = new Int32Array(U);
|
|
1525
|
+
// dead[u] === i <=> hand u holds a card dealt in runout i, so it takes no
|
|
1526
|
+
// part in that runout (neither as hero nor as villain).
|
|
1527
|
+
const dead = new Int32Array(U).fill(-1);
|
|
1528
|
+
const cardTot = useCount ? new Int32Array(52) : null;
|
|
1347
1529
|
let win = 0, tie = 0, lose = 0;
|
|
1530
|
+
|
|
1531
|
+
// Large-field scratch: sort keys (value*U + u, exact in a double) and
|
|
1532
|
+
// per-card villain counts strictly below / equal to the current value.
|
|
1533
|
+
const keys = useCount ? new Float64Array(U) : null;
|
|
1534
|
+
const order = useCount ? new Int32Array(U) : null;
|
|
1535
|
+
const lessC = useCount ? new Int32Array(52) : null;
|
|
1536
|
+
const eqC = useCount ? new Int32Array(52) : null;
|
|
1537
|
+
|
|
1348
1538
|
for (let i = runStart; i < runEnd; i++) {
|
|
1349
1539
|
const b = i << 2;
|
|
1350
1540
|
const d0 = bb0 | comboSuits[b], d1 = bb1 | comboSuits[b + 1], d2 = bb2 | comboSuits[b + 2], d3 = bb3 | comboSuits[b + 3];
|
|
1351
|
-
for (let
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
+
|
|
1543
|
+
// Mark hands holding a runout card; for counting, also remove those
|
|
1544
|
+
// villains from the field total and the per-card totals.
|
|
1545
|
+
let nVi = nV;
|
|
1546
|
+
if (useCount) cardTot.set(cardTotal);
|
|
1547
|
+
for (let s = 0; s < 4; s++) {
|
|
1548
|
+
let m = comboSuits[b + s];
|
|
1549
|
+
while (m) {
|
|
1550
|
+
const low = m & -m; m ^= low;
|
|
1551
|
+
const c = (31 - Math.clz32(low)) * 4 + s;
|
|
1552
|
+
for (let k = cardUOff[c], e = cardUOff[c + 1]; k < e; k++) {
|
|
1553
|
+
const u = cardU[k];
|
|
1554
|
+
if (dead[u] === i) continue;
|
|
1555
|
+
dead[u] = i;
|
|
1556
|
+
if (useCount) { const vm = vMult[u]; if (vm) { nVi -= vm; cardTot[uA[u]] -= vm; cardTot[uB[u]] -= vm; } }
|
|
1557
|
+
}
|
|
1363
1558
|
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if (!useCount) {
|
|
1562
|
+
for (let p = 0; p < P; p++) {
|
|
1563
|
+
const hu = pHa[p], vu = pVa[p];
|
|
1564
|
+
if (dead[hu] === i || dead[vu] === i) continue;
|
|
1565
|
+
const hv = uVals[hu], vv = uVals[vu]; if (hv > vv) win++; else if (vv > hv) lose++; else tie++;
|
|
1566
|
+
}
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
for (let u = 0; u < U; u++) keys[u] = uVals[u] * U + u;
|
|
1571
|
+
keys.sort();
|
|
1572
|
+
for (let k = 0; k < U; k++) { const key = keys[k]; order[k] = key - Math.floor(key / U) * U; }
|
|
1573
|
+
lessC.fill(0);
|
|
1574
|
+
|
|
1575
|
+
let below = 0; // villain entries with value < current group value
|
|
1576
|
+
for (let g = 0; g < U;) {
|
|
1577
|
+
const x = uVals[order[g]];
|
|
1578
|
+
let e = g + 1; while (e < U && uVals[order[e]] === x) e++;
|
|
1579
|
+
// Villain counts at exactly x: total and per card.
|
|
1580
|
+
let eqTotal = 0;
|
|
1581
|
+
for (let k = g; k < e; k++) { const u = order[k]; if (dead[u] === i) continue; const m = vMult[u]; if (m) { eqTotal += m; eqC[uA[u]] += m; eqC[uB[u]] += m; } }
|
|
1582
|
+
const above = nVi - below - eqTotal;
|
|
1583
|
+
// Heroes at x. A villain holding both hero cards (the identical hand)
|
|
1584
|
+
// is subtracted on each card, so add it back once (it ties at x).
|
|
1585
|
+
for (let k = g; k < e; k++) {
|
|
1586
|
+
const u = order[k], m = hMult[u];
|
|
1587
|
+
if (!m || dead[u] === i) continue;
|
|
1588
|
+
const a = uA[u], c = uB[u];
|
|
1589
|
+
const w = below - lessC[a] - lessC[c];
|
|
1590
|
+
const t = eqTotal - eqC[a] - eqC[c] + vMult[u];
|
|
1591
|
+
const l = above - (cardTot[a] - lessC[a] - eqC[a]) - (cardTot[c] - lessC[c] - eqC[c]);
|
|
1592
|
+
win += w * m; tie += t * m; lose += l * m;
|
|
1593
|
+
}
|
|
1594
|
+
// Fold this group into the running "below" counts.
|
|
1595
|
+
for (let k = g; k < e; k++) { const u = order[k]; if (dead[u] === i) continue; const m = vMult[u]; if (m) { lessC[uA[u]] += m; lessC[uB[u]] += m; eqC[uA[u]] = 0; eqC[uB[u]] = 0; } }
|
|
1596
|
+
below += eqTotal;
|
|
1597
|
+
g = e;
|
|
1366
1598
|
}
|
|
1367
1599
|
}
|
|
1368
1600
|
return { win, tie, lose };
|
|
@@ -1386,86 +1618,37 @@ class BitVal {
|
|
|
1386
1618
|
return await this._compareRangeOptimizedSequential(setup, progressCallback, progressInterval);
|
|
1387
1619
|
}
|
|
1388
1620
|
|
|
1389
|
-
//
|
|
1390
|
-
const
|
|
1391
|
-
? this._getBoardSuitCounts(setup.boardCards)
|
|
1392
|
-
: null;
|
|
1393
|
-
|
|
1394
|
-
// Group validMatchups by canonical key pair
|
|
1395
|
-
// This ensures we only process matchups that actually exist, avoiding Cartesian product issues
|
|
1396
|
-
const matchupGroups = new Map();
|
|
1397
|
-
for (const matchup of setup.validMatchups) {
|
|
1398
|
-
const mHKey = this._getCanonicalKey(
|
|
1399
|
-
matchup.heroHand,
|
|
1400
|
-
setup.boardCards,
|
|
1401
|
-
setup.numberOfBoardCards,
|
|
1402
|
-
boardSuitCounts
|
|
1403
|
-
);
|
|
1404
|
-
const mVKey = this._getCanonicalKey(
|
|
1405
|
-
matchup.villainHand,
|
|
1406
|
-
setup.boardCards,
|
|
1407
|
-
setup.numberOfBoardCards,
|
|
1408
|
-
boardSuitCounts
|
|
1409
|
-
);
|
|
1410
|
-
const key = `${mHKey}:${mVKey}`;
|
|
1411
|
-
|
|
1412
|
-
if (!matchupGroups.has(key)) {
|
|
1413
|
-
matchupGroups.set(key, {
|
|
1414
|
-
heroKey: mHKey,
|
|
1415
|
-
villainKey: mVKey,
|
|
1416
|
-
representative: matchup,
|
|
1417
|
-
count: 0
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1420
|
-
matchupGroups.get(key).count++;
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// Collect all matchups to evaluate (only those that actually exist)
|
|
1424
|
-
const matchupsToEvaluate = [];
|
|
1425
|
-
for (const [key, group] of matchupGroups) {
|
|
1426
|
-
const validPair = group.representative;
|
|
1427
|
-
matchupsToEvaluate.push({
|
|
1428
|
-
key,
|
|
1429
|
-
validCount: group.count,
|
|
1430
|
-
heroKey: group.heroKey,
|
|
1431
|
-
heroHand: validPair.heroHand,
|
|
1432
|
-
heroMask: validPair.heroMask,
|
|
1433
|
-
villainKey: group.villainKey,
|
|
1434
|
-
villainHand: validPair.villainHand,
|
|
1435
|
-
villainMask: validPair.villainMask
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// Calculate total matchups (number of unique canonical key pairs)
|
|
1621
|
+
// Group validMatchups by canonical key pair (only matchups that exist).
|
|
1622
|
+
const matchupsToEvaluate = this._groupMonteCarloMatchups(setup);
|
|
1440
1623
|
const totalMatchups = matchupsToEvaluate.length;
|
|
1441
|
-
|
|
1624
|
+
|
|
1442
1625
|
// Check if workers should be used
|
|
1443
|
-
const shouldUseWorkers = useWorkers &&
|
|
1444
|
-
typeof Worker !== 'undefined' &&
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
if (
|
|
1448
|
-
|
|
1626
|
+
const shouldUseWorkers = useWorkers &&
|
|
1627
|
+
typeof Worker !== 'undefined' &&
|
|
1628
|
+
totalMatchups >= 4;
|
|
1629
|
+
let workers = null;
|
|
1630
|
+
if (shouldUseWorkers) {
|
|
1631
|
+
const numWorkers = Math.min((typeof navigator !== 'undefined' && navigator.hardwareConcurrency) || 4, totalMatchups);
|
|
1632
|
+
// Persistent pool (created once, reused across calls). Creation can throw
|
|
1633
|
+
// synchronously (e.g. a cross-origin worker URL); fall back to sequential.
|
|
1634
|
+
try { workers = this._acquireWorkers(numWorkers); } catch (e) { workers = null; }
|
|
1635
|
+
}
|
|
1636
|
+
if (!workers || workers.length === 0) {
|
|
1449
1637
|
return await this._compareRangeOptimizedSequential(setup, progressCallback, progressInterval);
|
|
1450
1638
|
}
|
|
1451
|
-
|
|
1452
|
-
// Use Web Workers for parallelization
|
|
1453
|
-
const numWorkers = Math.min(navigator.hardwareConcurrency || 4, matchupsToEvaluate.length);
|
|
1454
|
-
const batchSize = Math.ceil(matchupsToEvaluate.length / numWorkers);
|
|
1455
|
-
const batches = [];
|
|
1456
|
-
for (let i = 0; i < matchupsToEvaluate.length; i += batchSize) {
|
|
1457
|
-
batches.push(matchupsToEvaluate.slice(i, i + batchSize));
|
|
1458
|
-
}
|
|
1639
|
+
const numWorkers = workers.length;
|
|
1459
1640
|
|
|
1460
|
-
//
|
|
1641
|
+
// Dynamic dispatch: hand out small chunks to whichever worker is free.
|
|
1642
|
+
// With Monte Carlo early-stop, matchup cost varies a lot (lopsided matchups
|
|
1643
|
+
// converge in ~8k draws, close ones run the full count), so equal static
|
|
1644
|
+
// batches left most workers idle behind the slowest one.
|
|
1645
|
+
const chunkSize = Math.max(1, Math.ceil(totalMatchups / (numWorkers * 8)));
|
|
1461
1646
|
const serializedSetup = this._serializeSetup(setup);
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
const promises = [];
|
|
1468
|
-
let completedMatchups = 0;
|
|
1647
|
+
let next = 0, completedMatchups = 0, win = 0, tie = 0, lose = 0, alive = numWorkers;
|
|
1648
|
+
const addResult = (r) => {
|
|
1649
|
+
win += r.matchupWin * r.validCount; tie += r.matchupTie * r.validCount; lose += r.matchupLose * r.validCount;
|
|
1650
|
+
completedMatchups++;
|
|
1651
|
+
};
|
|
1469
1652
|
|
|
1470
1653
|
// Progress tracking
|
|
1471
1654
|
let lastProgressTime = 0;
|
|
@@ -1478,67 +1661,50 @@ class BitVal {
|
|
|
1478
1661
|
}
|
|
1479
1662
|
}, progressUpdateInterval) : null;
|
|
1480
1663
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1664
|
+
// Each worker loop pulls the next chunk until none are left. A worker that
|
|
1665
|
+
// crashes is dropped from the pool and its in-flight chunk is evaluated on
|
|
1666
|
+
// the main thread, so one bad worker never loses work or fails the call.
|
|
1667
|
+
const runWorker = (worker, workerId) => new Promise((resolve, reject) => {
|
|
1668
|
+
let current = null;
|
|
1669
|
+
const cleanup = () => { worker.onmessage = null; worker.onerror = null; };
|
|
1670
|
+
const dispatch = () => {
|
|
1671
|
+
if (next >= totalMatchups) { cleanup(); resolve(); return; }
|
|
1672
|
+
const start = next; next = Math.min(totalMatchups, next + chunkSize);
|
|
1673
|
+
current = matchupsToEvaluate.slice(start, next);
|
|
1674
|
+
worker.postMessage({ matchups: current.map(m => this._serializeMatchup(m)), setupData: serializedSetup, workerId });
|
|
1675
|
+
};
|
|
1676
|
+
// One handler set per call; cleared on settle so the pooled worker is
|
|
1677
|
+
// clean for the next call. (Assumes compareRange calls don't overlap.)
|
|
1678
|
+
worker.onmessage = (e) => {
|
|
1679
|
+
if (!e.data.success) { cleanup(); reject(new Error(e.data.error || 'Worker error')); return; }
|
|
1680
|
+
for (const r of e.data.results) addResult(r);
|
|
1681
|
+
current = null;
|
|
1682
|
+
dispatch();
|
|
1683
|
+
};
|
|
1684
|
+
worker.onerror = (error) => {
|
|
1685
|
+
if (error && error.preventDefault) error.preventDefault();
|
|
1686
|
+
cleanup();
|
|
1687
|
+
this._discardWorker(worker);
|
|
1688
|
+
try {
|
|
1689
|
+
if (current) for (const m of current) addResult({ validCount: m.validCount, ...this._evaluateMatchupSync(m.heroMask, m.villainMask, setup) });
|
|
1690
|
+
current = null;
|
|
1691
|
+
// Last worker of this call died: finish the remaining chunks here.
|
|
1692
|
+
if (--alive === 0) {
|
|
1693
|
+
while (next < totalMatchups) { const m = matchupsToEvaluate[next++]; addResult({ validCount: m.validCount, ...this._evaluateMatchupSync(m.heroMask, m.villainMask, setup) }); }
|
|
1495
1694
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
reject(error);
|
|
1502
|
-
};
|
|
1503
|
-
|
|
1504
|
-
// Serialize matchups for this batch
|
|
1505
|
-
const serializedBatch = batches[i].map(m => this._serializeMatchup(m));
|
|
1695
|
+
resolve();
|
|
1696
|
+
} catch (err) { reject(err); }
|
|
1697
|
+
};
|
|
1698
|
+
dispatch();
|
|
1699
|
+
});
|
|
1506
1700
|
|
|
1507
|
-
worker.postMessage({
|
|
1508
|
-
matchups: serializedBatch,
|
|
1509
|
-
setupData: serializedSetup,
|
|
1510
|
-
workerId: i
|
|
1511
|
-
});
|
|
1512
|
-
});
|
|
1513
|
-
|
|
1514
|
-
promises.push(promise);
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// Wait for all workers and aggregate results
|
|
1518
|
-
let win = 0, tie = 0, lose = 0;
|
|
1519
1701
|
try {
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
for (const batchResults of allResults) {
|
|
1523
|
-
for (const result of batchResults) {
|
|
1524
|
-
win += result.matchupWin * result.validCount;
|
|
1525
|
-
tie += result.matchupTie * result.validCount;
|
|
1526
|
-
lose += result.matchupLose * result.validCount;
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1702
|
+
await Promise.all(workers.map((w, i) => runWorker(w, i)));
|
|
1529
1703
|
} finally {
|
|
1530
|
-
|
|
1531
|
-
if (progressIntervalId) {
|
|
1532
|
-
clearInterval(progressIntervalId);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1704
|
+
if (progressIntervalId) clearInterval(progressIntervalId);
|
|
1535
1705
|
// Workers are NOT terminated - they stay in the pool for the next call.
|
|
1536
1706
|
// Callers can free them explicitly with terminateWorkerPool().
|
|
1537
|
-
|
|
1538
|
-
// Final progress update
|
|
1539
|
-
if (progressCallback) {
|
|
1540
|
-
progressCallback(totalMatchups, totalMatchups, 'Complete');
|
|
1541
|
-
}
|
|
1707
|
+
if (progressCallback) progressCallback(totalMatchups, totalMatchups, 'Complete');
|
|
1542
1708
|
}
|
|
1543
1709
|
|
|
1544
1710
|
return { win, tie, lose };
|
|
@@ -1553,7 +1719,7 @@ class BitVal {
|
|
|
1553
1719
|
_acquireWorkers(count) {
|
|
1554
1720
|
if (!this._workerPool) this._workerPool = [];
|
|
1555
1721
|
while (this._workerPool.length < count) {
|
|
1556
|
-
this._workerPool.push(new Worker(
|
|
1722
|
+
this._workerPool.push(new Worker(this.workerUrl));
|
|
1557
1723
|
}
|
|
1558
1724
|
return this._workerPool.slice(0, count);
|
|
1559
1725
|
}
|
|
@@ -1646,26 +1812,6 @@ class BitVal {
|
|
|
1646
1812
|
}
|
|
1647
1813
|
}
|
|
1648
1814
|
|
|
1649
|
-
/**
|
|
1650
|
-
* Flattens hands to canonical keys for caching optimization.
|
|
1651
|
-
* @private
|
|
1652
|
-
*/
|
|
1653
|
-
_flattenHandsToCanonical(hands, boardCards = [], numberOfBoardCards = 5) {
|
|
1654
|
-
const canonicalMap = new Map();
|
|
1655
|
-
const canonicalToHand = new Map();
|
|
1656
|
-
const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
|
|
1657
|
-
|
|
1658
|
-
for (const hand of hands) {
|
|
1659
|
-
const key = this._getCanonicalKey(hand, boardCards, numberOfBoardCards, boardSuitCounts);
|
|
1660
|
-
canonicalMap.set(key, (canonicalMap.get(key) || 0) + 1);
|
|
1661
|
-
if (!canonicalToHand.has(key)) {
|
|
1662
|
-
canonicalToHand.set(key, hand);
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
return { canonicalMap, canonicalToHand };
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
1815
|
/**
|
|
1670
1816
|
* Gets board suit counts for flush potential calculation.
|
|
1671
1817
|
* @private
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poker-bitvaljs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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
|
-
"test": "node bench/correctness.js && node bench/determinism.js && node bench/exact-optimize.js 300 && node bench/eval-equivalence.js 1000000",
|
|
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
8
|
"test:full": "node bench/eval-equivalence.js --full && node bench/engine-differential.js && node bench/exact-optimize.js 2000",
|
|
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
|
+
"test:simulate": "node bench/simulate-equivalence.js",
|
|
13
14
|
"gen:preflop": "node bench/gen-preflop-table.js --out bench/preflop-table.bin",
|
|
14
15
|
"bench": "node bench/bench.js",
|
|
15
16
|
"bench:correct": "node bench/correctness.js",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"files": [
|
|
34
35
|
"bitval.js",
|
|
35
36
|
"bitval-worker.js",
|
|
37
|
+
"bench/preflop-table.bin",
|
|
36
38
|
"README.md"
|
|
37
39
|
]
|
|
38
40
|
}
|