poker-extval 3.3.1 → 3.3.3
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 +5 -3
- package/evaluator.js +78 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -201,18 +201,20 @@ The `evaluateHand()` function returns an object where:
|
|
|
201
201
|
- `isTwoUndercards` - Both hole cards rank below every board card
|
|
202
202
|
|
|
203
203
|
#### Other Categorization
|
|
204
|
-
- `isUnderPair` - Pocket pair ranked below every
|
|
204
|
+
- `isUnderPair` - Pocket pair ranked below every board card. With `holeCards` this works on any street, including the flop; without them the board is unknown, so it requires 6+ cards and means a pair ranked below every other card
|
|
205
205
|
- `isComboDraw` - Both a flush draw (not backdoor) and a straight draw
|
|
206
206
|
|
|
207
207
|
#### Draws
|
|
208
208
|
- `isFlushDraw` - Four cards of the same suit (one away from flush)
|
|
209
209
|
- `isBackdoorFlushDraw` - Three cards of the same suit (two away from flush)
|
|
210
|
-
- `isOpenEndedStraightDraw` -
|
|
211
|
-
- `isInsideStraightDraw` - One card away from straight (
|
|
210
|
+
- `isOpenEndedStraightDraw` - Four in a row that a card at either end completes (e.g. 9-8-7-6). A-K-Q-J and A-2-3-4 complete at one end only, so they count as inside draws
|
|
211
|
+
- `isInsideStraightDraw` - One card away from a straight that only one rank completes (a gap in the middle, or A-K-Q-J / A-2-3-4)
|
|
212
212
|
- `isStraightDraw` - Any type of straight draw (open-ended or inside, standard or wheel)
|
|
213
213
|
|
|
214
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
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
|
+
|
|
216
218
|
#### Wheel Straight Support
|
|
217
219
|
- `isStraightWheel` - Wheel straight (A-2-3-4-5)
|
|
218
220
|
- `isOpenEndedStraightDrawWheel` - Open-ended draw using wheel ranking
|
package/evaluator.js
CHANGED
|
@@ -65,7 +65,8 @@ class HandEvaluator {
|
|
|
65
65
|
*/
|
|
66
66
|
constructor(cards, strict = true, holeCards = null, originalCards = null) {
|
|
67
67
|
this.#validateCards(cards);
|
|
68
|
-
|
|
68
|
+
// Copy so that in-place sorting during evaluation never reorders the caller's array
|
|
69
|
+
this.cards = [...cards];
|
|
69
70
|
this.strict = strict;
|
|
70
71
|
this.originalCards = originalCards || (cards.length > 5 ? cards : null);
|
|
71
72
|
if (holeCards === null || holeCards === undefined) {
|
|
@@ -370,11 +371,13 @@ class HandEvaluator {
|
|
|
370
371
|
}
|
|
371
372
|
|
|
372
373
|
/**
|
|
373
|
-
* Gets
|
|
374
|
+
* Gets key card ranks for an evaluation, in tiebreak order.
|
|
375
|
+
* Ranks are grouped by how often they appear (so a full house compares its three of a kind
|
|
376
|
+
* before its pair), then sorted descending. In a wheel straight (A-2-3-4-5) the Ace counts low.
|
|
374
377
|
*
|
|
375
378
|
* @private
|
|
376
379
|
* @param {Object} evaluation - The evaluation object
|
|
377
|
-
* @returns {number[]} Array of rank values
|
|
380
|
+
* @returns {number[]} Array of rank values in tiebreak order
|
|
378
381
|
*/
|
|
379
382
|
#getKeyCardRanks(evaluation) {
|
|
380
383
|
const handStrength = {
|
|
@@ -384,8 +387,11 @@ class HandEvaluator {
|
|
|
384
387
|
};
|
|
385
388
|
for (const [hand] of Object.entries(handStrength)) {
|
|
386
389
|
if (evaluation[hand]) {
|
|
387
|
-
const
|
|
388
|
-
|
|
390
|
+
const ranks = (evaluation.keyCards[hand] || []).map(c => this.#getRankValue(c));
|
|
391
|
+
const isWheel = (hand === 'isStraight' || hand === 'isStraightFlush') && ranks.includes(13) && ranks.includes(4);
|
|
392
|
+
const values = isWheel ? ranks.map(r => r === 13 ? 0 : r) : ranks;
|
|
393
|
+
const counts = values.reduce((acc, r) => (acc[r] = (acc[r] || 0) + 1, acc), {});
|
|
394
|
+
return values.sort((a, b) => counts[b] - counts[a] || b - a);
|
|
389
395
|
}
|
|
390
396
|
}
|
|
391
397
|
return [];
|
|
@@ -457,7 +463,7 @@ class HandEvaluator {
|
|
|
457
463
|
for (const [key, evaluation] of Object.entries(result)) {
|
|
458
464
|
const strength = this.#getHandStrength(evaluation);
|
|
459
465
|
const bestStrength = bestEval ? this.#getHandStrength(bestEval) : 0;
|
|
460
|
-
if (!bestEval || strength > bestStrength || (strength === bestStrength && this.#compareEvaluations(evaluation, bestEval)
|
|
466
|
+
if (!bestEval || strength > bestStrength || (strength === bestStrength && this.#compareEvaluations(evaluation, bestEval) < 0)) {
|
|
461
467
|
bestKey = key;
|
|
462
468
|
bestEval = evaluation;
|
|
463
469
|
}
|
|
@@ -1082,13 +1088,14 @@ class HandEvaluator {
|
|
|
1082
1088
|
/**
|
|
1083
1089
|
* Determines if the hand is an open-ended straight draw (one card away from a straight on either end).
|
|
1084
1090
|
* Checks both standard and wheel-equivalent open-ended draws.
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1091
|
+
* Paired cards count once, so a paired hand can still hold a straight draw.
|
|
1092
|
+
* When hole cards are supplied, a hole card must add a rank the board does not already have.
|
|
1093
|
+
* Returns false if the hand is already a straight.
|
|
1087
1094
|
*
|
|
1088
1095
|
* @returns {boolean} True if the hand is an open-ended straight draw, false otherwise
|
|
1089
1096
|
* @example
|
|
1090
|
-
* const evaluator = new HandEvaluator(['
|
|
1091
|
-
* evaluator.isOpenEndedStraightDraw(); // Returns true (needs
|
|
1097
|
+
* const evaluator = new HandEvaluator(['Ks', 'Qs', 'Js', 'Ts', '4h']);
|
|
1098
|
+
* evaluator.isOpenEndedStraightDraw(); // Returns true (needs A or 9 to complete straight)
|
|
1092
1099
|
*/
|
|
1093
1100
|
#isOpenEndedStraightDraw() {
|
|
1094
1101
|
if (this.evaluation.isOpenEndedStraightDraw !== undefined) return this.evaluation.isOpenEndedStraightDraw;
|
|
@@ -1098,7 +1105,7 @@ class HandEvaluator {
|
|
|
1098
1105
|
if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
|
|
1099
1106
|
if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
|
|
1100
1107
|
if (this.evaluation.isStraight === undefined) this.#isStraight();
|
|
1101
|
-
if (this.evaluation.
|
|
1108
|
+
if (this.evaluation.isStraight) {
|
|
1102
1109
|
// Check wheel equivalent before returning false
|
|
1103
1110
|
if (this.#isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
|
|
1104
1111
|
return this.evaluation.isOpenEndedStraightDraw = false;
|
|
@@ -1112,8 +1119,8 @@ class HandEvaluator {
|
|
|
1112
1119
|
|
|
1113
1120
|
/**
|
|
1114
1121
|
* Determines if the hand is an open-ended straight draw using wheel ranking.
|
|
1115
|
-
* Checks for wheel-equivalent open-ended draws
|
|
1116
|
-
* Returns false if the hand already
|
|
1122
|
+
* Checks for wheel-equivalent open-ended draws. A-2-3-4 needs only a 5, so it counts as an inside draw.
|
|
1123
|
+
* Paired cards count once. Returns false if the hand is already a wheel straight.
|
|
1117
1124
|
*
|
|
1118
1125
|
* @returns {boolean} True if the hand is a wheel-equivalent open-ended straight draw, false otherwise
|
|
1119
1126
|
*/
|
|
@@ -1125,15 +1132,16 @@ class HandEvaluator {
|
|
|
1125
1132
|
if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
|
|
1126
1133
|
if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
|
|
1127
1134
|
if (this.evaluation.isStraightWheel === undefined) this.#isStraightWheel();
|
|
1128
|
-
if (this.evaluation.
|
|
1135
|
+
if (this.evaluation.isStraightWheel) return this.evaluation.isOpenEndedStraightDrawWheel = false;
|
|
1129
1136
|
return this.evaluation.isOpenEndedStraightDrawWheel = this.#findStraightDrawWindow([3], true) !== null;
|
|
1130
1137
|
}
|
|
1131
1138
|
|
|
1132
1139
|
/**
|
|
1133
1140
|
* Determines if the hand is an inside straight draw (one card away from a straight, but the needed card is in the middle).
|
|
1134
1141
|
* Checks both standard and wheel-equivalent inside draws.
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1142
|
+
* Paired cards count once, so a paired hand can still hold a straight draw.
|
|
1143
|
+
* When hole cards are supplied, a hole card must add a rank the board does not already have.
|
|
1144
|
+
* Returns false if the hand is already a straight.
|
|
1137
1145
|
*
|
|
1138
1146
|
* @returns {boolean} True if the hand is an inside straight draw, false otherwise
|
|
1139
1147
|
* @example
|
|
@@ -1148,7 +1156,7 @@ class HandEvaluator {
|
|
|
1148
1156
|
if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
|
|
1149
1157
|
if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
|
|
1150
1158
|
if (this.evaluation.isStraight === undefined) this.#isStraight();
|
|
1151
|
-
if (this.evaluation.
|
|
1159
|
+
if (this.evaluation.isStraight) {
|
|
1152
1160
|
// Check wheel equivalent before returning false
|
|
1153
1161
|
if (this.#isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
|
|
1154
1162
|
return this.evaluation.isInsideStraightDraw = false;
|
|
@@ -1164,7 +1172,7 @@ class HandEvaluator {
|
|
|
1164
1172
|
/**
|
|
1165
1173
|
* Determines if the hand is an inside straight draw using wheel ranking.
|
|
1166
1174
|
* Checks for wheel-equivalent inside draws (e.g., A-2-3-5 needs 4).
|
|
1167
|
-
* Returns false if the hand already
|
|
1175
|
+
* Paired cards count once. Returns false if the hand is already a wheel straight.
|
|
1168
1176
|
*
|
|
1169
1177
|
* @returns {boolean} True if the hand is a wheel-equivalent inside straight draw, false otherwise
|
|
1170
1178
|
*/
|
|
@@ -1176,7 +1184,7 @@ class HandEvaluator {
|
|
|
1176
1184
|
if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
|
|
1177
1185
|
if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
|
|
1178
1186
|
if (this.evaluation.isStraightWheel === undefined) this.#isStraightWheel();
|
|
1179
|
-
if (this.evaluation.
|
|
1187
|
+
if (this.evaluation.isStraightWheel) return this.evaluation.isInsideStraightDrawWheel = false;
|
|
1180
1188
|
|
|
1181
1189
|
return this.evaluation.isInsideStraightDrawWheel = this.#findStraightDrawWindow([4], true) !== null;
|
|
1182
1190
|
}
|
|
@@ -1696,9 +1704,13 @@ class HandEvaluator {
|
|
|
1696
1704
|
}
|
|
1697
1705
|
|
|
1698
1706
|
/**
|
|
1699
|
-
* Finds 4
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1707
|
+
* Finds 4 cards of distinct, rank-adjacent ranks whose rank span is one of the given distances.
|
|
1708
|
+
* Paired cards count once (a hole card is preferred as the representative of its rank), so a
|
|
1709
|
+
* paired hand such as 8-8-6-5-4 still finds its 8-6-5-4 draw. One-ended draws (A-K-Q-J and
|
|
1710
|
+
* A-2-3-4) count as inside draws, since only one rank completes them. Wheel windows must
|
|
1711
|
+
* contain the Ace. When hole cards are supplied, a
|
|
1712
|
+
* window whose ranks are all already on the board is skipped, since a draw the board gives
|
|
1713
|
+
* every player is not the player's draw.
|
|
1702
1714
|
*
|
|
1703
1715
|
* @private
|
|
1704
1716
|
* @param {number[]} distances - Accepted rank spans for the 4 cards (3 = open-ended, 4 = inside)
|
|
@@ -1708,11 +1720,24 @@ class HandEvaluator {
|
|
|
1708
1720
|
#findStraightDrawWindow(distances, wheel) {
|
|
1709
1721
|
const rankValue = wheel ? c => this.#getRankValueWheel(c) : c => this.#getRankValue(c);
|
|
1710
1722
|
if (wheel) this.#sortWheel(); else this.#sort();
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1723
|
+
const distinct = [];
|
|
1724
|
+
for (const card of this.cards) {
|
|
1725
|
+
const last = distinct[distinct.length - 1];
|
|
1726
|
+
if (last && rankValue(last) === rankValue(card)) {
|
|
1727
|
+
if (this.holeCards && this.holeCards.includes(card)) distinct[distinct.length - 1] = card;
|
|
1728
|
+
continue;
|
|
1715
1729
|
}
|
|
1730
|
+
distinct.push(card);
|
|
1731
|
+
}
|
|
1732
|
+
for (let i = 0; i + 4 <= distinct.length; i++) {
|
|
1733
|
+
const window = distinct.slice(i, i + 4);
|
|
1734
|
+
// Wheel ranking only matters when the Ace plays low; other windows are standard draws
|
|
1735
|
+
if (wheel && !window.some(c => c.startsWith('A'))) continue;
|
|
1736
|
+
let distance = rankValue(window[0]) - rankValue(window[3]);
|
|
1737
|
+
// Four in a row capped by the Ace (A-K-Q-J or A-2-3-4) completes at one end only,
|
|
1738
|
+
// so it has the same 4 outs as an inside draw
|
|
1739
|
+
if (distance === 3 && window.some(c => c.startsWith('A'))) distance = 4;
|
|
1740
|
+
if (distances.includes(distance) && this.#addsRankToBoard(window)) return window;
|
|
1716
1741
|
}
|
|
1717
1742
|
return null;
|
|
1718
1743
|
}
|
|
@@ -1743,6 +1768,23 @@ class HandEvaluator {
|
|
|
1743
1768
|
return cards.some(c => this.holeCards.includes(c));
|
|
1744
1769
|
}
|
|
1745
1770
|
|
|
1771
|
+
/**
|
|
1772
|
+
* Determines whether a straight draw's cards include a rank the board does not already have.
|
|
1773
|
+
* A hole card that only pairs a board rank adds nothing to a straight draw, so when hole cards
|
|
1774
|
+
* are supplied the draw must contain a rank found only in the hole cards. Without hole cards
|
|
1775
|
+
* every draw qualifies.
|
|
1776
|
+
*
|
|
1777
|
+
* @private
|
|
1778
|
+
* @param {string[]} cards - The cards forming the draw
|
|
1779
|
+
* @returns {boolean} True if no hole cards were supplied or a drawing rank is missing from the board
|
|
1780
|
+
*/
|
|
1781
|
+
#addsRankToBoard(cards) {
|
|
1782
|
+
if (!this.holeCards) return true;
|
|
1783
|
+
const fullHand = this.originalCards || this.cards;
|
|
1784
|
+
const boardRanks = new Set(fullHand.filter(c => !this.holeCards.includes(c)).map(c => c.slice(0, -1)));
|
|
1785
|
+
return cards.some(c => !boardRanks.has(c.slice(0, -1)));
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1746
1788
|
// ==================== HIGH CARD ====================
|
|
1747
1789
|
|
|
1748
1790
|
/**
|
|
@@ -1766,7 +1808,9 @@ class HandEvaluator {
|
|
|
1766
1808
|
|
|
1767
1809
|
/**
|
|
1768
1810
|
* Determines if the hand contains an under pair (pocket pair smaller than all board cards).
|
|
1769
|
-
*
|
|
1811
|
+
* With hole cards supplied, the hole cards must be a pocket pair ranked below every board card;
|
|
1812
|
+
* this works on any street, including a 5-card flop. Without hole cards the board is unknown, so
|
|
1813
|
+
* it applies only to 6+ cards where the pair rank is smaller than all other cards.
|
|
1770
1814
|
*
|
|
1771
1815
|
* @returns {boolean} True if the hand is an under pair, false otherwise
|
|
1772
1816
|
* @example
|
|
@@ -1777,6 +1821,13 @@ class HandEvaluator {
|
|
|
1777
1821
|
#isUnderPair() {
|
|
1778
1822
|
if (this.evaluation.isUnderPair !== undefined) return this.evaluation.isUnderPair;
|
|
1779
1823
|
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
1824
|
+
if (this.evaluation.isPair && this.holeCards) {
|
|
1825
|
+
const [first, second] = this.holeCards;
|
|
1826
|
+
const holeRank = first.slice(0, -1);
|
|
1827
|
+
if (second.slice(0, -1) !== holeRank || this.#getPairRank() !== holeRank) return this.evaluation.isUnderPair = false;
|
|
1828
|
+
const board = (this.originalCards || this.cards).filter(c => !this.holeCards.includes(c));
|
|
1829
|
+
return this.evaluation.isUnderPair = board.every(c => this.#getRankValue(c) > this.#getRankValue(first));
|
|
1830
|
+
}
|
|
1780
1831
|
if (!this.evaluation.isPair || !this.originalCards || this.originalCards.length < 6) {
|
|
1781
1832
|
return this.evaluation.isUnderPair = false;
|
|
1782
1833
|
}
|