poker-bitvaljs 1.0.1 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +67 -6
  2. package/bitval.js +242 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,9 +8,10 @@ High-performance poker hand evaluator optimized for browser environments, using
8
8
 
9
9
  - **Fast hand evaluation** using bitwise operations
10
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)
11
12
  - **Monte Carlo simulation** for preflop and postflop scenarios
12
13
  - **Exhaustive enumeration** for flop/turn scenarios (2 or fewer cards to come)
13
- - **Progress callbacks** for long-running calculations
14
+ - **Progress callbacks** with configurable update intervals
14
15
  - **Browser and Node.js compatible**
15
16
 
16
17
  ## Installation
@@ -53,7 +54,9 @@ const result = await bitval.compareRange(
53
54
  5, // number of board cards
54
55
  10000, // iterations
55
56
  true, // optimize (use canonical caching)
56
- null // progress callback (optional)
57
+ null, // progress callback (optional)
58
+ 100, // progress interval (optional, default: 100)
59
+ true // use workers (optional, default: true)
57
60
  );
58
61
 
59
62
  // Result: { win: 8120, tie: 30, lose: 1850 }
@@ -73,7 +76,27 @@ const result = await bitval.compareRange(
73
76
  true,
74
77
  (current, total, message) => {
75
78
  console.log(`${Math.round((current/total)*100)}% - ${message}`);
76
- }
79
+ },
80
+ 100, // Update progress every 100 matchups
81
+ true // Use Web Workers for parallelization
82
+ );
83
+ ```
84
+
85
+ ### With Web Workers Disabled
86
+
87
+ ```javascript
88
+ // Disable Web Workers (useful for debugging or when workers aren't supported)
89
+ const result = await bitval.compareRange(
90
+ heroHands,
91
+ villainHands,
92
+ [],
93
+ [],
94
+ 5,
95
+ 10000,
96
+ true,
97
+ null,
98
+ 100,
99
+ false // Disable Web Workers, use sequential execution
77
100
  );
78
101
  ```
79
102
 
@@ -134,9 +157,9 @@ Simulates a single hand vs hand matchup.
134
157
  **Returns:**
135
158
  - `{ win, tie, lose }`: Results object
136
159
 
137
- ### `compareRange(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize, progressCallback)`
160
+ ### `compareRange(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize, progressCallback, progressInterval, useWorkers)`
138
161
 
139
- Compares two ranges of hands with optional optimization.
162
+ Compares two ranges of hands with optional optimization and Web Workers parallelization.
140
163
 
141
164
  **Parameters:**
142
165
  - `heroHands` (Array): Array of hero hand strings (e.g., `['AsAh', 'AsAd']`)
@@ -146,20 +169,58 @@ Compares two ranges of hands with optional optimization.
146
169
  - `numberOfBoardCards` (Number): Total board cards (default: `5`)
147
170
  - `iterations` (Number): Number of simulations per matchup (default: `10000`)
148
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.
149
- - `progressCallback` (Function): Optional callback `(current, total, message) => {}`
172
+ - `progressCallback` (Function): Optional callback `(current, total, message) => {}` (default: `null`)
173
+ - `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).
150
175
 
151
176
  **Returns:**
152
177
  - `Promise<{ win, tie, lose }>`: Results object
153
178
 
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))
186
+
154
187
  ## Performance
155
188
 
156
189
  - **Optimized for browser environments** with efficient bitwise operations
190
+ - **Web Workers parallelization** provides 3-7x speedup on multi-core systems
157
191
  - Canonical key caching reduces redundant evaluations (may introduce ±0.5% margin of error)
158
192
  - Exhaustive enumeration for flop/turn (2 or fewer cards to come)
159
193
  - Monte Carlo simulation for preflop and river scenarios
194
+ - Adaptive progress reporting to minimize UI blocking
160
195
 
161
196
  **Test performance and benchmark online:** [https://darraghgeo.github.io/poker-bitvaljs/](https://darraghgeo.github.io/poker-bitvaljs/)
162
197
 
198
+ ### Performance Tips
199
+
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
204
+
205
+ ## Testing Locally
206
+
207
+ When testing locally, Web Workers require a proper HTTP origin. If you open `index.html` directly from the file system (`file://` protocol), workers will fail with a `SecurityError`.
208
+
209
+ **Solution: Use a local web server**
210
+
211
+ ```bash
212
+ # Python 3
213
+ python3 -m http.server 8000
214
+
215
+ # Node.js (if you have npx)
216
+ npx http-server -p 8000
217
+
218
+ # PHP (if installed)
219
+ php -S localhost:8000
220
+ ```
221
+
222
+ Then open `http://localhost:8000` in your browser. The application will automatically fall back to sequential execution if workers are unavailable.
223
+
163
224
  ## License
164
225
 
165
226
  MIT
package/bitval.js CHANGED
@@ -366,11 +366,13 @@ class BitVal {
366
366
  * @param {Number} iterations - Number of simulations per matchup (default: 10000)
367
367
  * @param {Boolean} optimize - Use canonical key caching (default: true)
368
368
  * @param {Function} progressCallback - Optional progress callback (current, total, message)
369
+ * @param {Number} progressInterval - Update progress every N matchups (default: 100)
370
+ * @param {Boolean} useWorkers - Use Web Workers for parallelization (default: true)
369
371
  * @returns {Promise<Object>} { win, tie, lose } - Results object
370
372
  */
371
- async compareRange(heroHands, villainHands, boardCards = [], deadCards = [], numberOfBoardCards = 5, iterations = 10000, optimize = true, progressCallback = null) {
373
+ async compareRange(heroHands, villainHands, boardCards = [], deadCards = [], numberOfBoardCards = 5, iterations = 10000, optimize = true, progressCallback = null, progressInterval = 100, useWorkers = true) {
372
374
  const setup = this._compareRangeSetup(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize);
373
- return optimize ? await this._compareRangeOptimized(setup, progressCallback) : await this._compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback);
375
+ return optimize ? await this._compareRangeOptimized(setup, progressCallback, progressInterval, useWorkers) : await this._compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback, progressInterval);
374
376
  }
375
377
 
376
378
  // ============================================
@@ -719,6 +721,63 @@ class BitVal {
719
721
  };
720
722
  }
721
723
 
724
+ /**
725
+ * Serializes setup object for worker communication (converts BigInts to strings).
726
+ * Note: Maps and complex objects are not sent to workers - workers only need basic setup data.
727
+ * @private
728
+ */
729
+ _serializeSetup(setup) {
730
+ return {
731
+ boardMask: setup.boardMask.toString(),
732
+ deadCardsMask: setup.deadCardsMask.toString(),
733
+ comboArray: setup.comboArray ? setup.comboArray.map(m => m.toString()) : null,
734
+ boardCards: setup.boardCards,
735
+ numberOfBoardCards: setup.numberOfBoardCards,
736
+ iterations: setup.iterations,
737
+ isExhaustive: setup.isExhaustive,
738
+ numberOfCardsToDeal: setup.numberOfCardsToDeal
739
+ // Note: hCanon, vCanon, validMatchups, validCounts are not needed in workers
740
+ // as matchups are pre-serialized and sent separately
741
+ };
742
+ }
743
+
744
+ /**
745
+ * Deserializes setup data from worker (converts string BigInts back to BigInt).
746
+ * @private
747
+ */
748
+ _deserializeSetup(setupData) {
749
+ return {
750
+ ...setupData,
751
+ boardMask: BigInt(setupData.boardMask),
752
+ deadCardsMask: BigInt(setupData.deadCardsMask),
753
+ comboArray: setupData.comboArray ? setupData.comboArray.map(s => BigInt(s)) : null
754
+ };
755
+ }
756
+
757
+ /**
758
+ * Serializes matchup data for worker communication (converts BigInt masks to strings).
759
+ * @private
760
+ */
761
+ _serializeMatchup(matchup) {
762
+ return {
763
+ ...matchup,
764
+ heroMask: matchup.heroMask.toString(),
765
+ villainMask: matchup.villainMask.toString()
766
+ };
767
+ }
768
+
769
+ /**
770
+ * Deserializes matchup data from worker (converts string BigInts back to BigInt).
771
+ * @private
772
+ */
773
+ _deserializeMatchup(matchupData) {
774
+ return {
775
+ ...matchupData,
776
+ heroMask: BigInt(matchupData.heroMask),
777
+ villainMask: BigInt(matchupData.villainMask)
778
+ };
779
+ }
780
+
722
781
  /**
723
782
  * Evaluates a single matchup with optional caching.
724
783
  * Optimized for browser performance with adaptive yielding and inlined hot loop.
@@ -819,7 +878,7 @@ class BitVal {
819
878
  * Compares ranges without optimization (evaluates all matchups individually).
820
879
  * @private
821
880
  */
822
- async _compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback = null) {
881
+ async _compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback = null, progressInterval = 100) {
823
882
  let win = 0, tie = 0, lose = 0;
824
883
  const total = setup.validMatchups.length;
825
884
  let lastProgressTime = 0;
@@ -830,7 +889,7 @@ class BitVal {
830
889
  const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(heroMask, villainMask, setup, null, progressCallback, i, total);
831
890
  win += matchupWin; tie += matchupTie; lose += matchupLose;
832
891
 
833
- if (progressCallback && i % 10 === 0) {
892
+ if (progressCallback && i % progressInterval === 0) {
834
893
  const now = Date.now();
835
894
  const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
836
895
 
@@ -867,10 +926,10 @@ class BitVal {
867
926
  }
868
927
 
869
928
  /**
870
- * Compares ranges with canonical key caching optimization.
929
+ * Compares ranges with canonical key caching optimization (sequential version).
871
930
  * @private
872
931
  */
873
- async _compareRangeOptimized(setup, progressCallback = null) {
932
+ async _compareRangeOptimizedSequential(setup, progressCallback = null, progressInterval = 100) {
874
933
  let win = 0, tie = 0, lose = 0;
875
934
  const evalCache = new FastestAutoClearingCache(16000000);
876
935
  let lastProgressTime = 0;
@@ -953,7 +1012,7 @@ class BitVal {
953
1012
  lose += matchupLose * validCount;
954
1013
 
955
1014
  matchupIndex++;
956
- if (progressCallback && matchupIndex % 10 === 0) {
1015
+ if (progressCallback && matchupIndex % progressInterval === 0) {
957
1016
  const now = Date.now();
958
1017
  const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
959
1018
 
@@ -991,6 +1050,182 @@ class BitVal {
991
1050
  return { win, tie, lose };
992
1051
  }
993
1052
 
1053
+ /**
1054
+ * Compares ranges with canonical key caching optimization.
1055
+ * Supports Web Workers for parallelization when enabled.
1056
+ * @private
1057
+ */
1058
+ async _compareRangeOptimized(setup, progressCallback = null, progressInterval = 100, useWorkers = true) {
1059
+ // Pre-calculate board suit counts once (constant for all matchups)
1060
+ const boardSuitCounts = setup.boardCards.length > 0
1061
+ ? this._getBoardSuitCounts(setup.boardCards)
1062
+ : null;
1063
+
1064
+ // Pre-build lookup map: canonical key pair -> valid matchup
1065
+ const canonicalToMatchup = new Map();
1066
+ for (const matchup of setup.validMatchups) {
1067
+ const mHKey = this._getCanonicalKey(
1068
+ matchup.heroHand,
1069
+ setup.boardCards,
1070
+ setup.numberOfBoardCards,
1071
+ boardSuitCounts
1072
+ );
1073
+ const mVKey = this._getCanonicalKey(
1074
+ matchup.villainHand,
1075
+ setup.boardCards,
1076
+ setup.numberOfBoardCards,
1077
+ boardSuitCounts
1078
+ );
1079
+ const key = `${mHKey}:${mVKey}`;
1080
+ if (!canonicalToMatchup.has(key)) {
1081
+ canonicalToMatchup.set(key, matchup);
1082
+ }
1083
+ }
1084
+
1085
+ // Calculate accurate total matchups (excluding invalid pairs with validCount=0)
1086
+ let totalMatchups = 0;
1087
+ for (const [hKey] of setup.hCanon.canonicalMap) {
1088
+ for (const [vKey] of setup.vCanon.canonicalMap) {
1089
+ const key = `${hKey}:${vKey}`;
1090
+ if ((setup.validCounts.get(key) || 0) > 0) {
1091
+ totalMatchups++;
1092
+ }
1093
+ }
1094
+ }
1095
+
1096
+ // Collect all matchups to evaluate
1097
+ const matchupsToEvaluate = [];
1098
+ for (const [hKey] of setup.hCanon.canonicalMap) {
1099
+ for (const [vKey] of setup.vCanon.canonicalMap) {
1100
+ const key = `${hKey}:${vKey}`;
1101
+ const validCount = setup.validCounts.get(key) || 0;
1102
+
1103
+ if (validCount === 0) continue;
1104
+
1105
+ const validPair = canonicalToMatchup.get(key);
1106
+ if (!validPair) {
1107
+ console.error(`[DIAG] ERROR: No valid pair found for ${hKey}:${vKey} despite validCount=${validCount}`);
1108
+ continue;
1109
+ }
1110
+
1111
+ matchupsToEvaluate.push({
1112
+ key,
1113
+ validCount,
1114
+ heroKey: hKey,
1115
+ heroHand: validPair.heroHand,
1116
+ heroMask: validPair.heroMask,
1117
+ villainKey: vKey,
1118
+ villainHand: validPair.villainHand,
1119
+ villainMask: validPair.villainMask
1120
+ });
1121
+ }
1122
+ }
1123
+
1124
+ // Check if workers should be used
1125
+ const shouldUseWorkers = useWorkers &&
1126
+ typeof Worker !== 'undefined' &&
1127
+ matchupsToEvaluate.length >= 4;
1128
+
1129
+ if (!shouldUseWorkers) {
1130
+ // Fallback to sequential execution
1131
+ return await this._compareRangeOptimizedSequential(setup, progressCallback, progressInterval);
1132
+ }
1133
+
1134
+ // Use Web Workers for parallelization
1135
+ const numWorkers = Math.min(navigator.hardwareConcurrency || 4, matchupsToEvaluate.length);
1136
+ const batchSize = Math.ceil(matchupsToEvaluate.length / numWorkers);
1137
+ const batches = [];
1138
+ for (let i = 0; i < matchupsToEvaluate.length; i += batchSize) {
1139
+ batches.push(matchupsToEvaluate.slice(i, i + batchSize));
1140
+ }
1141
+
1142
+ // Serialize setup for workers
1143
+ const serializedSetup = this._serializeSetup(setup);
1144
+
1145
+ // Create workers and distribute work
1146
+ const workers = [];
1147
+ const promises = [];
1148
+ let completedMatchups = 0;
1149
+
1150
+ // Progress tracking
1151
+ let lastProgressTime = 0;
1152
+ const progressUpdateInterval = 100;
1153
+ const progressIntervalId = progressCallback ? setInterval(() => {
1154
+ const now = Date.now();
1155
+ if (now - lastProgressTime >= progressUpdateInterval) {
1156
+ progressCallback(completedMatchups, totalMatchups, `Evaluating with ${numWorkers} workers: ${completedMatchups}/${totalMatchups}`);
1157
+ lastProgressTime = now;
1158
+ }
1159
+ }, progressUpdateInterval) : null;
1160
+
1161
+ for (let i = 0; i < batches.length; i++) {
1162
+ const worker = new Worker('bitval-worker.js');
1163
+ workers.push(worker);
1164
+
1165
+ const promise = new Promise((resolve, reject) => {
1166
+ worker.onmessage = (e) => {
1167
+ if (e.data.success) {
1168
+ completedMatchups += e.data.results.length;
1169
+ resolve(e.data.results);
1170
+ } else {
1171
+ reject(new Error(e.data.error || 'Worker error'));
1172
+ }
1173
+ worker.terminate();
1174
+ };
1175
+ worker.onerror = (error) => {
1176
+ reject(error);
1177
+ worker.terminate();
1178
+ };
1179
+
1180
+ // Serialize matchups for this batch
1181
+ const serializedBatch = batches[i].map(m => this._serializeMatchup(m));
1182
+
1183
+ worker.postMessage({
1184
+ matchups: serializedBatch,
1185
+ setupData: serializedSetup,
1186
+ workerId: i
1187
+ });
1188
+ });
1189
+
1190
+ promises.push(promise);
1191
+ }
1192
+
1193
+ // Wait for all workers and aggregate results
1194
+ let win = 0, tie = 0, lose = 0;
1195
+ try {
1196
+ const allResults = await Promise.all(promises);
1197
+
1198
+ for (const batchResults of allResults) {
1199
+ for (const result of batchResults) {
1200
+ win += result.matchupWin * result.validCount;
1201
+ tie += result.matchupTie * result.validCount;
1202
+ lose += result.matchupLose * result.validCount;
1203
+ }
1204
+ }
1205
+ } finally {
1206
+ // Clean up progress interval
1207
+ if (progressIntervalId) {
1208
+ clearInterval(progressIntervalId);
1209
+ }
1210
+
1211
+ // Ensure all workers are terminated
1212
+ workers.forEach(worker => {
1213
+ try {
1214
+ worker.terminate();
1215
+ } catch (e) {
1216
+ // Ignore termination errors
1217
+ }
1218
+ });
1219
+
1220
+ // Final progress update
1221
+ if (progressCallback) {
1222
+ progressCallback(totalMatchups, totalMatchups, 'Complete');
1223
+ }
1224
+ }
1225
+
1226
+ return { win, tie, lose };
1227
+ }
1228
+
994
1229
  /**
995
1230
  * Gets cached evaluation or evaluates and caches the result.
996
1231
  * @private
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-bitvaljs",
3
- "version": "1.0.1",
3
+ "version": "1.1.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": {