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.
Files changed (4) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +306 -0
  3. package/evaluator.js +984 -0
  4. 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
+