poker-extval 3.3.2 → 3.3.4
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 +8 -3
- package/evaluator.js +172 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,9 @@ const result = evaluateHand(['As', 'Ah', 'Kd', 'Kc', 'Qs'], false);
|
|
|
126
126
|
- Descriptive labels (isTopPair, etc.) are only true if they match the strongest hand
|
|
127
127
|
- Made flush or stronger cannot have straight draws (flush > straight)
|
|
128
128
|
- Draws can coexist when no made hand exists (flush draw + straight draw is valid)
|
|
129
|
+
- With 6 or more cards, draws are collected from **every** 5-card combination, not just the best one, so a draw using a card outside the best five is still reported. Straight draws are dropped once a straight or better is made, and flush draws once a flush or better is made. `isComboDraw` is true when the whole hand has both a flush draw and a straight draw.
|
|
130
|
+
|
|
131
|
+
**Draws by street**: the number of cards sets the street. With 5 cards (the flop) every draw is possible; with 6 (the turn) there is no `isBackdoorFlushDraw`, since one card to come cannot complete it; with 7 (the river) there are no draws at all. This applies in both modes.
|
|
129
132
|
|
|
130
133
|
### Evaluating Multiple Card Hands
|
|
131
134
|
|
|
@@ -191,6 +194,8 @@ The `evaluateHand()` function returns an object where:
|
|
|
191
194
|
- `isMiddleThreeOfAKind` - Three of a kind is neither highest nor lowest
|
|
192
195
|
- `isBottomThreeOfAKind` - Three of a kind is the lowest rank
|
|
193
196
|
|
|
197
|
+
**Note**: With `holeCards`, pair and three of a kind positions are judged against the **whole board**, including board cards left out of the 5-card combination: top means at or above the highest board card (so a pocket pair above the board is top pair), bottom means at or below the lowest, middle is anything between. A pair or three of a kind made only of board cards gets no position. Without `holeCards` the board is unknown, so positions are judged within the 5-card combination. Two pair positions are always judged within the 5-card combination.
|
|
198
|
+
|
|
194
199
|
#### Overcard Categorization (requires `holeCards`; not filtered by strict mode)
|
|
195
200
|
- `isOvercard` - At least one hole card ranks above every board card
|
|
196
201
|
- `isTwoOvercards` - Both hole cards rank above every board card
|
|
@@ -201,14 +206,14 @@ The `evaluateHand()` function returns an object where:
|
|
|
201
206
|
- `isTwoUndercards` - Both hole cards rank below every board card
|
|
202
207
|
|
|
203
208
|
#### Other Categorization
|
|
204
|
-
- `isUnderPair` - Pocket pair ranked below every
|
|
209
|
+
- `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
210
|
- `isComboDraw` - Both a flush draw (not backdoor) and a straight draw
|
|
206
211
|
|
|
207
212
|
#### Draws
|
|
208
213
|
- `isFlushDraw` - Four cards of the same suit (one away from flush)
|
|
209
214
|
- `isBackdoorFlushDraw` - Three cards of the same suit (two away from flush)
|
|
210
|
-
- `isOpenEndedStraightDraw` -
|
|
211
|
-
- `isInsideStraightDraw` - One card away from straight (
|
|
215
|
+
- `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
|
|
216
|
+
- `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
217
|
- `isStraightDraw` - Any type of straight draw (open-ended or inside, standard or wheel)
|
|
213
218
|
|
|
214
219
|
**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.
|
package/evaluator.js
CHANGED
|
@@ -36,6 +36,31 @@ const HOLE_CARD_CATEGORIES = new Set([
|
|
|
36
36
|
'isTwoUndercards'
|
|
37
37
|
]);
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Draw properties. On 6+ cards they describe the whole hand rather than one 5-card combination,
|
|
41
|
+
* so strict mode collects them from every combination (see #mergeDraws).
|
|
42
|
+
*
|
|
43
|
+
* @constant {string[]}
|
|
44
|
+
*/
|
|
45
|
+
const DRAW_PROPERTIES = [
|
|
46
|
+
'isFlushDraw',
|
|
47
|
+
'isBackdoorFlushDraw',
|
|
48
|
+
'isOpenEndedStraightDraw',
|
|
49
|
+
'isInsideStraightDraw',
|
|
50
|
+
'isStraightDraw',
|
|
51
|
+
'isOpenEndedStraightDrawWheel',
|
|
52
|
+
'isInsideStraightDrawWheel',
|
|
53
|
+
'isComboDraw'
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const STRAIGHT_DRAW_PROPERTIES = [
|
|
57
|
+
'isOpenEndedStraightDraw',
|
|
58
|
+
'isInsideStraightDraw',
|
|
59
|
+
'isStraightDraw',
|
|
60
|
+
'isOpenEndedStraightDrawWheel',
|
|
61
|
+
'isInsideStraightDrawWheel'
|
|
62
|
+
];
|
|
63
|
+
|
|
39
64
|
class HandEvaluator {
|
|
40
65
|
/**
|
|
41
66
|
* Creates an instance of HandEvaluator.
|
|
@@ -65,7 +90,8 @@ class HandEvaluator {
|
|
|
65
90
|
*/
|
|
66
91
|
constructor(cards, strict = true, holeCards = null, originalCards = null) {
|
|
67
92
|
this.#validateCards(cards);
|
|
68
|
-
|
|
93
|
+
// Copy so that in-place sorting during evaluation never reorders the caller's array
|
|
94
|
+
this.cards = [...cards];
|
|
69
95
|
this.strict = strict;
|
|
70
96
|
this.originalCards = originalCards || (cards.length > 5 ? cards : null);
|
|
71
97
|
if (holeCards === null || holeCards === undefined) {
|
|
@@ -271,6 +297,7 @@ class HandEvaluator {
|
|
|
271
297
|
this.#isInsideStraightDraw();
|
|
272
298
|
this.#isStraightDraw();
|
|
273
299
|
this.#isComboDraw();
|
|
300
|
+
this.#clearDrawsForStreet();
|
|
274
301
|
this.#isUnderPair();
|
|
275
302
|
this.#isOvercards();
|
|
276
303
|
this.#isHighCard();
|
|
@@ -370,11 +397,13 @@ class HandEvaluator {
|
|
|
370
397
|
}
|
|
371
398
|
|
|
372
399
|
/**
|
|
373
|
-
* Gets
|
|
400
|
+
* Gets key card ranks for an evaluation, in tiebreak order.
|
|
401
|
+
* Ranks are grouped by how often they appear (so a full house compares its three of a kind
|
|
402
|
+
* before its pair), then sorted descending. In a wheel straight (A-2-3-4-5) the Ace counts low.
|
|
374
403
|
*
|
|
375
404
|
* @private
|
|
376
405
|
* @param {Object} evaluation - The evaluation object
|
|
377
|
-
* @returns {number[]} Array of rank values
|
|
406
|
+
* @returns {number[]} Array of rank values in tiebreak order
|
|
378
407
|
*/
|
|
379
408
|
#getKeyCardRanks(evaluation) {
|
|
380
409
|
const handStrength = {
|
|
@@ -384,8 +413,11 @@ class HandEvaluator {
|
|
|
384
413
|
};
|
|
385
414
|
for (const [hand] of Object.entries(handStrength)) {
|
|
386
415
|
if (evaluation[hand]) {
|
|
387
|
-
const
|
|
388
|
-
|
|
416
|
+
const ranks = (evaluation.keyCards[hand] || []).map(c => this.#getRankValue(c));
|
|
417
|
+
const isWheel = (hand === 'isStraight' || hand === 'isStraightFlush') && ranks.includes(13) && ranks.includes(4);
|
|
418
|
+
const values = isWheel ? ranks.map(r => r === 13 ? 0 : r) : ranks;
|
|
419
|
+
const counts = values.reduce((acc, r) => (acc[r] = (acc[r] || 0) + 1, acc), {});
|
|
420
|
+
return values.sort((a, b) => counts[b] - counts[a] || b - a);
|
|
389
421
|
}
|
|
390
422
|
}
|
|
391
423
|
return [];
|
|
@@ -457,7 +489,7 @@ class HandEvaluator {
|
|
|
457
489
|
for (const [key, evaluation] of Object.entries(result)) {
|
|
458
490
|
const strength = this.#getHandStrength(evaluation);
|
|
459
491
|
const bestStrength = bestEval ? this.#getHandStrength(bestEval) : 0;
|
|
460
|
-
if (!bestEval || strength > bestStrength || (strength === bestStrength && this.#compareEvaluations(evaluation, bestEval)
|
|
492
|
+
if (!bestEval || strength > bestStrength || (strength === bestStrength && this.#compareEvaluations(evaluation, bestEval) < 0)) {
|
|
461
493
|
bestKey = key;
|
|
462
494
|
bestEval = evaluation;
|
|
463
495
|
}
|
|
@@ -495,7 +527,70 @@ class HandEvaluator {
|
|
|
495
527
|
result[key] = tempEvaluator.evaluation[evaluationKey];
|
|
496
528
|
}
|
|
497
529
|
|
|
498
|
-
|
|
530
|
+
if (!this.strict) return result;
|
|
531
|
+
const best = this.#findBestCombination(result);
|
|
532
|
+
this.#mergeDraws(Object.values(best)[0], result);
|
|
533
|
+
return best;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Removes draws that can no longer complete, based on how many cards the full hand holds:
|
|
538
|
+
* 5 cards is the flop (two cards to come), 6 is the turn (one card to come, so a backdoor
|
|
539
|
+
* draw cannot complete) and 7 or more is the river (no cards to come, so there are no draws).
|
|
540
|
+
*
|
|
541
|
+
* @private
|
|
542
|
+
*/
|
|
543
|
+
#clearDrawsForStreet() {
|
|
544
|
+
const cardCount = (this.originalCards || this.cards).length;
|
|
545
|
+
if (cardCount >= 7) {
|
|
546
|
+
for (const draw of DRAW_PROPERTIES) this.evaluation[draw] = false;
|
|
547
|
+
} else if (cardCount === 6) {
|
|
548
|
+
this.evaluation.isBackdoorFlushDraw = false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* In strict mode with 6+ cards only the best 5-card combination is returned, but a draw can
|
|
554
|
+
* use cards outside it (e.g. the hole card that makes four to a flush). Copies onto the best
|
|
555
|
+
* evaluation every draw found in any combination, with that combination's key and kicker
|
|
556
|
+
* cards, then drops draws the made hand already beats and recomputes the combo draw for the
|
|
557
|
+
* whole hand.
|
|
558
|
+
*
|
|
559
|
+
* @private
|
|
560
|
+
* @param {Object} best - The evaluation of the best combination (modified in place)
|
|
561
|
+
* @param {Object<string, Object>} result - Evaluations of every combination
|
|
562
|
+
*/
|
|
563
|
+
#mergeDraws(best, result) {
|
|
564
|
+
const clear = draw => {
|
|
565
|
+
best[draw] = false;
|
|
566
|
+
best.keyCards[draw] = [];
|
|
567
|
+
best.kickerCards[draw] = [];
|
|
568
|
+
};
|
|
569
|
+
for (const draw of DRAW_PROPERTIES) {
|
|
570
|
+
if (draw === 'isComboDraw' || best[draw]) continue;
|
|
571
|
+
const source = Object.values(result).find(evaluation => evaluation[draw]);
|
|
572
|
+
if (!source) continue;
|
|
573
|
+
best[draw] = true;
|
|
574
|
+
best.keyCards[draw] = source.keyCards[draw];
|
|
575
|
+
best.kickerCards[draw] = source.kickerCards[draw];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// A made straight or better needs no straight draw; a made flush or better needs no flush draw
|
|
579
|
+
const strongest = this.#getHandStrength(best);
|
|
580
|
+
if (strongest >= 5) STRAIGHT_DRAW_PROPERTIES.forEach(clear);
|
|
581
|
+
if (strongest >= 6) ['isFlushDraw', 'isBackdoorFlushDraw'].forEach(clear);
|
|
582
|
+
|
|
583
|
+
if (best.isComboDraw || !(best.isFlushDraw && best.isStraightDraw)) {
|
|
584
|
+
if (!(best.isFlushDraw && best.isStraightDraw)) clear('isComboDraw');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
best.isComboDraw = true;
|
|
588
|
+
const keyCards = [...new Set([...best.keyCards.isFlushDraw, ...best.keyCards.isStraightDraw])]
|
|
589
|
+
.sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
590
|
+
best.keyCards.isComboDraw = keyCards;
|
|
591
|
+
best.kickerCards.isComboDraw = this.cards
|
|
592
|
+
.filter(c => !keyCards.includes(c))
|
|
593
|
+
.sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
499
594
|
}
|
|
500
595
|
|
|
501
596
|
/**
|
|
@@ -791,6 +886,8 @@ class HandEvaluator {
|
|
|
791
886
|
if (this.evaluation.isTopPair !== undefined) return this.evaluation.isTopPair;
|
|
792
887
|
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
793
888
|
if (!this.evaluation.isPair) return this.evaluation.isTopPair = false;
|
|
889
|
+
const position = this.#getBoardPosition(this.#getPairRank());
|
|
890
|
+
if (position !== null) return this.evaluation.isTopPair = position === 'top';
|
|
794
891
|
const rankCounts = this.#getRankCounts();
|
|
795
892
|
return this.evaluation.isTopPair = rankCounts[this.cards[0].slice(0, -1)] === 2;
|
|
796
893
|
}
|
|
@@ -809,6 +906,8 @@ class HandEvaluator {
|
|
|
809
906
|
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
810
907
|
if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
|
|
811
908
|
if (!this.evaluation.isPair || this.evaluation.isTwoPair) return this.evaluation.isMiddlePair = false;
|
|
909
|
+
const position = this.#getBoardPosition(this.#getPairRank());
|
|
910
|
+
if (position !== null) return this.evaluation.isMiddlePair = position === 'middle';
|
|
812
911
|
const ranks = this.#getRankCountsSorted();
|
|
813
912
|
return this.evaluation.isMiddlePair = ranks.length === 4 && (ranks[1].count === 2 || ranks[2].count === 2);
|
|
814
913
|
}
|
|
@@ -826,6 +925,8 @@ class HandEvaluator {
|
|
|
826
925
|
if (this.evaluation.isBottomPair !== undefined) return this.evaluation.isBottomPair;
|
|
827
926
|
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
828
927
|
if (!this.evaluation.isPair) return this.evaluation.isBottomPair = false;
|
|
928
|
+
const position = this.#getBoardPosition(this.#getPairRank());
|
|
929
|
+
if (position !== null) return this.evaluation.isBottomPair = position === 'bottom';
|
|
829
930
|
const rankCounts = this.#getRankCounts();
|
|
830
931
|
return this.evaluation.isBottomPair = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 2;
|
|
831
932
|
}
|
|
@@ -898,6 +999,8 @@ class HandEvaluator {
|
|
|
898
999
|
if (this.evaluation.isTopThreeOfAKind !== undefined) return this.evaluation.isTopThreeOfAKind;
|
|
899
1000
|
if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
|
|
900
1001
|
if (!this.evaluation.isThreeOfAKind) return this.evaluation.isTopThreeOfAKind = false;
|
|
1002
|
+
const position = this.#getBoardPosition(this.#getTripsRank());
|
|
1003
|
+
if (position !== null) return this.evaluation.isTopThreeOfAKind = position === 'top';
|
|
901
1004
|
const rankCounts = this.#getRankCounts();
|
|
902
1005
|
return this.evaluation.isTopThreeOfAKind = rankCounts[this.cards[0].slice(0, -1)] === 3;
|
|
903
1006
|
}
|
|
@@ -915,6 +1018,8 @@ class HandEvaluator {
|
|
|
915
1018
|
if (this.evaluation.isMiddleThreeOfAKind !== undefined) return this.evaluation.isMiddleThreeOfAKind;
|
|
916
1019
|
if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
|
|
917
1020
|
if (!this.evaluation.isThreeOfAKind) return this.evaluation.isMiddleThreeOfAKind = false;
|
|
1021
|
+
const position = this.#getBoardPosition(this.#getTripsRank());
|
|
1022
|
+
if (position !== null) return this.evaluation.isMiddleThreeOfAKind = position === 'middle';
|
|
918
1023
|
const ranks = this.#getRankCountsSorted();
|
|
919
1024
|
return this.evaluation.isMiddleThreeOfAKind = ranks.length === 3 && ranks[1].count === 3;
|
|
920
1025
|
}
|
|
@@ -932,6 +1037,8 @@ class HandEvaluator {
|
|
|
932
1037
|
if (this.evaluation.isBottomThreeOfAKind !== undefined) return this.evaluation.isBottomThreeOfAKind;
|
|
933
1038
|
if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
|
|
934
1039
|
if (!this.evaluation.isThreeOfAKind) return this.evaluation.isBottomThreeOfAKind = false;
|
|
1040
|
+
const position = this.#getBoardPosition(this.#getTripsRank());
|
|
1041
|
+
if (position !== null) return this.evaluation.isBottomThreeOfAKind = position === 'bottom';
|
|
935
1042
|
const rankCounts = this.#getRankCounts();
|
|
936
1043
|
return this.evaluation.isBottomThreeOfAKind = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 3;
|
|
937
1044
|
}
|
|
@@ -1088,8 +1195,8 @@ class HandEvaluator {
|
|
|
1088
1195
|
*
|
|
1089
1196
|
* @returns {boolean} True if the hand is an open-ended straight draw, false otherwise
|
|
1090
1197
|
* @example
|
|
1091
|
-
* const evaluator = new HandEvaluator(['
|
|
1092
|
-
* evaluator.isOpenEndedStraightDraw(); // Returns true (needs
|
|
1198
|
+
* const evaluator = new HandEvaluator(['Ks', 'Qs', 'Js', 'Ts', '4h']);
|
|
1199
|
+
* evaluator.isOpenEndedStraightDraw(); // Returns true (needs A or 9 to complete straight)
|
|
1093
1200
|
*/
|
|
1094
1201
|
#isOpenEndedStraightDraw() {
|
|
1095
1202
|
if (this.evaluation.isOpenEndedStraightDraw !== undefined) return this.evaluation.isOpenEndedStraightDraw;
|
|
@@ -1113,7 +1220,7 @@ class HandEvaluator {
|
|
|
1113
1220
|
|
|
1114
1221
|
/**
|
|
1115
1222
|
* Determines if the hand is an open-ended straight draw using wheel ranking.
|
|
1116
|
-
* Checks for wheel-equivalent open-ended draws
|
|
1223
|
+
* Checks for wheel-equivalent open-ended draws. A-2-3-4 needs only a 5, so it counts as an inside draw.
|
|
1117
1224
|
* Paired cards count once. Returns false if the hand is already a wheel straight.
|
|
1118
1225
|
*
|
|
1119
1226
|
* @returns {boolean} True if the hand is a wheel-equivalent open-ended straight draw, false otherwise
|
|
@@ -1700,7 +1807,9 @@ class HandEvaluator {
|
|
|
1700
1807
|
/**
|
|
1701
1808
|
* Finds 4 cards of distinct, rank-adjacent ranks whose rank span is one of the given distances.
|
|
1702
1809
|
* 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.
|
|
1810
|
+
* 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
|
|
1811
|
+
* A-2-3-4) count as inside draws, since only one rank completes them. Wheel windows must
|
|
1812
|
+
* contain the Ace. When hole cards are supplied, a
|
|
1704
1813
|
* window whose ranks are all already on the board is skipped, since a draw the board gives
|
|
1705
1814
|
* every player is not the player's draw.
|
|
1706
1815
|
*
|
|
@@ -1723,7 +1832,12 @@ class HandEvaluator {
|
|
|
1723
1832
|
}
|
|
1724
1833
|
for (let i = 0; i + 4 <= distinct.length; i++) {
|
|
1725
1834
|
const window = distinct.slice(i, i + 4);
|
|
1726
|
-
|
|
1835
|
+
// Wheel ranking only matters when the Ace plays low; other windows are standard draws
|
|
1836
|
+
if (wheel && !window.some(c => c.startsWith('A'))) continue;
|
|
1837
|
+
let distance = rankValue(window[0]) - rankValue(window[3]);
|
|
1838
|
+
// Four in a row capped by the Ace (A-K-Q-J or A-2-3-4) completes at one end only,
|
|
1839
|
+
// so it has the same 4 outs as an inside draw
|
|
1840
|
+
if (distance === 3 && window.some(c => c.startsWith('A'))) distance = 4;
|
|
1727
1841
|
if (distances.includes(distance) && this.#addsRankToBoard(window)) return window;
|
|
1728
1842
|
}
|
|
1729
1843
|
return null;
|
|
@@ -1795,7 +1909,9 @@ class HandEvaluator {
|
|
|
1795
1909
|
|
|
1796
1910
|
/**
|
|
1797
1911
|
* Determines if the hand contains an under pair (pocket pair smaller than all board cards).
|
|
1798
|
-
*
|
|
1912
|
+
* With hole cards supplied, the hole cards must be a pocket pair ranked below every board card;
|
|
1913
|
+
* this works on any street, including a 5-card flop. Without hole cards the board is unknown, so
|
|
1914
|
+
* it applies only to 6+ cards where the pair rank is smaller than all other cards.
|
|
1799
1915
|
*
|
|
1800
1916
|
* @returns {boolean} True if the hand is an under pair, false otherwise
|
|
1801
1917
|
* @example
|
|
@@ -1806,6 +1922,13 @@ class HandEvaluator {
|
|
|
1806
1922
|
#isUnderPair() {
|
|
1807
1923
|
if (this.evaluation.isUnderPair !== undefined) return this.evaluation.isUnderPair;
|
|
1808
1924
|
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
1925
|
+
if (this.evaluation.isPair && this.holeCards) {
|
|
1926
|
+
const [first, second] = this.holeCards;
|
|
1927
|
+
const holeRank = first.slice(0, -1);
|
|
1928
|
+
if (second.slice(0, -1) !== holeRank || this.#getPairRank() !== holeRank) return this.evaluation.isUnderPair = false;
|
|
1929
|
+
const board = (this.originalCards || this.cards).filter(c => !this.holeCards.includes(c));
|
|
1930
|
+
return this.evaluation.isUnderPair = board.every(c => this.#getRankValue(c) > this.#getRankValue(first));
|
|
1931
|
+
}
|
|
1809
1932
|
if (!this.evaluation.isPair || !this.originalCards || this.originalCards.length < 6) {
|
|
1810
1933
|
return this.evaluation.isUnderPair = false;
|
|
1811
1934
|
}
|
|
@@ -1829,6 +1952,42 @@ class HandEvaluator {
|
|
|
1829
1952
|
}
|
|
1830
1953
|
return null;
|
|
1831
1954
|
}
|
|
1955
|
+
|
|
1956
|
+
/**
|
|
1957
|
+
* Gets the rank of the three of a kind in the current hand.
|
|
1958
|
+
*
|
|
1959
|
+
* @private
|
|
1960
|
+
* @returns {string|null} The rank of the three of a kind, or null if none exists
|
|
1961
|
+
*/
|
|
1962
|
+
#getTripsRank() {
|
|
1963
|
+
const rankCounts = this.#getRankCounts();
|
|
1964
|
+
return Object.keys(rankCounts).find(rank => rankCounts[rank] === 3) || null;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* Classifies a paired (or tripled) rank against the whole board, including board cards outside
|
|
1969
|
+
* the current 5-card combination. Requires hole cards: without them the board is unknown and
|
|
1970
|
+
* positions fall back to the 5-card combination.
|
|
1971
|
+
*
|
|
1972
|
+
* - 'top': the rank is at or above the highest board card (top pair, or a pocket pair above the board)
|
|
1973
|
+
* - 'bottom': the rank is at or below the lowest board card
|
|
1974
|
+
* - 'middle': anything in between
|
|
1975
|
+
* - 'none': no hole card has this rank, so the pair or trips belongs to the board
|
|
1976
|
+
*
|
|
1977
|
+
* @private
|
|
1978
|
+
* @param {string|null} rank - The rank to classify
|
|
1979
|
+
* @returns {string|null} The position, or null if no hole cards were supplied
|
|
1980
|
+
*/
|
|
1981
|
+
#getBoardPosition(rank) {
|
|
1982
|
+
if (!this.holeCards) return null;
|
|
1983
|
+
if (!rank || !this.holeCards.some(c => c.slice(0, -1) === rank)) return 'none';
|
|
1984
|
+
const fullHand = this.originalCards || this.cards;
|
|
1985
|
+
const boardValues = fullHand.filter(c => !this.holeCards.includes(c)).map(c => this.#getRankValue(c));
|
|
1986
|
+
const value = this.#getRankValue(rank + 's');
|
|
1987
|
+
if (value >= Math.max(...boardValues)) return 'top';
|
|
1988
|
+
if (value <= Math.min(...boardValues)) return 'bottom';
|
|
1989
|
+
return 'middle';
|
|
1990
|
+
}
|
|
1832
1991
|
|
|
1833
1992
|
// ==================== OVERCARDS ====================
|
|
1834
1993
|
|