@open-slot-ui/core 0.9.0 → 0.10.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/dist/index.d.cts CHANGED
@@ -1652,6 +1652,10 @@ interface ResponsiveOverride {
1652
1652
  type BlockSpec = {
1653
1653
  /** Rules-audit topics this block covers (see `auditRules`). */
1654
1654
  covers?: string[];
1655
+ /** The declared mode id this block EXPLAINS (e.g. `'bonus'`). Every declared mode
1656
+ * must have its own rules section — a heading naming it, or blocks tagged with
1657
+ * `explains` — with real prose; the audit enforces it (see `auditRules`). */
1658
+ explains?: string;
1655
1659
  } & ({
1656
1660
  kind: 'slider';
1657
1661
  id: string;
package/dist/index.d.ts CHANGED
@@ -1652,6 +1652,10 @@ interface ResponsiveOverride {
1652
1652
  type BlockSpec = {
1653
1653
  /** Rules-audit topics this block covers (see `auditRules`). */
1654
1654
  covers?: string[];
1655
+ /** The declared mode id this block EXPLAINS (e.g. `'bonus'`). Every declared mode
1656
+ * must have its own rules section — a heading naming it, or blocks tagged with
1657
+ * `explains` — with real prose; the audit enforces it (see `auditRules`). */
1658
+ explains?: string;
1655
1659
  } & ({
1656
1660
  kind: 'slider';
1657
1661
  id: string;
package/dist/index.js CHANGED
@@ -1410,6 +1410,9 @@ function validateSpec(spec) {
1410
1410
  return;
1411
1411
  }
1412
1412
  seeId(b.id, `${p}.id`);
1413
+ if (b.explains && b.explains !== "freeSpins" && spec.facts?.modes?.length && !spec.facts.modes.some((m) => m.id === b.explains)) {
1414
+ add("warn", `${p}.explains`, "explains-unknown-mode", `explains "${b.explains}" matches no declared mode id (facts.modes)`);
1415
+ }
1413
1416
  if (b.kind === "select" && (!b.options || b.options.length === 0)) {
1414
1417
  add("error", `${p}.options`, "select-empty", "a select block needs at least one option");
1415
1418
  }
@@ -1762,14 +1765,29 @@ function factsVars(facts, extra) {
1762
1765
  return extra ? { ...vars, ...extra } : vars;
1763
1766
  }
1764
1767
  function scanBlocks(blocks, resolve = (s) => s) {
1765
- const scan = { text: "", headings: "", covers: /* @__PURE__ */ new Set(), hasModeStats: false, hasLegal: false };
1768
+ const scan = { text: "", headings: "", covers: /* @__PURE__ */ new Set(), hasModeStats: false, hasLegal: false, sections: [] };
1769
+ let section = { heading: "", text: "", explains: /* @__PURE__ */ new Set(), covers: /* @__PURE__ */ new Set() };
1770
+ scan.sections.push(section);
1766
1771
  const addText = (t) => {
1767
- if (typeof t === "string" && t) scan.text += `
1768
- ${resolve(t)}`;
1772
+ if (typeof t === "string" && t) {
1773
+ const r = resolve(t);
1774
+ scan.text += `
1775
+ ${r}`;
1776
+ section.text += `
1777
+ ${r}`;
1778
+ }
1769
1779
  };
1770
1780
  const walk = (list) => {
1771
1781
  for (const b of list ?? []) {
1772
- for (const c of b.covers ?? []) scan.covers.add(c);
1782
+ if (b.kind === "heading" || b.kind === "subheading") {
1783
+ section = { heading: resolve(b.text), text: "", explains: /* @__PURE__ */ new Set(), covers: /* @__PURE__ */ new Set() };
1784
+ scan.sections.push(section);
1785
+ }
1786
+ for (const c of b.covers ?? []) {
1787
+ scan.covers.add(c);
1788
+ section.covers.add(c);
1789
+ }
1790
+ if (b.explains) section.explains.add(b.explains);
1773
1791
  const anyB = b;
1774
1792
  switch (b.kind) {
1775
1793
  case "mode-stats":
@@ -1784,11 +1802,14 @@ ${resolve(t)}`;
1784
1802
  addText(b.text);
1785
1803
  break;
1786
1804
  case "heading":
1787
- case "subheading":
1805
+ case "subheading": {
1806
+ const r = resolve(b.text);
1788
1807
  scan.headings += `
1789
- ${resolve(b.text)}`;
1790
- addText(b.text);
1808
+ ${r}`;
1809
+ scan.text += `
1810
+ ${r}`;
1791
1811
  break;
1812
+ }
1792
1813
  case "text":
1793
1814
  addText(b.text);
1794
1815
  break;
@@ -1857,6 +1878,17 @@ function mentionsTimes(text, x) {
1857
1878
  return new RegExp(`\\b${grouped}${frac}\\s*[x\xD7]`, "i").test(text);
1858
1879
  }
1859
1880
  var mentionsName = (text, name) => text.toLowerCase().includes(name.toLowerCase());
1881
+ function mentionsCost(text, cost) {
1882
+ if (mentionsTimes(text, cost) || mentionsTimes(text, cost + 1)) return true;
1883
+ if (cost > 0 && cost < 1) return new RegExp(`\\b${Math.round(cost * 100)}\\s*%`).test(text);
1884
+ return false;
1885
+ }
1886
+ function findModeSection(sections, m) {
1887
+ return sections.find(
1888
+ (s) => s.explains.has(m.id) || s.covers.has(`feature:${m.id}`) || s.heading !== "" && m.name !== "" && mentionsName(s.heading, m.name) || (m.kind === "base" || !m.kind) && /how to play|about the game/i.test(s.heading)
1889
+ );
1890
+ }
1891
+ var MIN_SECTION_PROSE = 40;
1860
1892
  function auditRules(facts, rules, opts = {}) {
1861
1893
  const out = [];
1862
1894
  try {
@@ -1877,8 +1909,17 @@ function auditRules(facts, rules, opts = {}) {
1877
1909
  } else if (!(scan.hasModeStats || covered(`maxwin:${m.id}`) || nameIn && mentionsTimes(scan.text, m.maxWinX))) {
1878
1910
  add("required", "rules-missing-maxwin", `maxwin:${m.id}`, `The max win of \u201C${m.name}\u201D (${formatTimes(m.maxWinX)}) is not stated in the rules.`);
1879
1911
  }
1880
- if (m.kind && m.kind !== "base" && !(covered(`feature:${m.id}`) || nameIn)) {
1881
- add("required", "rules-missing-feature", `feature:${m.id}`, `The configured feature \u201C${m.name}\u201D is not described in the rules.`);
1912
+ if (!m.id) continue;
1913
+ const sec = findModeSection(scan.sections, m);
1914
+ if (!sec) {
1915
+ add("required", "rules-missing-mode-section", `section:${m.id}`, `\u201C${m.name}\u201D has no rules section of its own \u2014 add a heading naming it (or tag its blocks with explains: "${m.id}") and explain how the mode plays.`);
1916
+ } else {
1917
+ if (sec.text.trim().length < MIN_SECTION_PROSE) {
1918
+ add("required", "rules-mode-section-empty", `section:${m.id}`, `The \u201C${m.name}\u201D section has a heading but no real explanation \u2014 describe how the mode actually plays.`);
1919
+ }
1920
+ if (m.kind && m.kind !== "base" && m.cost != null && !(sec.covers.has(`cost:${m.id}`) || mentionsCost(sec.text, m.cost))) {
1921
+ add("required", "rules-mode-missing-cost", `cost:${m.id}`, `State what \u201C${m.name}\u201D costs (${formatTimes(m.cost)} the bet \u2014 e.g. via {{cost.${m.id}}}) inside its own section.`);
1922
+ }
1882
1923
  }
1883
1924
  }
1884
1925
  const fs = facts?.freeSpins;