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 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 for the Monte Carlo path (default: `true`). The exact flop/turn path runs on the main thread.
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`. Generate the table with `npm run gen:preflop`.
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. Workers handle the Monte Carlo path only; the
120
- // exhaustive exact path runs on the main thread (see BitVal._compareRangeOptimized).
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
- const result = await evaluateMatchupInWorker(matchup.heroMask, matchup.villainMask, setup, _bitval);
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