poker-extval 3.2.1 → 3.3.0
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 +67 -3
- package/evaluator.js +210 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A comprehensive poker hand evaluation library for Node.js that evaluates poker h
|
|
|
7
7
|
- **Comprehensive Hand Evaluation**: Evaluates all standard poker hand types (Royal Flush, Straight Flush, Four of a Kind, Full House, Flush, Straight, Three of a Kind, Two Pair, Pair, High Card)
|
|
8
8
|
- **Strict Mode (Default)**: Only marks the strongest made hand and stronger draws, preventing weaker hands from being marked when stronger ones exist
|
|
9
9
|
- **Detailed Categorization**: Categorizes pairs (Top, Middle, Bottom), two pairs, and three of a kind by position
|
|
10
|
+
- **Overcard Categorization**: With hole cards supplied, classifies them against the board (two overcards, overcard and middle card, overcard and undercard, two middle cards, middle and undercard, two undercards)
|
|
10
11
|
- **Draw Detection**: Identifies flush draws, backdoor flush draws, open-ended straight draws, and inside straight draws
|
|
11
12
|
- **Key Cards and Kicker Cards**: Provides detailed card arrays showing which cards form each hand type and which are kickers
|
|
12
13
|
- **Wheel Straight Support**: Handles Ace-low straights (A-2-3-4-5) correctly
|
|
@@ -56,6 +57,49 @@ const evaluation = evaluator.evaluation;
|
|
|
56
57
|
const nonStrictEvaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs'], false);
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
### Overcards
|
|
61
|
+
|
|
62
|
+
The overcard categorizations describe your **hole cards** relative to the **board**, so the evaluator needs to know which two of the cards are yours. Pass them as the third argument:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
const { evaluateHand } = require('poker-extval');
|
|
66
|
+
|
|
67
|
+
const board = ['Ks', 'Qh', '8d', '3c'];
|
|
68
|
+
const holeCards = ['Ad', 'Jh'];
|
|
69
|
+
|
|
70
|
+
const result = evaluateHand([...board, ...holeCards], true, holeCards);
|
|
71
|
+
const evaluation = result[Object.keys(result)[0]];
|
|
72
|
+
|
|
73
|
+
evaluation.isOvercard; // true (the ace beats every board card)
|
|
74
|
+
evaluation.isOvercardAndMiddleCard; // true (ace is an overcard, jack sits between 3 and K)
|
|
75
|
+
evaluation.isTwoOvercards; // false
|
|
76
|
+
evaluation.keyCards.isOvercard; // ['Ad']
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Each hole card is classified against the board:
|
|
80
|
+
|
|
81
|
+
| Classification | Meaning |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| Overcard | Rank is higher than every board card |
|
|
84
|
+
| Middle card | Rank sits strictly between the highest and lowest board ranks |
|
|
85
|
+
| Undercard | Rank is lower than every board card |
|
|
86
|
+
| Paired | Rank matches a board card, so it belongs to none of the above |
|
|
87
|
+
|
|
88
|
+
The two classifications combine into one (and only one) of the six categories below. If either hole card pairs the board, none of the six is true — though `isOvercard` can still be true on its own.
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
// Board: K Q 8 3
|
|
92
|
+
evaluateHand([...board, 'Ad', 'Kc'], true, ['Ad', 'Kc']); // isOvercard only (the king pairs the board)
|
|
93
|
+
evaluateHand([...board, 'Jd', 'Th'], true, ['Jd', 'Th']); // isTwoMiddleCards
|
|
94
|
+
evaluateHand([...board, '4d', '2h'], true, ['4d', '2h']); // isMiddleAndUndercard (the 4 beats the 3)
|
|
95
|
+
evaluateHand([...board, '2d', '2h'], true, ['2d', '2h']); // isTwoUndercards
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Notes:**
|
|
99
|
+
- Hole cards are optional. Without them every overcard property is `false`.
|
|
100
|
+
- 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
|
+
- 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
|
+
|
|
59
103
|
### Strict Mode
|
|
60
104
|
|
|
61
105
|
By default, the evaluator uses **strict mode** which only marks the strongest made hand and stronger draws as true. This prevents weaker hands from being marked when a stronger hand exists.
|
|
@@ -146,6 +190,19 @@ The `evaluateHand()` function returns an object where:
|
|
|
146
190
|
- `isMiddleThreeOfAKind` - Three of a kind is neither highest nor lowest
|
|
147
191
|
- `isBottomThreeOfAKind` - Three of a kind is the lowest rank
|
|
148
192
|
|
|
193
|
+
#### Overcard Categorization (requires `holeCards`; not filtered by strict mode)
|
|
194
|
+
- `isOvercard` - At least one hole card ranks above every board card
|
|
195
|
+
- `isTwoOvercards` - Both hole cards rank above every board card
|
|
196
|
+
- `isOvercardAndMiddleCard` - One overcard, one middle card
|
|
197
|
+
- `isOvercardAndUndercard` - One overcard, one undercard
|
|
198
|
+
- `isTwoMiddleCards` - Both hole cards sit strictly between the highest and lowest board ranks
|
|
199
|
+
- `isMiddleAndUndercard` - One middle card, one undercard
|
|
200
|
+
- `isTwoUndercards` - Both hole cards rank below every board card
|
|
201
|
+
|
|
202
|
+
#### Other Categorization
|
|
203
|
+
- `isUnderPair` - Pocket pair ranked below every other card in the hand (requires 6+ cards)
|
|
204
|
+
- `isComboDraw` - Both a flush draw (not backdoor) and a straight draw
|
|
205
|
+
|
|
149
206
|
#### Draws
|
|
150
207
|
- `isFlushDraw` - Four cards of the same suit (one away from flush)
|
|
151
208
|
- `isBackdoorFlushDraw` - Three cards of the same suit (two away from flush)
|
|
@@ -162,6 +219,8 @@ The `evaluateHand()` function returns an object where:
|
|
|
162
219
|
- `keyCards` - Object with keys for each hand type/draw, values are arrays of cards (sorted) that constitute that specific hand type or draw. False hand types have empty arrays `[]`.
|
|
163
220
|
- `kickerCards` - Object with keys for each hand type/draw, values are arrays of cards (sorted) that don't constitute that specific hand type or draw. False hand types have empty arrays `[]`, except `isHighCard` which contains all 5 cards when true.
|
|
164
221
|
|
|
222
|
+
**Note**: For the overcard categorizations, `keyCards` holds the qualifying hole cards and `kickerCards` holds the remaining cards of the **full hand** (the board, plus any hole card that is not a key card) rather than the remaining cards of the 5-card combination.
|
|
223
|
+
|
|
165
224
|
**Note**: Each hand type has its own `keyCards` and `kickerCards` entries. For example, a hand with both a pair and a flush draw will have separate entries in `keyCards.isPair`, `keyCards.isFlushDraw`, `kickerCards.isPair`, and `kickerCards.isFlushDraw`.
|
|
166
225
|
|
|
167
226
|
### Example Response
|
|
@@ -352,6 +411,7 @@ The evaluator validates input and throws descriptive errors for:
|
|
|
352
411
|
- Invalid rank (not A, K, Q, J, T, 9-2)
|
|
353
412
|
- Invalid suit (not s, h, d, c)
|
|
354
413
|
- Duplicate cards
|
|
414
|
+
- Hole cards that are not an array of exactly 2 distinct cards drawn from `cards`
|
|
355
415
|
|
|
356
416
|
Example:
|
|
357
417
|
```javascript
|
|
@@ -370,23 +430,25 @@ Run the test suite:
|
|
|
370
430
|
npm test
|
|
371
431
|
```
|
|
372
432
|
|
|
373
|
-
The test suite includes
|
|
433
|
+
The test suite includes 700+ tests covering:
|
|
374
434
|
- All hand types
|
|
375
435
|
- Edge cases
|
|
376
436
|
- Invalid input handling
|
|
377
437
|
- Wheel straights
|
|
378
438
|
- Draw detection
|
|
379
439
|
- Categorization logic
|
|
440
|
+
- Overcard categorization and hole card validation
|
|
380
441
|
|
|
381
442
|
## API Reference
|
|
382
443
|
|
|
383
|
-
### `evaluateHand(cards, strict)`
|
|
444
|
+
### `evaluateHand(cards, strict, holeCards)`
|
|
384
445
|
|
|
385
446
|
Main function to evaluate poker hands.
|
|
386
447
|
|
|
387
448
|
**Parameters:**
|
|
388
449
|
- `cards` (string[]): Array of 5 or more card strings
|
|
389
450
|
- `strict` (boolean, optional): If `true`, only the strongest made hand and stronger draws are marked. If `false`, all applicable hand types are marked (default: `true`).
|
|
451
|
+
- `holeCards` (string[] | null, optional): Exactly 2 cards identifying the player's hole cards, both of which must appear in `cards`. Required for the overcard categorizations; without it those properties are always `false` (default: `null`).
|
|
390
452
|
|
|
391
453
|
**Returns:**
|
|
392
454
|
- Object mapping sorted hand strings to evaluation objects
|
|
@@ -399,7 +461,7 @@ Main function to evaluate poker hands.
|
|
|
399
461
|
#### Constructor
|
|
400
462
|
|
|
401
463
|
```javascript
|
|
402
|
-
new HandEvaluator(cards, strict)
|
|
464
|
+
new HandEvaluator(cards, strict, holeCards)
|
|
403
465
|
```
|
|
404
466
|
|
|
405
467
|
Creates a new HandEvaluator instance.
|
|
@@ -407,11 +469,13 @@ Creates a new HandEvaluator instance.
|
|
|
407
469
|
**Parameters:**
|
|
408
470
|
- `cards` (string[]): Array of 5 or more card strings
|
|
409
471
|
- `strict` (boolean, optional): If `true`, only the strongest made hand and stronger draws are marked. If `false`, all applicable hand types are marked (default: `true`).
|
|
472
|
+
- `holeCards` (string[] | null, optional): Exactly 2 cards identifying the player's hole cards, both of which must appear in `cards`. Required for the overcard categorizations (default: `null`).
|
|
410
473
|
|
|
411
474
|
**Properties:**
|
|
412
475
|
- `evaluation` - Object mapping sorted hand strings to evaluation objects (same format as `evaluateHand()` return value)
|
|
413
476
|
- `cards` - Array of input cards
|
|
414
477
|
- `strict` - Boolean indicating whether strict mode is enabled
|
|
478
|
+
- `holeCards` - Array of the 2 hole cards, or `null` if none were supplied
|
|
415
479
|
|
|
416
480
|
**Note**: All internal methods are private. Use the `evaluation` property to access evaluation results.
|
|
417
481
|
|
package/evaluator.js
CHANGED
|
@@ -19,6 +19,23 @@
|
|
|
19
19
|
* const result = evaluator.evaluation;
|
|
20
20
|
* // result contains 6 key-value pairs, one for each 5-card combination
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Evaluation properties that categorize the player's hole cards relative to the board.
|
|
24
|
+
* These describe the two hole cards rather than a 5-card hand, so their kicker cards are
|
|
25
|
+
* drawn from the full hand (the board) rather than from the 5-card combination.
|
|
26
|
+
*
|
|
27
|
+
* @constant {Set<string>}
|
|
28
|
+
*/
|
|
29
|
+
const HOLE_CARD_CATEGORIES = new Set([
|
|
30
|
+
'isOvercard',
|
|
31
|
+
'isTwoOvercards',
|
|
32
|
+
'isOvercardAndMiddleCard',
|
|
33
|
+
'isOvercardAndUndercard',
|
|
34
|
+
'isTwoMiddleCards',
|
|
35
|
+
'isMiddleAndUndercard',
|
|
36
|
+
'isTwoUndercards'
|
|
37
|
+
]);
|
|
38
|
+
|
|
22
39
|
class HandEvaluator {
|
|
23
40
|
/**
|
|
24
41
|
* Creates an instance of HandEvaluator.
|
|
@@ -28,8 +45,12 @@ class HandEvaluator {
|
|
|
28
45
|
* - s is suit: s (spades), h (hearts), d (diamonds), c (clubs)
|
|
29
46
|
* @param {boolean} [strict=true] - If true, only the strongest made hand and stronger draws are marked as true.
|
|
30
47
|
* If false, all applicable hand types are marked (current behavior).
|
|
48
|
+
* @param {string[]|null} [holeCards=null] - Optional array of exactly 2 cards identifying the player's
|
|
49
|
+
* hole cards. Required for the overcard categorizations (isOvercard, isTwoOvercards, etc.); without it
|
|
50
|
+
* those properties are always false. Both cards must be present in `cards`.
|
|
31
51
|
* @throws {Error} If cards is not an array, has fewer than 5 cards, contains invalid format,
|
|
32
52
|
* invalid rank, invalid suit, or duplicate cards
|
|
53
|
+
* @throws {Error} If holeCards is provided but is not an array of exactly 2 distinct cards drawn from `cards`
|
|
33
54
|
* @example
|
|
34
55
|
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
|
|
35
56
|
* @example
|
|
@@ -38,15 +59,65 @@ class HandEvaluator {
|
|
|
38
59
|
* @example
|
|
39
60
|
* // Non-strict mode - all applicable hands marked
|
|
40
61
|
* const nonStrictEvaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs'], false);
|
|
62
|
+
* @example
|
|
63
|
+
* // With hole cards - enables overcard categorization
|
|
64
|
+
* const withHoleCards = new HandEvaluator(['Ks', 'Qh', '8d', '3c', 'Ad', 'Jh'], true, ['Ad', 'Jh']);
|
|
41
65
|
*/
|
|
42
|
-
constructor(cards, strict = true, originalCards = null) {
|
|
66
|
+
constructor(cards, strict = true, holeCards = null, originalCards = null) {
|
|
43
67
|
this.#validateCards(cards);
|
|
44
68
|
this.cards = cards;
|
|
45
69
|
this.strict = strict;
|
|
46
70
|
this.originalCards = originalCards || (cards.length > 5 ? cards : null);
|
|
71
|
+
if (holeCards === null || holeCards === undefined) {
|
|
72
|
+
this.holeCards = null;
|
|
73
|
+
} else {
|
|
74
|
+
this.#validateHoleCards(holeCards, this.originalCards || cards);
|
|
75
|
+
this.holeCards = holeCards;
|
|
76
|
+
}
|
|
47
77
|
this.evaluation = this.#evaluateAllCombinations();
|
|
48
78
|
}
|
|
49
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Validates the optional hole cards array.
|
|
82
|
+
* Hole cards must be exactly 2 distinct cards that appear in the full hand.
|
|
83
|
+
*
|
|
84
|
+
* @private
|
|
85
|
+
* @param {string[]} holeCards - Array of hole card strings to validate
|
|
86
|
+
* @param {string[]} fullHand - The complete set of cards the hole cards must be drawn from
|
|
87
|
+
* @throws {Error} If holeCards is not an array
|
|
88
|
+
* @throws {Error} If holeCards does not contain exactly 2 cards
|
|
89
|
+
* @throws {Error} If any hole card is not a string
|
|
90
|
+
* @throws {Error} If duplicate hole cards are found
|
|
91
|
+
* @throws {Error} If a hole card is not present in the full hand
|
|
92
|
+
*/
|
|
93
|
+
#validateHoleCards(holeCards, fullHand) {
|
|
94
|
+
if (!Array.isArray(holeCards)) {
|
|
95
|
+
throw new Error('Hole cards must be an array');
|
|
96
|
+
}
|
|
97
|
+
if (holeCards.length !== 2) {
|
|
98
|
+
throw new Error(`Expected exactly 2 hole cards, received ${holeCards.length}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const handSet = new Set(fullHand);
|
|
102
|
+
const seenHoleCards = new Set();
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < holeCards.length; i++) {
|
|
105
|
+
const card = holeCards[i];
|
|
106
|
+
|
|
107
|
+
if (typeof card !== 'string') {
|
|
108
|
+
throw new Error(`Hole card at index ${i} must be a string`);
|
|
109
|
+
}
|
|
110
|
+
if (seenHoleCards.has(card)) {
|
|
111
|
+
throw new Error(`Duplicate hole card found: "${card}"`);
|
|
112
|
+
}
|
|
113
|
+
seenHoleCards.add(card);
|
|
114
|
+
|
|
115
|
+
if (!handSet.has(card)) {
|
|
116
|
+
throw new Error(`Hole card not found in cards: "${card}"`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
50
121
|
/**
|
|
51
122
|
* Validates the input cards array for correct format, length, and uniqueness.
|
|
52
123
|
*
|
|
@@ -201,6 +272,7 @@ class HandEvaluator {
|
|
|
201
272
|
this.#isStraightDraw();
|
|
202
273
|
this.#isComboDraw();
|
|
203
274
|
this.#isUnderPair();
|
|
275
|
+
this.#isOvercards();
|
|
204
276
|
this.#isHighCard();
|
|
205
277
|
|
|
206
278
|
// Add keyCards and kickerCards before restoring original evaluation
|
|
@@ -418,7 +490,7 @@ class HandEvaluator {
|
|
|
418
490
|
const result = {};
|
|
419
491
|
for (const combination of combinations) {
|
|
420
492
|
const key = this.#formatHandKey(combination);
|
|
421
|
-
const tempEvaluator = new HandEvaluator(combination, this.strict, this.originalCards);
|
|
493
|
+
const tempEvaluator = new HandEvaluator(combination, this.strict, this.holeCards, this.originalCards);
|
|
422
494
|
const evaluationKey = Object.keys(tempEvaluator.evaluation)[0];
|
|
423
495
|
result[key] = tempEvaluator.evaluation[evaluationKey];
|
|
424
496
|
}
|
|
@@ -1460,6 +1532,32 @@ class HandEvaluator {
|
|
|
1460
1532
|
return this.#getKeyCardsForPair();
|
|
1461
1533
|
}
|
|
1462
1534
|
|
|
1535
|
+
/**
|
|
1536
|
+
* Gets key cards for the general overcard property (the hole cards that beat the whole board).
|
|
1537
|
+
*
|
|
1538
|
+
* @private
|
|
1539
|
+
* @returns {string[]} Array of 1 or 2 overcards if isOvercard, empty array otherwise
|
|
1540
|
+
*/
|
|
1541
|
+
#getKeyCardsForOvercard() {
|
|
1542
|
+
if (!this.evaluation.isOvercard) return [];
|
|
1543
|
+
const classifications = this.#classifyHoleCards();
|
|
1544
|
+
return this.holeCards
|
|
1545
|
+
.filter((card, i) => classifications[i] === 'over')
|
|
1546
|
+
.sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Gets key cards for a two-card hole card categorization (both hole cards, sorted high to low).
|
|
1551
|
+
*
|
|
1552
|
+
* @private
|
|
1553
|
+
* @param {string} category - The evaluation property name (e.g. 'isTwoOvercards')
|
|
1554
|
+
* @returns {string[]} Array of both hole cards if the category is true, empty array otherwise
|
|
1555
|
+
*/
|
|
1556
|
+
#getKeyCardsForHoleCardCategory(category) {
|
|
1557
|
+
if (!this.evaluation[category]) return [];
|
|
1558
|
+
return [...this.holeCards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1463
1561
|
/**
|
|
1464
1562
|
* Gets key cards for wheel straight (5 cards forming wheel).
|
|
1465
1563
|
*
|
|
@@ -1539,6 +1637,13 @@ class HandEvaluator {
|
|
|
1539
1637
|
isInsideStraightDrawWheel: this.#getKeyCardsForInsideStraightDrawWheel(),
|
|
1540
1638
|
isComboDraw: this.#getKeyCardsForComboDraw(),
|
|
1541
1639
|
isUnderPair: this.#getKeyCardsForUnderPair(),
|
|
1640
|
+
isOvercard: this.#getKeyCardsForOvercard(),
|
|
1641
|
+
isTwoOvercards: this.#getKeyCardsForHoleCardCategory('isTwoOvercards'),
|
|
1642
|
+
isOvercardAndMiddleCard: this.#getKeyCardsForHoleCardCategory('isOvercardAndMiddleCard'),
|
|
1643
|
+
isOvercardAndUndercard: this.#getKeyCardsForHoleCardCategory('isOvercardAndUndercard'),
|
|
1644
|
+
isTwoMiddleCards: this.#getKeyCardsForHoleCardCategory('isTwoMiddleCards'),
|
|
1645
|
+
isMiddleAndUndercard: this.#getKeyCardsForHoleCardCategory('isMiddleAndUndercard'),
|
|
1646
|
+
isTwoUndercards: this.#getKeyCardsForHoleCardCategory('isTwoUndercards'),
|
|
1542
1647
|
isHighCard: this.#getKeyCardsForHighCard()
|
|
1543
1648
|
};
|
|
1544
1649
|
}
|
|
@@ -1559,6 +1664,14 @@ class HandEvaluator {
|
|
|
1559
1664
|
result[handType] = sortedCards;
|
|
1560
1665
|
} else if (keyCards.length === 0) {
|
|
1561
1666
|
result[handType] = [];
|
|
1667
|
+
} else if (HOLE_CARD_CATEGORIES.has(handType)) {
|
|
1668
|
+
// Hole card categorizations are evaluated against the board, which may include cards
|
|
1669
|
+
// outside the current 5-card combination, so kickers come from the full hand.
|
|
1670
|
+
const keyCardsSet = new Set(keyCards);
|
|
1671
|
+
const fullHand = this.originalCards || this.cards;
|
|
1672
|
+
result[handType] = fullHand
|
|
1673
|
+
.filter(c => !keyCardsSet.has(c))
|
|
1674
|
+
.sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
1562
1675
|
} else {
|
|
1563
1676
|
const keyCardsSet = new Set(keyCards);
|
|
1564
1677
|
result[handType] = this.cards
|
|
@@ -1667,6 +1780,95 @@ class HandEvaluator {
|
|
|
1667
1780
|
return null;
|
|
1668
1781
|
}
|
|
1669
1782
|
|
|
1783
|
+
// ==================== OVERCARDS ====================
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* Classifies each hole card relative to the board (every card in the full hand that is not a
|
|
1787
|
+
* hole card). A card is an "overcard" when its rank beats every board card, an "undercard" when
|
|
1788
|
+
* every board card beats it, and a "middle card" when it falls strictly between the highest and
|
|
1789
|
+
* lowest board ranks. A hole card whose rank matches any board rank pairs the board and is
|
|
1790
|
+
* classified as "paired", which belongs to none of the three categories.
|
|
1791
|
+
*
|
|
1792
|
+
* @private
|
|
1793
|
+
* @returns {string[]|null} Array of 2 classifications ('over', 'middle', 'under' or 'paired')
|
|
1794
|
+
* parallel to this.holeCards, or null if hole cards were not supplied
|
|
1795
|
+
* @example
|
|
1796
|
+
* // Board: K Q 8 3, hole cards: A J
|
|
1797
|
+
* this.#classifyHoleCards(); // Returns ['over', 'middle']
|
|
1798
|
+
*/
|
|
1799
|
+
#classifyHoleCards() {
|
|
1800
|
+
if (!this.holeCards || this.holeCards.length !== 2) return null;
|
|
1801
|
+
|
|
1802
|
+
const fullHand = this.originalCards || this.cards;
|
|
1803
|
+
const holeCardSet = new Set(this.holeCards);
|
|
1804
|
+
const board = fullHand.filter(c => !holeCardSet.has(c));
|
|
1805
|
+
if (board.length < 3) return null;
|
|
1806
|
+
|
|
1807
|
+
const boardRanks = new Set(board.map(c => c.slice(0, -1)));
|
|
1808
|
+
const boardValues = board.map(c => this.#getRankValue(c));
|
|
1809
|
+
const highestBoard = Math.max(...boardValues);
|
|
1810
|
+
const lowestBoard = Math.min(...boardValues);
|
|
1811
|
+
|
|
1812
|
+
return this.holeCards.map(card => {
|
|
1813
|
+
if (boardRanks.has(card.slice(0, -1))) return 'paired';
|
|
1814
|
+
const value = this.#getRankValue(card);
|
|
1815
|
+
if (value > highestBoard) return 'over';
|
|
1816
|
+
if (value < lowestBoard) return 'under';
|
|
1817
|
+
return 'middle';
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* Counts how many hole cards fall into each classification.
|
|
1823
|
+
*
|
|
1824
|
+
* @private
|
|
1825
|
+
* @returns {Object} Object with 'over', 'middle', 'under' and 'paired' counts
|
|
1826
|
+
*/
|
|
1827
|
+
#getHoleCardCounts() {
|
|
1828
|
+
const counts = { over: 0, middle: 0, under: 0, paired: 0 };
|
|
1829
|
+
const classifications = this.#classifyHoleCards();
|
|
1830
|
+
if (classifications) {
|
|
1831
|
+
for (const classification of classifications) counts[classification]++;
|
|
1832
|
+
}
|
|
1833
|
+
return counts;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Determines how the player's hole cards rank against the board and sets every overcard
|
|
1838
|
+
* categorization property. Requires hole cards to have been supplied; without them all
|
|
1839
|
+
* overcard properties are false.
|
|
1840
|
+
*
|
|
1841
|
+
* These categorizations describe the hole cards themselves, not the made hand, so they are
|
|
1842
|
+
* identical across every 5-card combination of a hand and are not filtered by strict mode.
|
|
1843
|
+
*
|
|
1844
|
+
* Sets: isOvercard, isTwoOvercards, isOvercardAndMiddleCard, isOvercardAndUndercard,
|
|
1845
|
+
* isTwoMiddleCards, isMiddleAndUndercard, isTwoUndercards.
|
|
1846
|
+
*
|
|
1847
|
+
* @returns {boolean} True if at least one hole card is an overcard, false otherwise
|
|
1848
|
+
* @example
|
|
1849
|
+
* // Board: K Q 8 3, Hand: AJ
|
|
1850
|
+
* const evaluator = new HandEvaluator(['Ks', 'Qh', '8d', '3c', 'Ad', 'Jh'], true, ['Ad', 'Jh']);
|
|
1851
|
+
* // isOvercard: true, isOvercardAndMiddleCard: true
|
|
1852
|
+
* @example
|
|
1853
|
+
* // Board: 9 7 4 2, Hand: AK (both hole cards beat every board card)
|
|
1854
|
+
* const twoOvercards = new HandEvaluator(['9s', '7h', '4d', '2c', 'Ad', 'Kh'], true, ['Ad', 'Kh']);
|
|
1855
|
+
* // isTwoOvercards: true
|
|
1856
|
+
*/
|
|
1857
|
+
#isOvercards() {
|
|
1858
|
+
if (this.evaluation.isOvercard !== undefined) return this.evaluation.isOvercard;
|
|
1859
|
+
|
|
1860
|
+
const counts = this.#getHoleCardCounts();
|
|
1861
|
+
|
|
1862
|
+
this.evaluation.isTwoOvercards = counts.over === 2;
|
|
1863
|
+
this.evaluation.isOvercardAndMiddleCard = counts.over === 1 && counts.middle === 1;
|
|
1864
|
+
this.evaluation.isOvercardAndUndercard = counts.over === 1 && counts.under === 1;
|
|
1865
|
+
this.evaluation.isTwoMiddleCards = counts.middle === 2;
|
|
1866
|
+
this.evaluation.isMiddleAndUndercard = counts.middle === 1 && counts.under === 1;
|
|
1867
|
+
this.evaluation.isTwoUndercards = counts.under === 2;
|
|
1868
|
+
|
|
1869
|
+
return this.evaluation.isOvercard = counts.over > 0;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1670
1872
|
/**
|
|
1671
1873
|
* Determines if the hand is high card only (no pairs, two pair, three of a kind, full house,
|
|
1672
1874
|
* four of a kind, flush, or straight).
|
|
@@ -1696,10 +1898,14 @@ class HandEvaluator {
|
|
|
1696
1898
|
* @param {string[]} cards - Array of 5 or more card strings in format "Rs" where R is rank (A, K, Q, J, T, 9, 8, 7, 6, 5, 4, 3, 2) and s is suit (s, h, d, c)
|
|
1697
1899
|
* @param {boolean} [strict=true] - If true, only the strongest made hand and stronger draws are marked as true.
|
|
1698
1900
|
* If false, all applicable hand types are marked.
|
|
1901
|
+
* @param {string[]|null} [holeCards=null] - Optional array of exactly 2 cards identifying the player's hole cards.
|
|
1902
|
+
* Required for the overcard categorizations (isOvercard, isTwoOvercards, isOvercardAndMiddleCard,
|
|
1903
|
+
* isOvercardAndUndercard, isTwoMiddleCards, isMiddleAndUndercard, isTwoUndercards); without it those
|
|
1904
|
+
* properties are always false. Both cards must be present in `cards`.
|
|
1699
1905
|
* @returns {Object} Object mapping sorted hand strings (e.g., "As, Ks, Qs, Js, Ts") to evaluation objects with boolean fields for all hand types and draws
|
|
1700
1906
|
*/
|
|
1701
|
-
function evaluateHand(cards, strict = true) {
|
|
1702
|
-
const evaluator = new HandEvaluator(cards, strict);
|
|
1907
|
+
function evaluateHand(cards, strict = true, holeCards = null) {
|
|
1908
|
+
const evaluator = new HandEvaluator(cards, strict, holeCards);
|
|
1703
1909
|
return evaluator.evaluation;
|
|
1704
1910
|
}
|
|
1705
1911
|
|