poker-extval 3.3.3 → 3.3.4

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 (3) hide show
  1. package/README.md +5 -0
  2. package/evaluator.js +138 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -126,6 +126,9 @@ const result = evaluateHand(['As', 'Ah', 'Kd', 'Kc', 'Qs'], false);
126
126
  - Descriptive labels (isTopPair, etc.) are only true if they match the strongest hand
127
127
  - Made flush or stronger cannot have straight draws (flush > straight)
128
128
  - Draws can coexist when no made hand exists (flush draw + straight draw is valid)
129
+ - With 6 or more cards, draws are collected from **every** 5-card combination, not just the best one, so a draw using a card outside the best five is still reported. Straight draws are dropped once a straight or better is made, and flush draws once a flush or better is made. `isComboDraw` is true when the whole hand has both a flush draw and a straight draw.
130
+
131
+ **Draws by street**: the number of cards sets the street. With 5 cards (the flop) every draw is possible; with 6 (the turn) there is no `isBackdoorFlushDraw`, since one card to come cannot complete it; with 7 (the river) there are no draws at all. This applies in both modes.
129
132
 
130
133
  ### Evaluating Multiple Card Hands
131
134
 
@@ -191,6 +194,8 @@ The `evaluateHand()` function returns an object where:
191
194
  - `isMiddleThreeOfAKind` - Three of a kind is neither highest nor lowest
192
195
  - `isBottomThreeOfAKind` - Three of a kind is the lowest rank
193
196
 
197
+ **Note**: With `holeCards`, pair and three of a kind positions are judged against the **whole board**, including board cards left out of the 5-card combination: top means at or above the highest board card (so a pocket pair above the board is top pair), bottom means at or below the lowest, middle is anything between. A pair or three of a kind made only of board cards gets no position. Without `holeCards` the board is unknown, so positions are judged within the 5-card combination. Two pair positions are always judged within the 5-card combination.
198
+
194
199
  #### Overcard Categorization (requires `holeCards`; not filtered by strict mode)
195
200
  - `isOvercard` - At least one hole card ranks above every board card
196
201
  - `isTwoOvercards` - Both hole cards rank above every board card
package/evaluator.js CHANGED
@@ -36,6 +36,31 @@ const HOLE_CARD_CATEGORIES = new Set([
36
36
  'isTwoUndercards'
37
37
  ]);
38
38
 
39
+ /**
40
+ * Draw properties. On 6+ cards they describe the whole hand rather than one 5-card combination,
41
+ * so strict mode collects them from every combination (see #mergeDraws).
42
+ *
43
+ * @constant {string[]}
44
+ */
45
+ const DRAW_PROPERTIES = [
46
+ 'isFlushDraw',
47
+ 'isBackdoorFlushDraw',
48
+ 'isOpenEndedStraightDraw',
49
+ 'isInsideStraightDraw',
50
+ 'isStraightDraw',
51
+ 'isOpenEndedStraightDrawWheel',
52
+ 'isInsideStraightDrawWheel',
53
+ 'isComboDraw'
54
+ ];
55
+
56
+ const STRAIGHT_DRAW_PROPERTIES = [
57
+ 'isOpenEndedStraightDraw',
58
+ 'isInsideStraightDraw',
59
+ 'isStraightDraw',
60
+ 'isOpenEndedStraightDrawWheel',
61
+ 'isInsideStraightDrawWheel'
62
+ ];
63
+
39
64
  class HandEvaluator {
40
65
  /**
41
66
  * Creates an instance of HandEvaluator.
@@ -272,6 +297,7 @@ class HandEvaluator {
272
297
  this.#isInsideStraightDraw();
273
298
  this.#isStraightDraw();
274
299
  this.#isComboDraw();
300
+ this.#clearDrawsForStreet();
275
301
  this.#isUnderPair();
276
302
  this.#isOvercards();
277
303
  this.#isHighCard();
@@ -501,7 +527,70 @@ class HandEvaluator {
501
527
  result[key] = tempEvaluator.evaluation[evaluationKey];
502
528
  }
503
529
 
504
- return this.strict ? this.#findBestCombination(result) : result;
530
+ if (!this.strict) return result;
531
+ const best = this.#findBestCombination(result);
532
+ this.#mergeDraws(Object.values(best)[0], result);
533
+ return best;
534
+ }
535
+
536
+ /**
537
+ * Removes draws that can no longer complete, based on how many cards the full hand holds:
538
+ * 5 cards is the flop (two cards to come), 6 is the turn (one card to come, so a backdoor
539
+ * draw cannot complete) and 7 or more is the river (no cards to come, so there are no draws).
540
+ *
541
+ * @private
542
+ */
543
+ #clearDrawsForStreet() {
544
+ const cardCount = (this.originalCards || this.cards).length;
545
+ if (cardCount >= 7) {
546
+ for (const draw of DRAW_PROPERTIES) this.evaluation[draw] = false;
547
+ } else if (cardCount === 6) {
548
+ this.evaluation.isBackdoorFlushDraw = false;
549
+ }
550
+ }
551
+
552
+ /**
553
+ * In strict mode with 6+ cards only the best 5-card combination is returned, but a draw can
554
+ * use cards outside it (e.g. the hole card that makes four to a flush). Copies onto the best
555
+ * evaluation every draw found in any combination, with that combination's key and kicker
556
+ * cards, then drops draws the made hand already beats and recomputes the combo draw for the
557
+ * whole hand.
558
+ *
559
+ * @private
560
+ * @param {Object} best - The evaluation of the best combination (modified in place)
561
+ * @param {Object<string, Object>} result - Evaluations of every combination
562
+ */
563
+ #mergeDraws(best, result) {
564
+ const clear = draw => {
565
+ best[draw] = false;
566
+ best.keyCards[draw] = [];
567
+ best.kickerCards[draw] = [];
568
+ };
569
+ for (const draw of DRAW_PROPERTIES) {
570
+ if (draw === 'isComboDraw' || best[draw]) continue;
571
+ const source = Object.values(result).find(evaluation => evaluation[draw]);
572
+ if (!source) continue;
573
+ best[draw] = true;
574
+ best.keyCards[draw] = source.keyCards[draw];
575
+ best.kickerCards[draw] = source.kickerCards[draw];
576
+ }
577
+
578
+ // A made straight or better needs no straight draw; a made flush or better needs no flush draw
579
+ const strongest = this.#getHandStrength(best);
580
+ if (strongest >= 5) STRAIGHT_DRAW_PROPERTIES.forEach(clear);
581
+ if (strongest >= 6) ['isFlushDraw', 'isBackdoorFlushDraw'].forEach(clear);
582
+
583
+ if (best.isComboDraw || !(best.isFlushDraw && best.isStraightDraw)) {
584
+ if (!(best.isFlushDraw && best.isStraightDraw)) clear('isComboDraw');
585
+ return;
586
+ }
587
+ best.isComboDraw = true;
588
+ const keyCards = [...new Set([...best.keyCards.isFlushDraw, ...best.keyCards.isStraightDraw])]
589
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
590
+ best.keyCards.isComboDraw = keyCards;
591
+ best.kickerCards.isComboDraw = this.cards
592
+ .filter(c => !keyCards.includes(c))
593
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
505
594
  }
506
595
 
507
596
  /**
@@ -797,6 +886,8 @@ class HandEvaluator {
797
886
  if (this.evaluation.isTopPair !== undefined) return this.evaluation.isTopPair;
798
887
  if (this.evaluation.isPair === undefined) this.#isPair();
799
888
  if (!this.evaluation.isPair) return this.evaluation.isTopPair = false;
889
+ const position = this.#getBoardPosition(this.#getPairRank());
890
+ if (position !== null) return this.evaluation.isTopPair = position === 'top';
800
891
  const rankCounts = this.#getRankCounts();
801
892
  return this.evaluation.isTopPair = rankCounts[this.cards[0].slice(0, -1)] === 2;
802
893
  }
@@ -815,6 +906,8 @@ class HandEvaluator {
815
906
  if (this.evaluation.isPair === undefined) this.#isPair();
816
907
  if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
817
908
  if (!this.evaluation.isPair || this.evaluation.isTwoPair) return this.evaluation.isMiddlePair = false;
909
+ const position = this.#getBoardPosition(this.#getPairRank());
910
+ if (position !== null) return this.evaluation.isMiddlePair = position === 'middle';
818
911
  const ranks = this.#getRankCountsSorted();
819
912
  return this.evaluation.isMiddlePair = ranks.length === 4 && (ranks[1].count === 2 || ranks[2].count === 2);
820
913
  }
@@ -832,6 +925,8 @@ class HandEvaluator {
832
925
  if (this.evaluation.isBottomPair !== undefined) return this.evaluation.isBottomPair;
833
926
  if (this.evaluation.isPair === undefined) this.#isPair();
834
927
  if (!this.evaluation.isPair) return this.evaluation.isBottomPair = false;
928
+ const position = this.#getBoardPosition(this.#getPairRank());
929
+ if (position !== null) return this.evaluation.isBottomPair = position === 'bottom';
835
930
  const rankCounts = this.#getRankCounts();
836
931
  return this.evaluation.isBottomPair = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 2;
837
932
  }
@@ -904,6 +999,8 @@ class HandEvaluator {
904
999
  if (this.evaluation.isTopThreeOfAKind !== undefined) return this.evaluation.isTopThreeOfAKind;
905
1000
  if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
906
1001
  if (!this.evaluation.isThreeOfAKind) return this.evaluation.isTopThreeOfAKind = false;
1002
+ const position = this.#getBoardPosition(this.#getTripsRank());
1003
+ if (position !== null) return this.evaluation.isTopThreeOfAKind = position === 'top';
907
1004
  const rankCounts = this.#getRankCounts();
908
1005
  return this.evaluation.isTopThreeOfAKind = rankCounts[this.cards[0].slice(0, -1)] === 3;
909
1006
  }
@@ -921,6 +1018,8 @@ class HandEvaluator {
921
1018
  if (this.evaluation.isMiddleThreeOfAKind !== undefined) return this.evaluation.isMiddleThreeOfAKind;
922
1019
  if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
923
1020
  if (!this.evaluation.isThreeOfAKind) return this.evaluation.isMiddleThreeOfAKind = false;
1021
+ const position = this.#getBoardPosition(this.#getTripsRank());
1022
+ if (position !== null) return this.evaluation.isMiddleThreeOfAKind = position === 'middle';
924
1023
  const ranks = this.#getRankCountsSorted();
925
1024
  return this.evaluation.isMiddleThreeOfAKind = ranks.length === 3 && ranks[1].count === 3;
926
1025
  }
@@ -938,6 +1037,8 @@ class HandEvaluator {
938
1037
  if (this.evaluation.isBottomThreeOfAKind !== undefined) return this.evaluation.isBottomThreeOfAKind;
939
1038
  if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
940
1039
  if (!this.evaluation.isThreeOfAKind) return this.evaluation.isBottomThreeOfAKind = false;
1040
+ const position = this.#getBoardPosition(this.#getTripsRank());
1041
+ if (position !== null) return this.evaluation.isBottomThreeOfAKind = position === 'bottom';
941
1042
  const rankCounts = this.#getRankCounts();
942
1043
  return this.evaluation.isBottomThreeOfAKind = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 3;
943
1044
  }
@@ -1851,6 +1952,42 @@ class HandEvaluator {
1851
1952
  }
1852
1953
  return null;
1853
1954
  }
1955
+
1956
+ /**
1957
+ * Gets the rank of the three of a kind in the current hand.
1958
+ *
1959
+ * @private
1960
+ * @returns {string|null} The rank of the three of a kind, or null if none exists
1961
+ */
1962
+ #getTripsRank() {
1963
+ const rankCounts = this.#getRankCounts();
1964
+ return Object.keys(rankCounts).find(rank => rankCounts[rank] === 3) || null;
1965
+ }
1966
+
1967
+ /**
1968
+ * Classifies a paired (or tripled) rank against the whole board, including board cards outside
1969
+ * the current 5-card combination. Requires hole cards: without them the board is unknown and
1970
+ * positions fall back to the 5-card combination.
1971
+ *
1972
+ * - 'top': the rank is at or above the highest board card (top pair, or a pocket pair above the board)
1973
+ * - 'bottom': the rank is at or below the lowest board card
1974
+ * - 'middle': anything in between
1975
+ * - 'none': no hole card has this rank, so the pair or trips belongs to the board
1976
+ *
1977
+ * @private
1978
+ * @param {string|null} rank - The rank to classify
1979
+ * @returns {string|null} The position, or null if no hole cards were supplied
1980
+ */
1981
+ #getBoardPosition(rank) {
1982
+ if (!this.holeCards) return null;
1983
+ if (!rank || !this.holeCards.some(c => c.slice(0, -1) === rank)) return 'none';
1984
+ const fullHand = this.originalCards || this.cards;
1985
+ const boardValues = fullHand.filter(c => !this.holeCards.includes(c)).map(c => this.#getRankValue(c));
1986
+ const value = this.#getRankValue(rank + 's');
1987
+ if (value >= Math.max(...boardValues)) return 'top';
1988
+ if (value <= Math.min(...boardValues)) return 'bottom';
1989
+ return 'middle';
1990
+ }
1854
1991
 
1855
1992
  // ==================== OVERCARDS ====================
1856
1993
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-extval",
3
- "version": "3.3.3",
3
+ "version": "3.3.4",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [