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.
- package/README.md +7 -0
- package/evaluator.js +71 -50
- 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.
|
|
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.
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
|
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
|
|
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
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
this.#sortWheel();
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
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 ====================
|