poker-irongrid 1.1.0 → 1.2.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 +21 -10
- package/dist/poker-grid/PokerGridManager.d.ts +45 -7
- package/dist/poker-grid/PokerGridManager.js +169 -175
- package/dist/poker-grid/components/DeadCardsInput.svelte +4 -4
- package/dist/poker-grid/components/DeadCardsInput.svelte.d.ts +2 -2
- package/dist/poker-grid/components/DetailsPanel.svelte.d.ts +2 -2
- package/dist/poker-grid/components/HandSelector.svelte +15 -97
- package/dist/poker-grid/components/HandSelector.svelte.d.ts +6 -4
- package/dist/poker-grid/components/PokerGridPanel.svelte +55 -6
- package/dist/poker-grid/components/PokerGridPanel.svelte.d.ts +4 -2
- package/dist/poker-grid/components/SelectionSummary.svelte +28 -173
- package/dist/poker-grid/components/SelectionSummary.svelte.d.ts +4 -6
- package/dist/poker-grid/index.d.ts +1 -0
- package/dist/poker-grid/index.js +1 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ npm install poker-irongrid
|
|
|
11
11
|
## Peer Dependencies
|
|
12
12
|
|
|
13
13
|
- `svelte`: ^5.0.0
|
|
14
|
-
- `poker-bitvaljs`: ^1.0
|
|
15
|
-
- `poker-extval`: ^
|
|
16
|
-
- `poker-rangeman`: ^
|
|
14
|
+
- `poker-bitvaljs`: ^1.13.0 (for equity evaluation)
|
|
15
|
+
- `poker-extval`: ^6.0.0-beta.6 (for hand evaluation)
|
|
16
|
+
- `poker-rangeman`: ^4.0.0-beta.11 (for range parsing and filtering)
|
|
17
17
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
@@ -84,7 +84,8 @@ npm install poker-irongrid
|
|
|
84
84
|
| `colors` | `Array<string>` | `[]` | Color palette for highlighting hands (HSL format recommended) |
|
|
85
85
|
| `defaultColor` | `string` | `'hsl(0, 0%, 100%)'` | Default background color for uncolored hands |
|
|
86
86
|
| `defaultSelectedColor` | `string` | `'hsl(0, 0%, 85%)'` | Default color for selected hands without explicit color |
|
|
87
|
-
| `evaluator` | `boolean` | `false` |
|
|
87
|
+
| `evaluator` | `boolean` | `false` | Show hero's live equity against the hands on the grid (requires hero cards) |
|
|
88
|
+
| `preflopTableUrl` | `string \| null` | `null` | URL of poker-bitvaljs's `bench/preflop-table.bin` for exact preflop equity (Monte Carlo without it) |
|
|
88
89
|
|
|
89
90
|
#### Initial State Props
|
|
90
91
|
|
|
@@ -216,12 +217,22 @@ The component includes comprehensive filtering options:
|
|
|
216
217
|
|
|
217
218
|
### Equity Evaluation
|
|
218
219
|
|
|
219
|
-
When `evaluator={true}
|
|
220
|
-
|
|
221
|
-
-
|
|
222
|
-
-
|
|
223
|
-
-
|
|
224
|
-
|
|
220
|
+
When `evaluator={true}`, hero's equity is calculated live (asynchronously) whenever the hero cards,
|
|
221
|
+
board, dead cards or the hands on the grid change; there is no button to press.
|
|
222
|
+
- The summary shows hero vs. villain equity against every selected or colored combo
|
|
223
|
+
- The color buttons' **Eq** mode shows hero's equity against each color's hands
|
|
224
|
+
- Both come from one poker-bitvaljs `compareRangeHands` call: exact on the flop, turn and river
|
|
225
|
+
(a few ms), Monte Carlo preflop and on 1-2 card boards
|
|
226
|
+
- Pass `preflopTableUrl` for exact, near-instant preflop equity (the table is ~1.1 MB, fetched once):
|
|
227
|
+
|
|
228
|
+
```svelte
|
|
229
|
+
<script>
|
|
230
|
+
import PokerGridPanel from 'poker-irongrid';
|
|
231
|
+
import preflopTableUrl from 'poker-bitvaljs/bench/preflop-table.bin?url'; // Vite
|
|
232
|
+
</script>
|
|
233
|
+
|
|
234
|
+
<PokerGridPanel evaluator={true} {preflopTableUrl} />
|
|
235
|
+
```
|
|
225
236
|
|
|
226
237
|
### Responsive Design
|
|
227
238
|
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads poker-bitvaljs's preflop table (bench/preflop-table.bin, ~1.1 MB) once, for exact, near-instant
|
|
3
|
+
* preflop equity; without it preflop equity is a Monte Carlo estimate (within about ±0.3%).
|
|
4
|
+
* @param {string} url - Where the app serves the table
|
|
5
|
+
* @returns {Promise<boolean>} Whether the table loaded
|
|
6
|
+
*/
|
|
7
|
+
export function loadPreflopTable(url: string): Promise<boolean>;
|
|
1
8
|
export class PokerGridManager {
|
|
2
9
|
constructor(options?: {});
|
|
3
10
|
hands: any;
|
|
@@ -114,6 +121,14 @@ export class PokerGridManager {
|
|
|
114
121
|
* @returns {Promise<Object>} Filtered hands object
|
|
115
122
|
*/
|
|
116
123
|
getFilteredHandsByCriteria(criteria: string[], boardCards?: string[], hands?: any): Promise<any>;
|
|
124
|
+
/**
|
|
125
|
+
* Keep the combos of a hands object that pass a test.
|
|
126
|
+
* @param {Object} hands - Hands object
|
|
127
|
+
* @param {(c1: string, c2: string) => boolean} test - Gets the two cards of each combo
|
|
128
|
+
* @param {string[]} deadCards - Combos holding these cards are left out
|
|
129
|
+
* @returns {Object} Matching hands
|
|
130
|
+
*/
|
|
131
|
+
filterCombos(hands: any, test: (c1: string, c2: string) => boolean, deadCards?: string[]): any;
|
|
117
132
|
/**
|
|
118
133
|
* Get hands matching a rank.
|
|
119
134
|
* @param {string} rank - Rank (A, K, Q, etc.)
|
|
@@ -123,7 +138,7 @@ export class PokerGridManager {
|
|
|
123
138
|
*/
|
|
124
139
|
getHandsByRank(rank: string, hands?: any, deadCards?: string[]): any;
|
|
125
140
|
/**
|
|
126
|
-
* Get hands
|
|
141
|
+
* Get hands holding at least one card of a suit.
|
|
127
142
|
* @param {string} suitName - Suit name (clubs, diamonds, hearts, spades)
|
|
128
143
|
* @param {Object} hands - Hands object
|
|
129
144
|
* @param {string[]} deadCards - Dead cards to exclude
|
|
@@ -153,7 +168,7 @@ export class PokerGridManager {
|
|
|
153
168
|
*/
|
|
154
169
|
getSuitedHands(hands?: any, deadCards?: string[]): any;
|
|
155
170
|
/**
|
|
156
|
-
* Get offsuit hands.
|
|
171
|
+
* Get offsuit hands (different suits; includes pocket pairs).
|
|
157
172
|
* @param {Object} hands - Hands object
|
|
158
173
|
* @param {string[]} deadCards - Dead cards to exclude
|
|
159
174
|
* @returns {Object} Matching hands
|
|
@@ -185,6 +200,15 @@ export class PokerGridManager {
|
|
|
185
200
|
* @returns {string|null} Hand strength type
|
|
186
201
|
*/
|
|
187
202
|
getHandStrengthType(hand: string, boardCards?: string[]): string | null;
|
|
203
|
+
strengthCacheBoard: any;
|
|
204
|
+
strengthCache: Map<any, any>;
|
|
205
|
+
/**
|
|
206
|
+
* Uncached hand strength type (see getHandStrengthType).
|
|
207
|
+
* @param {string} hand - Hand code
|
|
208
|
+
* @param {string[]} boardCards - Board cards
|
|
209
|
+
* @returns {string|null} Hand strength type
|
|
210
|
+
*/
|
|
211
|
+
computeHandStrengthType(hand: string, boardCards: string[]): string | null;
|
|
188
212
|
/**
|
|
189
213
|
* Get hands by hand strength.
|
|
190
214
|
* @param {string} targetStrength - Strength type
|
|
@@ -218,15 +242,29 @@ export class PokerGridManager {
|
|
|
218
242
|
*/
|
|
219
243
|
removeColor(hands: any, color: string): any;
|
|
220
244
|
/**
|
|
221
|
-
*
|
|
245
|
+
* Hero's equity against the villain hands on the grid, overall and per color, from one
|
|
246
|
+
* poker-bitvaljs compareRangeHands call (each villain combo's result against hero's hand).
|
|
247
|
+
* Villain hands are the combos that are selected or colored; hands holding a dead, board or
|
|
248
|
+
* hero card are left out.
|
|
222
249
|
* @param {Object} hands - Hands object
|
|
223
|
-
* @param {string[]} heroCards - Hero cards
|
|
224
|
-
* @param {string[]} boardCards - Board cards
|
|
225
|
-
* @param {string[]} deadCards - Dead cards
|
|
250
|
+
* @param {string[]} heroCards - Hero cards (two)
|
|
251
|
+
* @param {string[]} boardCards - Board cards (0-5)
|
|
252
|
+
* @param {string[]} deadCards - Dead cards (may include hero and board cards)
|
|
226
253
|
* @param {string[]} colors - Colors array
|
|
254
|
+
* @returns {Promise<{hero: number|null, villain: number|null, combos: number, byColor: Object}|null>}
|
|
255
|
+
* Equities in percent (null when hero meets no villain combo); null without two hero cards.
|
|
256
|
+
*/
|
|
257
|
+
calculateEquity(hands?: any, heroCards?: string[], boardCards?: string[], deadCards?: string[], colors?: string[]): Promise<{
|
|
258
|
+
hero: number | null;
|
|
259
|
+
villain: number | null;
|
|
260
|
+
combos: number;
|
|
261
|
+
byColor: any;
|
|
262
|
+
} | null>;
|
|
263
|
+
/**
|
|
264
|
+
* Hero's equity against each color's hands, rounded (0 when a color has no hands).
|
|
227
265
|
* @returns {Promise<Object>} Color equities object
|
|
228
266
|
*/
|
|
229
|
-
calculateColorEquities(hands?: any, heroCards?:
|
|
267
|
+
calculateColorEquities(hands?: any, heroCards?: any, boardCards?: any, deadCards?: any, colors?: any): Promise<any>;
|
|
230
268
|
/**
|
|
231
269
|
* Apply predefined range.
|
|
232
270
|
* @param {string} rangeName - Range name
|
|
@@ -11,6 +11,50 @@ import { SUITS } from './utils/constants.js';
|
|
|
11
11
|
// Handle CommonJS export
|
|
12
12
|
const BitVal = BitValModule?.default || BitValModule;
|
|
13
13
|
|
|
14
|
+
/** One shared evaluator: poker-bitvaljs keeps its Web Workers between calls. */
|
|
15
|
+
let sharedBitVal = null;
|
|
16
|
+
function getBitVal() {
|
|
17
|
+
if (!sharedBitVal) {
|
|
18
|
+
sharedBitVal = new BitVal();
|
|
19
|
+
// Monte Carlo spots (preflop, 1-2 card boards) sample boards once for every hand: much faster
|
|
20
|
+
sharedBitVal.mcSharedBoards = true;
|
|
21
|
+
}
|
|
22
|
+
return sharedBitVal;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let preflopTableLoad = null;
|
|
26
|
+
/**
|
|
27
|
+
* Loads poker-bitvaljs's preflop table (bench/preflop-table.bin, ~1.1 MB) once, for exact, near-instant
|
|
28
|
+
* preflop equity; without it preflop equity is a Monte Carlo estimate (within about ±0.3%).
|
|
29
|
+
* @param {string} url - Where the app serves the table
|
|
30
|
+
* @returns {Promise<boolean>} Whether the table loaded
|
|
31
|
+
*/
|
|
32
|
+
export function loadPreflopTable(url) {
|
|
33
|
+
if (!url) return Promise.resolve(false);
|
|
34
|
+
preflopTableLoad ||= fetch(url)
|
|
35
|
+
.then(response => {
|
|
36
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
37
|
+
return response.arrayBuffer();
|
|
38
|
+
})
|
|
39
|
+
.then(buffer => {
|
|
40
|
+
getBitVal().loadPreflopTable(buffer);
|
|
41
|
+
return true;
|
|
42
|
+
})
|
|
43
|
+
.catch(error => {
|
|
44
|
+
console.warn('poker-irongrid: preflop table not loaded, using Monte Carlo preflop equity', error);
|
|
45
|
+
return false;
|
|
46
|
+
});
|
|
47
|
+
return preflopTableLoad;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Order-independent key for a two-card combo ('AcKd' and 'KdAc' match). */
|
|
51
|
+
const comboKey = (combo) => {
|
|
52
|
+
const a = combo.substring(0, 2);
|
|
53
|
+
const b = combo.substring(2, 4);
|
|
54
|
+
return a < b ? a + b : b + a;
|
|
55
|
+
};
|
|
56
|
+
const isCombo = (hand) => typeof hand === 'string' && hand.length === 4;
|
|
57
|
+
|
|
14
58
|
// Criteria mappings for RangeManager.hitsHand()
|
|
15
59
|
const strengthCriteriaMap = {
|
|
16
60
|
'onePair': ['Pair'],
|
|
@@ -278,9 +322,13 @@ export class PokerGridManager {
|
|
|
278
322
|
* @returns {Object} Matching hands object
|
|
279
323
|
*/
|
|
280
324
|
rangeManagerToMatching(rmResult, hands = this.hands) {
|
|
325
|
+
// Match by both cards, whatever order poker-rangeman lists them in
|
|
326
|
+
const keyed = new Map();
|
|
327
|
+
Object.keys(hands || {}).forEach(h => { if (isCombo(h)) keyed.set(comboKey(h), h); });
|
|
281
328
|
const matching = {};
|
|
282
329
|
rmResult.toArray().forEach(hand => {
|
|
283
|
-
|
|
330
|
+
const original = keyed.get(comboKey(hand));
|
|
331
|
+
if (original) matching[original] = hands[original];
|
|
284
332
|
});
|
|
285
333
|
return matching;
|
|
286
334
|
}
|
|
@@ -308,6 +356,25 @@ export class PokerGridManager {
|
|
|
308
356
|
}
|
|
309
357
|
}
|
|
310
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Keep the combos of a hands object that pass a test.
|
|
361
|
+
* @param {Object} hands - Hands object
|
|
362
|
+
* @param {(c1: string, c2: string) => boolean} test - Gets the two cards of each combo
|
|
363
|
+
* @param {string[]} deadCards - Combos holding these cards are left out
|
|
364
|
+
* @returns {Object} Matching hands
|
|
365
|
+
*/
|
|
366
|
+
filterCombos(hands, test, deadCards = []) {
|
|
367
|
+
const matching = {};
|
|
368
|
+
Object.keys(hands || {}).forEach(h => {
|
|
369
|
+
if (!isCombo(h)) return;
|
|
370
|
+
const c1 = h.substring(0, 2);
|
|
371
|
+
const c2 = h.substring(2, 4);
|
|
372
|
+
if (deadCards.includes(c1) || deadCards.includes(c2)) return;
|
|
373
|
+
if (test(c1, c2)) matching[h] = hands[h];
|
|
374
|
+
});
|
|
375
|
+
return matching;
|
|
376
|
+
}
|
|
377
|
+
|
|
311
378
|
/**
|
|
312
379
|
* Get hands matching a rank.
|
|
313
380
|
* @param {string} rank - Rank (A, K, Q, etc.)
|
|
@@ -316,20 +383,11 @@ export class PokerGridManager {
|
|
|
316
383
|
* @returns {Object} Matching hands
|
|
317
384
|
*/
|
|
318
385
|
getHandsByRank(rank, hands = this.hands, deadCards = []) {
|
|
319
|
-
|
|
320
|
-
if (allHands.length === 0) return {};
|
|
321
|
-
try {
|
|
322
|
-
const rankRange = `${rank}Xs,${rank}Xo`;
|
|
323
|
-
const currentRm = this.getHandsRangeManager(hands, deadCards);
|
|
324
|
-
const matched = currentRm.intersect(rankRange);
|
|
325
|
-
return this.rangeManagerToMatching(matched, hands);
|
|
326
|
-
} catch (error) {
|
|
327
|
-
return {};
|
|
328
|
-
}
|
|
386
|
+
return this.filterCombos(hands, (c1, c2) => c1[0] === rank || c2[0] === rank, deadCards);
|
|
329
387
|
}
|
|
330
388
|
|
|
331
389
|
/**
|
|
332
|
-
* Get hands
|
|
390
|
+
* Get hands holding at least one card of a suit.
|
|
333
391
|
* @param {string} suitName - Suit name (clubs, diamonds, hearts, spades)
|
|
334
392
|
* @param {Object} hands - Hands object
|
|
335
393
|
* @param {string[]} deadCards - Dead cards to exclude
|
|
@@ -338,15 +396,7 @@ export class PokerGridManager {
|
|
|
338
396
|
getHandsBySuit(suitName, hands = this.hands, deadCards = []) {
|
|
339
397
|
const suitCode = SUITS.find(s => s.name === suitName)?.code;
|
|
340
398
|
if (!suitCode) return {};
|
|
341
|
-
|
|
342
|
-
if (allHands.length === 0) return {};
|
|
343
|
-
try {
|
|
344
|
-
const currentRm = this.getHandsRangeManager(hands, deadCards);
|
|
345
|
-
const matched = currentRm.hasSuit(suitCode);
|
|
346
|
-
return this.rangeManagerToMatching(matched, hands);
|
|
347
|
-
} catch (error) {
|
|
348
|
-
return {};
|
|
349
|
-
}
|
|
399
|
+
return this.filterCombos(hands, (c1, c2) => c1[1] === suitCode || c2[1] === suitCode, deadCards);
|
|
350
400
|
}
|
|
351
401
|
|
|
352
402
|
/**
|
|
@@ -359,15 +409,7 @@ export class PokerGridManager {
|
|
|
359
409
|
getHandsByDoubleSuit(suitName, hands = this.hands, deadCards = []) {
|
|
360
410
|
const suitCode = SUITS.find(s => s.name === suitName)?.code;
|
|
361
411
|
if (!suitCode) return {};
|
|
362
|
-
|
|
363
|
-
if (allHands.length === 0) return {};
|
|
364
|
-
try {
|
|
365
|
-
const currentRm = this.getHandsRangeManager(hands, deadCards);
|
|
366
|
-
const matched = currentRm.suitedOf(suitCode);
|
|
367
|
-
return this.rangeManagerToMatching(matched, hands);
|
|
368
|
-
} catch (error) {
|
|
369
|
-
return {};
|
|
370
|
-
}
|
|
412
|
+
return this.filterCombos(hands, (c1, c2) => c1[1] === suitCode && c2[1] === suitCode, deadCards);
|
|
371
413
|
}
|
|
372
414
|
|
|
373
415
|
/**
|
|
@@ -377,15 +419,7 @@ export class PokerGridManager {
|
|
|
377
419
|
* @returns {Object} Matching hands
|
|
378
420
|
*/
|
|
379
421
|
getPairedHands(hands = this.hands, deadCards = []) {
|
|
380
|
-
|
|
381
|
-
if (allHands.length === 0) return {};
|
|
382
|
-
try {
|
|
383
|
-
const currentRm = this.getHandsRangeManager(hands, deadCards);
|
|
384
|
-
const matched = currentRm.intersect('22+');
|
|
385
|
-
return this.rangeManagerToMatching(matched, hands);
|
|
386
|
-
} catch (error) {
|
|
387
|
-
return {};
|
|
388
|
-
}
|
|
422
|
+
return this.filterCombos(hands, (c1, c2) => c1[0] === c2[0], deadCards);
|
|
389
423
|
}
|
|
390
424
|
|
|
391
425
|
/**
|
|
@@ -395,46 +429,17 @@ export class PokerGridManager {
|
|
|
395
429
|
* @returns {Object} Matching hands
|
|
396
430
|
*/
|
|
397
431
|
getSuitedHands(hands = this.hands, deadCards = []) {
|
|
398
|
-
|
|
399
|
-
if (allHands.length === 0) return {};
|
|
400
|
-
try {
|
|
401
|
-
const currentRm = this.getHandsRangeManager(hands, deadCards);
|
|
402
|
-
// Use method chaining to combine all suited hands
|
|
403
|
-
let allSuited = new RangeManager([]);
|
|
404
|
-
['c', 'd', 'h', 's'].forEach(suit => {
|
|
405
|
-
allSuited = allSuited.include(currentRm.suitedOf(suit));
|
|
406
|
-
});
|
|
407
|
-
return this.rangeManagerToMatching(allSuited, hands);
|
|
408
|
-
} catch (error) {
|
|
409
|
-
return {};
|
|
410
|
-
}
|
|
432
|
+
return this.filterCombos(hands, (c1, c2) => c1[1] === c2[1], deadCards);
|
|
411
433
|
}
|
|
412
434
|
|
|
413
435
|
/**
|
|
414
|
-
* Get offsuit hands.
|
|
436
|
+
* Get offsuit hands (different suits; includes pocket pairs).
|
|
415
437
|
* @param {Object} hands - Hands object
|
|
416
438
|
* @param {string[]} deadCards - Dead cards to exclude
|
|
417
439
|
* @returns {Object} Matching hands
|
|
418
440
|
*/
|
|
419
441
|
getOffsuitHands(hands = this.hands, deadCards = []) {
|
|
420
|
-
|
|
421
|
-
if (allHands.length === 0) return {};
|
|
422
|
-
try {
|
|
423
|
-
const currentRm = this.getHandsRangeManager(hands, deadCards);
|
|
424
|
-
// Use method chaining to combine all offsuit hands
|
|
425
|
-
// For offsuit: get hands that have the suit but exclude suited hands
|
|
426
|
-
let allOffsuit = new RangeManager([]);
|
|
427
|
-
['c', 'd', 'h', 's'].forEach(suit => {
|
|
428
|
-
// Get hands that have this suit, then exclude suited hands
|
|
429
|
-
const hasSuit = currentRm.hasSuit(suit);
|
|
430
|
-
const suited = currentRm.suitedOf(suit);
|
|
431
|
-
const offsuit = hasSuit.exclude(suited);
|
|
432
|
-
allOffsuit = allOffsuit.include(offsuit);
|
|
433
|
-
});
|
|
434
|
-
return this.rangeManagerToMatching(allOffsuit, hands);
|
|
435
|
-
} catch (error) {
|
|
436
|
-
return {};
|
|
437
|
-
}
|
|
442
|
+
return this.filterCombos(hands, (c1, c2) => c1[1] !== c2[1], deadCards);
|
|
438
443
|
}
|
|
439
444
|
|
|
440
445
|
/**
|
|
@@ -485,6 +490,24 @@ export class PokerGridManager {
|
|
|
485
490
|
*/
|
|
486
491
|
getHandStrengthType(hand, boardCards = this.boardCards) {
|
|
487
492
|
if (!boardCards || boardCards.length < 3) return null;
|
|
493
|
+
const boardKey = boardCards.join('');
|
|
494
|
+
if (this.strengthCacheBoard !== boardKey) {
|
|
495
|
+
this.strengthCacheBoard = boardKey;
|
|
496
|
+
this.strengthCache = new Map();
|
|
497
|
+
}
|
|
498
|
+
if (this.strengthCache.has(hand)) return this.strengthCache.get(hand);
|
|
499
|
+
const type = this.computeHandStrengthType(hand, boardCards);
|
|
500
|
+
this.strengthCache.set(hand, type);
|
|
501
|
+
return type;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Uncached hand strength type (see getHandStrengthType).
|
|
506
|
+
* @param {string} hand - Hand code
|
|
507
|
+
* @param {string[]} boardCards - Board cards
|
|
508
|
+
* @returns {string|null} Hand strength type
|
|
509
|
+
*/
|
|
510
|
+
computeHandStrengthType(hand, boardCards) {
|
|
488
511
|
try {
|
|
489
512
|
const handCard1 = hand.substring(0, 2);
|
|
490
513
|
const handCard2 = hand.substring(2, 4);
|
|
@@ -507,7 +530,6 @@ export class PokerGridManager {
|
|
|
507
530
|
return null;
|
|
508
531
|
} catch (error) {
|
|
509
532
|
return null;
|
|
510
|
-
return null;
|
|
511
533
|
}
|
|
512
534
|
}
|
|
513
535
|
|
|
@@ -630,83 +652,79 @@ export class PokerGridManager {
|
|
|
630
652
|
}
|
|
631
653
|
|
|
632
654
|
/**
|
|
633
|
-
*
|
|
655
|
+
* Hero's equity against the villain hands on the grid, overall and per color, from one
|
|
656
|
+
* poker-bitvaljs compareRangeHands call (each villain combo's result against hero's hand).
|
|
657
|
+
* Villain hands are the combos that are selected or colored; hands holding a dead, board or
|
|
658
|
+
* hero card are left out.
|
|
634
659
|
* @param {Object} hands - Hands object
|
|
635
|
-
* @param {string[]} heroCards - Hero cards
|
|
636
|
-
* @param {string[]} boardCards - Board cards
|
|
637
|
-
* @param {string[]} deadCards - Dead cards
|
|
660
|
+
* @param {string[]} heroCards - Hero cards (two)
|
|
661
|
+
* @param {string[]} boardCards - Board cards (0-5)
|
|
662
|
+
* @param {string[]} deadCards - Dead cards (may include hero and board cards)
|
|
638
663
|
* @param {string[]} colors - Colors array
|
|
639
|
-
* @returns {Promise<Object>}
|
|
664
|
+
* @returns {Promise<{hero: number|null, villain: number|null, combos: number, byColor: Object}|null>}
|
|
665
|
+
* Equities in percent (null when hero meets no villain combo); null without two hero cards.
|
|
640
666
|
*/
|
|
641
|
-
async
|
|
642
|
-
if (heroCards?.length !== 2)
|
|
643
|
-
|
|
644
|
-
|
|
667
|
+
async calculateEquity(hands = this.hands, heroCards = this.heroCards, boardCards = this.boardCards, deadCards = this.deadCards, colors = this.colors) {
|
|
668
|
+
if (heroCards?.length !== 2) return null;
|
|
669
|
+
const board = boardCards || [];
|
|
670
|
+
const otherDead = (deadCards || []).filter(card => !heroCards.includes(card) && !board.includes(card));
|
|
671
|
+
const blocked = new Set([...heroCards, ...board, ...otherDead]);
|
|
672
|
+
|
|
673
|
+
const villainHands = Object.keys(hands || {}).filter(h =>
|
|
674
|
+
isCombo(h) &&
|
|
675
|
+
(this.hasExplicitColor(hands[h]) || this.isSelected(hands[h])) &&
|
|
676
|
+
!blocked.has(h.substring(0, 2)) && !blocked.has(h.substring(2, 4))
|
|
677
|
+
);
|
|
645
678
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
);
|
|
650
|
-
|
|
651
|
-
if (coloredHands.length === 0) {
|
|
652
|
-
const equities = {};
|
|
653
|
-
colors.forEach(c => equities[c] = 0);
|
|
654
|
-
return equities;
|
|
655
|
-
}
|
|
679
|
+
const byColor = {};
|
|
680
|
+
(colors || []).forEach(c => { byColor[c] = null; });
|
|
681
|
+
if (villainHands.length === 0) return { hero: null, villain: null, combos: 0, byColor };
|
|
656
682
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const heroHands = [heroHand];
|
|
660
|
-
const numberOfBoardCards = 5;
|
|
661
|
-
|
|
662
|
-
const deadCardsFromInput = (deadCards || []).filter(card =>
|
|
663
|
-
!(heroCards || []).includes(card) && !(boardCards || []).includes(card)
|
|
664
|
-
);
|
|
665
|
-
const deadCardsForBitval = [...heroCards, ...deadCardsFromInput];
|
|
666
|
-
|
|
667
|
-
const boardCardsCount = (boardCards || []).length;
|
|
668
|
-
const optimize = boardCardsCount < 3;
|
|
683
|
+
// Preflop, wait for the table if one is loading (otherwise the result would be Monte Carlo)
|
|
684
|
+
if (board.length === 0 && preflopTableLoad) await preflopTableLoad;
|
|
669
685
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const colorHands = coloredHands.filter(h =>
|
|
676
|
-
hands[h].some(item => item === color)
|
|
677
|
-
);
|
|
686
|
+
// compareRangeHands(hero, villain, board, dead, numberOfBoardCards, iterations, optimize,
|
|
687
|
+
// progressCallback, progressInterval, useWorkers): exact on the flop/turn/river, Monte Carlo otherwise
|
|
688
|
+
const result = await getBitVal().compareRangeHands(
|
|
689
|
+
[heroCards[0] + heroCards[1]], villainHands, board, otherDead, 5, 20000, true, null, 100, true
|
|
690
|
+
);
|
|
678
691
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
692
|
+
// Villain rows are each villain combo's own win/tie/lose against hero
|
|
693
|
+
const rowOf = new Map();
|
|
694
|
+
result.villain.hands.forEach((h, i) => rowOf.set(comboKey(h), i));
|
|
695
|
+
const heroEquityAgainst = (combos) => {
|
|
696
|
+
let heroShare = 0;
|
|
697
|
+
let total = 0;
|
|
698
|
+
combos.forEach(h => {
|
|
699
|
+
const i = rowOf.get(comboKey(h));
|
|
700
|
+
if (i === undefined) return;
|
|
701
|
+
const win = result.villain.win[i];
|
|
702
|
+
const tie = result.villain.tie[i];
|
|
703
|
+
const lose = result.villain.lose[i];
|
|
704
|
+
heroShare += lose + tie / 2;
|
|
705
|
+
total += win + tie + lose;
|
|
706
|
+
});
|
|
707
|
+
return total > 0 ? (heroShare / total) * 100 : null;
|
|
708
|
+
};
|
|
683
709
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
deadCardsForBitval,
|
|
690
|
-
numberOfBoardCards,
|
|
691
|
-
10000,
|
|
692
|
-
optimize,
|
|
693
|
-
null
|
|
694
|
-
);
|
|
710
|
+
const hero = heroEquityAgainst(villainHands);
|
|
711
|
+
(colors || []).forEach(color => {
|
|
712
|
+
const colorHands = villainHands.filter(h => hands[h].includes(color));
|
|
713
|
+
byColor[color] = colorHands.length > 0 ? heroEquityAgainst(colorHands) : null;
|
|
714
|
+
});
|
|
695
715
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
return {};
|
|
709
|
-
}
|
|
716
|
+
return { hero, villain: hero === null ? null : 100 - hero, combos: villainHands.length, byColor };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Hero's equity against each color's hands, rounded (0 when a color has no hands).
|
|
721
|
+
* @returns {Promise<Object>} Color equities object
|
|
722
|
+
*/
|
|
723
|
+
async calculateColorEquities(hands = this.hands, heroCards = this.heroCards, boardCards = this.boardCards, deadCards = this.deadCards, colors = this.colors) {
|
|
724
|
+
const equity = await this.calculateEquity(hands, heroCards, boardCards, deadCards, colors);
|
|
725
|
+
const equities = {};
|
|
726
|
+
(colors || []).forEach(c => { equities[c] = Math.round(equity?.byColor?.[c] ?? 0); });
|
|
727
|
+
return equities;
|
|
710
728
|
}
|
|
711
729
|
|
|
712
730
|
// ==================== Range Methods ====================
|
|
@@ -1037,47 +1055,24 @@ export class PokerGridManager {
|
|
|
1037
1055
|
if (activeHands.length === 0) return false;
|
|
1038
1056
|
|
|
1039
1057
|
try {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1058
|
+
const active = {};
|
|
1059
|
+
activeHands.forEach(h => { active[h] = hands[h]; });
|
|
1060
|
+
const allMatch = (matching) => Object.keys(matching).length === activeHands.length;
|
|
1061
|
+
|
|
1042
1062
|
switch (filterType) {
|
|
1043
1063
|
case 'rank':
|
|
1044
|
-
|
|
1045
|
-
const rankRange = `${filterValue}${filterValue},${filterValue}2s+,${filterValue}2o+`;
|
|
1046
|
-
return activeRm.intersect(rankRange).size() === activeHands.length;
|
|
1047
|
-
|
|
1064
|
+
return allMatch(this.getHandsByRank(filterValue, active));
|
|
1048
1065
|
case 'suit':
|
|
1049
|
-
|
|
1050
|
-
if (!suitCode) return false;
|
|
1051
|
-
return activeRm.hasSuit(suitCode).size() === activeHands.length;
|
|
1052
|
-
|
|
1066
|
+
return allMatch(this.getHandsBySuit(filterValue, active));
|
|
1053
1067
|
case 'doubleSuit':
|
|
1054
|
-
|
|
1055
|
-
if (!doubleSuitCode) return false;
|
|
1056
|
-
return activeRm.suitedOf(doubleSuitCode).size() === activeHands.length;
|
|
1057
|
-
|
|
1068
|
+
return allMatch(this.getHandsByDoubleSuit(filterValue, active));
|
|
1058
1069
|
case 'paired':
|
|
1059
|
-
return
|
|
1060
|
-
|
|
1070
|
+
return allMatch(this.getPairedHands(active));
|
|
1061
1071
|
case 'suited':
|
|
1062
|
-
|
|
1063
|
-
return ['c', 'd', 'h', 's'].some(suit =>
|
|
1064
|
-
activeRm.suitedOf(suit).size() === activeHands.length
|
|
1065
|
-
);
|
|
1066
|
-
|
|
1072
|
+
return allMatch(this.getSuitedHands(active));
|
|
1067
1073
|
case 'unsuited':
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return ['c', 'd', 'h', 's'].some(suit => {
|
|
1071
|
-
try {
|
|
1072
|
-
const hasSuit = activeRm.hasSuit(suit);
|
|
1073
|
-
const suited = activeRm.suitedOf(suit);
|
|
1074
|
-
const offsuit = hasSuit.exclude(suited);
|
|
1075
|
-
return offsuit.size() === activeHands.length;
|
|
1076
|
-
} catch (error) {
|
|
1077
|
-
return false;
|
|
1078
|
-
}
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1074
|
+
return allMatch(this.getOffsuitHands(active));
|
|
1075
|
+
|
|
1081
1076
|
case 'colored':
|
|
1082
1077
|
return activeHands.every(h =>
|
|
1083
1078
|
Array.isArray(hands[h]) && hands[h].some(item => typeof item === 'string' && item.startsWith('hsl('))
|
|
@@ -1123,7 +1118,6 @@ export class PokerGridManager {
|
|
|
1123
1118
|
}
|
|
1124
1119
|
} catch (error) {
|
|
1125
1120
|
return false;
|
|
1126
|
-
return false;
|
|
1127
1121
|
}
|
|
1128
1122
|
}
|
|
1129
1123
|
|
|
@@ -109,10 +109,10 @@
|
|
|
109
109
|
else if (type === 'board') board = formatted;
|
|
110
110
|
else if (type === 'dead') dead = formatted;
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
// Restore the caret right away: deferring it (e.g. to the next frame) moves it back
|
|
113
|
+
// after any keys typed in between, so fast typing landed in the wrong place
|
|
114
|
+
const newPos = Math.max(0, Math.min(cursorPos + diff, formatted.length));
|
|
115
|
+
input.setSelectionRange(newPos, newPos);
|
|
116
116
|
} else {
|
|
117
117
|
if (type === 'hero') hero = rawValue;
|
|
118
118
|
else if (type === 'board') board = rawValue;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default DeadCardsInput;
|
|
2
2
|
type DeadCardsInput = SvelteComponent<{
|
|
3
|
-
deadCards?: any[];
|
|
4
3
|
boardCards?: any[];
|
|
4
|
+
deadCards?: any[];
|
|
5
5
|
heroCards?: any[];
|
|
6
6
|
}, {
|
|
7
7
|
deadCardsChange: CustomEvent<any>;
|
|
@@ -11,8 +11,8 @@ type DeadCardsInput = SvelteComponent<{
|
|
|
11
11
|
$$bindings?: string;
|
|
12
12
|
};
|
|
13
13
|
declare const DeadCardsInput: $$__sveltets_2_IsomorphicComponent<{
|
|
14
|
-
deadCards?: any[];
|
|
15
14
|
boardCards?: any[];
|
|
15
|
+
deadCards?: any[];
|
|
16
16
|
heroCards?: any[];
|
|
17
17
|
}, {
|
|
18
18
|
deadCardsChange: CustomEvent<any>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export default DetailsPanel;
|
|
2
2
|
type DetailsPanel = SvelteComponent<{
|
|
3
|
+
boardCards?: any[];
|
|
3
4
|
hands?: {};
|
|
4
5
|
deadCards?: any[];
|
|
5
|
-
boardCards?: any[];
|
|
6
6
|
defaultColor?: string;
|
|
7
7
|
defaultSelectedColor?: string;
|
|
8
8
|
manager?: any;
|
|
@@ -14,9 +14,9 @@ type DetailsPanel = SvelteComponent<{
|
|
|
14
14
|
$$bindings?: string;
|
|
15
15
|
};
|
|
16
16
|
declare const DetailsPanel: $$__sveltets_2_IsomorphicComponent<{
|
|
17
|
+
boardCards?: any[];
|
|
17
18
|
hands?: {};
|
|
18
19
|
deadCards?: any[];
|
|
19
|
-
boardCards?: any[];
|
|
20
20
|
defaultColor?: string;
|
|
21
21
|
defaultSelectedColor?: string;
|
|
22
22
|
manager?: any;
|
|
@@ -6,18 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createEventDispatcher } from 'svelte';
|
|
8
8
|
import { RANKS, SUITS } from '../utils/index.js';
|
|
9
|
-
import BitValModule from 'poker-bitvaljs';
|
|
10
9
|
|
|
11
10
|
const dispatch = createEventDispatcher();
|
|
12
11
|
|
|
13
|
-
// Handle CommonJS export
|
|
14
|
-
const BitVal = BitValModule?.default || BitValModule;
|
|
15
|
-
|
|
16
12
|
export let hands = {};
|
|
17
13
|
export let boardCards = [];
|
|
18
14
|
export let evaluator = false;
|
|
19
15
|
export let heroCards = [];
|
|
20
|
-
|
|
16
|
+
/** Live equity from PokerGridPanel ({ byColor }), or null. */
|
|
17
|
+
export let equity = null;
|
|
18
|
+
/** True while a newer equity result is being calculated. */
|
|
19
|
+
export let equityPending = false;
|
|
21
20
|
export let colors = [
|
|
22
21
|
'hsl(60, 100%, 70%)',
|
|
23
22
|
'hsl(120, 100%, 70%)',
|
|
@@ -113,97 +112,16 @@
|
|
|
113
112
|
$: colorCounts = colorStats.counts;
|
|
114
113
|
$: colorPercentages = colorStats.percentages;
|
|
115
114
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
/** Track changes to trigger equity recalculation - only when colors change, not selections */
|
|
124
|
-
$: if (evaluator && colorDisplayMode === 'equity' && heroCards?.length === 2 && manager) {
|
|
125
|
-
// Only track colored hands, not selected-only hands
|
|
126
|
-
const currentColorHands = {};
|
|
127
|
-
colors.forEach(color => {
|
|
128
|
-
const coloredHands = Object.keys(hands || {}).filter(h => {
|
|
129
|
-
const handData = hands[h];
|
|
130
|
-
if (!Array.isArray(handData)) return false;
|
|
131
|
-
// Only include if hand has this color (not just selected)
|
|
132
|
-
return handData.some(item => item === color);
|
|
133
|
-
});
|
|
134
|
-
currentColorHands[color] = coloredHands.sort();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Check if any color's hands have changed
|
|
138
|
-
const hasChanged = colors.some(color => {
|
|
139
|
-
const current = JSON.stringify(currentColorHands[color] || []);
|
|
140
|
-
const last = JSON.stringify(lastColorHands[color] || []);
|
|
141
|
-
return current !== last;
|
|
115
|
+
/** Hero's equity against each color's hands, from the panel's live calculation. */
|
|
116
|
+
$: colorEquities = (() => {
|
|
117
|
+
const equities = {};
|
|
118
|
+
colors.forEach(c => {
|
|
119
|
+
const value = equity?.byColor?.[c];
|
|
120
|
+
equities[c] = value === null || value === undefined ? null : Math.round(value);
|
|
142
121
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const heroKey = JSON.stringify((heroCards || []).sort());
|
|
147
|
-
const deadKey = JSON.stringify((deadCards || []).sort());
|
|
148
|
-
const lastBoardKey = JSON.stringify((lastBoardCards || []).sort());
|
|
149
|
-
const lastHeroKey = JSON.stringify((lastHeroCards || []).sort());
|
|
150
|
-
const lastDeadKey = JSON.stringify((lastDeadCards || []).sort());
|
|
151
|
-
|
|
152
|
-
if (hasChanged || boardKey !== lastBoardKey || heroKey !== lastHeroKey || deadKey !== lastDeadKey) {
|
|
153
|
-
lastColorHands = currentColorHands;
|
|
154
|
-
lastBoardCards = [...(boardCards || [])];
|
|
155
|
-
lastHeroCards = [...(heroCards || [])];
|
|
156
|
-
lastDeadCards = [...(deadCards || [])];
|
|
157
|
-
|
|
158
|
-
// Use requestAnimationFrame to ensure UI updates first, then calculate
|
|
159
|
-
requestAnimationFrame(() => {
|
|
160
|
-
setTimeout(() => {
|
|
161
|
-
calculateColorEquities();
|
|
162
|
-
}, 0);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let lastBoardCards = [];
|
|
168
|
-
let lastHeroCards = [];
|
|
169
|
-
let lastDeadCards = [];
|
|
170
|
-
|
|
171
|
-
async function calculateColorEquities() {
|
|
172
|
-
if (isCalculatingEquity || !manager) return;
|
|
173
|
-
if (!evaluator || colorDisplayMode !== 'equity' || heroCards?.length !== 2) {
|
|
174
|
-
colorEquities = {};
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
isCalculatingEquity = true;
|
|
179
|
-
const currentKey = ++equityCalculationKey;
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
const newEquities = await manager.calculateColorEquities(hands, heroCards, boardCards, deadCards, colors);
|
|
183
|
-
|
|
184
|
-
// Check if calculation was invalidated
|
|
185
|
-
if (currentKey !== equityCalculationKey) {
|
|
186
|
-
isCalculatingEquity = false;
|
|
187
|
-
calculatingColors = {};
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
colorEquities = newEquities;
|
|
192
|
-
} catch (error) {
|
|
193
|
-
colorEquities = {};
|
|
194
|
-
colors.forEach(c => colorEquities[c] = 0);
|
|
195
|
-
} finally {
|
|
196
|
-
if (currentKey === equityCalculationKey) {
|
|
197
|
-
isCalculatingEquity = false;
|
|
198
|
-
calculatingColors = {};
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Check if a color is currently calculating equity */
|
|
204
|
-
function isColorCalculating(color) {
|
|
205
|
-
return calculatingColors[color] === true;
|
|
206
|
-
}
|
|
122
|
+
return equities;
|
|
123
|
+
})();
|
|
124
|
+
$: equityLoading = evaluator && colorDisplayMode === 'equity' && heroCards?.length === 2 && equityPending;
|
|
207
125
|
|
|
208
126
|
/** Toggle color display mode: count -> percentage -> equity -> count */
|
|
209
127
|
function toggleColorDisplayMode() {
|
|
@@ -229,7 +147,7 @@
|
|
|
229
147
|
if (colorDisplayMode === 'percentage') {
|
|
230
148
|
texts[color] = `${colorPercentages[color] || 0}%`;
|
|
231
149
|
} else if (colorDisplayMode === 'equity') {
|
|
232
|
-
texts[color] = `${colorEquities[color]
|
|
150
|
+
texts[color] = colorEquities[color] === null ? '–' : `${colorEquities[color]}%`;
|
|
233
151
|
} else {
|
|
234
152
|
texts[color] = colorCounts[color] || 0;
|
|
235
153
|
}
|
|
@@ -264,7 +182,7 @@
|
|
|
264
182
|
</button>
|
|
265
183
|
{#each colors as color}
|
|
266
184
|
<button
|
|
267
|
-
class="px-2 py-1 rounded border border-gray-300 hover:border-gray-400 transition-colors flex-shrink-0 whitespace-nowrap relative {
|
|
185
|
+
class="px-2 py-1 rounded border border-gray-300 hover:border-gray-400 transition-colors flex-shrink-0 whitespace-nowrap relative {equityLoading && colorCounts[color] ? 'pulsing' : ''}"
|
|
268
186
|
style="--button-color: {color}; background-color: var(--button-color);"
|
|
269
187
|
onclick={(e) => handleColorClick(color, e)}
|
|
270
188
|
title="Apply color to selected hands"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export default HandSelector;
|
|
2
2
|
type HandSelector = SvelteComponent<{
|
|
3
|
-
hands?: {};
|
|
4
|
-
deadCards?: any[];
|
|
5
3
|
boardCards?: any[];
|
|
4
|
+
hands?: {};
|
|
6
5
|
heroCards?: any[];
|
|
7
6
|
colors?: string[];
|
|
7
|
+
equity?: any;
|
|
8
8
|
manager?: any;
|
|
9
9
|
evaluator?: boolean;
|
|
10
|
+
equityPending?: boolean;
|
|
10
11
|
}, {
|
|
11
12
|
selectionChange: CustomEvent<any>;
|
|
12
13
|
} & {
|
|
@@ -15,13 +16,14 @@ type HandSelector = SvelteComponent<{
|
|
|
15
16
|
$$bindings?: string;
|
|
16
17
|
};
|
|
17
18
|
declare const HandSelector: $$__sveltets_2_IsomorphicComponent<{
|
|
18
|
-
hands?: {};
|
|
19
|
-
deadCards?: any[];
|
|
20
19
|
boardCards?: any[];
|
|
20
|
+
hands?: {};
|
|
21
21
|
heroCards?: any[];
|
|
22
22
|
colors?: string[];
|
|
23
|
+
equity?: any;
|
|
23
24
|
manager?: any;
|
|
24
25
|
evaluator?: boolean;
|
|
26
|
+
equityPending?: boolean;
|
|
25
27
|
}, {
|
|
26
28
|
selectionChange: CustomEvent<any>;
|
|
27
29
|
} & {
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Purpose: Orchestrates the full poker grid experience (dead inputs, range select, grid, controls, summary)
|
|
7
7
|
* with configurable visibility and two-way bindings for raw and visible hands.
|
|
8
8
|
*/
|
|
9
|
-
import { createEventDispatcher } from 'svelte';
|
|
10
|
-
import { PokerGridManager } from '../PokerGridManager.js';
|
|
9
|
+
import { createEventDispatcher, onDestroy } from 'svelte';
|
|
10
|
+
import { PokerGridManager, loadPreflopTable } from '../PokerGridManager.js';
|
|
11
11
|
import BaseGrid from './Grid.svelte';
|
|
12
12
|
import HandSelector from './HandSelector.svelte';
|
|
13
13
|
import SelectionSummary from './SelectionSummary.svelte';
|
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
export let defaultColor = 'hsl(0, 0%, 100%)';
|
|
30
30
|
export let defaultSelectedColor = 'hsl(0, 0%, 85%)';
|
|
31
31
|
export let colors = [];
|
|
32
|
+
/**
|
|
33
|
+
* Optional URL of poker-bitvaljs's preflop table (bench/preflop-table.bin) for exact preflop equity,
|
|
34
|
+
* e.g. with Vite: import preflopTableUrl from 'poker-bitvaljs/bench/preflop-table.bin?url'.
|
|
35
|
+
* Without it, preflop equity is a Monte Carlo estimate.
|
|
36
|
+
*/
|
|
37
|
+
export let preflopTableUrl = null;
|
|
38
|
+
$: if (evaluator && preflopTableUrl) loadPreflopTable(preflopTableUrl);
|
|
32
39
|
/** Predefined ranges for range selection. Array of {name: string, abbreviation: string} */
|
|
33
40
|
export let ranges = [];
|
|
34
41
|
|
|
@@ -93,6 +100,48 @@
|
|
|
93
100
|
/** Derive visible hands (filtered by dead cards). */
|
|
94
101
|
$: visibleHands = manager.filterByDeadCards(internalHands, deadCards);
|
|
95
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Live equity (evaluator mode): recalculated, asynchronously, whenever the hero cards, board,
|
|
105
|
+
* dead cards or the hands on the grid change. Results that arrive after a newer request are dropped.
|
|
106
|
+
*/
|
|
107
|
+
let equity = null;
|
|
108
|
+
let equityPending = false;
|
|
109
|
+
let equityRequest = 0;
|
|
110
|
+
let equityTimer = null;
|
|
111
|
+
let lastEquityKey = '';
|
|
112
|
+
|
|
113
|
+
$: equityKey = evaluator
|
|
114
|
+
? JSON.stringify({ heroCards, boardCards, deadCards, hands: visibleHands, colors })
|
|
115
|
+
: '';
|
|
116
|
+
$: if (equityKey !== lastEquityKey) {
|
|
117
|
+
lastEquityKey = equityKey;
|
|
118
|
+
scheduleEquity();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function scheduleEquity() {
|
|
122
|
+
clearTimeout(equityTimer);
|
|
123
|
+
const request = ++equityRequest;
|
|
124
|
+
if (!evaluator || heroCards?.length !== 2) {
|
|
125
|
+
equity = null;
|
|
126
|
+
equityPending = false;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
equityPending = true;
|
|
130
|
+
// A short debounce coalesces bursts (typing cards, dragging across the grid)
|
|
131
|
+
equityTimer = setTimeout(async () => {
|
|
132
|
+
try {
|
|
133
|
+
const result = await manager.calculateEquity(visibleHands, heroCards, boardCards, deadCards, colors);
|
|
134
|
+
if (request === equityRequest) equity = result;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (request === equityRequest) equity = null;
|
|
137
|
+
} finally {
|
|
138
|
+
if (request === equityRequest) equityPending = false;
|
|
139
|
+
}
|
|
140
|
+
}, 50);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
onDestroy(() => clearTimeout(equityTimer));
|
|
144
|
+
|
|
96
145
|
/** Highlighted and selected counts for summary. */
|
|
97
146
|
$: highlightedCount = Object.values(visibleHands || {}).filter(v => manager.hasExplicitColor(v)).length;
|
|
98
147
|
$: selectedCount = Object.values(visibleHands || {}).filter(v => manager.isSelected(v)).length;
|
|
@@ -228,9 +277,8 @@
|
|
|
228
277
|
selected={selectedCount}
|
|
229
278
|
{evaluator}
|
|
230
279
|
{heroCards}
|
|
231
|
-
{
|
|
232
|
-
{
|
|
233
|
-
{hands}
|
|
280
|
+
{equity}
|
|
281
|
+
{equityPending}
|
|
234
282
|
{visibleHands}
|
|
235
283
|
{manager}
|
|
236
284
|
/>
|
|
@@ -256,8 +304,9 @@
|
|
|
256
304
|
{boardCards}
|
|
257
305
|
{evaluator}
|
|
258
306
|
{heroCards}
|
|
259
|
-
{deadCards}
|
|
260
307
|
{colors}
|
|
308
|
+
{equity}
|
|
309
|
+
{equityPending}
|
|
261
310
|
{manager}
|
|
262
311
|
on:selectionChange={handleSelectionChange}
|
|
263
312
|
on:clearAll={() => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export default PokerGridPanel;
|
|
2
2
|
type PokerGridPanel = SvelteComponent<{
|
|
3
|
+
boardCards?: any[];
|
|
3
4
|
hands?: {};
|
|
4
5
|
deadCards?: any[];
|
|
5
|
-
boardCards?: any[];
|
|
6
6
|
heroCards?: any[];
|
|
7
7
|
colors?: any[];
|
|
8
8
|
defaultColor?: string;
|
|
@@ -17,6 +17,7 @@ type PokerGridPanel = SvelteComponent<{
|
|
|
17
17
|
showRangeSelect?: boolean;
|
|
18
18
|
showControls?: boolean;
|
|
19
19
|
showSummary?: boolean;
|
|
20
|
+
preflopTableUrl?: any;
|
|
20
21
|
initialHands?: any;
|
|
21
22
|
initialBoard?: any;
|
|
22
23
|
initialDead?: any;
|
|
@@ -31,9 +32,9 @@ type PokerGridPanel = SvelteComponent<{
|
|
|
31
32
|
$$bindings?: string;
|
|
32
33
|
};
|
|
33
34
|
declare const PokerGridPanel: $$__sveltets_2_IsomorphicComponent<{
|
|
35
|
+
boardCards?: any[];
|
|
34
36
|
hands?: {};
|
|
35
37
|
deadCards?: any[];
|
|
36
|
-
boardCards?: any[];
|
|
37
38
|
heroCards?: any[];
|
|
38
39
|
colors?: any[];
|
|
39
40
|
defaultColor?: string;
|
|
@@ -48,6 +49,7 @@ declare const PokerGridPanel: $$__sveltets_2_IsomorphicComponent<{
|
|
|
48
49
|
showRangeSelect?: boolean;
|
|
49
50
|
showControls?: boolean;
|
|
50
51
|
showSummary?: boolean;
|
|
52
|
+
preflopTableUrl?: any;
|
|
51
53
|
initialHands?: any;
|
|
52
54
|
initialBoard?: any;
|
|
53
55
|
initialDead?: any;
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
/** SelectionSummary: display counts for highlighted and selected combos. */
|
|
3
|
-
import BitValModule from 'poker-bitvaljs';
|
|
4
|
-
|
|
5
|
-
// Handle CommonJS export
|
|
6
|
-
const BitVal = BitValModule?.default || BitValModule;
|
|
7
|
-
|
|
8
3
|
export let highlighted = 0;
|
|
9
4
|
export let selected = 0;
|
|
10
5
|
export let evaluator = false;
|
|
11
6
|
export let heroCards = [];
|
|
12
|
-
|
|
13
|
-
export let
|
|
14
|
-
|
|
7
|
+
/** Live equity from PokerGridPanel: { hero, villain, combos, byColor } or null. */
|
|
8
|
+
export let equity = null;
|
|
9
|
+
/** True while a newer equity result is being calculated. */
|
|
10
|
+
export let equityPending = false;
|
|
15
11
|
export let visibleHands = {};
|
|
16
12
|
export let manager = null; // PokerGridManager instance
|
|
17
13
|
|
|
@@ -49,142 +45,9 @@
|
|
|
49
45
|
await copyToClipboard(selectedHands);
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let evaluationProgress = 0;
|
|
56
|
-
let evaluationKey = 0;
|
|
57
|
-
let lastEvaluationKey = '';
|
|
58
|
-
|
|
59
|
-
/** Reset results when inputs change */
|
|
60
|
-
$: {
|
|
61
|
-
if (evaluator && (heroCards?.length !== 2 || !hasHandsOnGrid)) {
|
|
62
|
-
evaluationResults = null;
|
|
63
|
-
}
|
|
64
|
-
const newKey = JSON.stringify({ heroCards, boardCards, deadCards, visibleHands });
|
|
65
|
-
if (newKey !== lastEvaluationKey) {
|
|
66
|
-
evaluationResults = null;
|
|
67
|
-
lastEvaluationKey = newKey;
|
|
68
|
-
}
|
|
69
|
-
evaluationKey = newKey;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Check if there are hands on the grid to evaluate against */
|
|
73
|
-
$: hasHandsOnGrid = visibleHands && Object.keys(visibleHands).length > 0;
|
|
74
|
-
|
|
75
|
-
/** Check if button should be enabled */
|
|
76
|
-
$: isButtonEnabled = evaluator && heroCards?.length === 2 && hasHandsOnGrid && !isEvaluating;
|
|
77
|
-
|
|
78
|
-
/** Check if button should be visible (always show when evaluator is true, even if disabled) */
|
|
79
|
-
$: showButton = evaluator;
|
|
80
|
-
|
|
81
|
-
/** Run equity evaluation */
|
|
82
|
-
async function runEvaluation() {
|
|
83
|
-
if (!isButtonEnabled) return;
|
|
84
|
-
|
|
85
|
-
isEvaluating = true;
|
|
86
|
-
evaluationResults = null;
|
|
87
|
-
evaluationProgress = 0;
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
// Get villain hands from visibleHands (already filtered for dead cards)
|
|
91
|
-
// Only hands that are highlighted (have colors) or selected
|
|
92
|
-
// A hand is highlighted if its value array contains an HSL color string
|
|
93
|
-
const hasExplicitColor = (values) => manager ? manager.hasExplicitColor(values) : false;
|
|
94
|
-
const isSelected = (values) => manager ? manager.isSelected(values) : false;
|
|
95
|
-
const villainHands = Object.entries(visibleHands || {})
|
|
96
|
-
.filter(([hand, values]) =>
|
|
97
|
-
hand && hand.length === 4 && // Valid hand format (e.g., 'AcKd')
|
|
98
|
-
(hasExplicitColor(values) || isSelected(values)) // Has color or is selected
|
|
99
|
-
)
|
|
100
|
-
.map(([hand]) => hand);
|
|
101
|
-
|
|
102
|
-
if (villainHands.length === 0) {
|
|
103
|
-
isEvaluating = false;
|
|
104
|
-
evaluationProgress = 0;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Create hero hands array (single hand)
|
|
109
|
-
const heroHand = heroCards[0] + heroCards[1];
|
|
110
|
-
const heroHands = [heroHand];
|
|
111
|
-
|
|
112
|
-
// Initialize BitVal
|
|
113
|
-
const bitval = new BitVal();
|
|
114
|
-
|
|
115
|
-
// numberOfBoardCards should always be 5 (total board cards to be dealt)
|
|
116
|
-
// For preflop, boardCards is empty but numberOfBoardCards is still 5
|
|
117
|
-
// For postflop, boardCards has known cards and numberOfBoardCards is 5
|
|
118
|
-
const numberOfBoardCards = 5;
|
|
119
|
-
|
|
120
|
-
// Dead cards for bitval: hero cards + dead cards from input (exclude board cards)
|
|
121
|
-
// Hero cards MUST be included so they don't get dealt to villain
|
|
122
|
-
// Board cards are excluded because bitval adds them internally
|
|
123
|
-
const deadCardsFromInput = (deadCards || []).filter(card =>
|
|
124
|
-
!(heroCards || []).includes(card) && !(boardCards || []).includes(card)
|
|
125
|
-
);
|
|
126
|
-
const deadCardsForBitval = [...heroCards, ...deadCardsFromInput];
|
|
127
|
-
|
|
128
|
-
// Disable optimize mode when 3 or more board cards are present (flop, turn, river)
|
|
129
|
-
// Optimize mode uses canonical caching which can introduce ±0.5% margin of error
|
|
130
|
-
// For 0, 1, or 2 board cards, use optimization for speed
|
|
131
|
-
// For 3+ board cards, accuracy is more important
|
|
132
|
-
const boardCardsCount = (boardCards || []).length;
|
|
133
|
-
const optimize = boardCardsCount < 3;
|
|
134
|
-
|
|
135
|
-
// Progress callback to update progress percentage
|
|
136
|
-
const progressCallback = (current, total, message) => {
|
|
137
|
-
const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
|
|
138
|
-
evaluationProgress = percentage;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// Prepare exact parameters for bitval.compareRange
|
|
142
|
-
const bitvalParams = {
|
|
143
|
-
heroHands,
|
|
144
|
-
villainHands,
|
|
145
|
-
boardCards: boardCards || [],
|
|
146
|
-
deadCards: deadCardsForBitval,
|
|
147
|
-
numberOfBoardCards,
|
|
148
|
-
iterations: 10000,
|
|
149
|
-
optimize,
|
|
150
|
-
progressCallback
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// Run evaluation
|
|
154
|
-
const result = await bitval.compareRange(
|
|
155
|
-
bitvalParams.heroHands,
|
|
156
|
-
bitvalParams.villainHands,
|
|
157
|
-
bitvalParams.boardCards,
|
|
158
|
-
bitvalParams.deadCards,
|
|
159
|
-
bitvalParams.numberOfBoardCards,
|
|
160
|
-
bitvalParams.iterations,
|
|
161
|
-
bitvalParams.optimize,
|
|
162
|
-
bitvalParams.progressCallback
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
// Calculate equity
|
|
166
|
-
const total = result.win + result.tie + result.lose;
|
|
167
|
-
const heroEquity = total > 0 ? ((result.win + result.tie / 2) / total) * 100 : 0;
|
|
168
|
-
const villainEquity = 100 - heroEquity;
|
|
169
|
-
|
|
170
|
-
evaluationResults = {
|
|
171
|
-
heroEquity: heroEquity,
|
|
172
|
-
villainEquity: villainEquity
|
|
173
|
-
};
|
|
174
|
-
evaluationProgress = 100;
|
|
175
|
-
} catch (error) {
|
|
176
|
-
evaluationResults = null;
|
|
177
|
-
evaluationProgress = 0;
|
|
178
|
-
} finally {
|
|
179
|
-
isEvaluating = false;
|
|
180
|
-
// Reset progress after a brief delay to show 100%
|
|
181
|
-
setTimeout(() => {
|
|
182
|
-
if (!isEvaluating) {
|
|
183
|
-
evaluationProgress = 0;
|
|
184
|
-
}
|
|
185
|
-
}, 500);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
48
|
+
/** Equity display: the panel recalculates it live whenever the inputs change. */
|
|
49
|
+
$: showEquity = evaluator && heroCards?.length === 2;
|
|
50
|
+
$: hasEquity = equity && equity.hero !== null && equity.hero !== undefined;
|
|
188
51
|
</script>
|
|
189
52
|
|
|
190
53
|
<div class="flex items-center justify-between mt-1">
|
|
@@ -211,40 +74,32 @@
|
|
|
211
74
|
</span>
|
|
212
75
|
</span>
|
|
213
76
|
<div class="flex items-center gap-2">
|
|
214
|
-
{#if
|
|
215
|
-
<div
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
</div>
|
|
222
|
-
{:else if showButton}
|
|
223
|
-
<button
|
|
224
|
-
type="button"
|
|
225
|
-
onclick={runEvaluation}
|
|
226
|
-
disabled={!isButtonEnabled}
|
|
227
|
-
class="px-2 py-1 text-xs rounded border transition-colors"
|
|
228
|
-
style="
|
|
229
|
-
background-color: {isButtonEnabled ? 'var(--irongrid-color-primary)' : 'var(--irongrid-color-bg-disabled)'};
|
|
230
|
-
color: {isButtonEnabled ? 'var(--irongrid-color-white)' : 'var(--irongrid-color-text-secondary)'};
|
|
231
|
-
border-color: {isButtonEnabled ? 'var(--irongrid-color-primary)' : 'var(--irongrid-color-bg-disabled)'};
|
|
232
|
-
cursor: {isButtonEnabled ? 'pointer' : 'not-allowed'};
|
|
233
|
-
opacity: {isButtonEnabled ? '1' : 'var(--irongrid-opacity-disabled)'};
|
|
234
|
-
"
|
|
235
|
-
onmouseover={(e) => {
|
|
236
|
-
if (isButtonEnabled) e.target.style.opacity = '0.9';
|
|
237
|
-
}}
|
|
238
|
-
onmouseout={(e) => {
|
|
239
|
-
if (isButtonEnabled) e.target.style.opacity = '1';
|
|
240
|
-
}}
|
|
77
|
+
{#if showEquity}
|
|
78
|
+
<div
|
|
79
|
+
class="text-xs equity-readout"
|
|
80
|
+
class:equity-pending={equityPending}
|
|
81
|
+
style="color: var(--irongrid-color-text-secondary);"
|
|
82
|
+
aria-live="polite"
|
|
83
|
+
title={hasEquity ? `Hero's equity against ${equity.combos} villain combos (selected or colored)` : ''}
|
|
241
84
|
>
|
|
242
|
-
|
|
243
|
-
|
|
85
|
+
{#if hasEquity}
|
|
86
|
+
<span>Hero: {Math.round(equity.hero)}% vs. Villain: {Math.round(equity.villain)}%</span>
|
|
87
|
+
{:else if equityPending}
|
|
88
|
+
<span>Calculating…</span>
|
|
89
|
+
{:else}
|
|
90
|
+
<span>Select villain hands for equity</span>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
244
93
|
{/if}
|
|
245
94
|
</div>
|
|
246
95
|
</div>
|
|
247
96
|
|
|
248
97
|
<style>
|
|
98
|
+
.equity-readout {
|
|
99
|
+
transition: opacity 120ms ease;
|
|
100
|
+
}
|
|
101
|
+
.equity-pending {
|
|
102
|
+
opacity: 0.55;
|
|
103
|
+
}
|
|
249
104
|
</style>
|
|
250
105
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
export default SelectionSummary;
|
|
2
2
|
type SelectionSummary = SvelteComponent<{
|
|
3
|
-
hands?: {};
|
|
4
|
-
deadCards?: any[];
|
|
5
|
-
boardCards?: any[];
|
|
6
3
|
heroCards?: any[];
|
|
7
4
|
selected?: number;
|
|
5
|
+
equity?: any;
|
|
8
6
|
manager?: any;
|
|
9
7
|
evaluator?: boolean;
|
|
8
|
+
equityPending?: boolean;
|
|
10
9
|
highlighted?: number;
|
|
11
10
|
visibleHands?: {};
|
|
12
11
|
}, {
|
|
@@ -15,13 +14,12 @@ type SelectionSummary = SvelteComponent<{
|
|
|
15
14
|
$$bindings?: string;
|
|
16
15
|
};
|
|
17
16
|
declare const SelectionSummary: $$__sveltets_2_IsomorphicComponent<{
|
|
18
|
-
hands?: {};
|
|
19
|
-
deadCards?: any[];
|
|
20
|
-
boardCards?: any[];
|
|
21
17
|
heroCards?: any[];
|
|
22
18
|
selected?: number;
|
|
19
|
+
equity?: any;
|
|
23
20
|
manager?: any;
|
|
24
21
|
evaluator?: boolean;
|
|
22
|
+
equityPending?: boolean;
|
|
25
23
|
highlighted?: number;
|
|
26
24
|
visibleHands?: {};
|
|
27
25
|
}, {
|
package/dist/poker-grid/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { default as PokerGridPanel } from './components/PokerGridPanel.svelte';
|
|
7
7
|
export { default } from './components/PokerGridPanel.svelte';
|
|
8
|
+
export { loadPreflopTable } from './PokerGridManager.js';
|
|
8
9
|
|
|
9
10
|
// Export theme CSS for consumers to import
|
|
10
11
|
import './theme.css';
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poker-irongrid",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/DarraghGeo/poker-irongrid.git"
|
|
7
|
+
},
|
|
4
8
|
"scripts": {
|
|
5
9
|
"dev": "vite dev",
|
|
6
10
|
"build": "vite build && npm run prepack",
|
|
@@ -31,9 +35,9 @@
|
|
|
31
35
|
"svelte": "^5.0.0"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
|
-
"poker-bitvaljs": "^1.0
|
|
35
|
-
"poker-extval": "^
|
|
36
|
-
"poker-rangeman": "^
|
|
38
|
+
"poker-bitvaljs": "^1.13.0",
|
|
39
|
+
"poker-extval": "^6.0.0-beta.6",
|
|
40
|
+
"poker-rangeman": "^4.0.0-beta.11"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
43
|
"@eslint/compat": "^1.4.0",
|