poker-bitvaljs 1.1.2 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +48 -27
  2. package/bitval-worker.js +66 -71
  3. package/bitval.js +574 -284
  4. package/package.json +11 -2
package/README.md CHANGED
@@ -6,11 +6,15 @@ High-performance poker hand evaluator optimized for browser environments, using
6
6
 
7
7
  ## Features
8
8
 
9
- - **Fast hand evaluation** using bitwise operations
10
- - **Range vs range equity calculations** with canonical key caching optimization
11
- - **Web Workers support** for parallel processing across multiple CPU cores (3-7x speedup)
12
- - **Monte Carlo simulation** for preflop and postflop scenarios
13
- - **Exhaustive enumeration** for flop/turn scenarios (2 or fewer cards to come)
9
+ - **Fast hand evaluation** using four 13-bit suit lanes and plain 32-bit integers
10
+ (no BigInt in the hot path) — ~28–70× faster than the previous BigInt engine
11
+ - **Exact range vs range equity** on the flop and turn via evaluate-once with
12
+ counting (byte-identical to brute force, no approximation)
13
+ - **Exact, instant preflop** via an optional precomputed equity table
14
+ (`loadPreflopTable`) — replaces Monte Carlo entirely for preflop
15
+ - **Monte Carlo** with confidence-interval early-stop for the remaining sampled
16
+ case (preflop with dead cards)
17
+ - **Web Workers** for the Monte Carlo path
14
18
  - **Progress callbacks** with configurable update intervals
15
19
  - **Browser and Node.js compatible**
16
20
 
@@ -157,9 +161,9 @@ Simulates a single hand vs hand matchup.
157
161
  **Returns:**
158
162
  - `{ win, tie, lose }`: Results object
159
163
 
160
- ### `compareRange(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize, progressCallback, progressInterval, useWorkers)`
164
+ ### `compareRange(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize, progressCallback, progressInterval, useWorkers, mcTargetPct)`
161
165
 
162
- Compares two ranges of hands with optional optimization and Web Workers parallelization.
166
+ Compares two ranges of hands.
163
167
 
164
168
  **Parameters:**
165
169
  - `heroHands` (Array): Array of hero hand strings (e.g., `['AsAh', 'AsAd']`)
@@ -167,40 +171,57 @@ Compares two ranges of hands with optional optimization and Web Workers parallel
167
171
  - `boardCards` (Array): Board cards as strings (default: `[]`)
168
172
  - `deadCards` (Array): Dead cards as strings (default: `[]`)
169
173
  - `numberOfBoardCards` (Number): Total board cards (default: `5`)
170
- - `iterations` (Number): Number of simulations per matchup (default: `10000`)
171
- - `optimize` (Boolean): Use canonical key caching for performance (default: `true`). **Note:** Optimizations may result in a ±0.5% margin of error compared to unoptimized calculations.
174
+ - `iterations` (Number): Monte Carlo samples per matchup (default: `10000`). Ignored on the flop/turn (exhaustive) and preflop-table paths, which are exact.
175
+ - `optimize` (Boolean): Use the optimized path (default: `true`). On the flop/turn this is **exact** (byte-identical to `optimize: false`), just much faster.
172
176
  - `progressCallback` (Function): Optional callback `(current, total, message) => {}` (default: `null`)
173
177
  - `progressInterval` (Number): Update progress callback every N matchups (default: `100`)
174
- - `useWorkers` (Boolean): Use Web Workers for parallelization across CPU cores (default: `true`). Automatically falls back to sequential execution if workers are unavailable or workload is too small (< 4 matchups).
178
+ - `useWorkers` (Boolean): Use Web Workers for the Monte Carlo path (default: `true`). The exact flop/turn path runs on the main thread.
179
+ - `mcTargetPct` (Number): Monte Carlo early-stop target — stop sampling a matchup once its 95% confidence-interval half-width is within this many equity percentage points (default: `0.3`). Pass `0` to force the full `iterations`. Only affects the Monte Carlo (preflop) path.
175
180
 
176
181
  **Returns:**
177
182
  - `Promise<{ win, tie, lose }>`: Results object
178
183
 
179
- **Web Workers Notes:**
180
- - Web Workers provide 3-7x speedup on multi-core systems by parallelizing matchup evaluations
181
- - Workers are automatically disabled when:
182
- - `useWorkers` is `false`
183
- - Workers are not supported by the browser
184
- - There are fewer than 4 matchups (overhead not worth it)
185
- - For local testing with `file://` protocol, use a local web server (see [Testing Locally](#testing-locally))
184
+ ### `loadPreflopTable(buffer)`
185
+
186
+ Loads the precomputed exact preflop equity table and enables the preflop fast
187
+ path in `compareRange` (empty board, no dead cards). Accepts an `ArrayBuffer`
188
+ (browser) or Node `Buffer`. Generate the table with `npm run gen:preflop`.
189
+
190
+ ```javascript
191
+ // Browser
192
+ const buf = await (await fetch('./bench/preflop-table.bin')).arrayBuffer();
193
+ bitval.loadPreflopTable(buf);
194
+
195
+ // Node
196
+ const fs = require('fs');
197
+ bitval.loadPreflopTable(fs.readFileSync('bench/preflop-table.bin'));
198
+ ```
199
+
200
+ Without a table loaded, preflop falls back to Monte Carlo — nothing else changes.
186
201
 
187
202
  ## Performance
188
203
 
189
- - **Optimized for browser environments** with efficient bitwise operations
190
- - **Web Workers parallelization** provides 3-7x speedup on multi-core systems
191
- - Canonical key caching reduces redundant evaluations (may introduce ±0.5% margin of error)
192
- - Exhaustive enumeration for flop/turn (2 or fewer cards to come)
193
- - Monte Carlo simulation for preflop and river scenarios
194
- - Adaptive progress reporting to minimize UI blocking
204
+ - **No BigInt in the hot path** — cards are four 13-bit suit lanes evaluated with
205
+ plain 32-bit integer ops. ~28–70× faster than the previous BigInt engine.
206
+ - **Flop/turn: exact and fast.** Each unique concrete hand is evaluated once per
207
+ runout; wide range-vs-range is resolved by counting. No ±0.5% approximation.
208
+ - **Preflop: exact table lookup** (with `loadPreflopTable`) instead of Monte
209
+ Carlo — effectively instant.
210
+ - **Monte Carlo** (only preflop with dead cards) uses confidence-interval
211
+ early-stop and Web Workers.
212
+
213
+ Reproduce the benchmarks with `node bench/pre-vs-post.js` and `npm run bench`;
214
+ correctness with `npm test` (and `npm run test:full` for the exhaustive proofs).
195
215
 
196
216
  **Test performance and benchmark online:** [https://darraghgeo.github.io/poker-bitvaljs/](https://darraghgeo.github.io/poker-bitvaljs/)
197
217
 
198
218
  ### Performance Tips
199
219
 
200
- - Enable Web Workers (default) for best performance on multi-core systems
201
- - Use canonical caching (`optimize: true`) for large range comparisons
202
- - Adjust `progressInterval` to balance UI responsiveness vs. performance
203
- - For very small workloads (< 4 matchups), workers are automatically disabled to avoid overhead
220
+ - Call `loadPreflopTable` once at startup for exact, instant preflop equity.
221
+ - Keep `optimize: true` (default) — on the flop/turn it is exact and much faster.
222
+ - Lower `mcTargetPct` (or set `0`) for higher-precision Monte Carlo; raise it for
223
+ faster, looser preflop-with-dead-cards estimates.
224
+ - Adjust `progressInterval` to balance UI responsiveness vs. performance.
204
225
 
205
226
  ## Testing Locally
206
227
 
package/bitval-worker.js CHANGED
@@ -36,103 +36,98 @@ function deserializeMatchup(matchupData) {
36
36
  * Evaluates a single matchup in the worker context
37
37
  * Similar to _evaluateMatchup but without progress callbacks
38
38
  */
39
- async function evaluateMatchupInWorker(heroMask, villainMask, setup, evalCache, cacheInfo, bitval) {
39
+ async function evaluateMatchupInWorker(heroMask, villainMask, setup, bitval) {
40
40
  let win = 0, tie = 0, lose = 0;
41
41
  const deadMask = heroMask | villainMask | setup.deadCardsMask;
42
- const matchupDeadMask = heroMask | villainMask; // Only matchup-level dead cards (excludes setup.deadCardsMask)
43
-
42
+
44
43
  // Initialize random number generator for Monte Carlo (not needed for exhaustive)
45
44
  if (!setup.isExhaustive) {
46
45
  bitval.xorShift = new XorShift32();
47
46
  }
48
47
 
49
- // Prepare for exhaustive enumeration if applicable
50
- let comboArray = null;
48
+ // BigInt-free (Number) hot loop, mirroring BitVal._evaluateMatchup. Cards are
49
+ // four 13-bit suit rank masks; bitval._eval7 does the evaluation with no BigInt.
51
50
  let iterations = setup.iterations;
51
+ const heroSuits = bitval._suitsFromBigMask(heroMask);
52
+ const villainSuits = bitval._suitsFromBigMask(villainMask);
53
+ const boardBaseSuits = bitval._suitsFromBigMask(setup.boardMask);
54
+ const hs0 = heroSuits[0], hs1 = heroSuits[1], hs2 = heroSuits[2], hs3 = heroSuits[3];
55
+ const vs0 = villainSuits[0], vs1 = villainSuits[1], vs2 = villainSuits[2], vs3 = villainSuits[3];
56
+ const bb0 = boardBaseSuits[0], bb1 = boardBaseSuits[1], bb2 = boardBaseSuits[2], bb3 = boardBaseSuits[3];
57
+
58
+ let comboSuits = null; // flat Int32Array, 4 entries per combo
59
+ let pool = null, poolLen = 0; // packed Int32Array of available cards
60
+ const nDeal = setup.numberOfCardsToDeal;
52
61
  if (setup.isExhaustive) {
53
- // Use comboArray from setup if available, otherwise compute it
54
- if (setup.comboArray && setup.comboArray.length > 0) {
55
- comboArray = setup.comboArray;
56
- iterations = comboArray.length;
57
- } else {
58
- // Compute comboArray if not provided
59
- const availableMasks = bitval._getAvailableCardMasksByLookUp(deadMask);
60
- comboArray = bitval._getCombinations(availableMasks, setup.numberOfCardsToDeal);
61
- iterations = comboArray.length;
62
- }
62
+ // Use comboArray from setup if available, otherwise compute it.
63
+ let comboArray = (setup.comboArray && setup.comboArray.length > 0)
64
+ ? setup.comboArray
65
+ : bitval._getCombinations(bitval._getAvailableCardMasksByLookUp(deadMask), nDeal);
66
+ comboSuits = bitval._buildComboSuits(comboArray);
67
+ iterations = comboSuits.length >> 2;
68
+ } else {
69
+ pool = bitval._availableSuitCards(deadMask);
70
+ poolLen = pool.length;
63
71
  }
64
-
72
+
73
+ // Monte Carlo early-stop (mirrors BitVal._evaluateMatchup).
74
+ const mcTarget = setup.isExhaustive ? 0 : (setup.mcTarget > 0 ? setup.mcTarget / 100 : 0);
75
+ const reqIters = iterations;
76
+ let ran = iterations;
77
+
65
78
  // Main evaluation loop
66
79
  for (let i = 0; i < iterations; i++) {
67
- // Generate board: use pre-computed combo for exhaustive, or deal randomly
68
- const board = setup.isExhaustive
69
- ? setup.boardMask | comboArray[i]
70
- : bitval.deal(setup.boardMask, deadMask, setup.numberOfCardsToDeal) | setup.boardMask;
71
-
72
- // Evaluate both hands
73
- let hEval, hKick, vEval, vKick;
74
- if (cacheInfo && evalCache) {
75
- [hEval, hKick] = bitval._getCachedEvaluation(cacheInfo.heroKey, cacheInfo.heroHand, board, evalCache, matchupDeadMask);
76
- [vEval, vKick] = bitval._getCachedEvaluation(cacheInfo.villainKey, cacheInfo.villainHand, board, evalCache, matchupDeadMask);
80
+ let d0, d1, d2, d3;
81
+ if (comboSuits) {
82
+ const b = i << 2;
83
+ d0 = bb0 | comboSuits[b]; d1 = bb1 | comboSuits[b + 1]; d2 = bb2 | comboSuits[b + 2]; d3 = bb3 | comboSuits[b + 3];
77
84
  } else {
78
- [hEval, hKick] = bitval.evaluate(heroMask | board);
79
- [vEval, vKick] = bitval.evaluate(villainMask | board);
85
+ d0 = bb0; d1 = bb1; d2 = bb2; d3 = bb3;
86
+ for (let kk = 0; kk < nDeal; kk++) {
87
+ const j = kk + bitval.xorShift.next(poolLen - kk);
88
+ const tmp = pool[kk]; pool[kk] = pool[j]; pool[j] = tmp;
89
+ const p = pool[kk], bit = p & 0xFFFF, suit = p >> 16;
90
+ if (suit === 0) d0 |= bit;
91
+ else if (suit === 1) d1 |= bit;
92
+ else if (suit === 2) d2 |= bit;
93
+ else d3 |= bit;
94
+ }
80
95
  }
81
-
82
- // Compare and accumulate results
83
- if (hEval > vEval || (hEval === vEval && hKick > vKick)) {
84
- win++;
85
- } else if (vEval > hEval || (vEval === hEval && vKick > hKick)) {
86
- lose++;
87
- } else {
88
- tie++;
96
+ const hE = bitval._eval7(hs0 | d0, hs1 | d1, hs2 | d2, hs3 | d3);
97
+ const vE = bitval._eval7(vs0 | d0, vs1 | d1, vs2 | d2, vs3 | d3);
98
+ if (hE > vE) win++;
99
+ else if (vE > hE) lose++;
100
+ else tie++;
101
+
102
+ if (mcTarget && (i & 4095) === 4095 && i >= 8191) {
103
+ const n = i + 1, p = (win + tie * 0.5) / n;
104
+ if (1.96 * Math.sqrt(p * (1 - p) / n) < mcTarget) { ran = n; break; }
89
105
  }
90
106
  }
91
-
107
+
108
+ if (ran < reqIters && ran > 0) {
109
+ const s = reqIters / ran;
110
+ win = Math.round(win * s); tie = Math.round(tie * s); lose = Math.round(lose * s);
111
+ }
112
+
92
113
  return { matchupWin: win, matchupTie: tie, matchupLose: lose };
93
114
  }
94
115
 
95
- // Worker message handler
116
+ // Reused BitVal instance (holds the lookup tables; cheap to keep around).
117
+ const _bitval = new BitVal();
118
+
119
+ // Worker message handler. Workers handle the Monte Carlo path only; the
120
+ // exhaustive exact path runs on the main thread (see BitVal._compareRangeOptimized).
96
121
  self.onmessage = async function(e) {
97
122
  try {
98
123
  const { matchups, setupData, workerId } = e.data;
99
-
100
- // Deserialize setup
101
124
  const setup = deserializeSetup(setupData);
102
-
103
- // Create BitVal instance and cache for this worker (reused for all matchups)
104
- const bitval = new BitVal();
105
- const evalCache = new FastestAutoClearingCache(16000000);
106
-
107
125
  const results = [];
108
-
109
- // Evaluate each matchup in the batch
126
+
110
127
  for (const matchupData of matchups) {
111
128
  const matchup = deserializeMatchup(matchupData);
112
-
113
- // Prepare cache info if available
114
- const cacheInfo = matchup.heroKey ? {
115
- heroKey: matchup.heroKey,
116
- heroHand: matchup.heroHand,
117
- villainKey: matchup.villainKey,
118
- villainHand: matchup.villainHand
119
- } : null;
120
-
121
- // Evaluate matchup
122
- const result = await evaluateMatchupInWorker(
123
- matchup.heroMask,
124
- matchup.villainMask,
125
- setup,
126
- evalCache,
127
- cacheInfo,
128
- bitval
129
- );
130
-
131
- results.push({
132
- key: matchup.key,
133
- validCount: matchup.validCount,
134
- ...result
135
- });
129
+ const result = await evaluateMatchupInWorker(matchup.heroMask, matchup.villainMask, setup, _bitval);
130
+ results.push({ key: matchup.key, validCount: matchup.validCount, ...result });
136
131
  }
137
132
 
138
133
  // Send results back to main thread