poker-extval 1.0.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/LICENSE +22 -0
- package/README.md +306 -0
- package/evaluator.js +984 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Darragh Geoghegan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Poker Hand Evaluator
|
|
2
|
+
|
|
3
|
+
A comprehensive poker hand evaluation library for Node.js that evaluates poker hands from 5 or more cards and identifies all possible made hands and draws. The library generates all possible 5-card combinations from the input cards and evaluates each combination, providing detailed categorization of hand types.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
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
|
+
- **Detailed Categorization**: Categorizes pairs (Top, Middle, Bottom), two pairs, and three of a kind by position
|
|
9
|
+
- **Draw Detection**: Identifies flush draws, backdoor flush draws, open-ended straight draws, and inside straight draws
|
|
10
|
+
- **Wheel Straight Support**: Handles Ace-low straights (A-2-3-4-5) correctly
|
|
11
|
+
- **Multiple Card Support**: Accepts 5 or more cards and evaluates all possible 5-card combinations
|
|
12
|
+
- **Extensive Documentation**: Full JSDoc comments for all methods
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install poker-extval
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic Usage
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
const { evaluateHand } = require('poker-extval');
|
|
26
|
+
|
|
27
|
+
// Evaluate a 5-card hand
|
|
28
|
+
const result = evaluateHand(['As', 'Ks', 'Qs', 'Js', 'Ts']);
|
|
29
|
+
console.log(result);
|
|
30
|
+
// {
|
|
31
|
+
// "As, Ks, Qs, Js, Ts": {
|
|
32
|
+
// "isRoyalFlush": true,
|
|
33
|
+
// "isStraightFlush": false,
|
|
34
|
+
// "isFlush": true,
|
|
35
|
+
// "isStraight": true,
|
|
36
|
+
// ...
|
|
37
|
+
// }
|
|
38
|
+
// }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Using the HandEvaluator Class
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
const { HandEvaluator } = require('poker-extval');
|
|
45
|
+
|
|
46
|
+
// Create an evaluator instance
|
|
47
|
+
const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts', '9s']);
|
|
48
|
+
|
|
49
|
+
// Access evaluation results
|
|
50
|
+
const evaluation = evaluator.evaluation;
|
|
51
|
+
// Object with keys for each 5-card combination
|
|
52
|
+
|
|
53
|
+
// Use utility methods
|
|
54
|
+
evaluator.sort(); // Sort cards by rank
|
|
55
|
+
evaluator.getDistance(); // Get distance between highest and lowest card
|
|
56
|
+
evaluator.countSuits(); // Count suits in hand
|
|
57
|
+
evaluator.countRanks(); // Count ranks in hand
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Evaluating Multiple Card Hands
|
|
61
|
+
|
|
62
|
+
When you provide more than 5 cards, the evaluator generates all possible 5-card combinations:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// 6 cards = 6 combinations (C(6,5) = 6)
|
|
66
|
+
const result6 = evaluateHand(['As', 'Ks', 'Qs', 'Js', 'Ts', '9s']);
|
|
67
|
+
console.log(Object.keys(result6).length); // 6
|
|
68
|
+
|
|
69
|
+
// 7 cards = 21 combinations (C(7,5) = 21)
|
|
70
|
+
const result7 = evaluateHand(['As', 'Ks', 'Qs', 'Js', 'Ts', '9s', '8s']);
|
|
71
|
+
console.log(Object.keys(result7).length); // 21
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Card Format
|
|
75
|
+
|
|
76
|
+
Cards must be strings in the format `"Rs"` where:
|
|
77
|
+
- **R** is the rank: `A`, `K`, `Q`, `J`, `T`, `9`, `8`, `7`, `6`, `5`, `4`, `3`, `2`
|
|
78
|
+
- **s** is the suit: `s` (spades), `h` (hearts), `d` (diamonds), `c` (clubs)
|
|
79
|
+
|
|
80
|
+
**Note**: The ten card is represented as `T`, not `10`.
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
- `"As"` = Ace of Spades
|
|
84
|
+
- `"Kh"` = King of Hearts
|
|
85
|
+
- `"Td"` = Ten of Diamonds
|
|
86
|
+
- `"2c"` = Two of Clubs
|
|
87
|
+
|
|
88
|
+
## Response Object Format
|
|
89
|
+
|
|
90
|
+
The `evaluateHand()` function returns an object where:
|
|
91
|
+
|
|
92
|
+
- **Keys**: Sorted hand strings in comma-space separated format (e.g., `"As, Ks, Qs, Js, Ts"`)
|
|
93
|
+
- **Values**: Evaluation objects containing boolean properties for all hand types
|
|
94
|
+
|
|
95
|
+
### Evaluation Object Properties
|
|
96
|
+
|
|
97
|
+
#### Made Hands
|
|
98
|
+
- `isRoyalFlush` - A-K-Q-J-T all same suit
|
|
99
|
+
- `isStraightFlush` - Five consecutive ranks, all same suit (not royal)
|
|
100
|
+
- `isFourOfAKind` - Four cards of the same rank
|
|
101
|
+
- `isFullHouse` - Three of one rank, two of another
|
|
102
|
+
- `isFlush` - All five cards same suit
|
|
103
|
+
- `isStraight` - Five consecutive ranks (includes wheel: A-2-3-4-5)
|
|
104
|
+
- `isThreeOfAKind` - Three cards of the same rank
|
|
105
|
+
- `isTwoPair` - Two pairs of different ranks
|
|
106
|
+
- `isPair` - Exactly one pair
|
|
107
|
+
- `isHighCard` - No made hand
|
|
108
|
+
|
|
109
|
+
#### Pair Categorization (only true if `isPair` is true)
|
|
110
|
+
- `isTopPair` - Pair is the highest card
|
|
111
|
+
- `isMiddlePair` - Pair is neither highest nor lowest
|
|
112
|
+
- `isBottomPair` - Pair is the lowest card
|
|
113
|
+
|
|
114
|
+
#### Two Pair Categorization (only true if `isTwoPair` is true)
|
|
115
|
+
- `isTopAndMiddlePair` - Both pairs are in top two ranks
|
|
116
|
+
- `isTopAndBottomPair` - One pair is highest, other is lowest
|
|
117
|
+
- `isMiddleAndBottomPair` - Both pairs are in middle and bottom ranks
|
|
118
|
+
|
|
119
|
+
#### Three of a Kind Categorization (only true if `isThreeOfAKind` is true)
|
|
120
|
+
- `isTopThreeOfAKind` - Three of a kind is the highest rank
|
|
121
|
+
- `isMiddleThreeOfAKind` - Three of a kind is neither highest nor lowest
|
|
122
|
+
- `isBottomThreeOfAKind` - Three of a kind is the lowest rank
|
|
123
|
+
|
|
124
|
+
#### Draws
|
|
125
|
+
- `isFlushDraw` - Four cards of the same suit (one away from flush)
|
|
126
|
+
- `isBackdoorFlushDraw` - Three cards of the same suit (two away from flush)
|
|
127
|
+
- `isOpenEndedStraightDraw` - One card away from straight on either end
|
|
128
|
+
- `isInsideStraightDraw` - One card away from straight (needed card in middle)
|
|
129
|
+
|
|
130
|
+
#### Wheel Straight Support
|
|
131
|
+
- `isStraightWheel` - Wheel straight (A-2-3-4-5)
|
|
132
|
+
- `isOpenEndedStraightDrawWheel` - Open-ended draw using wheel ranking
|
|
133
|
+
- `isInsideStraightDrawWheel` - Inside draw using wheel ranking
|
|
134
|
+
|
|
135
|
+
### Example Response
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
{
|
|
139
|
+
"As, Ks, Qs, Js, Ts": {
|
|
140
|
+
"isFourOfAKind": false,
|
|
141
|
+
"isFullHouse": false,
|
|
142
|
+
"isThreeOfAKind": false,
|
|
143
|
+
"isTwoPair": false,
|
|
144
|
+
"isPair": false,
|
|
145
|
+
"isFlush": true,
|
|
146
|
+
"isStraight": true,
|
|
147
|
+
"isRoyalFlush": true,
|
|
148
|
+
"isStraightFlush": true,
|
|
149
|
+
"isTopPair": false,
|
|
150
|
+
"isMiddlePair": false,
|
|
151
|
+
"isBottomPair": false,
|
|
152
|
+
"isTopAndMiddlePair": false,
|
|
153
|
+
"isTopAndBottomPair": false,
|
|
154
|
+
"isMiddleAndBottomPair": false,
|
|
155
|
+
"isTopThreeOfAKind": false,
|
|
156
|
+
"isMiddleThreeOfAKind": false,
|
|
157
|
+
"isBottomThreeOfAKind": false,
|
|
158
|
+
"isFlushDraw": false,
|
|
159
|
+
"isBackdoorFlushDraw": false,
|
|
160
|
+
"isOpenEndedStraightDraw": false,
|
|
161
|
+
"isInsideStraightDraw": false,
|
|
162
|
+
"isHighCard": false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Performance Benchmarks
|
|
168
|
+
|
|
169
|
+
This library prioritizes **extensiveness and accuracy over raw speed**. The evaluation system is designed to provide comprehensive analysis of all possible hand combinations, which means:
|
|
170
|
+
|
|
171
|
+
- **5-card hands**: Evaluated directly (~80,000 ops/sec)
|
|
172
|
+
- **6-card hands**: Generates 6 combinations (~11,000 ops/sec)
|
|
173
|
+
- **7-card hands**: Generates 21 combinations (~3,000 ops/sec)
|
|
174
|
+
|
|
175
|
+
### Running Benchmarks
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm run benchmark
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The benchmark suite includes:
|
|
182
|
+
- Hand type evaluation performance
|
|
183
|
+
- Card count comparisons (5, 6, 7 cards)
|
|
184
|
+
- Individual method performance
|
|
185
|
+
- Utility method performance
|
|
186
|
+
- Combination generation performance
|
|
187
|
+
- Complete scenario: 1326 x 7-card hands (27,846 total combinations)
|
|
188
|
+
|
|
189
|
+
### Benchmark Results Summary
|
|
190
|
+
|
|
191
|
+
Typical performance on modern hardware:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
5-card evaluation: ~0.01-0.02ms per hand
|
|
195
|
+
6-card evaluation: ~0.10-0.15ms per hand (6 combinations)
|
|
196
|
+
7-card evaluation: ~0.30-0.50ms per hand (21 combinations)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Note**: Performance may vary based on:
|
|
200
|
+
- Hardware specifications
|
|
201
|
+
- Node.js version
|
|
202
|
+
- System load
|
|
203
|
+
- Hand complexity (some hand types require more computation)
|
|
204
|
+
|
|
205
|
+
The library is optimized for correctness and comprehensive evaluation rather than maximum speed. For applications requiring extremely high throughput, consider caching strategies or specialized optimizations.
|
|
206
|
+
|
|
207
|
+
## Error Handling
|
|
208
|
+
|
|
209
|
+
The evaluator validates input and throws descriptive errors for:
|
|
210
|
+
|
|
211
|
+
- Non-array input
|
|
212
|
+
- Fewer than 5 cards
|
|
213
|
+
- Invalid card format
|
|
214
|
+
- Invalid rank (not A, K, Q, J, T, 9-2)
|
|
215
|
+
- Invalid suit (not s, h, d, c)
|
|
216
|
+
- Duplicate cards
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
```javascript
|
|
220
|
+
try {
|
|
221
|
+
evaluateHand(['As', 'Ks', 'Qs', 'Js']); // Only 4 cards
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(error.message); // "Expected at least 5 cards, received 4"
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Testing
|
|
228
|
+
|
|
229
|
+
Run the test suite:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npm test
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The test suite includes 572+ tests covering:
|
|
236
|
+
- All hand types
|
|
237
|
+
- Edge cases
|
|
238
|
+
- Invalid input handling
|
|
239
|
+
- Wheel straights
|
|
240
|
+
- Draw detection
|
|
241
|
+
- Categorization logic
|
|
242
|
+
|
|
243
|
+
## API Reference
|
|
244
|
+
|
|
245
|
+
### `evaluateHand(cards)`
|
|
246
|
+
|
|
247
|
+
Main function to evaluate poker hands.
|
|
248
|
+
|
|
249
|
+
**Parameters:**
|
|
250
|
+
- `cards` (string[]): Array of 5 or more card strings
|
|
251
|
+
|
|
252
|
+
**Returns:**
|
|
253
|
+
- Object mapping sorted hand strings to evaluation objects
|
|
254
|
+
|
|
255
|
+
**Throws:**
|
|
256
|
+
- Error if input is invalid
|
|
257
|
+
|
|
258
|
+
### `HandEvaluator` Class
|
|
259
|
+
|
|
260
|
+
#### Constructor
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
new HandEvaluator(cards)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Creates a new HandEvaluator instance.
|
|
267
|
+
|
|
268
|
+
#### Methods
|
|
269
|
+
|
|
270
|
+
- `sort(cards?)` - Sort cards by rank (highest to lowest)
|
|
271
|
+
- `sortWheel(cards?)` - Sort cards using wheel ranking (Ace low)
|
|
272
|
+
- `getDistance(cards?)` - Calculate distance between highest and lowest card
|
|
273
|
+
- `getDistanceWheel(cards?)` - Calculate distance using wheel ranking
|
|
274
|
+
- `countSuits()` - Count occurrences of each suit
|
|
275
|
+
- `countRanks()` - Count occurrences of each rank (sorted)
|
|
276
|
+
|
|
277
|
+
All evaluation methods (e.g., `isPair()`, `isFlush()`, etc.) are available but typically accessed through the `evaluation` property.
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
MIT License
|
|
282
|
+
|
|
283
|
+
Copyright (c) 2025 Darragh Geoghegan
|
|
284
|
+
|
|
285
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
286
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
287
|
+
in the Software without restriction, including without limitation the rights
|
|
288
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
289
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
290
|
+
furnished to do so, subject to the following conditions:
|
|
291
|
+
|
|
292
|
+
The above copyright notice and this permission notice shall be included in all
|
|
293
|
+
copies or substantial portions of the Software.
|
|
294
|
+
|
|
295
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
296
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
297
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
298
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
299
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
300
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
301
|
+
SOFTWARE.
|
|
302
|
+
|
|
303
|
+
## Contributing
|
|
304
|
+
|
|
305
|
+
Contributions are welcome! Please ensure all tests pass and add tests for new features.
|
|
306
|
+
|
package/evaluator.js
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poker Hand Evaluator
|
|
3
|
+
*
|
|
4
|
+
* Evaluates poker hands from 5 or more cards and identifies all possible made hands and draws.
|
|
5
|
+
* For inputs with more than 5 cards, generates all possible 5-card combinations and evaluates each one.
|
|
6
|
+
*
|
|
7
|
+
* @license MIT
|
|
8
|
+
* @copyright Copyright (c) 2025 Darragh Geoghegan
|
|
9
|
+
* @class HandEvaluator
|
|
10
|
+
* @example
|
|
11
|
+
* // Evaluate a 5-card hand
|
|
12
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
|
|
13
|
+
* const result = evaluator.evaluation;
|
|
14
|
+
* // result = { "As, Ks, Qs, Js, Ts": { isRoyalFlush: true, isStraight: true, ... } }
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Evaluate a 6-card hand (generates 6 combinations)
|
|
18
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts', '9s']);
|
|
19
|
+
* const result = evaluator.evaluation;
|
|
20
|
+
* // result contains 6 key-value pairs, one for each 5-card combination
|
|
21
|
+
*/
|
|
22
|
+
class HandEvaluator {
|
|
23
|
+
/**
|
|
24
|
+
* Creates an instance of HandEvaluator.
|
|
25
|
+
*
|
|
26
|
+
* @param {string[]} cards - Array of 5 or more card strings in format "Rs" where:
|
|
27
|
+
* - R is rank: A, K, Q, J, T, 9, 8, 7, 6, 5, 4, 3, 2
|
|
28
|
+
* - s is suit: s (spades), h (hearts), d (diamonds), c (clubs)
|
|
29
|
+
* @throws {Error} If cards is not an array, has fewer than 5 cards, contains invalid format,
|
|
30
|
+
* invalid rank, invalid suit, or duplicate cards
|
|
31
|
+
* @example
|
|
32
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
|
|
33
|
+
*/
|
|
34
|
+
constructor(cards) {
|
|
35
|
+
this.#validateCards(cards);
|
|
36
|
+
this.cards = cards;
|
|
37
|
+
this.evaluation = this.#evaluateAllCombinations();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates the input cards array for correct format, length, and uniqueness.
|
|
42
|
+
*
|
|
43
|
+
* @private
|
|
44
|
+
* @param {string[]} cards - Array of card strings to validate
|
|
45
|
+
* @throws {Error} If cards is not an array
|
|
46
|
+
* @throws {Error} If cards array has fewer than 5 cards
|
|
47
|
+
* @throws {Error} If any card is not a string
|
|
48
|
+
* @throws {Error} If any card has invalid format (less than 2 characters)
|
|
49
|
+
* @throws {Error} If any card has an invalid rank (not A, K, Q, J, T, 9-2)
|
|
50
|
+
* @throws {Error} If any card has an invalid suit (not s, h, d, c)
|
|
51
|
+
* @throws {Error} If duplicate cards are found
|
|
52
|
+
*/
|
|
53
|
+
#validateCards(cards) {
|
|
54
|
+
if (!Array.isArray(cards)) {
|
|
55
|
+
throw new Error('Cards must be an array');
|
|
56
|
+
}
|
|
57
|
+
if (cards.length < 5) {
|
|
58
|
+
throw new Error(`Expected at least 5 cards, received ${cards.length}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const validRanks = new Set(['A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2']);
|
|
62
|
+
const validSuits = new Set(['s', 'h', 'd', 'c']);
|
|
63
|
+
const seenCards = new Set();
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < cards.length; i++) {
|
|
66
|
+
const card = cards[i];
|
|
67
|
+
|
|
68
|
+
if (typeof card !== 'string') {
|
|
69
|
+
throw new Error(`Card at index ${i} must be a string`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if card has at least 2 characters (rank + suit)
|
|
73
|
+
if (card.length < 2) {
|
|
74
|
+
throw new Error(`Card at index ${i} has invalid format: "${card}"`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for duplicate cards
|
|
78
|
+
if (seenCards.has(card)) {
|
|
79
|
+
throw new Error(`Duplicate card found: "${card}"`);
|
|
80
|
+
}
|
|
81
|
+
seenCards.add(card);
|
|
82
|
+
|
|
83
|
+
// Extract rank and suit
|
|
84
|
+
// All ranks are single characters (A, K, Q, J, T, 9-2)
|
|
85
|
+
let rank, suit;
|
|
86
|
+
if (card.length >= 2) {
|
|
87
|
+
rank = card.slice(0, -1);
|
|
88
|
+
suit = card.slice(-1);
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(`Card at index ${i} has invalid format: "${card}"`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!validRanks.has(rank)) {
|
|
94
|
+
throw new Error(`Card at index ${i} has invalid rank: "${rank}"`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!validSuits.has(suit)) {
|
|
98
|
+
throw new Error(`Card at index ${i} has invalid suit: "${suit}"`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generates all possible combinations of a given size from an array of cards.
|
|
105
|
+
* Uses recursive algorithm to generate combinations without repetition.
|
|
106
|
+
*
|
|
107
|
+
* @private
|
|
108
|
+
* @param {string[]} cards - Array of card strings
|
|
109
|
+
* @param {number} size - Size of each combination (must be 5 for poker hands)
|
|
110
|
+
* @returns {string[][]} Array of arrays, where each inner array is a combination of cards
|
|
111
|
+
* @example
|
|
112
|
+
* // Generate all 5-card combinations from 6 cards
|
|
113
|
+
* this.#generateCombinations(['As', 'Ks', 'Qs', 'Js', 'Ts', '9s'], 5)
|
|
114
|
+
* // Returns 6 combinations: C(6,5) = 6
|
|
115
|
+
*/
|
|
116
|
+
#generateCombinations(cards, size) {
|
|
117
|
+
if (size === 0) return [[]];
|
|
118
|
+
if (cards.length < size) return [];
|
|
119
|
+
if (cards.length === size) return [cards];
|
|
120
|
+
|
|
121
|
+
const combinations = [];
|
|
122
|
+
for (let i = 0; i <= cards.length - size; i++) {
|
|
123
|
+
const head = cards[i];
|
|
124
|
+
const tail = cards.slice(i + 1);
|
|
125
|
+
const tailCombinations = this.#generateCombinations(tail, size - 1);
|
|
126
|
+
for (const combo of tailCombinations) {
|
|
127
|
+
combinations.push([head, ...combo]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return combinations;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Formats a hand of cards into a sorted, comma-space separated string key.
|
|
135
|
+
* Cards are sorted from highest to lowest rank (A, K, Q, J, T, 9, 8, 7, 6, 5, 4, 3, 2).
|
|
136
|
+
*
|
|
137
|
+
* @private
|
|
138
|
+
* @param {string[]} cards - Array of 5 card strings
|
|
139
|
+
* @returns {string} Formatted string like "As, Ks, Qs, Js, Ts"
|
|
140
|
+
* @example
|
|
141
|
+
* this.#formatHandKey(['Ts', 'As', 'Ks', 'Qs', 'Js'])
|
|
142
|
+
* // Returns "As, Ks, Qs, Js, Ts"
|
|
143
|
+
*/
|
|
144
|
+
#formatHandKey(cards) {
|
|
145
|
+
const sortedCards = [...cards];
|
|
146
|
+
sortedCards.sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
147
|
+
return sortedCards.join(', ');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Evaluates a single 5-card hand and returns an evaluation object with all hand types and draws.
|
|
152
|
+
* Temporarily sets this.evaluation to an empty object to allow evaluation methods to cache results.
|
|
153
|
+
*
|
|
154
|
+
* @private
|
|
155
|
+
* @returns {Object} Evaluation object with boolean properties for all hand types:
|
|
156
|
+
* - Made hands: isFourOfAKind, isFullHouse, isThreeOfAKind, isTwoPair, isPair, isFlush,
|
|
157
|
+
* isStraight, isRoyalFlush, isStraightFlush, isHighCard
|
|
158
|
+
* - Pair categorization: isTopPair, isMiddlePair, isBottomPair
|
|
159
|
+
* - Two pair categorization: isTopAndMiddlePair, isTopAndBottomPair, isMiddleAndBottomPair
|
|
160
|
+
* - Three of a kind categorization: isTopThreeOfAKind, isMiddleThreeOfAKind, isBottomThreeOfAKind
|
|
161
|
+
* - Draws: isFlushDraw, isBackdoorFlushDraw, isOpenEndedStraightDraw, isInsideStraightDraw
|
|
162
|
+
*/
|
|
163
|
+
#evaluateSingleHand() {
|
|
164
|
+
// Temporarily set this.evaluation to an empty object for the evaluation methods to use
|
|
165
|
+
const originalEvaluation = this.evaluation;
|
|
166
|
+
this.evaluation = {};
|
|
167
|
+
|
|
168
|
+
// Call all evaluation methods - they will populate this.evaluation
|
|
169
|
+
this.isFourOfAKind();
|
|
170
|
+
this.isFullHouse();
|
|
171
|
+
this.isThreeOfAKind();
|
|
172
|
+
this.isTwoPair();
|
|
173
|
+
this.isPair();
|
|
174
|
+
this.isFlush();
|
|
175
|
+
this.isTopPair();
|
|
176
|
+
this.isMiddlePair();
|
|
177
|
+
this.isBottomPair();
|
|
178
|
+
this.isTopAndMiddlePair();
|
|
179
|
+
this.isTopAndBottomPair();
|
|
180
|
+
this.isMiddleAndBottomPair();
|
|
181
|
+
this.isTopThreeOfAKind();
|
|
182
|
+
this.isMiddleThreeOfAKind();
|
|
183
|
+
this.isBottomThreeOfAKind();
|
|
184
|
+
this.isFlushDraw();
|
|
185
|
+
this.isBackdoorFlushDraw();
|
|
186
|
+
this.isStraight();
|
|
187
|
+
this.isRoyalFlush();
|
|
188
|
+
this.isStraightFlush();
|
|
189
|
+
this.isOpenEndedStraightDraw();
|
|
190
|
+
this.isInsideStraightDraw();
|
|
191
|
+
this.isHighCard();
|
|
192
|
+
|
|
193
|
+
// Get the evaluation object and restore original
|
|
194
|
+
const evaluation = this.evaluation;
|
|
195
|
+
this.evaluation = originalEvaluation;
|
|
196
|
+
return evaluation;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Evaluates all possible 5-card combinations from the input cards.
|
|
201
|
+
* For exactly 5 cards, evaluates directly. For more cards, generates all combinations
|
|
202
|
+
* and evaluates each one, returning an object mapping formatted hand strings to evaluation objects.
|
|
203
|
+
*
|
|
204
|
+
* @private
|
|
205
|
+
* @returns {Object<string, Object>} Object where:
|
|
206
|
+
* - Keys are formatted hand strings (e.g., "As, Ks, Qs, Js, Ts")
|
|
207
|
+
* - Values are evaluation objects with boolean properties for all hand types
|
|
208
|
+
* @example
|
|
209
|
+
* // With 5 cards: returns object with 1 key-value pair
|
|
210
|
+
* // With 6 cards: returns object with 6 key-value pairs (C(6,5) = 6)
|
|
211
|
+
* // With 7 cards: returns object with 21 key-value pairs (C(7,5) = 21)
|
|
212
|
+
*/
|
|
213
|
+
#evaluateAllCombinations() {
|
|
214
|
+
// If exactly 5 cards, evaluate directly and wrap in the new format
|
|
215
|
+
if (this.cards.length === 5) {
|
|
216
|
+
const key = this.#formatHandKey(this.cards);
|
|
217
|
+
const evaluation = this.#evaluateSingleHand();
|
|
218
|
+
return { [key]: evaluation };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// For more than 5 cards, generate all combinations
|
|
222
|
+
const combinations = this.#generateCombinations(this.cards, 5);
|
|
223
|
+
const result = {};
|
|
224
|
+
for (const combination of combinations) {
|
|
225
|
+
const key = this.#formatHandKey(combination);
|
|
226
|
+
// Create a temporary evaluator with the 5-card combination
|
|
227
|
+
// This will evaluate directly since it's exactly 5 cards
|
|
228
|
+
const tempEvaluator = new HandEvaluator(combination);
|
|
229
|
+
// Extract the evaluation object from the single key-value pair
|
|
230
|
+
const evaluationKey = Object.keys(tempEvaluator.evaluation)[0];
|
|
231
|
+
result[key] = tempEvaluator.evaluation[evaluationKey];
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Gets the numeric rank value for a card using standard poker ranking (Ace high).
|
|
238
|
+
* Ace = 13, King = 12, Queen = 11, Jack = 10, Ten = 9, 9 = 8, ..., 2 = 1
|
|
239
|
+
*
|
|
240
|
+
* @private
|
|
241
|
+
* @param {string} card - Card string in format "Rs" (e.g., "As", "Kh")
|
|
242
|
+
* @returns {number} Numeric rank value (1-13)
|
|
243
|
+
* @example
|
|
244
|
+
* this.#getRankValue('As') // Returns 13
|
|
245
|
+
* this.#getRankValue('2s') // Returns 1
|
|
246
|
+
* this.#getRankValue('Ts') // Returns 9
|
|
247
|
+
*/
|
|
248
|
+
#getRankValue(card) {
|
|
249
|
+
const rank = card.slice(0, -1);
|
|
250
|
+
const rankMap = { A: 13, K: 12, Q: 11, J: 10, T: 9, '9': 8, '8': 7, '7': 6, '6': 5, '5': 4, '4': 3, '3': 2, '2': 1 };
|
|
251
|
+
return rankMap[rank];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Gets the numeric rank value for a card using wheel ranking (Ace low for wheel straights).
|
|
256
|
+
* Ace = 1, 2 = 2, 3 = 3, ..., King = 13
|
|
257
|
+
* Used for evaluating wheel straights (A-2-3-4-5).
|
|
258
|
+
*
|
|
259
|
+
* @private
|
|
260
|
+
* @param {string} card - Card string in format "Rs" (e.g., "As", "2h")
|
|
261
|
+
* @returns {number} Numeric rank value (1-13)
|
|
262
|
+
* @example
|
|
263
|
+
* this.#getRankValueWheel('As') // Returns 1 (Ace low)
|
|
264
|
+
* this.#getRankValueWheel('2s') // Returns 2
|
|
265
|
+
* this.#getRankValueWheel('Ks') // Returns 13
|
|
266
|
+
*/
|
|
267
|
+
#getRankValueWheel(card) {
|
|
268
|
+
const rank = card.slice(0, -1);
|
|
269
|
+
const rankMap = { A: 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, T: 10, J: 11, Q: 12, K: 13 };
|
|
270
|
+
return rankMap[rank];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Sorts cards from highest to lowest rank using standard poker ranking (Ace high).
|
|
275
|
+
* Modifies the array in place. If no cards parameter is provided, sorts this.cards.
|
|
276
|
+
*
|
|
277
|
+
* @param {string[]} [cards=null] - Optional array of card strings to sort.
|
|
278
|
+
* If null or undefined, sorts this.cards instead.
|
|
279
|
+
* @returns {void}
|
|
280
|
+
* @example
|
|
281
|
+
* const evaluator = new HandEvaluator(['2s', 'As', 'Ks', '5s', 'Ts']);
|
|
282
|
+
* evaluator.sort(); // Modifies evaluator.cards to ['As', 'Ks', 'Ts', '5s', '2s']
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* const cards = ['2s', 'As', 'Ks'];
|
|
286
|
+
* evaluator.sort(cards); // Modifies cards array to ['As', 'Ks', '2s']
|
|
287
|
+
*/
|
|
288
|
+
sort(cards = null) {
|
|
289
|
+
const cardsToSort = cards || this.cards;
|
|
290
|
+
cardsToSort.sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Sorts cards from highest to lowest rank using wheel ranking (Ace low).
|
|
295
|
+
* Modifies the array in place. Used for evaluating wheel straights (A-2-3-4-5).
|
|
296
|
+
* If no cards parameter is provided, sorts this.cards.
|
|
297
|
+
*
|
|
298
|
+
* @param {string[]} [cards=null] - Optional array of card strings to sort.
|
|
299
|
+
* If null or undefined, sorts this.cards instead.
|
|
300
|
+
* @returns {void}
|
|
301
|
+
* @example
|
|
302
|
+
* const evaluator = new HandEvaluator(['As', '2s', '3s', '4s', '5s']);
|
|
303
|
+
* evaluator.sortWheel(); // Modifies evaluator.cards to ['5s', '4s', '3s', '2s', 'As']
|
|
304
|
+
*/
|
|
305
|
+
sortWheel(cards = null) {
|
|
306
|
+
const cardsToSort = cards || this.cards;
|
|
307
|
+
cardsToSort.sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Calculates the distance (rank difference) between the highest and lowest card.
|
|
312
|
+
* Uses standard poker ranking (Ace high). Distance of 4 indicates a possible straight.
|
|
313
|
+
*
|
|
314
|
+
* @param {string[]} [cards=null] - Optional array of card strings to evaluate.
|
|
315
|
+
* If null or undefined, uses this.cards instead.
|
|
316
|
+
* @returns {number} Distance between highest and lowest card (0-12)
|
|
317
|
+
* @example
|
|
318
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
|
|
319
|
+
* evaluator.getDistance(); // Returns 4 (A=13, T=9, 13-9=4)
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* evaluator.getDistance(['As', '2s', '3s', '4s', '5s']);
|
|
323
|
+
* // Returns 12 (A=13, 2=1, 13-1=12)
|
|
324
|
+
*/
|
|
325
|
+
getDistance(cards = null) {
|
|
326
|
+
if (cards) {
|
|
327
|
+
// If cards array provided, sort a copy and calculate distance
|
|
328
|
+
if (cards.length === 0) return 0;
|
|
329
|
+
const sortedCards = [...cards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
|
|
330
|
+
return this.#getRankValue(sortedCards[0]) - this.#getRankValue(sortedCards[sortedCards.length - 1]);
|
|
331
|
+
}
|
|
332
|
+
// If no cards provided, use instance cards property
|
|
333
|
+
if (this.cards.length === 0) return 0;
|
|
334
|
+
this.sort();
|
|
335
|
+
return this.#getRankValue(this.cards[0]) - this.#getRankValue(this.cards[this.cards.length - 1]);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Calculates the distance (rank difference) between the highest and lowest card using wheel ranking.
|
|
340
|
+
* Uses wheel ranking (Ace low). Distance of 4 indicates a possible wheel straight (A-2-3-4-5).
|
|
341
|
+
*
|
|
342
|
+
* @param {string[]} [cards=null] - Optional array of card strings to evaluate.
|
|
343
|
+
* If null or undefined, uses this.cards instead.
|
|
344
|
+
* @returns {number} Distance between highest and lowest card using wheel ranking (0-12)
|
|
345
|
+
* @example
|
|
346
|
+
* const evaluator = new HandEvaluator(['As', '2s', '3s', '4s', '5s']);
|
|
347
|
+
* evaluator.getDistanceWheel(); // Returns 4 (5=5, A=1, 5-1=4) - wheel straight
|
|
348
|
+
*/
|
|
349
|
+
getDistanceWheel(cards = null) {
|
|
350
|
+
if (cards) {
|
|
351
|
+
// If cards array provided, sort a copy using wheel values and calculate distance
|
|
352
|
+
if (cards.length === 0) return 0;
|
|
353
|
+
const sortedCards = [...cards].sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
|
|
354
|
+
return this.#getRankValueWheel(sortedCards[0]) - this.#getRankValueWheel(sortedCards[sortedCards.length - 1]);
|
|
355
|
+
}
|
|
356
|
+
// If no cards provided, use instance cards property
|
|
357
|
+
if (this.cards.length === 0) return 0;
|
|
358
|
+
this.sortWheel();
|
|
359
|
+
return this.#getRankValueWheel(this.cards[0]) - this.#getRankValueWheel(this.cards[this.cards.length - 1]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Counts the occurrences of each suit in the hand.
|
|
364
|
+
*
|
|
365
|
+
* @returns {Object<string, number>} Object mapping suit to count:
|
|
366
|
+
* { s: count, h: count, d: count, c: count }
|
|
367
|
+
* @example
|
|
368
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
|
|
369
|
+
* evaluator.countSuits(); // Returns { s: 5 }
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* const evaluator = new HandEvaluator(['As', 'Kh', 'Qd', 'Jc', 'Ts']);
|
|
373
|
+
* evaluator.countSuits(); // Returns { s: 2, h: 1, d: 1, c: 1 }
|
|
374
|
+
*/
|
|
375
|
+
countSuits() {
|
|
376
|
+
return this.cards.reduce((acc, card) => ({ ...acc, [card.slice(-1)]: (acc[card.slice(-1)] || 0) + 1 }), {});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Counts the occurrences of each rank and returns sorted counts (descending).
|
|
381
|
+
* Used to identify pairs, three of a kind, four of a kind, etc.
|
|
382
|
+
*
|
|
383
|
+
* @returns {number[]} Array of rank counts sorted in descending order
|
|
384
|
+
* @example
|
|
385
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Kc', 'Ks']);
|
|
386
|
+
* evaluator.countRanks(); // Returns [3, 2] (three aces, two kings)
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs']);
|
|
390
|
+
* evaluator.countRanks(); // Returns [2, 2, 1] (two aces, two kings, one queen)
|
|
391
|
+
*/
|
|
392
|
+
countRanks() {
|
|
393
|
+
this.sort();
|
|
394
|
+
const rankCounts = this.cards.reduce((acc, card) => (acc[card.slice(0, -1)] = (acc[card.slice(0, -1)] || 0) + 1, acc), {});
|
|
395
|
+
return Object.values(rankCounts).sort((a, b) => b - a);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Gets a map of rank to count for all cards in the hand.
|
|
400
|
+
*
|
|
401
|
+
* @private
|
|
402
|
+
* @returns {Object<string, number>} Object mapping rank to count
|
|
403
|
+
* @example
|
|
404
|
+
* // For ['As', 'Ah', 'Ad', 'Kc', 'Ks']
|
|
405
|
+
* // Returns { A: 3, K: 2 }
|
|
406
|
+
*/
|
|
407
|
+
#getRankCounts() {
|
|
408
|
+
this.sort();
|
|
409
|
+
return this.cards.reduce((acc, card) => (acc[card.slice(0, -1)] = (acc[card.slice(0, -1)] || 0) + 1, acc), {});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Gets rank counts sorted by rank value (highest first).
|
|
414
|
+
* Returns an array of objects with rank, count, and value properties.
|
|
415
|
+
*
|
|
416
|
+
* @private
|
|
417
|
+
* @returns {Array<{rank: string, count: number, value: number}>} Array of rank info objects
|
|
418
|
+
* @example
|
|
419
|
+
* // For ['As', 'Ah', 'Kd', 'Kc', 'Qs']
|
|
420
|
+
* // Returns [{ rank: 'A', count: 2, value: 13 }, { rank: 'K', count: 2, value: 12 }, ...]
|
|
421
|
+
*/
|
|
422
|
+
#getRankCountsSorted() {
|
|
423
|
+
const rankCounts = this.#getRankCounts();
|
|
424
|
+
return Object.keys(rankCounts).map(r => ({ rank: r, count: rankCounts[r], value: this.#getRankValue(r + 's') })).sort((a, b) => b.value - a.value);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Calculates the gaps (rank differences) between consecutive cards.
|
|
429
|
+
* Returns an array of 4 gap values representing the differences between adjacent cards.
|
|
430
|
+
*
|
|
431
|
+
* @private
|
|
432
|
+
* @returns {number[]} Array of 4 gap values
|
|
433
|
+
* @example
|
|
434
|
+
* // For ['As', 'Ks', 'Qs', 'Js', 'Ts'] (consecutive ranks)
|
|
435
|
+
* // Returns [1, 1, 1, 1] (all gaps are 1)
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* // For ['As', 'Ks', 'Qs', 'Js', '9s'] (gap at the end)
|
|
439
|
+
* // Returns [1, 1, 1, 2] (gap of 2 between J and 9)
|
|
440
|
+
*/
|
|
441
|
+
#getGaps() {
|
|
442
|
+
this.sort();
|
|
443
|
+
const gaps = [];
|
|
444
|
+
for (let i = 0; i < 4; i++) {
|
|
445
|
+
gaps.push(this.#getRankValue(this.cards[i]) - this.#getRankValue(this.cards[i + 1]));
|
|
446
|
+
}
|
|
447
|
+
return gaps;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Calculates the gaps (rank differences) between consecutive cards using wheel ranking.
|
|
452
|
+
* Returns an array of 4 gap values representing the differences between adjacent cards.
|
|
453
|
+
* Used for evaluating wheel straights and draws.
|
|
454
|
+
*
|
|
455
|
+
* @private
|
|
456
|
+
* @returns {number[]} Array of 4 gap values using wheel ranking
|
|
457
|
+
* @example
|
|
458
|
+
* // For ['As', '2s', '3s', '4s', '5s'] (wheel straight)
|
|
459
|
+
* // Returns [1, 1, 1, 1] (all gaps are 1)
|
|
460
|
+
*/
|
|
461
|
+
#getGapsWheel() {
|
|
462
|
+
this.sortWheel();
|
|
463
|
+
const gaps = [];
|
|
464
|
+
for (let i = 0; i < 4; i++) {
|
|
465
|
+
gaps.push(this.#getRankValueWheel(this.cards[i]) - this.#getRankValueWheel(this.cards[i + 1]));
|
|
466
|
+
}
|
|
467
|
+
return gaps;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ==================== MADE HANDS ====================
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Determines if the hand contains four cards of the same rank.
|
|
474
|
+
*
|
|
475
|
+
* @returns {boolean} True if the hand is four of a kind, false otherwise
|
|
476
|
+
* @example
|
|
477
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Ac', 'Ks']);
|
|
478
|
+
* evaluator.isFourOfAKind(); // Returns true
|
|
479
|
+
*/
|
|
480
|
+
isFourOfAKind() {
|
|
481
|
+
if (this.evaluation.isFourOfAKind !== undefined) return this.evaluation.isFourOfAKind;
|
|
482
|
+
const ranks = this.countRanks();
|
|
483
|
+
return this.evaluation.isFourOfAKind = ranks[0] === 4 && ranks[1] === 1;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Determines if the hand contains three cards of one rank and two cards of another rank.
|
|
488
|
+
*
|
|
489
|
+
* @returns {boolean} True if the hand is a full house, false otherwise
|
|
490
|
+
* @example
|
|
491
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Kc', 'Ks']);
|
|
492
|
+
* evaluator.isFullHouse(); // Returns true
|
|
493
|
+
*/
|
|
494
|
+
isFullHouse() {
|
|
495
|
+
if (this.evaluation.isFullHouse !== undefined) return this.evaluation.isFullHouse;
|
|
496
|
+
const ranks = this.countRanks();
|
|
497
|
+
return this.evaluation.isFullHouse = ranks[0] === 3 && ranks[1] === 2;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Determines if the hand contains three cards of the same rank (but not four of a kind or full house).
|
|
502
|
+
*
|
|
503
|
+
* @returns {boolean} True if the hand is three of a kind, false otherwise
|
|
504
|
+
* @example
|
|
505
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Kc', 'Qs']);
|
|
506
|
+
* evaluator.isThreeOfAKind(); // Returns true
|
|
507
|
+
*/
|
|
508
|
+
isThreeOfAKind() {
|
|
509
|
+
if (this.evaluation.isThreeOfAKind !== undefined) return this.evaluation.isThreeOfAKind;
|
|
510
|
+
const ranks = this.countRanks();
|
|
511
|
+
return this.evaluation.isThreeOfAKind = ranks[0] === 3 && ranks[1] === 1 && ranks[2] === 1;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Determines if the hand contains two pairs of different ranks.
|
|
516
|
+
*
|
|
517
|
+
* @returns {boolean} True if the hand is two pair, false otherwise
|
|
518
|
+
* @example
|
|
519
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs']);
|
|
520
|
+
* evaluator.isTwoPair(); // Returns true
|
|
521
|
+
*/
|
|
522
|
+
isTwoPair() {
|
|
523
|
+
if (this.evaluation.isTwoPair !== undefined) return this.evaluation.isTwoPair;
|
|
524
|
+
const ranks = this.countRanks();
|
|
525
|
+
return this.evaluation.isTwoPair = ranks[0] === 2 && ranks[1] === 2 && ranks[2] === 1;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Determines if the hand contains exactly one pair (two cards of the same rank).
|
|
530
|
+
*
|
|
531
|
+
* @returns {boolean} True if the hand is a pair, false otherwise
|
|
532
|
+
* @example
|
|
533
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Qc', 'Js']);
|
|
534
|
+
* evaluator.isPair(); // Returns true
|
|
535
|
+
*/
|
|
536
|
+
isPair() {
|
|
537
|
+
if (this.evaluation.isPair !== undefined) return this.evaluation.isPair;
|
|
538
|
+
const ranks = this.countRanks();
|
|
539
|
+
return this.evaluation.isPair = ranks[0] === 2 && ranks[1] === 1 && ranks[2] === 1 && ranks[3] === 1;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ==================== PAIR CATEGORIZATION ====================
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Determines if the hand contains a pair where the paired rank is the highest card.
|
|
546
|
+
* Requires that the hand is a pair (not two pair or higher).
|
|
547
|
+
*
|
|
548
|
+
* @returns {boolean} True if the hand is a top pair, false otherwise
|
|
549
|
+
* @example
|
|
550
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Qc', 'Js']);
|
|
551
|
+
* evaluator.isTopPair(); // Returns true (pair of aces, highest card)
|
|
552
|
+
*/
|
|
553
|
+
isTopPair() {
|
|
554
|
+
if (this.evaluation.isTopPair !== undefined) return this.evaluation.isTopPair;
|
|
555
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
556
|
+
if (!this.evaluation.isPair) return this.evaluation.isTopPair = false;
|
|
557
|
+
const rankCounts = this.#getRankCounts();
|
|
558
|
+
return this.evaluation.isTopPair = rankCounts[this.cards[0].slice(0, -1)] === 2;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Determines if the hand contains a pair where the paired rank is neither the highest nor lowest card.
|
|
563
|
+
* Requires that the hand is a single pair (not two pair or higher).
|
|
564
|
+
*
|
|
565
|
+
* @returns {boolean} True if the hand is a middle pair, false otherwise
|
|
566
|
+
* @example
|
|
567
|
+
* const evaluator = new HandEvaluator(['As', 'Kh', 'Kd', 'Qc', 'Js']);
|
|
568
|
+
* evaluator.isMiddlePair(); // Returns true (pair of kings, not highest or lowest)
|
|
569
|
+
*/
|
|
570
|
+
isMiddlePair() {
|
|
571
|
+
if (this.evaluation.isMiddlePair !== undefined) return this.evaluation.isMiddlePair;
|
|
572
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
573
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
574
|
+
if (!this.evaluation.isPair || this.evaluation.isTwoPair) return this.evaluation.isMiddlePair = false;
|
|
575
|
+
const ranks = this.#getRankCountsSorted();
|
|
576
|
+
return this.evaluation.isMiddlePair = ranks.length === 4 && (ranks[1].count === 2 || ranks[2].count === 2);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Determines if the hand contains a pair where the paired rank is the lowest card.
|
|
581
|
+
* Requires that the hand is a pair (not two pair or higher).
|
|
582
|
+
*
|
|
583
|
+
* @returns {boolean} True if the hand is a bottom pair, false otherwise
|
|
584
|
+
* @example
|
|
585
|
+
* const evaluator = new HandEvaluator(['As', 'Kh', 'Qd', '2c', '2s']);
|
|
586
|
+
* evaluator.isBottomPair(); // Returns true (pair of deuces, lowest card)
|
|
587
|
+
*/
|
|
588
|
+
isBottomPair() {
|
|
589
|
+
if (this.evaluation.isBottomPair !== undefined) return this.evaluation.isBottomPair;
|
|
590
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
591
|
+
if (!this.evaluation.isPair) return this.evaluation.isBottomPair = false;
|
|
592
|
+
const rankCounts = this.#getRankCounts();
|
|
593
|
+
return this.evaluation.isBottomPair = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 2;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ==================== TWO PAIR CATEGORIZATION ====================
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Determines if the hand contains two pairs where both pairs are in the top two ranks.
|
|
600
|
+
* Requires that the hand is two pair.
|
|
601
|
+
*
|
|
602
|
+
* @returns {boolean} True if the hand is top and middle pair, false otherwise
|
|
603
|
+
* @example
|
|
604
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs']);
|
|
605
|
+
* evaluator.isTopAndMiddlePair(); // Returns true (aces and kings)
|
|
606
|
+
*/
|
|
607
|
+
isTopAndMiddlePair() {
|
|
608
|
+
if (this.evaluation.isTopAndMiddlePair !== undefined) return this.evaluation.isTopAndMiddlePair;
|
|
609
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
610
|
+
if (!this.evaluation.isTwoPair) return this.evaluation.isTopAndMiddlePair = false;
|
|
611
|
+
const ranks = this.#getRankCountsSorted();
|
|
612
|
+
return this.evaluation.isTopAndMiddlePair = ranks[0].count === 2 && ranks[1].count === 2;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Determines if the hand contains two pairs where one pair is the highest rank and the other is the lowest rank.
|
|
617
|
+
* Requires that the hand is two pair.
|
|
618
|
+
*
|
|
619
|
+
* @returns {boolean} True if the hand is top and bottom pair, false otherwise
|
|
620
|
+
* @example
|
|
621
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Qc', '2s', '2h']);
|
|
622
|
+
* evaluator.isTopAndBottomPair(); // Returns true (aces and deuces)
|
|
623
|
+
*/
|
|
624
|
+
isTopAndBottomPair() {
|
|
625
|
+
if (this.evaluation.isTopAndBottomPair !== undefined) return this.evaluation.isTopAndBottomPair;
|
|
626
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
627
|
+
if (!this.evaluation.isTwoPair) return this.evaluation.isTopAndBottomPair = false;
|
|
628
|
+
const ranks = this.#getRankCountsSorted();
|
|
629
|
+
return this.evaluation.isTopAndBottomPair = ranks[0].count === 2 && ranks[ranks.length - 1].count === 2;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Determines if the hand contains two pairs where both pairs are in the middle and bottom ranks.
|
|
634
|
+
* Requires that the hand is two pair.
|
|
635
|
+
*
|
|
636
|
+
* @returns {boolean} True if the hand is middle and bottom pair, false otherwise
|
|
637
|
+
* @example
|
|
638
|
+
* const evaluator = new HandEvaluator(['As', 'Kh', 'Kd', '2c', '2s']);
|
|
639
|
+
* evaluator.isMiddleAndBottomPair(); // Returns true (kings and deuces)
|
|
640
|
+
*/
|
|
641
|
+
isMiddleAndBottomPair() {
|
|
642
|
+
if (this.evaluation.isMiddleAndBottomPair !== undefined) return this.evaluation.isMiddleAndBottomPair;
|
|
643
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
644
|
+
if (!this.evaluation.isTwoPair) return this.evaluation.isMiddleAndBottomPair = false;
|
|
645
|
+
const ranks = this.#getRankCountsSorted();
|
|
646
|
+
return this.evaluation.isMiddleAndBottomPair = ranks.length === 3 && ranks[1].count === 2 && ranks[2].count === 2;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ==================== THREE OF A KIND CATEGORIZATION ====================
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Determines if the hand contains three of a kind where the three cards are the highest rank.
|
|
653
|
+
* Requires that the hand is three of a kind.
|
|
654
|
+
*
|
|
655
|
+
* @returns {boolean} True if the hand is top three of a kind, false otherwise
|
|
656
|
+
* @example
|
|
657
|
+
* const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Kc', 'Qs']);
|
|
658
|
+
* evaluator.isTopThreeOfAKind(); // Returns true (three aces)
|
|
659
|
+
*/
|
|
660
|
+
isTopThreeOfAKind() {
|
|
661
|
+
if (this.evaluation.isTopThreeOfAKind !== undefined) return this.evaluation.isTopThreeOfAKind;
|
|
662
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
663
|
+
if (!this.evaluation.isThreeOfAKind) return this.evaluation.isTopThreeOfAKind = false;
|
|
664
|
+
const rankCounts = this.#getRankCounts();
|
|
665
|
+
return this.evaluation.isTopThreeOfAKind = rankCounts[this.cards[0].slice(0, -1)] === 3;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Determines if the hand contains three of a kind where the three cards are neither the highest nor lowest rank.
|
|
670
|
+
* Requires that the hand is three of a kind.
|
|
671
|
+
*
|
|
672
|
+
* @returns {boolean} True if the hand is middle three of a kind, false otherwise
|
|
673
|
+
* @example
|
|
674
|
+
* const evaluator = new HandEvaluator(['As', 'Kh', 'Kd', 'Kc', 'Qs']);
|
|
675
|
+
* evaluator.isMiddleThreeOfAKind(); // Returns true (three kings)
|
|
676
|
+
*/
|
|
677
|
+
isMiddleThreeOfAKind() {
|
|
678
|
+
if (this.evaluation.isMiddleThreeOfAKind !== undefined) return this.evaluation.isMiddleThreeOfAKind;
|
|
679
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
680
|
+
if (!this.evaluation.isThreeOfAKind) return this.evaluation.isMiddleThreeOfAKind = false;
|
|
681
|
+
const ranks = this.#getRankCountsSorted();
|
|
682
|
+
return this.evaluation.isMiddleThreeOfAKind = ranks.length === 3 && ranks[1].count === 3;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Determines if the hand contains three of a kind where the three cards are the lowest rank.
|
|
687
|
+
* Requires that the hand is three of a kind.
|
|
688
|
+
*
|
|
689
|
+
* @returns {boolean} True if the hand is bottom three of a kind, false otherwise
|
|
690
|
+
* @example
|
|
691
|
+
* const evaluator = new HandEvaluator(['As', 'Kh', '2d', '2c', '2s']);
|
|
692
|
+
* evaluator.isBottomThreeOfAKind(); // Returns true (three deuces)
|
|
693
|
+
*/
|
|
694
|
+
isBottomThreeOfAKind() {
|
|
695
|
+
if (this.evaluation.isBottomThreeOfAKind !== undefined) return this.evaluation.isBottomThreeOfAKind;
|
|
696
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
697
|
+
if (!this.evaluation.isThreeOfAKind) return this.evaluation.isBottomThreeOfAKind = false;
|
|
698
|
+
const rankCounts = this.#getRankCounts();
|
|
699
|
+
return this.evaluation.isBottomThreeOfAKind = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 3;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ==================== FLUSH EVALUATION ====================
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Determines if all five cards are of the same suit.
|
|
706
|
+
*
|
|
707
|
+
* @returns {boolean} True if the hand is a flush, false otherwise
|
|
708
|
+
* @example
|
|
709
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9s']);
|
|
710
|
+
* evaluator.isFlush(); // Returns true (all spades)
|
|
711
|
+
*/
|
|
712
|
+
isFlush() {
|
|
713
|
+
if (this.evaluation.isFlush !== undefined) return this.evaluation.isFlush;
|
|
714
|
+
const suits = this.countSuits();
|
|
715
|
+
return this.evaluation.isFlush = Object.values(suits).some(count => count === 5);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Determines if the hand has four cards of the same suit (one card away from a flush).
|
|
720
|
+
* Returns false if the hand is already a flush.
|
|
721
|
+
*
|
|
722
|
+
* @returns {boolean} True if the hand is a flush draw, false otherwise
|
|
723
|
+
* @example
|
|
724
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9h']);
|
|
725
|
+
* evaluator.isFlushDraw(); // Returns true (4 spades, 1 heart)
|
|
726
|
+
*/
|
|
727
|
+
isFlushDraw() {
|
|
728
|
+
if (this.evaluation.isFlushDraw !== undefined) return this.evaluation.isFlushDraw;
|
|
729
|
+
if (this.evaluation.isFlush === undefined) this.isFlush();
|
|
730
|
+
if (this.evaluation.isFlush) return this.evaluation.isFlushDraw = false;
|
|
731
|
+
const suits = this.countSuits();
|
|
732
|
+
return this.evaluation.isFlushDraw = Object.values(suits).some(count => count === 4);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Determines if the hand has three cards of the same suit (two cards away from a flush).
|
|
737
|
+
* Returns false if the hand is already a flush or flush draw.
|
|
738
|
+
*
|
|
739
|
+
* @returns {boolean} True if the hand is a backdoor flush draw, false otherwise
|
|
740
|
+
* @example
|
|
741
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Jh', '9h']);
|
|
742
|
+
* evaluator.isBackdoorFlushDraw(); // Returns true (3 spades, 2 hearts)
|
|
743
|
+
*/
|
|
744
|
+
isBackdoorFlushDraw() {
|
|
745
|
+
if (this.evaluation.isBackdoorFlushDraw !== undefined) return this.evaluation.isBackdoorFlushDraw;
|
|
746
|
+
if (this.evaluation.isFlush === undefined) this.isFlush();
|
|
747
|
+
if (this.evaluation.isFlushDraw === undefined) this.isFlushDraw();
|
|
748
|
+
if (this.evaluation.isFlush || this.evaluation.isFlushDraw) return this.evaluation.isBackdoorFlushDraw = false;
|
|
749
|
+
const suits = this.countSuits();
|
|
750
|
+
return this.evaluation.isBackdoorFlushDraw = Object.values(suits).some(count => count === 3);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ==================== STRAIGHT EVALUATION ====================
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Determines if the hand contains five consecutive ranks (a straight).
|
|
757
|
+
* Checks both standard straights and wheel straights (A-2-3-4-5).
|
|
758
|
+
* Returns false if the hand contains pairs or higher (which cannot be straights).
|
|
759
|
+
*
|
|
760
|
+
* @returns {boolean} True if the hand is a straight, false otherwise
|
|
761
|
+
* @example
|
|
762
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
|
|
763
|
+
* evaluator.isStraight(); // Returns true (A-K-Q-J-T)
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* const evaluator = new HandEvaluator(['As', '2s', '3s', '4s', '5s']);
|
|
767
|
+
* evaluator.isStraight(); // Returns true (wheel: A-2-3-4-5)
|
|
768
|
+
*/
|
|
769
|
+
isStraight() {
|
|
770
|
+
if (this.evaluation.isStraight !== undefined) return this.evaluation.isStraight;
|
|
771
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
772
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
773
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
774
|
+
if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
|
|
775
|
+
if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
|
|
776
|
+
if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind) return this.evaluation.isStraight = false;
|
|
777
|
+
this.sort();
|
|
778
|
+
const distance = this.getDistance();
|
|
779
|
+
if (distance === 4) return this.evaluation.isStraight = true;
|
|
780
|
+
if (this.isStraightWheel()) return this.evaluation.isStraight = true;
|
|
781
|
+
return this.evaluation.isStraight = false;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Determines if the hand is a wheel straight (A-2-3-4-5) using wheel ranking.
|
|
786
|
+
* Returns false if the hand contains pairs or higher (which cannot be straights).
|
|
787
|
+
*
|
|
788
|
+
* @returns {boolean} True if the hand is a wheel straight, false otherwise
|
|
789
|
+
* @example
|
|
790
|
+
* const evaluator = new HandEvaluator(['As', '2s', '3s', '4s', '5s']);
|
|
791
|
+
* evaluator.isStraightWheel(); // Returns true (wheel: A-2-3-4-5)
|
|
792
|
+
*/
|
|
793
|
+
isStraightWheel() {
|
|
794
|
+
if (this.evaluation.isStraightWheel !== undefined) return this.evaluation.isStraightWheel;
|
|
795
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
796
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
797
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
798
|
+
if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
|
|
799
|
+
if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
|
|
800
|
+
if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind) return this.evaluation.isStraightWheel = false;
|
|
801
|
+
this.sortWheel();
|
|
802
|
+
const distance = this.getDistanceWheel();
|
|
803
|
+
return this.evaluation.isStraightWheel = distance === 4;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Determines if the hand is a royal flush (A-K-Q-J-T all of the same suit).
|
|
808
|
+
* Requires that the hand is both a flush and a straight.
|
|
809
|
+
*
|
|
810
|
+
* @returns {boolean} True if the hand is a royal flush, false otherwise
|
|
811
|
+
* @example
|
|
812
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
|
|
813
|
+
* evaluator.isRoyalFlush(); // Returns true
|
|
814
|
+
*/
|
|
815
|
+
isRoyalFlush() {
|
|
816
|
+
if (this.evaluation.isRoyalFlush !== undefined) return this.evaluation.isRoyalFlush;
|
|
817
|
+
if (this.evaluation.isFlush === undefined) this.isFlush();
|
|
818
|
+
if (this.evaluation.isStraight === undefined) this.isStraight();
|
|
819
|
+
if (!this.evaluation.isFlush || !this.evaluation.isStraight) return this.evaluation.isRoyalFlush = false;
|
|
820
|
+
const ranks = new Set(this.cards.map(c => c.slice(0, -1)));
|
|
821
|
+
return this.evaluation.isRoyalFlush = ranks.has('A') && ranks.has('K') && ranks.has('Q') && ranks.has('J') && ranks.has('T');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Determines if the hand is a straight flush (five consecutive ranks, all same suit, but not royal flush).
|
|
826
|
+
* Requires that the hand is both a flush and a straight, but not a royal flush.
|
|
827
|
+
*
|
|
828
|
+
* @returns {boolean} True if the hand is a straight flush, false otherwise
|
|
829
|
+
* @example
|
|
830
|
+
* const evaluator = new HandEvaluator(['9s', '8s', '7s', '6s', '5s']);
|
|
831
|
+
* evaluator.isStraightFlush(); // Returns true
|
|
832
|
+
*/
|
|
833
|
+
isStraightFlush() {
|
|
834
|
+
if (this.evaluation.isStraightFlush !== undefined) return this.evaluation.isStraightFlush;
|
|
835
|
+
if (this.evaluation.isFlush === undefined) this.isFlush();
|
|
836
|
+
if (this.evaluation.isStraight === undefined) this.isStraight();
|
|
837
|
+
if (this.evaluation.isRoyalFlush === undefined) this.isRoyalFlush();
|
|
838
|
+
return this.evaluation.isStraightFlush = this.evaluation.isFlush && this.evaluation.isStraight && !this.evaluation.isRoyalFlush;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ==================== STRAIGHT DRAWS ====================
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Determines if the hand is an open-ended straight draw (one card away from a straight on either end).
|
|
845
|
+
* Checks both standard and wheel-equivalent open-ended draws.
|
|
846
|
+
* Returns false if the hand already contains pairs, two pair, three of a kind, full house, four of a kind, or a straight.
|
|
847
|
+
*
|
|
848
|
+
* @returns {boolean} True if the hand is an open-ended straight draw, false otherwise
|
|
849
|
+
* @example
|
|
850
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9h']);
|
|
851
|
+
* evaluator.isOpenEndedStraightDraw(); // Returns true (needs T or 9 to complete straight)
|
|
852
|
+
*/
|
|
853
|
+
isOpenEndedStraightDraw() {
|
|
854
|
+
if (this.evaluation.isOpenEndedStraightDraw !== undefined) return this.evaluation.isOpenEndedStraightDraw;
|
|
855
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
856
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
857
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
858
|
+
if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
|
|
859
|
+
if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
|
|
860
|
+
if (this.evaluation.isStraight === undefined) this.isStraight();
|
|
861
|
+
if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraight) {
|
|
862
|
+
// Check wheel equivalent before returning false
|
|
863
|
+
if (this.isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
|
|
864
|
+
return this.evaluation.isOpenEndedStraightDraw = false;
|
|
865
|
+
}
|
|
866
|
+
this.sort();
|
|
867
|
+
const result = this.getDistance(this.cards.slice(0,4)) === 3 || this.getDistance(this.cards.slice(1,5)) === 3;
|
|
868
|
+
if (result) return this.evaluation.isOpenEndedStraightDraw = true;
|
|
869
|
+
// Check wheel equivalent
|
|
870
|
+
if (this.isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
|
|
871
|
+
return this.evaluation.isOpenEndedStraightDraw = false;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Determines if the hand is an open-ended straight draw using wheel ranking.
|
|
876
|
+
* Checks for wheel-equivalent open-ended draws (e.g., A-2-3-4 needs 5 or 2).
|
|
877
|
+
* Returns false if the hand already contains pairs or higher, or is a wheel straight.
|
|
878
|
+
*
|
|
879
|
+
* @returns {boolean} True if the hand is a wheel-equivalent open-ended straight draw, false otherwise
|
|
880
|
+
*/
|
|
881
|
+
isOpenEndedStraightDrawWheel() {
|
|
882
|
+
if (this.evaluation.isOpenEndedStraightDrawWheel !== undefined) return this.evaluation.isOpenEndedStraightDrawWheel;
|
|
883
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
884
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
885
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
886
|
+
if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
|
|
887
|
+
if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
|
|
888
|
+
if (this.evaluation.isStraightWheel === undefined) this.isStraightWheel();
|
|
889
|
+
if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraightWheel) return this.evaluation.isOpenEndedStraightDrawWheel = false;
|
|
890
|
+
this.sortWheel();
|
|
891
|
+
return this.evaluation.isOpenEndedStraightDrawWheel = this.getDistanceWheel(this.cards.slice(0,4)) === 3 || this.getDistanceWheel(this.cards.slice(1,5)) === 3;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Determines if the hand is an inside straight draw (one card away from a straight, but the needed card is in the middle).
|
|
896
|
+
* Checks both standard and wheel-equivalent inside draws.
|
|
897
|
+
* Returns false if the hand already contains pairs, two pair, three of a kind, full house, four of a kind, or a straight.
|
|
898
|
+
*
|
|
899
|
+
* @returns {boolean} True if the hand is an inside straight draw, false otherwise
|
|
900
|
+
* @example
|
|
901
|
+
* const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9h']);
|
|
902
|
+
* evaluator.isInsideStraightDraw(); // Returns true (needs T in the middle)
|
|
903
|
+
*/
|
|
904
|
+
isInsideStraightDraw() {
|
|
905
|
+
if (this.evaluation.isInsideStraightDraw !== undefined) return this.evaluation.isInsideStraightDraw;
|
|
906
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
907
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
908
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
909
|
+
if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
|
|
910
|
+
if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
|
|
911
|
+
if (this.evaluation.isStraight === undefined) this.isStraight();
|
|
912
|
+
if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraight) {
|
|
913
|
+
// Check wheel equivalent before returning false
|
|
914
|
+
if (this.isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
|
|
915
|
+
return this.evaluation.isInsideStraightDraw = false;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
this.sort();
|
|
919
|
+
const result = this.getDistance(this.cards.slice(0,4)) === 4 || this.getDistance(this.cards.slice(1,5)) === 4;
|
|
920
|
+
if (result) return this.evaluation.isInsideStraightDraw = true;
|
|
921
|
+
// Check wheel equivalent
|
|
922
|
+
if (this.isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
|
|
923
|
+
return this.evaluation.isInsideStraightDraw = false;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Determines if the hand is an inside straight draw using wheel ranking.
|
|
928
|
+
* Checks for wheel-equivalent inside draws (e.g., A-2-3-5 needs 4).
|
|
929
|
+
* Returns false if the hand already contains pairs or higher, or is a wheel straight.
|
|
930
|
+
*
|
|
931
|
+
* @returns {boolean} True if the hand is a wheel-equivalent inside straight draw, false otherwise
|
|
932
|
+
*/
|
|
933
|
+
isInsideStraightDrawWheel() {
|
|
934
|
+
if (this.evaluation.isInsideStraightDrawWheel !== undefined) return this.evaluation.isInsideStraightDrawWheel;
|
|
935
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
936
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
937
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
938
|
+
if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
|
|
939
|
+
if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
|
|
940
|
+
if (this.evaluation.isStraightWheel === undefined) this.isStraightWheel();
|
|
941
|
+
if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraightWheel) return this.evaluation.isInsideStraightDrawWheel = false;
|
|
942
|
+
|
|
943
|
+
this.sortWheel();
|
|
944
|
+
return this.evaluation.isInsideStraightDrawWheel = this.getDistanceWheel(this.cards.slice(0,4)) === 4 || this.getDistanceWheel(this.cards.slice(1,5)) === 4;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ==================== HIGH CARD ====================
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Determines if the hand is high card only (no pairs, two pair, three of a kind, full house,
|
|
951
|
+
* four of a kind, flush, or straight).
|
|
952
|
+
*
|
|
953
|
+
* @returns {boolean} True if the hand is high card only, false otherwise
|
|
954
|
+
* @example
|
|
955
|
+
* const evaluator = new HandEvaluator(['As', 'Kh', 'Qd', 'Jc', '9s']);
|
|
956
|
+
* evaluator.isHighCard(); // Returns true (no made hand)
|
|
957
|
+
*/
|
|
958
|
+
isHighCard() {
|
|
959
|
+
if (this.evaluation.isHighCard !== undefined) return this.evaluation.isHighCard;
|
|
960
|
+
if (this.evaluation.isPair === undefined) this.isPair();
|
|
961
|
+
if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
|
|
962
|
+
if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
|
|
963
|
+
if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
|
|
964
|
+
if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
|
|
965
|
+
if (this.evaluation.isFlush === undefined) this.isFlush();
|
|
966
|
+
if (this.evaluation.isStraight === undefined) this.isStraight();
|
|
967
|
+
return this.evaluation.isHighCard = !this.evaluation.isPair && !this.evaluation.isTwoPair && !this.evaluation.isThreeOfAKind && !this.evaluation.isFullHouse && !this.evaluation.isFourOfAKind && !this.evaluation.isFlush && !this.evaluation.isStraight;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Evaluates poker hands from 5 or more cards and identifies all possible draws.
|
|
973
|
+
* Generates all 5-card combinations and evaluates each one.
|
|
974
|
+
*
|
|
975
|
+
* @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)
|
|
976
|
+
* @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
|
|
977
|
+
*/
|
|
978
|
+
function evaluateHand(cards) {
|
|
979
|
+
const evaluator = new HandEvaluator(cards);
|
|
980
|
+
return evaluator.evaluation;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
module.exports = { evaluateHand, HandEvaluator };
|
|
984
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "poker-extval",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Poker hand evaluator with comprehensive test suite",
|
|
5
|
+
"main": "evaluator.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"evaluator.js",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"test:watch": "jest --watch",
|
|
14
|
+
"benchmark": "node benchmark.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"poker",
|
|
18
|
+
"hand",
|
|
19
|
+
"evaluator",
|
|
20
|
+
"poker-hand",
|
|
21
|
+
"poker-evaluation",
|
|
22
|
+
"card-game",
|
|
23
|
+
"texas-holdem"
|
|
24
|
+
],
|
|
25
|
+
"author": "Darragh Geoghegan",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/DarraghGeo/poker-extval.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/DarraghGeo/poker-extval/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/DarraghGeo/poker-extval#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=12.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"jest": "^29.7.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|