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.
- package/bitval.js +729 -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,36 @@ 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
|
-
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
487
|
-
return numberOfBoardCards - boardCards.length;
|
|
598
|
+
return kickerValue;
|
|
488
599
|
}
|
|
489
600
|
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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 };
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
|
566
|
-
|
|
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
|
|
569
|
-
|
|
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 &&
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
632
|
-
|
|
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
|
|
642
|
-
|
|
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
|
|
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));
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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.
|
|
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": {
|