poker-extval 3.3.4 → 3.4.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 +27 -2
  2. package/evaluator.js +126 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -451,7 +451,7 @@ The test suite includes 700+ tests covering:
451
451
 
452
452
  ## API Reference
453
453
 
454
- ### `evaluateHand(cards, strict, holeCards)`
454
+ ### `evaluateHand(cards, strict, holeCards, villainRange)`
455
455
 
456
456
  Main function to evaluate poker hands.
457
457
 
@@ -459,6 +459,7 @@ Main function to evaluate poker hands.
459
459
  - `cards` (string[]): Array of 5 or more card strings
460
460
  - `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`).
461
461
  - `holeCards` (string[] | null, optional): Exactly 2 cards identifying the player's hole cards, both of which must appear in `cards`. Required for the overcard categorizations; without it those properties are always `false` (default: `null`).
462
+ - `villainRange` (string[] | null, optional): Villain hands as exact combos (`'AhKd'`) or shorthand (`'QQ'`, `'AKs'`, `'AKo'`, `'AK'`, `'TT+'`, `'ATs+'`, `'ATo+'`, `'AT+'`). When supplied, each evaluation gets a `villainRange` property; see [Villain Range Blockers](#villain-range-blockers) (default: `null`).
462
463
 
463
464
  **Returns:**
464
465
  - Object mapping sorted hand strings to evaluation objects
@@ -471,7 +472,7 @@ Main function to evaluate poker hands.
471
472
  #### Constructor
472
473
 
473
474
  ```javascript
474
- new HandEvaluator(cards, strict, holeCards)
475
+ new HandEvaluator(cards, strict, holeCards, villainRange)
475
476
  ```
476
477
 
477
478
  Creates a new HandEvaluator instance.
@@ -480,6 +481,7 @@ Creates a new HandEvaluator instance.
480
481
  - `cards` (string[]): Array of 5 or more card strings
481
482
  - `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`).
482
483
  - `holeCards` (string[] | null, optional): Exactly 2 cards identifying the player's hole cards, both of which must appear in `cards`. Required for the overcard categorizations (default: `null`).
484
+ - `villainRange` (string[] | null, optional): Villain hands as exact combos (`'AhKd'`) or shorthand (`'QQ'`, `'AKs'`, `'AKo'`, `'AK'`, `'TT+'`, `'ATs+'`, `'ATo+'`, `'AT+'`). When supplied, each evaluation gets a `villainRange` property; see [Villain Range Blockers](#villain-range-blockers) (default: `null`).
483
485
 
484
486
  **Properties:**
485
487
  - `evaluation` - Object mapping sorted hand strings to evaluation objects (same format as `evaluateHand()` return value)
@@ -487,6 +489,29 @@ Creates a new HandEvaluator instance.
487
489
  - `strict` - Boolean indicating whether strict mode is enabled
488
490
  - `holeCards` - Array of the 2 hole cards, or `null` if none were supplied
489
491
 
492
+ ### Villain Range Blockers
493
+
494
+ Pass a villain range as the fourth argument to see which of its combos the known cards rule out:
495
+
496
+ ```javascript
497
+ const board = ['Kh', '8d', '4c'];
498
+ const holeCards = ['Ah', 'Js'];
499
+ const result = evaluateHand([...board, ...holeCards], true, holeCards, ['AKs', 'QQ', 'JsTs']);
500
+ const { villainRange } = Object.values(result)[0];
501
+
502
+ villainRange.allBlocks;
503
+ // { unblocked: ['AsKs', 'AdKd', 'AcKc', 'QsQh', 'QsQd', 'QsQc', 'QhQd', 'QhQc', 'QdQc'],
504
+ // blocked: ['AhKh', 'JsTs'] }
505
+ ```
506
+
507
+ The result has three views, each `{ unblocked: [], blocked: [] }` of exact combos:
508
+
509
+ - `boardBlocks` - blocked by a board card (every card in `cards` that is not a hole card)
510
+ - `heroBlocks` - blocked by one of the hole cards
511
+ - `allBlocks` - blocked by any known card; `unblocked` here is what villain can actually hold
512
+
513
+ Shorthand is expanded to combos, so a partly blocked entry such as `AKs` is split across both lists. Combos are written high rank first (`'AhKd'`, and `'QsQh'` for pairs with suits ordered s, h, d, c), duplicates are removed, and input order is kept. Without `holeCards`, every card is treated as board, so `heroBlocks` blocks nothing. In non-strict mode every combination carries the same `villainRange`. The property is omitted when no range is supplied, and an invalid entry throws.
514
+
490
515
  **Note**: All internal methods are private. Use the `evaluation` property to access evaluation results.
491
516
 
492
517
  ## Releasing
package/evaluator.js CHANGED
@@ -73,9 +73,14 @@ class HandEvaluator {
73
73
  * @param {string[]|null} [holeCards=null] - Optional array of exactly 2 cards identifying the player's
74
74
  * hole cards. Required for the overcard categorizations (isOvercard, isTwoOvercards, etc.); without it
75
75
  * those properties are always false. Both cards must be present in `cards`.
76
+ * @param {string[]|null} [villainRange=null] - Optional array of villain hands, as exact combos ('AhKd') or
77
+ * shorthand ('QQ', 'AKs', 'AKo', 'AK', 'TT+', 'ATs+', 'ATo+', 'AT+'). When supplied, every evaluation
78
+ * gets a `villainRange` property listing which combos the known cards block (see #classifyVillainRange).
79
+ * @param {string[]|null} [originalCards=null] - Internal: the full hand when evaluating one 5-card combination
76
80
  * @throws {Error} If cards is not an array, has fewer than 5 cards, contains invalid format,
77
81
  * invalid rank, invalid suit, or duplicate cards
78
82
  * @throws {Error} If holeCards is provided but is not an array of exactly 2 distinct cards drawn from `cards`
83
+ * @throws {Error} If villainRange is provided but is not an array of valid combos or shorthand
79
84
  * @example
80
85
  * const evaluator = new HandEvaluator(['As', 'Ks', 'Qs', 'Js', 'Ts']);
81
86
  * @example
@@ -88,7 +93,7 @@ class HandEvaluator {
88
93
  * // With hole cards - enables overcard categorization
89
94
  * const withHoleCards = new HandEvaluator(['Ks', 'Qh', '8d', '3c', 'Ad', 'Jh'], true, ['Ad', 'Jh']);
90
95
  */
91
- constructor(cards, strict = true, holeCards = null, originalCards = null) {
96
+ constructor(cards, strict = true, holeCards = null, villainRange = null, originalCards = null) {
92
97
  this.#validateCards(cards);
93
98
  // Copy so that in-place sorting during evaluation never reorders the caller's array
94
99
  this.cards = [...cards];
@@ -100,7 +105,117 @@ class HandEvaluator {
100
105
  this.#validateHoleCards(holeCards, this.originalCards || cards);
101
106
  this.holeCards = holeCards;
102
107
  }
108
+ const villainCombos = villainRange === null || villainRange === undefined ? null : this.#parseVillainRange(villainRange);
103
109
  this.evaluation = this.#evaluateAllCombinations();
110
+ if (villainCombos) {
111
+ const classification = this.#classifyVillainRange(villainCombos);
112
+ for (const evaluation of Object.values(this.evaluation)) evaluation.villainRange = classification;
113
+ }
114
+ }
115
+
116
+ // ==================== VILLAIN RANGE ====================
117
+
118
+ /**
119
+ * Expands a villain range into exact two-card combos, in input order and without duplicates.
120
+ * Each combo is written high rank first, with suits ordered s, h, d, c for pairs (e.g. 'AhKd', 'QsQh').
121
+ *
122
+ * Accepted entries:
123
+ * - Exact combo: 'AhKd' (either card order)
124
+ * - Pair: 'QQ' (6 combos); 'TT+' adds every higher pair
125
+ * - Suited: 'AKs' (4 combos); offsuit: 'AKo' (12 combos); either: 'AK' (16 combos)
126
+ * - Plus forms keep the first rank and raise the second up to one below it: 'ATs+' is ATs, AJs, AQs, AKs
127
+ *
128
+ * @private
129
+ * @param {string[]} villainRange - Range entries to expand
130
+ * @returns {string[]} Exact combos
131
+ * @throws {Error} If villainRange is not an array or an entry is not a valid combo or shorthand
132
+ */
133
+ #parseVillainRange(villainRange) {
134
+ if (!Array.isArray(villainRange)) {
135
+ throw new Error('Villain range must be an array');
136
+ }
137
+ const ranks = 'AKQJT98765432';
138
+ const suits = 'shdc';
139
+ const combos = [];
140
+ const seen = new Set();
141
+ const add = (first, second) => {
142
+ // High rank first; for equal ranks, order suits s, h, d, c
143
+ const order = c => ranks.indexOf(c[0]) * 4 + suits.indexOf(c[1]);
144
+ const [a, b] = order(first) <= order(second) ? [first, second] : [second, first];
145
+ if (!seen.has(a + b)) {
146
+ seen.add(a + b);
147
+ combos.push(a + b);
148
+ }
149
+ };
150
+ const addRanks = (high, low, type) => {
151
+ for (const s1 of suits) {
152
+ for (const s2 of suits) {
153
+ if (high === low ? suits.indexOf(s2) <= suits.indexOf(s1) : (type === 's' && s1 !== s2) || (type === 'o' && s1 === s2)) continue;
154
+ add(high + s1, low + s2);
155
+ }
156
+ }
157
+ };
158
+
159
+ villainRange.forEach((entry, i) => {
160
+ const invalid = () => new Error(`Invalid villain range entry at index ${i}: "${entry}"`);
161
+ if (typeof entry !== 'string') throw invalid();
162
+
163
+ const exact = entry.match(/^([AKQJT2-9])([shdc])([AKQJT2-9])([shdc])$/);
164
+ if (exact) {
165
+ const [first, second] = [exact[1] + exact[2], exact[3] + exact[4]];
166
+ if (first === second) throw invalid();
167
+ add(first, second);
168
+ return;
169
+ }
170
+
171
+ const shorthand = entry.match(/^([AKQJT2-9])([AKQJT2-9])([so]?)(\+?)$/);
172
+ if (!shorthand) throw invalid();
173
+ const [, r1, r2, type, plus] = shorthand;
174
+ const [high, low] = ranks.indexOf(r1) <= ranks.indexOf(r2) ? [r1, r2] : [r2, r1];
175
+ if (high === low) {
176
+ if (type) throw invalid();
177
+ const lows = plus ? ranks.slice(0, ranks.indexOf(high) + 1) : high;
178
+ for (const rank of lows) addRanks(rank, rank);
179
+ return;
180
+ }
181
+ const lows = plus ? ranks.slice(ranks.indexOf(high) + 1, ranks.indexOf(low) + 1) : low;
182
+ for (const rank of lows) addRanks(high, rank, type);
183
+ });
184
+ return combos;
185
+ }
186
+
187
+ /**
188
+ * Splits villain combos by which known cards block them. A combo is blocked when it shares a
189
+ * card with the blockers, so villain cannot hold it.
190
+ *
191
+ * - boardBlocks: blocked by the board (every card that is not a hole card)
192
+ * - heroBlocks: blocked by the hole cards (always unblocked without hole cards)
193
+ * - allBlocks: blocked by any known card
194
+ *
195
+ * Without hole cards every card passed is treated as board.
196
+ *
197
+ * @private
198
+ * @param {string[]} combos - Exact combos from #parseVillainRange
199
+ * @returns {{boardBlocks: Object, heroBlocks: Object, allBlocks: Object}} Each with `unblocked` and `blocked` arrays
200
+ */
201
+ #classifyVillainRange(combos) {
202
+ const fullHand = this.originalCards || this.cards;
203
+ const heroCards = new Set(this.holeCards || []);
204
+ const boardCards = new Set(fullHand.filter(c => !heroCards.has(c)));
205
+ const allCards = new Set(fullHand);
206
+ const split = blockers => {
207
+ const result = { unblocked: [], blocked: [] };
208
+ for (const combo of combos) {
209
+ const isBlocked = blockers.has(combo.slice(0, 2)) || blockers.has(combo.slice(2));
210
+ result[isBlocked ? 'blocked' : 'unblocked'].push(combo);
211
+ }
212
+ return result;
213
+ };
214
+ return {
215
+ boardBlocks: split(boardCards),
216
+ heroBlocks: split(heroCards),
217
+ allBlocks: split(allCards)
218
+ };
104
219
  }
105
220
 
106
221
  /**
@@ -296,6 +411,9 @@ class HandEvaluator {
296
411
  this.#isOpenEndedStraightDraw();
297
412
  this.#isInsideStraightDraw();
298
413
  this.#isStraightDraw();
414
+ // Always set the wheel flags; the standard draw checks above only evaluate them as a fallback
415
+ this.#isOpenEndedStraightDrawWheel();
416
+ this.#isInsideStraightDrawWheel();
299
417
  this.#isComboDraw();
300
418
  this.#clearDrawsForStreet();
301
419
  this.#isUnderPair();
@@ -371,9 +489,7 @@ class HandEvaluator {
371
489
 
372
490
  // Filter draws: made flush or stronger cannot have straight draws
373
491
  if (strongest >= 6) {
374
- evaluation.isStraightDraw = false;
375
- evaluation.isOpenEndedStraightDraw = false;
376
- evaluation.isInsideStraightDraw = false;
492
+ for (const draw of STRAIGHT_DRAW_PROPERTIES) evaluation[draw] = false;
377
493
  }
378
494
  }
379
495
 
@@ -522,7 +638,7 @@ class HandEvaluator {
522
638
  const result = {};
523
639
  for (const combination of combinations) {
524
640
  const key = this.#formatHandKey(combination);
525
- const tempEvaluator = new HandEvaluator(combination, this.strict, this.holeCards, this.originalCards);
641
+ const tempEvaluator = new HandEvaluator(combination, this.strict, this.holeCards, null, this.originalCards);
526
642
  const evaluationKey = Object.keys(tempEvaluator.evaluation)[0];
527
643
  result[key] = tempEvaluator.evaluation[evaluationKey];
528
644
  }
@@ -2111,10 +2227,13 @@ class HandEvaluator {
2111
2227
  * Required for the overcard categorizations (isOvercard, isTwoOvercards, isOvercardAndMiddleCard,
2112
2228
  * isOvercardAndUndercard, isTwoMiddleCards, isMiddleAndUndercard, isTwoUndercards); without it those
2113
2229
  * properties are always false. Both cards must be present in `cards`.
2230
+ * @param {string[]|null} [villainRange=null] - Optional villain hands as exact combos ('AhKd') or shorthand
2231
+ * ('QQ', 'AKs', 'AKo', 'AK', 'TT+', 'ATs+'). When supplied, each evaluation includes
2232
+ * `villainRange: { boardBlocks, heroBlocks, allBlocks }`, each `{ unblocked: [], blocked: [] }` of exact combos.
2114
2233
  * @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
2115
2234
  */
2116
- function evaluateHand(cards, strict = true, holeCards = null) {
2117
- const evaluator = new HandEvaluator(cards, strict, holeCards);
2235
+ function evaluateHand(cards, strict = true, holeCards = null, villainRange = null) {
2236
+ const evaluator = new HandEvaluator(cards, strict, holeCards, villainRange);
2118
2237
  return evaluator.evaluation;
2119
2238
  }
2120
2239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-extval",
3
- "version": "3.3.4",
3
+ "version": "3.4.0",
4
4
  "description": "Poker hand evaluator with comprehensive test suite",
5
5
  "main": "evaluator.js",
6
6
  "files": [