poker-bitvaljs 1.1.1 → 1.2.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/bitval-worker.js +4 -3
  2. package/bitval.js +125 -124
  3. package/package.json +1 -1
package/bitval-worker.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Web Worker for parallel matchup evaluation
2
2
  // Loads bitval.js to access BitVal class and dependencies
3
- importScripts('bitval.js');
3
+ importScripts('./bitval.js');
4
4
 
5
5
  /**
6
6
  * Deserializes setup data from main thread (converts string BigInts back to BigInt)
@@ -39,6 +39,7 @@ function deserializeMatchup(matchupData) {
39
39
  async function evaluateMatchupInWorker(heroMask, villainMask, setup, evalCache, cacheInfo, 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)
42
43
 
43
44
  // Initialize random number generator for Monte Carlo (not needed for exhaustive)
44
45
  if (!setup.isExhaustive) {
@@ -71,8 +72,8 @@ async function evaluateMatchupInWorker(heroMask, villainMask, setup, evalCache,
71
72
  // Evaluate both hands
72
73
  let hEval, hKick, vEval, vKick;
73
74
  if (cacheInfo && evalCache) {
74
- [hEval, hKick] = bitval._getCachedEvaluation(cacheInfo.heroKey, cacheInfo.heroHand, board, evalCache);
75
- [vEval, vKick] = bitval._getCachedEvaluation(cacheInfo.villainKey, cacheInfo.villainHand, board, 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);
76
77
  } else {
77
78
  [hEval, hKick] = bitval.evaluate(heroMask | board);
78
79
  [vEval, vKick] = bitval.evaluate(villainMask | board);
package/bitval.js CHANGED
@@ -786,6 +786,7 @@ class BitVal {
786
786
  async _evaluateMatchup(heroMask, villainMask, setup, evalCache = null, progressCallback = null, matchupIndex = 0, totalMatchups = 1) {
787
787
  let win = 0, tie = 0, lose = 0;
788
788
  const deadMask = heroMask | villainMask | setup.deadCardsMask;
789
+ const matchupDeadMask = heroMask | villainMask; // Only matchup-level dead cards (excludes setup.deadCardsMask)
789
790
 
790
791
  // Initialize random number generator for Monte Carlo (not needed for exhaustive)
791
792
  if (!setup.isExhaustive) {
@@ -796,8 +797,8 @@ class BitVal {
796
797
  let comboArray = null;
797
798
  let iterations = setup.iterations;
798
799
  if (setup.isExhaustive) {
799
- const availableMasks = this._getAvailableCardMasksByLookUp(deadMask);
800
- comboArray = this._getCombinations(availableMasks, setup.numberOfCardsToDeal);
800
+ // Reuse pre-computed comboArray from setup (already computed in _compareRangeSetup)
801
+ comboArray = setup.comboArray;
801
802
  iterations = comboArray.length;
802
803
  }
803
804
 
@@ -815,8 +816,8 @@ class BitVal {
815
816
  // Evaluate both hands - inlined for performance (no function call overhead)
816
817
  let hEval, hKick, vEval, vKick;
817
818
  if (evalCache) {
818
- [hEval, hKick] = this._getCachedEvaluation(evalCache.heroKey, evalCache.heroHand, board, evalCache.cache);
819
- [vEval, vKick] = this._getCachedEvaluation(evalCache.villainKey, evalCache.villainHand, board, evalCache.cache);
819
+ [hEval, hKick] = this._getCachedEvaluation(evalCache.heroKey, evalCache.heroHand, board, evalCache.cache, matchupDeadMask);
820
+ [vEval, vKick] = this._getCachedEvaluation(evalCache.villainKey, evalCache.villainHand, board, evalCache.cache, matchupDeadMask);
820
821
  } else {
821
822
  [hEval, hKick] = this.evaluate(heroMask | board);
822
823
  [vEval, vKick] = this.evaluate(villainMask | board);
@@ -940,10 +941,9 @@ class BitVal {
940
941
  ? this._getBoardSuitCounts(setup.boardCards)
941
942
  : null;
942
943
 
943
- // Pre-build lookup map: canonical key pair -> valid matchup
944
- // This eliminates the O(n) find() call inside nested loops, reducing complexity
945
- // from O(heroCanon × villainCanon × validMatchups) to O(validMatchups + heroCanon × villainCanon)
946
- const canonicalToMatchup = new Map();
944
+ // Group validMatchups by canonical key pair
945
+ // This ensures we only process matchups that actually exist, avoiding Cartesian product issues
946
+ const matchupGroups = new Map();
947
947
  for (const matchup of setup.validMatchups) {
948
948
  const mHKey = this._getCanonicalKey(
949
949
  matchup.heroHand,
@@ -958,89 +958,78 @@ class BitVal {
958
958
  boardSuitCounts
959
959
  );
960
960
  const key = `${mHKey}:${mVKey}`;
961
- if (!canonicalToMatchup.has(key)) {
962
- canonicalToMatchup.set(key, matchup);
961
+
962
+ if (!matchupGroups.has(key)) {
963
+ matchupGroups.set(key, {
964
+ heroKey: mHKey,
965
+ villainKey: mVKey,
966
+ representative: matchup,
967
+ count: 0
968
+ });
963
969
  }
970
+ matchupGroups.get(key).count++;
964
971
  }
965
972
 
966
- // Calculate accurate total matchups (excluding invalid pairs with validCount=0)
967
- let totalMatchups = 0;
968
- for (const [hKey] of setup.hCanon.canonicalMap) {
969
- for (const [vKey] of setup.vCanon.canonicalMap) {
970
- const key = `${hKey}:${vKey}`;
971
- if ((setup.validCounts.get(key) || 0) > 0) {
972
- totalMatchups++;
973
- }
974
- }
975
- }
973
+ // Calculate accurate total matchups (number of unique canonical key pairs)
974
+ const totalMatchups = matchupGroups.size;
976
975
 
977
976
  let matchupIndex = 0;
978
- for (const [hKey] of setup.hCanon.canonicalMap) {
979
- for (const [vKey] of setup.vCanon.canonicalMap) {
980
- const key = `${hKey}:${vKey}`;
981
- const validCount = setup.validCounts.get(key) || 0;
982
-
983
- if (validCount === 0) continue;
984
-
985
- // O(1) lookup instead of O(n) find() - major performance improvement
986
- const validPair = canonicalToMatchup.get(key);
987
- if (!validPair) {
988
- console.error(`[DIAG] ERROR: No valid pair found for ${hKey}:${vKey} despite validCount=${validCount}`);
989
- continue;
990
- }
991
-
992
- const cache = {
993
- heroKey: hKey,
994
- heroHand: validPair.heroHand,
995
- villainKey: vKey,
996
- villainHand: validPair.villainHand,
997
- cache: evalCache
998
- };
999
-
1000
- const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(
1001
- validPair.heroMask,
1002
- validPair.villainMask,
1003
- setup,
1004
- cache,
1005
- progressCallback,
1006
- matchupIndex,
1007
- totalMatchups
1008
- );
1009
-
1010
- win += matchupWin * validCount;
1011
- tie += matchupTie * validCount;
1012
- lose += matchupLose * validCount;
977
+ // Iterate over actual matchup groups (not Cartesian product)
978
+ for (const [key, group] of matchupGroups) {
979
+ const validCount = group.count;
980
+ const validPair = group.representative;
981
+
982
+ const cache = {
983
+ heroKey: group.heroKey,
984
+ heroHand: validPair.heroHand,
985
+ villainKey: group.villainKey,
986
+ villainHand: validPair.villainHand,
987
+ cache: evalCache
988
+ };
989
+
990
+ const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(
991
+ validPair.heroMask,
992
+ validPair.villainMask,
993
+ setup,
994
+ cache,
995
+ progressCallback,
996
+ matchupIndex,
997
+ totalMatchups
998
+ );
999
+
1000
+ win += matchupWin * validCount;
1001
+ tie += matchupTie * validCount;
1002
+ lose += matchupLose * validCount;
1003
+
1004
+ matchupIndex++;
1005
+ if (progressCallback && matchupIndex % progressInterval === 0) {
1006
+ const now = Date.now();
1007
+ const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
1013
1008
 
1014
- matchupIndex++;
1015
- if (progressCallback && matchupIndex % progressInterval === 0) {
1016
- const now = Date.now();
1017
- const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
1018
-
1019
- // Use modern browser APIs for better scheduling when available
1020
- if (typeof scheduler !== 'undefined' && scheduler.postTask) {
1021
- await scheduler.postTask(() => {
1022
- if (shouldUpdateProgress) {
1023
- progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
1024
- lastProgressTime = now;
1025
- }
1026
- }, { priority: 'user-blocking' });
1027
- } else if (typeof requestIdleCallback !== 'undefined') {
1028
- await new Promise(resolve => {
1029
- requestIdleCallback(() => {
1030
- if (shouldUpdateProgress) {
1031
- progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
1032
- lastProgressTime = now;
1033
- }
1034
- resolve();
1035
- }, { timeout: 1 });
1036
- });
1037
- } else {
1038
- // Fallback to setTimeout
1039
- await new Promise(resolve => setTimeout(resolve, 0));
1009
+ // Use modern browser APIs for better scheduling when available
1010
+ if (typeof scheduler !== 'undefined' && scheduler.postTask) {
1011
+ await scheduler.postTask(() => {
1040
1012
  if (shouldUpdateProgress) {
1041
1013
  progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
1042
1014
  lastProgressTime = now;
1043
1015
  }
1016
+ }, { priority: 'user-blocking' });
1017
+ } else if (typeof requestIdleCallback !== 'undefined') {
1018
+ await new Promise(resolve => {
1019
+ requestIdleCallback(() => {
1020
+ if (shouldUpdateProgress) {
1021
+ progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
1022
+ lastProgressTime = now;
1023
+ }
1024
+ resolve();
1025
+ }, { timeout: 1 });
1026
+ });
1027
+ } else {
1028
+ // Fallback to setTimeout
1029
+ await new Promise(resolve => setTimeout(resolve, 0));
1030
+ if (shouldUpdateProgress) {
1031
+ progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
1032
+ lastProgressTime = now;
1044
1033
  }
1045
1034
  }
1046
1035
  }
@@ -1061,8 +1050,9 @@ class BitVal {
1061
1050
  ? this._getBoardSuitCounts(setup.boardCards)
1062
1051
  : null;
1063
1052
 
1064
- // Pre-build lookup map: canonical key pair -> valid matchup
1065
- const canonicalToMatchup = new Map();
1053
+ // Group validMatchups by canonical key pair
1054
+ // This ensures we only process matchups that actually exist, avoiding Cartesian product issues
1055
+ const matchupGroups = new Map();
1066
1056
  for (const matchup of setup.validMatchups) {
1067
1057
  const mHKey = this._getCanonicalKey(
1068
1058
  matchup.heroHand,
@@ -1077,50 +1067,37 @@ class BitVal {
1077
1067
  boardSuitCounts
1078
1068
  );
1079
1069
  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
- }
1070
+
1071
+ if (!matchupGroups.has(key)) {
1072
+ matchupGroups.set(key, {
1073
+ heroKey: mHKey,
1074
+ villainKey: mVKey,
1075
+ representative: matchup,
1076
+ count: 0
1077
+ });
1093
1078
  }
1079
+ matchupGroups.get(key).count++;
1094
1080
  }
1095
1081
 
1096
- // Collect all matchups to evaluate
1082
+ // Collect all matchups to evaluate (only those that actually exist)
1097
1083
  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
- }
1084
+ for (const [key, group] of matchupGroups) {
1085
+ const validPair = group.representative;
1086
+ matchupsToEvaluate.push({
1087
+ key,
1088
+ validCount: group.count,
1089
+ heroKey: group.heroKey,
1090
+ heroHand: validPair.heroHand,
1091
+ heroMask: validPair.heroMask,
1092
+ villainKey: group.villainKey,
1093
+ villainHand: validPair.villainHand,
1094
+ villainMask: validPair.villainMask
1095
+ });
1122
1096
  }
1123
1097
 
1098
+ // Calculate total matchups (number of unique canonical key pairs)
1099
+ const totalMatchups = matchupsToEvaluate.length;
1100
+
1124
1101
  // Check if workers should be used
1125
1102
  const shouldUseWorkers = useWorkers &&
1126
1103
  typeof Worker !== 'undefined' &&
@@ -1159,7 +1136,7 @@ class BitVal {
1159
1136
  }, progressUpdateInterval) : null;
1160
1137
 
1161
1138
  for (let i = 0; i < batches.length; i++) {
1162
- const worker = new Worker('bitval-worker.js');
1139
+ const worker = new Worker('./bitval-worker.js');
1163
1140
  workers.push(worker);
1164
1141
 
1165
1142
  const promise = new Promise((resolve, reject) => {
@@ -1226,12 +1203,34 @@ class BitVal {
1226
1203
  return { win, tie, lose };
1227
1204
  }
1228
1205
 
1206
+ /**
1207
+ * Generates a numeric hash key from canonical key and board for faster cache lookups.
1208
+ * @private
1209
+ */
1210
+ _getCacheKeyHash(canonicalKey, board, matchupDeadMask = 0n) {
1211
+ // Hash the canonical key string
1212
+ let hash = 0;
1213
+ for (let i = 0; i < canonicalKey.length; i++) {
1214
+ hash = ((hash << 5) - hash) + canonicalKey.charCodeAt(i);
1215
+ hash = hash & hash; // Convert to 32-bit integer
1216
+ }
1217
+ // Combine with board bits (use lower 32 bits of BigInt for fast XOR)
1218
+ const boardBits = Number(board & 0xFFFFFFFFn);
1219
+ hash = hash ^ boardBits;
1220
+
1221
+ // Include matchup dead mask to prevent cross-matchup cache collisions
1222
+ // This ensures different matchups (with different dead cards) don't share cache entries
1223
+ const deadBits = Number(matchupDeadMask & 0xFFFFFFFFn);
1224
+ return hash ^ deadBits;
1225
+ }
1226
+
1229
1227
  /**
1230
1228
  * Gets cached evaluation or evaluates and caches the result.
1229
+ * Uses numeric hash keys for faster cache lookups.
1231
1230
  * @private
1232
1231
  */
1233
- _getCachedEvaluation(canonicalKey, originalHand, completeBoard, evalCache) {
1234
- const cacheKey = canonicalKey + completeBoard.toString();
1232
+ _getCachedEvaluation(canonicalKey, originalHand, completeBoard, evalCache, matchupDeadMask = 0n) {
1233
+ const cacheKey = this._getCacheKeyHash(canonicalKey, completeBoard, matchupDeadMask);
1235
1234
  if (evalCache.has(cacheKey)) return evalCache.get(cacheKey);
1236
1235
  const handMask = this.getBitMasked(this._handStringToCards(originalHand));
1237
1236
  const evaluation = this.evaluate(handMask | completeBoard);
@@ -1311,6 +1310,7 @@ class BitVal {
1311
1310
  const canonicalMap = new Map();
1312
1311
  const canonicalToHand = new Map();
1313
1312
  const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
1313
+
1314
1314
  for (const hand of hands) {
1315
1315
  const key = this._getCanonicalKey(hand, boardCards, numberOfBoardCards, boardSuitCounts);
1316
1316
  canonicalMap.set(key, (canonicalMap.get(key) || 0) + 1);
@@ -1318,6 +1318,7 @@ class BitVal {
1318
1318
  canonicalToHand.set(key, hand);
1319
1319
  }
1320
1320
  }
1321
+
1321
1322
  return { canonicalMap, canonicalToHand };
1322
1323
  }
1323
1324
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-bitvaljs",
3
- "version": "1.1.1",
3
+ "version": "1.2.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": {