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 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
- - Both come from one poker-bitvaljs `compareRangeHands` call: exact on the flop, turn and river
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; hands holding a dead, board or
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
- * @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.
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; hands holding a dead, board or
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
- * @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.
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
- (this.hasExplicitColor(hands[h]) || this.isSelected(hands[h])) &&
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) equity = result;
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-irongrid",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/DarraghGeo/poker-irongrid.git"