poker-extval 3.0.0 → 3.1.1

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 +75 -16
  2. package/evaluator.js +195 -10
  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
@@ -135,6 +166,8 @@ The `evaluateHand()` function returns an object where:
135
166
 
136
167
  ### Example Response
137
168
 
169
+ **Example 1: Royal Flush (strict mode - default)**
170
+
138
171
  ```javascript
139
172
  {
140
173
  "As, Ks, Qs, Js, Ts": {
@@ -143,10 +176,10 @@ The `evaluateHand()` function returns an object where:
143
176
  "isThreeOfAKind": false,
144
177
  "isTwoPair": false,
145
178
  "isPair": false,
146
- "isFlush": true,
147
- "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)
148
181
  "isRoyalFlush": true,
149
- "isStraightFlush": true,
182
+ "isStraightFlush": false, // Filtered out in strict mode (royal flush is stronger)
150
183
  "isTopPair": false,
151
184
  "isMiddlePair": false,
152
185
  "isBottomPair": false,
@@ -167,8 +200,8 @@ The `evaluateHand()` function returns an object where:
167
200
  "isStraightFlush": [],
168
201
  "isFourOfAKind": [],
169
202
  "isFullHouse": [],
170
- "isFlush": ["As", "Ks", "Qs", "Js", "Ts"],
171
- "isStraight": ["As", "Ks", "Qs", "Js", "Ts"],
203
+ "isFlush": [], // Empty in strict mode (royal flush is stronger)
204
+ "isStraight": [], // Empty in strict mode (royal flush is stronger)
172
205
  "isThreeOfAKind": [],
173
206
  "isTwoPair": [],
174
207
  "isPair": [],
@@ -224,13 +257,13 @@ The `evaluateHand()` function returns an object where:
224
257
  }
225
258
  ```
226
259
 
227
- **Example 2: Pair with Flush Draw**
260
+ **Example 2: Pair with Flush Draw (strict mode - default)**
228
261
 
229
262
  ```javascript
230
263
  {
231
264
  "As, Ah, Ks, Qs, Js": {
232
265
  "isPair": true,
233
- "isFlushDraw": true,
266
+ "isFlushDraw": true, // Draws can coexist with made hands in strict mode
234
267
  // ... other boolean properties ...
235
268
  "keyCards": {
236
269
  "isPair": ["As", "Ah"],
@@ -246,6 +279,29 @@ The `evaluateHand()` function returns an object where:
246
279
  }
247
280
  ```
248
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
+ }
301
+ }
302
+ }
303
+ ```
304
+
249
305
  ## Performance Benchmarks
250
306
 
251
307
  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:
@@ -324,12 +380,13 @@ The test suite includes 572+ tests covering:
324
380
 
325
381
  ## API Reference
326
382
 
327
- ### `evaluateHand(cards)`
383
+ ### `evaluateHand(cards, strict)`
328
384
 
329
385
  Main function to evaluate poker hands.
330
386
 
331
387
  **Parameters:**
332
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`).
333
390
 
334
391
  **Returns:**
335
392
  - Object mapping sorted hand strings to evaluation objects
@@ -342,17 +399,19 @@ Main function to evaluate poker hands.
342
399
  #### Constructor
343
400
 
344
401
  ```javascript
345
- new HandEvaluator(cards)
402
+ new HandEvaluator(cards, strict)
346
403
  ```
347
404
 
348
405
  Creates a new HandEvaluator instance.
349
406
 
350
407
  **Parameters:**
351
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`).
352
410
 
353
411
  **Properties:**
354
412
  - `evaluation` - Object mapping sorted hand strings to evaluation objects (same format as `evaluateHand()` return value)
355
413
  - `cards` - Array of input cards
414
+ - `strict` - Boolean indicating whether strict mode is enabled
356
415
 
357
416
  **Note**: All internal methods are private. Use the `evaluation` property to access evaluation results.
358
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
 
@@ -197,12 +206,190 @@ class HandEvaluator {
197
206
  evaluation.keyCards = this.#buildKeyCardsObject();
198
207
  evaluation.kickerCards = this.#buildKickerCardsObject(evaluation.keyCards);
199
208
 
209
+ // Apply strict mode filtering if enabled
210
+ if (this.strict) {
211
+ this.#applyStrictMode(evaluation);
212
+ }
213
+
200
214
  // Restore original evaluation
201
215
  this.evaluation = originalEvaluation;
202
216
 
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
+
278
+ /**
279
+ * Gets the hand strength value for an evaluation.
280
+ *
281
+ * @private
282
+ * @param {Object} evaluation - The evaluation object
283
+ * @returns {number} Hand strength (10 = Royal Flush, 1 = High Card, 0 = no made hand)
284
+ */
285
+ #getHandStrength(evaluation) {
286
+ const handStrength = {
287
+ isRoyalFlush: 10, isStraightFlush: 9, isFourOfAKind: 8,
288
+ isFullHouse: 7, isFlush: 6, isStraight: 5, isThreeOfAKind: 4,
289
+ isTwoPair: 3, isPair: 2, isHighCard: 1
290
+ };
291
+ for (const [hand, strength] of Object.entries(handStrength)) {
292
+ if (evaluation[hand]) return strength;
293
+ }
294
+ return 0;
295
+ }
296
+
297
+ /**
298
+ * Gets sorted key card ranks for an evaluation.
299
+ *
300
+ * @private
301
+ * @param {Object} evaluation - The evaluation object
302
+ * @returns {number[]} Array of rank values sorted descending
303
+ */
304
+ #getKeyCardRanks(evaluation) {
305
+ const handStrength = {
306
+ isRoyalFlush: 10, isStraightFlush: 9, isFourOfAKind: 8,
307
+ isFullHouse: 7, isFlush: 6, isStraight: 5, isThreeOfAKind: 4,
308
+ isTwoPair: 3, isPair: 2, isHighCard: 1
309
+ };
310
+ for (const [hand] of Object.entries(handStrength)) {
311
+ if (evaluation[hand]) {
312
+ const keyCards = evaluation.keyCards[hand] || [];
313
+ return keyCards.map(c => this.#getRankValue(c)).sort((a, b) => b - a);
314
+ }
315
+ }
316
+ return [];
317
+ }
318
+
319
+ /**
320
+ * Gets sorted kicker ranks for an evaluation.
321
+ *
322
+ * @private
323
+ * @param {Object} evaluation - The evaluation object
324
+ * @returns {number[]} Array of rank values sorted descending
325
+ */
326
+ #getKickerRanks(evaluation) {
327
+ const handStrength = {
328
+ isRoyalFlush: 10, isStraightFlush: 9, isFourOfAKind: 8,
329
+ isFullHouse: 7, isFlush: 6, isStraight: 5, isThreeOfAKind: 4,
330
+ isTwoPair: 3, isPair: 2, isHighCard: 1
331
+ };
332
+ for (const [hand] of Object.entries(handStrength)) {
333
+ if (evaluation[hand]) {
334
+ const kickers = evaluation.kickerCards[hand] || [];
335
+ return kickers.map(c => this.#getRankValue(c)).sort((a, b) => b - a);
336
+ }
337
+ }
338
+ return [];
339
+ }
340
+
341
+ /**
342
+ * Compares two evaluations to determine which is better.
343
+ *
344
+ * @private
345
+ * @param {Object} eval1 - First evaluation object
346
+ * @param {Object} eval2 - Second evaluation object
347
+ * @returns {number} Positive if eval2 is better, negative if eval1 is better, 0 if equal
348
+ */
349
+ #compareEvaluations(eval1, eval2) {
350
+ const strength1 = this.#getHandStrength(eval1);
351
+ const strength2 = this.#getHandStrength(eval2);
352
+ if (strength1 !== strength2) return strength2 - strength1;
353
+
354
+ const keyRanks1 = this.#getKeyCardRanks(eval1);
355
+ const keyRanks2 = this.#getKeyCardRanks(eval2);
356
+ for (let i = 0; i < Math.max(keyRanks1.length, keyRanks2.length); i++) {
357
+ const rank1 = keyRanks1[i] || 0;
358
+ const rank2 = keyRanks2[i] || 0;
359
+ if (rank1 !== rank2) return rank2 - rank1;
360
+ }
361
+
362
+ const kickerRanks1 = this.#getKickerRanks(eval1);
363
+ const kickerRanks2 = this.#getKickerRanks(eval2);
364
+ for (let i = 0; i < Math.max(kickerRanks1.length, kickerRanks2.length); i++) {
365
+ const rank1 = kickerRanks1[i] || 0;
366
+ const rank2 = kickerRanks2[i] || 0;
367
+ if (rank1 !== rank2) return rank2 - rank1;
368
+ }
369
+ return 0;
370
+ }
371
+
372
+ /**
373
+ * Finds the best combination from a result object.
374
+ *
375
+ * @private
376
+ * @param {Object} result - Object mapping hand keys to evaluation objects
377
+ * @returns {Object} Object with only the best combination
378
+ */
379
+ #findBestCombination(result) {
380
+ let bestKey = null;
381
+ let bestEval = null;
382
+ for (const [key, evaluation] of Object.entries(result)) {
383
+ const strength = this.#getHandStrength(evaluation);
384
+ const bestStrength = bestEval ? this.#getHandStrength(bestEval) : 0;
385
+ if (!bestEval || strength > bestStrength || (strength === bestStrength && this.#compareEvaluations(evaluation, bestEval) > 0)) {
386
+ bestKey = key;
387
+ bestEval = evaluation;
388
+ }
389
+ }
390
+ return bestKey ? { [bestKey]: bestEval } : result;
391
+ }
392
+
206
393
  /**
207
394
  * Evaluates all possible 5-card combinations from the input cards.
208
395
  * For exactly 5 cards, evaluates directly. For more cards, generates all combinations
@@ -218,26 +405,22 @@ class HandEvaluator {
218
405
  * // With 7 cards: returns object with 21 key-value pairs (C(7,5) = 21)
219
406
  */
220
407
  #evaluateAllCombinations() {
221
- // If exactly 5 cards, evaluate directly and wrap in the new format
222
408
  if (this.cards.length === 5) {
223
409
  const key = this.#formatHandKey(this.cards);
224
410
  const evaluation = this.#evaluateSingleHand();
225
411
  return { [key]: evaluation };
226
412
  }
227
413
 
228
- // For more than 5 cards, generate all combinations
229
414
  const combinations = this.#generateCombinations(this.cards, 5);
230
415
  const result = {};
231
416
  for (const combination of combinations) {
232
417
  const key = this.#formatHandKey(combination);
233
- // Create a temporary evaluator with the 5-card combination
234
- // This will evaluate directly since it's exactly 5 cards
235
- const tempEvaluator = new HandEvaluator(combination);
236
- // Extract the evaluation object from the single key-value pair
418
+ const tempEvaluator = new HandEvaluator(combination, this.strict);
237
419
  const evaluationKey = Object.keys(tempEvaluator.evaluation)[0];
238
420
  result[key] = tempEvaluator.evaluation[evaluationKey];
239
421
  }
240
- return result;
422
+
423
+ return this.strict ? this.#findBestCombination(result) : result;
241
424
  }
242
425
 
243
426
  /**
@@ -1423,10 +1606,12 @@ class HandEvaluator {
1423
1606
  * Generates all 5-card combinations and evaluates each one.
1424
1607
  *
1425
1608
  * @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)
1609
+ * @param {boolean} [strict=true] - If true, only the strongest made hand and stronger draws are marked as true.
1610
+ * If false, all applicable hand types are marked.
1426
1611
  * @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
1427
1612
  */
1428
- function evaluateHand(cards) {
1429
- const evaluator = new HandEvaluator(cards);
1613
+ function evaluateHand(cards, strict = true) {
1614
+ const evaluator = new HandEvaluator(cards, strict);
1430
1615
  return evaluator.evaluation;
1431
1616
  }
1432
1617
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-extval",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [