poker-extval 3.3.0 → 3.3.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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/evaluator.js +71 -50
  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,8 @@ 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
+
213
216
  #### Wheel Straight Support
214
217
  - `isStraightWheel` - Wheel straight (A-2-3-4-5)
215
218
  - `isOpenEndedStraightDrawWheel` - Open-ended draw using wheel ranking
@@ -479,6 +482,10 @@ Creates a new HandEvaluator instance.
479
482
 
480
483
  **Note**: All internal methods are private. Use the `evaluation` property to access evaluation results.
481
484
 
485
+ ## Releasing
486
+
487
+ 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.
488
+
482
489
  ## License
483
490
 
484
491
  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,6 +1082,7 @@ 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.
1085
+ * When hole cards are supplied, the four drawing cards must include at least one hole card.
1083
1086
  * Returns false if the hand already contains pairs, two pair, three of a kind, full house, four of a kind, or a straight.
1084
1087
  *
1085
1088
  * @returns {boolean} True if the hand is an open-ended straight draw, false otherwise
@@ -1100,8 +1103,7 @@ class HandEvaluator {
1100
1103
  if (this.#isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
1101
1104
  return this.evaluation.isOpenEndedStraightDraw = false;
1102
1105
  }
1103
- this.#sort();
1104
- const result = this.#getDistance(this.cards.slice(0,4)) === 3 || this.#getDistance(this.cards.slice(1,5)) === 3;
1106
+ const result = this.#findStraightDrawWindow([3], false) !== null;
1105
1107
  if (result) return this.evaluation.isOpenEndedStraightDraw = true;
1106
1108
  // Check wheel equivalent
1107
1109
  if (this.#isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
@@ -1124,13 +1126,13 @@ class HandEvaluator {
1124
1126
  if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
1125
1127
  if (this.evaluation.isStraightWheel === undefined) this.#isStraightWheel();
1126
1128
  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
+ return this.evaluation.isOpenEndedStraightDrawWheel = this.#findStraightDrawWindow([3], true) !== null;
1129
1130
  }
1130
1131
 
1131
1132
  /**
1132
1133
  * Determines if the hand is an inside straight draw (one card away from a straight, but the needed card is in the middle).
1133
1134
  * Checks both standard and wheel-equivalent inside draws.
1135
+ * When hole cards are supplied, the four drawing cards must include at least one hole card.
1134
1136
  * Returns false if the hand already contains pairs, two pair, three of a kind, full house, four of a kind, or a straight.
1135
1137
  *
1136
1138
  * @returns {boolean} True if the hand is an inside straight draw, false otherwise
@@ -1152,8 +1154,7 @@ class HandEvaluator {
1152
1154
  return this.evaluation.isInsideStraightDraw = false;
1153
1155
  }
1154
1156
 
1155
- this.#sort();
1156
- const result = this.#getDistance(this.cards.slice(0,4)) === 4 || this.#getDistance(this.cards.slice(1,5)) === 4;
1157
+ const result = this.#findStraightDrawWindow([4], false) !== null;
1157
1158
  if (result) return this.evaluation.isInsideStraightDraw = true;
1158
1159
  // Check wheel equivalent
1159
1160
  if (this.#isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
@@ -1177,8 +1178,7 @@ class HandEvaluator {
1177
1178
  if (this.evaluation.isStraightWheel === undefined) this.#isStraightWheel();
1178
1179
  if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraightWheel) return this.evaluation.isInsideStraightDrawWheel = false;
1179
1180
 
1180
- this.#sortWheel();
1181
- return this.evaluation.isInsideStraightDrawWheel = this.#getDistanceWheel(this.cards.slice(0,4)) === 4 || this.#getDistanceWheel(this.cards.slice(1,5)) === 4;
1181
+ return this.evaluation.isInsideStraightDrawWheel = this.#findStraightDrawWindow([4], true) !== null;
1182
1182
  }
1183
1183
 
1184
1184
  /**
@@ -1454,8 +1454,7 @@ class HandEvaluator {
1454
1454
  if (!this.evaluation.isFlushDraw) return [];
1455
1455
  const suits = this.#countSuits();
1456
1456
  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));
1457
+ return this.#getCardsOfSuit(flushSuit);
1459
1458
  }
1460
1459
 
1461
1460
  /**
@@ -1468,8 +1467,7 @@ class HandEvaluator {
1468
1467
  if (!this.evaluation.isBackdoorFlushDraw) return [];
1469
1468
  const suits = this.#countSuits();
1470
1469
  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));
1470
+ return this.#getCardsOfSuit(flushSuit);
1473
1471
  }
1474
1472
 
1475
1473
  /**
@@ -1480,7 +1478,7 @@ class HandEvaluator {
1480
1478
  */
1481
1479
  #getKeyCardsForOpenEndedStraightDraw() {
1482
1480
  if (!this.evaluation.isOpenEndedStraightDraw) return [];
1483
- return this.#getStraightDrawCards();
1481
+ return this.#getStraightDrawCards([3]);
1484
1482
  }
1485
1483
 
1486
1484
  /**
@@ -1491,7 +1489,7 @@ class HandEvaluator {
1491
1489
  */
1492
1490
  #getKeyCardsForInsideStraightDraw() {
1493
1491
  if (!this.evaluation.isInsideStraightDraw) return [];
1494
- return this.#getStraightDrawCards();
1492
+ return this.#getStraightDrawCards([4]);
1495
1493
  }
1496
1494
 
1497
1495
  /**
@@ -1577,7 +1575,7 @@ class HandEvaluator {
1577
1575
  */
1578
1576
  #getKeyCardsForOpenEndedStraightDrawWheel() {
1579
1577
  if (!this.evaluation.isOpenEndedStraightDrawWheel) return [];
1580
- return this.#getStraightDrawCards();
1578
+ return this.#getStraightDrawCards([3]);
1581
1579
  }
1582
1580
 
1583
1581
  /**
@@ -1588,7 +1586,7 @@ class HandEvaluator {
1588
1586
  */
1589
1587
  #getKeyCardsForInsideStraightDrawWheel() {
1590
1588
  if (!this.evaluation.isInsideStraightDrawWheel) return [];
1591
- return this.#getStraightDrawCards();
1589
+ return this.#getStraightDrawCards([4]);
1592
1590
  }
1593
1591
 
1594
1592
  /**
@@ -1685,41 +1683,64 @@ class HandEvaluator {
1685
1683
 
1686
1684
  /**
1687
1685
  * Helper method to extract the 4 cards that form a straight draw.
1688
- * Handles both standard and wheel straight draws.
1686
+ * Handles both standard and wheel straight draws, preferring a standard draw.
1687
+ * When hole cards are supplied, only draws that use at least one hole card are returned.
1689
1688
  *
1690
1689
  * @private
1690
+ * @param {number[]} [distances=[3, 4]] - Accepted rank spans for the 4 drawing cards
1691
+ * (3 = open-ended, 4 = inside)
1691
1692
  * @returns {string[]} Array of 4 cards forming the draw, sorted from highest to lowest
1692
1693
  */
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));
1716
- }
1717
- const lastFourDistWheel = this.#getDistanceWheel(lastFourWheel);
1718
- if (lastFourDistWheel === 3 || lastFourDistWheel === 4) {
1719
- return [...lastFourWheel].sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
1694
+ #getStraightDrawCards(distances = [3, 4]) {
1695
+ return this.#findStraightDrawWindow(distances, false) || this.#findStraightDrawWindow(distances, true) || [];
1696
+ }
1697
+
1698
+ /**
1699
+ * Finds 4 rank-adjacent cards (the highest four or the lowest four) whose rank span is one of
1700
+ * the given distances. When hole cards are supplied, a window made only of board cards is
1701
+ * skipped, since a draw the board gives every player is not the player's draw.
1702
+ *
1703
+ * @private
1704
+ * @param {number[]} distances - Accepted rank spans for the 4 cards (3 = open-ended, 4 = inside)
1705
+ * @param {boolean} wheel - If true, ranks the Ace low (for wheel draws)
1706
+ * @returns {string[]|null} Array of 4 cards sorted from highest to lowest, or null if none match
1707
+ */
1708
+ #findStraightDrawWindow(distances, wheel) {
1709
+ const rankValue = wheel ? c => this.#getRankValueWheel(c) : c => this.#getRankValue(c);
1710
+ if (wheel) this.#sortWheel(); else this.#sort();
1711
+ for (const window of [this.cards.slice(0, 4), this.cards.slice(1, 5)]) {
1712
+ const distance = wheel ? this.#getDistanceWheel(window) : this.#getDistance(window);
1713
+ if (distances.includes(distance) && this.#usesHoleCard(window)) {
1714
+ return [...window].sort((a, b) => rankValue(b) - rankValue(a));
1715
+ }
1720
1716
  }
1721
-
1722
- return [];
1717
+ return null;
1718
+ }
1719
+
1720
+ /**
1721
+ * Gets the cards of a given suit in the hand, sorted from highest to lowest rank.
1722
+ *
1723
+ * @private
1724
+ * @param {string} suit - Suit character (s, h, d, c)
1725
+ * @returns {string[]} Cards of that suit
1726
+ */
1727
+ #getCardsOfSuit(suit) {
1728
+ return this.cards.filter(c => c.slice(-1) === suit)
1729
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1730
+ }
1731
+
1732
+ /**
1733
+ * Determines whether a draw's cards include at least one of the player's hole cards.
1734
+ * A draw made entirely of board cards is shared by every player, so when hole cards are
1735
+ * supplied it does not count as the player's draw. Without hole cards every draw qualifies.
1736
+ *
1737
+ * @private
1738
+ * @param {string[]} cards - The cards forming the draw
1739
+ * @returns {boolean} True if no hole cards were supplied or a hole card is among the cards
1740
+ */
1741
+ #usesHoleCard(cards) {
1742
+ if (!this.holeCards) return true;
1743
+ return cards.some(c => this.holeCards.includes(c));
1723
1744
  }
1724
1745
 
1725
1746
  // ==================== 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.1",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [