poker-bitvaljs 1.2.0 → 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 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,70 @@ 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`. The table (~1 MB) ships with the npm package at
189
+ `poker-bitvaljs/bench/preflop-table.bin`; regenerate it with `npm run gen:preflop`.
190
+
191
+ ```javascript
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();
194
+ bitval.loadPreflopTable(buf);
195
+
196
+ // Node
197
+ const fs = require('fs');
198
+ bitval.loadPreflopTable(fs.readFileSync(require.resolve('poker-bitvaljs/bench/preflop-table.bin')));
199
+ ```
200
+
201
+ Without a table loaded, preflop falls back to Monte Carlo — nothing else changes.
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.
186
210
 
187
211
  ## Performance
188
212
 
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
213
+ - **No BigInt in the hot path** — cards are four 13-bit suit lanes evaluated with
214
+ plain 32-bit integer ops. ~28–70× faster than the previous BigInt engine.
215
+ - **Flop/turn: exact and fast.** Each unique concrete hand is evaluated once per
216
+ runout; wide range-vs-range is resolved by counting. No ±0.5% approximation.
217
+ - **Preflop: exact table lookup** (with `loadPreflopTable`) instead of Monte
218
+ Carlo — effectively instant.
219
+ - **Monte Carlo** (only preflop with dead cards) uses confidence-interval
220
+ early-stop and Web Workers.
221
+
222
+ Reproduce the benchmarks with `node bench/pre-vs-post.js` and `npm run bench`;
223
+ correctness with `npm test` (and `npm run test:full` for the exhaustive proofs).
195
224
 
196
225
  **Test performance and benchmark online:** [https://darraghgeo.github.io/poker-bitvaljs/](https://darraghgeo.github.io/poker-bitvaljs/)
197
226
 
198
227
  ### Performance Tips
199
228
 
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
229
+ - Call `loadPreflopTable` once at startup for exact, instant preflop equity.
230
+ - Keep `optimize: true` (default) — on the flop/turn it is exact and much faster.
231
+ - Lower `mcTargetPct` (or set `0`) for higher-precision Monte Carlo; raise it for
232
+ faster, looser preflop-with-dead-cards estimates.
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.
204
238
 
205
239
  ## Testing Locally
206
240
 
Binary file
package/bitval-worker.js CHANGED
@@ -32,107 +32,22 @@ 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, evalCache, cacheInfo, bitval) {
40
- let win = 0, tie = 0, lose = 0;
41
- const deadMask = heroMask | villainMask | setup.deadCardsMask;
42
- const matchupDeadMask = heroMask | villainMask; // Only matchup-level dead cards (excludes setup.deadCardsMask)
43
-
44
- // Initialize random number generator for Monte Carlo (not needed for exhaustive)
45
- if (!setup.isExhaustive) {
46
- bitval.xorShift = new XorShift32();
47
- }
48
-
49
- // Prepare for exhaustive enumeration if applicable
50
- let comboArray = null;
51
- let iterations = setup.iterations;
52
- 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
- }
63
- }
64
-
65
- // Main evaluation loop
66
- 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);
77
- } else {
78
- [hEval, hKick] = bitval.evaluate(heroMask | board);
79
- [vEval, vKick] = bitval.evaluate(villainMask | board);
80
- }
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++;
89
- }
90
- }
91
-
92
- return { matchupWin: win, matchupTie: tie, matchupLose: lose };
93
- }
35
+ // Reused BitVal instance (holds the lookup tables; cheap to keep around).
36
+ const _bitval = new BitVal();
94
37
 
95
- // Worker message handler
38
+ // Worker message handler. Workers handle the Monte Carlo path only; the
39
+ // exhaustive exact path runs on the main thread (see BitVal._compareRangeOptimized).
96
40
  self.onmessage = async function(e) {
97
41
  try {
98
42
  const { matchups, setupData, workerId } = e.data;
99
-
100
- // Deserialize setup
101
43
  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
44
  const results = [];
108
-
109
- // Evaluate each matchup in the batch
45
+
110
46
  for (const matchupData of matchups) {
111
47
  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
- });
48
+ // Same synchronous loop as the main thread (BitVal._evaluateMatchupSync).
49
+ const result = _bitval._evaluateMatchupSync(matchup.heroMask, matchup.villainMask, setup);
50
+ results.push({ key: matchup.key, validCount: matchup.validCount, ...result });
136
51
  }
137
52
 
138
53
  // Send results back to main thread