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.
- package/README.md +9 -0
- package/evaluator.js +107 -57
- 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.
|
|
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,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
|
-
*
|
|
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.
|
|
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.#
|
|
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
|
|
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.
|
|
1127
|
-
this.#
|
|
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
|
-
*
|
|
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.
|
|
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.#
|
|
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
|
|
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.
|
|
1181
|
+
if (this.evaluation.isStraightWheel) return this.evaluation.isInsideStraightDrawWheel = false;
|
|
1179
1182
|
|
|
1180
|
-
this.#
|
|
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
|
|
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
|
|
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
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
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 ====================
|