poker-extval 3.3.3 → 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 +5 -0
- package/evaluator.js +138 -1
- 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
|
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.
|
|
@@ -272,6 +297,7 @@ class HandEvaluator {
|
|
|
272
297
|
this.#isInsideStraightDraw();
|
|
273
298
|
this.#isStraightDraw();
|
|
274
299
|
this.#isComboDraw();
|
|
300
|
+
this.#clearDrawsForStreet();
|
|
275
301
|
this.#isUnderPair();
|
|
276
302
|
this.#isOvercards();
|
|
277
303
|
this.#isHighCard();
|
|
@@ -501,7 +527,70 @@ class HandEvaluator {
|
|
|
501
527
|
result[key] = tempEvaluator.evaluation[evaluationKey];
|
|
502
528
|
}
|
|
503
529
|
|
|
504
|
-
|
|
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));
|
|
505
594
|
}
|
|
506
595
|
|
|
507
596
|
/**
|
|
@@ -797,6 +886,8 @@ class HandEvaluator {
|
|
|
797
886
|
if (this.evaluation.isTopPair !== undefined) return this.evaluation.isTopPair;
|
|
798
887
|
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
799
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';
|
|
800
891
|
const rankCounts = this.#getRankCounts();
|
|
801
892
|
return this.evaluation.isTopPair = rankCounts[this.cards[0].slice(0, -1)] === 2;
|
|
802
893
|
}
|
|
@@ -815,6 +906,8 @@ class HandEvaluator {
|
|
|
815
906
|
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
816
907
|
if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
|
|
817
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';
|
|
818
911
|
const ranks = this.#getRankCountsSorted();
|
|
819
912
|
return this.evaluation.isMiddlePair = ranks.length === 4 && (ranks[1].count === 2 || ranks[2].count === 2);
|
|
820
913
|
}
|
|
@@ -832,6 +925,8 @@ class HandEvaluator {
|
|
|
832
925
|
if (this.evaluation.isBottomPair !== undefined) return this.evaluation.isBottomPair;
|
|
833
926
|
if (this.evaluation.isPair === undefined) this.#isPair();
|
|
834
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';
|
|
835
930
|
const rankCounts = this.#getRankCounts();
|
|
836
931
|
return this.evaluation.isBottomPair = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 2;
|
|
837
932
|
}
|
|
@@ -904,6 +999,8 @@ class HandEvaluator {
|
|
|
904
999
|
if (this.evaluation.isTopThreeOfAKind !== undefined) return this.evaluation.isTopThreeOfAKind;
|
|
905
1000
|
if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
|
|
906
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';
|
|
907
1004
|
const rankCounts = this.#getRankCounts();
|
|
908
1005
|
return this.evaluation.isTopThreeOfAKind = rankCounts[this.cards[0].slice(0, -1)] === 3;
|
|
909
1006
|
}
|
|
@@ -921,6 +1018,8 @@ class HandEvaluator {
|
|
|
921
1018
|
if (this.evaluation.isMiddleThreeOfAKind !== undefined) return this.evaluation.isMiddleThreeOfAKind;
|
|
922
1019
|
if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
|
|
923
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';
|
|
924
1023
|
const ranks = this.#getRankCountsSorted();
|
|
925
1024
|
return this.evaluation.isMiddleThreeOfAKind = ranks.length === 3 && ranks[1].count === 3;
|
|
926
1025
|
}
|
|
@@ -938,6 +1037,8 @@ class HandEvaluator {
|
|
|
938
1037
|
if (this.evaluation.isBottomThreeOfAKind !== undefined) return this.evaluation.isBottomThreeOfAKind;
|
|
939
1038
|
if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
|
|
940
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';
|
|
941
1042
|
const rankCounts = this.#getRankCounts();
|
|
942
1043
|
return this.evaluation.isBottomThreeOfAKind = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 3;
|
|
943
1044
|
}
|
|
@@ -1851,6 +1952,42 @@ class HandEvaluator {
|
|
|
1851
1952
|
}
|
|
1852
1953
|
return null;
|
|
1853
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
|
+
}
|
|
1854
1991
|
|
|
1855
1992
|
// ==================== OVERCARDS ====================
|
|
1856
1993
|
|