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 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
- - Both come from one poker-bitvaljs `compareRangeHands` call: exact on the flop, turn and river
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 that are selected or colored; hands holding a dead, board or
248
- * hero card are left out.
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); null without two hero cards.
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 that are selected or colored; hands holding a dead, board or
658
- * hero card are left out.
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); null without two hero cards.
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
- function getGradient(hand) {
72
- return manager ? manager.getGradient(hand, hands, deadCards) : `linear-gradient(to right, ${defaultColor})`;
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
- $: handsLength = Object.keys(hands).length;
75
- $: handsString = JSON.stringify(hands);
76
- $: gradients = (() => {
109
+
110
+ $: looks = (() => {
77
111
  const result = {};
78
- try {
79
- const totalKeys = Object.keys(hands || {}).length;
80
- const selected = Object.keys(hands || {}).filter(k => Array.isArray(hands[k]) && hands[k].includes('selected')).length;
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] = getGradient(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
- style="background: {gradients[cellInfo.hand]} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)};"
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.display}</span>
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
- style="background: {gradients[cellInfo.hand]} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)};"
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.display}</span>
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, 1fr);
486
- gap: 0;
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, 6px) repeat(4, 1fr) var(--irongrid-subgrid-gap, 6px) repeat(4, 1fr);
492
- grid-template-rows: repeat(5, auto) var(--irongrid-subgrid-gap, 6px) repeat(4, auto) var(--irongrid-subgrid-gap, 6px) repeat(4, auto);
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
- transition: opacity var(--irongrid-transition-fast);
502
- min-height: var(--irongrid-grid-min-cell-height);
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:nth-child(13n) {
520
- border-right: none;
548
+ .grid-cell.ring-full {
549
+ box-shadow: inset 0 0 0 2px var(--irongrid-cell-ring-color, #1f2937);
521
550
  }
522
- .grid-cell:nth-child(n + 157) {
523
- border-bottom: none;
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-base);
538
- font-weight: var(--irongrid-font-weight-semibold);
539
- color: var(--irongrid-color-text-primary);
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
- @media (max-width: 768px) {
546
- .poker-grid-container {
547
- padding: var(--irongrid-spacing-sm);
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-xs);
584
+ font-size: var(--irongrid-cell-font-size-mobile, 0.6875rem);
579
585
  }
580
586
  }
581
587