poker-bitvaljs 1.0.0 → 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.
- package/README.md +67 -6
- package/bitval.js +964 -254
- package/package.json +1 -1
package/bitval.js
CHANGED
|
@@ -13,12 +13,55 @@ class XorShift32 {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Fastest auto-clearing cache with minimal overhead.
|
|
18
|
+
* Uses bitwise counter check to avoid expensive modulo operations.
|
|
19
|
+
*/
|
|
20
|
+
class FastestAutoClearingCache {
|
|
21
|
+
constructor(maxSize = 16000000) {
|
|
22
|
+
this.maxSize = maxSize;
|
|
23
|
+
this.clearThreshold = Math.floor(maxSize * 0.95); // Clear at 95% capacity
|
|
24
|
+
this.cache = new Map();
|
|
25
|
+
this.counter = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
has(key) {
|
|
29
|
+
return this.cache.has(key); // No overhead on hot path
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(key) {
|
|
33
|
+
return this.cache.get(key); // No overhead on hot path
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set(key, value) {
|
|
37
|
+
// Bitwise check is faster than modulo - check every ~1 million operations
|
|
38
|
+
// 0xFFFFF = 1,048,576 (2^20)
|
|
39
|
+
if ((++this.counter & 0xFFFFF) === 0) {
|
|
40
|
+
if (this.cache.size >= this.clearThreshold) {
|
|
41
|
+
this.cache.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this.cache.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clear() {
|
|
48
|
+
this.cache.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get size() {
|
|
52
|
+
return this.cache.size;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
16
56
|
class BitVal {
|
|
17
57
|
constructor() {
|
|
58
|
+
// Suit constants
|
|
18
59
|
this._DIAMOND = 3n;
|
|
19
60
|
this._HEART = 2n;
|
|
20
61
|
this._CLUB = 1n;
|
|
21
62
|
this._SPADE = 0n;
|
|
63
|
+
|
|
64
|
+
// Rank constants
|
|
22
65
|
this._ACE = (1n << 52n) | 1n;
|
|
23
66
|
this._KING = 1n << 48n;
|
|
24
67
|
this._QUEEN = 1n << 44n;
|
|
@@ -33,6 +76,7 @@ class BitVal {
|
|
|
33
76
|
this._THREE = 1n << 8n;
|
|
34
77
|
this._TWO = 1n << 4n;
|
|
35
78
|
|
|
79
|
+
// Card masks lookup table
|
|
36
80
|
this.CARD_MASKS = {
|
|
37
81
|
'As': this._ACE << this._SPADE,
|
|
38
82
|
'Ah': this._ACE << this._HEART,
|
|
@@ -90,6 +134,7 @@ class BitVal {
|
|
|
90
134
|
|
|
91
135
|
this.ALL_CARD_MASKS = Object.values(this.CARD_MASKS);
|
|
92
136
|
|
|
137
|
+
// Hand strength score constants
|
|
93
138
|
this.PAIR_SCORE = 1n << 56n;
|
|
94
139
|
this.TWO_PAIRS_SCORE = 1n << 57n;
|
|
95
140
|
this.TRIPS_SCORE = 1n << 58n;
|
|
@@ -99,11 +144,13 @@ class BitVal {
|
|
|
99
144
|
this.QUADS_SCORE = 1n << 62n;
|
|
100
145
|
this.STRAIGHT_FLUSH_SCORE = 1n << 63n;
|
|
101
146
|
|
|
147
|
+
// Bit manipulation constants
|
|
102
148
|
this.BIT_1 = BigInt("0b00010001000100010001000100010001000100010001000100010001");
|
|
103
149
|
this.BIT_2 = BigInt("0b00100010001000100010001000100010001000100010001000100010");
|
|
104
150
|
this.BIT_3 = BigInt("0b01000100010001000100010001000100010001000100010001000100");
|
|
105
151
|
this.BIT_4 = BigInt("0b10001000100010001000100010001000100010001000100010001000");
|
|
106
152
|
|
|
153
|
+
// Rank masks for kicker extraction
|
|
107
154
|
this.RANK_MASKS = [
|
|
108
155
|
240n,
|
|
109
156
|
3840n,
|
|
@@ -138,6 +185,67 @@ class BitVal {
|
|
|
138
185
|
};
|
|
139
186
|
}
|
|
140
187
|
|
|
188
|
+
// ============================================
|
|
189
|
+
// PUBLIC API - Core Hand Evaluation
|
|
190
|
+
// ============================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Evaluates a poker hand represented as a BigInt bitmask.
|
|
194
|
+
* @param {BigInt} handMask - Bitmask representing 5-7 cards
|
|
195
|
+
* @returns {Array} [evaluation, kickers] - Evaluation score and kicker bits
|
|
196
|
+
*/
|
|
197
|
+
evaluate(hand) {
|
|
198
|
+
let response = 0n;
|
|
199
|
+
if (response = this._bitStraightFlush(hand)) {
|
|
200
|
+
return [response | this.STRAIGHT_FLUSH_SCORE, null];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (response = this._bitQuads(hand)) {
|
|
204
|
+
let quadsRanksMask = this._getRankMaskFromBitmask(response);
|
|
205
|
+
let kickers = this._extractKickers(hand, quadsRanksMask, 1);
|
|
206
|
+
return [(response | this.QUADS_SCORE), kickers];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let trips = this._bitTrips(hand) & (0xFFFFFFFFFFFF0n | (1n << 52n));
|
|
210
|
+
let pairs = this._bitPairs(hand) & (0xFFFFFFFFFFFF0n | (1n << 52n));
|
|
211
|
+
|
|
212
|
+
if ((trips && pairs && trips ^ pairs) || this.countBits(trips) > 1) {
|
|
213
|
+
return [trips | this.FULL_HOUSE_SCORE, this.stripBits(pairs & ~trips, 1)];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (response = this._bitFlush(hand)) {
|
|
217
|
+
return [response | this.FLUSH_SCORE, null];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (response = this._bitStraight(hand)) {
|
|
221
|
+
return [response | this.STRAIGHT_SCORE, null];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (response = trips) {
|
|
225
|
+
let tripsRanksMask = this._getRankMaskFromBitmask(response);
|
|
226
|
+
let kickers = this._extractKickers(hand, tripsRanksMask, 2);
|
|
227
|
+
return [(response | this.TRIPS_SCORE), kickers];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (response = pairs) {
|
|
231
|
+
if (this.countBits(response) > 1) {
|
|
232
|
+
let pairRanksMask = this._getTwoPairRanksMask(response);
|
|
233
|
+
let kickers = this._extractKickers(hand, pairRanksMask, 1);
|
|
234
|
+
return [(response | this.TWO_PAIRS_SCORE), kickers];
|
|
235
|
+
}
|
|
236
|
+
let pairRanksMask = this._getRankMaskFromBitmask(response);
|
|
237
|
+
let kickers = this._extractKickers(hand, pairRanksMask, 3);
|
|
238
|
+
return [(response | this.PAIR_SCORE), kickers];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return [this.stripBits(this.normalize(hand), 5), null];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Gets the human-readable hand strength from an evaluation mask.
|
|
246
|
+
* @param {BigInt} handMask - Evaluation mask with score bits
|
|
247
|
+
* @returns {String} Hand strength name
|
|
248
|
+
*/
|
|
141
249
|
getHandStrengthFromMask(handMask) {
|
|
142
250
|
if (handMask & this.PAIR_SCORE) {
|
|
143
251
|
return "Pair";
|
|
@@ -166,6 +274,16 @@ class BitVal {
|
|
|
166
274
|
return "High Card";
|
|
167
275
|
}
|
|
168
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Simulates a single hand vs hand matchup using Monte Carlo or exhaustive enumeration.
|
|
279
|
+
* @param {Number} iterations - Number of simulations
|
|
280
|
+
* @param {Number} numberOfBoardCards - Total board cards (default: 5)
|
|
281
|
+
* @param {Array} hero - Hero's hole cards as array of strings
|
|
282
|
+
* @param {Array} villain - Villain's hole cards as array of strings
|
|
283
|
+
* @param {Array} board - Board cards as array of strings
|
|
284
|
+
* @param {Array} deadCards - Dead cards as array of strings
|
|
285
|
+
* @returns {Object} { win, tie, lose, ...handStrengths } - Results object
|
|
286
|
+
*/
|
|
169
287
|
simulate(iterations, numberOfBoardCards = 5, hero = [], villain = [], board = [], deadCards = []) {
|
|
170
288
|
let numberOfCardsToDeal = (numberOfBoardCards + 2) - (hero.length + board.length);
|
|
171
289
|
this.xorShift = new XorShift32();
|
|
@@ -234,78 +352,38 @@ class BitVal {
|
|
|
234
352
|
return result;
|
|
235
353
|
}
|
|
236
354
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
for (let i = 12n; i >= 0n && kickersFound < numKickers; i--) {
|
|
260
|
-
const rankMask = this.RANK_MASKS[i];
|
|
261
|
-
if (kickersOnly & rankMask) {
|
|
262
|
-
kickerValue |= (1n << (i + 13n));
|
|
263
|
-
kickersFound++;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return kickerValue;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
_getRankMaskFromBitmask(bitmask) {
|
|
271
|
-
return (this.RANK_MASK_LOOKUP[bitmask & 1n] || 0n) |
|
|
272
|
-
(this.RANK_MASK_LOOKUP[bitmask & (1n << 52n)] || 0n) |
|
|
273
|
-
(this.RANK_MASK_LOOKUP[bitmask] || 0n);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
_getTwoPairRanksMask(pairsBitmask) {
|
|
277
|
-
const firstBit = pairsBitmask & -pairsBitmask;
|
|
278
|
-
const firstMask = this._getRankMaskFromBitmask(firstBit);
|
|
279
|
-
|
|
280
|
-
const remaining = pairsBitmask & ~firstBit;
|
|
281
|
-
const secondBit = remaining & -remaining;
|
|
282
|
-
const secondMask = this._getRankMaskFromBitmask(secondBit);
|
|
283
|
-
|
|
284
|
-
return firstMask | secondMask;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
normalize(hand, bitShift = 4) {
|
|
288
|
-
let shift_bits = ((hand | hand >> 1n | hand >> 2n | hand >> 3n) & this.BIT_1) >> 4n;
|
|
289
|
-
let normal_bits = 0n;
|
|
290
|
-
bitShift = BigInt(bitShift);
|
|
291
|
-
|
|
292
|
-
for (let i = 0n; i < 13n; i++) {
|
|
293
|
-
let bit = (shift_bits & (1n << i * bitShift)) >> i * bitShift;
|
|
294
|
-
normal_bits += bit << i;
|
|
295
|
-
}
|
|
296
|
-
return normal_bits;
|
|
355
|
+
// ============================================
|
|
356
|
+
// PUBLIC API - Range Comparison
|
|
357
|
+
// ============================================
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Compares two ranges of hands with optional canonical key caching optimization.
|
|
361
|
+
* @param {Array} heroHands - Array of hero hand strings (e.g., ['AsAh', 'AsAd'])
|
|
362
|
+
* @param {Array} villainHands - Array of villain hand strings
|
|
363
|
+
* @param {Array} boardCards - Board cards as strings (default: [])
|
|
364
|
+
* @param {Array} deadCards - Dead cards as strings (default: [])
|
|
365
|
+
* @param {Number} numberOfBoardCards - Total board cards (default: 5)
|
|
366
|
+
* @param {Number} iterations - Number of simulations per matchup (default: 10000)
|
|
367
|
+
* @param {Boolean} optimize - Use canonical key caching (default: true)
|
|
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)
|
|
371
|
+
* @returns {Promise<Object>} { win, tie, lose } - Results object
|
|
372
|
+
*/
|
|
373
|
+
async compareRange(heroHands, villainHands, boardCards = [], deadCards = [], numberOfBoardCards = 5, iterations = 10000, optimize = true, progressCallback = null, progressInterval = 100, useWorkers = true) {
|
|
374
|
+
const setup = this._compareRangeSetup(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize);
|
|
375
|
+
return optimize ? await this._compareRangeOptimized(setup, progressCallback, progressInterval, useWorkers) : await this._compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback, progressInterval);
|
|
297
376
|
}
|
|
298
377
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
while (numberOfBitsToStrip > 0) {
|
|
303
|
-
hand &= (hand - 1n);
|
|
304
|
-
numberOfBitsToStrip--;
|
|
305
|
-
}
|
|
306
|
-
return hand;
|
|
307
|
-
}
|
|
378
|
+
// ============================================
|
|
379
|
+
// PUBLIC API - Utility Methods
|
|
380
|
+
// ============================================
|
|
308
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Converts an array of card strings to a BigInt bitmask.
|
|
384
|
+
* @param {Array} cards - Array of card strings (e.g., ['As', 'Kh', 'Qd'])
|
|
385
|
+
* @returns {BigInt} Bitmask representing the cards
|
|
386
|
+
*/
|
|
309
387
|
getBitMasked(cards) {
|
|
310
388
|
let sf_mask = 0n;
|
|
311
389
|
|
|
@@ -316,6 +394,11 @@ class BitVal {
|
|
|
316
394
|
return sf_mask;
|
|
317
395
|
}
|
|
318
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Counts the number of set bits in a bitmask.
|
|
399
|
+
* @param {BigInt} handMask - Bitmask to count
|
|
400
|
+
* @returns {Number} Number of set bits
|
|
401
|
+
*/
|
|
319
402
|
countBits(handMask) {
|
|
320
403
|
let count = 0;
|
|
321
404
|
|
|
@@ -326,6 +409,12 @@ class BitVal {
|
|
|
326
409
|
return count;
|
|
327
410
|
}
|
|
328
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Calculates combinations C(n, k).
|
|
414
|
+
* @param {Number} n - Total items
|
|
415
|
+
* @param {Number} k - Items to choose
|
|
416
|
+
* @returns {Number} Number of combinations
|
|
417
|
+
*/
|
|
329
418
|
combinations(n, k) {
|
|
330
419
|
if (k > n || k < 0) return 0;
|
|
331
420
|
if (k === 0 || k === n) return 1;
|
|
@@ -337,81 +426,78 @@ class BitVal {
|
|
|
337
426
|
return Math.floor(result);
|
|
338
427
|
}
|
|
339
428
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return masks;
|
|
352
|
-
}
|
|
429
|
+
/**
|
|
430
|
+
* Normalizes a hand bitmask by extracting rank information.
|
|
431
|
+
* @param {BigInt} hand - Hand bitmask
|
|
432
|
+
* @param {Number} bitShift - Bit shift amount (default: 4)
|
|
433
|
+
* @returns {BigInt} Normalized bitmask
|
|
434
|
+
*/
|
|
435
|
+
normalize(hand, bitShift = 4) {
|
|
436
|
+
let shift_bits = ((hand | hand >> 1n | hand >> 2n | hand >> 3n) & this.BIT_1) >> 4n;
|
|
437
|
+
let normal_bits = 0n;
|
|
438
|
+
bitShift = BigInt(bitShift);
|
|
353
439
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
let combos = [];
|
|
358
|
-
for (let i = 0; i < availableMasks.length; i++) {
|
|
359
|
-
for (let j = i + 1; j < availableMasks.length; j++) {
|
|
360
|
-
combos.push(availableMasks[i] | availableMasks[j]);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return combos;
|
|
440
|
+
for (let i = 0n; i < 13n; i++) {
|
|
441
|
+
let bit = (shift_bits & (1n << i * bitShift)) >> i * bitShift;
|
|
442
|
+
normal_bits += bit << i;
|
|
364
443
|
}
|
|
365
|
-
return
|
|
444
|
+
return normal_bits;
|
|
366
445
|
}
|
|
367
446
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
let kickers = this._extractKickers(hand, quadsRanksMask, 1);
|
|
377
|
-
return [(response | this.QUADS_SCORE), kickers];
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
let trips = this._bitTrips(hand) & (0xFFFFFFFFFFFF0n | (1n << 52n));
|
|
381
|
-
let pairs = this._bitPairs(hand) & (0xFFFFFFFFFFFF0n | (1n << 52n));
|
|
382
|
-
|
|
383
|
-
if ((trips && pairs && trips ^ pairs) || this.countBits(trips) > 1) {
|
|
384
|
-
return [trips | this.FULL_HOUSE_SCORE, this.stripBits(pairs & ~trips, 1)];
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (response = this._bitFlush(hand)) {
|
|
388
|
-
return [response | this.FLUSH_SCORE, null];
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (response = this._bitStraight(hand)) {
|
|
392
|
-
return [response | this.STRAIGHT_SCORE, null];
|
|
393
|
-
}
|
|
447
|
+
/**
|
|
448
|
+
* Strips bits from a hand mask to reduce it to desired size.
|
|
449
|
+
* @param {BigInt} hand - Hand bitmask
|
|
450
|
+
* @param {Number} desiredSizeOfMask - Desired number of bits (default: 5)
|
|
451
|
+
* @returns {BigInt} Stripped bitmask
|
|
452
|
+
*/
|
|
453
|
+
stripBits(hand, desiredSizeOfMask = 5) {
|
|
454
|
+
let numberOfBitsToStrip = this.countBits(hand) - desiredSizeOfMask;
|
|
394
455
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return [(response | this.TRIPS_SCORE), kickers];
|
|
456
|
+
while (numberOfBitsToStrip > 0) {
|
|
457
|
+
hand &= (hand - 1n);
|
|
458
|
+
numberOfBitsToStrip--;
|
|
399
459
|
}
|
|
460
|
+
return hand;
|
|
461
|
+
}
|
|
400
462
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Deals random cards from the deck, excluding dead cards.
|
|
465
|
+
* Optimized version that pre-computes available cards to avoid collision loops.
|
|
466
|
+
* @param {BigInt} hand - Current hand bitmask
|
|
467
|
+
* @param {BigInt} deadCards - Dead cards bitmask (default: 0n)
|
|
468
|
+
* @param {Number} numberOfCards - Number of cards to deal (default: 7)
|
|
469
|
+
* @returns {BigInt} Hand bitmask with new cards added
|
|
470
|
+
*/
|
|
471
|
+
deal(hand, deadCards = 0n, numberOfCards = 7) {
|
|
472
|
+
if (numberOfCards < 1) return hand;
|
|
473
|
+
|
|
474
|
+
// Pre-compute available cards once (major optimization - eliminates do-while collision loop)
|
|
475
|
+
const availableCards = this._getAvailableCardMasksByLookUp(hand | deadCards);
|
|
476
|
+
const availableCount = availableCards.length;
|
|
477
|
+
|
|
478
|
+
if (availableCount < numberOfCards) return hand;
|
|
479
|
+
|
|
480
|
+
// Use Fisher-Yates style selection with direct indexing (much faster than do-while)
|
|
481
|
+
for (let i = 0; i < numberOfCards; i++) {
|
|
482
|
+
const randomIndex = this.xorShift.next(availableCount - i);
|
|
483
|
+
const card = availableCards[randomIndex];
|
|
484
|
+
hand |= card;
|
|
485
|
+
|
|
486
|
+
// Swap selected card with last unselected card (Fisher-Yates shuffle)
|
|
487
|
+
availableCards[randomIndex] = availableCards[availableCount - 1 - i];
|
|
410
488
|
}
|
|
411
489
|
|
|
412
|
-
return
|
|
490
|
+
return hand;
|
|
413
491
|
}
|
|
414
492
|
|
|
493
|
+
// ============================================
|
|
494
|
+
// PRIVATE - Core Bitwise Evaluation Helpers
|
|
495
|
+
// ============================================
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Detects pairs in a hand using bitwise operations.
|
|
499
|
+
* @private
|
|
500
|
+
*/
|
|
415
501
|
_bitPairs(hand) {
|
|
416
502
|
let pairs = (
|
|
417
503
|
((this.BIT_1 & hand) << 1n) & (this.BIT_2 & hand) |
|
|
@@ -424,6 +510,10 @@ class BitVal {
|
|
|
424
510
|
return this.stripBits(pairs, 2);
|
|
425
511
|
}
|
|
426
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Detects trips (three of a kind) in a hand using bitwise operations.
|
|
515
|
+
* @private
|
|
516
|
+
*/
|
|
427
517
|
_bitTrips(hand) {
|
|
428
518
|
let a = (((this.BIT_1 & hand) << 1n & (this.BIT_2 & hand)) << 1n & (this.BIT_3 & hand)) >> 2n;
|
|
429
519
|
let b = (((this.BIT_2 & hand) << 1n & (this.BIT_3 & hand)) << 1n & (this.BIT_4 & hand)) >> 3n;
|
|
@@ -432,6 +522,10 @@ class BitVal {
|
|
|
432
522
|
return this.stripBits((a | b | c | d) & this.BIT_1, 1);
|
|
433
523
|
}
|
|
434
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Detects a flush in a hand using bitwise operations.
|
|
527
|
+
* @private
|
|
528
|
+
*/
|
|
435
529
|
_bitFlush(hand) {
|
|
436
530
|
let i = 0n;
|
|
437
531
|
while (i < 4) {
|
|
@@ -443,6 +537,10 @@ class BitVal {
|
|
|
443
537
|
return 0n;
|
|
444
538
|
}
|
|
445
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Detects a straight in a hand using bitwise operations.
|
|
542
|
+
* @private
|
|
543
|
+
*/
|
|
446
544
|
_bitStraight(hand) {
|
|
447
545
|
hand = (hand | hand >> 1n | hand >> 2n | hand >> 3n) & this.BIT_1;
|
|
448
546
|
|
|
@@ -455,6 +553,10 @@ class BitVal {
|
|
|
455
553
|
return this.stripBits(hand, 1);
|
|
456
554
|
}
|
|
457
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Detects quads (four of a kind) in a hand using bitwise operations.
|
|
558
|
+
* @private
|
|
559
|
+
*/
|
|
458
560
|
_bitQuads(hand) {
|
|
459
561
|
return ((((
|
|
460
562
|
(hand & this.BIT_1) << 1n &
|
|
@@ -464,6 +566,10 @@ class BitVal {
|
|
|
464
566
|
& this.BIT_4) >> 3n;
|
|
465
567
|
}
|
|
466
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Detects a straight flush in a hand using bitwise operations.
|
|
571
|
+
* @private
|
|
572
|
+
*/
|
|
467
573
|
_bitStraightFlush(hand) {
|
|
468
574
|
hand = hand &
|
|
469
575
|
(hand << 4n) &
|
|
@@ -474,61 +580,59 @@ class BitVal {
|
|
|
474
580
|
return this.stripBits(hand, 1);
|
|
475
581
|
}
|
|
476
582
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
583
|
+
/**
|
|
584
|
+
* Extracts kicker bits from a hand given the made hand rank mask.
|
|
585
|
+
* @private
|
|
586
|
+
*/
|
|
587
|
+
_extractKickers(hand, madeHandRanksMask, numKickers) {
|
|
588
|
+
const kickersOnly = hand & ~madeHandRanksMask;
|
|
589
|
+
let kickerValue = 0n;
|
|
590
|
+
let kickersFound = 0;
|
|
591
|
+
|
|
592
|
+
for (let i = 12n; i >= 0n && kickersFound < numKickers; i--) {
|
|
593
|
+
const rankMask = this.RANK_MASKS[i];
|
|
594
|
+
if (kickersOnly & rankMask) {
|
|
595
|
+
kickerValue |= (1n << (i + 13n));
|
|
596
|
+
kickersFound++;
|
|
597
|
+
}
|
|
482
598
|
}
|
|
483
|
-
return suitCounts;
|
|
484
|
-
}
|
|
485
599
|
|
|
486
|
-
|
|
487
|
-
return numberOfBoardCards - boardCards.length;
|
|
600
|
+
return kickerValue;
|
|
488
601
|
}
|
|
489
602
|
|
|
490
|
-
|
|
491
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Gets the rank mask from a bitmask for kicker extraction.
|
|
605
|
+
* @private
|
|
606
|
+
*/
|
|
607
|
+
_getRankMaskFromBitmask(bitmask) {
|
|
608
|
+
return (this.RANK_MASK_LOOKUP[bitmask & 1n] || 0n) |
|
|
609
|
+
(this.RANK_MASK_LOOKUP[bitmask & (1n << 52n)] || 0n) |
|
|
610
|
+
(this.RANK_MASK_LOOKUP[bitmask] || 0n);
|
|
492
611
|
}
|
|
493
612
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
if (r1 === r2) return high + low;
|
|
502
|
-
return high + low + (s1 === s2 ? 's' : 'o');
|
|
503
|
-
}
|
|
504
|
-
if (!boardSuitCounts) boardSuitCounts = this._getBoardSuitCounts(boardCards);
|
|
505
|
-
const cardsToCome = this._getCardsToCome(boardCards, numberOfBoardCards);
|
|
506
|
-
if (r1 === r2) {
|
|
507
|
-
const s1Pot = this._suitHasFlushPotential(s1, boardSuitCounts, cardsToCome);
|
|
508
|
-
const s2Pot = this._suitHasFlushPotential(s2, boardSuitCounts, cardsToCome);
|
|
509
|
-
if (s1Pot) return high + low + s1;
|
|
510
|
-
if (s2Pot) return high + low + s2;
|
|
511
|
-
return high + low;
|
|
512
|
-
}
|
|
513
|
-
const highPot = this._suitHasFlushPotential(highSuit, boardSuitCounts, cardsToCome);
|
|
514
|
-
const lowPot = this._suitHasFlushPotential(lowSuit, boardSuitCounts, cardsToCome);
|
|
515
|
-
return (highPot ? high + highSuit : high) + (lowPot ? low + lowSuit : low);
|
|
516
|
-
}
|
|
613
|
+
/**
|
|
614
|
+
* Gets the rank mask for two pairs.
|
|
615
|
+
* @private
|
|
616
|
+
*/
|
|
617
|
+
_getTwoPairRanksMask(pairsBitmask) {
|
|
618
|
+
const firstBit = pairsBitmask & -pairsBitmask;
|
|
619
|
+
const firstMask = this._getRankMaskFromBitmask(firstBit);
|
|
517
620
|
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const key = this._getCanonicalKey(hand, boardCards, numberOfBoardCards, boardSuitCounts);
|
|
524
|
-
canonicalMap.set(key, (canonicalMap.get(key) || 0) + 1);
|
|
525
|
-
if (!canonicalToHand.has(key)) {
|
|
526
|
-
canonicalToHand.set(key, hand);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return { canonicalMap, canonicalToHand };
|
|
621
|
+
const remaining = pairsBitmask & ~firstBit;
|
|
622
|
+
const secondBit = remaining & -remaining;
|
|
623
|
+
const secondMask = this._getRankMaskFromBitmask(secondBit);
|
|
624
|
+
|
|
625
|
+
return firstMask | secondMask;
|
|
530
626
|
}
|
|
531
627
|
|
|
628
|
+
// ============================================
|
|
629
|
+
// PRIVATE - Range Comparison Helpers
|
|
630
|
+
// ============================================
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Generates all valid matchups between hero and villain hands, excluding overlaps.
|
|
634
|
+
* @private
|
|
635
|
+
*/
|
|
532
636
|
_generateValidMatchups(heroHands, villainHands, boardCards = []) {
|
|
533
637
|
const valid = [];
|
|
534
638
|
const boardMask = boardCards.length > 0 ? this.getBitMasked(boardCards) : 0n;
|
|
@@ -544,6 +648,10 @@ class BitVal {
|
|
|
544
648
|
return valid;
|
|
545
649
|
}
|
|
546
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Calculates valid matchup counts per canonical key pair.
|
|
653
|
+
* @private
|
|
654
|
+
*/
|
|
547
655
|
_calculateValidCounts(validMatchups, hCanon, vCanon, boardCards, numberOfBoardCards) {
|
|
548
656
|
const counts = new Map();
|
|
549
657
|
const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
|
|
@@ -556,147 +664,749 @@ class BitVal {
|
|
|
556
664
|
return counts;
|
|
557
665
|
}
|
|
558
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Sets up range comparison by generating valid matchups and canonical mappings.
|
|
669
|
+
* @private
|
|
670
|
+
*/
|
|
559
671
|
_compareRangeSetup(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize) {
|
|
672
|
+
// Generate valid matchups (always needed)
|
|
560
673
|
const validMatchups = this._generateValidMatchups(heroHands, villainHands, boardCards);
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
674
|
+
|
|
675
|
+
// Calculate canonical mappings only if optimization is enabled
|
|
676
|
+
let hCanon = null;
|
|
677
|
+
let vCanon = null;
|
|
678
|
+
let validCounts = null;
|
|
679
|
+
if (optimize) {
|
|
680
|
+
hCanon = this._flattenHandsToCanonical(heroHands, boardCards, numberOfBoardCards);
|
|
681
|
+
vCanon = this._flattenHandsToCanonical(villainHands, boardCards, numberOfBoardCards);
|
|
682
|
+
validCounts = this._calculateValidCounts(validMatchups, hCanon, vCanon, boardCards, numberOfBoardCards);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Calculate bitmasks
|
|
564
686
|
const boardMask = this.getBitMasked(boardCards);
|
|
565
|
-
const
|
|
566
|
-
|
|
687
|
+
const deadCardsMask = this.getBitMasked([...boardCards, ...deadCards]);
|
|
688
|
+
|
|
689
|
+
// Calculate cards to deal and determine if exhaustive enumeration is possible
|
|
567
690
|
const numberOfCardsToDeal = numberOfBoardCards - boardCards.length;
|
|
568
|
-
const
|
|
569
|
-
|
|
691
|
+
const canBeExhaustive = numberOfCardsToDeal > 0 && numberOfCardsToDeal <= 2;
|
|
692
|
+
|
|
693
|
+
// Calculate available cards using bitmask (more efficient than Set)
|
|
694
|
+
const availableCards = 52 - this.countBits(deadCardsMask);
|
|
695
|
+
|
|
696
|
+
// Determine if we should use exhaustive enumeration
|
|
697
|
+
const exhaustiveCombinations = canBeExhaustive
|
|
698
|
+
? this.combinations(availableCards, numberOfCardsToDeal)
|
|
699
|
+
: Infinity;
|
|
570
700
|
iterations = Math.min(iterations, exhaustiveCombinations);
|
|
571
|
-
const isExhaustive = iterations === exhaustiveCombinations &&
|
|
572
|
-
|
|
573
|
-
|
|
701
|
+
const isExhaustive = canBeExhaustive && iterations === exhaustiveCombinations && exhaustiveCombinations < Infinity;
|
|
702
|
+
|
|
703
|
+
// Generate combo array only if exhaustive
|
|
704
|
+
const comboArray = isExhaustive
|
|
705
|
+
? this._getCombinations(this._getAvailableCardMasksByLookUp(deadCardsMask), numberOfCardsToDeal)
|
|
706
|
+
: null;
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
hCanon,
|
|
710
|
+
vCanon,
|
|
711
|
+
boardMask,
|
|
712
|
+
deadCardsMask,
|
|
713
|
+
iterations,
|
|
714
|
+
isExhaustive,
|
|
715
|
+
comboArray,
|
|
716
|
+
numberOfCardsToDeal,
|
|
717
|
+
validMatchups,
|
|
718
|
+
validCounts,
|
|
719
|
+
boardCards,
|
|
720
|
+
numberOfBoardCards
|
|
721
|
+
};
|
|
574
722
|
}
|
|
575
723
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
return
|
|
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
|
+
};
|
|
583
742
|
}
|
|
584
743
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
};
|
|
589
767
|
}
|
|
590
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
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Evaluates a single matchup with optional caching.
|
|
783
|
+
* Optimized for browser performance with adaptive yielding and inlined hot loop.
|
|
784
|
+
* @private
|
|
785
|
+
*/
|
|
591
786
|
async _evaluateMatchup(heroMask, villainMask, setup, evalCache = null, progressCallback = null, matchupIndex = 0, totalMatchups = 1) {
|
|
592
787
|
let win = 0, tie = 0, lose = 0;
|
|
593
788
|
const deadMask = heroMask | villainMask | setup.deadCardsMask;
|
|
594
|
-
if (!setup.isExhaustive) this.xorShift = new XorShift32();
|
|
595
789
|
|
|
596
|
-
//
|
|
790
|
+
// Initialize random number generator for Monte Carlo (not needed for exhaustive)
|
|
791
|
+
if (!setup.isExhaustive) {
|
|
792
|
+
this.xorShift = new XorShift32();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Prepare for exhaustive enumeration if applicable
|
|
597
796
|
let comboArray = null;
|
|
598
797
|
let iterations = setup.iterations;
|
|
599
798
|
if (setup.isExhaustive) {
|
|
600
|
-
const
|
|
601
|
-
const availableMasks = this._getAvailableCardMasksByLookUp(matchupDeadMask);
|
|
799
|
+
const availableMasks = this._getAvailableCardMasksByLookUp(deadMask);
|
|
602
800
|
comboArray = this._getCombinations(availableMasks, setup.numberOfCardsToDeal);
|
|
603
801
|
iterations = comboArray.length;
|
|
604
802
|
}
|
|
605
803
|
|
|
606
|
-
|
|
804
|
+
// Main evaluation loop - optimized with inlined functions and adaptive yielding
|
|
805
|
+
const yieldInterval = 5000; // Increased from 1000 - reduces async overhead while maintaining responsiveness
|
|
806
|
+
let lastProgressTime = 0;
|
|
807
|
+
const progressUpdateInterval = 100; // Update progress at most every 100ms
|
|
808
|
+
|
|
607
809
|
for (let i = 0; i < iterations; i++) {
|
|
608
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
810
|
+
// Generate board: use pre-computed combo for exhaustive, or deal randomly
|
|
811
|
+
const board = setup.isExhaustive
|
|
812
|
+
? setup.boardMask | comboArray[i]
|
|
813
|
+
: this.deal(setup.boardMask, deadMask, setup.numberOfCardsToDeal) | setup.boardMask;
|
|
814
|
+
|
|
815
|
+
// Evaluate both hands - inlined for performance (no function call overhead)
|
|
816
|
+
let hEval, hKick, vEval, vKick;
|
|
817
|
+
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);
|
|
820
|
+
} else {
|
|
821
|
+
[hEval, hKick] = this.evaluate(heroMask | board);
|
|
822
|
+
[vEval, vKick] = this.evaluate(villainMask | board);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Compare and accumulate results - inlined for performance
|
|
826
|
+
if (hEval > vEval || (hEval === vEval && hKick > vKick)) {
|
|
827
|
+
win++;
|
|
828
|
+
} else if (vEval > hEval || (vEval === hEval && vKick > hKick)) {
|
|
829
|
+
lose++;
|
|
830
|
+
} else {
|
|
831
|
+
tie++;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Adaptive yielding: less frequent but still maintains UI responsiveness
|
|
613
835
|
if (progressCallback && i > 0 && i % yieldInterval === 0) {
|
|
614
|
-
const
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
836
|
+
const now = Date.now();
|
|
837
|
+
const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
|
|
838
|
+
|
|
839
|
+
// Use modern browser APIs for better scheduling when available
|
|
840
|
+
if (typeof scheduler !== 'undefined' && scheduler.postTask) {
|
|
841
|
+
await scheduler.postTask(() => {
|
|
842
|
+
if (shouldUpdateProgress) {
|
|
843
|
+
const total = totalMatchups * iterations;
|
|
844
|
+
const current = matchupIndex * iterations + i;
|
|
845
|
+
progressCallback(current, total, `Matchup ${matchupIndex + 1}/${totalMatchups}: ${i.toLocaleString()}/${iterations.toLocaleString()}`);
|
|
846
|
+
lastProgressTime = now;
|
|
847
|
+
}
|
|
848
|
+
}, { priority: 'user-blocking' });
|
|
849
|
+
} else if (typeof requestIdleCallback !== 'undefined') {
|
|
850
|
+
await new Promise(resolve => {
|
|
851
|
+
requestIdleCallback(() => {
|
|
852
|
+
if (shouldUpdateProgress) {
|
|
853
|
+
const total = totalMatchups * iterations;
|
|
854
|
+
const current = matchupIndex * iterations + i;
|
|
855
|
+
progressCallback(current, total, `Matchup ${matchupIndex + 1}/${totalMatchups}: ${i.toLocaleString()}/${iterations.toLocaleString()}`);
|
|
856
|
+
lastProgressTime = now;
|
|
857
|
+
}
|
|
858
|
+
resolve();
|
|
859
|
+
}, { timeout: 1 });
|
|
860
|
+
});
|
|
861
|
+
} else {
|
|
862
|
+
// Fallback to setTimeout
|
|
863
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
864
|
+
if (shouldUpdateProgress) {
|
|
865
|
+
const total = totalMatchups * iterations;
|
|
866
|
+
const current = matchupIndex * iterations + i;
|
|
867
|
+
progressCallback(current, total, `Matchup ${matchupIndex + 1}/${totalMatchups}: ${i.toLocaleString()}/${iterations.toLocaleString()}`);
|
|
868
|
+
lastProgressTime = now;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
618
871
|
}
|
|
619
872
|
}
|
|
873
|
+
|
|
620
874
|
return { matchupWin: win, matchupTie: tie, matchupLose: lose };
|
|
621
875
|
}
|
|
622
876
|
|
|
623
|
-
|
|
877
|
+
/**
|
|
878
|
+
* Compares ranges without optimization (evaluates all matchups individually).
|
|
879
|
+
* @private
|
|
880
|
+
*/
|
|
881
|
+
async _compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback = null, progressInterval = 100) {
|
|
624
882
|
let win = 0, tie = 0, lose = 0;
|
|
625
883
|
const total = setup.validMatchups.length;
|
|
884
|
+
let lastProgressTime = 0;
|
|
885
|
+
const progressUpdateInterval = 100; // Update progress at most every 100ms
|
|
886
|
+
|
|
626
887
|
for (let i = 0; i < total; i++) {
|
|
627
888
|
const { heroMask, villainMask } = setup.validMatchups[i];
|
|
628
889
|
const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(heroMask, villainMask, setup, null, progressCallback, i, total);
|
|
629
890
|
win += matchupWin; tie += matchupTie; lose += matchupLose;
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
891
|
+
|
|
892
|
+
if (progressCallback && i % progressInterval === 0) {
|
|
893
|
+
const now = Date.now();
|
|
894
|
+
const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
|
|
895
|
+
|
|
896
|
+
// Use modern browser APIs for better scheduling when available
|
|
897
|
+
if (typeof scheduler !== 'undefined' && scheduler.postTask) {
|
|
898
|
+
await scheduler.postTask(() => {
|
|
899
|
+
if (shouldUpdateProgress) {
|
|
900
|
+
progressCallback(i, total, `Evaluating ${i}/${total}`);
|
|
901
|
+
lastProgressTime = now;
|
|
902
|
+
}
|
|
903
|
+
}, { priority: 'user-blocking' });
|
|
904
|
+
} else if (typeof requestIdleCallback !== 'undefined') {
|
|
905
|
+
await new Promise(resolve => {
|
|
906
|
+
requestIdleCallback(() => {
|
|
907
|
+
if (shouldUpdateProgress) {
|
|
908
|
+
progressCallback(i, total, `Evaluating ${i}/${total}`);
|
|
909
|
+
lastProgressTime = now;
|
|
910
|
+
}
|
|
911
|
+
resolve();
|
|
912
|
+
}, { timeout: 1 });
|
|
913
|
+
});
|
|
914
|
+
} else {
|
|
915
|
+
// Fallback to setTimeout
|
|
916
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
917
|
+
if (shouldUpdateProgress) {
|
|
918
|
+
progressCallback(i, total, `Evaluating ${i}/${total}`);
|
|
919
|
+
lastProgressTime = now;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
633
922
|
}
|
|
634
923
|
}
|
|
635
924
|
if (progressCallback) progressCallback(total, total, 'Complete');
|
|
636
925
|
return { win, tie, lose };
|
|
637
926
|
}
|
|
638
927
|
|
|
639
|
-
|
|
928
|
+
/**
|
|
929
|
+
* Compares ranges with canonical key caching optimization (sequential version).
|
|
930
|
+
* @private
|
|
931
|
+
*/
|
|
932
|
+
async _compareRangeOptimizedSequential(setup, progressCallback = null, progressInterval = 100) {
|
|
640
933
|
let win = 0, tie = 0, lose = 0;
|
|
641
|
-
const evalCache = new
|
|
642
|
-
|
|
934
|
+
const evalCache = new FastestAutoClearingCache(16000000);
|
|
935
|
+
let lastProgressTime = 0;
|
|
936
|
+
const progressUpdateInterval = 100; // Update progress at most every 100ms
|
|
937
|
+
|
|
938
|
+
// Pre-calculate board suit counts once (constant for all matchups)
|
|
939
|
+
const boardSuitCounts = setup.boardCards.length > 0
|
|
940
|
+
? this._getBoardSuitCounts(setup.boardCards)
|
|
941
|
+
: null;
|
|
942
|
+
|
|
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();
|
|
947
|
+
for (const matchup of setup.validMatchups) {
|
|
948
|
+
const mHKey = this._getCanonicalKey(
|
|
949
|
+
matchup.heroHand,
|
|
950
|
+
setup.boardCards,
|
|
951
|
+
setup.numberOfBoardCards,
|
|
952
|
+
boardSuitCounts
|
|
953
|
+
);
|
|
954
|
+
const mVKey = this._getCanonicalKey(
|
|
955
|
+
matchup.villainHand,
|
|
956
|
+
setup.boardCards,
|
|
957
|
+
setup.numberOfBoardCards,
|
|
958
|
+
boardSuitCounts
|
|
959
|
+
);
|
|
960
|
+
const key = `${mHKey}:${mVKey}`;
|
|
961
|
+
if (!canonicalToMatchup.has(key)) {
|
|
962
|
+
canonicalToMatchup.set(key, matchup);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
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
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
643
977
|
let matchupIndex = 0;
|
|
644
|
-
for (const [hKey
|
|
645
|
-
const
|
|
646
|
-
const hMask = this.getBitMasked(this._handStringToCards(hHand));
|
|
647
|
-
for (const [vKey, vMult] of setup.vCanon.canonicalMap) {
|
|
648
|
-
const vHand = setup.vCanon.canonicalToHand.get(vKey);
|
|
649
|
-
const vMask = this.getBitMasked(this._handStringToCards(vHand));
|
|
978
|
+
for (const [hKey] of setup.hCanon.canonicalMap) {
|
|
979
|
+
for (const [vKey] of setup.vCanon.canonicalMap) {
|
|
650
980
|
const key = `${hKey}:${vKey}`;
|
|
651
981
|
const validCount = setup.validCounts.get(key) || 0;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const boardSuitCounts = setup.boardCards.length > 0 ? this._getBoardSuitCounts(setup.boardCards) : null;
|
|
658
|
-
const validPair = setup.validMatchups.find(m => {
|
|
659
|
-
const mHKey = this._getCanonicalKey(m.heroHand, setup.boardCards, setup.numberOfBoardCards, boardSuitCounts);
|
|
660
|
-
const mVKey = this._getCanonicalKey(m.villainHand, setup.boardCards, setup.numberOfBoardCards, boardSuitCounts);
|
|
661
|
-
return mHKey === hKey && mVKey === vKey;
|
|
662
|
-
});
|
|
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);
|
|
663
987
|
if (!validPair) {
|
|
664
988
|
console.error(`[DIAG] ERROR: No valid pair found for ${hKey}:${vKey} despite validCount=${validCount}`);
|
|
665
|
-
matchupIndex++;
|
|
666
989
|
continue;
|
|
667
990
|
}
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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;
|
|
1013
|
+
|
|
675
1014
|
matchupIndex++;
|
|
676
|
-
if (progressCallback && matchupIndex %
|
|
677
|
-
|
|
678
|
-
|
|
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));
|
|
1040
|
+
if (shouldUpdateProgress) {
|
|
1041
|
+
progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
|
|
1042
|
+
lastProgressTime = now;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
679
1045
|
}
|
|
680
1046
|
}
|
|
681
1047
|
}
|
|
1048
|
+
|
|
682
1049
|
if (progressCallback) progressCallback(matchupIndex, matchupIndex, 'Complete');
|
|
683
1050
|
return { win, tie, lose };
|
|
684
1051
|
}
|
|
685
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
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Gets cached evaluation or evaluates and caches the result.
|
|
1231
|
+
* @private
|
|
1232
|
+
*/
|
|
1233
|
+
_getCachedEvaluation(canonicalKey, originalHand, completeBoard, evalCache) {
|
|
1234
|
+
const cacheKey = canonicalKey + completeBoard.toString();
|
|
1235
|
+
if (evalCache.has(cacheKey)) return evalCache.get(cacheKey);
|
|
1236
|
+
const handMask = this.getBitMasked(this._handStringToCards(originalHand));
|
|
1237
|
+
const evaluation = this.evaluate(handMask | completeBoard);
|
|
1238
|
+
evalCache.set(cacheKey, evaluation);
|
|
1239
|
+
return evaluation;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Compares two hand evaluations to determine winner.
|
|
1244
|
+
* @private
|
|
1245
|
+
*/
|
|
1246
|
+
_compareEvaluations(heroEval, heroKicker, villainEval, villainKicker) {
|
|
1247
|
+
if (heroEval > villainEval || (heroEval === villainEval && heroKicker > villainKicker)) return 1;
|
|
1248
|
+
if (villainEval > heroEval || (villainEval === heroEval && villainKicker > heroKicker)) return -1;
|
|
1249
|
+
return 0;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Gets canonical key for a hand based on board context (for caching optimization).
|
|
1254
|
+
* @private
|
|
1255
|
+
*/
|
|
1256
|
+
_getCanonicalKey(hand, boardCards = [], numberOfBoardCards = 5, boardSuitCounts = null) {
|
|
1257
|
+
const r1 = hand[0], r2 = hand[2], s1 = hand[1], s2 = hand[3];
|
|
1258
|
+
const rankOrder = 'AKQJT98765432';
|
|
1259
|
+
const r1Idx = rankOrder.indexOf(r1), r2Idx = rankOrder.indexOf(r2);
|
|
1260
|
+
const high = r1Idx < r2Idx ? r1 : r2, low = r1Idx < r2Idx ? r2 : r1;
|
|
1261
|
+
const highSuit = r1Idx < r2Idx ? s1 : s2, lowSuit = r1Idx < r2Idx ? s2 : s1;
|
|
1262
|
+
if (boardCards.length === 0) {
|
|
1263
|
+
if (r1 === r2) return high + low;
|
|
1264
|
+
return high + low + (s1 === s2 ? 's' : 'o');
|
|
1265
|
+
}
|
|
1266
|
+
if (!boardSuitCounts) boardSuitCounts = this._getBoardSuitCounts(boardCards);
|
|
1267
|
+
const cardsToCome = this._getCardsToCome(boardCards, numberOfBoardCards);
|
|
1268
|
+
if (r1 === r2) {
|
|
1269
|
+
const s1Pot = this._suitHasFlushPotential(s1, boardSuitCounts, cardsToCome);
|
|
1270
|
+
const s2Pot = this._suitHasFlushPotential(s2, boardSuitCounts, cardsToCome);
|
|
1271
|
+
if (s1Pot) return high + low + s1;
|
|
1272
|
+
if (s2Pot) return high + low + s2;
|
|
1273
|
+
return high + low;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Postflop non-pairs: use 'x' notation to remove ambiguity
|
|
1277
|
+
const isSuited = s1 === s2;
|
|
1278
|
+
const highPot = this._suitHasFlushPotential(highSuit, boardSuitCounts, cardsToCome);
|
|
1279
|
+
const lowPot = this._suitHasFlushPotential(lowSuit, boardSuitCounts, cardsToCome);
|
|
1280
|
+
|
|
1281
|
+
if (isSuited) {
|
|
1282
|
+
// Both cards same suit
|
|
1283
|
+
if (highPot) {
|
|
1284
|
+
return high + highSuit + low + lowSuit; // e.g., "AsKs" (both spades)
|
|
1285
|
+
} else {
|
|
1286
|
+
return high + low + 's'; // e.g., "AKs" (suited, no flush potential)
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
// Offsuit hands
|
|
1290
|
+
if (highPot && !lowPot) {
|
|
1291
|
+
// Only high has flush potential
|
|
1292
|
+
return high + highSuit + low + 'x'; // e.g., "AsKx" (A is spade, K is not)
|
|
1293
|
+
} else if (!highPot && lowPot) {
|
|
1294
|
+
// Only low has flush potential
|
|
1295
|
+
return high + 'x' + low + lowSuit; // e.g., "AxKs" (K is spade, A is not)
|
|
1296
|
+
} else if (highPot && lowPot) {
|
|
1297
|
+
// Both have flush potential (different suits)
|
|
1298
|
+
return high + highSuit + low + lowSuit; // e.g., "AsKh" (both suits specified, no x)
|
|
1299
|
+
} else {
|
|
1300
|
+
// Neither has flush potential
|
|
1301
|
+
return high + low + 'o'; // e.g., "AKo"
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Flattens hands to canonical keys for caching optimization.
|
|
1308
|
+
* @private
|
|
1309
|
+
*/
|
|
1310
|
+
_flattenHandsToCanonical(hands, boardCards = [], numberOfBoardCards = 5) {
|
|
1311
|
+
const canonicalMap = new Map();
|
|
1312
|
+
const canonicalToHand = new Map();
|
|
1313
|
+
const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
|
|
1314
|
+
for (const hand of hands) {
|
|
1315
|
+
const key = this._getCanonicalKey(hand, boardCards, numberOfBoardCards, boardSuitCounts);
|
|
1316
|
+
canonicalMap.set(key, (canonicalMap.get(key) || 0) + 1);
|
|
1317
|
+
if (!canonicalToHand.has(key)) {
|
|
1318
|
+
canonicalToHand.set(key, hand);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return { canonicalMap, canonicalToHand };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Gets board suit counts for flush potential calculation.
|
|
1326
|
+
* @private
|
|
1327
|
+
*/
|
|
1328
|
+
_getBoardSuitCounts(boardCards) {
|
|
1329
|
+
const suitCounts = new Map();
|
|
1330
|
+
for (const card of boardCards) {
|
|
1331
|
+
const suit = card[1];
|
|
1332
|
+
suitCounts.set(suit, (suitCounts.get(suit) || 0) + 1);
|
|
1333
|
+
}
|
|
1334
|
+
return suitCounts;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Gets number of cards to come.
|
|
1339
|
+
* @private
|
|
1340
|
+
*/
|
|
1341
|
+
_getCardsToCome(boardCards, numberOfBoardCards) {
|
|
1342
|
+
return numberOfBoardCards - boardCards.length;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Checks if a suit has flush potential given board and cards to come.
|
|
1347
|
+
* @private
|
|
1348
|
+
*/
|
|
1349
|
+
_suitHasFlushPotential(suit, boardSuitCounts, cardsToCome) {
|
|
1350
|
+
return (boardSuitCounts.get(suit) || 0) + cardsToCome >= 4;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// ============================================
|
|
1354
|
+
// PRIVATE - Utility Helpers
|
|
1355
|
+
// ============================================
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Converts a hand string to an array of card strings.
|
|
1359
|
+
* @private
|
|
1360
|
+
*/
|
|
686
1361
|
_handStringToCards(hand) {
|
|
687
1362
|
return [hand.substring(0, 2), hand.substring(2, 4)];
|
|
688
1363
|
}
|
|
689
1364
|
|
|
1365
|
+
/**
|
|
1366
|
+
* Gets available card masks by lookup, excluding dead cards.
|
|
1367
|
+
* @private
|
|
1368
|
+
*/
|
|
1369
|
+
_getAvailableCardMasksByLookUp(deadCards) {
|
|
1370
|
+
let masks = [];
|
|
1371
|
+
let deck = ~deadCards;
|
|
1372
|
+
|
|
1373
|
+
for (const card in this.CARD_MASKS) {
|
|
1374
|
+
const cardMask = this.CARD_MASKS[card];
|
|
1375
|
+
if ((cardMask & deck) === cardMask) {
|
|
1376
|
+
masks.push(cardMask);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
return masks;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Gets combinations of available card masks.
|
|
1385
|
+
* @private
|
|
1386
|
+
*/
|
|
1387
|
+
_getCombinations(availableMasks, k) {
|
|
1388
|
+
if (k === 1) return availableMasks;
|
|
1389
|
+
if (k === 2) {
|
|
1390
|
+
let combos = [];
|
|
1391
|
+
for (let i = 0; i < availableMasks.length; i++) {
|
|
1392
|
+
for (let j = i + 1; j < availableMasks.length; j++) {
|
|
1393
|
+
combos.push(availableMasks[i] | availableMasks[j]);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return combos;
|
|
1397
|
+
}
|
|
1398
|
+
return [];
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Simulates a single matchup (legacy method, uses simulate internally).
|
|
1403
|
+
* @private
|
|
1404
|
+
*/
|
|
690
1405
|
_simulateMatchup(heroHand, villainHand, board, deadCards, numberOfBoardCards, iterations) {
|
|
691
1406
|
const heroCards = this._handStringToCards(heroHand);
|
|
692
1407
|
const villainCards = this._handStringToCards(villainHand);
|
|
693
1408
|
return this.simulate(iterations, numberOfBoardCards, heroCards, villainCards, board, deadCards);
|
|
694
1409
|
}
|
|
695
|
-
|
|
696
|
-
async compareRange(heroHands, villainHands, boardCards = [], deadCards = [], numberOfBoardCards = 5, iterations = 10000, optimize = true, progressCallback = null) {
|
|
697
|
-
const setup = this._compareRangeSetup(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize);
|
|
698
|
-
return optimize ? await this._compareRangeOptimized(setup, progressCallback) : await this._compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback);
|
|
699
|
-
}
|
|
700
1410
|
}
|
|
701
1411
|
|
|
702
1412
|
if (typeof module !== 'undefined' && module.exports) {
|