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.
- package/bitval-worker.js +4 -3
- package/bitval.js +125 -124
- 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
|
-
|
|
800
|
-
comboArray =
|
|
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
|
-
//
|
|
944
|
-
// This
|
|
945
|
-
|
|
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
|
-
|
|
962
|
-
|
|
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 (
|
|
967
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1015
|
-
if (
|
|
1016
|
-
|
|
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
|
-
//
|
|
1065
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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 [
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
|
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.
|
|
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": {
|