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.
- package/README.md +27 -2
- package/evaluator.js +126 -7
- 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
|
|
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
|
|