poker-irongrid 1.3.0 → 2.0.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 -1
- package/dist/poker-grid/PokerGridManager.d.ts +82 -4
- package/dist/poker-grid/PokerGridManager.js +178 -8
- package/dist/poker-grid/components/Grid.svelte +75 -69
- package/dist/poker-grid/components/HandSelector.svelte +468 -406
- package/dist/poker-grid/components/HandSelector.svelte.d.ts +6 -2
- package/dist/poker-grid/components/PokerGridPanel.svelte +43 -4
- package/dist/poker-grid/components/PokerGridPanel.svelte.d.ts +4 -2
- package/dist/poker-grid/theme.css +13 -1
- 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
|
|
|
@@ -216,13 +217,32 @@ The component includes comprehensive filtering options:
|
|
|
216
217
|
- Visual distinction between colored and uncolored hands
|
|
217
218
|
- Color statistics display
|
|
218
219
|
|
|
220
|
+
### Design (2.0)
|
|
221
|
+
|
|
222
|
+
- **Grid:** small tiles with rounded corners and 2px gaps (`--irongrid-cell-gap`, `--irongrid-cell-radius`),
|
|
223
|
+
two-character labels (AK above the diagonal is suited, below is offsuit). Hands in the range are filled with
|
|
224
|
+
their color (lighter when only some combos are in); hands outside it get a faint suited / offsuit / pair tint
|
|
225
|
+
(`--irongrid-cell-suited-bg`, `--irongrid-cell-offsuit-bg`, `--irongrid-cell-paired-bg`) and grey text.
|
|
226
|
+
Selected colored hands get a ring (`--irongrid-cell-ring-color`). With `subgrid`, the 3x3 blocks are set apart.
|
|
227
|
+
- **Loading a range** (range select) fills a fresh grid with the first color; nothing is selected.
|
|
228
|
+
- **Controls** (`showControls`): color chips showing each color's share of the range (%), combos (#) or hero's
|
|
229
|
+
equity (Eq), and × to clear; the share selected with All / Invert; a **Made × Draw** table (made hand by draw:
|
|
230
|
+
none, straight, flush, combo, backdoor flush) or **Made × Equity** (hero's equity against the hands, in 20%
|
|
231
|
+
bands); rank filters; and a suit matrix with Suited / Offsuit. Draws must use a hole card (poker-extval with
|
|
232
|
+
`holeCards`). Clicking any table, rank or suit cell selects its hands (ctrl/cmd adds, shift removes); a color
|
|
233
|
+
chip then colors the selection.
|
|
234
|
+
|
|
219
235
|
### Equity Evaluation
|
|
220
236
|
|
|
221
237
|
When `evaluator={true}`, hero's equity is calculated live (asynchronously) whenever the hero cards,
|
|
222
238
|
board, dead cards or the hands on the grid change; there is no button to press.
|
|
223
239
|
- The summary shows hero vs. villain equity against every selected or colored combo
|
|
224
240
|
- The color buttons' **Eq** mode shows hero's equity against each color's hands
|
|
225
|
-
-
|
|
241
|
+
- With `equityColors` (default), the villain hands are colored by hero's equity against each combo, in
|
|
242
|
+
one equal-width band per color (6 colors: 0–16.7%, 16.7–33.3%, … 83.3–100%, `colors[0]` lowest), so the
|
|
243
|
+
color buttons show each band's count, share and equity. Hands you color yourself keep their color; band
|
|
244
|
+
colors follow the selection and go away without hero cards
|
|
245
|
+
- All of it comes from one poker-bitvaljs `compareRangeHands` call: exact on the flop, turn and river
|
|
226
246
|
(a few ms), Monte Carlo preflop and on 1-2 card boards
|
|
227
247
|
- Pass `preflopTableUrl` for exact, near-instant preflop equity (the table is ~1.1 MB, fetched once):
|
|
228
248
|
|
|
@@ -226,6 +226,52 @@ export class PokerGridManager {
|
|
|
226
226
|
* @returns {Promise<Object>} Matching hands
|
|
227
227
|
*/
|
|
228
228
|
getHandsByDraw(targetDraw: string, hands?: any, boardCards?: string[], boardLength?: number): Promise<any>;
|
|
229
|
+
/** The color of a hand's values (its hsl entry), or null. */
|
|
230
|
+
colorOf(values: any): any;
|
|
231
|
+
/**
|
|
232
|
+
* Combos in the range: those with a color or selected (dead combos are expected to be filtered out already).
|
|
233
|
+
* @param {Object} hands - Hands object (visible hands)
|
|
234
|
+
* @returns {string[]}
|
|
235
|
+
*/
|
|
236
|
+
getRangeCombos(hands?: any): string[];
|
|
237
|
+
/**
|
|
238
|
+
* Select combos with the usual modifiers: plain replaces the selection, ctrl/cmd adds, shift removes.
|
|
239
|
+
* @param {Object} hands - Hands object
|
|
240
|
+
* @param {string[]} combos - Combos to select
|
|
241
|
+
* @param {{ctrl?: boolean, shift?: boolean}} modifiers
|
|
242
|
+
* @returns {Object} Updated hands object
|
|
243
|
+
*/
|
|
244
|
+
selectCombos(hands: any, combos: string[], modifiers?: {
|
|
245
|
+
ctrl?: boolean;
|
|
246
|
+
shift?: boolean;
|
|
247
|
+
}): any;
|
|
248
|
+
/**
|
|
249
|
+
* Invert the selection within the range.
|
|
250
|
+
* @param {Object} hands - Hands object
|
|
251
|
+
* @returns {Object} Updated hands object
|
|
252
|
+
*/
|
|
253
|
+
invertRangeSelection(hands: any): any;
|
|
254
|
+
/**
|
|
255
|
+
* Classify combos on a board for the Made x Draw table: made hand (row) and draw (column), from
|
|
256
|
+
* poker-extval with the hole cards, so draws must use a hole card. Cached per board.
|
|
257
|
+
* Made: highCard, onePair, twoPair, trips, straight, flush, fullHouse, quads, straightFlush.
|
|
258
|
+
* Draw (flop/turn only): combo (flush + straight draw), flush, straight, backdoor (backdoor flush draw,
|
|
259
|
+
* flop only), none.
|
|
260
|
+
* @param {string[]} combos - Combos
|
|
261
|
+
* @param {string[]} boardCards - Board (3-5 cards)
|
|
262
|
+
* @returns {Map<string, {made: string, draw: string}>}
|
|
263
|
+
*/
|
|
264
|
+
classifyCombos(combos: string[], boardCards?: string[]): Map<string, {
|
|
265
|
+
made: string;
|
|
266
|
+
draw: string;
|
|
267
|
+
}>;
|
|
268
|
+
classifyCacheBoard: any;
|
|
269
|
+
classifyCache: Map<any, any>;
|
|
270
|
+
/** Uncached classifyCombos for one combo, or null if the cards clash. */
|
|
271
|
+
classifyCombo(combo: any, boardCards: any): {
|
|
272
|
+
made: string;
|
|
273
|
+
draw: string;
|
|
274
|
+
};
|
|
229
275
|
/**
|
|
230
276
|
* Apply color to hands.
|
|
231
277
|
* @param {Object} hands - Current hands object
|
|
@@ -244,27 +290,59 @@ export class PokerGridManager {
|
|
|
244
290
|
/**
|
|
245
291
|
* Hero's equity against the villain hands on the grid, overall and per color, from one
|
|
246
292
|
* poker-bitvaljs compareRangeHands call (each villain combo's result against hero's hand).
|
|
247
|
-
* Villain hands are the combos
|
|
248
|
-
*
|
|
293
|
+
* Villain hands are the combos in the range (colored or selected); hands holding a dead, board or hero
|
|
294
|
+
* card are left out.
|
|
249
295
|
* @param {Object} hands - Hands object
|
|
250
296
|
* @param {string[]} heroCards - Hero cards (two)
|
|
251
297
|
* @param {string[]} boardCards - Board cards (0-5)
|
|
252
298
|
* @param {string[]} deadCards - Dead cards (may include hero and board cards)
|
|
253
299
|
* @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);
|
|
300
|
+
* @returns {Promise<{hero: number|null, villain: number|null, combos: number, byColor: Object, byCombo: Object}|null>}
|
|
301
|
+
* Equities in percent (null when hero meets no villain combo); byCombo is hero's equity against each
|
|
302
|
+
* villain combo. Null without two hero cards.
|
|
256
303
|
*/
|
|
257
304
|
calculateEquity(hands?: any, heroCards?: string[], boardCards?: string[], deadCards?: string[], colors?: string[]): Promise<{
|
|
258
305
|
hero: number | null;
|
|
259
306
|
villain: number | null;
|
|
260
307
|
combos: number;
|
|
261
308
|
byColor: any;
|
|
309
|
+
byCombo: any;
|
|
262
310
|
} | null>;
|
|
311
|
+
/**
|
|
312
|
+
* Color villain combos by hero's equity against them: one equal-width band per color, colors[0] for
|
|
313
|
+
* hero's lowest equity (villain's strongest hands) up to the last color for hero's highest. With 6
|
|
314
|
+
* colors the bands are 0-16.7%, 16.7-33.3%, ... 83.3-100%.
|
|
315
|
+
* Only combos without a color, still holding the color this method gave them, or holding their base
|
|
316
|
+
* color (e.g. from loading a range) are recolored, so hand-applied colors stay. Combos this method colored
|
|
317
|
+
* that are no longer in `byCombo` (dead, or no equity) go back to their base color, or lose the color.
|
|
318
|
+
* @param {Object} hands - Hands object
|
|
319
|
+
* @param {Object} byCombo - Hero's equity (%) against each villain combo, from calculateEquity
|
|
320
|
+
* @param {string[]} colors - Colors, one per band
|
|
321
|
+
* @param {Map<string, string>} autoColors - Colors set by earlier calls (combo -> color)
|
|
322
|
+
* @param {Map<string, string>} baseColors - Colors combos had before banding (combo -> color)
|
|
323
|
+
* @returns {{hands: Object, autoColors: Map<string, string>, changed: boolean}}
|
|
324
|
+
*/
|
|
325
|
+
applyEquityBands(hands: any, byCombo: any, colors: string[], autoColors?: Map<string, string>, baseColors?: Map<string, string>): {
|
|
326
|
+
hands: any;
|
|
327
|
+
autoColors: Map<string, string>;
|
|
328
|
+
changed: boolean;
|
|
329
|
+
};
|
|
263
330
|
/**
|
|
264
331
|
* Hero's equity against each color's hands, rounded (0 when a color has no hands).
|
|
265
332
|
* @returns {Promise<Object>} Color equities object
|
|
266
333
|
*/
|
|
267
334
|
calculateColorEquities(hands?: any, heroCards?: any, boardCards?: any, deadCards?: any, colors?: any): Promise<any>;
|
|
335
|
+
/**
|
|
336
|
+
* Load a predefined range as a fresh grid: every combo gets `color` and nothing is selected.
|
|
337
|
+
* @param {string} rangeName - Range name
|
|
338
|
+
* @param {Array} ranges - Ranges array
|
|
339
|
+
* @param {string} color - Color for the range's combos
|
|
340
|
+
* @returns {{hands: Object, rangeName: string|null}}
|
|
341
|
+
*/
|
|
342
|
+
loadRange(rangeName: string, ranges: any[], color: string): {
|
|
343
|
+
hands: any;
|
|
344
|
+
rangeName: string | null;
|
|
345
|
+
};
|
|
268
346
|
/**
|
|
269
347
|
* Apply predefined range.
|
|
270
348
|
* @param {string} rangeName - Range name
|
|
@@ -617,6 +617,112 @@ export class PokerGridManager {
|
|
|
617
617
|
return this.rangeManagerToMatching(drawOnlyRm, hands);
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
// ==================== Range Views (design) ====================
|
|
621
|
+
|
|
622
|
+
/** The color of a hand's values (its hsl entry), or null. */
|
|
623
|
+
colorOf(values) {
|
|
624
|
+
return (values || []).find(item => typeof item === 'string' && item.startsWith('hsl(')) || null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Combos in the range: those with a color or selected (dead combos are expected to be filtered out already).
|
|
629
|
+
* @param {Object} hands - Hands object (visible hands)
|
|
630
|
+
* @returns {string[]}
|
|
631
|
+
*/
|
|
632
|
+
getRangeCombos(hands = this.hands) {
|
|
633
|
+
return Object.keys(hands || {}).filter(h => isCombo(h) && (this.hasExplicitColor(hands[h]) || this.isSelected(hands[h])));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Select combos with the usual modifiers: plain replaces the selection, ctrl/cmd adds, shift removes.
|
|
638
|
+
* @param {Object} hands - Hands object
|
|
639
|
+
* @param {string[]} combos - Combos to select
|
|
640
|
+
* @param {{ctrl?: boolean, shift?: boolean}} modifiers
|
|
641
|
+
* @returns {Object} Updated hands object
|
|
642
|
+
*/
|
|
643
|
+
selectCombos(hands, combos, modifiers = {}) {
|
|
644
|
+
const newHands = modifiers.ctrl || modifiers.shift ? { ...hands } : this.unselectAll(hands);
|
|
645
|
+
combos.forEach(h => {
|
|
646
|
+
const values = (newHands[h] || []).filter(item => item !== 'selected');
|
|
647
|
+
newHands[h] = modifiers.shift ? values : [...values, 'selected'];
|
|
648
|
+
});
|
|
649
|
+
return newHands;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Invert the selection within the range.
|
|
654
|
+
* @param {Object} hands - Hands object
|
|
655
|
+
* @returns {Object} Updated hands object
|
|
656
|
+
*/
|
|
657
|
+
invertRangeSelection(hands) {
|
|
658
|
+
const newHands = { ...hands };
|
|
659
|
+
this.getRangeCombos(hands).forEach(h => {
|
|
660
|
+
const values = newHands[h] || [];
|
|
661
|
+
newHands[h] = values.includes('selected') ? values.filter(item => item !== 'selected') : [...values, 'selected'];
|
|
662
|
+
});
|
|
663
|
+
return newHands;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Classify combos on a board for the Made x Draw table: made hand (row) and draw (column), from
|
|
668
|
+
* poker-extval with the hole cards, so draws must use a hole card. Cached per board.
|
|
669
|
+
* Made: highCard, onePair, twoPair, trips, straight, flush, fullHouse, quads, straightFlush.
|
|
670
|
+
* Draw (flop/turn only): combo (flush + straight draw), flush, straight, backdoor (backdoor flush draw,
|
|
671
|
+
* flop only), none.
|
|
672
|
+
* @param {string[]} combos - Combos
|
|
673
|
+
* @param {string[]} boardCards - Board (3-5 cards)
|
|
674
|
+
* @returns {Map<string, {made: string, draw: string}>}
|
|
675
|
+
*/
|
|
676
|
+
classifyCombos(combos, boardCards = this.boardCards) {
|
|
677
|
+
const result = new Map();
|
|
678
|
+
if (!boardCards || boardCards.length < 3) return result;
|
|
679
|
+
const key = boardCards.join('');
|
|
680
|
+
if (this.classifyCacheBoard !== key) {
|
|
681
|
+
this.classifyCacheBoard = key;
|
|
682
|
+
this.classifyCache = new Map();
|
|
683
|
+
}
|
|
684
|
+
combos.forEach(h => {
|
|
685
|
+
if (!this.classifyCache.has(h)) {
|
|
686
|
+
this.classifyCache.set(h, this.classifyCombo(h, boardCards));
|
|
687
|
+
}
|
|
688
|
+
const value = this.classifyCache.get(h);
|
|
689
|
+
if (value) result.set(h, value);
|
|
690
|
+
});
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Uncached classifyCombos for one combo, or null if the cards clash. */
|
|
695
|
+
classifyCombo(combo, boardCards) {
|
|
696
|
+
const hole = [combo.substring(0, 2), combo.substring(2, 4)];
|
|
697
|
+
if (hole.some(card => boardCards.includes(card))) return null;
|
|
698
|
+
try {
|
|
699
|
+
const evaluation = Object.values(evaluateHand([...boardCards, ...hole], { holeCards: hole }))[0];
|
|
700
|
+
if (!evaluation) return null;
|
|
701
|
+
let made = 'highCard';
|
|
702
|
+
if (evaluation.isStraightFlush || evaluation.isRoyalFlush) made = 'straightFlush';
|
|
703
|
+
else if (evaluation.isFourOfAKind) made = 'quads';
|
|
704
|
+
else if (evaluation.isFullHouse) made = 'fullHouse';
|
|
705
|
+
else if (evaluation.isFlush) made = 'flush';
|
|
706
|
+
else if (evaluation.isStraight) made = 'straight';
|
|
707
|
+
else if (evaluation.isThreeOfAKind) made = 'trips';
|
|
708
|
+
else if (evaluation.isTwoPair) made = 'twoPair';
|
|
709
|
+
else if (evaluation.isPair) made = 'onePair';
|
|
710
|
+
|
|
711
|
+
let draw = 'none';
|
|
712
|
+
if (boardCards.length < 5) {
|
|
713
|
+
const flushDraw = !!evaluation.isFlushDraw;
|
|
714
|
+
const straightDraw = !!evaluation.isStraightDraw;
|
|
715
|
+
if (flushDraw && straightDraw) draw = 'combo';
|
|
716
|
+
else if (flushDraw) draw = 'flush';
|
|
717
|
+
else if (straightDraw) draw = 'straight';
|
|
718
|
+
else if (boardCards.length === 3 && evaluation.isBackdoorFlushDraw) draw = 'backdoor';
|
|
719
|
+
}
|
|
720
|
+
return { made, draw };
|
|
721
|
+
} catch (error) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
620
726
|
// ==================== Color Methods ====================
|
|
621
727
|
|
|
622
728
|
/**
|
|
@@ -654,15 +760,16 @@ export class PokerGridManager {
|
|
|
654
760
|
/**
|
|
655
761
|
* Hero's equity against the villain hands on the grid, overall and per color, from one
|
|
656
762
|
* poker-bitvaljs compareRangeHands call (each villain combo's result against hero's hand).
|
|
657
|
-
* Villain hands are the combos
|
|
658
|
-
*
|
|
763
|
+
* Villain hands are the combos in the range (colored or selected); hands holding a dead, board or hero
|
|
764
|
+
* card are left out.
|
|
659
765
|
* @param {Object} hands - Hands object
|
|
660
766
|
* @param {string[]} heroCards - Hero cards (two)
|
|
661
767
|
* @param {string[]} boardCards - Board cards (0-5)
|
|
662
768
|
* @param {string[]} deadCards - Dead cards (may include hero and board cards)
|
|
663
769
|
* @param {string[]} colors - Colors array
|
|
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);
|
|
770
|
+
* @returns {Promise<{hero: number|null, villain: number|null, combos: number, byColor: Object, byCombo: Object}|null>}
|
|
771
|
+
* Equities in percent (null when hero meets no villain combo); byCombo is hero's equity against each
|
|
772
|
+
* villain combo. Null without two hero cards.
|
|
666
773
|
*/
|
|
667
774
|
async calculateEquity(hands = this.hands, heroCards = this.heroCards, boardCards = this.boardCards, deadCards = this.deadCards, colors = this.colors) {
|
|
668
775
|
if (heroCards?.length !== 2) return null;
|
|
@@ -678,7 +785,7 @@ export class PokerGridManager {
|
|
|
678
785
|
|
|
679
786
|
const byColor = {};
|
|
680
787
|
(colors || []).forEach(c => { byColor[c] = null; });
|
|
681
|
-
if (villainHands.length === 0) return { hero: null, villain: null, combos: 0, byColor };
|
|
788
|
+
if (villainHands.length === 0) return { hero: null, villain: null, combos: 0, byColor, byCombo: {} };
|
|
682
789
|
|
|
683
790
|
// Preflop, wait for the table if one is loading (otherwise the result would be Monte Carlo)
|
|
684
791
|
if (board.length === 0 && preflopTableLoad) await preflopTableLoad;
|
|
@@ -708,12 +815,60 @@ export class PokerGridManager {
|
|
|
708
815
|
};
|
|
709
816
|
|
|
710
817
|
const hero = heroEquityAgainst(villainHands);
|
|
818
|
+
const byCombo = {};
|
|
819
|
+
villainHands.forEach(h => { byCombo[h] = heroEquityAgainst([h]); });
|
|
711
820
|
(colors || []).forEach(color => {
|
|
712
821
|
const colorHands = villainHands.filter(h => hands[h].includes(color));
|
|
713
822
|
byColor[color] = colorHands.length > 0 ? heroEquityAgainst(colorHands) : null;
|
|
714
823
|
});
|
|
715
824
|
|
|
716
|
-
return { hero, villain: hero === null ? null : 100 - hero, combos: villainHands.length, byColor };
|
|
825
|
+
return { hero, villain: hero === null ? null : 100 - hero, combos: villainHands.length, byColor, byCombo };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Color villain combos by hero's equity against them: one equal-width band per color, colors[0] for
|
|
830
|
+
* hero's lowest equity (villain's strongest hands) up to the last color for hero's highest. With 6
|
|
831
|
+
* colors the bands are 0-16.7%, 16.7-33.3%, ... 83.3-100%.
|
|
832
|
+
* Only combos without a color, still holding the color this method gave them, or holding their base
|
|
833
|
+
* color (e.g. from loading a range) are recolored, so hand-applied colors stay. Combos this method colored
|
|
834
|
+
* that are no longer in `byCombo` (dead, or no equity) go back to their base color, or lose the color.
|
|
835
|
+
* @param {Object} hands - Hands object
|
|
836
|
+
* @param {Object} byCombo - Hero's equity (%) against each villain combo, from calculateEquity
|
|
837
|
+
* @param {string[]} colors - Colors, one per band
|
|
838
|
+
* @param {Map<string, string>} autoColors - Colors set by earlier calls (combo -> color)
|
|
839
|
+
* @param {Map<string, string>} baseColors - Colors combos had before banding (combo -> color)
|
|
840
|
+
* @returns {{hands: Object, autoColors: Map<string, string>, changed: boolean}}
|
|
841
|
+
*/
|
|
842
|
+
applyEquityBands(hands, byCombo, colors, autoColors = new Map(), baseColors = new Map()) {
|
|
843
|
+
const next = new Map();
|
|
844
|
+
const newHands = { ...hands };
|
|
845
|
+
let changed = false;
|
|
846
|
+
const colorOf = (values) => (values || []).find(item => typeof item === 'string' && item.startsWith('hsl('));
|
|
847
|
+
const setColor = (h, color) => {
|
|
848
|
+
const kept = (newHands[h] || []).filter(item => !(typeof item === 'string' && item.startsWith('hsl(')));
|
|
849
|
+
newHands[h] = color ? [...kept, color] : kept;
|
|
850
|
+
changed = true;
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
if (colors && colors.length > 0) {
|
|
854
|
+
const bandWidth = 100 / colors.length;
|
|
855
|
+
Object.entries(byCombo || {}).forEach(([h, equity]) => {
|
|
856
|
+
if (equity === null || equity === undefined || Number.isNaN(equity)) return;
|
|
857
|
+
const current = colorOf(newHands[h]);
|
|
858
|
+
if (current && autoColors.get(h) !== current && baseColors.get(h) !== current) return; // colored by hand
|
|
859
|
+
const color = colors[Math.min(colors.length - 1, Math.floor(equity / bandWidth))];
|
|
860
|
+
next.set(h, color);
|
|
861
|
+
if (current !== color) setColor(h, color);
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Combos that lost their equity color (no longer villain hands, or no equity)
|
|
866
|
+
autoColors.forEach((color, h) => {
|
|
867
|
+
if (next.has(h) || !newHands[h]) return;
|
|
868
|
+
if (colorOf(newHands[h]) === color) setColor(h, baseColors.get(h) || null);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
return { hands: changed ? newHands : hands, autoColors: next, changed };
|
|
717
872
|
}
|
|
718
873
|
|
|
719
874
|
/**
|
|
@@ -729,6 +884,21 @@ export class PokerGridManager {
|
|
|
729
884
|
|
|
730
885
|
// ==================== Range Methods ====================
|
|
731
886
|
|
|
887
|
+
/**
|
|
888
|
+
* Load a predefined range as a fresh grid: every combo gets `color` and nothing is selected.
|
|
889
|
+
* @param {string} rangeName - Range name
|
|
890
|
+
* @param {Array} ranges - Ranges array
|
|
891
|
+
* @param {string} color - Color for the range's combos
|
|
892
|
+
* @returns {{hands: Object, rangeName: string|null}}
|
|
893
|
+
*/
|
|
894
|
+
loadRange(rangeName, ranges, color) {
|
|
895
|
+
const selectedRange = ranges.find(r => r.name === rangeName);
|
|
896
|
+
if (!selectedRange) return { hands: {}, rangeName: null };
|
|
897
|
+
const hands = {};
|
|
898
|
+
new RangeManager(selectedRange.abbreviation).toArray().forEach(h => { hands[h] = [color]; });
|
|
899
|
+
return { hands, rangeName };
|
|
900
|
+
}
|
|
901
|
+
|
|
732
902
|
/**
|
|
733
903
|
* Apply predefined range.
|
|
734
904
|
* @param {string} rangeName - Range name
|
|
@@ -1033,8 +1203,8 @@ export class PokerGridManager {
|
|
|
1033
1203
|
newSelection[h] = (newSelection[h] || []).filter(item => item !== 'selected');
|
|
1034
1204
|
});
|
|
1035
1205
|
} else {
|
|
1036
|
-
// Normal: Apply color to selected hands
|
|
1037
|
-
newSelection = this.applyColor(newSelection, color, selectedHands);
|
|
1206
|
+
// Normal: Apply color to selected hands, then clear the selection
|
|
1207
|
+
newSelection = this.unselectAll(this.applyColor(newSelection, color, selectedHands));
|
|
1038
1208
|
}
|
|
1039
1209
|
|
|
1040
1210
|
return newSelection;
|
|
@@ -68,25 +68,52 @@
|
|
|
68
68
|
return manager ? manager.getCellInfo(rowIndex, colIndex) : { hand: '', display: '', type: '' };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* How a cell looks: in range (combos colored or selected), the range color, lighter when only some of the
|
|
73
|
+
* hand's live combos are in the range; out of range, a faint suited/offsuit/pair tint. Selected colored
|
|
74
|
+
* combos get a ring.
|
|
75
|
+
*/
|
|
76
|
+
/** Out-of-range tints (theme variables, with defaults). */
|
|
77
|
+
const TINTS = {
|
|
78
|
+
suited: 'var(--irongrid-cell-suited-bg, #eef0f9)',
|
|
79
|
+
offsuit: 'var(--irongrid-cell-offsuit-bg, #f6f3ee)',
|
|
80
|
+
paired: 'var(--irongrid-cell-paired-bg, #f1f2f4)'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function cellLook(hand, type, handsMap, dead) {
|
|
84
|
+
const combos = getCombinations(hand);
|
|
85
|
+
const live = combos.filter(c => !dead.includes(c.substring(0, 2)) && !dead.includes(c.substring(2, 4)));
|
|
86
|
+
const counts = new Map();
|
|
87
|
+
let inRange = 0;
|
|
88
|
+
let selected = 0;
|
|
89
|
+
let coloredSelected = 0;
|
|
90
|
+
live.forEach(c => {
|
|
91
|
+
const values = handsMap[c] || [];
|
|
92
|
+
const color = manager ? manager.colorOf(values) : null;
|
|
93
|
+
const isSelected = values.includes('selected');
|
|
94
|
+
if (!color && !isSelected) return;
|
|
95
|
+
inRange++;
|
|
96
|
+
if (isSelected) selected++;
|
|
97
|
+
if (color && isSelected) coloredSelected++;
|
|
98
|
+
const key = color || defaultSelectedColor;
|
|
99
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
100
|
+
});
|
|
101
|
+
if (live.length === 0) return { background: TINTS[type], inRange: false, dead: true, ring: '' };
|
|
102
|
+
if (inRange === 0) return { background: TINTS[type], inRange: false, dead: false, ring: '' };
|
|
103
|
+
let dominant = null;
|
|
104
|
+
counts.forEach((n, color) => { if (!dominant || n > counts.get(dominant)) dominant = color; });
|
|
105
|
+
const strength = Math.round(45 + 55 * (inRange / live.length));
|
|
106
|
+
const ring = coloredSelected === 0 ? '' : selected === inRange ? 'full' : 'partial';
|
|
107
|
+
return { background: `color-mix(in srgb, ${dominant} ${strength}%, white)`, inRange: true, dead: false, ring };
|
|
73
108
|
}
|
|
74
|
-
|
|
75
|
-
$:
|
|
76
|
-
$: gradients = (() => {
|
|
109
|
+
|
|
110
|
+
$: looks = (() => {
|
|
77
111
|
const result = {};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
void totalKeys;
|
|
82
|
-
void selected;
|
|
83
|
-
} catch (e) {
|
|
84
|
-
// Ignore gradient recompute errors
|
|
85
|
-
}
|
|
86
|
-
ranks.forEach((rowRank, rowIndex) => {
|
|
87
|
-
ranks.forEach((colRank, colIndex) => {
|
|
112
|
+
const dead = deadCards || [];
|
|
113
|
+
ranks.forEach((_, rowIndex) => {
|
|
114
|
+
ranks.forEach((__, colIndex) => {
|
|
88
115
|
const cellInfo = getCellInfo(rowIndex, colIndex);
|
|
89
|
-
result[cellInfo.hand] =
|
|
116
|
+
result[cellInfo.hand] = cellLook(cellInfo.hand, cellInfo.type, hands || {}, dead);
|
|
90
117
|
});
|
|
91
118
|
});
|
|
92
119
|
return result;
|
|
@@ -431,7 +458,11 @@
|
|
|
431
458
|
<button
|
|
432
459
|
type="button"
|
|
433
460
|
class="grid-cell {isDragging ? 'dragging' : ''} {interactive ? '' : showTips ? 'non-interactive show-tips' : 'non-interactive'}"
|
|
434
|
-
|
|
461
|
+
class:in-range={looks[cellInfo.hand].inRange}
|
|
462
|
+
class:dead={looks[cellInfo.hand].dead}
|
|
463
|
+
class:ring-full={looks[cellInfo.hand].ring === 'full'}
|
|
464
|
+
class:ring-partial={looks[cellInfo.hand].ring === 'partial'}
|
|
465
|
+
style="background: {looks[cellInfo.hand].background} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)};"
|
|
435
466
|
data-row={rowIndex}
|
|
436
467
|
data-col={colIndex}
|
|
437
468
|
onclick={(e) => handleClick(e, cellInfo)}
|
|
@@ -441,18 +472,20 @@
|
|
|
441
472
|
onkeydown={(e) => handleKeyDown(e, cellInfo)}
|
|
442
473
|
title={cellInfo.display}
|
|
443
474
|
>
|
|
444
|
-
<span class="cell-text">{cellInfo.
|
|
475
|
+
<span class="cell-text">{cellInfo.hand.substring(0, 2)}</span>
|
|
445
476
|
</button>
|
|
446
477
|
{:else}
|
|
447
478
|
<div
|
|
448
479
|
class="grid-cell non-interactive"
|
|
449
|
-
|
|
480
|
+
class:in-range={looks[cellInfo.hand].inRange}
|
|
481
|
+
class:dead={looks[cellInfo.hand].dead}
|
|
482
|
+
style="background: {looks[cellInfo.hand].background} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)};"
|
|
450
483
|
data-row={rowIndex}
|
|
451
484
|
data-col={colIndex}
|
|
452
485
|
role="presentation"
|
|
453
486
|
title={cellInfo.display}
|
|
454
487
|
>
|
|
455
|
-
<span class="cell-text">{cellInfo.
|
|
488
|
+
<span class="cell-text">{cellInfo.hand.substring(0, 2)}</span>
|
|
456
489
|
</div>
|
|
457
490
|
{/if}
|
|
458
491
|
{/each}
|
|
@@ -482,33 +515,29 @@
|
|
|
482
515
|
.poker-grid {
|
|
483
516
|
display: grid;
|
|
484
517
|
grid-template-columns: repeat(13, 1fr);
|
|
485
|
-
grid-template-rows: repeat(13,
|
|
486
|
-
gap:
|
|
487
|
-
border: var(--irongrid-border-width) solid var(--irongrid-color-border);
|
|
518
|
+
grid-template-rows: repeat(13, auto);
|
|
519
|
+
gap: var(--irongrid-cell-gap, 2px);
|
|
488
520
|
}
|
|
489
521
|
/* 3x3 blocks: spacer tracks after T and 6 (A-T | 9-6 | 5-2) */
|
|
490
522
|
.poker-grid.subgrid {
|
|
491
|
-
grid-template-columns: repeat(5, 1fr) var(--irongrid-subgrid-gap,
|
|
492
|
-
grid-template-rows: repeat(5, auto) var(--irongrid-subgrid-gap,
|
|
493
|
-
border: none;
|
|
523
|
+
grid-template-columns: repeat(5, 1fr) var(--irongrid-subgrid-gap, 4px) repeat(4, 1fr) var(--irongrid-subgrid-gap, 4px) repeat(4, 1fr);
|
|
524
|
+
grid-template-rows: repeat(5, auto) var(--irongrid-subgrid-gap, 4px) repeat(4, auto) var(--irongrid-subgrid-gap, 4px) repeat(4, auto);
|
|
494
525
|
}
|
|
495
526
|
.grid-cell {
|
|
496
527
|
aspect-ratio: 1;
|
|
528
|
+
min-width: 0;
|
|
497
529
|
display: flex;
|
|
498
530
|
align-items: center;
|
|
499
531
|
justify-content: center;
|
|
500
532
|
cursor: pointer;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
border-right: var(--irongrid-border-width) solid var(--irongrid-color-border);
|
|
504
|
-
border-bottom: var(--irongrid-border-width) solid var(--irongrid-color-border);
|
|
533
|
+
border-radius: var(--irongrid-cell-radius, 3px);
|
|
534
|
+
transition: background-color var(--irongrid-transition-fast), box-shadow var(--irongrid-transition-fast);
|
|
505
535
|
user-select: none;
|
|
506
536
|
-webkit-user-select: none;
|
|
507
537
|
-moz-user-select: none;
|
|
508
538
|
-ms-user-select: none;
|
|
509
539
|
}
|
|
510
540
|
button.grid-cell {
|
|
511
|
-
background: none;
|
|
512
541
|
border: none;
|
|
513
542
|
padding: 0;
|
|
514
543
|
margin: 0;
|
|
@@ -516,11 +545,14 @@
|
|
|
516
545
|
color: inherit;
|
|
517
546
|
text-align: inherit;
|
|
518
547
|
}
|
|
519
|
-
.grid-cell
|
|
520
|
-
|
|
548
|
+
.grid-cell.ring-full {
|
|
549
|
+
box-shadow: inset 0 0 0 2px var(--irongrid-cell-ring-color, #1f2937);
|
|
521
550
|
}
|
|
522
|
-
.grid-cell
|
|
523
|
-
|
|
551
|
+
.grid-cell.ring-partial {
|
|
552
|
+
box-shadow: inset 0 0 0 1px var(--irongrid-cell-ring-color, #1f2937);
|
|
553
|
+
}
|
|
554
|
+
.grid-cell.dead {
|
|
555
|
+
opacity: 0.45;
|
|
524
556
|
}
|
|
525
557
|
.grid-cell.dragging {
|
|
526
558
|
cursor: crosshair;
|
|
@@ -534,48 +566,22 @@
|
|
|
534
566
|
pointer-events: auto;
|
|
535
567
|
}
|
|
536
568
|
.cell-text {
|
|
537
|
-
font-size: var(--irongrid-font-size
|
|
538
|
-
font-weight:
|
|
539
|
-
|
|
569
|
+
font-size: var(--irongrid-cell-font-size, 0.8125rem);
|
|
570
|
+
font-weight: 500;
|
|
571
|
+
letter-spacing: 0.01em;
|
|
572
|
+
color: var(--irongrid-cell-muted-text, #a3a8b3);
|
|
540
573
|
pointer-events: none;
|
|
541
574
|
text-rendering: optimizeLegibility;
|
|
542
575
|
-webkit-font-smoothing: antialiased;
|
|
543
576
|
-moz-osx-font-smoothing: grayscale;
|
|
544
577
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
.poker-grid {
|
|
550
|
-
gap: 0;
|
|
551
|
-
}
|
|
552
|
-
.grid-cell {
|
|
553
|
-
min-height: var(--irongrid-grid-min-cell-height-tablet);
|
|
554
|
-
}
|
|
555
|
-
.cell-text {
|
|
556
|
-
font-size: var(--irongrid-font-size-sm);
|
|
557
|
-
}
|
|
578
|
+
.grid-cell.in-range .cell-text {
|
|
579
|
+
font-weight: 700;
|
|
580
|
+
color: var(--irongrid-color-text-primary);
|
|
558
581
|
}
|
|
559
582
|
@media (max-width: 480px) {
|
|
560
|
-
.poker-grid-container {
|
|
561
|
-
padding: var(--irongrid-grid-container-padding);
|
|
562
|
-
}
|
|
563
|
-
.grid-cell {
|
|
564
|
-
min-height: var(--irongrid-grid-min-cell-height-mobile);
|
|
565
|
-
}
|
|
566
|
-
.cell-text {
|
|
567
|
-
font-size: var(--irongrid-font-size-xxs);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
@media (max-width: 375px) {
|
|
571
|
-
.poker-grid-container {
|
|
572
|
-
padding: var(--irongrid-grid-container-padding-small);
|
|
573
|
-
}
|
|
574
|
-
.grid-cell {
|
|
575
|
-
min-height: var(--irongrid-grid-min-cell-height-small);
|
|
576
|
-
}
|
|
577
583
|
.cell-text {
|
|
578
|
-
font-size: var(--irongrid-font-size-
|
|
584
|
+
font-size: var(--irongrid-cell-font-size-mobile, 0.6875rem);
|
|
579
585
|
}
|
|
580
586
|
}
|
|
581
587
|
|