poker-bitvaljs 1.1.0 → 1.1.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 (3) hide show
  1. package/bitval-worker.js +153 -0
  2. package/bitval.js +179 -123
  3. package/package.json +3 -2
@@ -0,0 +1,153 @@
1
+ // Web Worker for parallel matchup evaluation
2
+ // Loads bitval.js to access BitVal class and dependencies
3
+ importScripts('./bitval.js');
4
+
5
+ /**
6
+ * Deserializes setup data from main thread (converts string BigInts back to BigInt)
7
+ */
8
+ function deserializeSetup(setupData) {
9
+ const setup = {
10
+ ...setupData,
11
+ boardMask: BigInt(setupData.boardMask),
12
+ deadCardsMask: BigInt(setupData.deadCardsMask),
13
+ comboArray: setupData.comboArray ? setupData.comboArray.map(s => BigInt(s)) : null,
14
+ // Keep other properties as-is
15
+ boardCards: setupData.boardCards,
16
+ numberOfBoardCards: setupData.numberOfBoardCards,
17
+ iterations: setupData.iterations,
18
+ isExhaustive: setupData.isExhaustive,
19
+ numberOfCardsToDeal: setupData.numberOfCardsToDeal
20
+ };
21
+ return setup;
22
+ }
23
+
24
+ /**
25
+ * Deserializes matchup data (converts string BigInt masks back to BigInt)
26
+ */
27
+ function deserializeMatchup(matchupData) {
28
+ return {
29
+ ...matchupData,
30
+ heroMask: BigInt(matchupData.heroMask),
31
+ villainMask: BigInt(matchupData.villainMask)
32
+ };
33
+ }
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
+ }
94
+
95
+ // Worker message handler
96
+ self.onmessage = async function(e) {
97
+ try {
98
+ const { matchups, setupData, workerId } = e.data;
99
+
100
+ // Deserialize setup
101
+ 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
+ const results = [];
108
+
109
+ // Evaluate each matchup in the batch
110
+ for (const matchupData of matchups) {
111
+ 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
+ });
136
+ }
137
+
138
+ // Send results back to main thread
139
+ self.postMessage({
140
+ workerId,
141
+ results,
142
+ success: true
143
+ });
144
+ } catch (error) {
145
+ // Send error back to main thread
146
+ self.postMessage({
147
+ workerId: e.data.workerId,
148
+ error: error.message,
149
+ success: false
150
+ });
151
+ }
152
+ };
153
+
package/bitval.js CHANGED
@@ -672,6 +672,13 @@ class BitVal {
672
672
  // Generate valid matchups (always needed)
673
673
  const validMatchups = this._generateValidMatchups(heroHands, villainHands, boardCards);
674
674
 
675
+ // Log all valid matchups for debugging
676
+ console.log(`\n[Valid Matchups] Generated ${validMatchups.length} valid matchups:`);
677
+ for (let i = 0; i < validMatchups.length; i++) {
678
+ const m = validMatchups[i];
679
+ console.log(` ${i + 1}. Hero: ${m.heroHand} vs Villain: ${m.villainHand}`);
680
+ }
681
+
675
682
  // Calculate canonical mappings only if optimization is enabled
676
683
  let hCanon = null;
677
684
  let vCanon = null;
@@ -786,6 +793,7 @@ class BitVal {
786
793
  async _evaluateMatchup(heroMask, villainMask, setup, evalCache = null, progressCallback = null, matchupIndex = 0, totalMatchups = 1) {
787
794
  let win = 0, tie = 0, lose = 0;
788
795
  const deadMask = heroMask | villainMask | setup.deadCardsMask;
796
+ const matchupDeadMask = heroMask | villainMask; // Only matchup-level dead cards (excludes setup.deadCardsMask)
789
797
 
790
798
  // Initialize random number generator for Monte Carlo (not needed for exhaustive)
791
799
  if (!setup.isExhaustive) {
@@ -796,8 +804,8 @@ class BitVal {
796
804
  let comboArray = null;
797
805
  let iterations = setup.iterations;
798
806
  if (setup.isExhaustive) {
799
- const availableMasks = this._getAvailableCardMasksByLookUp(deadMask);
800
- comboArray = this._getCombinations(availableMasks, setup.numberOfCardsToDeal);
807
+ // Reuse pre-computed comboArray from setup (already computed in _compareRangeSetup)
808
+ comboArray = setup.comboArray;
801
809
  iterations = comboArray.length;
802
810
  }
803
811
 
@@ -815,8 +823,8 @@ class BitVal {
815
823
  // Evaluate both hands - inlined for performance (no function call overhead)
816
824
  let hEval, hKick, vEval, vKick;
817
825
  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);
826
+ [hEval, hKick] = this._getCachedEvaluation(evalCache.heroKey, evalCache.heroHand, board, evalCache.cache, matchupDeadMask);
827
+ [vEval, vKick] = this._getCachedEvaluation(evalCache.villainKey, evalCache.villainHand, board, evalCache.cache, matchupDeadMask);
820
828
  } else {
821
829
  [hEval, hKick] = this.evaluate(heroMask | board);
822
830
  [vEval, vKick] = this.evaluate(villainMask | board);
@@ -940,10 +948,10 @@ class BitVal {
940
948
  ? this._getBoardSuitCounts(setup.boardCards)
941
949
  : null;
942
950
 
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();
951
+ // Group validMatchups by canonical key pair
952
+ // This ensures we only process matchups that actually exist, avoiding Cartesian product issues
953
+ const matchupGroups = new Map();
954
+ console.log(`\n[Sequential - Grouping Matchups] Processing ${setup.validMatchups.length} valid matchups:`);
947
955
  for (const matchup of setup.validMatchups) {
948
956
  const mHKey = this._getCanonicalKey(
949
957
  matchup.heroHand,
@@ -958,89 +966,94 @@ class BitVal {
958
966
  boardSuitCounts
959
967
  );
960
968
  const key = `${mHKey}:${mVKey}`;
961
- if (!canonicalToMatchup.has(key)) {
962
- canonicalToMatchup.set(key, matchup);
969
+
970
+ console.log(` Hero: ${matchup.heroHand} (${mHKey}) vs Villain: ${matchup.villainHand} (${mVKey}) -> Key: ${key}`);
971
+
972
+ if (!matchupGroups.has(key)) {
973
+ matchupGroups.set(key, {
974
+ heroKey: mHKey,
975
+ villainKey: mVKey,
976
+ representative: matchup,
977
+ count: 0
978
+ });
963
979
  }
980
+ matchupGroups.get(key).count++;
964
981
  }
965
-
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
- }
982
+ console.log(`\n[Sequential - Matchup Groups] Created ${matchupGroups.size} unique canonical key pairs:`);
983
+ for (const [key, group] of matchupGroups) {
984
+ console.log(` ${key}: ${group.count} matchup(s), example: ${group.representative.heroHand} vs ${group.representative.villainHand}`);
975
985
  }
976
986
 
987
+ // Calculate accurate total matchups (number of unique canonical key pairs)
988
+ const totalMatchups = matchupGroups.size;
989
+
977
990
  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;
991
+ // Iterate over actual matchup groups (not Cartesian product)
992
+ console.log(`\n[Sequential - Evaluating Matchups] Starting evaluation of ${totalMatchups} unique matchups:`);
993
+ for (const [key, group] of matchupGroups) {
994
+ const validCount = group.count;
995
+ const validPair = group.representative;
996
+
997
+ // Log hand and canonical key mapping for debugging
998
+ console.log(`[Matchup] Hero: ${validPair.heroHand} (Key: ${group.heroKey}) vs Villain: ${validPair.villainHand} (Key: ${group.villainKey}) | Count: ${validCount}`);
999
+
1000
+ const cache = {
1001
+ heroKey: group.heroKey,
1002
+ heroHand: validPair.heroHand,
1003
+ villainKey: group.villainKey,
1004
+ villainHand: validPair.villainHand,
1005
+ cache: evalCache
1006
+ };
1007
+
1008
+ const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(
1009
+ validPair.heroMask,
1010
+ validPair.villainMask,
1011
+ setup,
1012
+ cache,
1013
+ progressCallback,
1014
+ matchupIndex,
1015
+ totalMatchups
1016
+ );
1017
+
1018
+ // Log equity results for this matchup
1019
+ const totalIterations = matchupWin + matchupTie + matchupLose;
1020
+ const equity = totalIterations > 0 ? (matchupWin + matchupTie / 2) / totalIterations * 100 : 0;
1021
+ const weightedEquity = equity * validCount;
1022
+ console.log(`[Equity] ${validPair.heroHand} vs ${validPair.villainHand} (${validCount}x): Win=${matchupWin}, Tie=${matchupTie}, Lose=${matchupLose}, Total=${totalIterations}, Equity=${equity.toFixed(2)}%, Weighted=${weightedEquity.toFixed(2)}%`);
1023
+
1024
+ win += matchupWin * validCount;
1025
+ tie += matchupTie * validCount;
1026
+ lose += matchupLose * validCount;
1027
+
1028
+ matchupIndex++;
1029
+ if (progressCallback && matchupIndex % progressInterval === 0) {
1030
+ const now = Date.now();
1031
+ const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
1013
1032
 
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));
1033
+ // Use modern browser APIs for better scheduling when available
1034
+ if (typeof scheduler !== 'undefined' && scheduler.postTask) {
1035
+ await scheduler.postTask(() => {
1040
1036
  if (shouldUpdateProgress) {
1041
1037
  progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
1042
1038
  lastProgressTime = now;
1043
1039
  }
1040
+ }, { priority: 'user-blocking' });
1041
+ } else if (typeof requestIdleCallback !== 'undefined') {
1042
+ await new Promise(resolve => {
1043
+ requestIdleCallback(() => {
1044
+ if (shouldUpdateProgress) {
1045
+ progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
1046
+ lastProgressTime = now;
1047
+ }
1048
+ resolve();
1049
+ }, { timeout: 1 });
1050
+ });
1051
+ } else {
1052
+ // Fallback to setTimeout
1053
+ await new Promise(resolve => setTimeout(resolve, 0));
1054
+ if (shouldUpdateProgress) {
1055
+ progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
1056
+ lastProgressTime = now;
1044
1057
  }
1045
1058
  }
1046
1059
  }
@@ -1061,8 +1074,10 @@ class BitVal {
1061
1074
  ? this._getBoardSuitCounts(setup.boardCards)
1062
1075
  : null;
1063
1076
 
1064
- // Pre-build lookup map: canonical key pair -> valid matchup
1065
- const canonicalToMatchup = new Map();
1077
+ // Group validMatchups by canonical key pair
1078
+ // This ensures we only process matchups that actually exist, avoiding Cartesian product issues
1079
+ const matchupGroups = new Map();
1080
+ console.log(`\n[Parallel - Grouping Matchups] Processing ${setup.validMatchups.length} valid matchups:`);
1066
1081
  for (const matchup of setup.validMatchups) {
1067
1082
  const mHKey = this._getCanonicalKey(
1068
1083
  matchup.heroHand,
@@ -1077,49 +1092,45 @@ class BitVal {
1077
1092
  boardSuitCounts
1078
1093
  );
1079
1094
  const key = `${mHKey}:${mVKey}`;
1080
- if (!canonicalToMatchup.has(key)) {
1081
- canonicalToMatchup.set(key, matchup);
1095
+
1096
+ console.log(` Hero: ${matchup.heroHand} (${mHKey}) vs Villain: ${matchup.villainHand} (${mVKey}) -> Key: ${key}`);
1097
+
1098
+ if (!matchupGroups.has(key)) {
1099
+ matchupGroups.set(key, {
1100
+ heroKey: mHKey,
1101
+ villainKey: mVKey,
1102
+ representative: matchup,
1103
+ count: 0
1104
+ });
1082
1105
  }
1106
+ matchupGroups.get(key).count++;
1083
1107
  }
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
- }
1108
+ console.log(`\n[Parallel - Matchup Groups] Created ${matchupGroups.size} unique canonical key pairs:`);
1109
+ for (const [key, group] of matchupGroups) {
1110
+ console.log(` ${key}: ${group.count} matchup(s), example: ${group.representative.heroHand} vs ${group.representative.villainHand}`);
1094
1111
  }
1095
1112
 
1096
- // Collect all matchups to evaluate
1113
+ // Collect all matchups to evaluate (only those that actually exist)
1097
1114
  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
- }
1115
+ console.log(`\n[Parallel - Collecting Matchups] Building matchup list from ${matchupGroups.size} groups:`);
1116
+ for (const [key, group] of matchupGroups) {
1117
+ const validPair = group.representative;
1118
+ console.log(` Adding: ${key} (${group.count}x) - Hero: ${validPair.heroHand} vs Villain: ${validPair.villainHand}`);
1119
+ matchupsToEvaluate.push({
1120
+ key,
1121
+ validCount: group.count,
1122
+ heroKey: group.heroKey,
1123
+ heroHand: validPair.heroHand,
1124
+ heroMask: validPair.heroMask,
1125
+ villainKey: group.villainKey,
1126
+ villainHand: validPair.villainHand,
1127
+ villainMask: validPair.villainMask
1128
+ });
1122
1129
  }
1130
+ console.log(`\n[Parallel - Matchups to Evaluate] Total: ${matchupsToEvaluate.length} unique matchups`);
1131
+
1132
+ // Calculate total matchups (number of unique canonical key pairs)
1133
+ const totalMatchups = matchupsToEvaluate.length;
1123
1134
 
1124
1135
  // Check if workers should be used
1125
1136
  const shouldUseWorkers = useWorkers &&
@@ -1159,7 +1170,7 @@ class BitVal {
1159
1170
  }, progressUpdateInterval) : null;
1160
1171
 
1161
1172
  for (let i = 0; i < batches.length; i++) {
1162
- const worker = new Worker('bitval-worker.js');
1173
+ const worker = new Worker('./bitval-worker.js');
1163
1174
  workers.push(worker);
1164
1175
 
1165
1176
  const promise = new Promise((resolve, reject) => {
@@ -1197,6 +1208,18 @@ class BitVal {
1197
1208
 
1198
1209
  for (const batchResults of allResults) {
1199
1210
  for (const result of batchResults) {
1211
+ // Log equity results for this matchup
1212
+ const totalIterations = result.matchupWin + result.matchupTie + result.matchupLose;
1213
+ const equity = totalIterations > 0 ? (result.matchupWin + result.matchupTie / 2) / totalIterations * 100 : 0;
1214
+ const weightedEquity = equity * result.validCount;
1215
+
1216
+ // Find the matchup details from matchupsToEvaluate
1217
+ const matchup = matchupsToEvaluate.find(m => m.key === result.key);
1218
+ const heroHand = matchup ? matchup.heroHand : 'Unknown';
1219
+ const villainHand = matchup ? matchup.villainHand : 'Unknown';
1220
+
1221
+ console.log(`[Equity] ${heroHand} vs ${villainHand} (${result.validCount}x): Win=${result.matchupWin}, Tie=${result.matchupTie}, Lose=${result.matchupLose}, Total=${totalIterations}, Equity=${equity.toFixed(2)}%, Weighted=${weightedEquity.toFixed(2)}%`);
1222
+
1200
1223
  win += result.matchupWin * result.validCount;
1201
1224
  tie += result.matchupTie * result.validCount;
1202
1225
  lose += result.matchupLose * result.validCount;
@@ -1226,12 +1249,34 @@ class BitVal {
1226
1249
  return { win, tie, lose };
1227
1250
  }
1228
1251
 
1252
+ /**
1253
+ * Generates a numeric hash key from canonical key and board for faster cache lookups.
1254
+ * @private
1255
+ */
1256
+ _getCacheKeyHash(canonicalKey, board, matchupDeadMask = 0n) {
1257
+ // Hash the canonical key string
1258
+ let hash = 0;
1259
+ for (let i = 0; i < canonicalKey.length; i++) {
1260
+ hash = ((hash << 5) - hash) + canonicalKey.charCodeAt(i);
1261
+ hash = hash & hash; // Convert to 32-bit integer
1262
+ }
1263
+ // Combine with board bits (use lower 32 bits of BigInt for fast XOR)
1264
+ const boardBits = Number(board & 0xFFFFFFFFn);
1265
+ hash = hash ^ boardBits;
1266
+
1267
+ // Include matchup dead mask to prevent cross-matchup cache collisions
1268
+ // This ensures different matchups (with different dead cards) don't share cache entries
1269
+ const deadBits = Number(matchupDeadMask & 0xFFFFFFFFn);
1270
+ return hash ^ deadBits;
1271
+ }
1272
+
1229
1273
  /**
1230
1274
  * Gets cached evaluation or evaluates and caches the result.
1275
+ * Uses numeric hash keys for faster cache lookups.
1231
1276
  * @private
1232
1277
  */
1233
- _getCachedEvaluation(canonicalKey, originalHand, completeBoard, evalCache) {
1234
- const cacheKey = canonicalKey + completeBoard.toString();
1278
+ _getCachedEvaluation(canonicalKey, originalHand, completeBoard, evalCache, matchupDeadMask = 0n) {
1279
+ const cacheKey = this._getCacheKeyHash(canonicalKey, completeBoard, matchupDeadMask);
1235
1280
  if (evalCache.has(cacheKey)) return evalCache.get(cacheKey);
1236
1281
  const handMask = this.getBitMasked(this._handStringToCards(originalHand));
1237
1282
  const evaluation = this.evaluate(handMask | completeBoard);
@@ -1311,13 +1356,24 @@ class BitVal {
1311
1356
  const canonicalMap = new Map();
1312
1357
  const canonicalToHand = new Map();
1313
1358
  const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
1359
+
1360
+ console.log(`\n[Canonical Mapping] Processing ${hands.length} hands:`);
1314
1361
  for (const hand of hands) {
1315
1362
  const key = this._getCanonicalKey(hand, boardCards, numberOfBoardCards, boardSuitCounts);
1316
1363
  canonicalMap.set(key, (canonicalMap.get(key) || 0) + 1);
1317
1364
  if (!canonicalToHand.has(key)) {
1318
1365
  canonicalToHand.set(key, hand);
1319
1366
  }
1367
+ console.log(` Hand: ${hand} -> Canonical Key: ${key}`);
1368
+ }
1369
+
1370
+ // Show summary of canonical key groupings
1371
+ console.log(`\n[Canonical Summary] ${canonicalMap.size} unique canonical keys from ${hands.length} hands:`);
1372
+ for (const [key, count] of canonicalMap.entries()) {
1373
+ const representativeHand = canonicalToHand.get(key);
1374
+ console.log(` ${key}: ${count} hand(s), example: ${representativeHand}`);
1320
1375
  }
1376
+
1321
1377
  return { canonicalMap, canonicalToHand };
1322
1378
  }
1323
1379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-bitvaljs",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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": {
@@ -19,10 +19,11 @@
19
19
  "license": "MIT",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/DarraghGeo/poker-bitvaljs.git"
22
+ "url": "git+https://github.com/DarraghGeo/poker-bitvaljs.git"
23
23
  },
24
24
  "files": [
25
25
  "bitval.js",
26
+ "bitval-worker.js",
26
27
  "README.md"
27
28
  ]
28
29
  }