poker-extval 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/evaluator.js +207 -8
- package/package.json +1 -1
package/evaluator.js
CHANGED
|
@@ -39,10 +39,11 @@ class HandEvaluator {
|
|
|
39
39
|
* // Non-strict mode - all applicable hands marked
|
|
40
40
|
* const nonStrictEvaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs'], false);
|
|
41
41
|
*/
|
|
42
|
-
constructor(cards, strict = true) {
|
|
42
|
+
constructor(cards, strict = true, originalCards = null) {
|
|
43
43
|
this.#validateCards(cards);
|
|
44
44
|
this.cards = cards;
|
|
45
45
|
this.strict = strict;
|
|
46
|
+
this.originalCards = originalCards || (cards.length > 5 ? cards : null);
|
|
46
47
|
this.evaluation = this.#evaluateAllCombinations();
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -198,6 +199,8 @@ class HandEvaluator {
|
|
|
198
199
|
this.#isOpenEndedStraightDraw();
|
|
199
200
|
this.#isInsideStraightDraw();
|
|
200
201
|
this.#isStraightDraw();
|
|
202
|
+
this.#isComboDraw();
|
|
203
|
+
this.#isUnderPair();
|
|
201
204
|
this.#isHighCard();
|
|
202
205
|
|
|
203
206
|
// Add keyCards and kickerCards before restoring original evaluation
|
|
@@ -275,6 +278,121 @@ class HandEvaluator {
|
|
|
275
278
|
}
|
|
276
279
|
}
|
|
277
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Gets the hand strength value for an evaluation.
|
|
283
|
+
*
|
|
284
|
+
* @private
|
|
285
|
+
* @param {Object} evaluation - The evaluation object
|
|
286
|
+
* @returns {number} Hand strength (10 = Royal Flush, 1 = High Card, 0 = no made hand)
|
|
287
|
+
*/
|
|
288
|
+
#getHandStrength(evaluation) {
|
|
289
|
+
const handStrength = {
|
|
290
|
+
isRoyalFlush: 10, isStraightFlush: 9, isFourOfAKind: 8,
|
|
291
|
+
isFullHouse: 7, isFlush: 6, isStraight: 5, isThreeOfAKind: 4,
|
|
292
|
+
isTwoPair: 3, isPair: 2, isHighCard: 1
|
|
293
|
+
};
|
|
294
|
+
for (const [hand, strength] of Object.entries(handStrength)) {
|
|
295
|
+
if (evaluation[hand]) return strength;
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Gets sorted key card ranks for an evaluation.
|
|
302
|
+
*
|
|
303
|
+
* @private
|
|
304
|
+
* @param {Object} evaluation - The evaluation object
|
|
305
|
+
* @returns {number[]} Array of rank values sorted descending
|
|
306
|
+
*/
|
|
307
|
+
#getKeyCardRanks(evaluation) {
|
|
308
|
+
const handStrength = {
|
|
309
|
+
isRoyalFlush: 10, isStraightFlush: 9, isFourOfAKind: 8,
|
|
310
|
+
isFullHouse: 7, isFlush: 6, isStraight: 5, isThreeOfAKind: 4,
|
|
311
|
+
isTwoPair: 3, isPair: 2, isHighCard: 1
|
|
312
|
+
};
|
|
313
|
+
for (const [hand] of Object.entries(handStrength)) {
|
|
314
|
+
if (evaluation[hand]) {
|
|
315
|
+
const keyCards = evaluation.keyCards[hand] || [];
|
|
316
|
+
return keyCards.map(c => this.#getRankValue(c)).sort((a, b) => b - a);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Gets sorted kicker ranks for an evaluation.
|
|
324
|
+
*
|
|
325
|
+
* @private
|
|
326
|
+
* @param {Object} evaluation - The evaluation object
|
|
327
|
+
* @returns {number[]} Array of rank values sorted descending
|
|
328
|
+
*/
|
|
329
|
+
#getKickerRanks(evaluation) {
|
|
330
|
+
const handStrength = {
|
|
331
|
+
isRoyalFlush: 10, isStraightFlush: 9, isFourOfAKind: 8,
|
|
332
|
+
isFullHouse: 7, isFlush: 6, isStraight: 5, isThreeOfAKind: 4,
|
|
333
|
+
isTwoPair: 3, isPair: 2, isHighCard: 1
|
|
334
|
+
};
|
|
335
|
+
for (const [hand] of Object.entries(handStrength)) {
|
|
336
|
+
if (evaluation[hand]) {
|
|
337
|
+
const kickers = evaluation.kickerCards[hand] || [];
|
|
338
|
+
return kickers.map(c => this.#getRankValue(c)).sort((a, b) => b - a);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Compares two evaluations to determine which is better.
|
|
346
|
+
*
|
|
347
|
+
* @private
|
|
348
|
+
* @param {Object} eval1 - First evaluation object
|
|
349
|
+
* @param {Object} eval2 - Second evaluation object
|
|
350
|
+
* @returns {number} Positive if eval2 is better, negative if eval1 is better, 0 if equal
|
|
351
|
+
*/
|
|
352
|
+
#compareEvaluations(eval1, eval2) {
|
|
353
|
+
const strength1 = this.#getHandStrength(eval1);
|
|
354
|
+
const strength2 = this.#getHandStrength(eval2);
|
|
355
|
+
if (strength1 !== strength2) return strength2 - strength1;
|
|
356
|
+
|
|
357
|
+
const keyRanks1 = this.#getKeyCardRanks(eval1);
|
|
358
|
+
const keyRanks2 = this.#getKeyCardRanks(eval2);
|
|
359
|
+
for (let i = 0; i < Math.max(keyRanks1.length, keyRanks2.length); i++) {
|
|
360
|
+
const rank1 = keyRanks1[i] || 0;
|
|
361
|
+
const rank2 = keyRanks2[i] || 0;
|
|
362
|
+
if (rank1 !== rank2) return rank2 - rank1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const kickerRanks1 = this.#getKickerRanks(eval1);
|
|
366
|
+
const kickerRanks2 = this.#getKickerRanks(eval2);
|
|
367
|
+
for (let i = 0; i < Math.max(kickerRanks1.length, kickerRanks2.length); i++) {
|
|
368
|
+
const rank1 = kickerRanks1[i] || 0;
|
|
369
|
+
const rank2 = kickerRanks2[i] || 0;
|
|
370
|
+
if (rank1 !== rank2) return rank2 - rank1;
|
|
371
|
+
}
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Finds the best combination from a result object.
|
|
377
|
+
*
|
|
378
|
+
* @private
|
|
379
|
+
* @param {Object} result - Object mapping hand keys to evaluation objects
|
|
380
|
+
* @returns {Object} Object with only the best combination
|
|
381
|
+
*/
|
|
382
|
+
#findBestCombination(result) {
|
|
383
|
+
let bestKey = null;
|
|
384
|
+
let bestEval = null;
|
|
385
|
+
for (const [key, evaluation] of Object.entries(result)) {
|
|
386
|
+
const strength = this.#getHandStrength(evaluation);
|
|
387
|
+
const bestStrength = bestEval ? this.#getHandStrength(bestEval) : 0;
|
|
388
|
+
if (!bestEval || strength > bestStrength || (strength === bestStrength && this.#compareEvaluations(evaluation, bestEval) > 0)) {
|
|
389
|
+
bestKey = key;
|
|
390
|
+
bestEval = evaluation;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return bestKey ? { [bestKey]: bestEval } : result;
|
|
394
|
+
}
|
|
395
|
+
|
|
278
396
|
/**
|
|
279
397
|
* Evaluates all possible 5-card combinations from the input cards.
|
|
280
398
|
* For exactly 5 cards, evaluates directly. For more cards, generates all combinations
|
|
@@ -290,26 +408,22 @@ class HandEvaluator {
|
|
|
290
408
|
* // With 7 cards: returns object with 21 key-value pairs (C(7,5) = 21)
|
|
291
409
|
*/
|
|
292
410
|
#evaluateAllCombinations() {
|
|
293
|
-
// If exactly 5 cards, evaluate directly and wrap in the new format
|
|
294
411
|
if (this.cards.length === 5) {
|
|
295
412
|
const key = this.#formatHandKey(this.cards);
|
|
296
413
|
const evaluation = this.#evaluateSingleHand();
|
|
297
414
|
return { [key]: evaluation };
|
|
298
415
|
}
|
|
299
416
|
|
|
300
|
-
// For more than 5 cards, generate all combinations
|
|
301
417
|
const combinations = this.#generateCombinations(this.cards, 5);
|
|
302
418
|
const result = {};
|
|
303
419
|
for (const combination of combinations) {
|
|
304
420
|
const key = this.#formatHandKey(combination);
|
|
305
|
-
|
|
306
|
-
// This will evaluate directly since it's exactly 5 cards
|
|
307
|
-
const tempEvaluator = new HandEvaluator(combination);
|
|
308
|
-
// Extract the evaluation object from the single key-value pair
|
|
421
|
+
const tempEvaluator = new HandEvaluator(combination, this.strict, this.originalCards);
|
|
309
422
|
const evaluationKey = Object.keys(tempEvaluator.evaluation)[0];
|
|
310
423
|
result[key] = tempEvaluator.evaluation[evaluationKey];
|
|
311
424
|
}
|
|
312
|
-
|
|
425
|
+
|
|
426
|
+
return this.strict ? this.#findBestCombination(result) : result;
|
|
313
427
|
}
|
|
314
428
|
|
|
315
429
|
/**
|
|
@@ -1319,6 +1433,33 @@ class HandEvaluator {
|
|
|
1319
1433
|
return this.#getStraightDrawCards();
|
|
1320
1434
|
}
|
|
1321
1435
|
|
|
1436
|
+
/**
|
|
1437
|
+
* Gets key cards for combo draw (flush draw + straight draw).
|
|
1438
|
+
* Returns the union of flush draw and straight draw cards.
|
|
1439
|
+
*
|
|
1440
|
+
* @private
|
|
1441
|
+
* @returns {string[]} Array of cards forming the combo draw, empty array otherwise
|
|
1442
|
+
*/
|
|
1443
|
+
#getKeyCardsForComboDraw() {
|
|
1444
|
+
if (!this.evaluation.isComboDraw) return [];
|
|
1445
|
+
const flushCards = this.#getKeyCardsForFlushDraw();
|
|
1446
|
+
const straightCards = this.#getKeyCardsForStraightDraw();
|
|
1447
|
+
const allCards = [...flushCards, ...straightCards];
|
|
1448
|
+
const uniqueCards = [...new Set(allCards)];
|
|
1449
|
+
return uniqueCards.sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Gets key cards for under pair (the two cards forming the pair).
|
|
1454
|
+
*
|
|
1455
|
+
* @private
|
|
1456
|
+
* @returns {string[]} Array of 2 cards if under pair, empty array otherwise
|
|
1457
|
+
*/
|
|
1458
|
+
#getKeyCardsForUnderPair() {
|
|
1459
|
+
if (!this.evaluation.isUnderPair) return [];
|
|
1460
|
+
return this.#getKeyCardsForPair();
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1322
1463
|
/**
|
|
1323
1464
|
* Gets key cards for wheel straight (5 cards forming wheel).
|
|
1324
1465
|
*
|
|
@@ -1396,6 +1537,8 @@ class HandEvaluator {
|
|
|
1396
1537
|
isStraightWheel: this.#getKeyCardsForStraightWheel(),
|
|
1397
1538
|
isOpenEndedStraightDrawWheel: this.#getKeyCardsForOpenEndedStraightDrawWheel(),
|
|
1398
1539
|
isInsideStraightDrawWheel: this.#getKeyCardsForInsideStraightDrawWheel(),
|
|
1540
|
+
isComboDraw: this.#getKeyCardsForComboDraw(),
|
|
1541
|
+
isUnderPair: this.#getKeyCardsForUnderPair(),
|
|
1399
1542
|
isHighCard: this.#getKeyCardsForHighCard()
|
|
1400
1543
|
};
|
|
1401
1544
|
}
|
|
@@ -1468,6 +1611,62 @@ class HandEvaluator {
|
|
|
1468
1611
|
|
|
1469
1612
|
// ==================== HIGH CARD ====================
|
|
1470
1613
|
|
|
1614
|
+
/**
|
|
1615
|
+
* Determines if the hand has both a flush draw and a straight draw simultaneously.
|
|
1616
|
+
* Requires both isFlushDraw (not backdoor) and isStraightDraw to be true.
|
|
1617
|
+
*
|
|
1618
|
+
* @returns {boolean} True if the hand is a combo draw, false otherwise
|
|
1619
|
+
* @example
|
|
1620
|
+
* const evaluator = new HandEvaluator(['9s', '8s', '7s', '6s', 'Kh']);
|
|
1621
|
+
* evaluator.isComboDraw(); // Returns true (flush draw + straight draw)
|
|
1622
|
+
*/
|
|
1623
|
+
#isComboDraw() {
|
|
1624
|
+
if (this.evaluation.isComboDraw !== undefined) return this.evaluation.isComboDraw;
|
|
1625
|
+
if (this.evaluation.isFlushDraw === undefined) this.#isFlushDraw();
|
|
1626
|
+
if (this.evaluation.isStraightDraw === undefined) this.#isStraightDraw();
|
|
1627
|
+
if (this.evaluation.isBackdoorFlushDraw === undefined) this.#isBackdoorFlushDraw();
|
|
1628
|
+
return this.evaluation.isComboDraw = this.evaluation.isFlushDraw &&
|
|
1629
|
+
this.evaluation.isStraightDraw &&
|
|
1630
|
+
!this.evaluation.isBackdoorFlushDraw;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Determines if the hand contains an under pair (pocket pair smaller than all board cards).
|
|
1635
|
+
* Only applicable when evaluating 6+ cards where the pair rank is smaller than all other cards.
|
|
1636
|
+
*
|
|
1637
|
+
* @returns {boolean} True if the hand is an under pair, false otherwise
|
|
1638
|
+
* @example
|
|
1639
|
+
* // Board: K♠ Q♠ J♠ 8♠, Hand: 77
|
|
1640
|
+
* const evaluator = new HandEvaluator(['Ks', 'Qh', 'Jd', '8c', '7c', '7h']);
|
|
1641
|
+
* evaluator.isUnderPair(); // Returns true (pair of 7s is smaller than all board cards)
|
|
1642
|
+
*/
|
|
1643
|
+
#isUnderPair() {
|
|
1644
|
+
if (this.evaluation.isUnderPair !== undefined) return this.evaluation.isUnderPair;
|
|
1645
|
+
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
1646
|
+
if (!this.evaluation.isPair || !this.originalCards || this.originalCards.length < 6) {
|
|
1647
|
+
return this.evaluation.isUnderPair = false;
|
|
1648
|
+
}
|
|
1649
|
+
const pairRank = this.#getPairRank();
|
|
1650
|
+
if (!pairRank) return this.evaluation.isUnderPair = false;
|
|
1651
|
+
const otherCards = this.originalCards.filter(c => c.slice(0, -1) !== pairRank);
|
|
1652
|
+
const minOtherRank = Math.min(...otherCards.map(c => this.#getRankValue(c)));
|
|
1653
|
+
return this.evaluation.isUnderPair = this.#getRankValue(pairRank + 's') < minOtherRank;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Gets the rank of the pair in the current hand.
|
|
1658
|
+
*
|
|
1659
|
+
* @private
|
|
1660
|
+
* @returns {string|null} The rank of the pair, or null if no pair exists
|
|
1661
|
+
*/
|
|
1662
|
+
#getPairRank() {
|
|
1663
|
+
const rankCounts = this.#getRankCounts();
|
|
1664
|
+
for (const [rank, count] of Object.entries(rankCounts)) {
|
|
1665
|
+
if (count === 2) return rank;
|
|
1666
|
+
}
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1471
1670
|
/**
|
|
1472
1671
|
* Determines if the hand is high card only (no pairs, two pair, three of a kind, full house,
|
|
1473
1672
|
* four of a kind, flush, or straight).
|