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.
Files changed (2) hide show
  1. package/evaluator.js +207 -8
  2. 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
- // Create a temporary evaluator with the 5-card combination
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
- return result;
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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-extval",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [