poker-irongrid 1.3.0 → 1.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 +6 -1
- package/dist/poker-grid/PokerGridManager.d.ts +26 -5
- package/dist/poker-grid/PokerGridManager.js +62 -8
- package/dist/poker-grid/components/Grid.svelte +16 -2
- package/dist/poker-grid/components/PokerGridPanel.svelte +31 -3
- package/dist/poker-grid/components/PokerGridPanel.svelte.d.ts +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,7 @@ npm install poker-irongrid
|
|
|
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
87
|
| `evaluator` | `boolean` | `false` | Show hero's live equity against the hands on the grid (requires hero cards) |
|
|
88
|
+
| `equityColors` | `boolean` | `true` | With `evaluator`, color villain hands by hero's equity against each combo: equal-width bands, one per color (`colors[0]` = hero's lowest equity). Hands you color yourself keep their color |
|
|
88
89
|
| `subgrid` | `boolean` | `false` | Split the grid into 3x3 blocks (A–T, 9–6, 5–2) with space after the T and 6 rows/columns; spacing set by `--irongrid-subgrid-gap` (default `6px`) |
|
|
89
90
|
| `preflopTableUrl` | `string \| null` | `null` | URL of poker-bitvaljs's `bench/preflop-table.bin` for exact preflop equity (Monte Carlo without it) |
|
|
90
91
|
|
|
@@ -222,7 +223,11 @@ When `evaluator={true}`, hero's equity is calculated live (asynchronously) whene
|
|
|
222
223
|
board, dead cards or the hands on the grid change; there is no button to press.
|
|
223
224
|
- The summary shows hero vs. villain equity against every selected or colored combo
|
|
224
225
|
- The color buttons' **Eq** mode shows hero's equity against each color's hands
|
|
225
|
-
-
|
|
226
|
+
- With `equityColors` (default), the villain hands are colored by hero's equity against each combo, in
|
|
227
|
+
one equal-width band per color (6 colors: 0–16.7%, 16.7–33.3%, … 83.3–100%, `colors[0]` lowest), so the
|
|
228
|
+
color buttons show each band's count, share and equity. Hands you color yourself keep their color; band
|
|
229
|
+
colors follow the selection and go away without hero cards
|
|
230
|
+
- All of it comes from one poker-bitvaljs `compareRangeHands` call: exact on the flop, turn and river
|
|
226
231
|
(a few ms), Monte Carlo preflop and on 1-2 card boards
|
|
227
232
|
- Pass `preflopTableUrl` for exact, near-instant preflop equity (the table is ~1.1 MB, fetched once):
|
|
228
233
|
|
|
@@ -244,22 +244,43 @@ export class PokerGridManager {
|
|
|
244
244
|
/**
|
|
245
245
|
* Hero's equity against the villain hands on the grid, overall and per color, from one
|
|
246
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
|
|
248
|
-
* hero card are left out.
|
|
247
|
+
* Villain hands are the combos that are selected or colored by hand (colors in `autoColors`, set by
|
|
248
|
+
* applyEquityBands, don't count); hands holding a dead, board or hero card are left out.
|
|
249
249
|
* @param {Object} hands - Hands object
|
|
250
250
|
* @param {string[]} heroCards - Hero cards (two)
|
|
251
251
|
* @param {string[]} boardCards - Board cards (0-5)
|
|
252
252
|
* @param {string[]} deadCards - Dead cards (may include hero and board cards)
|
|
253
253
|
* @param {string[]} colors - Colors array
|
|
254
|
-
* @
|
|
255
|
-
*
|
|
254
|
+
* @param {Map<string, string>|null} autoColors - Colors applyEquityBands set (combo -> color)
|
|
255
|
+
* @returns {Promise<{hero: number|null, villain: number|null, combos: number, byColor: Object, byCombo: Object}|null>}
|
|
256
|
+
* Equities in percent (null when hero meets no villain combo); byCombo is hero's equity against each
|
|
257
|
+
* villain combo. Null without two hero cards.
|
|
256
258
|
*/
|
|
257
|
-
calculateEquity(hands?: any, heroCards?: string[], boardCards?: string[], deadCards?: string[], colors?: string[]): Promise<{
|
|
259
|
+
calculateEquity(hands?: any, heroCards?: string[], boardCards?: string[], deadCards?: string[], colors?: string[], autoColors?: Map<string, string> | null): Promise<{
|
|
258
260
|
hero: number | null;
|
|
259
261
|
villain: number | null;
|
|
260
262
|
combos: number;
|
|
261
263
|
byColor: any;
|
|
264
|
+
byCombo: any;
|
|
262
265
|
} | null>;
|
|
266
|
+
/**
|
|
267
|
+
* Color villain combos by hero's equity against them: one equal-width band per color, colors[0] for
|
|
268
|
+
* hero's lowest equity (villain's strongest hands) up to the last color for hero's highest. With 6
|
|
269
|
+
* colors the bands are 0-16.7%, 16.7-33.3%, ... 83.3-100%.
|
|
270
|
+
* Only combos without a color, or still holding the color this method gave them, are recolored, so
|
|
271
|
+
* hand-applied colors stay. Colors this method set are removed from combos that are no longer in
|
|
272
|
+
* `byCombo` (deselected, dead, or no equity).
|
|
273
|
+
* @param {Object} hands - Hands object
|
|
274
|
+
* @param {Object} byCombo - Hero's equity (%) against each villain combo, from calculateEquity
|
|
275
|
+
* @param {string[]} colors - Colors, one per band
|
|
276
|
+
* @param {Map<string, string>} autoColors - Colors set by earlier calls (combo -> color)
|
|
277
|
+
* @returns {{hands: Object, autoColors: Map<string, string>, changed: boolean}}
|
|
278
|
+
*/
|
|
279
|
+
applyEquityBands(hands: any, byCombo: any, colors: string[], autoColors?: Map<string, string>): {
|
|
280
|
+
hands: any;
|
|
281
|
+
autoColors: Map<string, string>;
|
|
282
|
+
changed: boolean;
|
|
283
|
+
};
|
|
263
284
|
/**
|
|
264
285
|
* Hero's equity against each color's hands, rounded (0 when a color has no hands).
|
|
265
286
|
* @returns {Promise<Object>} Color equities object
|
|
@@ -654,31 +654,38 @@ export class PokerGridManager {
|
|
|
654
654
|
/**
|
|
655
655
|
* Hero's equity against the villain hands on the grid, overall and per color, from one
|
|
656
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
|
|
658
|
-
* hero card are left out.
|
|
657
|
+
* Villain hands are the combos that are selected or colored by hand (colors in `autoColors`, set by
|
|
658
|
+
* applyEquityBands, don't count); hands holding a dead, board or hero card are left out.
|
|
659
659
|
* @param {Object} hands - Hands object
|
|
660
660
|
* @param {string[]} heroCards - Hero cards (two)
|
|
661
661
|
* @param {string[]} boardCards - Board cards (0-5)
|
|
662
662
|
* @param {string[]} deadCards - Dead cards (may include hero and board cards)
|
|
663
663
|
* @param {string[]} colors - Colors array
|
|
664
|
-
* @
|
|
665
|
-
*
|
|
664
|
+
* @param {Map<string, string>|null} autoColors - Colors applyEquityBands set (combo -> color)
|
|
665
|
+
* @returns {Promise<{hero: number|null, villain: number|null, combos: number, byColor: Object, byCombo: Object}|null>}
|
|
666
|
+
* Equities in percent (null when hero meets no villain combo); byCombo is hero's equity against each
|
|
667
|
+
* villain combo. Null without two hero cards.
|
|
666
668
|
*/
|
|
667
|
-
async calculateEquity(hands = this.hands, heroCards = this.heroCards, boardCards = this.boardCards, deadCards = this.deadCards, colors = this.colors) {
|
|
669
|
+
async calculateEquity(hands = this.hands, heroCards = this.heroCards, boardCards = this.boardCards, deadCards = this.deadCards, colors = this.colors, autoColors = null) {
|
|
668
670
|
if (heroCards?.length !== 2) return null;
|
|
669
671
|
const board = boardCards || [];
|
|
670
672
|
const otherDead = (deadCards || []).filter(card => !heroCards.includes(card) && !board.includes(card));
|
|
671
673
|
const blocked = new Set([...heroCards, ...board, ...otherDead]);
|
|
672
674
|
|
|
675
|
+
const colorOf = (h) => (hands[h] || []).find(item => typeof item === 'string' && item.startsWith('hsl('));
|
|
676
|
+
const coloredByHand = (h) => {
|
|
677
|
+
const color = colorOf(h);
|
|
678
|
+
return !!color && !(autoColors && autoColors.get(h) === color);
|
|
679
|
+
};
|
|
673
680
|
const villainHands = Object.keys(hands || {}).filter(h =>
|
|
674
681
|
isCombo(h) &&
|
|
675
|
-
(
|
|
682
|
+
(coloredByHand(h) || this.isSelected(hands[h])) &&
|
|
676
683
|
!blocked.has(h.substring(0, 2)) && !blocked.has(h.substring(2, 4))
|
|
677
684
|
);
|
|
678
685
|
|
|
679
686
|
const byColor = {};
|
|
680
687
|
(colors || []).forEach(c => { byColor[c] = null; });
|
|
681
|
-
if (villainHands.length === 0) return { hero: null, villain: null, combos: 0, byColor };
|
|
688
|
+
if (villainHands.length === 0) return { hero: null, villain: null, combos: 0, byColor, byCombo: {} };
|
|
682
689
|
|
|
683
690
|
// Preflop, wait for the table if one is loading (otherwise the result would be Monte Carlo)
|
|
684
691
|
if (board.length === 0 && preflopTableLoad) await preflopTableLoad;
|
|
@@ -708,12 +715,59 @@ export class PokerGridManager {
|
|
|
708
715
|
};
|
|
709
716
|
|
|
710
717
|
const hero = heroEquityAgainst(villainHands);
|
|
718
|
+
const byCombo = {};
|
|
719
|
+
villainHands.forEach(h => { byCombo[h] = heroEquityAgainst([h]); });
|
|
711
720
|
(colors || []).forEach(color => {
|
|
712
721
|
const colorHands = villainHands.filter(h => hands[h].includes(color));
|
|
713
722
|
byColor[color] = colorHands.length > 0 ? heroEquityAgainst(colorHands) : null;
|
|
714
723
|
});
|
|
715
724
|
|
|
716
|
-
return { hero, villain: hero === null ? null : 100 - hero, combos: villainHands.length, byColor };
|
|
725
|
+
return { hero, villain: hero === null ? null : 100 - hero, combos: villainHands.length, byColor, byCombo };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Color villain combos by hero's equity against them: one equal-width band per color, colors[0] for
|
|
730
|
+
* hero's lowest equity (villain's strongest hands) up to the last color for hero's highest. With 6
|
|
731
|
+
* colors the bands are 0-16.7%, 16.7-33.3%, ... 83.3-100%.
|
|
732
|
+
* Only combos without a color, or still holding the color this method gave them, are recolored, so
|
|
733
|
+
* hand-applied colors stay. Colors this method set are removed from combos that are no longer in
|
|
734
|
+
* `byCombo` (deselected, dead, or no equity).
|
|
735
|
+
* @param {Object} hands - Hands object
|
|
736
|
+
* @param {Object} byCombo - Hero's equity (%) against each villain combo, from calculateEquity
|
|
737
|
+
* @param {string[]} colors - Colors, one per band
|
|
738
|
+
* @param {Map<string, string>} autoColors - Colors set by earlier calls (combo -> color)
|
|
739
|
+
* @returns {{hands: Object, autoColors: Map<string, string>, changed: boolean}}
|
|
740
|
+
*/
|
|
741
|
+
applyEquityBands(hands, byCombo, colors, autoColors = new Map()) {
|
|
742
|
+
const next = new Map();
|
|
743
|
+
const newHands = { ...hands };
|
|
744
|
+
let changed = false;
|
|
745
|
+
const colorOf = (values) => (values || []).find(item => typeof item === 'string' && item.startsWith('hsl('));
|
|
746
|
+
const setColor = (h, color) => {
|
|
747
|
+
const kept = (newHands[h] || []).filter(item => !(typeof item === 'string' && item.startsWith('hsl(')));
|
|
748
|
+
newHands[h] = color ? [...kept, color] : kept;
|
|
749
|
+
changed = true;
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
if (colors && colors.length > 0) {
|
|
753
|
+
const bandWidth = 100 / colors.length;
|
|
754
|
+
Object.entries(byCombo || {}).forEach(([h, equity]) => {
|
|
755
|
+
if (equity === null || equity === undefined || Number.isNaN(equity)) return;
|
|
756
|
+
const current = colorOf(newHands[h]);
|
|
757
|
+
if (current && autoColors.get(h) !== current) return; // colored by hand
|
|
758
|
+
const color = colors[Math.min(colors.length - 1, Math.floor(equity / bandWidth))];
|
|
759
|
+
next.set(h, color);
|
|
760
|
+
if (current !== color) setColor(h, color);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Combos that lost their equity color (no longer villain hands, or no equity)
|
|
765
|
+
autoColors.forEach((color, h) => {
|
|
766
|
+
if (next.has(h) || !newHands[h]) return;
|
|
767
|
+
if (colorOf(newHands[h]) === color) setColor(h, null);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
return { hands: changed ? newHands : hands, autoColors: next, changed };
|
|
717
771
|
}
|
|
718
772
|
|
|
719
773
|
/**
|
|
@@ -29,6 +29,20 @@
|
|
|
29
29
|
return index + 1 + (split ? (index >= 5 ? 1 : 0) + (index >= 9 ? 1 : 0) : 0);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Hairline frame around each 3x3 block (subgrid): inset shadows on the block's edge cells, so cells stay
|
|
34
|
+
* flush with their neighbours and only the blocks are outlined.
|
|
35
|
+
*/
|
|
36
|
+
function blockFrame(row, col, split) {
|
|
37
|
+
if (!split) return '';
|
|
38
|
+
const shadows = [];
|
|
39
|
+
if (row === 0 || row === 5 || row === 9) shadows.push(`inset 0 var(--irongrid-border-width) 0 var(--irongrid-color-border)`);
|
|
40
|
+
if (row === 4 || row === 8 || row === 12) shadows.push(`inset 0 calc(-1 * var(--irongrid-border-width)) 0 var(--irongrid-color-border)`);
|
|
41
|
+
if (col === 0 || col === 5 || col === 9) shadows.push(`inset var(--irongrid-border-width) 0 0 var(--irongrid-color-border)`);
|
|
42
|
+
if (col === 4 || col === 8 || col === 12) shadows.push(`inset calc(-1 * var(--irongrid-border-width)) 0 0 var(--irongrid-color-border)`);
|
|
43
|
+
return shadows.length ? `box-shadow: ${shadows.join(', ')};` : '';
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
/** The grid cell under a point (or null), from the cells' own positions, so it works with any spacing. */
|
|
33
47
|
function cellAtPoint(gridElement, x, y) {
|
|
34
48
|
const cells = gridElement.querySelectorAll('[data-row]');
|
|
@@ -431,7 +445,7 @@
|
|
|
431
445
|
<button
|
|
432
446
|
type="button"
|
|
433
447
|
class="grid-cell {isDragging ? 'dragging' : ''} {interactive ? '' : showTips ? 'non-interactive show-tips' : 'non-interactive'}"
|
|
434
|
-
style="background: {gradients[cellInfo.hand]} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)};"
|
|
448
|
+
style="background: {gradients[cellInfo.hand]} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)}; {blockFrame(rowIndex, colIndex, subgrid)}"
|
|
435
449
|
data-row={rowIndex}
|
|
436
450
|
data-col={colIndex}
|
|
437
451
|
onclick={(e) => handleClick(e, cellInfo)}
|
|
@@ -446,7 +460,7 @@
|
|
|
446
460
|
{:else}
|
|
447
461
|
<div
|
|
448
462
|
class="grid-cell non-interactive"
|
|
449
|
-
style="background: {gradients[cellInfo.hand]} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)};"
|
|
463
|
+
style="background: {gradients[cellInfo.hand]} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)}; {blockFrame(rowIndex, colIndex, subgrid)}"
|
|
450
464
|
data-row={rowIndex}
|
|
451
465
|
data-col={colIndex}
|
|
452
466
|
role="presentation"
|
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
* Without it, preflop equity is a Monte Carlo estimate.
|
|
38
38
|
*/
|
|
39
39
|
export let preflopTableUrl = null;
|
|
40
|
+
/**
|
|
41
|
+
* With `evaluator`, color the villain hands by hero's equity against each combo: one equal-width band per
|
|
42
|
+
* color (colors[0] = hero's lowest equity). Hands you color yourself keep their color.
|
|
43
|
+
*/
|
|
44
|
+
export let equityColors = true;
|
|
40
45
|
$: if (evaluator && preflopTableUrl) loadPreflopTable(preflopTableUrl);
|
|
41
46
|
/** Predefined ranges for range selection. Array of {name: string, abbreviation: string} */
|
|
42
47
|
export let ranges = [];
|
|
@@ -113,27 +118,49 @@
|
|
|
113
118
|
let lastEquityKey = '';
|
|
114
119
|
|
|
115
120
|
$: equityKey = evaluator
|
|
116
|
-
? JSON.stringify({ heroCards, boardCards, deadCards, hands: visibleHands, colors })
|
|
121
|
+
? JSON.stringify({ heroCards, boardCards, deadCards, hands: visibleHands, colors, equityColors })
|
|
117
122
|
: '';
|
|
118
123
|
$: if (equityKey !== lastEquityKey) {
|
|
119
124
|
lastEquityKey = equityKey;
|
|
120
125
|
scheduleEquity();
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
/** Colors set by equity banding (combo -> color), so they can be updated or removed later. */
|
|
129
|
+
let autoColors = new Map();
|
|
130
|
+
|
|
131
|
+
/** Apply (or, with no equity, remove) the equity band colors. */
|
|
132
|
+
function applyEquityColors(byCombo) {
|
|
133
|
+
if (!equityColors && autoColors.size === 0) return;
|
|
134
|
+
const result = manager.applyEquityBands(internalHands, equityColors ? byCombo || {} : {}, colors, autoColors);
|
|
135
|
+
autoColors = result.autoColors;
|
|
136
|
+
if (result.changed) {
|
|
137
|
+
internalHands = result.hands;
|
|
138
|
+
hands = result.hands;
|
|
139
|
+
dispatch('selectionChange', { hands: result.hands, visibleHands });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
function scheduleEquity() {
|
|
124
144
|
clearTimeout(equityTimer);
|
|
125
145
|
const request = ++equityRequest;
|
|
126
146
|
if (!evaluator || heroCards?.length !== 2) {
|
|
127
147
|
equity = null;
|
|
128
148
|
equityPending = false;
|
|
149
|
+
// Remove the band colors outside this reactive update (as results are applied, after it)
|
|
150
|
+
equityTimer = setTimeout(() => {
|
|
151
|
+
if (request === equityRequest) applyEquityColors(null);
|
|
152
|
+
}, 0);
|
|
129
153
|
return;
|
|
130
154
|
}
|
|
131
155
|
equityPending = true;
|
|
132
156
|
// A short debounce coalesces bursts (typing cards, dragging across the grid)
|
|
133
157
|
equityTimer = setTimeout(async () => {
|
|
134
158
|
try {
|
|
135
|
-
const result = await manager.calculateEquity(visibleHands, heroCards, boardCards, deadCards, colors);
|
|
136
|
-
if (request === equityRequest)
|
|
159
|
+
const result = await manager.calculateEquity(visibleHands, heroCards, boardCards, deadCards, colors, autoColors);
|
|
160
|
+
if (request === equityRequest) {
|
|
161
|
+
equity = result;
|
|
162
|
+
applyEquityColors(result?.byCombo);
|
|
163
|
+
}
|
|
137
164
|
} catch (error) {
|
|
138
165
|
if (request === equityRequest) equity = null;
|
|
139
166
|
} finally {
|
|
@@ -315,6 +342,7 @@
|
|
|
315
342
|
on:clearAll={() => {
|
|
316
343
|
internalHands = {};
|
|
317
344
|
hands = {};
|
|
345
|
+
autoColors = new Map();
|
|
318
346
|
}}
|
|
319
347
|
/>
|
|
320
348
|
</div>
|
|
@@ -19,13 +19,14 @@ type PokerGridPanel = SvelteComponent<{
|
|
|
19
19
|
showControls?: boolean;
|
|
20
20
|
showSummary?: boolean;
|
|
21
21
|
preflopTableUrl?: any;
|
|
22
|
+
equityColors?: boolean;
|
|
22
23
|
initialHands?: any;
|
|
23
24
|
initialBoard?: any;
|
|
24
25
|
initialDead?: any;
|
|
25
26
|
}, {
|
|
26
27
|
cellClick: CustomEvent<any>;
|
|
27
|
-
rangeSelected: CustomEvent<any>;
|
|
28
28
|
selectionChange: CustomEvent<any>;
|
|
29
|
+
rangeSelected: CustomEvent<any>;
|
|
29
30
|
deadCardsChange: CustomEvent<any>;
|
|
30
31
|
} & {
|
|
31
32
|
[evt: string]: CustomEvent<any>;
|
|
@@ -52,13 +53,14 @@ declare const PokerGridPanel: $$__sveltets_2_IsomorphicComponent<{
|
|
|
52
53
|
showControls?: boolean;
|
|
53
54
|
showSummary?: boolean;
|
|
54
55
|
preflopTableUrl?: any;
|
|
56
|
+
equityColors?: boolean;
|
|
55
57
|
initialHands?: any;
|
|
56
58
|
initialBoard?: any;
|
|
57
59
|
initialDead?: any;
|
|
58
60
|
}, {
|
|
59
61
|
cellClick: CustomEvent<any>;
|
|
60
|
-
rangeSelected: CustomEvent<any>;
|
|
61
62
|
selectionChange: CustomEvent<any>;
|
|
63
|
+
rangeSelected: CustomEvent<any>;
|
|
62
64
|
deadCardsChange: CustomEvent<any>;
|
|
63
65
|
} & {
|
|
64
66
|
[evt: string]: CustomEvent<any>;
|