poker-extval 2.0.0 → 3.1.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 (3) hide show
  1. package/README.md +155 -16
  2. package/evaluator.js +463 -106
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,8 +5,10 @@ A comprehensive poker hand evaluation library for Node.js that evaluates poker h
5
5
  ## Features
6
6
 
7
7
  - **Comprehensive Hand Evaluation**: Evaluates all standard poker hand types (Royal Flush, Straight Flush, Four of a Kind, Full House, Flush, Straight, Three of a Kind, Two Pair, Pair, High Card)
8
+ - **Strict Mode (Default)**: Only marks the strongest made hand and stronger draws, preventing weaker hands from being marked when stronger ones exist
8
9
  - **Detailed Categorization**: Categorizes pairs (Top, Middle, Bottom), two pairs, and three of a kind by position
9
10
  - **Draw Detection**: Identifies flush draws, backdoor flush draws, open-ended straight draws, and inside straight draws
11
+ - **Key Cards and Kicker Cards**: Provides detailed card arrays showing which cards form each hand type and which are kickers
10
12
  - **Wheel Straight Support**: Handles Ace-low straights (A-2-3-4-5) correctly
11
13
  - **Multiple Card Support**: Accepts 5 or more cards and evaluates all possible 5-card combinations
12
14
  - **Extensive Documentation**: Full JSDoc comments for all methods
@@ -24,15 +26,15 @@ npm install poker-extval
24
26
  ```javascript
25
27
  const { evaluateHand } = require('poker-extval');
26
28
 
27
- // Evaluate a 5-card hand
29
+ // Evaluate a 5-card hand (strict mode enabled by default)
28
30
  const result = evaluateHand(['As', 'Ks', 'Qs', 'Js', 'Ts']);
29
31
  console.log(result);
30
32
  // {
31
33
  // "As, Ks, Qs, Js, Ts": {
32
34
  // "isRoyalFlush": true,
33
35
  // "isStraightFlush": false,
34
- // "isFlush": true,
35
- // "isStraight": true,
36
+ // "isFlush": false, // Filtered out in strict mode (royal flush is stronger)
37
+ // "isStraight": false, // Filtered out in strict mode (royal flush is stronger)
36
38
  // ...
37
39
  // }
38
40
  // }
@@ -43,14 +45,43 @@ console.log(result);
43
45
  ```javascript
44
46
  const { HandEvaluator } = require('poker-extval');
45
47
 
46
- // Create an evaluator instance
48
+ // Create an evaluator instance (strict mode enabled by default)
47
49
  const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts', '9s']);
48
50
 
49
51
  // Access evaluation results
50
52
  const evaluation = evaluator.evaluation;
51
53
  // Object with keys for each 5-card combination
54
+
55
+ // Non-strict mode (all applicable hands marked)
56
+ const nonStrictEvaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs'], false);
52
57
  ```
53
58
 
59
+ ### Strict Mode
60
+
61
+ By default, the evaluator uses **strict mode** which only marks the strongest made hand and stronger draws as true. This prevents weaker hands from being marked when a stronger hand exists.
62
+
63
+ **Example with strict mode (default):**
64
+ ```javascript
65
+ const result = evaluateHand(['As', 'Ah', 'Kd', 'Kc', 'Qs']);
66
+ // isTwoPair: true
67
+ // isPair: false (filtered out because two pair is stronger)
68
+ // isTopAndMiddlePair: true
69
+ ```
70
+
71
+ **Example with non-strict mode:**
72
+ ```javascript
73
+ const result = evaluateHand(['As', 'Ah', 'Kd', 'Kc', 'Qs'], false);
74
+ // isTwoPair: true
75
+ // isPair: false (evaluation logic prevents this, not strict mode)
76
+ // isTopAndMiddlePair: true
77
+ ```
78
+
79
+ **Strict Mode Rules:**
80
+ - Only the strongest made hand is marked as true
81
+ - Descriptive labels (isTopPair, etc.) are only true if they match the strongest hand
82
+ - Made flush or stronger cannot have straight draws (flush > straight)
83
+ - Draws can coexist when no made hand exists (flush draw + straight draw is valid)
84
+
54
85
  ### Evaluating Multiple Card Hands
55
86
 
56
87
  When you provide more than 5 cards, the evaluator generates all possible 5-card combinations:
@@ -100,17 +131,17 @@ The `evaluateHand()` function returns an object where:
100
131
  - `isPair` - Exactly one pair
101
132
  - `isHighCard` - No made hand
102
133
 
103
- #### Pair Categorization (only true if `isPair` is true)
134
+ #### Pair Categorization (only true if `isPair` is true in strict mode)
104
135
  - `isTopPair` - Pair is the highest card
105
136
  - `isMiddlePair` - Pair is neither highest nor lowest
106
137
  - `isBottomPair` - Pair is the lowest card
107
138
 
108
- #### Two Pair Categorization (only true if `isTwoPair` is true)
139
+ #### Two Pair Categorization (only true if `isTwoPair` is true in strict mode)
109
140
  - `isTopAndMiddlePair` - Both pairs are in top two ranks
110
141
  - `isTopAndBottomPair` - One pair is highest, other is lowest
111
142
  - `isMiddleAndBottomPair` - Both pairs are in middle and bottom ranks
112
143
 
113
- #### Three of a Kind Categorization (only true if `isThreeOfAKind` is true)
144
+ #### Three of a Kind Categorization (only true if `isThreeOfAKind` is true in strict mode)
114
145
  - `isTopThreeOfAKind` - Three of a kind is the highest rank
115
146
  - `isMiddleThreeOfAKind` - Three of a kind is neither highest nor lowest
116
147
  - `isBottomThreeOfAKind` - Three of a kind is the lowest rank
@@ -128,11 +159,15 @@ The `evaluateHand()` function returns an object where:
128
159
  - `isInsideStraightDrawWheel` - Inside draw using wheel ranking
129
160
 
130
161
  #### Card Arrays
131
- - `keyCards` - Array of cards (sorted) that constitute the made hand or draw
132
- - `kickerCards` - Array of cards (sorted) that don't constitute the made hand or draw
162
+ - `keyCards` - Object with keys for each hand type/draw, values are arrays of cards (sorted) that constitute that specific hand type or draw. False hand types have empty arrays `[]`.
163
+ - `kickerCards` - Object with keys for each hand type/draw, values are arrays of cards (sorted) that don't constitute that specific hand type or draw. False hand types have empty arrays `[]`, except `isHighCard` which contains all 5 cards when true.
164
+
165
+ **Note**: Each hand type has its own `keyCards` and `kickerCards` entries. For example, a hand with both a pair and a flush draw will have separate entries in `keyCards.isPair`, `keyCards.isFlushDraw`, `kickerCards.isPair`, and `kickerCards.isFlushDraw`.
133
166
 
134
167
  ### Example Response
135
168
 
169
+ **Example 1: Royal Flush (strict mode - default)**
170
+
136
171
  ```javascript
137
172
  {
138
173
  "As, Ks, Qs, Js, Ts": {
@@ -141,10 +176,10 @@ The `evaluateHand()` function returns an object where:
141
176
  "isThreeOfAKind": false,
142
177
  "isTwoPair": false,
143
178
  "isPair": false,
144
- "isFlush": true,
145
- "isStraight": true,
179
+ "isFlush": false, // Filtered out in strict mode (royal flush is stronger)
180
+ "isStraight": false, // Filtered out in strict mode (royal flush is stronger)
146
181
  "isRoyalFlush": true,
147
- "isStraightFlush": true,
182
+ "isStraightFlush": false, // Filtered out in strict mode (royal flush is stronger)
148
183
  "isTopPair": false,
149
184
  "isMiddlePair": false,
150
185
  "isBottomPair": false,
@@ -160,8 +195,109 @@ The `evaluateHand()` function returns an object where:
160
195
  "isInsideStraightDraw": false,
161
196
  "isStraightDraw": false,
162
197
  "isHighCard": false,
163
- "keyCards": ["As", "Ks", "Qs", "Js", "Ts"],
164
- "kickerCards": []
198
+ "keyCards": {
199
+ "isRoyalFlush": ["As", "Ks", "Qs", "Js", "Ts"],
200
+ "isStraightFlush": [],
201
+ "isFourOfAKind": [],
202
+ "isFullHouse": [],
203
+ "isFlush": [], // Empty in strict mode (royal flush is stronger)
204
+ "isStraight": [], // Empty in strict mode (royal flush is stronger)
205
+ "isThreeOfAKind": [],
206
+ "isTwoPair": [],
207
+ "isPair": [],
208
+ "isTopPair": [],
209
+ "isMiddlePair": [],
210
+ "isBottomPair": [],
211
+ "isTopAndMiddlePair": [],
212
+ "isTopAndBottomPair": [],
213
+ "isMiddleAndBottomPair": [],
214
+ "isTopThreeOfAKind": [],
215
+ "isMiddleThreeOfAKind": [],
216
+ "isBottomThreeOfAKind": [],
217
+ "isFlushDraw": [],
218
+ "isBackdoorFlushDraw": [],
219
+ "isOpenEndedStraightDraw": [],
220
+ "isInsideStraightDraw": [],
221
+ "isStraightDraw": [],
222
+ "isStraightWheel": [],
223
+ "isOpenEndedStraightDrawWheel": [],
224
+ "isInsideStraightDrawWheel": [],
225
+ "isHighCard": []
226
+ },
227
+ "kickerCards": {
228
+ "isRoyalFlush": [],
229
+ "isStraightFlush": [],
230
+ "isFourOfAKind": [],
231
+ "isFullHouse": [],
232
+ "isFlush": [],
233
+ "isStraight": [],
234
+ "isThreeOfAKind": [],
235
+ "isTwoPair": [],
236
+ "isPair": [],
237
+ "isTopPair": [],
238
+ "isMiddlePair": [],
239
+ "isBottomPair": [],
240
+ "isTopAndMiddlePair": [],
241
+ "isTopAndBottomPair": [],
242
+ "isMiddleAndBottomPair": [],
243
+ "isTopThreeOfAKind": [],
244
+ "isMiddleThreeOfAKind": [],
245
+ "isBottomThreeOfAKind": [],
246
+ "isFlushDraw": [],
247
+ "isBackdoorFlushDraw": [],
248
+ "isOpenEndedStraightDraw": [],
249
+ "isInsideStraightDraw": [],
250
+ "isStraightDraw": [],
251
+ "isStraightWheel": [],
252
+ "isOpenEndedStraightDrawWheel": [],
253
+ "isInsideStraightDrawWheel": [],
254
+ "isHighCard": []
255
+ }
256
+ }
257
+ }
258
+ ```
259
+
260
+ **Example 2: Pair with Flush Draw (strict mode - default)**
261
+
262
+ ```javascript
263
+ {
264
+ "As, Ah, Ks, Qs, Js": {
265
+ "isPair": true,
266
+ "isFlushDraw": true, // Draws can coexist with made hands in strict mode
267
+ // ... other boolean properties ...
268
+ "keyCards": {
269
+ "isPair": ["As", "Ah"],
270
+ "isFlushDraw": ["As", "Ks", "Qs", "Js"],
271
+ // ... other hand types with empty arrays ...
272
+ },
273
+ "kickerCards": {
274
+ "isPair": ["Ks", "Qs", "Js"],
275
+ "isFlushDraw": ["Ah"],
276
+ // ... other hand types with empty arrays ...
277
+ }
278
+ }
279
+ }
280
+ ```
281
+
282
+ **Example 3: Two Pair (strict mode - default)**
283
+
284
+ ```javascript
285
+ {
286
+ "As, Ah, Kd, Kc, Qs": {
287
+ "isTwoPair": true,
288
+ "isPair": false, // Filtered out in strict mode (two pair is stronger)
289
+ "isTopAndMiddlePair": true,
290
+ // ... other boolean properties ...
291
+ "keyCards": {
292
+ "isTwoPair": ["As", "Ah", "Kd", "Kc"],
293
+ "isPair": [], // Empty in strict mode
294
+ // ... other hand types with empty arrays ...
295
+ },
296
+ "kickerCards": {
297
+ "isTwoPair": ["Qs"],
298
+ "isPair": [], // Empty in strict mode
299
+ // ... other hand types with empty arrays ...
300
+ }
165
301
  }
166
302
  }
167
303
  ```
@@ -244,12 +380,13 @@ The test suite includes 572+ tests covering:
244
380
 
245
381
  ## API Reference
246
382
 
247
- ### `evaluateHand(cards)`
383
+ ### `evaluateHand(cards, strict)`
248
384
 
249
385
  Main function to evaluate poker hands.
250
386
 
251
387
  **Parameters:**
252
388
  - `cards` (string[]): Array of 5 or more card strings
389
+ - `strict` (boolean, optional): If `true`, only the strongest made hand and stronger draws are marked. If `false`, all applicable hand types are marked (default: `true`).
253
390
 
254
391
  **Returns:**
255
392
  - Object mapping sorted hand strings to evaluation objects
@@ -262,17 +399,19 @@ Main function to evaluate poker hands.
262
399
  #### Constructor
263
400
 
264
401
  ```javascript
265
- new HandEvaluator(cards)
402
+ new HandEvaluator(cards, strict)
266
403
  ```
267
404
 
268
405
  Creates a new HandEvaluator instance.
269
406
 
270
407
  **Parameters:**
271
408
  - `cards` (string[]): Array of 5 or more card strings
409
+ - `strict` (boolean, optional): If `true`, only the strongest made hand and stronger draws are marked. If `false`, all applicable hand types are marked (default: `true`).
272
410
 
273
411
  **Properties:**
274
412
  - `evaluation` - Object mapping sorted hand strings to evaluation objects (same format as `evaluateHand()` return value)
275
413
  - `cards` - Array of input cards
414
+ - `strict` - Boolean indicating whether strict mode is enabled
276
415
 
277
416
  **Note**: All internal methods are private. Use the `evaluation` property to access evaluation results.
278
417
 
package/evaluator.js CHANGED
@@ -26,14 +26,23 @@ class HandEvaluator {
26
26
  * @param {string[]} cards - Array of 5 or more card strings in format "Rs" where:
27
27
  * - R is rank: A, K, Q, J, T, 9, 8, 7, 6, 5, 4, 3, 2
28
28
  * - s is suit: s (spades), h (hearts), d (diamonds), c (clubs)
29
+ * @param {boolean} [strict=true] - If true, only the strongest made hand and stronger draws are marked as true.
30
+ * If false, all applicable hand types are marked (current behavior).
29
31
  * @throws {Error} If cards is not an array, has fewer than 5 cards, contains invalid format,
30
32
  * invalid rank, invalid suit, or duplicate cards
31
33
  * @example
32
34
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
35
+ * @example
36
+ * // Strict mode (default) - only strongest hand marked
37
+ * const strictEvaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs'], true);
38
+ * @example
39
+ * // Non-strict mode - all applicable hands marked
40
+ * const nonStrictEvaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs'], false);
33
41
  */
34
- constructor(cards) {
42
+ constructor(cards, strict = true) {
35
43
  this.#validateCards(cards);
36
44
  this.cards = cards;
45
+ this.strict = strict;
37
46
  this.evaluation = this.#evaluateAllCombinations();
38
47
  }
39
48
 
@@ -194,8 +203,13 @@ class HandEvaluator {
194
203
  // Add keyCards and kickerCards before restoring original evaluation
195
204
  // (these methods need access to this.evaluation)
196
205
  const evaluation = this.evaluation;
197
- evaluation.keyCards = this.#getKeyCards();
198
- evaluation.kickerCards = this.#getKickerCards();
206
+ evaluation.keyCards = this.#buildKeyCardsObject();
207
+ evaluation.kickerCards = this.#buildKickerCardsObject(evaluation.keyCards);
208
+
209
+ // Apply strict mode filtering if enabled
210
+ if (this.strict) {
211
+ this.#applyStrictMode(evaluation);
212
+ }
199
213
 
200
214
  // Restore original evaluation
201
215
  this.evaluation = originalEvaluation;
@@ -203,6 +217,64 @@ class HandEvaluator {
203
217
  return evaluation;
204
218
  }
205
219
 
220
+ /**
221
+ * Applies strict mode filtering to evaluation results.
222
+ * Only the strongest made hand and stronger draws are kept as true.
223
+ *
224
+ * @private
225
+ * @param {Object} evaluation - The evaluation object to filter
226
+ */
227
+ #applyStrictMode(evaluation) {
228
+ // Hand strength lookup (strongest = highest number)
229
+ const handStrength = {
230
+ isRoyalFlush: 10,
231
+ isStraightFlush: 9,
232
+ isFourOfAKind: 8,
233
+ isFullHouse: 7,
234
+ isFlush: 6,
235
+ isStraight: 5,
236
+ isThreeOfAKind: 4,
237
+ isTwoPair: 3,
238
+ isPair: 2,
239
+ isHighCard: 1
240
+ };
241
+
242
+ // Find strongest made hand
243
+ let strongest = 0;
244
+ for (const [hand, strength] of Object.entries(handStrength)) {
245
+ if (evaluation[hand]) strongest = Math.max(strongest, strength);
246
+ }
247
+
248
+ // Disable weaker made hands in single pass
249
+ for (const [hand, strength] of Object.entries(handStrength)) {
250
+ if (strength < strongest) evaluation[hand] = false;
251
+ }
252
+
253
+ // Disable descriptive labels if not matching strongest
254
+ if (strongest !== 2) {
255
+ evaluation.isTopPair = false;
256
+ evaluation.isMiddlePair = false;
257
+ evaluation.isBottomPair = false;
258
+ }
259
+ if (strongest !== 3) {
260
+ evaluation.isTopAndMiddlePair = false;
261
+ evaluation.isTopAndBottomPair = false;
262
+ evaluation.isMiddleAndBottomPair = false;
263
+ }
264
+ if (strongest !== 4) {
265
+ evaluation.isTopThreeOfAKind = false;
266
+ evaluation.isMiddleThreeOfAKind = false;
267
+ evaluation.isBottomThreeOfAKind = false;
268
+ }
269
+
270
+ // Filter draws: made flush or stronger cannot have straight draws
271
+ if (strongest >= 6) {
272
+ evaluation.isStraightDraw = false;
273
+ evaluation.isOpenEndedStraightDraw = false;
274
+ evaluation.isInsideStraightDraw = false;
275
+ }
276
+ }
277
+
206
278
  /**
207
279
  * Evaluates all possible 5-card combinations from the input cards.
208
280
  * For exactly 5 cards, evaluates directly. For more cards, generates all combinations
@@ -971,6 +1043,390 @@ class HandEvaluator {
971
1043
  return [];
972
1044
  }
973
1045
 
1046
+ // ==================== KEY CARDS EXTRACTION BY HAND TYPE ====================
1047
+
1048
+ /**
1049
+ * Gets key cards for royal flush (all 5 cards).
1050
+ *
1051
+ * @private
1052
+ * @returns {string[]} Array of 5 cards if royal flush, empty array otherwise
1053
+ */
1054
+ #getKeyCardsForRoyalFlush() {
1055
+ if (!this.evaluation.isRoyalFlush) return [];
1056
+ return [...this.cards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1057
+ }
1058
+
1059
+ /**
1060
+ * Gets key cards for straight flush (all 5 cards).
1061
+ *
1062
+ * @private
1063
+ * @returns {string[]} Array of 5 cards if straight flush, empty array otherwise
1064
+ */
1065
+ #getKeyCardsForStraightFlush() {
1066
+ if (!this.evaluation.isStraightFlush) return [];
1067
+ return [...this.cards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1068
+ }
1069
+
1070
+ /**
1071
+ * Gets key cards for four of a kind (4 cards of same rank).
1072
+ *
1073
+ * @private
1074
+ * @returns {string[]} Array of 4 cards if four of a kind, empty array otherwise
1075
+ */
1076
+ #getKeyCardsForFourOfAKind() {
1077
+ if (!this.evaluation.isFourOfAKind) return [];
1078
+ const rankCounts = this.#getRankCounts();
1079
+ const fourRank = Object.keys(rankCounts).find(r => rankCounts[r] === 4);
1080
+ return this.cards.filter(c => c.slice(0, -1) === fourRank)
1081
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1082
+ }
1083
+
1084
+ /**
1085
+ * Gets key cards for full house (all 5 cards).
1086
+ *
1087
+ * @private
1088
+ * @returns {string[]} Array of 5 cards if full house, empty array otherwise
1089
+ */
1090
+ #getKeyCardsForFullHouse() {
1091
+ if (!this.evaluation.isFullHouse) return [];
1092
+ return [...this.cards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1093
+ }
1094
+
1095
+ /**
1096
+ * Gets key cards for flush (all 5 cards of same suit).
1097
+ *
1098
+ * @private
1099
+ * @returns {string[]} Array of 5 cards if flush, empty array otherwise
1100
+ */
1101
+ #getKeyCardsForFlush() {
1102
+ if (!this.evaluation.isFlush) return [];
1103
+ const suits = this.#countSuits();
1104
+ const flushSuit = Object.keys(suits).find(s => suits[s] === 5);
1105
+ return this.cards.filter(c => c.slice(-1) === flushSuit)
1106
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1107
+ }
1108
+
1109
+ /**
1110
+ * Gets key cards for straight (5 cards forming straight).
1111
+ *
1112
+ * @private
1113
+ * @returns {string[]} Array of 5 cards if straight, empty array otherwise
1114
+ */
1115
+ #getKeyCardsForStraight() {
1116
+ if (!this.evaluation.isStraight) return [];
1117
+ return this.#getStraightCards();
1118
+ }
1119
+
1120
+ /**
1121
+ * Gets key cards for three of a kind (3 cards of same rank).
1122
+ *
1123
+ * @private
1124
+ * @returns {string[]} Array of 3 cards if three of a kind, empty array otherwise
1125
+ */
1126
+ #getKeyCardsForThreeOfAKind() {
1127
+ if (!this.evaluation.isThreeOfAKind) return [];
1128
+ const rankCounts = this.#getRankCounts();
1129
+ const threeRank = Object.keys(rankCounts).find(r => rankCounts[r] === 3);
1130
+ return this.cards.filter(c => c.slice(0, -1) === threeRank)
1131
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1132
+ }
1133
+
1134
+ /**
1135
+ * Gets key cards for two pair (4 cards forming 2 pairs).
1136
+ *
1137
+ * @private
1138
+ * @returns {string[]} Array of 4 cards if two pair, empty array otherwise
1139
+ */
1140
+ #getKeyCardsForTwoPair() {
1141
+ if (!this.evaluation.isTwoPair) return [];
1142
+ const rankCounts = this.#getRankCounts();
1143
+ const pairRanks = Object.keys(rankCounts).filter(r => rankCounts[r] === 2);
1144
+ return this.cards.filter(c => pairRanks.includes(c.slice(0, -1)))
1145
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1146
+ }
1147
+
1148
+ /**
1149
+ * Gets key cards for pair (2 cards of same rank).
1150
+ *
1151
+ * @private
1152
+ * @returns {string[]} Array of 2 cards if pair, empty array otherwise
1153
+ */
1154
+ #getKeyCardsForPair() {
1155
+ if (!this.evaluation.isPair) return [];
1156
+ const rankCounts = this.#getRankCounts();
1157
+ const pairRank = Object.keys(rankCounts).find(r => rankCounts[r] === 2);
1158
+ return this.cards.filter(c => c.slice(0, -1) === pairRank)
1159
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1160
+ }
1161
+
1162
+ /**
1163
+ * Gets key cards for top pair (2 cards if top pair).
1164
+ *
1165
+ * @private
1166
+ * @returns {string[]} Array of 2 cards if top pair, empty array otherwise
1167
+ */
1168
+ #getKeyCardsForTopPair() {
1169
+ if (!this.evaluation.isTopPair) return [];
1170
+ return this.#getKeyCardsForPair();
1171
+ }
1172
+
1173
+ /**
1174
+ * Gets key cards for middle pair (2 cards if middle pair).
1175
+ *
1176
+ * @private
1177
+ * @returns {string[]} Array of 2 cards if middle pair, empty array otherwise
1178
+ */
1179
+ #getKeyCardsForMiddlePair() {
1180
+ if (!this.evaluation.isMiddlePair) return [];
1181
+ return this.#getKeyCardsForPair();
1182
+ }
1183
+
1184
+ /**
1185
+ * Gets key cards for bottom pair (2 cards if bottom pair).
1186
+ *
1187
+ * @private
1188
+ * @returns {string[]} Array of 2 cards if bottom pair, empty array otherwise
1189
+ */
1190
+ #getKeyCardsForBottomPair() {
1191
+ if (!this.evaluation.isBottomPair) return [];
1192
+ return this.#getKeyCardsForPair();
1193
+ }
1194
+
1195
+ /**
1196
+ * Gets key cards for top and middle pair (4 cards if top and middle pair).
1197
+ *
1198
+ * @private
1199
+ * @returns {string[]} Array of 4 cards if top and middle pair, empty array otherwise
1200
+ */
1201
+ #getKeyCardsForTopAndMiddlePair() {
1202
+ if (!this.evaluation.isTopAndMiddlePair) return [];
1203
+ return this.#getKeyCardsForTwoPair();
1204
+ }
1205
+
1206
+ /**
1207
+ * Gets key cards for top and bottom pair (4 cards if top and bottom pair).
1208
+ *
1209
+ * @private
1210
+ * @returns {string[]} Array of 4 cards if top and bottom pair, empty array otherwise
1211
+ */
1212
+ #getKeyCardsForTopAndBottomPair() {
1213
+ if (!this.evaluation.isTopAndBottomPair) return [];
1214
+ return this.#getKeyCardsForTwoPair();
1215
+ }
1216
+
1217
+ /**
1218
+ * Gets key cards for middle and bottom pair (4 cards if middle and bottom pair).
1219
+ *
1220
+ * @private
1221
+ * @returns {string[]} Array of 4 cards if middle and bottom pair, empty array otherwise
1222
+ */
1223
+ #getKeyCardsForMiddleAndBottomPair() {
1224
+ if (!this.evaluation.isMiddleAndBottomPair) return [];
1225
+ return this.#getKeyCardsForTwoPair();
1226
+ }
1227
+
1228
+ /**
1229
+ * Gets key cards for top three of a kind (3 cards if top three).
1230
+ *
1231
+ * @private
1232
+ * @returns {string[]} Array of 3 cards if top three of a kind, empty array otherwise
1233
+ */
1234
+ #getKeyCardsForTopThreeOfAKind() {
1235
+ if (!this.evaluation.isTopThreeOfAKind) return [];
1236
+ return this.#getKeyCardsForThreeOfAKind();
1237
+ }
1238
+
1239
+ /**
1240
+ * Gets key cards for middle three of a kind (3 cards if middle three).
1241
+ *
1242
+ * @private
1243
+ * @returns {string[]} Array of 3 cards if middle three of a kind, empty array otherwise
1244
+ */
1245
+ #getKeyCardsForMiddleThreeOfAKind() {
1246
+ if (!this.evaluation.isMiddleThreeOfAKind) return [];
1247
+ return this.#getKeyCardsForThreeOfAKind();
1248
+ }
1249
+
1250
+ /**
1251
+ * Gets key cards for bottom three of a kind (3 cards if bottom three).
1252
+ *
1253
+ * @private
1254
+ * @returns {string[]} Array of 3 cards if bottom three of a kind, empty array otherwise
1255
+ */
1256
+ #getKeyCardsForBottomThreeOfAKind() {
1257
+ if (!this.evaluation.isBottomThreeOfAKind) return [];
1258
+ return this.#getKeyCardsForThreeOfAKind();
1259
+ }
1260
+
1261
+ /**
1262
+ * Gets key cards for flush draw (4 cards of same suit).
1263
+ *
1264
+ * @private
1265
+ * @returns {string[]} Array of 4 cards if flush draw, empty array otherwise
1266
+ */
1267
+ #getKeyCardsForFlushDraw() {
1268
+ if (!this.evaluation.isFlushDraw) return [];
1269
+ const suits = this.#countSuits();
1270
+ const flushSuit = Object.keys(suits).find(s => suits[s] === 4);
1271
+ return this.cards.filter(c => c.slice(-1) === flushSuit)
1272
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1273
+ }
1274
+
1275
+ /**
1276
+ * Gets key cards for backdoor flush draw (3 cards of same suit).
1277
+ *
1278
+ * @private
1279
+ * @returns {string[]} Array of 3 cards if backdoor flush draw, empty array otherwise
1280
+ */
1281
+ #getKeyCardsForBackdoorFlushDraw() {
1282
+ if (!this.evaluation.isBackdoorFlushDraw) return [];
1283
+ const suits = this.#countSuits();
1284
+ const flushSuit = Object.keys(suits).find(s => suits[s] === 3);
1285
+ return this.cards.filter(c => c.slice(-1) === flushSuit)
1286
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1287
+ }
1288
+
1289
+ /**
1290
+ * Gets key cards for open-ended straight draw (4 cards forming draw).
1291
+ *
1292
+ * @private
1293
+ * @returns {string[]} Array of 4 cards if open-ended straight draw, empty array otherwise
1294
+ */
1295
+ #getKeyCardsForOpenEndedStraightDraw() {
1296
+ if (!this.evaluation.isOpenEndedStraightDraw) return [];
1297
+ return this.#getStraightDrawCards();
1298
+ }
1299
+
1300
+ /**
1301
+ * Gets key cards for inside straight draw (4 cards forming draw).
1302
+ *
1303
+ * @private
1304
+ * @returns {string[]} Array of 4 cards if inside straight draw, empty array otherwise
1305
+ */
1306
+ #getKeyCardsForInsideStraightDraw() {
1307
+ if (!this.evaluation.isInsideStraightDraw) return [];
1308
+ return this.#getStraightDrawCards();
1309
+ }
1310
+
1311
+ /**
1312
+ * Gets key cards for straight draw (4 cards forming any straight draw).
1313
+ *
1314
+ * @private
1315
+ * @returns {string[]} Array of 4 cards if straight draw, empty array otherwise
1316
+ */
1317
+ #getKeyCardsForStraightDraw() {
1318
+ if (!this.evaluation.isStraightDraw) return [];
1319
+ return this.#getStraightDrawCards();
1320
+ }
1321
+
1322
+ /**
1323
+ * Gets key cards for wheel straight (5 cards forming wheel).
1324
+ *
1325
+ * @private
1326
+ * @returns {string[]} Array of 5 cards if wheel straight, empty array otherwise
1327
+ */
1328
+ #getKeyCardsForStraightWheel() {
1329
+ if (!this.evaluation.isStraightWheel) return [];
1330
+ return this.#getStraightCards();
1331
+ }
1332
+
1333
+ /**
1334
+ * Gets key cards for open-ended straight draw wheel (4 cards forming wheel draw).
1335
+ *
1336
+ * @private
1337
+ * @returns {string[]} Array of 4 cards if wheel open-ended draw, empty array otherwise
1338
+ */
1339
+ #getKeyCardsForOpenEndedStraightDrawWheel() {
1340
+ if (!this.evaluation.isOpenEndedStraightDrawWheel) return [];
1341
+ return this.#getStraightDrawCards();
1342
+ }
1343
+
1344
+ /**
1345
+ * Gets key cards for inside straight draw wheel (4 cards forming wheel draw).
1346
+ *
1347
+ * @private
1348
+ * @returns {string[]} Array of 4 cards if wheel inside draw, empty array otherwise
1349
+ */
1350
+ #getKeyCardsForInsideStraightDrawWheel() {
1351
+ if (!this.evaluation.isInsideStraightDrawWheel) return [];
1352
+ return this.#getStraightDrawCards();
1353
+ }
1354
+
1355
+ /**
1356
+ * Gets key cards for high card (empty array).
1357
+ *
1358
+ * @private
1359
+ * @returns {string[]} Always returns empty array
1360
+ */
1361
+ #getKeyCardsForHighCard() {
1362
+ return [];
1363
+ }
1364
+
1365
+ /**
1366
+ * Builds the keyCards object with entries for all hand types.
1367
+ *
1368
+ * @private
1369
+ * @returns {Object} Object with keys for all hand types, values are arrays of key cards
1370
+ */
1371
+ #buildKeyCardsObject() {
1372
+ return {
1373
+ isRoyalFlush: this.#getKeyCardsForRoyalFlush(),
1374
+ isStraightFlush: this.#getKeyCardsForStraightFlush(),
1375
+ isFourOfAKind: this.#getKeyCardsForFourOfAKind(),
1376
+ isFullHouse: this.#getKeyCardsForFullHouse(),
1377
+ isFlush: this.#getKeyCardsForFlush(),
1378
+ isStraight: this.#getKeyCardsForStraight(),
1379
+ isThreeOfAKind: this.#getKeyCardsForThreeOfAKind(),
1380
+ isTwoPair: this.#getKeyCardsForTwoPair(),
1381
+ isPair: this.#getKeyCardsForPair(),
1382
+ isTopPair: this.#getKeyCardsForTopPair(),
1383
+ isMiddlePair: this.#getKeyCardsForMiddlePair(),
1384
+ isBottomPair: this.#getKeyCardsForBottomPair(),
1385
+ isTopAndMiddlePair: this.#getKeyCardsForTopAndMiddlePair(),
1386
+ isTopAndBottomPair: this.#getKeyCardsForTopAndBottomPair(),
1387
+ isMiddleAndBottomPair: this.#getKeyCardsForMiddleAndBottomPair(),
1388
+ isTopThreeOfAKind: this.#getKeyCardsForTopThreeOfAKind(),
1389
+ isMiddleThreeOfAKind: this.#getKeyCardsForMiddleThreeOfAKind(),
1390
+ isBottomThreeOfAKind: this.#getKeyCardsForBottomThreeOfAKind(),
1391
+ isFlushDraw: this.#getKeyCardsForFlushDraw(),
1392
+ isBackdoorFlushDraw: this.#getKeyCardsForBackdoorFlushDraw(),
1393
+ isOpenEndedStraightDraw: this.#getKeyCardsForOpenEndedStraightDraw(),
1394
+ isInsideStraightDraw: this.#getKeyCardsForInsideStraightDraw(),
1395
+ isStraightDraw: this.#getKeyCardsForStraightDraw(),
1396
+ isStraightWheel: this.#getKeyCardsForStraightWheel(),
1397
+ isOpenEndedStraightDrawWheel: this.#getKeyCardsForOpenEndedStraightDrawWheel(),
1398
+ isInsideStraightDrawWheel: this.#getKeyCardsForInsideStraightDrawWheel(),
1399
+ isHighCard: this.#getKeyCardsForHighCard()
1400
+ };
1401
+ }
1402
+
1403
+ /**
1404
+ * Builds the kickerCards object with entries for all hand types.
1405
+ *
1406
+ * @private
1407
+ * @param {Object} keyCardsObj - The keyCards object
1408
+ * @returns {Object} Object with keys for all hand types, values are arrays of kicker cards
1409
+ */
1410
+ #buildKickerCardsObject(keyCardsObj) {
1411
+ const sortedCards = [...this.cards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1412
+ const result = {};
1413
+
1414
+ for (const [handType, keyCards] of Object.entries(keyCardsObj)) {
1415
+ if (handType === 'isHighCard') {
1416
+ result[handType] = sortedCards;
1417
+ } else if (keyCards.length === 0) {
1418
+ result[handType] = [];
1419
+ } else {
1420
+ const keyCardsSet = new Set(keyCards);
1421
+ result[handType] = this.cards
1422
+ .filter(c => !keyCardsSet.has(c))
1423
+ .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1424
+ }
1425
+ }
1426
+
1427
+ return result;
1428
+ }
1429
+
974
1430
  /**
975
1431
  * Helper method to extract the 4 cards that form a straight draw.
976
1432
  * Handles both standard and wheel straight draws.
@@ -1010,107 +1466,6 @@ class HandEvaluator {
1010
1466
  return [];
1011
1467
  }
1012
1468
 
1013
- /**
1014
- * Determines the primary made hand or draw and extracts the key cards.
1015
- * Assumes cards are already sorted and evaluation is complete.
1016
- *
1017
- * @private
1018
- * @returns {string[]} Array of key cards, sorted from highest to lowest rank
1019
- */
1020
- #getKeyCards() {
1021
- // Ensure cards are sorted once (they likely already are from evaluation)
1022
- const sortedCards = [...this.cards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1023
-
1024
- // Calculate these once and reuse
1025
- const rankCounts = this.#getRankCounts();
1026
- const suits = this.#countSuits();
1027
-
1028
- // Use evaluation results directly - no re-checking
1029
- // Check made hands in priority order
1030
- if (this.evaluation.isRoyalFlush || this.evaluation.isStraightFlush ||
1031
- this.evaluation.isFullHouse) {
1032
- return sortedCards; // All 5 cards are key cards
1033
- }
1034
-
1035
- if (this.evaluation.isStraight) {
1036
- return this.#getStraightCards();
1037
- }
1038
-
1039
- if (this.evaluation.isFourOfAKind) {
1040
- const fourRank = Object.keys(rankCounts).find(r => rankCounts[r] === 4);
1041
- return sortedCards.filter(c => c.slice(0, -1) === fourRank)
1042
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1043
- }
1044
-
1045
- if (this.evaluation.isFlush) {
1046
- const flushSuit = Object.keys(suits).find(s => suits[s] === 5);
1047
- return sortedCards.filter(c => c.slice(-1) === flushSuit)
1048
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1049
- }
1050
-
1051
- if (this.evaluation.isThreeOfAKind) {
1052
- const threeRank = Object.keys(rankCounts).find(r => rankCounts[r] === 3);
1053
- return sortedCards.filter(c => c.slice(0, -1) === threeRank)
1054
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1055
- }
1056
-
1057
- if (this.evaluation.isTwoPair) {
1058
- const pairRanks = Object.keys(rankCounts).filter(r => rankCounts[r] === 2);
1059
- return sortedCards.filter(c => pairRanks.includes(c.slice(0, -1)))
1060
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1061
- }
1062
-
1063
- if (this.evaluation.isPair) {
1064
- const pairRank = Object.keys(rankCounts).find(r => rankCounts[r] === 2);
1065
- return sortedCards.filter(c => c.slice(0, -1) === pairRank)
1066
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1067
- }
1068
-
1069
- // Check draws (only if no made hand)
1070
- if (this.evaluation.isFlushDraw) {
1071
- const flushSuit = Object.keys(suits).find(s => suits[s] === 4);
1072
- return sortedCards.filter(c => c.slice(-1) === flushSuit)
1073
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1074
- }
1075
-
1076
- if (this.evaluation.isBackdoorFlushDraw) {
1077
- const flushSuit = Object.keys(suits).find(s => suits[s] === 3);
1078
- return sortedCards.filter(c => c.slice(-1) === flushSuit)
1079
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1080
- }
1081
-
1082
- if (this.evaluation.isOpenEndedStraightDraw || this.evaluation.isInsideStraightDraw) {
1083
- const drawCards = this.#getStraightDrawCards();
1084
- // Only return if we actually found draw cards (safety check)
1085
- if (drawCards.length === 4) {
1086
- return drawCards;
1087
- }
1088
- }
1089
-
1090
- // High card or no hand/draw
1091
- return [];
1092
- }
1093
-
1094
- /**
1095
- * Gets the kicker cards (cards not in the made hand or draw).
1096
- *
1097
- * @private
1098
- * @returns {string[]} Array of kicker cards, sorted from highest to lowest rank
1099
- */
1100
- #getKickerCards() {
1101
- const keyCards = this.#getKeyCards();
1102
- if (keyCards.length === 0) {
1103
- // All cards are kickers (high card)
1104
- return [...this.cards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1105
- }
1106
-
1107
- // Use Set for O(1) lookup
1108
- const keyCardsSet = new Set(keyCards);
1109
- return this.cards
1110
- .filter(c => !keyCardsSet.has(c))
1111
- .sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
1112
- }
1113
-
1114
1469
  // ==================== HIGH CARD ====================
1115
1470
 
1116
1471
  /**
@@ -1140,10 +1495,12 @@ class HandEvaluator {
1140
1495
  * Generates all 5-card combinations and evaluates each one.
1141
1496
  *
1142
1497
  * @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)
1498
+ * @param {boolean} [strict=true] - If true, only the strongest made hand and stronger draws are marked as true.
1499
+ * If false, all applicable hand types are marked.
1143
1500
  * @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
1144
1501
  */
1145
- function evaluateHand(cards) {
1146
- const evaluator = new HandEvaluator(cards);
1502
+ function evaluateHand(cards, strict = true) {
1503
+ const evaluator = new HandEvaluator(cards, strict);
1147
1504
  return evaluator.evaluation;
1148
1505
  }
1149
1506
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-extval",
3
- "version": "2.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [