poker-extval 1.0.1 → 2.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 (3) hide show
  1. package/README.md +15 -15
  2. package/evaluator.js +330 -184
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,12 +49,6 @@ const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts', '9s']);
49
49
  // Access evaluation results
50
50
  const evaluation = evaluator.evaluation;
51
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
52
  ```
59
53
 
60
54
  ### Evaluating Multiple Card Hands
@@ -126,12 +120,17 @@ The `evaluateHand()` function returns an object where:
126
120
  - `isBackdoorFlushDraw` - Three cards of the same suit (two away from flush)
127
121
  - `isOpenEndedStraightDraw` - One card away from straight on either end
128
122
  - `isInsideStraightDraw` - One card away from straight (needed card in middle)
123
+ - `isStraightDraw` - Any type of straight draw (open-ended or inside, standard or wheel)
129
124
 
130
125
  #### Wheel Straight Support
131
126
  - `isStraightWheel` - Wheel straight (A-2-3-4-5)
132
127
  - `isOpenEndedStraightDrawWheel` - Open-ended draw using wheel ranking
133
128
  - `isInsideStraightDrawWheel` - Inside draw using wheel ranking
134
129
 
130
+ #### 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
133
+
135
134
  ### Example Response
136
135
 
137
136
  ```javascript
@@ -159,7 +158,10 @@ The `evaluateHand()` function returns an object where:
159
158
  "isBackdoorFlushDraw": false,
160
159
  "isOpenEndedStraightDraw": false,
161
160
  "isInsideStraightDraw": false,
162
- "isHighCard": false
161
+ "isStraightDraw": false,
162
+ "isHighCard": false,
163
+ "keyCards": ["As", "Ks", "Qs", "Js", "Ts"],
164
+ "kickerCards": []
163
165
  }
164
166
  }
165
167
  ```
@@ -265,16 +267,14 @@ new HandEvaluator(cards)
265
267
 
266
268
  Creates a new HandEvaluator instance.
267
269
 
268
- #### Methods
270
+ **Parameters:**
271
+ - `cards` (string[]): Array of 5 or more card strings
269
272
 
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)
273
+ **Properties:**
274
+ - `evaluation` - Object mapping sorted hand strings to evaluation objects (same format as `evaluateHand()` return value)
275
+ - `cards` - Array of input cards
276
276
 
277
- All evaluation methods (e.g., `isPair()`, `isFlush()`, etc.) are available but typically accessed through the `evaluation` property.
277
+ **Note**: All internal methods are private. Use the `evaluation` property to access evaluation results.
278
278
 
279
279
  ## License
280
280
 
package/evaluator.js CHANGED
@@ -166,34 +166,40 @@ class HandEvaluator {
166
166
  this.evaluation = {};
167
167
 
168
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.isStraightDraw();
192
- this.isHighCard();
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.#isStraightDraw();
192
+ this.#isHighCard();
193
193
 
194
- // Get the evaluation object and restore original
194
+ // Add keyCards and kickerCards before restoring original evaluation
195
+ // (these methods need access to this.evaluation)
195
196
  const evaluation = this.evaluation;
197
+ evaluation.keyCards = this.#getKeyCards();
198
+ evaluation.kickerCards = this.#getKickerCards();
199
+
200
+ // Restore original evaluation
196
201
  this.evaluation = originalEvaluation;
202
+
197
203
  return evaluation;
198
204
  }
199
205
 
@@ -275,18 +281,12 @@ class HandEvaluator {
275
281
  * Sorts cards from highest to lowest rank using standard poker ranking (Ace high).
276
282
  * Modifies the array in place. If no cards parameter is provided, sorts this.cards.
277
283
  *
284
+ * @private
278
285
  * @param {string[]} [cards=null] - Optional array of card strings to sort.
279
286
  * If null or undefined, sorts this.cards instead.
280
287
  * @returns {void}
281
- * @example
282
- * const evaluator = new HandEvaluator(['2s', 'As', 'Ks', '5s', 'Ts']);
283
- * evaluator.sort(); // Modifies evaluator.cards to ['As', 'Ks', 'Ts', '5s', '2s']
284
- *
285
- * @example
286
- * const cards = ['2s', 'As', 'Ks'];
287
- * evaluator.sort(cards); // Modifies cards array to ['As', 'Ks', '2s']
288
288
  */
289
- sort(cards = null) {
289
+ #sort(cards = null) {
290
290
  const cardsToSort = cards || this.cards;
291
291
  cardsToSort.sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
292
292
  }
@@ -296,14 +296,12 @@ class HandEvaluator {
296
296
  * Modifies the array in place. Used for evaluating wheel straights (A-2-3-4-5).
297
297
  * If no cards parameter is provided, sorts this.cards.
298
298
  *
299
+ * @private
299
300
  * @param {string[]} [cards=null] - Optional array of card strings to sort.
300
301
  * If null or undefined, sorts this.cards instead.
301
302
  * @returns {void}
302
- * @example
303
- * const evaluator = new HandEvaluator(['As', '2s', '3s', '4s', '5s']);
304
- * evaluator.sortWheel(); // Modifies evaluator.cards to ['5s', '4s', '3s', '2s', 'As']
305
303
  */
306
- sortWheel(cards = null) {
304
+ #sortWheel(cards = null) {
307
305
  const cardsToSort = cards || this.cards;
308
306
  cardsToSort.sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
309
307
  }
@@ -312,18 +310,12 @@ class HandEvaluator {
312
310
  * Calculates the distance (rank difference) between the highest and lowest card.
313
311
  * Uses standard poker ranking (Ace high). Distance of 4 indicates a possible straight.
314
312
  *
313
+ * @private
315
314
  * @param {string[]} [cards=null] - Optional array of card strings to evaluate.
316
315
  * If null or undefined, uses this.cards instead.
317
316
  * @returns {number} Distance between highest and lowest card (0-12)
318
- * @example
319
- * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
320
- * evaluator.getDistance(); // Returns 4 (A=13, T=9, 13-9=4)
321
- *
322
- * @example
323
- * evaluator.getDistance(['As', '2s', '3s', '4s', '5s']);
324
- * // Returns 12 (A=13, 2=1, 13-1=12)
325
317
  */
326
- getDistance(cards = null) {
318
+ #getDistance(cards = null) {
327
319
  if (cards) {
328
320
  // If cards array provided, sort a copy and calculate distance
329
321
  if (cards.length === 0) return 0;
@@ -332,7 +324,7 @@ class HandEvaluator {
332
324
  }
333
325
  // If no cards provided, use instance cards property
334
326
  if (this.cards.length === 0) return 0;
335
- this.sort();
327
+ this.#sort();
336
328
  return this.#getRankValue(this.cards[0]) - this.#getRankValue(this.cards[this.cards.length - 1]);
337
329
  }
338
330
 
@@ -340,14 +332,12 @@ class HandEvaluator {
340
332
  * Calculates the distance (rank difference) between the highest and lowest card using wheel ranking.
341
333
  * Uses wheel ranking (Ace low). Distance of 4 indicates a possible wheel straight (A-2-3-4-5).
342
334
  *
335
+ * @private
343
336
  * @param {string[]} [cards=null] - Optional array of card strings to evaluate.
344
337
  * If null or undefined, uses this.cards instead.
345
338
  * @returns {number} Distance between highest and lowest card using wheel ranking (0-12)
346
- * @example
347
- * const evaluator = new HandEvaluator(['As', '2s', '3s', '4s', '5s']);
348
- * evaluator.getDistanceWheel(); // Returns 4 (5=5, A=1, 5-1=4) - wheel straight
349
339
  */
350
- getDistanceWheel(cards = null) {
340
+ #getDistanceWheel(cards = null) {
351
341
  if (cards) {
352
342
  // If cards array provided, sort a copy using wheel values and calculate distance
353
343
  if (cards.length === 0) return 0;
@@ -356,24 +346,18 @@ class HandEvaluator {
356
346
  }
357
347
  // If no cards provided, use instance cards property
358
348
  if (this.cards.length === 0) return 0;
359
- this.sortWheel();
349
+ this.#sortWheel();
360
350
  return this.#getRankValueWheel(this.cards[0]) - this.#getRankValueWheel(this.cards[this.cards.length - 1]);
361
351
  }
362
352
 
363
353
  /**
364
354
  * Counts the occurrences of each suit in the hand.
365
355
  *
356
+ * @private
366
357
  * @returns {Object<string, number>} Object mapping suit to count:
367
358
  * { s: count, h: count, d: count, c: count }
368
- * @example
369
- * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
370
- * evaluator.countSuits(); // Returns { s: 5 }
371
- *
372
- * @example
373
- * const evaluator = new HandEvaluator(['As', 'Kh', 'Qd', 'Jc', 'Ts']);
374
- * evaluator.countSuits(); // Returns { s: 2, h: 1, d: 1, c: 1 }
375
359
  */
376
- countSuits() {
360
+ #countSuits() {
377
361
  return this.cards.reduce((acc, card) => ({ ...acc, [card.slice(-1)]: (acc[card.slice(-1)] || 0) + 1 }), {});
378
362
  }
379
363
 
@@ -381,17 +365,11 @@ class HandEvaluator {
381
365
  * Counts the occurrences of each rank and returns sorted counts (descending).
382
366
  * Used to identify pairs, three of a kind, four of a kind, etc.
383
367
  *
368
+ * @private
384
369
  * @returns {number[]} Array of rank counts sorted in descending order
385
- * @example
386
- * const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Kc', 'Ks']);
387
- * evaluator.countRanks(); // Returns [3, 2] (three aces, two kings)
388
- *
389
- * @example
390
- * const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs']);
391
- * evaluator.countRanks(); // Returns [2, 2, 1] (two aces, two kings, one queen)
392
370
  */
393
- countRanks() {
394
- this.sort();
371
+ #countRanks() {
372
+ this.#sort();
395
373
  const rankCounts = this.cards.reduce((acc, card) => (acc[card.slice(0, -1)] = (acc[card.slice(0, -1)] || 0) + 1, acc), {});
396
374
  return Object.values(rankCounts).sort((a, b) => b - a);
397
375
  }
@@ -406,7 +384,7 @@ class HandEvaluator {
406
384
  * // Returns { A: 3, K: 2 }
407
385
  */
408
386
  #getRankCounts() {
409
- this.sort();
387
+ this.#sort();
410
388
  return this.cards.reduce((acc, card) => (acc[card.slice(0, -1)] = (acc[card.slice(0, -1)] || 0) + 1, acc), {});
411
389
  }
412
390
 
@@ -440,7 +418,7 @@ class HandEvaluator {
440
418
  * // Returns [1, 1, 1, 2] (gap of 2 between J and 9)
441
419
  */
442
420
  #getGaps() {
443
- this.sort();
421
+ this.#sort();
444
422
  const gaps = [];
445
423
  for (let i = 0; i < 4; i++) {
446
424
  gaps.push(this.#getRankValue(this.cards[i]) - this.#getRankValue(this.cards[i + 1]));
@@ -460,7 +438,7 @@ class HandEvaluator {
460
438
  * // Returns [1, 1, 1, 1] (all gaps are 1)
461
439
  */
462
440
  #getGapsWheel() {
463
- this.sortWheel();
441
+ this.#sortWheel();
464
442
  const gaps = [];
465
443
  for (let i = 0; i < 4; i++) {
466
444
  gaps.push(this.#getRankValueWheel(this.cards[i]) - this.#getRankValueWheel(this.cards[i + 1]));
@@ -478,9 +456,9 @@ class HandEvaluator {
478
456
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Ac', 'Ks']);
479
457
  * evaluator.isFourOfAKind(); // Returns true
480
458
  */
481
- isFourOfAKind() {
459
+ #isFourOfAKind() {
482
460
  if (this.evaluation.isFourOfAKind !== undefined) return this.evaluation.isFourOfAKind;
483
- const ranks = this.countRanks();
461
+ const ranks = this.#countRanks();
484
462
  return this.evaluation.isFourOfAKind = ranks[0] === 4 && ranks[1] === 1;
485
463
  }
486
464
 
@@ -492,9 +470,9 @@ class HandEvaluator {
492
470
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Kc', 'Ks']);
493
471
  * evaluator.isFullHouse(); // Returns true
494
472
  */
495
- isFullHouse() {
473
+ #isFullHouse() {
496
474
  if (this.evaluation.isFullHouse !== undefined) return this.evaluation.isFullHouse;
497
- const ranks = this.countRanks();
475
+ const ranks = this.#countRanks();
498
476
  return this.evaluation.isFullHouse = ranks[0] === 3 && ranks[1] === 2;
499
477
  }
500
478
 
@@ -506,9 +484,9 @@ class HandEvaluator {
506
484
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Kc', 'Qs']);
507
485
  * evaluator.isThreeOfAKind(); // Returns true
508
486
  */
509
- isThreeOfAKind() {
487
+ #isThreeOfAKind() {
510
488
  if (this.evaluation.isThreeOfAKind !== undefined) return this.evaluation.isThreeOfAKind;
511
- const ranks = this.countRanks();
489
+ const ranks = this.#countRanks();
512
490
  return this.evaluation.isThreeOfAKind = ranks[0] === 3 && ranks[1] === 1 && ranks[2] === 1;
513
491
  }
514
492
 
@@ -520,9 +498,9 @@ class HandEvaluator {
520
498
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs']);
521
499
  * evaluator.isTwoPair(); // Returns true
522
500
  */
523
- isTwoPair() {
501
+ #isTwoPair() {
524
502
  if (this.evaluation.isTwoPair !== undefined) return this.evaluation.isTwoPair;
525
- const ranks = this.countRanks();
503
+ const ranks = this.#countRanks();
526
504
  return this.evaluation.isTwoPair = ranks[0] === 2 && ranks[1] === 2 && ranks[2] === 1;
527
505
  }
528
506
 
@@ -534,9 +512,9 @@ class HandEvaluator {
534
512
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Qc', 'Js']);
535
513
  * evaluator.isPair(); // Returns true
536
514
  */
537
- isPair() {
515
+ #isPair() {
538
516
  if (this.evaluation.isPair !== undefined) return this.evaluation.isPair;
539
- const ranks = this.countRanks();
517
+ const ranks = this.#countRanks();
540
518
  return this.evaluation.isPair = ranks[0] === 2 && ranks[1] === 1 && ranks[2] === 1 && ranks[3] === 1;
541
519
  }
542
520
 
@@ -551,9 +529,9 @@ class HandEvaluator {
551
529
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Qc', 'Js']);
552
530
  * evaluator.isTopPair(); // Returns true (pair of aces, highest card)
553
531
  */
554
- isTopPair() {
532
+ #isTopPair() {
555
533
  if (this.evaluation.isTopPair !== undefined) return this.evaluation.isTopPair;
556
- if (this.evaluation.isPair === undefined) this.isPair();
534
+ if (this.evaluation.isPair === undefined) this.#isPair();
557
535
  if (!this.evaluation.isPair) return this.evaluation.isTopPair = false;
558
536
  const rankCounts = this.#getRankCounts();
559
537
  return this.evaluation.isTopPair = rankCounts[this.cards[0].slice(0, -1)] === 2;
@@ -568,10 +546,10 @@ class HandEvaluator {
568
546
  * const evaluator = new HandEvaluator(['As', 'Kh', 'Kd', 'Qc', 'Js']);
569
547
  * evaluator.isMiddlePair(); // Returns true (pair of kings, not highest or lowest)
570
548
  */
571
- isMiddlePair() {
549
+ #isMiddlePair() {
572
550
  if (this.evaluation.isMiddlePair !== undefined) return this.evaluation.isMiddlePair;
573
- if (this.evaluation.isPair === undefined) this.isPair();
574
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
551
+ if (this.evaluation.isPair === undefined) this.#isPair();
552
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
575
553
  if (!this.evaluation.isPair || this.evaluation.isTwoPair) return this.evaluation.isMiddlePair = false;
576
554
  const ranks = this.#getRankCountsSorted();
577
555
  return this.evaluation.isMiddlePair = ranks.length === 4 && (ranks[1].count === 2 || ranks[2].count === 2);
@@ -586,9 +564,9 @@ class HandEvaluator {
586
564
  * const evaluator = new HandEvaluator(['As', 'Kh', 'Qd', '2c', '2s']);
587
565
  * evaluator.isBottomPair(); // Returns true (pair of deuces, lowest card)
588
566
  */
589
- isBottomPair() {
567
+ #isBottomPair() {
590
568
  if (this.evaluation.isBottomPair !== undefined) return this.evaluation.isBottomPair;
591
- if (this.evaluation.isPair === undefined) this.isPair();
569
+ if (this.evaluation.isPair === undefined) this.#isPair();
592
570
  if (!this.evaluation.isPair) return this.evaluation.isBottomPair = false;
593
571
  const rankCounts = this.#getRankCounts();
594
572
  return this.evaluation.isBottomPair = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 2;
@@ -605,9 +583,9 @@ class HandEvaluator {
605
583
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Kc', 'Qs']);
606
584
  * evaluator.isTopAndMiddlePair(); // Returns true (aces and kings)
607
585
  */
608
- isTopAndMiddlePair() {
586
+ #isTopAndMiddlePair() {
609
587
  if (this.evaluation.isTopAndMiddlePair !== undefined) return this.evaluation.isTopAndMiddlePair;
610
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
588
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
611
589
  if (!this.evaluation.isTwoPair) return this.evaluation.isTopAndMiddlePair = false;
612
590
  const ranks = this.#getRankCountsSorted();
613
591
  return this.evaluation.isTopAndMiddlePair = ranks[0].count === 2 && ranks[1].count === 2;
@@ -622,9 +600,9 @@ class HandEvaluator {
622
600
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Kd', 'Qc', '2s', '2h']);
623
601
  * evaluator.isTopAndBottomPair(); // Returns true (aces and deuces)
624
602
  */
625
- isTopAndBottomPair() {
603
+ #isTopAndBottomPair() {
626
604
  if (this.evaluation.isTopAndBottomPair !== undefined) return this.evaluation.isTopAndBottomPair;
627
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
605
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
628
606
  if (!this.evaluation.isTwoPair) return this.evaluation.isTopAndBottomPair = false;
629
607
  const ranks = this.#getRankCountsSorted();
630
608
  return this.evaluation.isTopAndBottomPair = ranks[0].count === 2 && ranks[ranks.length - 1].count === 2;
@@ -639,9 +617,9 @@ class HandEvaluator {
639
617
  * const evaluator = new HandEvaluator(['As', 'Kh', 'Kd', '2c', '2s']);
640
618
  * evaluator.isMiddleAndBottomPair(); // Returns true (kings and deuces)
641
619
  */
642
- isMiddleAndBottomPair() {
620
+ #isMiddleAndBottomPair() {
643
621
  if (this.evaluation.isMiddleAndBottomPair !== undefined) return this.evaluation.isMiddleAndBottomPair;
644
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
622
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
645
623
  if (!this.evaluation.isTwoPair) return this.evaluation.isMiddleAndBottomPair = false;
646
624
  const ranks = this.#getRankCountsSorted();
647
625
  return this.evaluation.isMiddleAndBottomPair = ranks.length === 3 && ranks[1].count === 2 && ranks[2].count === 2;
@@ -658,9 +636,9 @@ class HandEvaluator {
658
636
  * const evaluator = new HandEvaluator(['As', 'Ah', 'Ad', 'Kc', 'Qs']);
659
637
  * evaluator.isTopThreeOfAKind(); // Returns true (three aces)
660
638
  */
661
- isTopThreeOfAKind() {
639
+ #isTopThreeOfAKind() {
662
640
  if (this.evaluation.isTopThreeOfAKind !== undefined) return this.evaluation.isTopThreeOfAKind;
663
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
641
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
664
642
  if (!this.evaluation.isThreeOfAKind) return this.evaluation.isTopThreeOfAKind = false;
665
643
  const rankCounts = this.#getRankCounts();
666
644
  return this.evaluation.isTopThreeOfAKind = rankCounts[this.cards[0].slice(0, -1)] === 3;
@@ -675,9 +653,9 @@ class HandEvaluator {
675
653
  * const evaluator = new HandEvaluator(['As', 'Kh', 'Kd', 'Kc', 'Qs']);
676
654
  * evaluator.isMiddleThreeOfAKind(); // Returns true (three kings)
677
655
  */
678
- isMiddleThreeOfAKind() {
656
+ #isMiddleThreeOfAKind() {
679
657
  if (this.evaluation.isMiddleThreeOfAKind !== undefined) return this.evaluation.isMiddleThreeOfAKind;
680
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
658
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
681
659
  if (!this.evaluation.isThreeOfAKind) return this.evaluation.isMiddleThreeOfAKind = false;
682
660
  const ranks = this.#getRankCountsSorted();
683
661
  return this.evaluation.isMiddleThreeOfAKind = ranks.length === 3 && ranks[1].count === 3;
@@ -692,9 +670,9 @@ class HandEvaluator {
692
670
  * const evaluator = new HandEvaluator(['As', 'Kh', '2d', '2c', '2s']);
693
671
  * evaluator.isBottomThreeOfAKind(); // Returns true (three deuces)
694
672
  */
695
- isBottomThreeOfAKind() {
673
+ #isBottomThreeOfAKind() {
696
674
  if (this.evaluation.isBottomThreeOfAKind !== undefined) return this.evaluation.isBottomThreeOfAKind;
697
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
675
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
698
676
  if (!this.evaluation.isThreeOfAKind) return this.evaluation.isBottomThreeOfAKind = false;
699
677
  const rankCounts = this.#getRankCounts();
700
678
  return this.evaluation.isBottomThreeOfAKind = rankCounts[this.cards[this.cards.length - 1].slice(0, -1)] === 3;
@@ -710,9 +688,9 @@ class HandEvaluator {
710
688
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9s']);
711
689
  * evaluator.isFlush(); // Returns true (all spades)
712
690
  */
713
- isFlush() {
691
+ #isFlush() {
714
692
  if (this.evaluation.isFlush !== undefined) return this.evaluation.isFlush;
715
- const suits = this.countSuits();
693
+ const suits = this.#countSuits();
716
694
  return this.evaluation.isFlush = Object.values(suits).some(count => count === 5);
717
695
  }
718
696
 
@@ -725,11 +703,11 @@ class HandEvaluator {
725
703
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9h']);
726
704
  * evaluator.isFlushDraw(); // Returns true (4 spades, 1 heart)
727
705
  */
728
- isFlushDraw() {
706
+ #isFlushDraw() {
729
707
  if (this.evaluation.isFlushDraw !== undefined) return this.evaluation.isFlushDraw;
730
- if (this.evaluation.isFlush === undefined) this.isFlush();
708
+ if (this.evaluation.isFlush === undefined) this.#isFlush();
731
709
  if (this.evaluation.isFlush) return this.evaluation.isFlushDraw = false;
732
- const suits = this.countSuits();
710
+ const suits = this.#countSuits();
733
711
  return this.evaluation.isFlushDraw = Object.values(suits).some(count => count === 4);
734
712
  }
735
713
 
@@ -742,12 +720,12 @@ class HandEvaluator {
742
720
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Jh', '9h']);
743
721
  * evaluator.isBackdoorFlushDraw(); // Returns true (3 spades, 2 hearts)
744
722
  */
745
- isBackdoorFlushDraw() {
723
+ #isBackdoorFlushDraw() {
746
724
  if (this.evaluation.isBackdoorFlushDraw !== undefined) return this.evaluation.isBackdoorFlushDraw;
747
- if (this.evaluation.isFlush === undefined) this.isFlush();
748
- if (this.evaluation.isFlushDraw === undefined) this.isFlushDraw();
725
+ if (this.evaluation.isFlush === undefined) this.#isFlush();
726
+ if (this.evaluation.isFlushDraw === undefined) this.#isFlushDraw();
749
727
  if (this.evaluation.isFlush || this.evaluation.isFlushDraw) return this.evaluation.isBackdoorFlushDraw = false;
750
- const suits = this.countSuits();
728
+ const suits = this.#countSuits();
751
729
  return this.evaluation.isBackdoorFlushDraw = Object.values(suits).some(count => count === 3);
752
730
  }
753
731
 
@@ -767,18 +745,18 @@ class HandEvaluator {
767
745
  * const evaluator = new HandEvaluator(['As', '2s', '3s', '4s', '5s']);
768
746
  * evaluator.isStraight(); // Returns true (wheel: A-2-3-4-5)
769
747
  */
770
- isStraight() {
748
+ #isStraight() {
771
749
  if (this.evaluation.isStraight !== undefined) return this.evaluation.isStraight;
772
- if (this.evaluation.isPair === undefined) this.isPair();
773
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
774
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
775
- if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
776
- if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
750
+ if (this.evaluation.isPair === undefined) this.#isPair();
751
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
752
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
753
+ if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
754
+ if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
777
755
  if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind) return this.evaluation.isStraight = false;
778
- this.sort();
779
- const distance = this.getDistance();
756
+ this.#sort();
757
+ const distance = this.#getDistance();
780
758
  if (distance === 4) return this.evaluation.isStraight = true;
781
- if (this.isStraightWheel()) return this.evaluation.isStraight = true;
759
+ if (this.#isStraightWheel()) return this.evaluation.isStraight = true;
782
760
  return this.evaluation.isStraight = false;
783
761
  }
784
762
 
@@ -791,16 +769,16 @@ class HandEvaluator {
791
769
  * const evaluator = new HandEvaluator(['As', '2s', '3s', '4s', '5s']);
792
770
  * evaluator.isStraightWheel(); // Returns true (wheel: A-2-3-4-5)
793
771
  */
794
- isStraightWheel() {
772
+ #isStraightWheel() {
795
773
  if (this.evaluation.isStraightWheel !== undefined) return this.evaluation.isStraightWheel;
796
- if (this.evaluation.isPair === undefined) this.isPair();
797
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
798
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
799
- if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
800
- if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
774
+ if (this.evaluation.isPair === undefined) this.#isPair();
775
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
776
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
777
+ if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
778
+ if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
801
779
  if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind) return this.evaluation.isStraightWheel = false;
802
- this.sortWheel();
803
- const distance = this.getDistanceWheel();
780
+ this.#sortWheel();
781
+ const distance = this.#getDistanceWheel();
804
782
  return this.evaluation.isStraightWheel = distance === 4;
805
783
  }
806
784
 
@@ -813,10 +791,10 @@ class HandEvaluator {
813
791
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
814
792
  * evaluator.isRoyalFlush(); // Returns true
815
793
  */
816
- isRoyalFlush() {
794
+ #isRoyalFlush() {
817
795
  if (this.evaluation.isRoyalFlush !== undefined) return this.evaluation.isRoyalFlush;
818
- if (this.evaluation.isFlush === undefined) this.isFlush();
819
- if (this.evaluation.isStraight === undefined) this.isStraight();
796
+ if (this.evaluation.isFlush === undefined) this.#isFlush();
797
+ if (this.evaluation.isStraight === undefined) this.#isStraight();
820
798
  if (!this.evaluation.isFlush || !this.evaluation.isStraight) return this.evaluation.isRoyalFlush = false;
821
799
  const ranks = new Set(this.cards.map(c => c.slice(0, -1)));
822
800
  return this.evaluation.isRoyalFlush = ranks.has('A') && ranks.has('K') && ranks.has('Q') && ranks.has('J') && ranks.has('T');
@@ -831,11 +809,11 @@ class HandEvaluator {
831
809
  * const evaluator = new HandEvaluator(['9s', '8s', '7s', '6s', '5s']);
832
810
  * evaluator.isStraightFlush(); // Returns true
833
811
  */
834
- isStraightFlush() {
812
+ #isStraightFlush() {
835
813
  if (this.evaluation.isStraightFlush !== undefined) return this.evaluation.isStraightFlush;
836
- if (this.evaluation.isFlush === undefined) this.isFlush();
837
- if (this.evaluation.isStraight === undefined) this.isStraight();
838
- if (this.evaluation.isRoyalFlush === undefined) this.isRoyalFlush();
814
+ if (this.evaluation.isFlush === undefined) this.#isFlush();
815
+ if (this.evaluation.isStraight === undefined) this.#isStraight();
816
+ if (this.evaluation.isRoyalFlush === undefined) this.#isRoyalFlush();
839
817
  return this.evaluation.isStraightFlush = this.evaluation.isFlush && this.evaluation.isStraight && !this.evaluation.isRoyalFlush;
840
818
  }
841
819
 
@@ -851,24 +829,24 @@ class HandEvaluator {
851
829
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9h']);
852
830
  * evaluator.isOpenEndedStraightDraw(); // Returns true (needs T or 9 to complete straight)
853
831
  */
854
- isOpenEndedStraightDraw() {
832
+ #isOpenEndedStraightDraw() {
855
833
  if (this.evaluation.isOpenEndedStraightDraw !== undefined) return this.evaluation.isOpenEndedStraightDraw;
856
- if (this.evaluation.isPair === undefined) this.isPair();
857
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
858
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
859
- if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
860
- if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
861
- if (this.evaluation.isStraight === undefined) this.isStraight();
834
+ if (this.evaluation.isPair === undefined) this.#isPair();
835
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
836
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
837
+ if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
838
+ if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
839
+ if (this.evaluation.isStraight === undefined) this.#isStraight();
862
840
  if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraight) {
863
841
  // Check wheel equivalent before returning false
864
- if (this.isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
842
+ if (this.#isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
865
843
  return this.evaluation.isOpenEndedStraightDraw = false;
866
844
  }
867
- this.sort();
868
- const result = this.getDistance(this.cards.slice(0,4)) === 3 || this.getDistance(this.cards.slice(1,5)) === 3;
845
+ this.#sort();
846
+ const result = this.#getDistance(this.cards.slice(0,4)) === 3 || this.#getDistance(this.cards.slice(1,5)) === 3;
869
847
  if (result) return this.evaluation.isOpenEndedStraightDraw = true;
870
848
  // Check wheel equivalent
871
- if (this.isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
849
+ if (this.#isOpenEndedStraightDrawWheel()) return this.evaluation.isOpenEndedStraightDraw = true;
872
850
  return this.evaluation.isOpenEndedStraightDraw = false;
873
851
  }
874
852
 
@@ -879,17 +857,17 @@ class HandEvaluator {
879
857
  *
880
858
  * @returns {boolean} True if the hand is a wheel-equivalent open-ended straight draw, false otherwise
881
859
  */
882
- isOpenEndedStraightDrawWheel() {
860
+ #isOpenEndedStraightDrawWheel() {
883
861
  if (this.evaluation.isOpenEndedStraightDrawWheel !== undefined) return this.evaluation.isOpenEndedStraightDrawWheel;
884
- if (this.evaluation.isPair === undefined) this.isPair();
885
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
886
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
887
- if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
888
- if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
889
- if (this.evaluation.isStraightWheel === undefined) this.isStraightWheel();
862
+ if (this.evaluation.isPair === undefined) this.#isPair();
863
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
864
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
865
+ if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
866
+ if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
867
+ if (this.evaluation.isStraightWheel === undefined) this.#isStraightWheel();
890
868
  if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraightWheel) return this.evaluation.isOpenEndedStraightDrawWheel = false;
891
- this.sortWheel();
892
- return this.evaluation.isOpenEndedStraightDrawWheel = this.getDistanceWheel(this.cards.slice(0,4)) === 3 || this.getDistanceWheel(this.cards.slice(1,5)) === 3;
869
+ this.#sortWheel();
870
+ return this.evaluation.isOpenEndedStraightDrawWheel = this.#getDistanceWheel(this.cards.slice(0,4)) === 3 || this.#getDistanceWheel(this.cards.slice(1,5)) === 3;
893
871
  }
894
872
 
895
873
  /**
@@ -902,25 +880,25 @@ class HandEvaluator {
902
880
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9h']);
903
881
  * evaluator.isInsideStraightDraw(); // Returns true (needs T in the middle)
904
882
  */
905
- isInsideStraightDraw() {
883
+ #isInsideStraightDraw() {
906
884
  if (this.evaluation.isInsideStraightDraw !== undefined) return this.evaluation.isInsideStraightDraw;
907
- if (this.evaluation.isPair === undefined) this.isPair();
908
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
909
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
910
- if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
911
- if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
912
- if (this.evaluation.isStraight === undefined) this.isStraight();
885
+ if (this.evaluation.isPair === undefined) this.#isPair();
886
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
887
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
888
+ if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
889
+ if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
890
+ if (this.evaluation.isStraight === undefined) this.#isStraight();
913
891
  if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraight) {
914
892
  // Check wheel equivalent before returning false
915
- if (this.isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
893
+ if (this.#isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
916
894
  return this.evaluation.isInsideStraightDraw = false;
917
895
  }
918
896
 
919
- this.sort();
920
- const result = this.getDistance(this.cards.slice(0,4)) === 4 || this.getDistance(this.cards.slice(1,5)) === 4;
897
+ this.#sort();
898
+ const result = this.#getDistance(this.cards.slice(0,4)) === 4 || this.#getDistance(this.cards.slice(1,5)) === 4;
921
899
  if (result) return this.evaluation.isInsideStraightDraw = true;
922
900
  // Check wheel equivalent
923
- if (this.isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
901
+ if (this.#isInsideStraightDrawWheel()) return this.evaluation.isInsideStraightDraw = true;
924
902
  return this.evaluation.isInsideStraightDraw = false;
925
903
  }
926
904
 
@@ -931,18 +909,18 @@ class HandEvaluator {
931
909
  *
932
910
  * @returns {boolean} True if the hand is a wheel-equivalent inside straight draw, false otherwise
933
911
  */
934
- isInsideStraightDrawWheel() {
912
+ #isInsideStraightDrawWheel() {
935
913
  if (this.evaluation.isInsideStraightDrawWheel !== undefined) return this.evaluation.isInsideStraightDrawWheel;
936
- if (this.evaluation.isPair === undefined) this.isPair();
937
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
938
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
939
- if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
940
- if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
941
- if (this.evaluation.isStraightWheel === undefined) this.isStraightWheel();
914
+ if (this.evaluation.isPair === undefined) this.#isPair();
915
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
916
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
917
+ if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
918
+ if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
919
+ if (this.evaluation.isStraightWheel === undefined) this.#isStraightWheel();
942
920
  if (this.evaluation.isPair || this.evaluation.isTwoPair || this.evaluation.isThreeOfAKind || this.evaluation.isFullHouse || this.evaluation.isFourOfAKind || this.evaluation.isStraightWheel) return this.evaluation.isInsideStraightDrawWheel = false;
943
921
 
944
- this.sortWheel();
945
- return this.evaluation.isInsideStraightDrawWheel = this.getDistanceWheel(this.cards.slice(0,4)) === 4 || this.getDistanceWheel(this.cards.slice(1,5)) === 4;
922
+ this.#sortWheel();
923
+ return this.evaluation.isInsideStraightDrawWheel = this.#getDistanceWheel(this.cards.slice(0,4)) === 4 || this.#getDistanceWheel(this.cards.slice(1,5)) === 4;
946
924
  }
947
925
 
948
926
  /**
@@ -958,13 +936,181 @@ class HandEvaluator {
958
936
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '8h']);
959
937
  * evaluator.isStraightDraw(); // Returns true (inside straight draw)
960
938
  */
961
- isStraightDraw() {
939
+ #isStraightDraw() {
962
940
  if (this.evaluation.isStraightDraw !== undefined) return this.evaluation.isStraightDraw;
963
- if (this.evaluation.isOpenEndedStraightDraw === undefined) this.isOpenEndedStraightDraw();
964
- if (this.evaluation.isInsideStraightDraw === undefined) this.isInsideStraightDraw();
941
+ if (this.evaluation.isOpenEndedStraightDraw === undefined) this.#isOpenEndedStraightDraw();
942
+ if (this.evaluation.isInsideStraightDraw === undefined) this.#isInsideStraightDraw();
965
943
  return this.evaluation.isStraightDraw = this.evaluation.isOpenEndedStraightDraw || this.evaluation.isInsideStraightDraw;
966
944
  }
967
945
 
946
+ // ==================== KEY CARDS AND KICKER CARDS ====================
947
+
948
+ /**
949
+ * Helper method to extract the 5 cards that form a straight.
950
+ * Handles both standard straights and wheel straights.
951
+ *
952
+ * @private
953
+ * @returns {string[]} Array of 5 cards forming the straight, sorted from highest to lowest
954
+ */
955
+ #getStraightCards() {
956
+ // Try standard straight first
957
+ this.#sort();
958
+ const gaps = this.#getGaps();
959
+ if (gaps.every(g => g === 1)) {
960
+ return [...this.cards].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
961
+ }
962
+
963
+ // Try wheel straight
964
+ this.#sortWheel();
965
+ const gapsWheel = this.#getGapsWheel();
966
+ if (gapsWheel.every(g => g === 1)) {
967
+ // Convert back to standard sorting for return
968
+ return [...this.cards].sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
969
+ }
970
+
971
+ return [];
972
+ }
973
+
974
+ /**
975
+ * Helper method to extract the 4 cards that form a straight draw.
976
+ * Handles both standard and wheel straight draws.
977
+ *
978
+ * @private
979
+ * @returns {string[]} Array of 4 cards forming the draw, sorted from highest to lowest
980
+ */
981
+ #getStraightDrawCards() {
982
+ // Try standard straight draw
983
+ this.#sort();
984
+ const firstFour = this.cards.slice(0, 4);
985
+ const lastFour = this.cards.slice(1, 5);
986
+
987
+ const firstFourDist = this.#getDistance(firstFour);
988
+ if (firstFourDist === 3 || firstFourDist === 4) {
989
+ return [...firstFour].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
990
+ }
991
+ const lastFourDist = this.#getDistance(lastFour);
992
+ if (lastFourDist === 3 || lastFourDist === 4) {
993
+ return [...lastFour].sort((a, b) => this.#getRankValue(b) - this.#getRankValue(a));
994
+ }
995
+
996
+ // Try wheel straight draw
997
+ this.#sortWheel();
998
+ const firstFourWheel = this.cards.slice(0, 4);
999
+ const lastFourWheel = this.cards.slice(1, 5);
1000
+
1001
+ const firstFourDistWheel = this.#getDistanceWheel(firstFourWheel);
1002
+ if (firstFourDistWheel === 3 || firstFourDistWheel === 4) {
1003
+ return [...firstFourWheel].sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
1004
+ }
1005
+ const lastFourDistWheel = this.#getDistanceWheel(lastFourWheel);
1006
+ if (lastFourDistWheel === 3 || lastFourDistWheel === 4) {
1007
+ return [...lastFourWheel].sort((a, b) => this.#getRankValueWheel(b) - this.#getRankValueWheel(a));
1008
+ }
1009
+
1010
+ return [];
1011
+ }
1012
+
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
+
968
1114
  // ==================== HIGH CARD ====================
969
1115
 
970
1116
  /**
@@ -976,15 +1122,15 @@ class HandEvaluator {
976
1122
  * const evaluator = new HandEvaluator(['As', 'Kh', 'Qd', 'Jc', '9s']);
977
1123
  * evaluator.isHighCard(); // Returns true (no made hand)
978
1124
  */
979
- isHighCard() {
1125
+ #isHighCard() {
980
1126
  if (this.evaluation.isHighCard !== undefined) return this.evaluation.isHighCard;
981
- if (this.evaluation.isPair === undefined) this.isPair();
982
- if (this.evaluation.isTwoPair === undefined) this.isTwoPair();
983
- if (this.evaluation.isThreeOfAKind === undefined) this.isThreeOfAKind();
984
- if (this.evaluation.isFullHouse === undefined) this.isFullHouse();
985
- if (this.evaluation.isFourOfAKind === undefined) this.isFourOfAKind();
986
- if (this.evaluation.isFlush === undefined) this.isFlush();
987
- if (this.evaluation.isStraight === undefined) this.isStraight();
1127
+ if (this.evaluation.isPair === undefined) this.#isPair();
1128
+ if (this.evaluation.isTwoPair === undefined) this.#isTwoPair();
1129
+ if (this.evaluation.isThreeOfAKind === undefined) this.#isThreeOfAKind();
1130
+ if (this.evaluation.isFullHouse === undefined) this.#isFullHouse();
1131
+ if (this.evaluation.isFourOfAKind === undefined) this.#isFourOfAKind();
1132
+ if (this.evaluation.isFlush === undefined) this.#isFlush();
1133
+ if (this.evaluation.isStraight === undefined) this.#isStraight();
988
1134
  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;
989
1135
  }
990
1136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-extval",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [