poker-extval 3.3.0 → 3.3.2

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 +9 -0
  2. package/evaluator.js +107 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -97,6 +97,7 @@ evaluateHand([...board, '2d', '2h'], true, ['2d', '2h']); // isTwoUndercards
97
97
 
98
98
  **Notes:**
99
99
  - Hole cards are optional. Without them every overcard property is `false`.
100
+ - Supplying hole cards also restricts draws to those that use at least one hole card (see [Draws](#draws)).
100
101
  - Both hole cards must appear in the `cards` array, but they do **not** have to appear in the 5-card combination being evaluated — the classification is a property of the whole hand and is identical across every combination.
101
102
  - These categorizations describe the hole cards, not the made hand, so **strict mode does not filter them**. A hand that flops a straight with A-J on a K-Q-T-3 board reports both `isStraight` and `isOvercardAndMiddleCard`.
102
103
 
@@ -210,6 +211,10 @@ The `evaluateHand()` function returns an object where:
210
211
  - `isInsideStraightDraw` - One card away from straight (needed card in middle)
211
212
  - `isStraightDraw` - Any type of straight draw (open-ended or inside, standard or wheel)
212
213
 
214
+ **Note**: When `holeCards` are supplied, every draw (including `isComboDraw` and the wheel draws) must use at least one hole card. A draw made entirely of board cards is shared by every player, so it is not reported: 2-2 on a 9-8-7-6 board is not an open-ended straight draw, and A♦-Q♦ on a K♠-7♠-2♠ flop is not a backdoor flush draw. Without `holeCards` the evaluator cannot tell the board from the hand, so draws are detected from all cards.
215
+
216
+ **Note**: Straight draws count paired cards once, so a paired hand can still hold one: 5-4 on an 8-8-6 flop is a pair and an inside straight draw. With `holeCards`, a hole card that only pairs a board rank does not create a straight draw (9-2 on a 9-8-7-6 board has no straight draw of its own).
217
+
213
218
  #### Wheel Straight Support
214
219
  - `isStraightWheel` - Wheel straight (A-2-3-4-5)
215
220
  - `isOpenEndedStraightDrawWheel` - Open-ended draw using wheel ranking
@@ -479,6 +484,10 @@ Creates a new HandEvaluator instance.
479
484
 
480
485
  **Note**: All internal methods are private. Use the `evaluation` property to access evaluation results.
481
486
 
487
+ ## Releasing
488
+
489
+ Pushing to `main` publishes to npm via GitHub Actions (`.github/workflows/publish.yml`), using npm trusted publishing so no token is stored. Every push to `main` is released: the workflow runs the tests, bumps the patch version, commits the bump back to `main`, tags it `v<version>` and publishes. There is no need to bump the version by hand for patch releases. For a minor or major release, bump it in your change (`npm version minor|major --no-git-tag-version`); a version that is not on npm yet is published as is, without an extra bump.
490
+
482
491
  ## License
483
492
 
484
493
  MIT License
package/evaluator.js CHANGED
@@ -954,7 +954,8 @@ class HandEvaluator {
954
954
 
955
955
  /**
956
956
  * Determines if the hand has four cards of the same suit (one card away from a flush).
957
- * Returns false if the hand is already a flush.
957
+ * Returns false if the hand is already a flush. When hole cards are supplied, at least one
958
+ * of the four suited cards must be a hole card.
958
959
  *
959
960
  * @returns {boolean} True if the hand is a flush draw, false otherwise
960
961
  * @example
@@ -966,12 +967,13 @@ class HandEvaluator {
966
967
  if (this.evaluation.isFlush === undefined) this.#isFlush();
967
968
  if (this.evaluation.isFlush) return this.evaluation.isFlushDraw = false;
968
969
  const suits = this.#countSuits();
969
- return this.evaluation.isFlushDraw = Object.values(suits).some(count => count === 4);
970
+ return this.evaluation.isFlushDraw = Object.keys(suits).some(s => suits[s] === 4 && this.#usesHoleCard(this.#getCardsOfSuit(s)));
970
971
  }
971
972
 
972
973
  /**
973
974
  * Determines if the hand has three cards of the same suit (two cards away from a flush).
974
- * Returns false if the hand is already a flush or flush draw.
975
+ * Returns false if the hand is already a flush or flush draw. When hole cards are supplied,
976
+ * at least one of the three suited cards must be a hole card.
975
977
  *
976
978
  * @returns {boolean} True if the hand is a backdoor flush draw, false otherwise
977
979
  * @example
@@ -984,7 +986,7 @@ class HandEvaluator {
984
986
  if (this.evaluation.isFlushDraw === undefined) this.#isFlushDraw();
985
987
  if (this.evaluation.isFlush || this.evaluation.isFlushDraw) return this.evaluation.isBackdoorFlushDraw = false;
986
988
  const suits = this.#countSuits();
987
- return this.evaluation.isBackdoorFlushDraw = Object.values(suits).some(count => count === 3);
989
+ return this.evaluation.isBackdoorFlushDraw = Object.keys(suits).some(s => suits[s] === 3 && this.#usesHoleCard(this.#getCardsOfSuit(s)));
988
990
  }
989
991
 
990
992
  // ==================== STRAIGHT EVALUATION ====================
@@ -1080,7 +1082,9 @@ class HandEvaluator {
1080
1082
  /**
1081
1083
  * Determines if the hand is an open-ended straight draw (one card away from a straight on either end).
1082
1084
  * Checks both standard and wheel-equivalent open-ended draws.
1083
- * Returns false if the hand already contains pairs, two pair, three of a kind, full house, four of a kind, or a straight.
1085
+ * Paired cards count once, so a paired hand can still hold a straight draw.
1086
+ * When hole cards are supplied, a hole card must add a rank the board does not already have.
1087
+ * Returns false if the hand is already a straight.
1084
1088
  *
1085
1089
  * @returns {boolean} True if the hand is an open-ended straight draw, false otherwise
1086
1090
  * @example
@@ -1095,13 +1099,12 @@ class HandEvaluator {
1095
1099
  if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
1096
1100
  if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
1097
1101
  if (this.evaluation.isStraight === undefined) this.#isStraight();
1098
- if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraight) {
1102
+ if (this.evaluation.isStraight) {
1099
1103
  // Check wheel equivalent before returning false
1100
1104
  if (this.#isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
1101
1105
  return this.evaluation.isOpenEndedStraightDraw = false;
1102
1106
  }
1103
- this.#sort();
1104
- const result = this.#getDistance(this.cards.slice(0,4)) === 3 || this.#getDistance(this.cards.slice(1,5)) === 3;
1107
+ const result = this.#findStraightDrawWindow([3], false) !== null;
1105
1108
  if (result) return this.evaluation.isOpenEndedStraightDraw = true;
1106
1109
  // Check wheel equivalent
1107
1110
  if (this.#isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
@@ -1111,7 +1114,7 @@ class HandEvaluator {
1111
1114
  /**
1112
1115
  * Determines if the hand is an open-ended straight draw using wheel ranking.
1113
1116
  * Checks for wheel-equivalent open-ended draws (e.g., A-2-3-4 needs 5 or 2).
1114
- * Returns false if the hand already contains pairs or higher, or is a wheel straight.
1117
+ * Paired cards count once. Returns false if the hand is already a wheel straight.
1115
1118
  *
1116
1119
  * @returns {boolean} True if the hand is a wheel-equivalent open-ended straight draw, false otherwise
1117
1120
  */
@@ -1123,15 +1126,16 @@ class HandEvaluator {
1123
1126
  if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
1124
1127
  if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
1125
1128
  if (this.evaluation.isStraightWheel === undefined) this.#isStraightWheel();
1126
- if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraightWheel) return this.evaluation.isOpenEndedStraightDrawWheel = false;
1127
- this.#sortWheel();
1128
- return this.evaluation.isOpenEndedStraightDrawWheel = this.#getDistanceWheel(this.cards.slice(0,4)) === 3 || this.#getDistanceWheel(this.cards.slice(1,5)) === 3;
1129
+ if (this.evaluation.isStraightWheel) return this.evaluation.isOpenEndedStraightDrawWheel = false;
1130
+ return this.evaluation.isOpenEndedStraightDrawWheel = this.#findStraightDrawWindow([3], true) !== null;
1129
1131
  }
1130
1132
 
1131
1133
  /**
1132
1134
  * Determines if the hand is an inside straight draw (one card away from a straight, but the needed card is in the middle).
1133
1135
  * Checks both standard and wheel-equivalent inside draws.
1134
- * Returns false if the hand already contains pairs, two pair, three of a kind, full house, four of a kind, or a straight.
1136
+ * Paired cards count once, so a paired hand can still hold a straight draw.
1137
+ * When hole cards are supplied, a hole card must add a rank the board does not already have.
1138
+ * Returns false if the hand is already a straight.
1135
1139
  *
1136
1140
  * @returns {boolean} True if the hand is an inside straight draw, false otherwise
1137
1141
  * @example
@@ -1146,14 +1150,13 @@ class HandEvaluator {
1146
1150
  if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
1147
1151
  if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
1148
1152
  if (this.evaluation.isStraight === undefined) this.#isStraight();
1149
- if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraight) {
1153
+ if (this.evaluation.isStraight) {
1150
1154
  // Check wheel equivalent before returning false
1151
1155
  if (this.#isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
1152
1156
  return this.evaluation.isInsideStraightDraw = false;
1153
1157
  }
1154
1158
 
1155
- this.#sort();
1156
- const result = this.#getDistance(this.cards.slice(0,4)) === 4 || this.#getDistance(this.cards.slice(1,5)) === 4;
1159
+ const result = this.#findStraightDrawWindow([4], false) !== null;
1157
1160
  if (result) return this.evaluation.isInsideStraightDraw = true;
1158
1161
  // Check wheel equivalent
1159
1162
  if (this.#isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
@@ -1163,7 +1166,7 @@ class HandEvaluator {
1163
1166
  /**
1164
1167
  * Determines if the hand is an inside straight draw using wheel ranking.
1165
1168
  * Checks for wheel-equivalent inside draws (e.g., A-2-3-5 needs 4).
1166
- * Returns false if the hand already contains pairs or higher, or is a wheel straight.
1169
+ * Paired cards count once. Returns false if the hand is already a wheel straight.
1167
1170
  *
1168
1171
  * @returns {boolean} True if the hand is a wheel-equivalent inside straight draw, false otherwise
1169
1172
  */
@@ -1175,10 +1178,9 @@ class HandEvaluator {
1175
1178
  if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
1176
1179
  if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
1177
1180
  if (this.evaluation.isStraightWheel === undefined) this.#isStraightWheel();
1178
- if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraightWheel) return this.evaluation.isInsideStraightDrawWheel = false;
1181
+ if (this.evaluation.isStraightWheel) return this.evaluation.isInsideStraightDrawWheel = false;
1179
1182
 
1180
- this.#sortWheel();
1181
- return this.evaluation.isInsideStraightDrawWheel = this.#getDistanceWheel(this.cards.slice(0,4)) === 4 || this.#getDistanceWheel(this.cards.slice(1,5)) === 4;
1183
+ return this.evaluation.isInsideStraightDrawWheel = this.#findStraightDrawWindow([4], true) !== null;
1182
1184
  }
1183
1185
 
1184
1186
  /**
@@ -1454,8 +1456,7 @@ class HandEvaluator {
1454
1456
  if (!this.evaluation.isFlushDraw) return [];
1455
1457
  const suits = this.#countSuits();
1456
1458
  const flushSuit = Object.keys(suits).find(s => suits[s] === 4);
1457
- return this.cards.filter(c => c.slice(-1) === flushSuit)
1458
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1459
+ return this.#getCardsOfSuit(flushSuit);
1459
1460
  }
1460
1461
 
1461
1462
  /**
@@ -1468,8 +1469,7 @@ class HandEvaluator {
1468
1469
  if (!this.evaluation.isBackdoorFlushDraw) return [];
1469
1470
  const suits = this.#countSuits();
1470
1471
  const flushSuit = Object.keys(suits).find(s => suits[s] === 3);
1471
- return this.cards.filter(c => c.slice(-1) === flushSuit)
1472
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1472
+ return this.#getCardsOfSuit(flushSuit);
1473
1473
  }
1474
1474
 
1475
1475
  /**
@@ -1480,7 +1480,7 @@ class HandEvaluator {
1480
1480
  */
1481
1481
  #getKeyCardsForOpenEndedStraightDraw() {
1482
1482
  if (!this.evaluation.isOpenEndedStraightDraw) return [];
1483
- return this.#getStraightDrawCards();
1483
+ return this.#getStraightDrawCards([3]);
1484
1484
  }
1485
1485
 
1486
1486
  /**
@@ -1491,7 +1491,7 @@ class HandEvaluator {
1491
1491
  */
1492
1492
  #getKeyCardsForInsideStraightDraw() {
1493
1493
  if (!this.evaluation.isInsideStraightDraw) return [];
1494
- return this.#getStraightDrawCards();
1494
+ return this.#getStraightDrawCards([4]);
1495
1495
  }
1496
1496
 
1497
1497
  /**
@@ -1577,7 +1577,7 @@ class HandEvaluator {
1577
1577
  */
1578
1578
  #getKeyCardsForOpenEndedStraightDrawWheel() {
1579
1579
  if (!this.evaluation.isOpenEndedStraightDrawWheel) return [];
1580
- return this.#getStraightDrawCards();
1580
+ return this.#getStraightDrawCards([3]);
1581
1581
  }
1582
1582
 
1583
1583
  /**
@@ -1588,7 +1588,7 @@ class HandEvaluator {
1588
1588
  */
1589
1589
  #getKeyCardsForInsideStraightDrawWheel() {
1590
1590
  if (!this.evaluation.isInsideStraightDrawWheel) return [];
1591
- return this.#getStraightDrawCards();
1591
+ return this.#getStraightDrawCards([4]);
1592
1592
  }
1593
1593
 
1594
1594
  /**
@@ -1685,41 +1685,91 @@ class HandEvaluator {
1685
1685
 
1686
1686
  /**
1687
1687
  * Helper method to extract the 4 cards that form a straight draw.
1688
- * Handles both standard and wheel straight draws.
1688
+ * Handles both standard and wheel straight draws, preferring a standard draw.
1689
+ * When hole cards are supplied, only draws that use at least one hole card are returned.
1689
1690
  *
1690
1691
  * @private
1692
+ * @param {number[]} [distances=[3, 4]] - Accepted rank spans for the 4 drawing cards
1693
+ * (3 = open-ended, 4 = inside)
1691
1694
  * @returns {string[]} Array of 4 cards forming the draw, sorted from highest to lowest
1692
1695
  */
1693
- #getStraightDrawCards() {
1694
- // Try standard straight draw
1695
- this.#sort();
1696
- const firstFour = this.cards.slice(0, 4);
1697
- const lastFour = this.cards.slice(1, 5);
1698
-
1699
- const firstFourDist = this.#getDistance(firstFour);
1700
- if (firstFourDist === 3 || firstFourDist === 4) {
1701
- return [...firstFour].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1702
- }
1703
- const lastFourDist = this.#getDistance(lastFour);
1704
- if (lastFourDist === 3 || lastFourDist === 4) {
1705
- return [...lastFour].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1706
- }
1707
-
1708
- // Try wheel straight draw
1709
- this.#sortWheel();
1710
- const firstFourWheel = this.cards.slice(0, 4);
1711
- const lastFourWheel = this.cards.slice(1, 5);
1712
-
1713
- const firstFourDistWheel = this.#getDistanceWheel(firstFourWheel);
1714
- if (firstFourDistWheel === 3 || firstFourDistWheel === 4) {
1715
- return [...firstFourWheel].sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
1696
+ #getStraightDrawCards(distances = [3, 4]) {
1697
+ return this.#findStraightDrawWindow(distances, false) || this.#findStraightDrawWindow(distances, true) || [];
1698
+ }
1699
+
1700
+ /**
1701
+ * Finds 4 cards of distinct, rank-adjacent ranks whose rank span is one of the given distances.
1702
+ * Paired cards count once (a hole card is preferred as the representative of its rank), so a
1703
+ * paired hand such as 8-8-6-5-4 still finds its 8-6-5-4 draw. When hole cards are supplied, a
1704
+ * window whose ranks are all already on the board is skipped, since a draw the board gives
1705
+ * every player is not the player's draw.
1706
+ *
1707
+ * @private
1708
+ * @param {number[]} distances - Accepted rank spans for the 4 cards (3 = open-ended, 4 = inside)
1709
+ * @param {boolean} wheel - If true, ranks the Ace low (for wheel draws)
1710
+ * @returns {string[]|null} Array of 4 cards sorted from highest to lowest, or null if none match
1711
+ */
1712
+ #findStraightDrawWindow(distances, wheel) {
1713
+ const rankValue = wheel ? c => this.#getRankValueWheel(c) : c => this.#getRankValue(c);
1714
+ if (wheel) this.#sortWheel(); else this.#sort();
1715
+ const distinct = [];
1716
+ for (const card of this.cards) {
1717
+ const last = distinct[distinct.length - 1];
1718
+ if (last && rankValue(last) === rankValue(card)) {
1719
+ if (this.holeCards && this.holeCards.includes(card)) distinct[distinct.length - 1] = card;
1720
+ continue;
1721
+ }
1722
+ distinct.push(card);
1716
1723
  }
1717
- const lastFourDistWheel = this.#getDistanceWheel(lastFourWheel);
1718
- if (lastFourDistWheel === 3 || lastFourDistWheel === 4) {
1719
- return [...lastFourWheel].sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
1724
+ for (let i = 0; i + 4 <= distinct.length; i++) {
1725
+ const window = distinct.slice(i, i + 4);
1726
+ const distance = rankValue(window[0]) - rankValue(window[3]);
1727
+ if (distances.includes(distance) && this.#addsRankToBoard(window)) return window;
1720
1728
  }
1721
-
1722
- return [];
1729
+ return null;
1730
+ }
1731
+
1732
+ /**
1733
+ * Gets the cards of a given suit in the hand, sorted from highest to lowest rank.
1734
+ *
1735
+ * @private
1736
+ * @param {string} suit - Suit character (s, h, d, c)
1737
+ * @returns {string[]} Cards of that suit
1738
+ */
1739
+ #getCardsOfSuit(suit) {
1740
+ return this.cards.filter(c => c.slice(-1) === suit)
1741
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1742
+ }
1743
+
1744
+ /**
1745
+ * Determines whether a draw's cards include at least one of the player's hole cards.
1746
+ * A draw made entirely of board cards is shared by every player, so when hole cards are
1747
+ * supplied it does not count as the player's draw. Without hole cards every draw qualifies.
1748
+ *
1749
+ * @private
1750
+ * @param {string[]} cards - The cards forming the draw
1751
+ * @returns {boolean} True if no hole cards were supplied or a hole card is among the cards
1752
+ */
1753
+ #usesHoleCard(cards) {
1754
+ if (!this.holeCards) return true;
1755
+ return cards.some(c => this.holeCards.includes(c));
1756
+ }
1757
+
1758
+ /**
1759
+ * Determines whether a straight draw's cards include a rank the board does not already have.
1760
+ * A hole card that only pairs a board rank adds nothing to a straight draw, so when hole cards
1761
+ * are supplied the draw must contain a rank found only in the hole cards. Without hole cards
1762
+ * every draw qualifies.
1763
+ *
1764
+ * @private
1765
+ * @param {string[]} cards - The cards forming the draw
1766
+ * @returns {boolean} True if no hole cards were supplied or a drawing rank is missing from the board
1767
+ */
1768
+ #addsRankToBoard(cards) {
1769
+ if (!this.holeCards) return true;
1770
+ const fullHand = this.originalCards || this.cards;
1771
+ const boardRanks = new Set(fullHand.filter(c => !this.holeCards.includes(c)).map(c => c.slice(0, -1)));
1772
+ return cards.some(c => !boardRanks.has(c.slice(0, -1)));
1723
1773
  }
1724
1774
 
1725
1775
  // ==================== HIGH CARD ====================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-extval",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [