@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.cjs CHANGED
@@ -1412,6 +1412,9 @@ function validateSpec(spec) {
1412
1412
  return;
1413
1413
  }
1414
1414
  seeId(b.id, `${p}.id`);
1415
+ if (b.explains && b.explains !== "freeSpins" && spec.facts?.modes?.length && !spec.facts.modes.some((m) => m.id === b.explains)) {
1416
+ add("warn", `${p}.explains`, "explains-unknown-mode", `explains "${b.explains}" matches no declared mode id (facts.modes)`);
1417
+ }
1415
1418
  if (b.kind === "select" && (!b.options || b.options.length === 0)) {
1416
1419
  add("error", `${p}.options`, "select-empty", "a select block needs at least one option");
1417
1420
  }
@@ -1764,14 +1767,29 @@ function factsVars(facts, extra) {
1764
1767
  return extra ? { ...vars, ...extra } : vars;
1765
1768
  }
1766
1769
  function scanBlocks(blocks, resolve = (s) => s) {
1767
- const scan = { text: "", headings: "", covers: /* @__PURE__ */ new Set(), hasModeStats: false, hasLegal: false };
1770
+ const scan = { text: "", headings: "", covers: /* @__PURE__ */ new Set(), hasModeStats: false, hasLegal: false, sections: [] };
1771
+ let section = { heading: "", text: "", explains: /* @__PURE__ */ new Set(), covers: /* @__PURE__ */ new Set() };
1772
+ scan.sections.push(section);
1768
1773
  const addText = (t) => {
1769
- if (typeof t === "string" && t) scan.text += `
1770
- ${resolve(t)}`;
1774
+ if (typeof t === "string" && t) {
1775
+ const r = resolve(t);
1776
+ scan.text += `
1777
+ ${r}`;
1778
+ section.text += `
1779
+ ${r}`;
1780
+ }
1771
1781
  };
1772
1782
  const walk = (list) => {
1773
1783
  for (const b of list ?? []) {
1774
- for (const c of b.covers ?? []) scan.covers.add(c);
1784
+ if (b.kind === "heading" || b.kind === "subheading") {
1785
+ section = { heading: resolve(b.text), text: "", explains: /* @__PURE__ */ new Set(), covers: /* @__PURE__ */ new Set() };
1786
+ scan.sections.push(section);
1787
+ }
1788
+ for (const c of b.covers ?? []) {
1789
+ scan.covers.add(c);
1790
+ section.covers.add(c);
1791
+ }
1792
+ if (b.explains) section.explains.add(b.explains);
1775
1793
  const anyB = b;
1776
1794
  switch (b.kind) {
1777
1795
  case "mode-stats":
@@ -1786,11 +1804,14 @@ ${resolve(t)}`;
1786
1804
  addText(b.text);
1787
1805
  break;
1788
1806
  case "heading":
1789
- case "subheading":
1807
+ case "subheading": {
1808
+ const r = resolve(b.text);
1790
1809
  scan.headings += `
1791
- ${resolve(b.text)}`;
1792
- addText(b.text);
1810
+ ${r}`;
1811
+ scan.text += `
1812
+ ${r}`;
1793
1813
  break;
1814
+ }
1794
1815
  case "text":
1795
1816
  addText(b.text);
1796
1817
  break;
@@ -1859,6 +1880,17 @@ function mentionsTimes(text, x) {
1859
1880
  return new RegExp(`\\b${grouped}${frac}\\s*[x\xD7]`, "i").test(text);
1860
1881
  }
1861
1882
  var mentionsName = (text, name) => text.toLowerCase().includes(name.toLowerCase());
1883
+ function mentionsCost(text, cost) {
1884
+ if (mentionsTimes(text, cost) || mentionsTimes(text, cost + 1)) return true;
1885
+ if (cost > 0 && cost < 1) return new RegExp(`\\b${Math.round(cost * 100)}\\s*%`).test(text);
1886
+ return false;
1887
+ }
1888
+ function findModeSection(sections, m) {
1889
+ return sections.find(
1890
+ (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)
1891
+ );
1892
+ }
1893
+ var MIN_SECTION_PROSE = 40;
1862
1894
  function auditRules(facts, rules, opts = {}) {
1863
1895
  const out = [];
1864
1896
  try {
@@ -1879,8 +1911,17 @@ function auditRules(facts, rules, opts = {}) {
1879
1911
  } else if (!(scan.hasModeStats || covered(`maxwin:${m.id}`) || nameIn && mentionsTimes(scan.text, m.maxWinX))) {
1880
1912
  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.`);
1881
1913
  }
1882
- if (m.kind && m.kind !== "base" && !(covered(`feature:${m.id}`) || nameIn)) {
1883
- add("required", "rules-missing-feature", `feature:${m.id}`, `The configured feature \u201C${m.name}\u201D is not described in the rules.`);
1914
+ if (!m.id) continue;
1915
+ const sec = findModeSection(scan.sections, m);
1916
+ if (!sec) {
1917
+ 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.`);
1918
+ } else {
1919
+ if (sec.text.trim().length < MIN_SECTION_PROSE) {
1920
+ 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.`);
1921
+ }
1922
+ if (m.kind && m.kind !== "base" && m.cost != null && !(sec.covers.has(`cost:${m.id}`) || mentionsCost(sec.text, m.cost))) {
1923
+ 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.`);
1924
+ }
1884
1925
  }
1885
1926
  }
1886
1927
  const fs = facts?.freeSpins;