poker-extval 3.3.2 → 3.3.3

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 +3 -3
  2. package/evaluator.js +34 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -201,14 +201,14 @@ The `evaluateHand()` function returns an object where:
201
201
  - `isTwoUndercards` - Both hole cards rank below every board card
202
202
 
203
203
  #### Other Categorization
204
- - `isUnderPair` - Pocket pair ranked below every other card in the hand (requires 6+ cards)
204
+ - `isUnderPair` - Pocket pair ranked below every board card. With `holeCards` this works on any street, including the flop; without them the board is unknown, so it requires 6+ cards and means a pair ranked below every other card
205
205
  - `isComboDraw` - Both a flush draw (not backdoor) and a straight draw
206
206
 
207
207
  #### Draws
208
208
  - `isFlushDraw` - Four cards of the same suit (one away from flush)
209
209
  - `isBackdoorFlushDraw` - Three cards of the same suit (two away from flush)
210
- - `isOpenEndedStraightDraw` - One card away from straight on either end
211
- - `isInsideStraightDraw` - One card away from straight (needed card in middle)
210
+ - `isOpenEndedStraightDraw` - Four in a row that a card at either end completes (e.g. 9-8-7-6). A-K-Q-J and A-2-3-4 complete at one end only, so they count as inside draws
211
+ - `isInsideStraightDraw` - One card away from a straight that only one rank completes (a gap in the middle, or A-K-Q-J / A-2-3-4)
212
212
  - `isStraightDraw` - Any type of straight draw (open-ended or inside, standard or wheel)
213
213
 
214
214
  **Note**: When `holeCards` are supplied, every draw (including `isComboDraw` and the wheel draws) must use at least one hole card. A draw made entirely of board cards is shared by every player, so it is not reported: 2-2 on a 9-8-7-6 board is not an open-ended straight draw, and A♦-Q♦ on a K♠-7♠-2♠ flop is not a backdoor flush draw. Without `holeCards` the evaluator cannot tell the board from the hand, so draws are detected from all cards.
package/evaluator.js CHANGED
@@ -65,7 +65,8 @@ class HandEvaluator {
65
65
  */
66
66
  constructor(cards, strict = true, holeCards = null, originalCards = null) {
67
67
  this.#validateCards(cards);
68
- this.cards = cards;
68
+ // Copy so that in-place sorting during evaluation never reorders the caller's array
69
+ this.cards = [...cards];
69
70
  this.strict = strict;
70
71
  this.originalCards = originalCards || (cards.length > 5 ? cards : null);
71
72
  if (holeCards === null || holeCards === undefined) {
@@ -370,11 +371,13 @@ class HandEvaluator {
370
371
  }
371
372
 
372
373
  /**
373
- * Gets sorted key card ranks for an evaluation.
374
+ * Gets key card ranks for an evaluation, in tiebreak order.
375
+ * Ranks are grouped by how often they appear (so a full house compares its three of a kind
376
+ * before its pair), then sorted descending. In a wheel straight (A-2-3-4-5) the Ace counts low.
374
377
  *
375
378
  * @private
376
379
  * @param {Object} evaluation - The evaluation object
377
- * @returns {number[]} Array of rank values sorted descending
380
+ * @returns {number[]} Array of rank values in tiebreak order
378
381
  */
379
382
  #getKeyCardRanks(evaluation) {
380
383
  const handStrength = {
@@ -384,8 +387,11 @@ class HandEvaluator {
384
387
  };
385
388
  for (const [hand] of Object.entries(handStrength)) {
386
389
  if (evaluation[hand]) {
387
- const keyCards = evaluation.keyCards[hand] || [];
388
- return keyCards.map(c => this.#getRankValue(c)).sort((a, b) => b - a);
390
+ const ranks = (evaluation.keyCards[hand] || []).map(c => this.#getRankValue(c));
391
+ const isWheel = (hand === 'isStraight' || hand === 'isStraightFlush') && ranks.includes(13) && ranks.includes(4);
392
+ const values = isWheel ? ranks.map(r => r === 13 ? 0 : r) : ranks;
393
+ const counts = values.reduce((acc, r) => (acc[r] = (acc[r] || 0) + 1, acc), {});
394
+ return values.sort((a, b) => counts[b] - counts[a] || b - a);
389
395
  }
390
396
  }
391
397
  return [];
@@ -457,7 +463,7 @@ class HandEvaluator {
457
463
  for (const [key, evaluation] of Object.entries(result)) {
458
464
  const strength = this.#getHandStrength(evaluation);
459
465
  const bestStrength = bestEval ? this.#getHandStrength(bestEval) : 0;
460
- if (!bestEval || strength > bestStrength || (strength === bestStrength && this.#compareEvaluations(evaluation, bestEval) > 0)) {
466
+ if (!bestEval || strength > bestStrength || (strength === bestStrength && this.#compareEvaluations(evaluation, bestEval) < 0)) {
461
467
  bestKey = key;
462
468
  bestEval = evaluation;
463
469
  }
@@ -1088,8 +1094,8 @@ class HandEvaluator {
1088
1094
  *
1089
1095
  * @returns {boolean} True if the hand is an open-ended straight draw, false otherwise
1090
1096
  * @example
1091
- * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', '9h']);
1092
- * evaluator.isOpenEndedStraightDraw(); // Returns true (needs T or 9 to complete straight)
1097
+ * const evaluator = new HandEvaluator(['Ks', 'Qs', 'Js', 'Ts', '4h']);
1098
+ * evaluator.isOpenEndedStraightDraw(); // Returns true (needs A or 9 to complete straight)
1093
1099
  */
1094
1100
  #isOpenEndedStraightDraw() {
1095
1101
  if (this.evaluation.isOpenEndedStraightDraw !== undefined) return this.evaluation.isOpenEndedStraightDraw;
@@ -1113,7 +1119,7 @@ class HandEvaluator {
1113
1119
 
1114
1120
  /**
1115
1121
  * Determines if the hand is an open-ended straight draw using wheel ranking.
1116
- * Checks for wheel-equivalent open-ended draws (e.g., A-2-3-4 needs 5 or 2).
1122
+ * Checks for wheel-equivalent open-ended draws. A-2-3-4 needs only a 5, so it counts as an inside draw.
1117
1123
  * Paired cards count once. Returns false if the hand is already a wheel straight.
1118
1124
  *
1119
1125
  * @returns {boolean} True if the hand is a wheel-equivalent open-ended straight draw, false otherwise
@@ -1700,7 +1706,9 @@ class HandEvaluator {
1700
1706
  /**
1701
1707
  * Finds 4 cards of distinct, rank-adjacent ranks whose rank span is one of the given distances.
1702
1708
  * Paired cards count once (a hole card is preferred as the representative of its rank), so a
1703
- * paired hand such as 8-8-6-5-4 still finds its 8-6-5-4 draw. When hole cards are supplied, a
1709
+ * paired hand such as 8-8-6-5-4 still finds its 8-6-5-4 draw. One-ended draws (A-K-Q-J and
1710
+ * A-2-3-4) count as inside draws, since only one rank completes them. Wheel windows must
1711
+ * contain the Ace. When hole cards are supplied, a
1704
1712
  * window whose ranks are all already on the board is skipped, since a draw the board gives
1705
1713
  * every player is not the player's draw.
1706
1714
  *
@@ -1723,7 +1731,12 @@ class HandEvaluator {
1723
1731
  }
1724
1732
  for (let i = 0; i + 4 <= distinct.length; i++) {
1725
1733
  const window = distinct.slice(i, i + 4);
1726
- const distance = rankValue(window[0]) - rankValue(window[3]);
1734
+ // Wheel ranking only matters when the Ace plays low; other windows are standard draws
1735
+ if (wheel && !window.some(c => c.startsWith('A'))) continue;
1736
+ let distance = rankValue(window[0]) - rankValue(window[3]);
1737
+ // Four in a row capped by the Ace (A-K-Q-J or A-2-3-4) completes at one end only,
1738
+ // so it has the same 4 outs as an inside draw
1739
+ if (distance === 3 && window.some(c => c.startsWith('A'))) distance = 4;
1727
1740
  if (distances.includes(distance) && this.#addsRankToBoard(window)) return window;
1728
1741
  }
1729
1742
  return null;
@@ -1795,7 +1808,9 @@ class HandEvaluator {
1795
1808
 
1796
1809
  /**
1797
1810
  * Determines if the hand contains an under pair (pocket pair smaller than all board cards).
1798
- * Only applicable when evaluating 6+ cards where the pair rank is smaller than all other cards.
1811
+ * With hole cards supplied, the hole cards must be a pocket pair ranked below every board card;
1812
+ * this works on any street, including a 5-card flop. Without hole cards the board is unknown, so
1813
+ * it applies only to 6+ cards where the pair rank is smaller than all other cards.
1799
1814
  *
1800
1815
  * @returns {boolean} True if the hand is an under pair, false otherwise
1801
1816
  * @example
@@ -1806,6 +1821,13 @@ class HandEvaluator {
1806
1821
  #isUnderPair() {
1807
1822
  if (this.evaluation.isUnderPair !== undefined) return this.evaluation.isUnderPair;
1808
1823
  if (this.evaluation.isPair === undefined) this.#isPair();
1824
+ if (this.evaluation.isPair && this.holeCards) {
1825
+ const [first, second] = this.holeCards;
1826
+ const holeRank = first.slice(0, -1);
1827
+ if (second.slice(0, -1) !== holeRank || this.#getPairRank() !== holeRank) return this.evaluation.isUnderPair = false;
1828
+ const board = (this.originalCards || this.cards).filter(c => !this.holeCards.includes(c));
1829
+ return this.evaluation.isUnderPair = board.every(c => this.#getRankValue(c) > this.#getRankValue(first));
1830
+ }
1809
1831
  if (!this.evaluation.isPair || !this.originalCards || this.originalCards.length < 6) {
1810
1832
  return this.evaluation.isUnderPair = false;
1811
1833
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-extval",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [