poker-bitvaljs 1.0.0 → 1.0.1

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 (2) hide show
  1. package/bitval.js +729 -254
  2. 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,36 @@ class BitVal {
234
352
  return result;
235
353
  }
236
354
 
237
- deal(hand, deadCards = 0n, numberOfCards = 7) {
238
- if (numberOfCards < 1) return hand;
239
- let deck = ~(hand | deadCards);
240
- let card;
241
-
242
- for (let i = 0; i < numberOfCards; i++) {
243
- do {
244
- card = this.ALL_CARD_MASKS[this.xorShift.next(52)];
245
- } while ((card & deck) !== card);
246
-
247
- hand |= card;
248
- deck &= ~card;
249
- }
250
-
251
- return hand;
252
- }
253
-
254
- _extractKickers(hand, madeHandRanksMask, numKickers) {
255
- const kickersOnly = hand & ~madeHandRanksMask;
256
- let kickerValue = 0n;
257
- let kickersFound = 0;
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
+ * @returns {Promise<Object>} { win, tie, lose } - Results object
370
+ */
371
+ async compareRange(heroHands, villainHands, boardCards = [], deadCards = [], numberOfBoardCards = 5, iterations = 10000, optimize = true, progressCallback = null) {
372
+ const setup = this._compareRangeSetup(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize);
373
+ return optimize ? await this._compareRangeOptimized(setup, progressCallback) : await this._compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback);
297
374
  }
298
375
 
299
- stripBits(hand, desiredSizeOfMask = 5) {
300
- let numberOfBitsToStrip = this.countBits(hand) - desiredSizeOfMask;
301
-
302
- while (numberOfBitsToStrip > 0) {
303
- hand &= (hand - 1n);
304
- numberOfBitsToStrip--;
305
- }
306
- return hand;
307
- }
376
+ // ============================================
377
+ // PUBLIC API - Utility Methods
378
+ // ============================================
308
379
 
380
+ /**
381
+ * Converts an array of card strings to a BigInt bitmask.
382
+ * @param {Array} cards - Array of card strings (e.g., ['As', 'Kh', 'Qd'])
383
+ * @returns {BigInt} Bitmask representing the cards
384
+ */
309
385
  getBitMasked(cards) {
310
386
  let sf_mask = 0n;
311
387
 
@@ -316,6 +392,11 @@ class BitVal {
316
392
  return sf_mask;
317
393
  }
318
394
 
395
+ /**
396
+ * Counts the number of set bits in a bitmask.
397
+ * @param {BigInt} handMask - Bitmask to count
398
+ * @returns {Number} Number of set bits
399
+ */
319
400
  countBits(handMask) {
320
401
  let count = 0;
321
402
 
@@ -326,6 +407,12 @@ class BitVal {
326
407
  return count;
327
408
  }
328
409
 
410
+ /**
411
+ * Calculates combinations C(n, k).
412
+ * @param {Number} n - Total items
413
+ * @param {Number} k - Items to choose
414
+ * @returns {Number} Number of combinations
415
+ */
329
416
  combinations(n, k) {
330
417
  if (k > n || k < 0) return 0;
331
418
  if (k === 0 || k === n) return 1;
@@ -337,81 +424,78 @@ class BitVal {
337
424
  return Math.floor(result);
338
425
  }
339
426
 
340
- _getAvailableCardMasksByLookUp(deadCards) {
341
- let masks = [];
342
- let deck = ~deadCards;
343
-
344
- for (const card in this.CARD_MASKS) {
345
- const cardMask = this.CARD_MASKS[card];
346
- if ((cardMask & deck) === cardMask) {
347
- masks.push(cardMask);
348
- }
349
- }
350
-
351
- return masks;
352
- }
427
+ /**
428
+ * Normalizes a hand bitmask by extracting rank information.
429
+ * @param {BigInt} hand - Hand bitmask
430
+ * @param {Number} bitShift - Bit shift amount (default: 4)
431
+ * @returns {BigInt} Normalized bitmask
432
+ */
433
+ normalize(hand, bitShift = 4) {
434
+ let shift_bits = ((hand | hand >> 1n | hand >> 2n | hand >> 3n) & this.BIT_1) >> 4n;
435
+ let normal_bits = 0n;
436
+ bitShift = BigInt(bitShift);
353
437
 
354
- _getCombinations(availableMasks, k) {
355
- if (k === 1) return availableMasks;
356
- if (k === 2) {
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;
438
+ for (let i = 0n; i < 13n; i++) {
439
+ let bit = (shift_bits & (1n << i * bitShift)) >> i * bitShift;
440
+ normal_bits += bit << i;
364
441
  }
365
- return [];
442
+ return normal_bits;
366
443
  }
367
444
 
368
- evaluate(hand) {
369
- let response = 0n;
370
- if (response = this._bitStraightFlush(hand)) {
371
- return [response | this.STRAIGHT_FLUSH_SCORE, null];
372
- }
373
-
374
- if (response = this._bitQuads(hand)) {
375
- let quadsRanksMask = this._getRankMaskFromBitmask(response);
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
- }
445
+ /**
446
+ * Strips bits from a hand mask to reduce it to desired size.
447
+ * @param {BigInt} hand - Hand bitmask
448
+ * @param {Number} desiredSizeOfMask - Desired number of bits (default: 5)
449
+ * @returns {BigInt} Stripped bitmask
450
+ */
451
+ stripBits(hand, desiredSizeOfMask = 5) {
452
+ let numberOfBitsToStrip = this.countBits(hand) - desiredSizeOfMask;
394
453
 
395
- if (response = trips) {
396
- let tripsRanksMask = this._getRankMaskFromBitmask(response);
397
- let kickers = this._extractKickers(hand, tripsRanksMask, 2);
398
- return [(response | this.TRIPS_SCORE), kickers];
454
+ while (numberOfBitsToStrip > 0) {
455
+ hand &= (hand - 1n);
456
+ numberOfBitsToStrip--;
399
457
  }
458
+ return hand;
459
+ }
400
460
 
401
- if (response = pairs) {
402
- if (this.countBits(response) > 1) {
403
- let pairRanksMask = this._getTwoPairRanksMask(response);
404
- let kickers = this._extractKickers(hand, pairRanksMask, 1);
405
- return [(response | this.TWO_PAIRS_SCORE), kickers];
406
- }
407
- let pairRanksMask = this._getRankMaskFromBitmask(response);
408
- let kickers = this._extractKickers(hand, pairRanksMask, 3);
409
- return [(response | this.PAIR_SCORE), kickers];
461
+ /**
462
+ * Deals random cards from the deck, excluding dead cards.
463
+ * Optimized version that pre-computes available cards to avoid collision loops.
464
+ * @param {BigInt} hand - Current hand bitmask
465
+ * @param {BigInt} deadCards - Dead cards bitmask (default: 0n)
466
+ * @param {Number} numberOfCards - Number of cards to deal (default: 7)
467
+ * @returns {BigInt} Hand bitmask with new cards added
468
+ */
469
+ deal(hand, deadCards = 0n, numberOfCards = 7) {
470
+ if (numberOfCards < 1) return hand;
471
+
472
+ // Pre-compute available cards once (major optimization - eliminates do-while collision loop)
473
+ const availableCards = this._getAvailableCardMasksByLookUp(hand | deadCards);
474
+ const availableCount = availableCards.length;
475
+
476
+ if (availableCount < numberOfCards) return hand;
477
+
478
+ // Use Fisher-Yates style selection with direct indexing (much faster than do-while)
479
+ for (let i = 0; i < numberOfCards; i++) {
480
+ const randomIndex = this.xorShift.next(availableCount - i);
481
+ const card = availableCards[randomIndex];
482
+ hand |= card;
483
+
484
+ // Swap selected card with last unselected card (Fisher-Yates shuffle)
485
+ availableCards[randomIndex] = availableCards[availableCount - 1 - i];
410
486
  }
411
487
 
412
- return [this.stripBits(this.normalize(hand), 5), null];
488
+ return hand;
413
489
  }
414
490
 
491
+ // ============================================
492
+ // PRIVATE - Core Bitwise Evaluation Helpers
493
+ // ============================================
494
+
495
+ /**
496
+ * Detects pairs in a hand using bitwise operations.
497
+ * @private
498
+ */
415
499
  _bitPairs(hand) {
416
500
  let pairs = (
417
501
  ((this.BIT_1 & hand) << 1n) & (this.BIT_2 & hand) |
@@ -424,6 +508,10 @@ class BitVal {
424
508
  return this.stripBits(pairs, 2);
425
509
  }
426
510
 
511
+ /**
512
+ * Detects trips (three of a kind) in a hand using bitwise operations.
513
+ * @private
514
+ */
427
515
  _bitTrips(hand) {
428
516
  let a = (((this.BIT_1 & hand) << 1n & (this.BIT_2 & hand)) << 1n & (this.BIT_3 & hand)) >> 2n;
429
517
  let b = (((this.BIT_2 & hand) << 1n & (this.BIT_3 & hand)) << 1n & (this.BIT_4 & hand)) >> 3n;
@@ -432,6 +520,10 @@ class BitVal {
432
520
  return this.stripBits((a | b | c | d) & this.BIT_1, 1);
433
521
  }
434
522
 
523
+ /**
524
+ * Detects a flush in a hand using bitwise operations.
525
+ * @private
526
+ */
435
527
  _bitFlush(hand) {
436
528
  let i = 0n;
437
529
  while (i < 4) {
@@ -443,6 +535,10 @@ class BitVal {
443
535
  return 0n;
444
536
  }
445
537
 
538
+ /**
539
+ * Detects a straight in a hand using bitwise operations.
540
+ * @private
541
+ */
446
542
  _bitStraight(hand) {
447
543
  hand = (hand | hand >> 1n | hand >> 2n | hand >> 3n) & this.BIT_1;
448
544
 
@@ -455,6 +551,10 @@ class BitVal {
455
551
  return this.stripBits(hand, 1);
456
552
  }
457
553
 
554
+ /**
555
+ * Detects quads (four of a kind) in a hand using bitwise operations.
556
+ * @private
557
+ */
458
558
  _bitQuads(hand) {
459
559
  return ((((
460
560
  (hand & this.BIT_1) << 1n &
@@ -464,6 +564,10 @@ class BitVal {
464
564
  & this.BIT_4) >> 3n;
465
565
  }
466
566
 
567
+ /**
568
+ * Detects a straight flush in a hand using bitwise operations.
569
+ * @private
570
+ */
467
571
  _bitStraightFlush(hand) {
468
572
  hand = hand &
469
573
  (hand << 4n) &
@@ -474,61 +578,59 @@ class BitVal {
474
578
  return this.stripBits(hand, 1);
475
579
  }
476
580
 
477
- _getBoardSuitCounts(boardCards) {
478
- const suitCounts = new Map();
479
- for (const card of boardCards) {
480
- const suit = card[1];
481
- suitCounts.set(suit, (suitCounts.get(suit) || 0) + 1);
581
+ /**
582
+ * Extracts kicker bits from a hand given the made hand rank mask.
583
+ * @private
584
+ */
585
+ _extractKickers(hand, madeHandRanksMask, numKickers) {
586
+ const kickersOnly = hand & ~madeHandRanksMask;
587
+ let kickerValue = 0n;
588
+ let kickersFound = 0;
589
+
590
+ for (let i = 12n; i >= 0n && kickersFound < numKickers; i--) {
591
+ const rankMask = this.RANK_MASKS[i];
592
+ if (kickersOnly & rankMask) {
593
+ kickerValue |= (1n << (i + 13n));
594
+ kickersFound++;
595
+ }
482
596
  }
483
- return suitCounts;
484
- }
485
597
 
486
- _getCardsToCome(boardCards, numberOfBoardCards) {
487
- return numberOfBoardCards - boardCards.length;
598
+ return kickerValue;
488
599
  }
489
600
 
490
- _suitHasFlushPotential(suit, boardSuitCounts, cardsToCome) {
491
- return (boardSuitCounts.get(suit) || 0) + cardsToCome >= 4;
601
+ /**
602
+ * Gets the rank mask from a bitmask for kicker extraction.
603
+ * @private
604
+ */
605
+ _getRankMaskFromBitmask(bitmask) {
606
+ return (this.RANK_MASK_LOOKUP[bitmask & 1n] || 0n) |
607
+ (this.RANK_MASK_LOOKUP[bitmask & (1n << 52n)] || 0n) |
608
+ (this.RANK_MASK_LOOKUP[bitmask] || 0n);
492
609
  }
493
610
 
494
- _getCanonicalKey(hand, boardCards = [], numberOfBoardCards = 5, boardSuitCounts = null) {
495
- const r1 = hand[0], r2 = hand[2], s1 = hand[1], s2 = hand[3];
496
- const rankOrder = 'AKQJT98765432';
497
- const r1Idx = rankOrder.indexOf(r1), r2Idx = rankOrder.indexOf(r2);
498
- const high = r1Idx < r2Idx ? r1 : r2, low = r1Idx < r2Idx ? r2 : r1;
499
- const highSuit = r1Idx < r2Idx ? s1 : s2, lowSuit = r1Idx < r2Idx ? s2 : s1;
500
- if (boardCards.length === 0) {
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
- }
611
+ /**
612
+ * Gets the rank mask for two pairs.
613
+ * @private
614
+ */
615
+ _getTwoPairRanksMask(pairsBitmask) {
616
+ const firstBit = pairsBitmask & -pairsBitmask;
617
+ const firstMask = this._getRankMaskFromBitmask(firstBit);
517
618
 
518
- _flattenHandsToCanonical(hands, boardCards = [], numberOfBoardCards = 5) {
519
- const canonicalMap = new Map();
520
- const canonicalToHand = new Map();
521
- const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
522
- for (const hand of hands) {
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 };
619
+ const remaining = pairsBitmask & ~firstBit;
620
+ const secondBit = remaining & -remaining;
621
+ const secondMask = this._getRankMaskFromBitmask(secondBit);
622
+
623
+ return firstMask | secondMask;
530
624
  }
531
625
 
626
+ // ============================================
627
+ // PRIVATE - Range Comparison Helpers
628
+ // ============================================
629
+
630
+ /**
631
+ * Generates all valid matchups between hero and villain hands, excluding overlaps.
632
+ * @private
633
+ */
532
634
  _generateValidMatchups(heroHands, villainHands, boardCards = []) {
533
635
  const valid = [];
534
636
  const boardMask = boardCards.length > 0 ? this.getBitMasked(boardCards) : 0n;
@@ -544,6 +646,10 @@ class BitVal {
544
646
  return valid;
545
647
  }
546
648
 
649
+ /**
650
+ * Calculates valid matchup counts per canonical key pair.
651
+ * @private
652
+ */
547
653
  _calculateValidCounts(validMatchups, hCanon, vCanon, boardCards, numberOfBoardCards) {
548
654
  const counts = new Map();
549
655
  const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
@@ -556,147 +662,516 @@ class BitVal {
556
662
  return counts;
557
663
  }
558
664
 
665
+ /**
666
+ * Sets up range comparison by generating valid matchups and canonical mappings.
667
+ * @private
668
+ */
559
669
  _compareRangeSetup(heroHands, villainHands, boardCards, deadCards, numberOfBoardCards, iterations, optimize) {
670
+ // Generate valid matchups (always needed)
560
671
  const validMatchups = this._generateValidMatchups(heroHands, villainHands, boardCards);
561
- const hCanon = optimize ? this._flattenHandsToCanonical(heroHands, boardCards, numberOfBoardCards) : null;
562
- const vCanon = optimize ? this._flattenHandsToCanonical(villainHands, boardCards, numberOfBoardCards) : null;
563
- const validCounts = optimize ? this._calculateValidCounts(validMatchups, hCanon, vCanon, boardCards, numberOfBoardCards) : null;
672
+
673
+ // Calculate canonical mappings only if optimization is enabled
674
+ let hCanon = null;
675
+ let vCanon = null;
676
+ let validCounts = null;
677
+ if (optimize) {
678
+ hCanon = this._flattenHandsToCanonical(heroHands, boardCards, numberOfBoardCards);
679
+ vCanon = this._flattenHandsToCanonical(villainHands, boardCards, numberOfBoardCards);
680
+ validCounts = this._calculateValidCounts(validMatchups, hCanon, vCanon, boardCards, numberOfBoardCards);
681
+ }
682
+
683
+ // Calculate bitmasks
564
684
  const boardMask = this.getBitMasked(boardCards);
565
- const allDeadCards = [...boardCards, ...deadCards];
566
- const deadCardsMask = this.getBitMasked(allDeadCards);
685
+ const deadCardsMask = this.getBitMasked([...boardCards, ...deadCards]);
686
+
687
+ // Calculate cards to deal and determine if exhaustive enumeration is possible
567
688
  const numberOfCardsToDeal = numberOfBoardCards - boardCards.length;
568
- const availableCards = 52 - new Set(allDeadCards).size;
569
- const exhaustiveCombinations = numberOfCardsToDeal > 0 && numberOfCardsToDeal <= 2 ? this.combinations(availableCards, numberOfCardsToDeal) : Infinity;
689
+ const canBeExhaustive = numberOfCardsToDeal > 0 && numberOfCardsToDeal <= 2;
690
+
691
+ // Calculate available cards using bitmask (more efficient than Set)
692
+ const availableCards = 52 - this.countBits(deadCardsMask);
693
+
694
+ // Determine if we should use exhaustive enumeration
695
+ const exhaustiveCombinations = canBeExhaustive
696
+ ? this.combinations(availableCards, numberOfCardsToDeal)
697
+ : Infinity;
570
698
  iterations = Math.min(iterations, exhaustiveCombinations);
571
- const isExhaustive = iterations === exhaustiveCombinations && numberOfCardsToDeal > 0 && numberOfCardsToDeal <= 2 && exhaustiveCombinations < Infinity;
572
- const comboArray = isExhaustive ? this._getCombinations(this._getAvailableCardMasksByLookUp(deadCardsMask), numberOfCardsToDeal) : null;
573
- return { hCanon, vCanon, boardMask, deadCardsMask, iterations, isExhaustive, comboArray, numberOfCardsToDeal, validMatchups, validCounts, boardCards, numberOfBoardCards };
574
- }
575
-
576
- _getCachedEvaluation(canonicalKey, originalHand, completeBoard, evalCache) {
577
- const cacheKey = canonicalKey + completeBoard.toString();
578
- if (evalCache.has(cacheKey)) return evalCache.get(cacheKey);
579
- const handMask = this.getBitMasked(this._handStringToCards(originalHand));
580
- const evaluation = this.evaluate(handMask | completeBoard);
581
- evalCache.set(cacheKey, evaluation);
582
- return evaluation;
583
- }
584
-
585
- _compareEvaluations(heroEval, heroKicker, villainEval, villainKicker) {
586
- if (heroEval > villainEval || (heroEval === villainEval && heroKicker > villainKicker)) return 1;
587
- if (villainEval > heroEval || (villainEval === heroEval && villainKicker > heroKicker)) return -1;
588
- return 0;
699
+ const isExhaustive = canBeExhaustive && iterations === exhaustiveCombinations && exhaustiveCombinations < Infinity;
700
+
701
+ // Generate combo array only if exhaustive
702
+ const comboArray = isExhaustive
703
+ ? this._getCombinations(this._getAvailableCardMasksByLookUp(deadCardsMask), numberOfCardsToDeal)
704
+ : null;
705
+
706
+ return {
707
+ hCanon,
708
+ vCanon,
709
+ boardMask,
710
+ deadCardsMask,
711
+ iterations,
712
+ isExhaustive,
713
+ comboArray,
714
+ numberOfCardsToDeal,
715
+ validMatchups,
716
+ validCounts,
717
+ boardCards,
718
+ numberOfBoardCards
719
+ };
589
720
  }
590
721
 
722
+ /**
723
+ * Evaluates a single matchup with optional caching.
724
+ * Optimized for browser performance with adaptive yielding and inlined hot loop.
725
+ * @private
726
+ */
591
727
  async _evaluateMatchup(heroMask, villainMask, setup, evalCache = null, progressCallback = null, matchupIndex = 0, totalMatchups = 1) {
592
728
  let win = 0, tie = 0, lose = 0;
593
729
  const deadMask = heroMask | villainMask | setup.deadCardsMask;
594
- if (!setup.isExhaustive) this.xorShift = new XorShift32();
595
730
 
596
- // For exhaustive, generate comboArray excluding THIS matchup's hero/villain cards
731
+ // Initialize random number generator for Monte Carlo (not needed for exhaustive)
732
+ if (!setup.isExhaustive) {
733
+ this.xorShift = new XorShift32();
734
+ }
735
+
736
+ // Prepare for exhaustive enumeration if applicable
597
737
  let comboArray = null;
598
738
  let iterations = setup.iterations;
599
739
  if (setup.isExhaustive) {
600
- const matchupDeadMask = heroMask | villainMask | setup.deadCardsMask;
601
- const availableMasks = this._getAvailableCardMasksByLookUp(matchupDeadMask);
740
+ const availableMasks = this._getAvailableCardMasksByLookUp(deadMask);
602
741
  comboArray = this._getCombinations(availableMasks, setup.numberOfCardsToDeal);
603
742
  iterations = comboArray.length;
604
743
  }
605
744
 
606
- const yieldInterval = 1000;
745
+ // Main evaluation loop - optimized with inlined functions and adaptive yielding
746
+ const yieldInterval = 5000; // Increased from 1000 - reduces async overhead while maintaining responsiveness
747
+ let lastProgressTime = 0;
748
+ const progressUpdateInterval = 100; // Update progress at most every 100ms
749
+
607
750
  for (let i = 0; i < iterations; i++) {
608
- const board = setup.isExhaustive && comboArray ? setup.boardMask | comboArray[i] : this.deal(setup.boardMask, deadMask, setup.numberOfCardsToDeal) | setup.boardMask;
609
- const [hEval, hKick] = evalCache ? this._getCachedEvaluation(evalCache.heroKey, evalCache.heroHand, board, evalCache.cache) : this.evaluate(heroMask | board);
610
- const [vEval, vKick] = evalCache ? this._getCachedEvaluation(evalCache.villainKey, evalCache.villainHand, board, evalCache.cache) : this.evaluate(villainMask | board);
611
- const result = this._compareEvaluations(hEval, hKick, vEval, vKick);
612
- if (result === 1) win++; else if (result === -1) lose++; else tie++;
751
+ // Generate board: use pre-computed combo for exhaustive, or deal randomly
752
+ const board = setup.isExhaustive
753
+ ? setup.boardMask | comboArray[i]
754
+ : this.deal(setup.boardMask, deadMask, setup.numberOfCardsToDeal) | setup.boardMask;
755
+
756
+ // Evaluate both hands - inlined for performance (no function call overhead)
757
+ let hEval, hKick, vEval, vKick;
758
+ if (evalCache) {
759
+ [hEval, hKick] = this._getCachedEvaluation(evalCache.heroKey, evalCache.heroHand, board, evalCache.cache);
760
+ [vEval, vKick] = this._getCachedEvaluation(evalCache.villainKey, evalCache.villainHand, board, evalCache.cache);
761
+ } else {
762
+ [hEval, hKick] = this.evaluate(heroMask | board);
763
+ [vEval, vKick] = this.evaluate(villainMask | board);
764
+ }
765
+
766
+ // Compare and accumulate results - inlined for performance
767
+ if (hEval > vEval || (hEval === vEval && hKick > vKick)) {
768
+ win++;
769
+ } else if (vEval > hEval || (vEval === hEval && vKick > hKick)) {
770
+ lose++;
771
+ } else {
772
+ tie++;
773
+ }
774
+
775
+ // Adaptive yielding: less frequent but still maintains UI responsiveness
613
776
  if (progressCallback && i > 0 && i % yieldInterval === 0) {
614
- const total = totalMatchups * iterations;
615
- const current = matchupIndex * iterations + i;
616
- progressCallback(current, total, `Matchup ${matchupIndex + 1}/${totalMatchups}: ${i.toLocaleString()}/${iterations.toLocaleString()}`);
617
- await new Promise(resolve => setTimeout(resolve, 0));
777
+ const now = Date.now();
778
+ const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
779
+
780
+ // Use modern browser APIs for better scheduling when available
781
+ if (typeof scheduler !== 'undefined' && scheduler.postTask) {
782
+ await scheduler.postTask(() => {
783
+ if (shouldUpdateProgress) {
784
+ const total = totalMatchups * iterations;
785
+ const current = matchupIndex * iterations + i;
786
+ progressCallback(current, total, `Matchup ${matchupIndex + 1}/${totalMatchups}: ${i.toLocaleString()}/${iterations.toLocaleString()}`);
787
+ lastProgressTime = now;
788
+ }
789
+ }, { priority: 'user-blocking' });
790
+ } else if (typeof requestIdleCallback !== 'undefined') {
791
+ await new Promise(resolve => {
792
+ requestIdleCallback(() => {
793
+ if (shouldUpdateProgress) {
794
+ const total = totalMatchups * iterations;
795
+ const current = matchupIndex * iterations + i;
796
+ progressCallback(current, total, `Matchup ${matchupIndex + 1}/${totalMatchups}: ${i.toLocaleString()}/${iterations.toLocaleString()}`);
797
+ lastProgressTime = now;
798
+ }
799
+ resolve();
800
+ }, { timeout: 1 });
801
+ });
802
+ } else {
803
+ // Fallback to setTimeout
804
+ await new Promise(resolve => setTimeout(resolve, 0));
805
+ if (shouldUpdateProgress) {
806
+ const total = totalMatchups * iterations;
807
+ const current = matchupIndex * iterations + i;
808
+ progressCallback(current, total, `Matchup ${matchupIndex + 1}/${totalMatchups}: ${i.toLocaleString()}/${iterations.toLocaleString()}`);
809
+ lastProgressTime = now;
810
+ }
811
+ }
618
812
  }
619
813
  }
814
+
620
815
  return { matchupWin: win, matchupTie: tie, matchupLose: lose };
621
816
  }
622
817
 
818
+ /**
819
+ * Compares ranges without optimization (evaluates all matchups individually).
820
+ * @private
821
+ */
623
822
  async _compareRangeUnoptimized(heroHands, villainHands, setup, progressCallback = null) {
624
823
  let win = 0, tie = 0, lose = 0;
625
824
  const total = setup.validMatchups.length;
825
+ let lastProgressTime = 0;
826
+ const progressUpdateInterval = 100; // Update progress at most every 100ms
827
+
626
828
  for (let i = 0; i < total; i++) {
627
829
  const { heroMask, villainMask } = setup.validMatchups[i];
628
830
  const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(heroMask, villainMask, setup, null, progressCallback, i, total);
629
831
  win += matchupWin; tie += matchupTie; lose += matchupLose;
832
+
630
833
  if (progressCallback && i % 10 === 0) {
631
- progressCallback(i, total, `Evaluating ${i}/${total}`);
632
- await new Promise(resolve => setTimeout(resolve, 0));
834
+ const now = Date.now();
835
+ const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
836
+
837
+ // Use modern browser APIs for better scheduling when available
838
+ if (typeof scheduler !== 'undefined' && scheduler.postTask) {
839
+ await scheduler.postTask(() => {
840
+ if (shouldUpdateProgress) {
841
+ progressCallback(i, total, `Evaluating ${i}/${total}`);
842
+ lastProgressTime = now;
843
+ }
844
+ }, { priority: 'user-blocking' });
845
+ } else if (typeof requestIdleCallback !== 'undefined') {
846
+ await new Promise(resolve => {
847
+ requestIdleCallback(() => {
848
+ if (shouldUpdateProgress) {
849
+ progressCallback(i, total, `Evaluating ${i}/${total}`);
850
+ lastProgressTime = now;
851
+ }
852
+ resolve();
853
+ }, { timeout: 1 });
854
+ });
855
+ } else {
856
+ // Fallback to setTimeout
857
+ await new Promise(resolve => setTimeout(resolve, 0));
858
+ if (shouldUpdateProgress) {
859
+ progressCallback(i, total, `Evaluating ${i}/${total}`);
860
+ lastProgressTime = now;
861
+ }
862
+ }
633
863
  }
634
864
  }
635
865
  if (progressCallback) progressCallback(total, total, 'Complete');
636
866
  return { win, tie, lose };
637
867
  }
638
868
 
869
+ /**
870
+ * Compares ranges with canonical key caching optimization.
871
+ * @private
872
+ */
639
873
  async _compareRangeOptimized(setup, progressCallback = null) {
640
874
  let win = 0, tie = 0, lose = 0;
641
- const evalCache = new Map();
642
- const totalMatchups = setup.hCanon.canonicalMap.size * setup.vCanon.canonicalMap.size;
875
+ const evalCache = new FastestAutoClearingCache(16000000);
876
+ let lastProgressTime = 0;
877
+ const progressUpdateInterval = 100; // Update progress at most every 100ms
878
+
879
+ // Pre-calculate board suit counts once (constant for all matchups)
880
+ const boardSuitCounts = setup.boardCards.length > 0
881
+ ? this._getBoardSuitCounts(setup.boardCards)
882
+ : null;
883
+
884
+ // Pre-build lookup map: canonical key pair -> valid matchup
885
+ // This eliminates the O(n) find() call inside nested loops, reducing complexity
886
+ // from O(heroCanon × villainCanon × validMatchups) to O(validMatchups + heroCanon × villainCanon)
887
+ const canonicalToMatchup = new Map();
888
+ for (const matchup of setup.validMatchups) {
889
+ const mHKey = this._getCanonicalKey(
890
+ matchup.heroHand,
891
+ setup.boardCards,
892
+ setup.numberOfBoardCards,
893
+ boardSuitCounts
894
+ );
895
+ const mVKey = this._getCanonicalKey(
896
+ matchup.villainHand,
897
+ setup.boardCards,
898
+ setup.numberOfBoardCards,
899
+ boardSuitCounts
900
+ );
901
+ const key = `${mHKey}:${mVKey}`;
902
+ if (!canonicalToMatchup.has(key)) {
903
+ canonicalToMatchup.set(key, matchup);
904
+ }
905
+ }
906
+
907
+ // Calculate accurate total matchups (excluding invalid pairs with validCount=0)
908
+ let totalMatchups = 0;
909
+ for (const [hKey] of setup.hCanon.canonicalMap) {
910
+ for (const [vKey] of setup.vCanon.canonicalMap) {
911
+ const key = `${hKey}:${vKey}`;
912
+ if ((setup.validCounts.get(key) || 0) > 0) {
913
+ totalMatchups++;
914
+ }
915
+ }
916
+ }
917
+
643
918
  let matchupIndex = 0;
644
- for (const [hKey, hMult] of setup.hCanon.canonicalMap) {
645
- const hHand = setup.hCanon.canonicalToHand.get(hKey);
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));
919
+ for (const [hKey] of setup.hCanon.canonicalMap) {
920
+ for (const [vKey] of setup.vCanon.canonicalMap) {
650
921
  const key = `${hKey}:${vKey}`;
651
922
  const validCount = setup.validCounts.get(key) || 0;
652
- if (validCount === 0) {
653
- matchupIndex++;
654
- continue;
655
- }
656
- // ALWAYS find a valid pair from validMatchups to ensure no board overlap
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
- });
923
+
924
+ if (validCount === 0) continue;
925
+
926
+ // O(1) lookup instead of O(n) find() - major performance improvement
927
+ const validPair = canonicalToMatchup.get(key);
663
928
  if (!validPair) {
664
929
  console.error(`[DIAG] ERROR: No valid pair found for ${hKey}:${vKey} despite validCount=${validCount}`);
665
- matchupIndex++;
666
930
  continue;
667
931
  }
668
- const evalHand = validPair.heroHand;
669
- const evalVillain = validPair.villainHand;
670
- const evalHMask = validPair.heroMask;
671
- const evalVMask = validPair.villainMask;
672
- const cache = { heroKey: hKey, heroHand: evalHand, villainKey: vKey, villainHand: evalVillain, cache: evalCache };
673
- const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(evalHMask, evalVMask, setup, cache, progressCallback, matchupIndex, totalMatchups);
674
- win += matchupWin * validCount; tie += matchupTie * validCount; lose += matchupLose * validCount;
932
+
933
+ const cache = {
934
+ heroKey: hKey,
935
+ heroHand: validPair.heroHand,
936
+ villainKey: vKey,
937
+ villainHand: validPair.villainHand,
938
+ cache: evalCache
939
+ };
940
+
941
+ const { matchupWin, matchupTie, matchupLose } = await this._evaluateMatchup(
942
+ validPair.heroMask,
943
+ validPair.villainMask,
944
+ setup,
945
+ cache,
946
+ progressCallback,
947
+ matchupIndex,
948
+ totalMatchups
949
+ );
950
+
951
+ win += matchupWin * validCount;
952
+ tie += matchupTie * validCount;
953
+ lose += matchupLose * validCount;
954
+
675
955
  matchupIndex++;
676
956
  if (progressCallback && matchupIndex % 10 === 0) {
677
- progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
678
- await new Promise(resolve => setTimeout(resolve, 0));
957
+ const now = Date.now();
958
+ const shouldUpdateProgress = now - lastProgressTime >= progressUpdateInterval;
959
+
960
+ // Use modern browser APIs for better scheduling when available
961
+ if (typeof scheduler !== 'undefined' && scheduler.postTask) {
962
+ await scheduler.postTask(() => {
963
+ if (shouldUpdateProgress) {
964
+ progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
965
+ lastProgressTime = now;
966
+ }
967
+ }, { priority: 'user-blocking' });
968
+ } else if (typeof requestIdleCallback !== 'undefined') {
969
+ await new Promise(resolve => {
970
+ requestIdleCallback(() => {
971
+ if (shouldUpdateProgress) {
972
+ progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
973
+ lastProgressTime = now;
974
+ }
975
+ resolve();
976
+ }, { timeout: 1 });
977
+ });
978
+ } else {
979
+ // Fallback to setTimeout
980
+ await new Promise(resolve => setTimeout(resolve, 0));
981
+ if (shouldUpdateProgress) {
982
+ progressCallback(matchupIndex, totalMatchups, `Evaluating ${matchupIndex}`);
983
+ lastProgressTime = now;
984
+ }
985
+ }
679
986
  }
680
987
  }
681
988
  }
989
+
682
990
  if (progressCallback) progressCallback(matchupIndex, matchupIndex, 'Complete');
683
991
  return { win, tie, lose };
684
992
  }
685
993
 
994
+ /**
995
+ * Gets cached evaluation or evaluates and caches the result.
996
+ * @private
997
+ */
998
+ _getCachedEvaluation(canonicalKey, originalHand, completeBoard, evalCache) {
999
+ const cacheKey = canonicalKey + completeBoard.toString();
1000
+ if (evalCache.has(cacheKey)) return evalCache.get(cacheKey);
1001
+ const handMask = this.getBitMasked(this._handStringToCards(originalHand));
1002
+ const evaluation = this.evaluate(handMask | completeBoard);
1003
+ evalCache.set(cacheKey, evaluation);
1004
+ return evaluation;
1005
+ }
1006
+
1007
+ /**
1008
+ * Compares two hand evaluations to determine winner.
1009
+ * @private
1010
+ */
1011
+ _compareEvaluations(heroEval, heroKicker, villainEval, villainKicker) {
1012
+ if (heroEval > villainEval || (heroEval === villainEval && heroKicker > villainKicker)) return 1;
1013
+ if (villainEval > heroEval || (villainEval === heroEval && villainKicker > heroKicker)) return -1;
1014
+ return 0;
1015
+ }
1016
+
1017
+ /**
1018
+ * Gets canonical key for a hand based on board context (for caching optimization).
1019
+ * @private
1020
+ */
1021
+ _getCanonicalKey(hand, boardCards = [], numberOfBoardCards = 5, boardSuitCounts = null) {
1022
+ const r1 = hand[0], r2 = hand[2], s1 = hand[1], s2 = hand[3];
1023
+ const rankOrder = 'AKQJT98765432';
1024
+ const r1Idx = rankOrder.indexOf(r1), r2Idx = rankOrder.indexOf(r2);
1025
+ const high = r1Idx < r2Idx ? r1 : r2, low = r1Idx < r2Idx ? r2 : r1;
1026
+ const highSuit = r1Idx < r2Idx ? s1 : s2, lowSuit = r1Idx < r2Idx ? s2 : s1;
1027
+ if (boardCards.length === 0) {
1028
+ if (r1 === r2) return high + low;
1029
+ return high + low + (s1 === s2 ? 's' : 'o');
1030
+ }
1031
+ if (!boardSuitCounts) boardSuitCounts = this._getBoardSuitCounts(boardCards);
1032
+ const cardsToCome = this._getCardsToCome(boardCards, numberOfBoardCards);
1033
+ if (r1 === r2) {
1034
+ const s1Pot = this._suitHasFlushPotential(s1, boardSuitCounts, cardsToCome);
1035
+ const s2Pot = this._suitHasFlushPotential(s2, boardSuitCounts, cardsToCome);
1036
+ if (s1Pot) return high + low + s1;
1037
+ if (s2Pot) return high + low + s2;
1038
+ return high + low;
1039
+ }
1040
+
1041
+ // Postflop non-pairs: use 'x' notation to remove ambiguity
1042
+ const isSuited = s1 === s2;
1043
+ const highPot = this._suitHasFlushPotential(highSuit, boardSuitCounts, cardsToCome);
1044
+ const lowPot = this._suitHasFlushPotential(lowSuit, boardSuitCounts, cardsToCome);
1045
+
1046
+ if (isSuited) {
1047
+ // Both cards same suit
1048
+ if (highPot) {
1049
+ return high + highSuit + low + lowSuit; // e.g., "AsKs" (both spades)
1050
+ } else {
1051
+ return high + low + 's'; // e.g., "AKs" (suited, no flush potential)
1052
+ }
1053
+ } else {
1054
+ // Offsuit hands
1055
+ if (highPot && !lowPot) {
1056
+ // Only high has flush potential
1057
+ return high + highSuit + low + 'x'; // e.g., "AsKx" (A is spade, K is not)
1058
+ } else if (!highPot && lowPot) {
1059
+ // Only low has flush potential
1060
+ return high + 'x' + low + lowSuit; // e.g., "AxKs" (K is spade, A is not)
1061
+ } else if (highPot && lowPot) {
1062
+ // Both have flush potential (different suits)
1063
+ return high + highSuit + low + lowSuit; // e.g., "AsKh" (both suits specified, no x)
1064
+ } else {
1065
+ // Neither has flush potential
1066
+ return high + low + 'o'; // e.g., "AKo"
1067
+ }
1068
+ }
1069
+ }
1070
+
1071
+ /**
1072
+ * Flattens hands to canonical keys for caching optimization.
1073
+ * @private
1074
+ */
1075
+ _flattenHandsToCanonical(hands, boardCards = [], numberOfBoardCards = 5) {
1076
+ const canonicalMap = new Map();
1077
+ const canonicalToHand = new Map();
1078
+ const boardSuitCounts = boardCards.length > 0 ? this._getBoardSuitCounts(boardCards) : null;
1079
+ for (const hand of hands) {
1080
+ const key = this._getCanonicalKey(hand, boardCards, numberOfBoardCards, boardSuitCounts);
1081
+ canonicalMap.set(key, (canonicalMap.get(key) || 0) + 1);
1082
+ if (!canonicalToHand.has(key)) {
1083
+ canonicalToHand.set(key, hand);
1084
+ }
1085
+ }
1086
+ return { canonicalMap, canonicalToHand };
1087
+ }
1088
+
1089
+ /**
1090
+ * Gets board suit counts for flush potential calculation.
1091
+ * @private
1092
+ */
1093
+ _getBoardSuitCounts(boardCards) {
1094
+ const suitCounts = new Map();
1095
+ for (const card of boardCards) {
1096
+ const suit = card[1];
1097
+ suitCounts.set(suit, (suitCounts.get(suit) || 0) + 1);
1098
+ }
1099
+ return suitCounts;
1100
+ }
1101
+
1102
+ /**
1103
+ * Gets number of cards to come.
1104
+ * @private
1105
+ */
1106
+ _getCardsToCome(boardCards, numberOfBoardCards) {
1107
+ return numberOfBoardCards - boardCards.length;
1108
+ }
1109
+
1110
+ /**
1111
+ * Checks if a suit has flush potential given board and cards to come.
1112
+ * @private
1113
+ */
1114
+ _suitHasFlushPotential(suit, boardSuitCounts, cardsToCome) {
1115
+ return (boardSuitCounts.get(suit) || 0) + cardsToCome >= 4;
1116
+ }
1117
+
1118
+ // ============================================
1119
+ // PRIVATE - Utility Helpers
1120
+ // ============================================
1121
+
1122
+ /**
1123
+ * Converts a hand string to an array of card strings.
1124
+ * @private
1125
+ */
686
1126
  _handStringToCards(hand) {
687
1127
  return [hand.substring(0, 2), hand.substring(2, 4)];
688
1128
  }
689
1129
 
1130
+ /**
1131
+ * Gets available card masks by lookup, excluding dead cards.
1132
+ * @private
1133
+ */
1134
+ _getAvailableCardMasksByLookUp(deadCards) {
1135
+ let masks = [];
1136
+ let deck = ~deadCards;
1137
+
1138
+ for (const card in this.CARD_MASKS) {
1139
+ const cardMask = this.CARD_MASKS[card];
1140
+ if ((cardMask & deck) === cardMask) {
1141
+ masks.push(cardMask);
1142
+ }
1143
+ }
1144
+
1145
+ return masks;
1146
+ }
1147
+
1148
+ /**
1149
+ * Gets combinations of available card masks.
1150
+ * @private
1151
+ */
1152
+ _getCombinations(availableMasks, k) {
1153
+ if (k === 1) return availableMasks;
1154
+ if (k === 2) {
1155
+ let combos = [];
1156
+ for (let i = 0; i < availableMasks.length; i++) {
1157
+ for (let j = i + 1; j < availableMasks.length; j++) {
1158
+ combos.push(availableMasks[i] | availableMasks[j]);
1159
+ }
1160
+ }
1161
+ return combos;
1162
+ }
1163
+ return [];
1164
+ }
1165
+
1166
+ /**
1167
+ * Simulates a single matchup (legacy method, uses simulate internally).
1168
+ * @private
1169
+ */
690
1170
  _simulateMatchup(heroHand, villainHand, board, deadCards, numberOfBoardCards, iterations) {
691
1171
  const heroCards = this._handStringToCards(heroHand);
692
1172
  const villainCards = this._handStringToCards(villainHand);
693
1173
  return this.simulate(iterations, numberOfBoardCards, heroCards, villainCards, board, deadCards);
694
1174
  }
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
1175
  }
701
1176
 
702
1177
  if (typeof module !== 'undefined' && module.exports) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-bitvaljs",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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": {