SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%

lobby: 20 hand-composed game posters (symbol-svg mirrors reel art); crash engine + 10 risk game definitions certified; crash_rounds table

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 16 days ago (Sep 8, 2026) parent 51ee542

24 changed files +6,600 −55

modified apps/simulator/src/cli.ts +14 −3
@@ -10,8 +10,8 @@
10 10 import fs from "node:fs";
11 11 import path from "node:path";
12 12 import { fileURLToPath } from "node:url";
13 import { certify, formatCertification, validateDefinition, DEFAULT_CERTIFICATION_RULES } from "@spinza/game-core";
14 import { RAW_GAMES, GAMES } from "@spinza/games";
13 +import { certify, certifyCrash, formatCertification, simulateCrash, validateDefinition, DEFAULT_CERTIFICATION_RULES } from "@spinza/game-core";
14 +import { RAW_GAMES, GAMES, CRASH_GAMES, CRASH_BY_SLUG } from "@spinza/games";
15 15 import { calibrate, simulateParallel } from "./index";
16 16
17 17 const here = path.dirname(fileURLToPath(import.meta.url));
@@ -94,7 +94,18 @@ async function main() {
94 94 fs.mkdirSync(certDir, { recursive: true });
95 95 const spins = num("spins", 1_000_000);
96 96 let failed = 0;
97 for (const slug of targets(sel)) {
97 + const list = sel === "crash" ? CRASH_GAMES.map((g) => g.slug) : targets(sel);
98 + for (const slug of list) {
99 + const crashDef = CRASH_BY_SLUG.get(slug);
100 + if (crashDef) {
101 + console.log(`\nCertifying ${crashDef.name} (${slug}@${crashDef.version}, crash) — ${spins.toLocaleString("en-US")} rounds`);
102 + const res = simulateCrash(crashDef, { spins });
103 + const report = certifyCrash(crashDef, res, { ...DEFAULT_CERTIFICATION_RULES, minSpins: Math.min(DEFAULT_CERTIFICATION_RULES.minSpins, spins) });
104 + fs.writeFileSync(path.join(certDir, `${slug}.json`), JSON.stringify(report, null, 2) + "\n");
105 + console.log(formatCertification(report));
106 + if (report.status === "FAIL") failed++;
107 + continue;
108 + }
98 109 const g = GAMES.find((x) => x.slug === slug)!;
99 110 console.log(`\nCertifying ${g.name} (${slug}@${g.version}) payScale=${g.payScale} — ${spins.toLocaleString("en-US")} spins`);
100 111 const res = await simulateParallel(slug, { spins, payScale: g.payScale, threads, onProgress: progressBar });
modified apps/web/src/components/lobby/game-art.tsx +50 −50
@@ -94,7 +94,7 @@ const DIMS: Record<ArtVariant, { w: number; h: number }> = {
94 94 function focal(v: ArtVariant, W: number, H: number, showName: boolean): { cx: number; cy: number; R: number } {
95 95 switch (v) {
96 96 case "card":
97 return { cx: W / 2, cy: showName ? H * 0.41 : H * 0.5, R: W * 0.42 };
97 + return { cx: W / 2, cy: showName ? H * 0.385 : H * 0.5, R: W * 0.405 };
98 98 case "tile":
99 99 return { cx: W / 2, cy: H / 2, R: W * 0.4 };
100 100 case "hero":
@@ -212,7 +212,7 @@ function NeonVault(s: Stage) {
212 212 <Sym s={s} id="V" x={cx - R * 0.78} y={cy - R * 0.5} size={R * 0.4} rotate={-8} />
213 213 <rect x={f(cx - R * 1.1)} y={f(scanY - R * 0.05)} width={f(R * 2.2)} height={f(R * 0.1)} fill={`url(#${id}-nv3)`} />
214 214 <line x1={f(cx - R * 1.1)} y1={f(scanY)} x2={f(cx + R * 1.1)} y2={f(scanY)} stroke={p.secondary} strokeWidth={1.5} strokeOpacity={0.9} />
215 <Small x={cx} y={cy + R * 1.18} text="×2 ×3 ×5 ×8 ×12 ×20 ×30 ×50" color={p.primary} size={R * 0.075} tracking="0.18em" opacity={0.7} />
215 + <Small x={cx} y={cy + R * 1.0} text="×2 ×3 ×5 ×8 ×12 ×20 ×30 ×50" color={p.primary} size={R * 0.075} tracking="0.18em" opacity={0.7} />
216 216 </>
217 217 ) : null}
218 218 </g>
@@ -257,7 +257,7 @@ function CosmicCollapse(s: Stage) {
257 257 ? orbit.map((o) => <Sym key={o.id} s={s} id={o.id} x={cx + Math.cos(o.a) * R * o.d} y={cy + Math.sin(o.a) * R * o.d * 0.72} size={R * o.k} rotate={o.rot} />)
258 258 : null}
259 259 {!compact ? (
260 <text x={f(cx)} y={f(cy + R * 1.22)} textAnchor="middle" dominantBaseline="central" fontWeight={700} fill={p.primary} style={{ fontFamily: FONT, letterSpacing: "0.1em" }}>
260 + <text x={f(cx)} y={f(cy + R * 1.0)} textAnchor="middle" dominantBaseline="central" fontWeight={700} fill={p.primary} style={{ fontFamily: FONT, letterSpacing: "0.1em" }}>
261 261 <tspan fontSize={f(R * 0.07)} fillOpacity={0.35}>×1 ×2 ×3 ×5 ×8 </tspan>
262 262 <tspan fontSize={f(R * 0.11)} fill={p.glow} fillOpacity={0.9}>×13</tspan>
263 263 </text>
@@ -279,7 +279,7 @@ function GoldenEmperor(s: Stage) {
279 279 { x: cx, m: "×3" },
280 280 { x: cx + R * 0.66, m: "×5" },
281 281 ];
282 const py = cy + R * 0.62;
282 + const py = cy + R * 0.55;
283 283 return (
284 284 <g>
285 285 <defs>
@@ -301,7 +301,7 @@ function GoldenEmperor(s: Stage) {
301 301 {pearls.map((o) => (
302 302 <g key={o.m}>
303 303 <Sym s={s} id="W" x={o.x} y={py} size={R * 0.5} />
304 <Pill x={o.x} y={py + R * 0.38} text={o.m} color={p.primary} ink={p.glow} size={R * 0.085} />
304 + <Pill x={o.x} y={py + R * 0.36} text={o.m} color={p.primary} ink={p.glow} size={R * 0.085} />
305 305 </g>
306 306 ))}
307 307 <Sym s={s} id="S" x={cx - R * 0.7} y={cy - R * 0.4} size={R * 0.4} />
@@ -332,9 +332,9 @@ function DiamondHeist(s: Stage) {
332 332 const lasers = `M${f(cx - R * 1.4)} ${f(cy - R * 1.3)}L${f(cx + R * 1.4)} ${f(cy + R * 0.2)} M${f(cx - R * 1.4)} ${f(cy + R * 0.9)}L${f(cx + R * 1.4)} ${f(cy - R * 1.1)}`;
333 333 const tiers = ["MINI", "MINOR", "MAJOR", "GRAND"];
334 334 const coins = [
335 { x: cx - R * 0.72, y: cy + R * 0.62, v: "5×" },
336 { x: cx, y: cy + R * 0.8, v: "20×" },
337 { x: cx + R * 0.72, y: cy + R * 0.62, v: "10×" },
335 + { x: cx - R * 0.72, y: cy + R * 0.5, v: "5×" },
336 + { x: cx, y: cy + R * 0.66, v: "20×" },
337 + { x: cx + R * 0.72, y: cy + R * 0.5, v: "10×" },
338 338 ];
339 339 return (
340 340 <g>
@@ -360,16 +360,16 @@ function DiamondHeist(s: Stage) {
360 360 {coins.map((c) => (
361 361 <g key={c.v}>
362 362 <Sym s={s} id="D" x={c.x} y={c.y} size={R * 0.4} />
363 <Pill x={c.x} y={c.y + R * 0.3} text={c.v} color={p.primary} ink={p.glow} size={R * 0.08} />
363 + <Pill x={c.x} y={c.y + R * 0.28} text={c.v} color={p.primary} ink={p.glow} size={R * 0.08} />
364 364 </g>
365 365 ))}
366 366 {tiers.map((t, i) => {
367 const y = cy - R * 1.15 + i * R * 0.27;
367 + const y = cy - R * 0.62 + i * R * 0.3;
368 368 const grand = i === 3;
369 369 return <Pill key={t} x={cx + R * 1.02} y={y} text={t} color={grand ? "#ffd166" : p.secondary} ink={grand ? "#ffd166" : "#dfe4ea"} bg={grand ? "#2a2008" : "#000"} size={R * 0.075} w={R * 0.5} />;
370 370 })}
371 <Sym s={s} id="W" x={cx - R * 0.98} y={cy - R * 0.55} size={R * 0.5} rotate={-30} />
372 <Sym s={s} id="P1" x={cx - R * 1.0} y={cy - R * 1.08} size={R * 0.38} />
371 + <Sym s={s} id="W" x={cx - R * 1.0} y={cy - R * 0.62} size={R * 0.5} rotate={-30} />
372 + <Sym s={s} id="P1" x={cx + R * 1.0} y={cy - R * 1.0} size={R * 0.38} />
373 373 </>
374 374 ) : null}
375 375 <path d={sparkles} fill="#fff" fillOpacity={0.85} />
@@ -416,7 +416,7 @@ function ArcticFortune(s: Stage) {
416 416 <Sym s={s} id="P1" x={cx + R * 0.72} y={cy + R * 0.5} size={R * 0.62} />
417 417 <Sym s={s} id="H1" x={cx - R * 0.78} y={cy - R * 0.45} size={R * 0.44} />
418 418 <Sym s={s} id="S" x={cx + R * 0.1} y={cy - R * 0.7} size={R * 0.38} />
419 <Small x={cx} y={cy + R * 1.22} text="×1 ×2 ×3 ×4 ×5" color="#e8f8ff" size={R * 0.08} tracking="0.16em" opacity={0.75} />
419 + <Small x={cx} y={cy + R * 1.0} text="×1 ×2 ×3 ×4 ×5" color="#e8f8ff" size={R * 0.08} tracking="0.16em" opacity={0.75} />
420 420 </>
421 421 ) : (
422 422 <Sym s={s} id="W" x={cx} y={cy} size={R * 0.9} />
@@ -472,7 +472,7 @@ function InfernoReels(s: Stage) {
472 472 <Sym s={s} id="S" x={cx + R * 0.92} y={cy - R * 0.75} size={R * 0.46} rotate={16} />
473 473 <Sym s={s} id="H1" x={cx - R * 0.95} y={cy + R * 0.55} size={R * 0.42} rotate={22} />
474 474 <Sym s={s} id="H2" x={cx + R * 0.95} y={cy + R * 0.55} size={R * 0.42} rotate={-20} />
475 <Small x={cx} y={cy + R * 1.2} text="×2 · ×3 · ×5 · ×10" color="#ffd166" size={R * 0.085} tracking="0.16em" weight={700} opacity={0.9} />
475 + <Small x={cx} y={cy + R * 1.0} text="×2 · ×3 · ×5 · ×10" color="#ffd166" size={R * 0.085} tracking="0.16em" weight={700} opacity={0.9} />
476 476 </>
477 477 ) : null}
478 478 <path d={embers} fill={p.glow} fillOpacity={0.8} />
@@ -531,7 +531,7 @@ function QuantumJackpot(s: Stage) {
531 531 <Sym s={s} id="W" x={cx - R * 0.85} y={cy - R * 0.85} size={R * 0.48} />
532 532 <Sym s={s} id="H1" x={cx + R * 0.85} y={cy - R * 0.9} size={R * 0.44} />
533 533 <Sym s={s} id="S" x={cx - R * 0.9} y={cy + R * 0.7} size={R * 0.4} />
534 <Small x={cx} y={cy + R * 1.25} text="MINI 20× · MINOR 50× · MAJOR 200× · GRAND 1,000×" color={p.glow} size={R * 0.062} tracking="0.1em" opacity={0.8} />
534 + <Small x={cx} y={cy + R * 1.0} text="MINI 20× · MINOR 50× · MAJOR 200× · GRAND 1,000×" color={p.glow} size={R * 0.062} tracking="0.1em" opacity={0.8} />
535 535 </>
536 536 ) : null}
537 537 </g>
@@ -603,8 +603,8 @@ function MidnightTokyo(s: Stage) {
603 603 /* 09 — PHARAOH PROTOCOL: pyramid with a scanning laser, artifact meter */
604 604 function PharaohProtocol(s: Stage) {
605 605 const { W, H, cx, cy, R, id, p, compact } = s;
606 const base = cy + R * 0.85;
607 const apex = cy - R * 0.85;
606 + const base = cy + R * 0.7;
607 + const apex = cy - R * 0.98;
608 608 const half = R * 1.05;
609 609 const tri = `${f(cx - half)},${f(base)} ${f(cx)},${f(apex)} ${f(cx + half)},${f(base)}`;
610 610 let bricks = "";
@@ -614,7 +614,7 @@ function PharaohProtocol(s: Stage) {
614 614 const segs = 12;
615 615 const mw = R * 1.4;
616 616 const mx = cx - mw / 2;
617 const my = cy + R * 1.12;
617 + const my = cy + R * 0.86;
618 618 let segOff = "";
619 619 let segOn = "";
620 620 for (let i = 0; i < segs; i++) {
@@ -633,8 +633,8 @@ function PharaohProtocol(s: Stage) {
633 633 </clipPath>
634 634 </defs>
635 635 <rect width={W} height={H} fill={`url(#${id}-pp1)`} />
636 <circle cx={f(cx + R * 0.75)} cy={f(cy - R * 0.72)} r={f(R * 0.55)} fill={`url(#${id}-pp3)`} />
637 <circle cx={f(cx + R * 0.75)} cy={f(cy - R * 0.72)} r={f(R * 0.2)} fill="#fb923c" fillOpacity={0.9} />
636 + <circle cx={f(cx + R * 0.78)} cy={f(cy - R * 0.8)} r={f(R * 0.55)} fill={`url(#${id}-pp3)`} />
637 + <circle cx={f(cx + R * 0.78)} cy={f(cy - R * 0.8)} r={f(R * 0.2)} fill="#fb923c" fillOpacity={0.9} />
638 638 <ellipse cx={f(cx - R * 0.6)} cy={f(base + R * 0.15)} rx={f(W * 0.7)} ry={f(R * 0.3)} fill="#3a2810" />
639 639 <ellipse cx={f(cx + R * 0.9)} cy={f(base + R * 0.25)} rx={f(W * 0.6)} ry={f(R * 0.28)} fill="#2a1c0a" />
640 640 <polygon points={tri} fill={`url(#${id}-pp2)`} />
@@ -645,14 +645,14 @@ function PharaohProtocol(s: Stage) {
645 645 <Sym s={s} id="W" x={cx} y={apex - R * 0.05} size={R * 0.72} plate={false} />
646 646 {!compact ? (
647 647 <>
648 <Sym s={s} id="P1" x={cx + R * 0.62} y={cy + R * 0.35} size={R * 0.62} />
649 <Sym s={s} id="H1" x={cx - R * 0.8} y={cy + R * 0.3} size={R * 0.5} />
650 <Sym s={s} id="AR" x={cx - R * 0.55} y={cy + R * 0.72} size={R * 0.36} />
651 <Sym s={s} id="AR" x={cx - R * 0.2} y={cy + R * 0.78} size={R * 0.36} />
652 <Sym s={s} id="AR" x={cx + R * 0.15} y={cy + R * 0.72} size={R * 0.36} />
648 + <Sym s={s} id="P1" x={cx + R * 0.66} y={cy + R * 0.22} size={R * 0.62} />
649 + <Sym s={s} id="H1" x={cx - R * 0.82} y={cy + R * 0.18} size={R * 0.5} />
650 + <Sym s={s} id="AR" x={cx - R * 0.5} y={cy + R * 0.6} size={R * 0.36} />
651 + <Sym s={s} id="AR" x={cx - R * 0.15} y={cy + R * 0.64} size={R * 0.36} />
652 + <Sym s={s} id="AR" x={cx + R * 0.2} y={cy + R * 0.6} size={R * 0.36} />
653 653 <path d={segOff} fill="#fff" fillOpacity={0.12} />
654 654 <path d={segOn} fill={p.secondary} fillOpacity={0.95} />
655 <Small x={cx} y={my + R * 0.16} text="ARTIFACT SCANNER · 7 / 12" color={p.secondary} size={R * 0.065} tracking="0.2em" opacity={0.85} />
655 + <Small x={cx} y={my + R * 0.15} text="ARTIFACT SCANNER · 7 / 12" color={p.secondary} size={R * 0.065} tracking="0.2em" opacity={0.85} />
656 656 </>
657 657 ) : null}
658 658 <path d={dust} fill={p.primary} fillOpacity={0.5} />
@@ -664,7 +664,7 @@ function PharaohProtocol(s: Stage) {
664 664 function LuckyCircuit(s: Stage) {
665 665 const { W, H, cx, cy, R, id, p, compact } = s;
666 666 const bw = R * 2.2;
667 const bh = R * 2.05;
667 + const bh = R * 1.9;
668 668 const bx = cx - bw / 2;
669 669 const by = cy - bh / 2 - R * 0.05;
670 670 let scan = "";
@@ -698,13 +698,13 @@ function LuckyCircuit(s: Stage) {
698 698 <Sym s={s} id="MY" x={cx} y={cy - R * 0.08} size={R * 1.0} />
699 699 {!compact ? (
700 700 <>
701 <Sym s={s} id="W" x={cx - R * 0.78} y={cy - R * 0.72} size={R * 0.48} rotate={-12} />
702 <Sym s={s} id="P1" x={cx + R * 0.78} y={cy - R * 0.72} size={R * 0.48} rotate={10} />
701 + <Sym s={s} id="W" x={cx - R * 0.78} y={cy - R * 0.5} size={R * 0.48} rotate={-12} />
702 + <Sym s={s} id="P1" x={cx + R * 0.78} y={cy - R * 0.5} size={R * 0.48} rotate={10} />
703 703 <Sym s={s} id="H1" x={cx - R * 0.78} y={cy + R * 0.6} size={R * 0.46} />
704 704 <Sym s={s} id="M2" x={cx + R * 0.78} y={cy + R * 0.6} size={R * 0.46} />
705 705 <Sym s={s} id="S" x={cx} y={cy + R * 0.72} size={R * 0.38} />
706 <rect x={f(cx - R * 0.55)} y={f(by - R * 0.42)} width={f(R * 1.1)} height={f(R * 0.24)} rx={f(R * 0.05)} fill={p.glow} />
707 <Small x={cx} y={by - R * 0.3} text="INSERT COIN" color="#fff" size={R * 0.1} weight={800} tracking="0.2em" />
706 + <rect x={f(cx - R * 0.55)} y={f(by + R * 0.06)} width={f(R * 1.1)} height={f(R * 0.24)} rx={f(R * 0.05)} fill={p.glow} />
707 + <Small x={cx} y={by + R * 0.18} text="INSERT COIN" color="#fff" size={R * 0.1} weight={800} tracking="0.2em" />
708 708 </>
709 709 ) : null}
710 710 </g>
@@ -731,8 +731,8 @@ function VoidMiner(s: Stage) {
731 731 [cx + R * 0.7, cy + R * 0.95, R * 0.3, 8],
732 732 [cx - R * 0.15, cy - R * 1.12, R * 0.16, 7],
733 733 ] as const;
734 const drillX = cx + R * 0.72;
735 const drillY = cy - R * 0.72;
734 + const drillX = cx + R * 0.6;
735 + const drillY = cy - R * 0.58;
736 736 const circ = 2 * PI * R * 0.66;
737 737 const seg = circ / 5;
738 738 return (
@@ -755,8 +755,8 @@ function VoidMiner(s: Stage) {
755 755 <line x1={f(drillX)} y1={f(drillY)} x2={f(cx + R * 1.0)} y2={f(cy - R * 0.2)} stroke={p.secondary} strokeWidth={2} strokeOpacity={0.9} strokeDasharray="6 4" />
756 756 <Sym s={s} id="W" x={drillX} y={drillY} size={R * 0.56} rotate={30} />
757 757 <Sym s={s} id="P1" x={cx - R * 0.72} y={cy - R * 0.5} size={R * 0.56} />
758 <Sym s={s} id="H1" x={cx - R * 0.55} y={cy + R * 0.82} size={R * 0.44} />
759 <Sym s={s} id="H2" x={cx + R * 0.15} y={cy + R * 0.95} size={R * 0.4} />
758 + <Sym s={s} id="H1" x={cx - R * 0.55} y={cy + R * 0.8} size={R * 0.44} />
759 + <Sym s={s} id="H2" x={cx + R * 0.2} y={cy + R * 0.82} size={R * 0.4} />
760 760 <Small x={cx + R * 0.02} y={cy - R * 0.98} text="CORE LV 3 / 5" color={p.secondary} size={R * 0.075} tracking="0.2em" weight={700} />
761 761 <Small x={cx + R * 1.12} y={cy + R * 0.4} text="×1 ×2 ×3 ×4 ×5" color={p.primary} size={R * 0.07} tracking="0.16em" rotate={-90} opacity={0.75} />
762 762 </>
@@ -796,7 +796,7 @@ function RoyalCircuit(s: Stage) {
796 796 <path d={track} stroke="#1d1d26" strokeWidth={f(R * 0.56)} fill="none" strokeLinecap="round" />
797 797 <path d={track} stroke="#fff" strokeOpacity={0.35} strokeWidth={2} fill="none" strokeDasharray="14 18" />
798 798 <path d={streaks} stroke={p.primary} strokeOpacity={0.55} strokeWidth={2} strokeLinecap="round" />
799 <rect x={f(cx - R * 1.3)} y={f(cy + R * 1.02)} width={f(R * 2.6)} height={f(R * 0.16)} fill={`url(#${id}-rc0)`} />
799 + <rect x={f(cx - R * 1.3)} y={f(cy + R * 0.9)} width={f(R * 2.6)} height={f(R * 0.14)} fill={`url(#${id}-rc0)`} />
800 800 <Sym s={s} id="W" x={cx + R * 0.1} y={cy + R * 0.08} size={R * 0.96} rotate={-10} />
801 801 {!compact ? (
802 802 <>
@@ -831,7 +831,7 @@ function DragonCore(s: Stage) {
831 831 const scales = [
832 832 { x: cx + R * 0.95, y: cy + R * 0.45, r: 15 },
833 833 { x: cx + R * 0.72, y: cy + R * 0.8, r: 30 },
834 { x: cx + R * 0.35, y: cy + R * 1.0, r: 45 },
834 + { x: cx + R * 0.4, y: cy + R * 0.8, r: 45 },
835 835 ];
836 836 return (
837 837 <g>
@@ -854,7 +854,7 @@ function DragonCore(s: Stage) {
854 854 <Sym s={s} id="W" x={cx - R * 0.9} y={cy + R * 0.7} size={R * 0.56} rotate={-12} />
855 855 <Sym s={s} id="S" x={cx - R * 0.95} y={cy - R * 0.75} size={R * 0.42} />
856 856 <Sym s={s} id="H1" x={cx + R * 0.95} y={cy - R * 0.75} size={R * 0.42} />
857 <Small x={cx} y={cy + R * 1.24} text="DRAGON CHARGE 13 / 20" color={p.secondary} size={R * 0.075} tracking="0.2em" weight={700} opacity={0.9} />
857 + <Small x={cx} y={cy + R * 1.0} text="DRAGON CHARGE 13 / 20" color={p.secondary} size={R * 0.075} tracking="0.2em" weight={700} opacity={0.9} />
858 858 </>
859 859 ) : null}
860 860 <path d={embers} fill={p.secondary} fillOpacity={0.75} />
@@ -869,7 +869,7 @@ function DeepTreasure(s: Stage) {
869 869 for (let i = 0; i < 26; i++) bubbles.push([s.rnd() * W, cy - R * 1.3 + s.rnd() * R * 2.6, 2 + s.rnd() * R * 0.04]);
870 870 const levels = ["×1", "×2", "×3", "×4", "×6", "×8", "×10"];
871 871 const top = cy - R * 1.05;
872 const lh = R * 0.36;
872 + const lh = R * 0.3;
873 873 const chest = (x: number, y: number, w: number, key: string) => (
874 874 <g key={key}>
875 875 <rect x={f(x - w / 2)} y={f(y - w * 0.28)} width={f(w)} height={f(w * 0.62)} rx={f(w * 0.06)} fill="#7a4a12" stroke="#fbbf24" strokeWidth={1.5} />
@@ -896,7 +896,7 @@ function DeepTreasure(s: Stage) {
896 896 : null}
897 897 {!compact ? chest(cx - R * 0.78, cy - R * 0.5, R * 0.42, "c1") : null}
898 898 {!compact ? chest(cx - R * 0.3, cy + R * 0.15, R * 0.5, "c2") : null}
899 {chest(cx + R * 0.3, cy + R * 0.85, R * 0.6, "c3")}
899 + {chest(cx + R * 0.3, cy + R * 0.72, R * 0.6, "c3")}
900 900 <Sym s={s} id="P1" x={cx + R * 0.3} y={cy + R * 0.2} size={R * 0.9} />
901 901 {!compact ? (
902 902 <>
@@ -1007,7 +1007,7 @@ function WildTemple(s: Stage) {
1007 1007 <Sym s={s} id="H1" x={cx + R * 0.98} y={cy + R * 0.62} size={R * 0.5} />
1008 1008 <Sym s={s} id="H2" x={cx - R * 1.05} y={cy - R * 0.45} size={R * 0.4} />
1009 1009 <Sym s={s} id="H2" x={cx + R * 1.05} y={cy - R * 0.45} size={R * 0.4} />
1010 <Sym s={s} id="M1" x={cx + R * 0.5} y={cy + R * 1.0} size={R * 0.34} rotate={30} />
1010 + <Sym s={s} id="M1" x={cx + R * 0.55} y={cy + R * 0.85} size={R * 0.34} rotate={30} />
1011 1011 </>
1012 1012 ) : null}
1013 1013 <path d={motes} fill={p.secondary} fillOpacity={0.55} />
@@ -1057,7 +1057,7 @@ function ZeroGravity(s: Stage) {
1057 1057 <Sym s={s} id="P1" x={cx - R * 0.95} y={cy + R * 0.02} size={R * 0.48} />
1058 1058 <Sym s={s} id="S" x={cx + R * 1.05} y={cy + R * 0.42} size={R * 0.42} />
1059 1059 <Sym s={s} id="H2" x={cx + R * 0.02} y={cy - R * 1.12} size={R * 0.38} rotate={12} />
1060 <Sym s={s} id="M1" x={cx + R * 0.98} y={cy + R * 1.0} size={R * 0.34} rotate={-30} />
1060 + <Sym s={s} id="M1" x={cx + R * 1.0} y={cy + R * 0.85} size={R * 0.34} rotate={-30} />
1061 1061 </>
1062 1062 ) : null}
1063 1063 </g>
@@ -1073,7 +1073,7 @@ function ReelReactor(s: Stage) {
1073 1073 const at = (t: number) => a0 + (a1 - a0) * t;
1074 1074 const needle = at(0.72);
1075 1075 let hazard = "";
1076 const hy = cy + R * 1.08;
1076 + const hy = cy + R * 0.95;
1077 1077 for (let x = -R * 0.3; x < W + R; x += R * 0.22) hazard += `M${f(x)} ${f(hy)}h${f(R * 0.11)}l${f(-R * 0.1)} ${f(R * 0.12)}h${f(-R * 0.11)}z `;
1078 1078 const tick = (t: number, label: string, color: string) => {
1079 1079 const a = at(t);
@@ -1096,7 +1096,7 @@ function ReelReactor(s: Stage) {
1096 1096 {!compact ? (
1097 1097 <>
1098 1098 <Small x={cx} y={cy + R * 0.62} text="HEAT 72" color={p.secondary} size={R * 0.12} weight={800} tracking="0.06em" />
1099 <Pill x={cx + R * 0.72} y={cy - R * 1.1} text="MELTDOWN" color="#ef4444" ink="#ff8a8a" bg="#2a0808" size={R * 0.07} />
1099 + <Pill x={cx + R * 0.1} y={cy - R * 1.12} text="MELTDOWN" color="#ef4444" ink="#ff8a8a" bg="#2a0808" size={R * 0.07} />
1100 1100 <Sym s={s} id="W" x={cx - R * 0.9} y={cy + R * 0.62} size={R * 0.5} />
1101 1101 <Sym s={s} id="S" x={cx + R * 0.9} y={cy + R * 0.62} size={R * 0.5} rotate={15} />
1102 1102 <Sym s={s} id="H1" x={cx - R * 1.05} y={cy - R * 0.55} size={R * 0.4} />
@@ -1157,16 +1157,16 @@ function SpinzaOriginal(s: Stage) {
1157 1157 <Sym s={s} id="W" x={cx} y={cy} size={R * 0.9} plate={false} />
1158 1158 {!compact ? (
1159 1159 <>
1160 <Small x={cx} y={cy + R * 0.98} text="MEGA METER 19 / 30" color={p.glow} size={R * 0.07} tracking="0.22em" weight={700} opacity={0.9} />
1160 + <Small x={cx} y={cy + R * 0.9} text="MEGA METER 19 / 30" color={p.glow} size={R * 0.07} tracking="0.22em" weight={700} opacity={0.9} />
1161 1161 <Sym s={s} id="MY" x={cx - R * 0.98} y={cy - R * 0.62} size={R * 0.62} />
1162 1162 <Sym s={s} id="Z" x={cx - R * 0.98} y={cy + R * 0.3} size={R * 0.5} />
1163 <rect x={f(colX - R * 0.26)} y={f(cy - R * 1.25)} width={f(R * 0.52)} height={f(R * 2.4)} rx={f(R * 0.05)} fill="#fff" fillOpacity={0.04} stroke="#fff" strokeOpacity={0.12} />
1163 + <rect x={f(colX - R * 0.26)} y={f(cy - R * 1.12)} width={f(R * 0.52)} height={f(R * 2.12)} rx={f(R * 0.05)} fill="#fff" fillOpacity={0.04} stroke="#fff" strokeOpacity={0.12} />
1164 1164 {fall.map((idd, i) => (
1165 1165 <Sym key={idd} s={s} id={idd} x={colX} y={cy - R * 0.95 + i * R * 0.56} size={R * 0.46} opacity={0.55 + i * 0.15} />
1166 1166 ))}
1167 <Pill x={cx - R * 0.5} y={cy - R * 1.18} text="WILD DROP" color={p.secondary} ink={p.secondary} size={R * 0.065} />
1168 <Pill x={cx + R * 0.3} y={cy - R * 1.18} text="MULTIPLIER DROP" color={p.glow} ink={p.glow} size={R * 0.065} />
1169 <Small x={cx} y={cy + R * 1.2} text="×1 ×2 ×3 ×4 ×6 ×8 ×10 ×15" color={p.primary} size={R * 0.07} tracking="0.16em" opacity={0.7} />
1167 + <Pill x={cx - R * 0.28} y={cy - R * 1.04} text="WILD DROP" color={p.secondary} ink={p.secondary} size={R * 0.065} />
1168 + <Pill x={cx + R * 0.44} y={cy - R * 1.04} text="MULTIPLIER DROP" color={p.glow} ink={p.glow} size={R * 0.065} />
1169 + <Small x={cx} y={cy + R * 1.02} text="×1 ×2 ×3 ×4 ×6 ×8 ×10 ×15" color={p.primary} size={R * 0.07} tracking="0.16em" opacity={0.7} />
1170 1170 </>
1171 1171 ) : null}
1172 1172 </g>
@@ -1308,13 +1308,13 @@ function Title({ s, name, showName }: { s: Stage; name: string; showName: boolea
1308 1308 const eyebrow = spec.eyebrow && cfg.eye > 0 && v !== "tile";
1309 1309 const eyeSize = v === "hero" ? size * 0.14 : v === "banner" ? size * 0.15 : Math.max(9.5, size * 0.2);
1310 1310 const fontStyle: React.CSSProperties = { fontFamily: FONT, letterSpacing: `${tracking}em` };
1311 const transform = spec.skew ? `skewX(${spec.skew})` : undefined;
1312 1311
1313 1312 const render = (props: React.SVGProps<SVGTextElement>, key: string) =>
1314 1313 lines.map((l, i) => {
1315 1314 const isSub = spec.subline && i === 1;
1316 1315 const y = isSub ? baseY + subSize * 1.7 : baseY + i * lineH;
1317 1316 const base: React.CSSProperties = isSub ? { fontFamily: FONT, letterSpacing: "0.32em" } : fontStyle;
1317 + const transform = spec.skew ? `translate(${f(x)} ${f(y)}) skewX(${spec.skew}) translate(${f(-x)} ${f(-y)})` : undefined;
1318 1318 return (
1319 1319 <text key={`${key}${i}`} x={f(x)} y={f(y)} textAnchor={anchor} fontSize={f(isSub ? subSize : size)} fontWeight={isSub ? 700 : weight} transform={transform} {...props} style={{ ...base, ...props.style }}>
1320 1320 {l}
added games/certifications/bank-heist.json +299 −0
@@ -0,0 +1,299 @@
1 +{
2 + "game": "bank-heist",
3 + "name": "Bank Heist",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.96391712,
8 + "deviation": 0.003917119999999996,
9 + "hitRate": 0.222629,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 490.31,
13 + "stdDev": 4.352455250737556,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 2500×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "0.392% (tolerance ±0.92%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "96.39%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "490.3× (cap 2500×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:39.538Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1554742,
67 + "share": 0.777371
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 135679,
74 + "share": 0.0678395
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79436,
81 + "share": 0.039718
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 143327,
88 + "share": 0.0716635
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47641,
95 + "share": 0.0238205
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 23971,
102 + "share": 0.0119855
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14602,
109 + "share": 0.007301
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 346,
116 + "share": 0.000173
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 256,
123 + "share": 0.000128
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.9390514
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.9494347
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.9586966
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.9608768
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.96147524
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9642069666666667
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9594297714285714
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.9628259
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.9643958666666667
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.96498998
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9642288181818182
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.96441395
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9657298307692308
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9646824857142857
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9639896533333333
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.9642843625
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9641510117647059
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9650899444444444
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.965027852631579
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.96534183
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.9647746857142857
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.9648164636363636
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.9642805043478261
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.963466475
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.963158392
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.963437423076923
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9637005703703704
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9648829785714286
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.9652764551724138
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.9650323133333333
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9638652967741935
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.96357528125
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9641130666666666
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.9641277705882353
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9643895371428571
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9642674055555556
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.9640625027027027
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9635775736842105
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9641630205128205
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.96391712
293 + }
294 + ],
295 + "featureCounts": {
296 + "Signal jammer": 600223
297 + },
298 + "durationMs": 134
299 +}
added games/certifications/black-hole.json +297 −0
@@ -0,0 +1,297 @@
1 +{
2 + "game": "black-hole",
3 + "name": "Black Hole",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.95368702,
8 + "deviation": -0.0063129799999999126,
9 + "hitRate": 0.2224205,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 489.78,
13 + "stdDev": 4.167256251226399,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 10000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "-0.631% (tolerance ±0.88%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "95.37%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "489.8× (cap 10000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:39.773Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1555159,
67 + "share": 0.7775795
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 136143,
74 + "share": 0.0680715
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79553,
81 + "share": 0.0397765
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 142705,
88 + "share": 0.0713525
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47848,
95 + "share": 0.023924
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 23848,
102 + "share": 0.011924
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14190,
109 + "share": 0.007095
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 312,
116 + "share": 0.000156
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 242,
123 + "share": 0.000121
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.921937
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.9377734
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.9353091333333333
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.93739225
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.93914564
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9347733
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9388381714285714
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.943534225
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.9437072
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.94537304
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9467291818181818
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.9489026
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9520744923076923
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9523814285714286
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9508651866666666
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.9510268375
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9504187294117648
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9504978555555555
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.9502219052631579
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.95194051
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.9534165714285714
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.9541829
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.9539388086956522
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.9536442166666667
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.953784856
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.9539154615384615
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9542466888888889
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9538773285714286
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.9536818620689655
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.9545845466666667
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9545137741935484
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.95469646875
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9549692848484849
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.9543618882352941
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9543698685714286
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9543075722222222
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.9546324378378378
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9546098052631579
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9546309282051282
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.95368702
293 + }
294 + ],
295 + "featureCounts": {},
296 + "durationMs": 104
297 +}
added games/certifications/core-meltdown.json +300 −0
@@ -0,0 +1,300 @@
1 +{
2 + "game": "core-meltdown",
3 + "name": "Core Meltdown",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.954057215,
8 + "deviation": -0.005942785000000006,
9 + "hitRate": 0.223306,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 494.09,
13 + "stdDev": 4.097552547967906,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 5000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "-0.594% (tolerance ±0.87%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "95.41%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "494.1× (cap 5000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:40.153Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1553388,
67 + "share": 0.776694
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 136851,
74 + "share": 0.0684255
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79895,
81 + "share": 0.0399475
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 143513,
88 + "share": 0.0717565
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47803,
95 + "share": 0.0239015
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 23696,
102 + "share": 0.011848
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14316,
109 + "share": 0.007158
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 331,
116 + "share": 0.0001655
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 207,
123 + "share": 0.0001035
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.947085
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.9343246
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.9442656666666667
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.9465809
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.94889528
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9459146
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9462324
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.9444719
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.9446777111111111
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.94909574
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9501834909090909
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.9498570166666667
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9506063692307692
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9512420714285714
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9517908133333334
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.9524256
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9533394588235294
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9521214666666666
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.9524753789473684
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.95258458
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.9535204666666667
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.9536495181818182
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.953184947826087
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.9544278333333334
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.954959856
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.954769376923077
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9548739037037037
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9549191928571429
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.9545073793103448
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.9543815066666667
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9548629483870967
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.95486700625
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9546481818181818
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.9551030529411765
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9554977371428571
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9563426055555555
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.9555952540540541
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9547046315789474
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9543590512820512
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.954057215
293 + }
294 + ],
295 + "featureCounts": {
296 + "Power spike": 337235,
297 + "Coolant surge": 1011023
298 + },
299 + "durationMs": 236
300 +}
added games/certifications/deep-dive.json +299 −0
@@ -0,0 +1,299 @@
1 +{
2 + "game": "deep-dive",
3 + "name": "Deep Dive",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.960874195,
8 + "deviation": 0.0008741950000000775,
9 + "hitRate": 0.2230625,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 496.58,
13 + "stdDev": 4.223505991227474,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 5000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "0.087% (tolerance ±0.90%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "96.09%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "496.6× (cap 5000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:39.274Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1553875,
67 + "share": 0.7769375
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 136388,
74 + "share": 0.068194
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79522,
81 + "share": 0.039761
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 143328,
88 + "share": 0.071664
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47784,
95 + "share": 0.023892
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 24199,
102 + "share": 0.0120995
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14322,
109 + "share": 0.007161
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 327,
116 + "share": 0.0001635
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 255,
123 + "share": 0.0001275
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.981744
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.9688415
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.9660274666666666
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.95946645
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.95799912
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9605313
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9544169142857143
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.956806225
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.9591186222222222
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.9582865
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9583992545454545
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.9582501166666667
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9575540307692307
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9579928571428571
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9563829866666667
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.956648
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9576155058823529
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9573925555555556
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.9598498210526316
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.95725369
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.956701380952381
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.9588518727272727
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.9587854173913043
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.9592476833333333
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.959404616
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.9605161615384615
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9614446666666666
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9608630642857143
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.9613541931034483
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.9613371266666667
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9614718967741935
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.9618734875
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9609947878787879
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.961304194117647
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9610557142857142
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9604515166666666
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.960938627027027
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9602719631578948
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9605036974358975
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.960874195
293 + }
294 + ],
295 + "featureCounts": {
296 + "Leviathan current": 900870
297 + },
298 + "durationMs": 180
299 +}
added games/certifications/elevator-999.json +297 −0
@@ -0,0 +1,297 @@
1 +{
2 + "game": "elevator-999",
3 + "name": "Elevator 999",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.95612571,
8 + "deviation": -0.003874289999999947,
9 + "hitRate": 0.222886,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 493.46,
13 + "stdDev": 4.1735026631809315,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 10000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "-0.387% (tolerance ±0.89%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "95.61%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "493.5× (cap 10000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:40.408Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1554228,
67 + "share": 0.777114
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 135939,
74 + "share": 0.0679695
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79524,
81 + "share": 0.039762
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 143668,
88 + "share": 0.071834
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47831,
95 + "share": 0.0239155
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 24002,
102 + "share": 0.012001
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14244,
109 + "share": 0.007122
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 318,
116 + "share": 0.000159
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 246,
123 + "share": 0.000123
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.9487956
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.946483
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.9573534666666667
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.9524104
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.94865168
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9495020666666667
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9475804857142858
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.947544775
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.9488880222222222
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.94755926
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9461799636363636
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.9465434
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9502770923076923
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9491107857142858
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9505048666666667
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.95172005
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9514129764705882
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9521773444444445
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.9526824210526316
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.95212469
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.9524791619047619
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.9540800727272727
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.9545876347826087
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.9541590833333333
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.954415472
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.9558631923076923
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9565765111111111
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9553519714285714
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.9552527172413793
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.9553981466666667
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9556198387096774
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.955715275
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9556010484848485
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.9555573294117647
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9559632114285714
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9563817555555556
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.9553239405405406
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9555535315789474
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9557663487179487
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.95612571
293 + }
294 + ],
295 + "featureCounts": {},
296 + "durationMs": 102
297 +}
added games/certifications/freefall.json +299 −0
@@ -0,0 +1,299 @@
1 +{
2 + "game": "freefall",
3 + "name": "Freefall",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.95592331,
8 + "deviation": -0.004076689999999994,
9 + "hitRate": 0.2225345,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 488.05,
13 + "stdDev": 4.222390940107298,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 5000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "-0.408% (tolerance ±0.90%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "95.59%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "488.1× (cap 5000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:39.917Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1554931,
67 + "share": 0.7774655
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 135705,
74 + "share": 0.0678525
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79838,
81 + "share": 0.039919
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 143175,
88 + "share": 0.0715875
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47642,
95 + "share": 0.023821
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 23893,
102 + "share": 0.0119465
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14238,
109 + "share": 0.007119
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 336,
116 + "share": 0.000168
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 242,
123 + "share": 0.000121
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.9455122
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.95023
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.9491773333333333
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.95242235
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.95341364
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9565252
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9588479428571428
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.9583923
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.959085
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.95956182
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9600586909090909
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.9625846
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9619937538461538
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9598496285714285
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9605832933333334
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.9599182875
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9606216235294117
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9605883222222222
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.960027652631579
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.95981255
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.9599223809523809
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.959446
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.9593016608695653
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.9579431333333334
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.95793356
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.9572521
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9586696222222222
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9580878357142857
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.9575984413793104
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.95777812
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9592348
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.95834230625
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9585262181818182
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.9580600882352941
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9583517771428571
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9580838333333334
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.9578505567567568
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9569594684210526
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9566630974358974
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.95592331
293 + }
294 + ],
295 + "featureCounts": {
296 + "Thermal": 500918
297 + },
298 + "durationMs": 144
299 +}
added games/certifications/rocket-run.json +299 −0
@@ -0,0 +1,299 @@
1 +{
2 + "game": "rocket-run",
3 + "name": "Rocket Run",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.95872351,
8 + "deviation": -0.0012764899999999635,
9 + "hitRate": 0.222655,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 496.19,
13 + "stdDev": 4.190558032312003,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 10000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "-0.128% (tolerance ±0.89%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "95.87%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "496.2× (cap 10000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:39.404Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1554690,
67 + "share": 0.777345
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 135789,
74 + "share": 0.0678945
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79462,
81 + "share": 0.039731
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 143319,
88 + "share": 0.0716595
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47888,
95 + "share": 0.023944
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 23883,
102 + "share": 0.0119415
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14390,
109 + "share": 0.007195
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 318,
116 + "share": 0.000159
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 261,
123 + "share": 0.0001305
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.941627
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.9437129
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.957419
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.94758415
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.94541232
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9559577333333333
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9613262
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.959756825
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.9600729333333333
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.95988924
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9573219272727272
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.9563683666666667
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9558367538461539
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9554104714285714
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9554266133333333
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.955499925
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9568464
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9556221
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.9550166631578947
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.95432584
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.9553890571428572
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.9550377363636363
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.9555198869565218
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.9558961083333334
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.955973944
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.9560392153846153
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9562569111111111
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9578046857142857
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.9579996965517241
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.9577707533333333
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9565746064516129
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.9574897375
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9569376181818182
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.9573828647058824
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9578783085714285
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9587225388888889
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.958616572972973
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9588811789473685
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9588245384615385
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.95872351
293 + }
294 + ],
295 + "featureCounts": {
296 + "Afterburner": 499587
297 + },
298 + "durationMs": 129
299 +}
added games/certifications/skyfall.json +299 −0
@@ -0,0 +1,299 @@
1 +{
2 + "game": "skyfall",
3 + "name": "Skyfall",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.95860249,
8 + "deviation": -0.0013975099999999907,
9 + "hitRate": 0.222615,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 483.01,
13 + "stdDev": 4.239707875888016,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 5000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "-0.140% (tolerance ±0.90%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "95.86%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "483.0× (cap 5000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:39.093Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1554770,
67 + "share": 0.777385
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 136034,
74 + "share": 0.068017
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79026,
81 + "share": 0.039513
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 144004,
88 + "share": 0.072002
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47484,
95 + "share": 0.023742
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 23707,
102 + "share": 0.0118535
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14398,
109 + "share": 0.007199
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 317,
116 + "share": 0.0001585
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 260,
123 + "share": 0.00013
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.9434694
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.9585828
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.9522996
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.9610722
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.96353284
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9633070333333333
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9618062571428572
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.9597582
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.9606117777777777
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.96003238
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9617228545454546
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.9624842333333333
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9603122
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9613523
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9590834133333334
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.9580979
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9583622941176471
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9577526777777777
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.9572920421052632
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.95663994
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.957581
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.9582303636363636
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.9585211739130435
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.958033575
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.958688112
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.9574196076923077
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9588178296296296
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9584433714285714
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.959079075862069
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.9580548933333334
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9580809225806451
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.9587533
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9587091454545454
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.9581568764705882
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9577804971428572
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9574161777777778
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.9587515081081082
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9594349315789473
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9593111641025641
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.95860249
293 + }
294 + ],
295 + "featureCounts": {
296 + "Tailwind": 698209
297 + },
298 + "durationMs": 144
299 +}
added games/certifications/storm-chase.json +299 −0
@@ -0,0 +1,299 @@
1 +{
2 + "game": "storm-chase",
3 + "name": "Storm Chase",
4 + "version": "1.0.0",
5 + "spins": 2000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.96051224,
8 + "deviation": 0.0005122400000000527,
9 + "hitRate": 0.2228855,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 496.84,
13 + "stdDev": 4.3140528179673545,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 5000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "2,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "0.051% (tolerance ±0.92%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "96.05%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "496.8× (cap 5000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:54:40.304Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 1554229,
67 + "share": 0.7771145
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 136259,
74 + "share": 0.0681295
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 79466,
81 + "share": 0.039733
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 143458,
88 + "share": 0.071729
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 47725,
95 + "share": 0.0238625
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 23938,
102 + "share": 0.011969
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 14360,
109 + "share": 0.00718
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 288,
116 + "share": 0.000144
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 277,
123 + "share": 0.0001385
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 50000,
136 + "rtp": 0.9730172
137 + },
138 + {
139 + "spins": 100000,
140 + "rtp": 0.9643629
141 + },
142 + {
143 + "spins": 150000,
144 + "rtp": 0.9585766
145 + },
146 + {
147 + "spins": 200000,
148 + "rtp": 0.95669855
149 + },
150 + {
151 + "spins": 250000,
152 + "rtp": 0.95881768
153 + },
154 + {
155 + "spins": 300000,
156 + "rtp": 0.9565838
157 + },
158 + {
159 + "spins": 350000,
160 + "rtp": 0.9590141142857143
161 + },
162 + {
163 + "spins": 400000,
164 + "rtp": 0.95782775
165 + },
166 + {
167 + "spins": 450000,
168 + "rtp": 0.9578645777777778
169 + },
170 + {
171 + "spins": 500000,
172 + "rtp": 0.95912488
173 + },
174 + {
175 + "spins": 550000,
176 + "rtp": 0.9574977636363636
177 + },
178 + {
179 + "spins": 600000,
180 + "rtp": 0.9575701666666666
181 + },
182 + {
183 + "spins": 650000,
184 + "rtp": 0.9580566923076923
185 + },
186 + {
187 + "spins": 700000,
188 + "rtp": 0.9587008571428571
189 + },
190 + {
191 + "spins": 750000,
192 + "rtp": 0.9566733866666667
193 + },
194 + {
195 + "spins": 800000,
196 + "rtp": 0.958426275
197 + },
198 + {
199 + "spins": 850000,
200 + "rtp": 0.9592964705882353
201 + },
202 + {
203 + "spins": 900000,
204 + "rtp": 0.9597390222222222
205 + },
206 + {
207 + "spins": 950000,
208 + "rtp": 0.9582122947368421
209 + },
210 + {
211 + "spins": 1000000,
212 + "rtp": 0.95642522
213 + },
214 + {
215 + "spins": 1050000,
216 + "rtp": 0.9571531238095238
217 + },
218 + {
219 + "spins": 1100000,
220 + "rtp": 0.9571702545454546
221 + },
222 + {
223 + "spins": 1150000,
224 + "rtp": 0.9580738608695653
225 + },
226 + {
227 + "spins": 1200000,
228 + "rtp": 0.9598063
229 + },
230 + {
231 + "spins": 1250000,
232 + "rtp": 0.961180552
233 + },
234 + {
235 + "spins": 1300000,
236 + "rtp": 0.9620072461538461
237 + },
238 + {
239 + "spins": 1350000,
240 + "rtp": 0.9613264222222222
241 + },
242 + {
243 + "spins": 1400000,
244 + "rtp": 0.9610075642857143
245 + },
246 + {
247 + "spins": 1450000,
248 + "rtp": 0.9613439379310345
249 + },
250 + {
251 + "spins": 1500000,
252 + "rtp": 0.9618556466666667
253 + },
254 + {
255 + "spins": 1550000,
256 + "rtp": 0.9620184064516129
257 + },
258 + {
259 + "spins": 1600000,
260 + "rtp": 0.961062275
261 + },
262 + {
263 + "spins": 1650000,
264 + "rtp": 0.9602330909090909
265 + },
266 + {
267 + "spins": 1700000,
268 + "rtp": 0.9603225823529412
269 + },
270 + {
271 + "spins": 1750000,
272 + "rtp": 0.9609292628571429
273 + },
274 + {
275 + "spins": 1800000,
276 + "rtp": 0.9610316055555556
277 + },
278 + {
279 + "spins": 1850000,
280 + "rtp": 0.9605304486486487
281 + },
282 + {
283 + "spins": 1900000,
284 + "rtp": 0.9598998894736842
285 + },
286 + {
287 + "spins": 1950000,
288 + "rtp": 0.9598532871794871
289 + },
290 + {
291 + "spins": 2000000,
292 + "rtp": 0.96051224
293 + }
294 + ],
295 + "featureCounts": {
296 + "Rear-flank gust": 599214
297 + },
298 + "durationMs": 150
299 +}
added games/certifications/volcano.json +299 −0
@@ -0,0 +1,299 @@
1 +{
2 + "game": "volcano",
3 + "name": "Volcano",
4 + "version": "1.0.0",
5 + "spins": 10000000,
6 + "configuredRtp": 0.96,
7 + "observedRtp": 0.959338234,
8 + "deviation": -0.0006617660000000081,
9 + "hitRate": 0.2229919,
10 + "bonusRate": 0,
11 + "freeSpinRate": 0,
12 + "maxWinMultiplier": 499.53,
13 + "stdDev": 4.216198570180634,
14 + "status": "PASS",
15 + "checks": [
16 + {
17 + "name": "definition",
18 + "pass": true,
19 + "detail": "rtp 0.96, cap 5000×"
20 + },
21 + {
22 + "name": "spins",
23 + "pass": true,
24 + "detail": "10,000,000 rounds"
25 + },
26 + {
27 + "name": "rtp-deviation",
28 + "pass": true,
29 + "detail": "-0.066% (tolerance ±0.40%)"
30 + },
31 + {
32 + "name": "rtp-band",
33 + "pass": true,
34 + "detail": "95.93%"
35 + },
36 + {
37 + "name": "max-win",
38 + "pass": true,
39 + "detail": "499.5× (cap 5000×)"
40 + },
41 + {
42 + "name": "commitment",
43 + "pass": true,
44 + "detail": "SHA-256 commit/reveal verified"
45 + }
46 + ],
47 + "certifiedAt": "2026-09-08T02:55:00.512Z",
48 + "rules": {
49 + "maxDeviation": 0.004,
50 + "minSpins": 1000000,
51 + "rtpBand": [
52 + 0.93,
53 + 0.99
54 + ],
55 + "hitRateBand": [
56 + 0.08,
57 + 0.6
58 + ],
59 + "maxCappedShare": 0.0005
60 + },
61 + "distribution": [
62 + {
63 + "label": "0×",
64 + "min": 0,
65 + "max": 0,
66 + "count": 7770081,
67 + "share": 0.7770081
68 + },
69 + {
70 + "label": "1–1.5×",
71 + "min": 1,
72 + "max": 1.5,
73 + "count": 680310,
74 + "share": 0.068031
75 + },
76 + {
77 + "label": "1.5–2×",
78 + "min": 1.5,
79 + "max": 2,
80 + "count": 398150,
81 + "share": 0.039815
82 + },
83 + {
84 + "label": "2–5×",
85 + "min": 2,
86 + "max": 5,
87 + "count": 718172,
88 + "share": 0.0718172
89 + },
90 + {
91 + "label": "5–10×",
92 + "min": 5,
93 + "max": 10,
94 + "count": 238792,
95 + "share": 0.0238792
96 + },
97 + {
98 + "label": "10–20×",
99 + "min": 10,
100 + "max": 20,
101 + "count": 119631,
102 + "share": 0.0119631
103 + },
104 + {
105 + "label": "20–50×",
106 + "min": 20,
107 + "max": 50,
108 + "count": 72023,
109 + "share": 0.0072023
110 + },
111 + {
112 + "label": "50–100×",
113 + "min": 50,
114 + "max": 100,
115 + "count": 1605,
116 + "share": 0.0001605
117 + },
118 + {
119 + "label": "100–500×",
120 + "min": 100,
121 + "max": 500,
122 + "count": 1236,
123 + "share": 0.0001236
124 + },
125 + {
126 + "label": "500×+",
127 + "min": 500,
128 + "max": null,
129 + "count": 0,
130 + "share": 0
131 + }
132 + ],
133 + "convergence": [
134 + {
135 + "spins": 250000,
136 + "rtp": 0.94169216
137 + },
138 + {
139 + "spins": 500000,
140 + "rtp": 0.94914986
141 + },
142 + {
143 + "spins": 750000,
144 + "rtp": 0.9507861866666667
145 + },
146 + {
147 + "spins": 1000000,
148 + "rtp": 0.95573486
149 + },
150 + {
151 + "spins": 1250000,
152 + "rtp": 0.956596928
153 + },
154 + {
155 + "spins": 1500000,
156 + "rtp": 0.9575963
157 + },
158 + {
159 + "spins": 1750000,
160 + "rtp": 0.9575368114285714
161 + },
162 + {
163 + "spins": 2000000,
164 + "rtp": 0.957738045
165 + },
166 + {
167 + "spins": 2250000,
168 + "rtp": 0.95767596
169 + },
170 + {
171 + "spins": 2500000,
172 + "rtp": 0.956817976
173 + },
174 + {
175 + "spins": 2750000,
176 + "rtp": 0.9571074036363636
177 + },
178 + {
179 + "spins": 3000000,
180 + "rtp": 0.9580076266666666
181 + },
182 + {
183 + "spins": 3250000,
184 + "rtp": 0.9587537661538461
185 + },
186 + {
187 + "spins": 3500000,
188 + "rtp": 0.9579410371428572
189 + },
190 + {
191 + "spins": 3750000,
192 + "rtp": 0.9579896106666667
193 + },
194 + {
195 + "spins": 4000000,
196 + "rtp": 0.958503635
197 + },
198 + {
199 + "spins": 4250000,
200 + "rtp": 0.9576985623529412
201 + },
202 + {
203 + "spins": 4500000,
204 + "rtp": 0.9581201422222222
205 + },
206 + {
207 + "spins": 4750000,
208 + "rtp": 0.9584606673684211
209 + },
210 + {
211 + "spins": 5000000,
212 + "rtp": 0.958283548
213 + },
214 + {
215 + "spins": 5250000,
216 + "rtp": 0.9588206609523809
217 + },
218 + {
219 + "spins": 5500000,
220 + "rtp": 0.95855014
221 + },
222 + {
223 + "spins": 5750000,
224 + "rtp": 0.958530735652174
225 + },
226 + {
227 + "spins": 6000000,
228 + "rtp": 0.9584187233333333
229 + },
230 + {
231 + "spins": 6250000,
232 + "rtp": 0.9581929376
233 + },
234 + {
235 + "spins": 6500000,
236 + "rtp": 0.9583126523076924
237 + },
238 + {
239 + "spins": 6750000,
240 + "rtp": 0.9583265866666667
241 + },
242 + {
243 + "spins": 7000000,
244 + "rtp": 0.9591130057142857
245 + },
246 + {
247 + "spins": 7250000,
248 + "rtp": 0.9592312579310345
249 + },
250 + {
251 + "spins": 7500000,
252 + "rtp": 0.9592946186666667
253 + },
254 + {
255 + "spins": 7750000,
256 + "rtp": 0.9590616141935484
257 + },
258 + {
259 + "spins": 8000000,
260 + "rtp": 0.95862608375
261 + },
262 + {
263 + "spins": 8250000,
264 + "rtp": 0.9591213866666667
265 + },
266 + {
267 + "spins": 8500000,
268 + "rtp": 0.9591908482352941
269 + },
270 + {
271 + "spins": 8750000,
272 + "rtp": 0.9592896285714285
273 + },
274 + {
275 + "spins": 9000000,
276 + "rtp": 0.9596593677777778
277 + },
278 + {
279 + "spins": 9250000,
280 + "rtp": 0.9594241383783784
281 + },
282 + {
283 + "spins": 9500000,
284 + "rtp": 0.9593100105263158
285 + },
286 + {
287 + "spins": 9750000,
288 + "rtp": 0.9592953548717948
289 + },
290 + {
291 + "spins": 10000000,
292 + "rtp": 0.959338234
293 + }
294 + ],
295 + "featureCounts": {
296 + "Crystal vein": 2999811
297 + },
298 + "durationMs": 619
299 +}
modified games/src/client.ts +1 −0
@@ -4,3 +4,4 @@
4 4 * the RNG and engine never ship to the client.
5 5 */
6 6 export { GAMES, GAMES_BY_SLUG, getGame, FEATURED_ORDER, FIRST_GAME_RECOMMENDATIONS } from "./index";
7 +export { CRASH_GAMES, CRASH_BY_SLUG } from "./crash";
added games/src/crash/index.ts +238 −0
@@ -0,0 +1,238 @@
1 +import { defineCrashGame, type CrashGameDefinition } from "@spinza/game-core";
2 +
3 +/**
4 + * Spinza Risk Games — ten cash-out games sharing one provably-fair engine.
5 + * Every game: P(crash ≥ x) = 0.96 / x. Curves and events only change pacing.
6 + */
7 +
8 +const RULES_COMMON = [
9 + "Place a bet and start the round. The multiplier begins at 1.00× and climbs.",
10 + "Press the big button at any moment to lock in your bet × the current multiplier.",
11 + "If the round ends before you cash out, the bet is lost.",
12 + "Optional auto cash-out settles automatically at your target multiplier.",
13 + "Every round is provably fair: the crash point is committed (SHA-256) before the round starts and revealed after it ends.",
14 +];
15 +
16 +export const skyfall = defineCrashGame({
17 + slug: "skyfall",
18 + name: "Skyfall",
19 + version: "1.0.0",
20 + tagline: "How high before the sky runs out?",
21 + description: "A jet climbs through clouds, storms and the stratosphere into open space. The multiplier rises with the altitude — cash out before the aircraft vanishes.",
22 + theme: "aviation ascent",
23 + rtp: 0.96,
24 + maxMultiplier: 5000,
25 + volatility: "high",
26 + curve: { type: "exp", k: 0.075 },
27 + events: { chance: 0.35, max: 1, window: [3, 18], kinds: [{ id: "tailwind", label: "Tailwind", factor: 1.8, duration: [2, 4], weight: 1 }] },
28 + milestones: [
29 + { multiplier: 1.5, label: "Cloud layer" },
30 + { multiplier: 3, label: "Storm front" },
31 + { multiplier: 8, label: "Stratosphere" },
32 + { multiplier: 25, label: "Edge of space" },
33 + { multiplier: 100, label: "Orbit" },
34 + ],
35 + presentation: { scene: "sky", palette: { primary: "#7dd3fc", secondary: "#fbbf24", glow: "#bae6fd", bg: "#08111f", surface: "#0f2540" }, verb: "CASH OUT", unit: "m", unitScale: 1200, ambience: "lunar" },
36 + rules: [...RULES_COMMON, "A Tailwind event speeds the climb for a few seconds without changing the odds."],
37 +});
38 +
39 +export const deepDive = defineCrashGame({
40 + slug: "deep-dive",
41 + name: "Deep Dive",
42 + version: "1.0.0",
43 + tagline: "The abyss pays. The abyss also implodes.",
44 + description: "A research submarine sinks into the ocean. 1,000 m is ×2, 3,000 m is ×5, the abyss is ×20 and beyond. Surface before the hull gives in. Leviathans sometimes drag you deeper, faster.",
45 + theme: "deep-sea descent",
46 + rtp: 0.96,
47 + maxMultiplier: 5000,
48 + volatility: "high",
49 + curve: { type: "exp", k: 0.07 },
50 + events: { chance: 0.3, max: 2, window: [4, 20], kinds: [{ id: "leviathan", label: "Leviathan current", factor: 2.2, duration: [1.5, 3], weight: 1 }] },
51 + milestones: [
52 + { multiplier: 2, label: "1,000 m — twilight zone" },
53 + { multiplier: 5, label: "3,000 m — midnight zone" },
54 + { multiplier: 20, label: "Abyssal plain" },
55 + { multiplier: 80, label: "Hadal trench" },
56 + ],
57 + presentation: { scene: "ocean", palette: { primary: "#22d3ee", secondary: "#34d399", glow: "#67e8f9", bg: "#03111c", surface: "#062a3a" }, verb: "SURFACE", unit: "m", unitScale: 500, ambience: "ocean" },
58 + rules: [...RULES_COMMON, "Leviathan currents briefly accelerate the descent — a pacing event only."],
59 +});
60 +
61 +export const rocketRun = defineCrashGame({
62 + slug: "rocket-run",
63 + name: "Rocket Run",
64 + version: "1.0.0",
65 + tagline: "Mars or bust.",
66 + description: "A rocket leaves the pad for Mars. Every second adds to the multiplier and some runs reach absurd heights — until an engine failure, a collision or an overheat ends it instantly. Secure the payload whenever you like.",
67 + theme: "space launch",
68 + rtp: 0.96,
69 + maxMultiplier: 10000,
70 + volatility: "extreme",
71 + curve: { type: "exp", k: 0.095 },
72 + events: { chance: 0.25, max: 1, window: [2, 14], kinds: [{ id: "afterburner", label: "Afterburner", factor: 2, duration: [1.5, 3], weight: 1 }] },
73 + milestones: [
74 + { multiplier: 2, label: "Max-Q" },
75 + { multiplier: 5, label: "Stage separation" },
76 + { multiplier: 15, label: "Escape velocity" },
77 + { multiplier: 60, label: "Trans-Mars injection" },
78 + { multiplier: 300, label: "Mars orbit" },
79 + ],
80 + presentation: { scene: "rocket", palette: { primary: "#fb923c", secondary: "#f472b6", glow: "#fdba74", bg: "#0a0612", surface: "#1b0f2a" }, verb: "SECURE PAYLOAD", unit: "km", unitScale: 40, ambience: "space" },
81 + rules: [...RULES_COMMON, "Afterburner windows speed up the climb for a moment."],
82 +});
83 +
84 +export const bankHeist = defineCrashGame({
85 + slug: "bank-heist",
86 + name: "Bank Heist",
87 + version: "1.0.0",
88 + tagline: "The bag keeps filling. The sirens keep coming.",
89 + description: "You are inside the vault and the bag of fictional cash keeps growing: ×1.1, ×1.4, ×2, ×3… Stay longer for more, but the police can arrive at any second. Escape before they do and the multiplier is yours.",
90 + theme: "heist",
91 + rtp: 0.96,
92 + maxMultiplier: 2500,
93 + volatility: "medium",
94 + curve: { type: "exp", k: 0.062 },
95 + events: { chance: 0.3, max: 1, window: [3, 16], kinds: [{ id: "jammer", label: "Signal jammer", factor: 1.6, duration: [2, 4], weight: 1 }] },
96 + milestones: [
97 + { multiplier: 1.4, label: "Teller drawers" },
98 + { multiplier: 2, label: "Safe deposit boxes" },
99 + { multiplier: 4, label: "Main vault" },
100 + { multiplier: 12, label: "Diamond room" },
101 + { multiplier: 50, label: "Federal reserve" },
102 + ],
103 + presentation: { scene: "bank", palette: { primary: "#c9a961", secondary: "#ef4444", glow: "#e8cf8f", bg: "#0b0a08", surface: "#1b1811" }, verb: "ESCAPE NOW", unit: "bags", unitScale: 1, ambience: "heist" },
104 + rules: [...RULES_COMMON, "Signal jammer windows fill the bag faster for a few seconds."],
105 +});
106 +
107 +export const volcano = defineCrashGame({
108 + slug: "volcano",
109 + name: "Volcano",
110 + version: "1.0.0",
111 + tagline: "Deeper crystals. Rising lava.",
112 + description: "An explorer descends into a live volcano to harvest crystals. The deeper, the richer — but the lava level rises unpredictably. Trigger EVACUATE before the eruption.",
113 + theme: "volcanic descent",
114 + rtp: 0.96,
115 + maxMultiplier: 5000,
116 + volatility: "high",
117 + curve: { type: "exp", k: 0.08 },
118 + events: { chance: 0.3, max: 1, window: [3, 15], kinds: [{ id: "vein", label: "Crystal vein", factor: 1.9, duration: [1.5, 3], weight: 1 }] },
119 + milestones: [
120 + { multiplier: 1.5, label: "Ash caves" },
121 + { multiplier: 3, label: "Obsidian galleries" },
122 + { multiplier: 8, label: "Crystal chamber" },
123 + { multiplier: 30, label: "Magma core" },
124 + ],
125 + presentation: { scene: "volcano", palette: { primary: "#ff5c3d", secondary: "#fbbf24", glow: "#ff8a5c", bg: "#120604", surface: "#2a0c07" }, verb: "EVACUATE", unit: "m", unitScale: 60, ambience: "fire" },
126 + rules: [...RULES_COMMON, "Crystal veins speed up the harvest briefly."],
127 +});
128 +
129 +export const blackHole = defineCrashGame({
130 + slug: "black-hole",
131 + name: "Black Hole",
132 + version: "1.0.0",
133 + tagline: "Time slows. The multiplier doesn't.",
134 + description: "Your ship drifts toward an event horizon. The multiplier creeps at first, then explodes as space-time bends: ×10 becomes ×15, ×25, ×50 in a heartbeat. WARP OUT before you are pulled in.",
135 + theme: "relativistic descent",
136 + rtp: 0.96,
137 + maxMultiplier: 10000,
138 + volatility: "extreme",
139 + curve: { type: "power", a: 0.02, p: 2.6 },
140 + milestones: [
141 + { multiplier: 1.5, label: "Accretion disk" },
142 + { multiplier: 4, label: "Photon sphere" },
143 + { multiplier: 15, label: "Spaghettification" },
144 + { multiplier: 60, label: "Event horizon" },
145 + ],
146 + presentation: { scene: "blackhole", palette: { primary: "#a78bfa", secondary: "#f0abfc", glow: "#c4b5fd", bg: "#050308", surface: "#120a24" }, verb: "WARP OUT", unit: "km", unitScale: 1000, ambience: "void" },
147 + rules: [...RULES_COMMON, "The curve accelerates sharply near the horizon — the same odds, much faster."],
148 +});
149 +
150 +export const freefall = defineCrashGame({
151 + slug: "freefall",
152 + name: "Freefall",
153 + version: "1.0.0",
154 + tagline: "Pull late. Not too late.",
155 + description: "A skydiver leaps from the edge of the atmosphere. The reward grows during the fall — slowly at altitude, brutally fast near the ground. Open the chute in time or lose the round.",
156 + theme: "skydiving",
157 + rtp: 0.96,
158 + maxMultiplier: 5000,
159 + volatility: "high",
160 + curve: { type: "power", a: 0.05, p: 2.1 },
161 + events: { chance: 0.25, max: 1, window: [3, 14], kinds: [{ id: "thermal", label: "Thermal", factor: 0.5, duration: [2, 3], weight: 1 }] },
162 + milestones: [
163 + { multiplier: 1.3, label: "Terminal velocity" },
164 + { multiplier: 3, label: "Cloud deck" },
165 + { multiplier: 10, label: "Treetops" },
166 + { multiplier: 40, label: "Ground rush" },
167 + ],
168 + presentation: { scene: "freefall", palette: { primary: "#60a5fa", secondary: "#f97316", glow: "#93c5fd", bg: "#07101e", surface: "#0e2140" }, verb: "OPEN CHUTE", unit: "m", unitScale: -300, ambience: "lunar" },
169 + rules: [...RULES_COMMON, "Thermal updrafts slow the fall for a moment — more time to decide."],
170 +});
171 +
172 +export const coreMeltdown = defineCrashGame({
173 + slug: "core-meltdown",
174 + name: "Core Meltdown",
175 + version: "1.0.0",
176 + tagline: "Stable… stable… SHUT DOWN.",
177 + description: "An experimental reactor produces energy as long as it stays stable. Temperature climbs; the multiplier climbs with it. Cooling events can buy you time. Shut down before the limit or lose the batch.",
178 + theme: "reactor control room",
179 + rtp: 0.96,
180 + maxMultiplier: 5000,
181 + volatility: "high",
182 + curve: { type: "exp", k: 0.07 },
183 + events: { chance: 0.45, max: 2, window: [2, 18], kinds: [{ id: "coolant", label: "Coolant surge", factor: 0.45, duration: [2, 3.5], weight: 3 }, { id: "spike", label: "Power spike", factor: 1.8, duration: [1, 2], weight: 1 }] },
184 + milestones: [
185 + { multiplier: 1.5, label: "Nominal" },
186 + { multiplier: 3, label: "Elevated" },
187 + { multiplier: 8, label: "Critical" },
188 + { multiplier: 25, label: "Containment stress" },
189 + ],
190 + presentation: { scene: "reactor", palette: { primary: "#a3e635", secondary: "#facc15", glow: "#bef264", bg: "#070c06", surface: "#111c0e" }, verb: "SHUT DOWN", unit: "°C", unitScale: 90, ambience: "reactor" },
191 + rules: [...RULES_COMMON, "Coolant surges slow the temperature climb; power spikes speed it up."],
192 +});
193 +
194 +export const stormChase = defineCrashGame({
195 + slug: "storm-chase",
196 + name: "Storm Chase",
197 + version: "1.0.0",
198 + tagline: "Drive into the wall cloud.",
199 + description: "You race toward a giant tornado. The closer to the core, the higher the multiplier — but the storm can turn and swallow the truck without warning. ESCAPE while the road is still there.",
200 + theme: "storm chasing",
201 + rtp: 0.96,
202 + maxMultiplier: 5000,
203 + volatility: "high",
204 + curve: { type: "exp", k: 0.078 },
205 + events: { chance: 0.3, max: 1, window: [3, 15], kinds: [{ id: "gust", label: "Rear-flank gust", factor: 1.9, duration: [1.5, 3], weight: 1 }] },
206 + milestones: [
207 + { multiplier: 1.5, label: "Outflow" },
208 + { multiplier: 3, label: "Debris field" },
209 + { multiplier: 8, label: "Wall cloud" },
210 + { multiplier: 30, label: "Vortex" },
211 + ],
212 + presentation: { scene: "storm", palette: { primary: "#94a3b8", secondary: "#a3e635", glow: "#cbd5e1", bg: "#0a0d12", surface: "#1a212c" }, verb: "ESCAPE", unit: "m", unitScale: -400, ambience: "reactor" },
213 + rules: [...RULES_COMMON, "Rear-flank gusts pull you toward the core faster for a moment."],
214 +});
215 +
216 +export const elevator999 = defineCrashGame({
217 + slug: "elevator-999",
218 + name: "Elevator 999",
219 + version: "1.0.0",
220 + tagline: "Every floor is a decision.",
221 + description: "An elevator climbs an endless tower. Each floor raises the multiplier; you may step out and cash out at any floor. The elevator can fail at any moment. Floors 25, 50, 100 and 250 are express zones where the climb accelerates.",
222 + theme: "infinite tower",
223 + rtp: 0.96,
224 + maxMultiplier: 10000,
225 + volatility: "high",
226 + curve: { type: "steps", stepSeconds: 0.55, base: 1.05 },
227 + milestones: [
228 + { multiplier: Math.pow(1.05, 25), label: "Floor 25 — Sky lobby" },
229 + { multiplier: Math.pow(1.05, 50), label: "Floor 50 — Observation deck" },
230 + { multiplier: Math.pow(1.05, 100), label: "Floor 100 — Cloud gardens" },
231 + { multiplier: Math.pow(1.05, 250), label: "Floor 250 — Stratos suite" },
232 + ],
233 + presentation: { scene: "elevator", palette: { primary: "#e8cf8f", secondary: "#38bdf8", glow: "#f3e2ad", bg: "#08090d", surface: "#151821" }, verb: "STEP OUT", unit: "floor", unitScale: 1, ambience: "obsidian" },
234 + rules: [...RULES_COMMON, "The multiplier grows 5% per floor (1.05× compounded). Special floors mark express zones."],
235 +});
236 +
237 +export const CRASH_GAMES: CrashGameDefinition[] = [skyfall, deepDive, rocketRun, bankHeist, volcano, blackHole, freefall, coreMeltdown, stormChase, elevator999];
238 +export const CRASH_BY_SLUG = new Map(CRASH_GAMES.map((g) => [g.slug, g]));
modified games/src/index.ts +1 −0
@@ -29,3 +29,4 @@ export function getGame(slug: string): GameDefinition | undefined {
29 29 export const FEATURED_ORDER = ["spinza-original", "neon-vault", "cosmic-collapse", "obsidian", "diamond-heist", "quantum-jackpot"];
30 30 export const FIRST_GAME_RECOMMENDATIONS = ["neon-vault", "cosmic-collapse", "spinza-original"];
31 31 export { RAW } from "./registry";
32 +export { CRASH_GAMES, CRASH_BY_SLUG } from "./crash";
added packages/database/drizzle/0001_lumpy_hulk.sql +27 −0
@@ -0,0 +1,27 @@
1 +CREATE TABLE "crash_rounds" (
2 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3 + "round_id" varchar(32) NOT NULL,
4 + "user_id" uuid NOT NULL,
5 + "game_id" uuid NOT NULL,
6 + "game_slug" varchar(64) NOT NULL,
7 + "game_version" varchar(16) NOT NULL,
8 + "client_round_id" uuid NOT NULL,
9 + "bet" bigint NOT NULL,
10 + "crash_multiplier" numeric(12, 2) NOT NULL,
11 + "crash_at_ms" integer NOT NULL,
12 + "seed" text NOT NULL,
13 + "commitment" varchar(64) NOT NULL,
14 + "events" jsonb DEFAULT '[]'::jsonb NOT NULL,
15 + "auto_cashout" numeric(12, 2),
16 + "status" varchar(12) DEFAULT 'running' NOT NULL,
17 + "cashout_multiplier" numeric(12, 2),
18 + "win" bigint DEFAULT 0 NOT NULL,
19 + "started_at" timestamp with time zone DEFAULT now() NOT NULL,
20 + "settled_at" timestamp with time zone
21 +);
22 +--> statement-breakpoint
23 +ALTER TABLE "crash_rounds" ADD CONSTRAINT "crash_rounds_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
24 +ALTER TABLE "crash_rounds" ADD CONSTRAINT "crash_rounds_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
25 +CREATE UNIQUE INDEX "crash_rounds_round_id_idx" ON "crash_rounds" USING btree ("round_id");--> statement-breakpoint
26 +CREATE UNIQUE INDEX "crash_rounds_user_client_idx" ON "crash_rounds" USING btree ("user_id","client_round_id");--> statement-breakpoint
27 +CREATE INDEX "crash_rounds_user_status_idx" ON "crash_rounds" USING btree ("user_id","status");
\ No newline at end of file
added packages/database/drizzle/meta/0001_snapshot.json +2801 −0
@@ -0,0 +1,2801 @@
1 +{
2 + "id": "423e47ca-9681-4a28-97a8-d582818cd2d7",
3 + "prevId": "ce701f34-3e3b-4c96-a7b0-2d2f90472e21",
4 + "version": "7",
5 + "dialect": "postgresql",
6 + "tables": {
7 + "public.achievements": {
8 + "name": "achievements",
9 + "schema": "",
10 + "columns": {
11 + "key": {
12 + "name": "key",
13 + "type": "varchar(48)",
14 + "primaryKey": true,
15 + "notNull": true
16 + },
17 + "name": {
18 + "name": "name",
19 + "type": "varchar(80)",
20 + "primaryKey": false,
21 + "notNull": true
22 + },
23 + "description": {
24 + "name": "description",
25 + "type": "text",
26 + "primaryKey": false,
27 + "notNull": true
28 + },
29 + "category": {
30 + "name": "category",
31 + "type": "varchar(24)",
32 + "primaryKey": false,
33 + "notNull": true
34 + },
35 + "metric": {
36 + "name": "metric",
37 + "type": "varchar(32)",
38 + "primaryKey": false,
39 + "notNull": true
40 + },
41 + "target": {
42 + "name": "target",
43 + "type": "bigint",
44 + "primaryKey": false,
45 + "notNull": true
46 + },
47 + "reward_credits": {
48 + "name": "reward_credits",
49 + "type": "integer",
50 + "primaryKey": false,
51 + "notNull": true,
52 + "default": 0
53 + },
54 + "reward_xp": {
55 + "name": "reward_xp",
56 + "type": "integer",
57 + "primaryKey": false,
58 + "notNull": true,
59 + "default": 0
60 + },
61 + "icon": {
62 + "name": "icon",
63 + "type": "varchar(32)",
64 + "primaryKey": false,
65 + "notNull": true,
66 + "default": "'star'"
67 + },
68 + "sort_order": {
69 + "name": "sort_order",
70 + "type": "integer",
71 + "primaryKey": false,
72 + "notNull": true,
73 + "default": 0
74 + },
75 + "enabled": {
76 + "name": "enabled",
77 + "type": "boolean",
78 + "primaryKey": false,
79 + "notNull": true,
80 + "default": true
81 + }
82 + },
83 + "indexes": {},
84 + "foreignKeys": {},
85 + "compositePrimaryKeys": {},
86 + "uniqueConstraints": {},
87 + "policies": {},
88 + "checkConstraints": {},
89 + "isRLSEnabled": false
90 + },
91 + "public.admin_sessions": {
92 + "name": "admin_sessions",
93 + "schema": "",
94 + "columns": {
95 + "id": {
96 + "name": "id",
97 + "type": "uuid",
98 + "primaryKey": true,
99 + "notNull": true,
100 + "default": "gen_random_uuid()"
101 + },
102 + "admin_id": {
103 + "name": "admin_id",
104 + "type": "uuid",
105 + "primaryKey": false,
106 + "notNull": true
107 + },
108 + "token_hash": {
109 + "name": "token_hash",
110 + "type": "text",
111 + "primaryKey": false,
112 + "notNull": true
113 + },
114 + "created_at": {
115 + "name": "created_at",
116 + "type": "timestamp with time zone",
117 + "primaryKey": false,
118 + "notNull": true,
119 + "default": "now()"
120 + },
121 + "expires_at": {
122 + "name": "expires_at",
123 + "type": "timestamp with time zone",
124 + "primaryKey": false,
125 + "notNull": true
126 + },
127 + "ip": {
128 + "name": "ip",
129 + "type": "varchar(64)",
130 + "primaryKey": false,
131 + "notNull": false
132 + }
133 + },
134 + "indexes": {
135 + "admin_sessions_token_idx": {
136 + "name": "admin_sessions_token_idx",
137 + "columns": [
138 + {
139 + "expression": "token_hash",
140 + "isExpression": false,
141 + "asc": true,
142 + "nulls": "last"
143 + }
144 + ],
145 + "isUnique": true,
146 + "concurrently": false,
147 + "method": "btree",
148 + "with": {}
149 + }
150 + },
151 + "foreignKeys": {
152 + "admin_sessions_admin_id_admin_users_id_fk": {
153 + "name": "admin_sessions_admin_id_admin_users_id_fk",
154 + "tableFrom": "admin_sessions",
155 + "tableTo": "admin_users",
156 + "columnsFrom": [
157 + "admin_id"
158 + ],
159 + "columnsTo": [
160 + "id"
161 + ],
162 + "onDelete": "cascade",
163 + "onUpdate": "no action"
164 + }
165 + },
166 + "compositePrimaryKeys": {},
167 + "uniqueConstraints": {},
168 + "policies": {},
169 + "checkConstraints": {},
170 + "isRLSEnabled": false
171 + },
172 + "public.admin_users": {
173 + "name": "admin_users",
174 + "schema": "",
175 + "columns": {
176 + "id": {
177 + "name": "id",
178 + "type": "uuid",
179 + "primaryKey": true,
180 + "notNull": true,
181 + "default": "gen_random_uuid()"
182 + },
183 + "username": {
184 + "name": "username",
185 + "type": "varchar(32)",
186 + "primaryKey": false,
187 + "notNull": true
188 + },
189 + "password_hash": {
190 + "name": "password_hash",
191 + "type": "text",
192 + "primaryKey": false,
193 + "notNull": true
194 + },
195 + "totp_secret": {
196 + "name": "totp_secret",
197 + "type": "text",
198 + "primaryKey": false,
199 + "notNull": true
200 + },
201 + "role": {
202 + "name": "role",
203 + "type": "varchar(16)",
204 + "primaryKey": false,
205 + "notNull": true,
206 + "default": "'admin'"
207 + },
208 + "created_at": {
209 + "name": "created_at",
210 + "type": "timestamp with time zone",
211 + "primaryKey": false,
212 + "notNull": true,
213 + "default": "now()"
214 + },
215 + "last_login_at": {
216 + "name": "last_login_at",
217 + "type": "timestamp with time zone",
218 + "primaryKey": false,
219 + "notNull": false
220 + },
221 + "disabled": {
222 + "name": "disabled",
223 + "type": "boolean",
224 + "primaryKey": false,
225 + "notNull": true,
226 + "default": false
227 + }
228 + },
229 + "indexes": {
230 + "admin_users_username_idx": {
231 + "name": "admin_users_username_idx",
232 + "columns": [
233 + {
234 + "expression": "username",
235 + "isExpression": false,
236 + "asc": true,
237 + "nulls": "last"
238 + }
239 + ],
240 + "isUnique": true,
241 + "concurrently": false,
242 + "method": "btree",
243 + "with": {}
244 + }
245 + },
246 + "foreignKeys": {},
247 + "compositePrimaryKeys": {},
248 + "uniqueConstraints": {},
249 + "policies": {},
250 + "checkConstraints": {},
251 + "isRLSEnabled": false
252 + },
253 + "public.crash_rounds": {
254 + "name": "crash_rounds",
255 + "schema": "",
256 + "columns": {
257 + "id": {
258 + "name": "id",
259 + "type": "uuid",
260 + "primaryKey": true,
261 + "notNull": true,
262 + "default": "gen_random_uuid()"
263 + },
264 + "round_id": {
265 + "name": "round_id",
266 + "type": "varchar(32)",
267 + "primaryKey": false,
268 + "notNull": true
269 + },
270 + "user_id": {
271 + "name": "user_id",
272 + "type": "uuid",
273 + "primaryKey": false,
274 + "notNull": true
275 + },
276 + "game_id": {
277 + "name": "game_id",
278 + "type": "uuid",
279 + "primaryKey": false,
280 + "notNull": true
281 + },
282 + "game_slug": {
283 + "name": "game_slug",
284 + "type": "varchar(64)",
285 + "primaryKey": false,
286 + "notNull": true
287 + },
288 + "game_version": {
289 + "name": "game_version",
290 + "type": "varchar(16)",
291 + "primaryKey": false,
292 + "notNull": true
293 + },
294 + "client_round_id": {
295 + "name": "client_round_id",
296 + "type": "uuid",
297 + "primaryKey": false,
298 + "notNull": true
299 + },
300 + "bet": {
301 + "name": "bet",
302 + "type": "bigint",
303 + "primaryKey": false,
304 + "notNull": true
305 + },
306 + "crash_multiplier": {
307 + "name": "crash_multiplier",
308 + "type": "numeric(12, 2)",
309 + "primaryKey": false,
310 + "notNull": true
311 + },
312 + "crash_at_ms": {
313 + "name": "crash_at_ms",
314 + "type": "integer",
315 + "primaryKey": false,
316 + "notNull": true
317 + },
318 + "seed": {
319 + "name": "seed",
320 + "type": "text",
321 + "primaryKey": false,
322 + "notNull": true
323 + },
324 + "commitment": {
325 + "name": "commitment",
326 + "type": "varchar(64)",
327 + "primaryKey": false,
328 + "notNull": true
329 + },
330 + "events": {
331 + "name": "events",
332 + "type": "jsonb",
333 + "primaryKey": false,
334 + "notNull": true,
335 + "default": "'[]'::jsonb"
336 + },
337 + "auto_cashout": {
338 + "name": "auto_cashout",
339 + "type": "numeric(12, 2)",
340 + "primaryKey": false,
341 + "notNull": false
342 + },
343 + "status": {
344 + "name": "status",
345 + "type": "varchar(12)",
346 + "primaryKey": false,
347 + "notNull": true,
348 + "default": "'running'"
349 + },
350 + "cashout_multiplier": {
351 + "name": "cashout_multiplier",
352 + "type": "numeric(12, 2)",
353 + "primaryKey": false,
354 + "notNull": false
355 + },
356 + "win": {
357 + "name": "win",
358 + "type": "bigint",
359 + "primaryKey": false,
360 + "notNull": true,
361 + "default": 0
362 + },
363 + "started_at": {
364 + "name": "started_at",
365 + "type": "timestamp with time zone",
366 + "primaryKey": false,
367 + "notNull": true,
368 + "default": "now()"
369 + },
370 + "settled_at": {
371 + "name": "settled_at",
372 + "type": "timestamp with time zone",
373 + "primaryKey": false,
374 + "notNull": false
375 + }
376 + },
377 + "indexes": {
378 + "crash_rounds_round_id_idx": {
379 + "name": "crash_rounds_round_id_idx",
380 + "columns": [
381 + {
382 + "expression": "round_id",
383 + "isExpression": false,
384 + "asc": true,
385 + "nulls": "last"
386 + }
387 + ],
388 + "isUnique": true,
389 + "concurrently": false,
390 + "method": "btree",
391 + "with": {}
392 + },
393 + "crash_rounds_user_client_idx": {
394 + "name": "crash_rounds_user_client_idx",
395 + "columns": [
396 + {
397 + "expression": "user_id",
398 + "isExpression": false,
399 + "asc": true,
400 + "nulls": "last"
401 + },
402 + {
403 + "expression": "client_round_id",
404 + "isExpression": false,
405 + "asc": true,
406 + "nulls": "last"
407 + }
408 + ],
409 + "isUnique": true,
410 + "concurrently": false,
411 + "method": "btree",
412 + "with": {}
413 + },
414 + "crash_rounds_user_status_idx": {
415 + "name": "crash_rounds_user_status_idx",
416 + "columns": [
417 + {
418 + "expression": "user_id",
419 + "isExpression": false,
420 + "asc": true,
421 + "nulls": "last"
422 + },
423 + {
424 + "expression": "status",
425 + "isExpression": false,
426 + "asc": true,
427 + "nulls": "last"
428 + }
429 + ],
430 + "isUnique": false,
431 + "concurrently": false,
432 + "method": "btree",
433 + "with": {}
434 + }
435 + },
436 + "foreignKeys": {
437 + "crash_rounds_user_id_users_id_fk": {
438 + "name": "crash_rounds_user_id_users_id_fk",
439 + "tableFrom": "crash_rounds",
440 + "tableTo": "users",
441 + "columnsFrom": [
442 + "user_id"
443 + ],
444 + "columnsTo": [
445 + "id"
446 + ],
447 + "onDelete": "cascade",
448 + "onUpdate": "no action"
449 + },
450 + "crash_rounds_game_id_games_id_fk": {
451 + "name": "crash_rounds_game_id_games_id_fk",
452 + "tableFrom": "crash_rounds",
453 + "tableTo": "games",
454 + "columnsFrom": [
455 + "game_id"
456 + ],
457 + "columnsTo": [
458 + "id"
459 + ],
460 + "onDelete": "no action",
461 + "onUpdate": "no action"
462 + }
463 + },
464 + "compositePrimaryKeys": {},
465 + "uniqueConstraints": {},
466 + "policies": {},
467 + "checkConstraints": {},
468 + "isRLSEnabled": false
469 + },
470 + "public.credit_transactions": {
471 + "name": "credit_transactions",
472 + "schema": "",
473 + "columns": {
474 + "id": {
475 + "name": "id",
476 + "type": "uuid",
477 + "primaryKey": true,
478 + "notNull": true,
479 + "default": "gen_random_uuid()"
480 + },
481 + "user_id": {
482 + "name": "user_id",
483 + "type": "uuid",
484 + "primaryKey": false,
485 + "notNull": true
486 + },
487 + "type": {
488 + "name": "type",
489 + "type": "varchar(24)",
490 + "primaryKey": false,
491 + "notNull": true
492 + },
493 + "amount": {
494 + "name": "amount",
495 + "type": "bigint",
496 + "primaryKey": false,
497 + "notNull": true
498 + },
499 + "balance_after": {
500 + "name": "balance_after",
501 + "type": "bigint",
502 + "primaryKey": false,
503 + "notNull": true
504 + },
505 + "reference": {
506 + "name": "reference",
507 + "type": "text",
508 + "primaryKey": false,
509 + "notNull": false
510 + },
511 + "meta": {
512 + "name": "meta",
513 + "type": "jsonb",
514 + "primaryKey": false,
515 + "notNull": false
516 + },
517 + "created_at": {
518 + "name": "created_at",
519 + "type": "timestamp with time zone",
520 + "primaryKey": false,
521 + "notNull": true,
522 + "default": "now()"
523 + }
524 + },
525 + "indexes": {
526 + "ctx_user_created_idx": {
527 + "name": "ctx_user_created_idx",
528 + "columns": [
529 + {
530 + "expression": "user_id",
531 + "isExpression": false,
532 + "asc": true,
533 + "nulls": "last"
534 + },
535 + {
536 + "expression": "created_at",
537 + "isExpression": false,
538 + "asc": true,
539 + "nulls": "last"
540 + }
541 + ],
542 + "isUnique": false,
543 + "concurrently": false,
544 + "method": "btree",
545 + "with": {}
546 + },
547 + "ctx_type_idx": {
548 + "name": "ctx_type_idx",
549 + "columns": [
550 + {
551 + "expression": "type",
552 + "isExpression": false,
553 + "asc": true,
554 + "nulls": "last"
555 + }
556 + ],
557 + "isUnique": false,
558 + "concurrently": false,
559 + "method": "btree",
560 + "with": {}
561 + },
562 + "ctx_reference_idx": {
563 + "name": "ctx_reference_idx",
564 + "columns": [
565 + {
566 + "expression": "reference",
567 + "isExpression": false,
568 + "asc": true,
569 + "nulls": "last"
570 + }
571 + ],
572 + "isUnique": false,
573 + "concurrently": false,
574 + "method": "btree",
575 + "with": {}
576 + }
577 + },
578 + "foreignKeys": {
579 + "credit_transactions_user_id_users_id_fk": {
580 + "name": "credit_transactions_user_id_users_id_fk",
581 + "tableFrom": "credit_transactions",
582 + "tableTo": "users",
583 + "columnsFrom": [
584 + "user_id"
585 + ],
586 + "columnsTo": [
587 + "id"
588 + ],
589 + "onDelete": "cascade",
590 + "onUpdate": "no action"
591 + }
592 + },
593 + "compositePrimaryKeys": {},
594 + "uniqueConstraints": {},
595 + "policies": {},
596 + "checkConstraints": {},
597 + "isRLSEnabled": false
598 + },
599 + "public.daily_rewards": {
600 + "name": "daily_rewards",
601 + "schema": "",
602 + "columns": {
603 + "user_id": {
604 + "name": "user_id",
605 + "type": "uuid",
606 + "primaryKey": true,
607 + "notNull": true
608 + },
609 + "streak_day": {
610 + "name": "streak_day",
611 + "type": "integer",
612 + "primaryKey": false,
613 + "notNull": true,
614 + "default": 0
615 + },
616 + "last_claimed_at": {
617 + "name": "last_claimed_at",
618 + "type": "timestamp with time zone",
619 + "primaryKey": false,
620 + "notNull": false
621 + },
622 + "next_available_at": {
623 + "name": "next_available_at",
624 + "type": "timestamp with time zone",
625 + "primaryKey": false,
626 + "notNull": false
627 + },
628 + "total_claimed": {
629 + "name": "total_claimed",
630 + "type": "bigint",
631 + "primaryKey": false,
632 + "notNull": true,
633 + "default": 0
634 + },
635 + "claims": {
636 + "name": "claims",
637 + "type": "integer",
638 + "primaryKey": false,
639 + "notNull": true,
640 + "default": 0
641 + }
642 + },
643 + "indexes": {},
644 + "foreignKeys": {
645 + "daily_rewards_user_id_users_id_fk": {
646 + "name": "daily_rewards_user_id_users_id_fk",
647 + "tableFrom": "daily_rewards",
648 + "tableTo": "users",
649 + "columnsFrom": [
650 + "user_id"
651 + ],
652 + "columnsTo": [
653 + "id"
654 + ],
655 + "onDelete": "cascade",
656 + "onUpdate": "no action"
657 + }
658 + },
659 + "compositePrimaryKeys": {},
660 + "uniqueConstraints": {},
661 + "policies": {},
662 + "checkConstraints": {},
663 + "isRLSEnabled": false
664 + },
665 + "public.favorites": {
666 + "name": "favorites",
667 + "schema": "",
668 + "columns": {
669 + "user_id": {
670 + "name": "user_id",
671 + "type": "uuid",
672 + "primaryKey": false,
673 + "notNull": true
674 + },
675 + "game_id": {
676 + "name": "game_id",
677 + "type": "uuid",
678 + "primaryKey": false,
679 + "notNull": true
680 + },
681 + "created_at": {
682 + "name": "created_at",
683 + "type": "timestamp with time zone",
684 + "primaryKey": false,
685 + "notNull": true,
686 + "default": "now()"
687 + }
688 + },
689 + "indexes": {},
690 + "foreignKeys": {
691 + "favorites_user_id_users_id_fk": {
692 + "name": "favorites_user_id_users_id_fk",
693 + "tableFrom": "favorites",
694 + "tableTo": "users",
695 + "columnsFrom": [
696 + "user_id"
697 + ],
698 + "columnsTo": [
699 + "id"
700 + ],
701 + "onDelete": "cascade",
702 + "onUpdate": "no action"
703 + },
704 + "favorites_game_id_games_id_fk": {
705 + "name": "favorites_game_id_games_id_fk",
706 + "tableFrom": "favorites",
707 + "tableTo": "games",
708 + "columnsFrom": [
709 + "game_id"
710 + ],
711 + "columnsTo": [
712 + "id"
713 + ],
714 + "onDelete": "cascade",
715 + "onUpdate": "no action"
716 + }
717 + },
718 + "compositePrimaryKeys": {
719 + "favorites_user_id_game_id_pk": {
720 + "name": "favorites_user_id_game_id_pk",
721 + "columns": [
722 + "user_id",
723 + "game_id"
724 + ]
725 + }
726 + },
727 + "uniqueConstraints": {},
728 + "policies": {},
729 + "checkConstraints": {},
730 + "isRLSEnabled": false
731 + },
732 + "public.feature_flags": {
733 + "name": "feature_flags",
734 + "schema": "",
735 + "columns": {
736 + "key": {
737 + "name": "key",
738 + "type": "varchar(64)",
739 + "primaryKey": true,
740 + "notNull": true
741 + },
742 + "enabled": {
743 + "name": "enabled",
744 + "type": "boolean",
745 + "primaryKey": false,
746 + "notNull": true,
747 + "default": true
748 + },
749 + "description": {
750 + "name": "description",
751 + "type": "text",
752 + "primaryKey": false,
753 + "notNull": false
754 + },
755 + "updated_at": {
756 + "name": "updated_at",
757 + "type": "timestamp with time zone",
758 + "primaryKey": false,
759 + "notNull": true,
760 + "default": "now()"
761 + }
762 + },
763 + "indexes": {},
764 + "foreignKeys": {},
765 + "compositePrimaryKeys": {},
766 + "uniqueConstraints": {},
767 + "policies": {},
768 + "checkConstraints": {},
769 + "isRLSEnabled": false
770 + },
771 + "public.game_rounds": {
772 + "name": "game_rounds",
773 + "schema": "",
774 + "columns": {
775 + "id": {
776 + "name": "id",
777 + "type": "uuid",
778 + "primaryKey": true,
779 + "notNull": true,
780 + "default": "gen_random_uuid()"
781 + },
782 + "round_id": {
783 + "name": "round_id",
784 + "type": "varchar(32)",
785 + "primaryKey": false,
786 + "notNull": true
787 + },
788 + "user_id": {
789 + "name": "user_id",
790 + "type": "uuid",
791 + "primaryKey": false,
792 + "notNull": true
793 + },
794 + "game_id": {
795 + "name": "game_id",
796 + "type": "uuid",
797 + "primaryKey": false,
798 + "notNull": true
799 + },
800 + "game_slug": {
801 + "name": "game_slug",
802 + "type": "varchar(64)",
803 + "primaryKey": false,
804 + "notNull": true
805 + },
806 + "game_version": {
807 + "name": "game_version",
808 + "type": "varchar(16)",
809 + "primaryKey": false,
810 + "notNull": true
811 + },
812 + "client_round_id": {
813 + "name": "client_round_id",
814 + "type": "uuid",
815 + "primaryKey": false,
816 + "notNull": true
817 + },
818 + "bet": {
819 + "name": "bet",
820 + "type": "bigint",
821 + "primaryKey": false,
822 + "notNull": true
823 + },
824 + "win": {
825 + "name": "win",
826 + "type": "bigint",
827 + "primaryKey": false,
828 + "notNull": true
829 + },
830 + "multiplier": {
831 + "name": "multiplier",
832 + "type": "numeric(12, 4)",
833 + "primaryKey": false,
834 + "notNull": true
835 + },
836 + "balance_after": {
837 + "name": "balance_after",
838 + "type": "bigint",
839 + "primaryKey": false,
840 + "notNull": true
841 + },
842 + "result": {
843 + "name": "result",
844 + "type": "jsonb",
845 + "primaryKey": false,
846 + "notNull": true
847 + },
848 + "features": {
849 + "name": "features",
850 + "type": "text[]",
851 + "primaryKey": false,
852 + "notNull": true,
853 + "default": "'{}'::text[]"
854 + },
855 + "free_spins": {
856 + "name": "free_spins",
857 + "type": "boolean",
858 + "primaryKey": false,
859 + "notNull": true,
860 + "default": false
861 + },
862 + "bonus": {
863 + "name": "bonus",
864 + "type": "boolean",
865 + "primaryKey": false,
866 + "notNull": true,
867 + "default": false
868 + },
869 + "jackpot_tier": {
870 + "name": "jackpot_tier",
871 + "type": "varchar(8)",
872 + "primaryKey": false,
873 + "notNull": false
874 + },
875 + "rng_reference": {
876 + "name": "rng_reference",
877 + "type": "text",
878 + "primaryKey": false,
879 + "notNull": true
880 + },
881 + "duration_ms": {
882 + "name": "duration_ms",
883 + "type": "integer",
884 + "primaryKey": false,
885 + "notNull": false
886 + },
887 + "created_at": {
888 + "name": "created_at",
889 + "type": "timestamp with time zone",
890 + "primaryKey": false,
891 + "notNull": true,
892 + "default": "now()"
893 + }
894 + },
895 + "indexes": {
896 + "game_rounds_round_id_idx": {
897 + "name": "game_rounds_round_id_idx",
898 + "columns": [
899 + {
900 + "expression": "round_id",
901 + "isExpression": false,
902 + "asc": true,
903 + "nulls": "last"
904 + }
905 + ],
906 + "isUnique": true,
907 + "concurrently": false,
908 + "method": "btree",
909 + "with": {}
910 + },
911 + "game_rounds_user_client_idx": {
912 + "name": "game_rounds_user_client_idx",
913 + "columns": [
914 + {
915 + "expression": "user_id",
916 + "isExpression": false,
917 + "asc": true,
918 + "nulls": "last"
919 + },
920 + {
921 + "expression": "client_round_id",
922 + "isExpression": false,
923 + "asc": true,
924 + "nulls": "last"
925 + }
926 + ],
927 + "isUnique": true,
928 + "concurrently": false,
929 + "method": "btree",
930 + "with": {}
931 + },
932 + "game_rounds_user_created_idx": {
933 + "name": "game_rounds_user_created_idx",
934 + "columns": [
935 + {
936 + "expression": "user_id",
937 + "isExpression": false,
938 + "asc": true,
939 + "nulls": "last"
940 + },
941 + {
942 + "expression": "created_at",
943 + "isExpression": false,
944 + "asc": true,
945 + "nulls": "last"
946 + }
947 + ],
948 + "isUnique": false,
949 + "concurrently": false,
950 + "method": "btree",
951 + "with": {}
952 + },
953 + "game_rounds_game_created_idx": {
954 + "name": "game_rounds_game_created_idx",
955 + "columns": [
956 + {
957 + "expression": "game_id",
958 + "isExpression": false,
959 + "asc": true,
960 + "nulls": "last"
961 + },
962 + {
963 + "expression": "created_at",
964 + "isExpression": false,
965 + "asc": true,
966 + "nulls": "last"
967 + }
968 + ],
969 + "isUnique": false,
970 + "concurrently": false,
971 + "method": "btree",
972 + "with": {}
973 + },
974 + "game_rounds_created_idx": {
975 + "name": "game_rounds_created_idx",
976 + "columns": [
977 + {
978 + "expression": "created_at",
979 + "isExpression": false,
980 + "asc": true,
981 + "nulls": "last"
982 + }
983 + ],
984 + "isUnique": false,
985 + "concurrently": false,
986 + "method": "btree",
987 + "with": {}
988 + },
989 + "game_rounds_win_idx": {
990 + "name": "game_rounds_win_idx",
991 + "columns": [
992 + {
993 + "expression": "win",
994 + "isExpression": false,
995 + "asc": true,
996 + "nulls": "last"
997 + }
998 + ],
999 + "isUnique": false,
1000 + "concurrently": false,
1001 + "method": "btree",
1002 + "with": {}
1003 + }
1004 + },
1005 + "foreignKeys": {
1006 + "game_rounds_user_id_users_id_fk": {
1007 + "name": "game_rounds_user_id_users_id_fk",
1008 + "tableFrom": "game_rounds",
1009 + "tableTo": "users",
1010 + "columnsFrom": [
1011 + "user_id"
1012 + ],
1013 + "columnsTo": [
1014 + "id"
1015 + ],
1016 + "onDelete": "cascade",
1017 + "onUpdate": "no action"
1018 + },
1019 + "game_rounds_game_id_games_id_fk": {
1020 + "name": "game_rounds_game_id_games_id_fk",
1021 + "tableFrom": "game_rounds",
1022 + "tableTo": "games",
1023 + "columnsFrom": [
1024 + "game_id"
1025 + ],
1026 + "columnsTo": [
1027 + "id"
1028 + ],
1029 + "onDelete": "no action",
1030 + "onUpdate": "no action"
1031 + }
1032 + },
1033 + "compositePrimaryKeys": {},
1034 + "uniqueConstraints": {},
1035 + "policies": {},
1036 + "checkConstraints": {},
1037 + "isRLSEnabled": false
1038 + },
1039 + "public.game_states": {
1040 + "name": "game_states",
1041 + "schema": "",
1042 + "columns": {
1043 + "user_id": {
1044 + "name": "user_id",
1045 + "type": "uuid",
1046 + "primaryKey": false,
1047 + "notNull": true
1048 + },
1049 + "game_id": {
1050 + "name": "game_id",
1051 + "type": "uuid",
1052 + "primaryKey": false,
1053 + "notNull": true
1054 + },
1055 + "state": {
1056 + "name": "state",
1057 + "type": "jsonb",
1058 + "primaryKey": false,
1059 + "notNull": true
1060 + },
1061 + "updated_at": {
1062 + "name": "updated_at",
1063 + "type": "timestamp with time zone",
1064 + "primaryKey": false,
1065 + "notNull": true,
1066 + "default": "now()"
1067 + }
1068 + },
1069 + "indexes": {},
1070 + "foreignKeys": {
1071 + "game_states_user_id_users_id_fk": {
1072 + "name": "game_states_user_id_users_id_fk",
1073 + "tableFrom": "game_states",
1074 + "tableTo": "users",
1075 + "columnsFrom": [
1076 + "user_id"
1077 + ],
1078 + "columnsTo": [
1079 + "id"
1080 + ],
1081 + "onDelete": "cascade",
1082 + "onUpdate": "no action"
1083 + },
1084 + "game_states_game_id_games_id_fk": {
1085 + "name": "game_states_game_id_games_id_fk",
1086 + "tableFrom": "game_states",
1087 + "tableTo": "games",
1088 + "columnsFrom": [
1089 + "game_id"
1090 + ],
1091 + "columnsTo": [
1092 + "id"
1093 + ],
1094 + "onDelete": "cascade",
1095 + "onUpdate": "no action"
1096 + }
1097 + },
1098 + "compositePrimaryKeys": {
1099 + "game_states_user_id_game_id_pk": {
1100 + "name": "game_states_user_id_game_id_pk",
1101 + "columns": [
1102 + "user_id",
1103 + "game_id"
1104 + ]
1105 + }
1106 + },
1107 + "uniqueConstraints": {},
1108 + "policies": {},
1109 + "checkConstraints": {},
1110 + "isRLSEnabled": false
1111 + },
1112 + "public.game_statistics": {
1113 + "name": "game_statistics",
1114 + "schema": "",
1115 + "columns": {
1116 + "game_id": {
1117 + "name": "game_id",
1118 + "type": "uuid",
1119 + "primaryKey": true,
1120 + "notNull": true
1121 + },
1122 + "launches": {
1123 + "name": "launches",
1124 + "type": "bigint",
1125 + "primaryKey": false,
1126 + "notNull": true,
1127 + "default": 0
1128 + },
1129 + "spins": {
1130 + "name": "spins",
1131 + "type": "bigint",
1132 + "primaryKey": false,
1133 + "notNull": true,
1134 + "default": 0
1135 + },
1136 + "wagered": {
1137 + "name": "wagered",
1138 + "type": "bigint",
1139 + "primaryKey": false,
1140 + "notNull": true,
1141 + "default": 0
1142 + },
1143 + "won": {
1144 + "name": "won",
1145 + "type": "bigint",
1146 + "primaryKey": false,
1147 + "notNull": true,
1148 + "default": 0
1149 + },
1150 + "wins": {
1151 + "name": "wins",
1152 + "type": "bigint",
1153 + "primaryKey": false,
1154 + "notNull": true,
1155 + "default": 0
1156 + },
1157 + "bonuses": {
1158 + "name": "bonuses",
1159 + "type": "bigint",
1160 + "primaryKey": false,
1161 + "notNull": true,
1162 + "default": 0
1163 + },
1164 + "free_spins": {
1165 + "name": "free_spins",
1166 + "type": "bigint",
1167 + "primaryKey": false,
1168 + "notNull": true,
1169 + "default": 0
1170 + },
1171 + "big_wins": {
1172 + "name": "big_wins",
1173 + "type": "bigint",
1174 + "primaryKey": false,
1175 + "notNull": true,
1176 + "default": 0
1177 + },
1178 + "max_win": {
1179 + "name": "max_win",
1180 + "type": "bigint",
1181 + "primaryKey": false,
1182 + "notNull": true,
1183 + "default": 0
1184 + },
1185 + "max_multiplier": {
1186 + "name": "max_multiplier",
1187 + "type": "numeric(12, 2)",
1188 + "primaryKey": false,
1189 + "notNull": true,
1190 + "default": "'0'"
1191 + },
1192 + "favorites": {
1193 + "name": "favorites",
1194 + "type": "integer",
1195 + "primaryKey": false,
1196 + "notNull": true,
1197 + "default": 0
1198 + },
1199 + "updated_at": {
1200 + "name": "updated_at",
1201 + "type": "timestamp with time zone",
1202 + "primaryKey": false,
1203 + "notNull": true,
1204 + "default": "now()"
1205 + }
1206 + },
1207 + "indexes": {},
1208 + "foreignKeys": {
1209 + "game_statistics_game_id_games_id_fk": {
1210 + "name": "game_statistics_game_id_games_id_fk",
1211 + "tableFrom": "game_statistics",
1212 + "tableTo": "games",
1213 + "columnsFrom": [
1214 + "game_id"
1215 + ],
1216 + "columnsTo": [
1217 + "id"
1218 + ],
1219 + "onDelete": "cascade",
1220 + "onUpdate": "no action"
1221 + }
1222 + },
1223 + "compositePrimaryKeys": {},
1224 + "uniqueConstraints": {},
1225 + "policies": {},
1226 + "checkConstraints": {},
1227 + "isRLSEnabled": false
1228 + },
1229 + "public.game_versions": {
1230 + "name": "game_versions",
1231 + "schema": "",
1232 + "columns": {
1233 + "id": {
1234 + "name": "id",
1235 + "type": "uuid",
1236 + "primaryKey": true,
1237 + "notNull": true,
1238 + "default": "gen_random_uuid()"
1239 + },
1240 + "game_id": {
1241 + "name": "game_id",
1242 + "type": "uuid",
1243 + "primaryKey": false,
1244 + "notNull": true
1245 + },
1246 + "version": {
1247 + "name": "version",
1248 + "type": "varchar(16)",
1249 + "primaryKey": false,
1250 + "notNull": true
1251 + },
1252 + "definition": {
1253 + "name": "definition",
1254 + "type": "jsonb",
1255 + "primaryKey": false,
1256 + "notNull": true
1257 + },
1258 + "definition_hash": {
1259 + "name": "definition_hash",
1260 + "type": "varchar(64)",
1261 + "primaryKey": false,
1262 + "notNull": true
1263 + },
1264 + "certification": {
1265 + "name": "certification",
1266 + "type": "jsonb",
1267 + "primaryKey": false,
1268 + "notNull": false
1269 + },
1270 + "status": {
1271 + "name": "status",
1272 + "type": "varchar(16)",
1273 + "primaryKey": false,
1274 + "notNull": true,
1275 + "default": "'draft'"
1276 + },
1277 + "created_at": {
1278 + "name": "created_at",
1279 + "type": "timestamp with time zone",
1280 + "primaryKey": false,
1281 + "notNull": true,
1282 + "default": "now()"
1283 + }
1284 + },
1285 + "indexes": {
1286 + "game_versions_game_version_idx": {
1287 + "name": "game_versions_game_version_idx",
1288 + "columns": [
1289 + {
1290 + "expression": "game_id",
1291 + "isExpression": false,
1292 + "asc": true,
1293 + "nulls": "last"
1294 + },
1295 + {
1296 + "expression": "version",
1297 + "isExpression": false,
1298 + "asc": true,
1299 + "nulls": "last"
1300 + }
1301 + ],
1302 + "isUnique": true,
1303 + "concurrently": false,
1304 + "method": "btree",
1305 + "with": {}
1306 + }
1307 + },
1308 + "foreignKeys": {
1309 + "game_versions_game_id_games_id_fk": {
1310 + "name": "game_versions_game_id_games_id_fk",
1311 + "tableFrom": "game_versions",
1312 + "tableTo": "games",
1313 + "columnsFrom": [
1314 + "game_id"
1315 + ],
1316 + "columnsTo": [
1317 + "id"
1318 + ],
1319 + "onDelete": "cascade",
1320 + "onUpdate": "no action"
1321 + }
1322 + },
1323 + "compositePrimaryKeys": {},
1324 + "uniqueConstraints": {},
1325 + "policies": {},
1326 + "checkConstraints": {},
1327 + "isRLSEnabled": false
1328 + },
1329 + "public.games": {
1330 + "name": "games",
1331 + "schema": "",
1332 + "columns": {
1333 + "id": {
1334 + "name": "id",
1335 + "type": "uuid",
1336 + "primaryKey": true,
1337 + "notNull": true,
1338 + "default": "gen_random_uuid()"
1339 + },
1340 + "slug": {
1341 + "name": "slug",
1342 + "type": "varchar(64)",
1343 + "primaryKey": false,
1344 + "notNull": true
1345 + },
1346 + "name": {
1347 + "name": "name",
1348 + "type": "varchar(80)",
1349 + "primaryKey": false,
1350 + "notNull": true
1351 + },
1352 + "version": {
1353 + "name": "version",
1354 + "type": "varchar(16)",
1355 + "primaryKey": false,
1356 + "notNull": true
1357 + },
1358 + "lifecycle": {
1359 + "name": "lifecycle",
1360 + "type": "varchar(16)",
1361 + "primaryKey": false,
1362 + "notNull": true,
1363 + "default": "'draft'"
1364 + },
1365 + "sort_order": {
1366 + "name": "sort_order",
1367 + "type": "integer",
1368 + "primaryKey": false,
1369 + "notNull": true,
1370 + "default": 0
1371 + },
1372 + "is_featured": {
1373 + "name": "is_featured",
1374 + "type": "boolean",
1375 + "primaryKey": false,
1376 + "notNull": true,
1377 + "default": false
1378 + },
1379 + "is_new": {
1380 + "name": "is_new",
1381 + "type": "boolean",
1382 + "primaryKey": false,
1383 + "notNull": true,
1384 + "default": true
1385 + },
1386 + "summary": {
1387 + "name": "summary",
1388 + "type": "jsonb",
1389 + "primaryKey": false,
1390 + "notNull": true
1391 + },
1392 + "created_at": {
1393 + "name": "created_at",
1394 + "type": "timestamp with time zone",
1395 + "primaryKey": false,
1396 + "notNull": true,
1397 + "default": "now()"
1398 + },
1399 + "published_at": {
1400 + "name": "published_at",
1401 + "type": "timestamp with time zone",
1402 + "primaryKey": false,
1403 + "notNull": false
1404 + },
1405 + "updated_at": {
1406 + "name": "updated_at",
1407 + "type": "timestamp with time zone",
1408 + "primaryKey": false,
1409 + "notNull": true,
1410 + "default": "now()"
1411 + }
1412 + },
1413 + "indexes": {
1414 + "games_slug_idx": {
1415 + "name": "games_slug_idx",
1416 + "columns": [
1417 + {
1418 + "expression": "slug",
1419 + "isExpression": false,
1420 + "asc": true,
1421 + "nulls": "last"
1422 + }
1423 + ],
1424 + "isUnique": true,
1425 + "concurrently": false,
1426 + "method": "btree",
1427 + "with": {}
1428 + }
1429 + },
1430 + "foreignKeys": {},
1431 + "compositePrimaryKeys": {},
1432 + "uniqueConstraints": {},
1433 + "policies": {},
1434 + "checkConstraints": {},
1435 + "isRLSEnabled": false
1436 + },
1437 + "public.leaderboards": {
1438 + "name": "leaderboards",
1439 + "schema": "",
1440 + "columns": {
1441 + "id": {
1442 + "name": "id",
1443 + "type": "uuid",
1444 + "primaryKey": true,
1445 + "notNull": true,
1446 + "default": "gen_random_uuid()"
1447 + },
1448 + "category": {
1449 + "name": "category",
1450 + "type": "varchar(32)",
1451 + "primaryKey": false,
1452 + "notNull": true
1453 + },
1454 + "period_key": {
1455 + "name": "period_key",
1456 + "type": "varchar(16)",
1457 + "primaryKey": false,
1458 + "notNull": true
1459 + },
1460 + "user_id": {
1461 + "name": "user_id",
1462 + "type": "uuid",
1463 + "primaryKey": false,
1464 + "notNull": true
1465 + },
1466 + "value": {
1467 + "name": "value",
1468 + "type": "numeric(18, 2)",
1469 + "primaryKey": false,
1470 + "notNull": true
1471 + },
1472 + "game_slug": {
1473 + "name": "game_slug",
1474 + "type": "varchar(64)",
1475 + "primaryKey": false,
1476 + "notNull": false
1477 + },
1478 + "round_id": {
1479 + "name": "round_id",
1480 + "type": "varchar(32)",
1481 + "primaryKey": false,
1482 + "notNull": false
1483 + },
1484 + "updated_at": {
1485 + "name": "updated_at",
1486 + "type": "timestamp with time zone",
1487 + "primaryKey": false,
1488 + "notNull": true,
1489 + "default": "now()"
1490 + }
1491 + },
1492 + "indexes": {
1493 + "leaderboards_unique_idx": {
1494 + "name": "leaderboards_unique_idx",
1495 + "columns": [
1496 + {
1497 + "expression": "category",
1498 + "isExpression": false,
1499 + "asc": true,
1500 + "nulls": "last"
1501 + },
1502 + {
1503 + "expression": "period_key",
1504 + "isExpression": false,
1505 + "asc": true,
1506 + "nulls": "last"
1507 + },
1508 + {
1509 + "expression": "user_id",
1510 + "isExpression": false,
1511 + "asc": true,
1512 + "nulls": "last"
1513 + }
1514 + ],
1515 + "isUnique": true,
1516 + "concurrently": false,
1517 + "method": "btree",
1518 + "with": {}
1519 + },
1520 + "leaderboards_rank_idx": {
1521 + "name": "leaderboards_rank_idx",
1522 + "columns": [
1523 + {
1524 + "expression": "category",
1525 + "isExpression": false,
1526 + "asc": true,
1527 + "nulls": "last"
1528 + },
1529 + {
1530 + "expression": "period_key",
1531 + "isExpression": false,
1532 + "asc": true,
1533 + "nulls": "last"
1534 + },
1535 + {
1536 + "expression": "value",
1537 + "isExpression": false,
1538 + "asc": true,
1539 + "nulls": "last"
1540 + }
1541 + ],
1542 + "isUnique": false,
1543 + "concurrently": false,
1544 + "method": "btree",
1545 + "with": {}
1546 + }
1547 + },
1548 + "foreignKeys": {
1549 + "leaderboards_user_id_users_id_fk": {
1550 + "name": "leaderboards_user_id_users_id_fk",
1551 + "tableFrom": "leaderboards",
1552 + "tableTo": "users",
1553 + "columnsFrom": [
1554 + "user_id"
1555 + ],
1556 + "columnsTo": [
1557 + "id"
1558 + ],
1559 + "onDelete": "cascade",
1560 + "onUpdate": "no action"
1561 + }
1562 + },
1563 + "compositePrimaryKeys": {},
1564 + "uniqueConstraints": {},
1565 + "policies": {},
1566 + "checkConstraints": {},
1567 + "isRLSEnabled": false
1568 + },
1569 + "public.missions": {
1570 + "name": "missions",
1571 + "schema": "",
1572 + "columns": {
1573 + "key": {
1574 + "name": "key",
1575 + "type": "varchar(48)",
1576 + "primaryKey": true,
1577 + "notNull": true
1578 + },
1579 + "name": {
1580 + "name": "name",
1581 + "type": "varchar(80)",
1582 + "primaryKey": false,
1583 + "notNull": true
1584 + },
1585 + "description": {
1586 + "name": "description",
1587 + "type": "text",
1588 + "primaryKey": false,
1589 + "notNull": true
1590 + },
1591 + "period": {
1592 + "name": "period",
1593 + "type": "varchar(8)",
1594 + "primaryKey": false,
1595 + "notNull": true
1596 + },
1597 + "metric": {
1598 + "name": "metric",
1599 + "type": "varchar(32)",
1600 + "primaryKey": false,
1601 + "notNull": true
1602 + },
1603 + "target": {
1604 + "name": "target",
1605 + "type": "bigint",
1606 + "primaryKey": false,
1607 + "notNull": true
1608 + },
1609 + "reward_credits": {
1610 + "name": "reward_credits",
1611 + "type": "integer",
1612 + "primaryKey": false,
1613 + "notNull": true,
1614 + "default": 0
1615 + },
1616 + "reward_xp": {
1617 + "name": "reward_xp",
1618 + "type": "integer",
1619 + "primaryKey": false,
1620 + "notNull": true,
1621 + "default": 0
1622 + },
1623 + "enabled": {
1624 + "name": "enabled",
1625 + "type": "boolean",
1626 + "primaryKey": false,
1627 + "notNull": true,
1628 + "default": true
1629 + },
1630 + "sort_order": {
1631 + "name": "sort_order",
1632 + "type": "integer",
1633 + "primaryKey": false,
1634 + "notNull": true,
1635 + "default": 0
1636 + }
1637 + },
1638 + "indexes": {},
1639 + "foreignKeys": {},
1640 + "compositePrimaryKeys": {},
1641 + "uniqueConstraints": {},
1642 + "policies": {},
1643 + "checkConstraints": {},
1644 + "isRLSEnabled": false
1645 + },
1646 + "public.platform_settings": {
1647 + "name": "platform_settings",
1648 + "schema": "",
1649 + "columns": {
1650 + "key": {
1651 + "name": "key",
1652 + "type": "varchar(64)",
1653 + "primaryKey": true,
1654 + "notNull": true
1655 + },
1656 + "value": {
1657 + "name": "value",
1658 + "type": "jsonb",
1659 + "primaryKey": false,
1660 + "notNull": true
1661 + },
1662 + "updated_at": {
1663 + "name": "updated_at",
1664 + "type": "timestamp with time zone",
1665 + "primaryKey": false,
1666 + "notNull": true,
1667 + "default": "now()"
1668 + }
1669 + },
1670 + "indexes": {},
1671 + "foreignKeys": {},
1672 + "compositePrimaryKeys": {},
1673 + "uniqueConstraints": {},
1674 + "policies": {},
1675 + "checkConstraints": {},
1676 + "isRLSEnabled": false
1677 + },
1678 + "public.player_levels": {
1679 + "name": "player_levels",
1680 + "schema": "",
1681 + "columns": {
1682 + "level": {
1683 + "name": "level",
1684 + "type": "integer",
1685 + "primaryKey": true,
1686 + "notNull": true
1687 + },
1688 + "xp_required": {
1689 + "name": "xp_required",
1690 + "type": "bigint",
1691 + "primaryKey": false,
1692 + "notNull": true
1693 + },
1694 + "xp_cumulative": {
1695 + "name": "xp_cumulative",
1696 + "type": "bigint",
1697 + "primaryKey": false,
1698 + "notNull": true
1699 + },
1700 + "reward_credits": {
1701 + "name": "reward_credits",
1702 + "type": "integer",
1703 + "primaryKey": false,
1704 + "notNull": true
1705 + },
1706 + "title": {
1707 + "name": "title",
1708 + "type": "varchar(48)",
1709 + "primaryKey": false,
1710 + "notNull": true
1711 + }
1712 + },
1713 + "indexes": {},
1714 + "foreignKeys": {},
1715 + "compositePrimaryKeys": {},
1716 + "uniqueConstraints": {},
1717 + "policies": {},
1718 + "checkConstraints": {},
1719 + "isRLSEnabled": false
1720 + },
1721 + "public.recovery_codes": {
1722 + "name": "recovery_codes",
1723 + "schema": "",
1724 + "columns": {
1725 + "user_id": {
1726 + "name": "user_id",
1727 + "type": "uuid",
1728 + "primaryKey": true,
1729 + "notNull": true
1730 + },
1731 + "code_hash": {
1732 + "name": "code_hash",
1733 + "type": "text",
1734 + "primaryKey": false,
1735 + "notNull": true
1736 + },
1737 + "created_at": {
1738 + "name": "created_at",
1739 + "type": "timestamp with time zone",
1740 + "primaryKey": false,
1741 + "notNull": true,
1742 + "default": "now()"
1743 + },
1744 + "rotated_at": {
1745 + "name": "rotated_at",
1746 + "type": "timestamp with time zone",
1747 + "primaryKey": false,
1748 + "notNull": false
1749 + },
1750 + "used_at": {
1751 + "name": "used_at",
1752 + "type": "timestamp with time zone",
1753 + "primaryKey": false,
1754 + "notNull": false
1755 + },
1756 + "use_count": {
1757 + "name": "use_count",
1758 + "type": "integer",
1759 + "primaryKey": false,
1760 + "notNull": true,
1761 + "default": 0
1762 + }
1763 + },
1764 + "indexes": {},
1765 + "foreignKeys": {
1766 + "recovery_codes_user_id_users_id_fk": {
1767 + "name": "recovery_codes_user_id_users_id_fk",
1768 + "tableFrom": "recovery_codes",
1769 + "tableTo": "users",
1770 + "columnsFrom": [
1771 + "user_id"
1772 + ],
1773 + "columnsTo": [
1774 + "id"
1775 + ],
1776 + "onDelete": "cascade",
1777 + "onUpdate": "no action"
1778 + }
1779 + },
1780 + "compositePrimaryKeys": {},
1781 + "uniqueConstraints": {},
1782 + "policies": {},
1783 + "checkConstraints": {},
1784 + "isRLSEnabled": false
1785 + },
1786 + "public.security_events": {
1787 + "name": "security_events",
1788 + "schema": "",
1789 + "columns": {
1790 + "id": {
1791 + "name": "id",
1792 + "type": "uuid",
1793 + "primaryKey": true,
1794 + "notNull": true,
1795 + "default": "gen_random_uuid()"
1796 + },
1797 + "user_id": {
1798 + "name": "user_id",
1799 + "type": "uuid",
1800 + "primaryKey": false,
1801 + "notNull": false
1802 + },
1803 + "admin_id": {
1804 + "name": "admin_id",
1805 + "type": "uuid",
1806 + "primaryKey": false,
1807 + "notNull": false
1808 + },
1809 + "type": {
1810 + "name": "type",
1811 + "type": "varchar(48)",
1812 + "primaryKey": false,
1813 + "notNull": true
1814 + },
1815 + "severity": {
1816 + "name": "severity",
1817 + "type": "varchar(8)",
1818 + "primaryKey": false,
1819 + "notNull": true,
1820 + "default": "'info'"
1821 + },
1822 + "ip": {
1823 + "name": "ip",
1824 + "type": "varchar(64)",
1825 + "primaryKey": false,
1826 + "notNull": false
1827 + },
1828 + "user_agent": {
1829 + "name": "user_agent",
1830 + "type": "text",
1831 + "primaryKey": false,
1832 + "notNull": false
1833 + },
1834 + "meta": {
1835 + "name": "meta",
1836 + "type": "jsonb",
1837 + "primaryKey": false,
1838 + "notNull": false
1839 + },
1840 + "created_at": {
1841 + "name": "created_at",
1842 + "type": "timestamp with time zone",
1843 + "primaryKey": false,
1844 + "notNull": true,
1845 + "default": "now()"
1846 + }
1847 + },
1848 + "indexes": {
1849 + "security_events_created_idx": {
1850 + "name": "security_events_created_idx",
1851 + "columns": [
1852 + {
1853 + "expression": "created_at",
1854 + "isExpression": false,
1855 + "asc": true,
1856 + "nulls": "last"
1857 + }
1858 + ],
1859 + "isUnique": false,
1860 + "concurrently": false,
1861 + "method": "btree",
1862 + "with": {}
1863 + },
1864 + "security_events_type_idx": {
1865 + "name": "security_events_type_idx",
1866 + "columns": [
1867 + {
1868 + "expression": "type",
1869 + "isExpression": false,
1870 + "asc": true,
1871 + "nulls": "last"
1872 + }
1873 + ],
1874 + "isUnique": false,
1875 + "concurrently": false,
1876 + "method": "btree",
1877 + "with": {}
1878 + },
1879 + "security_events_user_idx": {
1880 + "name": "security_events_user_idx",
1881 + "columns": [
1882 + {
1883 + "expression": "user_id",
1884 + "isExpression": false,
1885 + "asc": true,
1886 + "nulls": "last"
1887 + }
1888 + ],
1889 + "isUnique": false,
1890 + "concurrently": false,
1891 + "method": "btree",
1892 + "with": {}
1893 + }
1894 + },
1895 + "foreignKeys": {
1896 + "security_events_user_id_users_id_fk": {
1897 + "name": "security_events_user_id_users_id_fk",
1898 + "tableFrom": "security_events",
1899 + "tableTo": "users",
1900 + "columnsFrom": [
1901 + "user_id"
1902 + ],
1903 + "columnsTo": [
1904 + "id"
1905 + ],
1906 + "onDelete": "set null",
1907 + "onUpdate": "no action"
1908 + }
1909 + },
1910 + "compositePrimaryKeys": {},
1911 + "uniqueConstraints": {},
1912 + "policies": {},
1913 + "checkConstraints": {},
1914 + "isRLSEnabled": false
1915 + },
1916 + "public.sessions": {
1917 + "name": "sessions",
1918 + "schema": "",
1919 + "columns": {
1920 + "id": {
1921 + "name": "id",
1922 + "type": "uuid",
1923 + "primaryKey": true,
1924 + "notNull": true,
1925 + "default": "gen_random_uuid()"
1926 + },
1927 + "user_id": {
1928 + "name": "user_id",
1929 + "type": "uuid",
1930 + "primaryKey": false,
1931 + "notNull": true
1932 + },
1933 + "token_hash": {
1934 + "name": "token_hash",
1935 + "type": "text",
1936 + "primaryKey": false,
1937 + "notNull": true
1938 + },
1939 + "created_at": {
1940 + "name": "created_at",
1941 + "type": "timestamp with time zone",
1942 + "primaryKey": false,
1943 + "notNull": true,
1944 + "default": "now()"
1945 + },
1946 + "expires_at": {
1947 + "name": "expires_at",
1948 + "type": "timestamp with time zone",
1949 + "primaryKey": false,
1950 + "notNull": true
1951 + },
1952 + "last_seen_at": {
1953 + "name": "last_seen_at",
1954 + "type": "timestamp with time zone",
1955 + "primaryKey": false,
1956 + "notNull": true,
1957 + "default": "now()"
1958 + },
1959 + "user_agent": {
1960 + "name": "user_agent",
1961 + "type": "text",
1962 + "primaryKey": false,
1963 + "notNull": false
1964 + },
1965 + "ip": {
1966 + "name": "ip",
1967 + "type": "varchar(64)",
1968 + "primaryKey": false,
1969 + "notNull": false
1970 + }
1971 + },
1972 + "indexes": {
1973 + "sessions_token_hash_idx": {
1974 + "name": "sessions_token_hash_idx",
1975 + "columns": [
1976 + {
1977 + "expression": "token_hash",
1978 + "isExpression": false,
1979 + "asc": true,
1980 + "nulls": "last"
1981 + }
1982 + ],
1983 + "isUnique": true,
1984 + "concurrently": false,
1985 + "method": "btree",
1986 + "with": {}
1987 + },
1988 + "sessions_user_idx": {
1989 + "name": "sessions_user_idx",
1990 + "columns": [
1991 + {
1992 + "expression": "user_id",
1993 + "isExpression": false,
1994 + "asc": true,
1995 + "nulls": "last"
1996 + }
1997 + ],
1998 + "isUnique": false,
1999 + "concurrently": false,
2000 + "method": "btree",
2001 + "with": {}
2002 + }
2003 + },
2004 + "foreignKeys": {
2005 + "sessions_user_id_users_id_fk": {
2006 + "name": "sessions_user_id_users_id_fk",
2007 + "tableFrom": "sessions",
2008 + "tableTo": "users",
2009 + "columnsFrom": [
2010 + "user_id"
2011 + ],
2012 + "columnsTo": [
2013 + "id"
2014 + ],
2015 + "onDelete": "cascade",
2016 + "onUpdate": "no action"
2017 + }
2018 + },
2019 + "compositePrimaryKeys": {},
2020 + "uniqueConstraints": {},
2021 + "policies": {},
2022 + "checkConstraints": {},
2023 + "isRLSEnabled": false
2024 + },
2025 + "public.simulation_runs": {
2026 + "name": "simulation_runs",
2027 + "schema": "",
2028 + "columns": {
2029 + "id": {
2030 + "name": "id",
2031 + "type": "uuid",
2032 + "primaryKey": true,
2033 + "notNull": true,
2034 + "default": "gen_random_uuid()"
2035 + },
2036 + "game_slug": {
2037 + "name": "game_slug",
2038 + "type": "varchar(64)",
2039 + "primaryKey": false,
2040 + "notNull": true
2041 + },
2042 + "game_version": {
2043 + "name": "game_version",
2044 + "type": "varchar(16)",
2045 + "primaryKey": false,
2046 + "notNull": true
2047 + },
2048 + "spins": {
2049 + "name": "spins",
2050 + "type": "bigint",
2051 + "primaryKey": false,
2052 + "notNull": true
2053 + },
2054 + "status": {
2055 + "name": "status",
2056 + "type": "varchar(16)",
2057 + "primaryKey": false,
2058 + "notNull": true,
2059 + "default": "'running'"
2060 + },
2061 + "progress": {
2062 + "name": "progress",
2063 + "type": "bigint",
2064 + "primaryKey": false,
2065 + "notNull": true,
2066 + "default": 0
2067 + },
2068 + "result": {
2069 + "name": "result",
2070 + "type": "jsonb",
2071 + "primaryKey": false,
2072 + "notNull": false
2073 + },
2074 + "error": {
2075 + "name": "error",
2076 + "type": "text",
2077 + "primaryKey": false,
2078 + "notNull": false
2079 + },
2080 + "requested_by": {
2081 + "name": "requested_by",
2082 + "type": "uuid",
2083 + "primaryKey": false,
2084 + "notNull": false
2085 + },
2086 + "created_at": {
2087 + "name": "created_at",
2088 + "type": "timestamp with time zone",
2089 + "primaryKey": false,
2090 + "notNull": true,
2091 + "default": "now()"
2092 + },
2093 + "finished_at": {
2094 + "name": "finished_at",
2095 + "type": "timestamp with time zone",
2096 + "primaryKey": false,
2097 + "notNull": false
2098 + }
2099 + },
2100 + "indexes": {
2101 + "simulation_runs_game_idx": {
2102 + "name": "simulation_runs_game_idx",
2103 + "columns": [
2104 + {
2105 + "expression": "game_slug",
2106 + "isExpression": false,
2107 + "asc": true,
2108 + "nulls": "last"
2109 + },
2110 + {
2111 + "expression": "created_at",
2112 + "isExpression": false,
2113 + "asc": true,
2114 + "nulls": "last"
2115 + }
2116 + ],
2117 + "isUnique": false,
2118 + "concurrently": false,
2119 + "method": "btree",
2120 + "with": {}
2121 + }
2122 + },
2123 + "foreignKeys": {},
2124 + "compositePrimaryKeys": {},
2125 + "uniqueConstraints": {},
2126 + "policies": {},
2127 + "checkConstraints": {},
2128 + "isRLSEnabled": false
2129 + },
2130 + "public.user_achievements": {
2131 + "name": "user_achievements",
2132 + "schema": "",
2133 + "columns": {
2134 + "user_id": {
2135 + "name": "user_id",
2136 + "type": "uuid",
2137 + "primaryKey": false,
2138 + "notNull": true
2139 + },
2140 + "achievement_key": {
2141 + "name": "achievement_key",
2142 + "type": "varchar(48)",
2143 + "primaryKey": false,
2144 + "notNull": true
2145 + },
2146 + "unlocked_at": {
2147 + "name": "unlocked_at",
2148 + "type": "timestamp with time zone",
2149 + "primaryKey": false,
2150 + "notNull": true,
2151 + "default": "now()"
2152 + }
2153 + },
2154 + "indexes": {},
2155 + "foreignKeys": {
2156 + "user_achievements_user_id_users_id_fk": {
2157 + "name": "user_achievements_user_id_users_id_fk",
2158 + "tableFrom": "user_achievements",
2159 + "tableTo": "users",
2160 + "columnsFrom": [
2161 + "user_id"
2162 + ],
2163 + "columnsTo": [
2164 + "id"
2165 + ],
2166 + "onDelete": "cascade",
2167 + "onUpdate": "no action"
2168 + },
2169 + "user_achievements_achievement_key_achievements_key_fk": {
2170 + "name": "user_achievements_achievement_key_achievements_key_fk",
2171 + "tableFrom": "user_achievements",
2172 + "tableTo": "achievements",
2173 + "columnsFrom": [
2174 + "achievement_key"
2175 + ],
2176 + "columnsTo": [
2177 + "key"
2178 + ],
2179 + "onDelete": "cascade",
2180 + "onUpdate": "no action"
2181 + }
2182 + },
2183 + "compositePrimaryKeys": {
2184 + "user_achievements_user_id_achievement_key_pk": {
2185 + "name": "user_achievements_user_id_achievement_key_pk",
2186 + "columns": [
2187 + "user_id",
2188 + "achievement_key"
2189 + ]
2190 + }
2191 + },
2192 + "uniqueConstraints": {},
2193 + "policies": {},
2194 + "checkConstraints": {},
2195 + "isRLSEnabled": false
2196 + },
2197 + "public.user_game_stats": {
2198 + "name": "user_game_stats",
2199 + "schema": "",
2200 + "columns": {
2201 + "user_id": {
2202 + "name": "user_id",
2203 + "type": "uuid",
2204 + "primaryKey": false,
2205 + "notNull": true
2206 + },
2207 + "game_id": {
2208 + "name": "game_id",
2209 + "type": "uuid",
2210 + "primaryKey": false,
2211 + "notNull": true
2212 + },
2213 + "spins": {
2214 + "name": "spins",
2215 + "type": "bigint",
2216 + "primaryKey": false,
2217 + "notNull": true,
2218 + "default": 0
2219 + },
2220 + "wagered": {
2221 + "name": "wagered",
2222 + "type": "bigint",
2223 + "primaryKey": false,
2224 + "notNull": true,
2225 + "default": 0
2226 + },
2227 + "won": {
2228 + "name": "won",
2229 + "type": "bigint",
2230 + "primaryKey": false,
2231 + "notNull": true,
2232 + "default": 0
2233 + },
2234 + "bonuses": {
2235 + "name": "bonuses",
2236 + "type": "integer",
2237 + "primaryKey": false,
2238 + "notNull": true,
2239 + "default": 0
2240 + },
2241 + "biggest_win": {
2242 + "name": "biggest_win",
2243 + "type": "bigint",
2244 + "primaryKey": false,
2245 + "notNull": true,
2246 + "default": 0
2247 + },
2248 + "biggest_multiplier": {
2249 + "name": "biggest_multiplier",
2250 + "type": "numeric(12, 2)",
2251 + "primaryKey": false,
2252 + "notNull": true,
2253 + "default": "'0'"
2254 + },
2255 + "last_played_at": {
2256 + "name": "last_played_at",
2257 + "type": "timestamp with time zone",
2258 + "primaryKey": false,
2259 + "notNull": true,
2260 + "default": "now()"
2261 + },
2262 + "first_played_at": {
2263 + "name": "first_played_at",
2264 + "type": "timestamp with time zone",
2265 + "primaryKey": false,
2266 + "notNull": true,
2267 + "default": "now()"
2268 + }
2269 + },
2270 + "indexes": {
2271 + "ugs_user_last_idx": {
2272 + "name": "ugs_user_last_idx",
2273 + "columns": [
2274 + {
2275 + "expression": "user_id",
2276 + "isExpression": false,
2277 + "asc": true,
2278 + "nulls": "last"
2279 + },
2280 + {
2281 + "expression": "last_played_at",
2282 + "isExpression": false,
2283 + "asc": true,
2284 + "nulls": "last"
2285 + }
2286 + ],
2287 + "isUnique": false,
2288 + "concurrently": false,
2289 + "method": "btree",
2290 + "with": {}
2291 + }
2292 + },
2293 + "foreignKeys": {
2294 + "user_game_stats_user_id_users_id_fk": {
2295 + "name": "user_game_stats_user_id_users_id_fk",
2296 + "tableFrom": "user_game_stats",
2297 + "tableTo": "users",
2298 + "columnsFrom": [
2299 + "user_id"
2300 + ],
2301 + "columnsTo": [
2302 + "id"
2303 + ],
2304 + "onDelete": "cascade",
2305 + "onUpdate": "no action"
2306 + },
2307 + "user_game_stats_game_id_games_id_fk": {
2308 + "name": "user_game_stats_game_id_games_id_fk",
2309 + "tableFrom": "user_game_stats",
2310 + "tableTo": "games",
2311 + "columnsFrom": [
2312 + "game_id"
2313 + ],
2314 + "columnsTo": [
2315 + "id"
2316 + ],
2317 + "onDelete": "cascade",
2318 + "onUpdate": "no action"
2319 + }
2320 + },
2321 + "compositePrimaryKeys": {
2322 + "user_game_stats_user_id_game_id_pk": {
2323 + "name": "user_game_stats_user_id_game_id_pk",
2324 + "columns": [
2325 + "user_id",
2326 + "game_id"
2327 + ]
2328 + }
2329 + },
2330 + "uniqueConstraints": {},
2331 + "policies": {},
2332 + "checkConstraints": {},
2333 + "isRLSEnabled": false
2334 + },
2335 + "public.user_missions": {
2336 + "name": "user_missions",
2337 + "schema": "",
2338 + "columns": {
2339 + "id": {
2340 + "name": "id",
2341 + "type": "uuid",
2342 + "primaryKey": true,
2343 + "notNull": true,
2344 + "default": "gen_random_uuid()"
2345 + },
2346 + "user_id": {
2347 + "name": "user_id",
2348 + "type": "uuid",
2349 + "primaryKey": false,
2350 + "notNull": true
2351 + },
2352 + "mission_key": {
2353 + "name": "mission_key",
2354 + "type": "varchar(48)",
2355 + "primaryKey": false,
2356 + "notNull": true
2357 + },
2358 + "period_key": {
2359 + "name": "period_key",
2360 + "type": "varchar(16)",
2361 + "primaryKey": false,
2362 + "notNull": true
2363 + },
2364 + "progress": {
2365 + "name": "progress",
2366 + "type": "bigint",
2367 + "primaryKey": false,
2368 + "notNull": true,
2369 + "default": 0
2370 + },
2371 + "progress_set": {
2372 + "name": "progress_set",
2373 + "type": "text[]",
2374 + "primaryKey": false,
2375 + "notNull": true,
2376 + "default": "'{}'::text[]"
2377 + },
2378 + "completed_at": {
2379 + "name": "completed_at",
2380 + "type": "timestamp with time zone",
2381 + "primaryKey": false,
2382 + "notNull": false
2383 + },
2384 + "claimed_at": {
2385 + "name": "claimed_at",
2386 + "type": "timestamp with time zone",
2387 + "primaryKey": false,
2388 + "notNull": false
2389 + },
2390 + "expires_at": {
2391 + "name": "expires_at",
2392 + "type": "timestamp with time zone",
2393 + "primaryKey": false,
2394 + "notNull": true
2395 + }
2396 + },
2397 + "indexes": {
2398 + "user_missions_unique_idx": {
2399 + "name": "user_missions_unique_idx",
2400 + "columns": [
2401 + {
2402 + "expression": "user_id",
2403 + "isExpression": false,
2404 + "asc": true,
2405 + "nulls": "last"
2406 + },
2407 + {
2408 + "expression": "mission_key",
2409 + "isExpression": false,
2410 + "asc": true,
2411 + "nulls": "last"
2412 + },
2413 + {
2414 + "expression": "period_key",
2415 + "isExpression": false,
2416 + "asc": true,
2417 + "nulls": "last"
2418 + }
2419 + ],
2420 + "isUnique": true,
2421 + "concurrently": false,
2422 + "method": "btree",
2423 + "with": {}
2424 + },
2425 + "user_missions_user_idx": {
2426 + "name": "user_missions_user_idx",
2427 + "columns": [
2428 + {
2429 + "expression": "user_id",
2430 + "isExpression": false,
2431 + "asc": true,
2432 + "nulls": "last"
2433 + }
2434 + ],
2435 + "isUnique": false,
2436 + "concurrently": false,
2437 + "method": "btree",
2438 + "with": {}
2439 + }
2440 + },
2441 + "foreignKeys": {
2442 + "user_missions_user_id_users_id_fk": {
2443 + "name": "user_missions_user_id_users_id_fk",
2444 + "tableFrom": "user_missions",
2445 + "tableTo": "users",
2446 + "columnsFrom": [
2447 + "user_id"
2448 + ],
2449 + "columnsTo": [
2450 + "id"
2451 + ],
2452 + "onDelete": "cascade",
2453 + "onUpdate": "no action"
2454 + },
2455 + "user_missions_mission_key_missions_key_fk": {
2456 + "name": "user_missions_mission_key_missions_key_fk",
2457 + "tableFrom": "user_missions",
2458 + "tableTo": "missions",
2459 + "columnsFrom": [
2460 + "mission_key"
2461 + ],
2462 + "columnsTo": [
2463 + "key"
2464 + ],
2465 + "onDelete": "cascade",
2466 + "onUpdate": "no action"
2467 + }
2468 + },
2469 + "compositePrimaryKeys": {},
2470 + "uniqueConstraints": {},
2471 + "policies": {},
2472 + "checkConstraints": {},
2473 + "isRLSEnabled": false
2474 + },
2475 + "public.user_settings": {
2476 + "name": "user_settings",
2477 + "schema": "",
2478 + "columns": {
2479 + "user_id": {
2480 + "name": "user_id",
2481 + "type": "uuid",
2482 + "primaryKey": true,
2483 + "notNull": true
2484 + },
2485 + "sound_enabled": {
2486 + "name": "sound_enabled",
2487 + "type": "boolean",
2488 + "primaryKey": false,
2489 + "notNull": true,
2490 + "default": true
2491 + },
2492 + "music_volume": {
2493 + "name": "music_volume",
2494 + "type": "numeric(3, 2)",
2495 + "primaryKey": false,
2496 + "notNull": true,
2497 + "default": "'0.6'"
2498 + },
2499 + "effects_volume": {
2500 + "name": "effects_volume",
2501 + "type": "numeric(3, 2)",
2502 + "primaryKey": false,
2503 + "notNull": true,
2504 + "default": "'0.8'"
2505 + },
2506 + "master_volume": {
2507 + "name": "master_volume",
2508 + "type": "numeric(3, 2)",
2509 + "primaryKey": false,
2510 + "notNull": true,
2511 + "default": "'0.8'"
2512 + },
2513 + "reduce_motion": {
2514 + "name": "reduce_motion",
2515 + "type": "boolean",
2516 + "primaryKey": false,
2517 + "notNull": true,
2518 + "default": false
2519 + },
2520 + "animation_intensity": {
2521 + "name": "animation_intensity",
2522 + "type": "varchar(8)",
2523 + "primaryKey": false,
2524 + "notNull": true,
2525 + "default": "'high'"
2526 + },
2527 + "session_reminder_minutes": {
2528 + "name": "session_reminder_minutes",
2529 + "type": "integer",
2530 + "primaryKey": false,
2531 + "notNull": true,
2532 + "default": 60
2533 + },
2534 + "break_reminder": {
2535 + "name": "break_reminder",
2536 + "type": "boolean",
2537 + "primaryKey": false,
2538 + "notNull": true,
2539 + "default": true
2540 + },
2541 + "leaderboard_opt_in": {
2542 + "name": "leaderboard_opt_in",
2543 + "type": "boolean",
2544 + "primaryKey": false,
2545 + "notNull": true,
2546 + "default": true
2547 + },
2548 + "updated_at": {
2549 + "name": "updated_at",
2550 + "type": "timestamp with time zone",
2551 + "primaryKey": false,
2552 + "notNull": true,
2553 + "default": "now()"
2554 + }
2555 + },
2556 + "indexes": {},
2557 + "foreignKeys": {
2558 + "user_settings_user_id_users_id_fk": {
2559 + "name": "user_settings_user_id_users_id_fk",
2560 + "tableFrom": "user_settings",
2561 + "tableTo": "users",
2562 + "columnsFrom": [
2563 + "user_id"
2564 + ],
2565 + "columnsTo": [
2566 + "id"
2567 + ],
2568 + "onDelete": "cascade",
2569 + "onUpdate": "no action"
2570 + }
2571 + },
2572 + "compositePrimaryKeys": {},
2573 + "uniqueConstraints": {},
2574 + "policies": {},
2575 + "checkConstraints": {},
2576 + "isRLSEnabled": false
2577 + },
2578 + "public.users": {
2579 + "name": "users",
2580 + "schema": "",
2581 + "columns": {
2582 + "id": {
2583 + "name": "id",
2584 + "type": "uuid",
2585 + "primaryKey": true,
2586 + "notNull": true,
2587 + "default": "gen_random_uuid()"
2588 + },
2589 + "username": {
2590 + "name": "username",
2591 + "type": "varchar(24)",
2592 + "primaryKey": false,
2593 + "notNull": true
2594 + },
2595 + "username_normalized": {
2596 + "name": "username_normalized",
2597 + "type": "varchar(24)",
2598 + "primaryKey": false,
2599 + "notNull": true
2600 + },
2601 + "password_hash": {
2602 + "name": "password_hash",
2603 + "type": "text",
2604 + "primaryKey": false,
2605 + "notNull": true
2606 + },
2607 + "level": {
2608 + "name": "level",
2609 + "type": "integer",
2610 + "primaryKey": false,
2611 + "notNull": true,
2612 + "default": 1
2613 + },
2614 + "xp": {
2615 + "name": "xp",
2616 + "type": "bigint",
2617 + "primaryKey": false,
2618 + "notNull": true,
2619 + "default": 0
2620 + },
2621 + "status": {
2622 + "name": "status",
2623 + "type": "varchar(16)",
2624 + "primaryKey": false,
2625 + "notNull": true,
2626 + "default": "'active'"
2627 + },
2628 + "age_confirmed_at": {
2629 + "name": "age_confirmed_at",
2630 + "type": "timestamp with time zone",
2631 + "primaryKey": false,
2632 + "notNull": false
2633 + },
2634 + "created_at": {
2635 + "name": "created_at",
2636 + "type": "timestamp with time zone",
2637 + "primaryKey": false,
2638 + "notNull": true,
2639 + "default": "now()"
2640 + },
2641 + "last_login_at": {
2642 + "name": "last_login_at",
2643 + "type": "timestamp with time zone",
2644 + "primaryKey": false,
2645 + "notNull": false
2646 + },
2647 + "last_rescue_at": {
2648 + "name": "last_rescue_at",
2649 + "type": "timestamp with time zone",
2650 + "primaryKey": false,
2651 + "notNull": false
2652 + },
2653 + "total_spins": {
2654 + "name": "total_spins",
2655 + "type": "bigint",
2656 + "primaryKey": false,
2657 + "notNull": true,
2658 + "default": 0
2659 + },
2660 + "games_played": {
2661 + "name": "games_played",
2662 + "type": "integer",
2663 + "primaryKey": false,
2664 + "notNull": true,
2665 + "default": 0
2666 + },
2667 + "biggest_win": {
2668 + "name": "biggest_win",
2669 + "type": "bigint",
2670 + "primaryKey": false,
2671 + "notNull": true,
2672 + "default": 0
2673 + },
2674 + "biggest_multiplier": {
2675 + "name": "biggest_multiplier",
2676 + "type": "numeric(12, 2)",
2677 + "primaryKey": false,
2678 + "notNull": true,
2679 + "default": "'0'"
2680 + }
2681 + },
2682 + "indexes": {
2683 + "users_username_normalized_idx": {
2684 + "name": "users_username_normalized_idx",
2685 + "columns": [
2686 + {
2687 + "expression": "username_normalized",
2688 + "isExpression": false,
2689 + "asc": true,
2690 + "nulls": "last"
2691 + }
2692 + ],
2693 + "isUnique": true,
2694 + "concurrently": false,
2695 + "method": "btree",
2696 + "with": {}
2697 + },
2698 + "users_username_idx": {
2699 + "name": "users_username_idx",
2700 + "columns": [
2701 + {
2702 + "expression": "username",
2703 + "isExpression": false,
2704 + "asc": true,
2705 + "nulls": "last"
2706 + }
2707 + ],
2708 + "isUnique": true,
2709 + "concurrently": false,
2710 + "method": "btree",
2711 + "with": {}
2712 + }
2713 + },
2714 + "foreignKeys": {},
2715 + "compositePrimaryKeys": {},
2716 + "uniqueConstraints": {},
2717 + "policies": {},
2718 + "checkConstraints": {},
2719 + "isRLSEnabled": false
2720 + },
2721 + "public.wallets": {
2722 + "name": "wallets",
2723 + "schema": "",
2724 + "columns": {
2725 + "user_id": {
2726 + "name": "user_id",
2727 + "type": "uuid",
2728 + "primaryKey": true,
2729 + "notNull": true
2730 + },
2731 + "balance": {
2732 + "name": "balance",
2733 + "type": "bigint",
2734 + "primaryKey": false,
2735 + "notNull": true,
2736 + "default": 0
2737 + },
2738 + "lifetime_wagered": {
2739 + "name": "lifetime_wagered",
2740 + "type": "bigint",
2741 + "primaryKey": false,
2742 + "notNull": true,
2743 + "default": 0
2744 + },
2745 + "lifetime_won": {
2746 + "name": "lifetime_won",
2747 + "type": "bigint",
2748 + "primaryKey": false,
2749 + "notNull": true,
2750 + "default": 0
2751 + },
2752 + "lifetime_granted": {
2753 + "name": "lifetime_granted",
2754 + "type": "bigint",
2755 + "primaryKey": false,
2756 + "notNull": true,
2757 + "default": 0
2758 + },
2759 + "updated_at": {
2760 + "name": "updated_at",
2761 + "type": "timestamp with time zone",
2762 + "primaryKey": false,
2763 + "notNull": true,
2764 + "default": "now()"
2765 + }
2766 + },
2767 + "indexes": {},
2768 + "foreignKeys": {
2769 + "wallets_user_id_users_id_fk": {
2770 + "name": "wallets_user_id_users_id_fk",
2771 + "tableFrom": "wallets",
2772 + "tableTo": "users",
2773 + "columnsFrom": [
2774 + "user_id"
2775 + ],
2776 + "columnsTo": [
2777 + "id"
2778 + ],
2779 + "onDelete": "cascade",
2780 + "onUpdate": "no action"
2781 + }
2782 + },
2783 + "compositePrimaryKeys": {},
2784 + "uniqueConstraints": {},
2785 + "policies": {},
2786 + "checkConstraints": {},
2787 + "isRLSEnabled": false
2788 + }
2789 + },
2790 + "enums": {},
2791 + "schemas": {},
2792 + "sequences": {},
2793 + "roles": {},
2794 + "policies": {},
2795 + "views": {},
2796 + "_meta": {
2797 + "columns": {},
2798 + "schemas": {},
2799 + "tables": {}
2800 + }
2801 +}
\ No newline at end of file
modified packages/database/drizzle/meta/_journal.json +7 −0
@@ -8,6 +8,13 @@
8 8 "when": 1788821559876,
9 9 "tag": "0000_cuddly_shen",
10 10 "breakpoints": true
11 + },
12 + {
13 + "idx": 1,
14 + "version": "7",
15 + "when": 1788836033256,
16 + "tag": "0001_lumpy_hulk",
17 + "breakpoints": true
11 18 }
12 19 ]
13 20 }
\ No newline at end of file
modified packages/database/src/schema.ts +31 −0
@@ -175,6 +175,37 @@ export const gameRounds = pgTable(
175 175 ],
176 176 );
177 177
178 +/** In-flight cash-out ("crash") rounds. Settled rounds are also written to game_rounds. */
179 +export const crashRounds = pgTable(
180 + "crash_rounds",
181 + {
182 + id: uuid().primaryKey().defaultRandom(),
183 + roundId: varchar({ length: 32 }).notNull(),
184 + userId: uuid().notNull().references(() => users.id, { onDelete: "cascade" }),
185 + gameId: uuid().notNull().references(() => games.id),
186 + gameSlug: varchar({ length: 64 }).notNull(),
187 + gameVersion: varchar({ length: 16 }).notNull(),
188 + clientRoundId: uuid().notNull(),
189 + bet: bigint({ mode: "number" }).notNull(),
190 + crashMultiplier: numeric({ precision: 12, scale: 2 }).notNull(),
191 + crashAtMs: integer().notNull(),
192 + seed: text().notNull(),
193 + commitment: varchar({ length: 64 }).notNull(),
194 + events: jsonb().$type<unknown[]>().notNull().default(sql`'[]'::jsonb`),
195 + autoCashout: numeric({ precision: 12, scale: 2 }),
196 + status: varchar({ length: 12 }).notNull().default("running"), // running | cashed | crashed
197 + cashoutMultiplier: numeric({ precision: 12, scale: 2 }),
198 + win: bigint({ mode: "number" }).notNull().default(0),
199 + startedAt: now(),
200 + settledAt: ts(),
201 + },
202 + (t) => [
203 + uniqueIndex("crash_rounds_round_id_idx").on(t.roundId),
204 + uniqueIndex("crash_rounds_user_client_idx").on(t.userId, t.clientRoundId),
205 + index("crash_rounds_user_status_idx").on(t.userId, t.status),
206 + ],
207 +);
208 +
178 209 export const gameStatistics = pgTable("game_statistics", {
179 210 gameId: uuid().primaryKey().references(() => games.id, { onDelete: "cascade" }),
180 211 launches: bigint({ mode: "number" }).notNull().default(0),
modified packages/database/src/sync-games.ts +50 −2
@@ -2,8 +2,8 @@ import fs from "node:fs";
2 2 import path from "node:path";
3 3 import { createHash } from "node:crypto";
4 4 import { createRequire } from "node:module";
5 import { GAMES, FEATURED_ORDER } from "@spinza/games";
6 import { validateDefinition, type CertificationReport, type GameDefinition } from "@spinza/game-core";
5 +import { GAMES, FEATURED_ORDER, CRASH_GAMES } from "@spinza/games";
6 +import { validateDefinition, type CertificationReport, type GameDefinition, type CrashGameDefinition } from "@spinza/game-core";
7 7 import { eq, sql } from "drizzle-orm";
8 8 import type { Db } from "./index";
9 9 import { gameStatistics, gameVersions, games } from "./schema";
@@ -24,8 +24,31 @@ export function loadCertification(slug: string): CertificationReport | null {
24 24 }
25 25 }
26 26
27 +export function crashSummary(def: CrashGameDefinition) {
28 + return {
29 + kind: "crash",
30 + category: "risk",
31 + tagline: def.tagline,
32 + description: def.description,
33 + theme: def.theme,
34 + volatility: def.volatility,
35 + grid: { reels: 0, rows: 0 },
36 + minBet: def.minBet,
37 + maxBet: def.maxBet,
38 + maxMultiplier: def.maxMultiplier,
39 + features: def.featureNames,
40 + tags: def.tags,
41 + palette: def.presentation.palette,
42 + presentation: { frame: "glass", backdrop: def.presentation.scene, particles: "energy", ambience: def.presentation.ambience, scene: def.presentation.scene, verb: def.presentation.verb },
43 + isJackpot: false,
44 + rtp: def.rtp,
45 + };
46 +}
47 +
27 48 export function publicSummary(def: GameDefinition) {
28 49 return {
50 + kind: "slot",
51 + category: "slots",
29 52 tagline: def.tagline,
30 53 description: def.description,
31 54 theme: def.theme,
@@ -120,5 +143,30 @@ export async function syncGames(db: Db, log: (s: string) => void = () => {}): Pr
120 143 reports.push({ slug: def.slug, version: def.version, lifecycle, certified });
121 144 log(` ${certified ? "✓" : "·"} ${def.slug}@${def.version} → ${lifecycle}`);
122 145 }
146 + for (const def of CRASH_GAMES) {
147 + order++;
148 + const cert = loadCertification(def.slug);
149 + const certified = !!cert && cert.status === "PASS" && cert.version === def.version;
150 + const existing = await db.query.games.findFirst({ where: eq(games.slug, def.slug) });
151 + let lifecycle = certified ? "published" : "simulation";
152 + if (existing?.lifecycle === "disabled") lifecycle = "disabled";
153 + const summary = { ...crashSummary(def), certification: cert ? { status: cert.status, spins: cert.spins, observedRtp: cert.observedRtp, hitRate: cert.hitRate, certifiedAt: cert.certifiedAt } : null };
154 + const isNew = existing ? existing.isNew && Date.now() - existing.createdAt.getTime() < 14 * 86400_000 : true;
155 + const [row] = await db
156 + .insert(games)
157 + .values({ slug: def.slug, name: def.name, version: def.version, lifecycle, sortOrder: order, isFeatured: false, isNew, summary, publishedAt: certified ? new Date() : null })
158 + .onConflictDoUpdate({
159 + target: games.slug,
160 + set: { name: def.name, version: def.version, lifecycle, sortOrder: order, isNew, summary, publishedAt: certified ? sql`coalesce(${games.publishedAt}, now())` : games.publishedAt, updatedAt: new Date() },
161 + })
162 + .returning();
163 + await db
164 + .insert(gameVersions)
165 + .values({ gameId: row.id, version: def.version, definition: def as unknown as Record<string, unknown>, definitionHash: createHash("sha256").update(JSON.stringify(def)).digest("hex"), certification: (cert as unknown as Record<string, unknown>) ?? null, status: certified ? "published" : "simulation" })
166 + .onConflictDoUpdate({ target: [gameVersions.gameId, gameVersions.version], set: { definition: def as unknown as Record<string, unknown>, certification: (cert as unknown as Record<string, unknown>) ?? null, status: certified ? "published" : "simulation" } });
167 + await db.insert(gameStatistics).values({ gameId: row.id }).onConflictDoNothing();
168 + reports.push({ slug: def.slug, version: def.version, lifecycle, certified });
169 + log(` ${certified ? "✓" : "·"} ${def.slug}@${def.version} (crash) → ${lifecycle}`);
170 + }
123 171 return reports;
124 172 }
modified packages/game-core/src/client.ts +2 −0
@@ -8,3 +8,5 @@ export type { CertificationReport, CertificationRules, ValidationIssue } from ".
8 8 export { emptyPlayerState } from "./types";
9 9 export { waysCount } from "./define";
10 10 export { LINES_10, LINES_20, LINES_25 } from "./evaluate";
11 +export type { CrashGameDefinition, CrashEvent, CrashCurve, CrashScene, CrashMilestone } from "./crash/curve";
12 +export { multiplierAt, effectiveTime, timeForMultiplier, floorAt } from "./crash/curve";
added packages/game-core/src/crash/curve.ts +162 −0
@@ -0,0 +1,162 @@
1 +/**
2 + * Crash curve math shared by server and browser (pure, no crypto).
3 + */
4 +export type CrashScene = "sky" | "ocean" | "rocket" | "bank" | "volcano" | "blackhole" | "freefall" | "reactor" | "storm" | "elevator";
5 +
6 +export type CrashCurve =
7 + /** m(τ) = e^(k·τ) — classic steady acceleration. */
8 + | { type: "exp"; k: number }
9 + /** m(τ) = 1 + a·τ^p — slow start, brutal end (freefall, black hole). */
10 + | { type: "power"; a: number; p: number }
11 + /** Discrete floors: m = base^floor, one floor every `stepSeconds` (elevator). */
12 + | { type: "steps"; stepSeconds: number; base: number };
13 +
14 +export interface CrashEventKind {
15 + id: string;
16 + label: string;
17 + /** Speed factor applied to the time flow while the window is active (>1 boost, <1 cooling). */
18 + factor: number;
19 + /** Window duration in seconds. */
20 + duration: [number, number];
21 + weight: number;
22 +}
23 +
24 +export interface CrashEventsConfig {
25 + /** Chance that a round contains at least one event. */
26 + chance: number;
27 + /** Max events per round. */
28 + max: number;
29 + /** Earliest / latest start (seconds of *real* time). */
30 + window: [number, number];
31 + kinds: CrashEventKind[];
32 +}
33 +
34 +export interface CrashMilestone {
35 + multiplier: number;
36 + label: string;
37 +}
38 +
39 +export interface CrashGameDefinition {
40 + kind: "crash";
41 + slug: string;
42 + name: string;
43 + version: string;
44 + tagline: string;
45 + description: string;
46 + theme: string;
47 + tags: string[];
48 + rtp: number;
49 + minBet: number;
50 + maxBet: number;
51 + /** Hard cap on the crash multiplier. */
52 + maxMultiplier: number;
53 + curve: CrashCurve;
54 + events?: CrashEventsConfig;
55 + /** Story beats shown as the multiplier passes them (cosmetic). */
56 + milestones: CrashMilestone[];
57 + presentation: {
58 + scene: CrashScene;
59 + palette: { primary: string; secondary: string; glow: string; bg: string; surface: string };
60 + /** Main button label. */
61 + verb: string;
62 + /** Progress unit shown next to the multiplier, e.g. "m", "km", "°C", "floor". */
63 + unit: string;
64 + /** Unit value at multiplier x (cosmetic mapping). */
65 + unitScale: number;
66 + ambience: string;
67 + };
68 + rules: string[];
69 + featureNames: string[];
70 + volatility: "low" | "medium" | "high" | "extreme";
71 +}
72 +
73 +export interface CrashEvent {
74 + id: string;
75 + label: string;
76 + /** Real-time window in ms since round start. */
77 + startMs: number;
78 + endMs: number;
79 + factor: number;
80 +}
81 +
82 +export interface CrashRoundSetup {
83 + crashMultiplier: number;
84 + /** ms of real time at which the crash occurs (derived from the curve + events). */
85 + crashAtMs: number;
86 + seed: string;
87 + commitment: string;
88 + events: CrashEvent[];
89 +}
90 +
91 +/* ---------------------------------------------------------------- curve */
92 +
93 +/** Effective ("game") time after applying event speed windows, in seconds. */
94 +export function effectiveTime(events: CrashEvent[], elapsedMs: number): number {
95 + let tau = 0;
96 + let cursor = 0;
97 + const sorted = [...events].sort((a, b) => a.startMs - b.startMs);
98 + for (const e of sorted) {
99 + if (elapsedMs <= cursor) break;
100 + const plainEnd = Math.min(e.startMs, elapsedMs);
101 + if (plainEnd > cursor) tau += (plainEnd - cursor) / 1000;
102 + const winEnd = Math.min(e.endMs, elapsedMs);
103 + if (winEnd > e.startMs) tau += ((winEnd - e.startMs) / 1000) * e.factor;
104 + cursor = Math.max(cursor, e.endMs);
105 + }
106 + if (elapsedMs > cursor) tau += (elapsedMs - cursor) / 1000;
107 + return tau;
108 +}
109 +
110 +function curveValue(curve: CrashCurve, tau: number): number {
111 + switch (curve.type) {
112 + case "exp":
113 + return Math.exp(curve.k * tau);
114 + case "power":
115 + return 1 + curve.a * Math.pow(tau, curve.p);
116 + case "steps":
117 + return Math.pow(curve.base, Math.floor(tau / curve.stepSeconds));
118 + }
119 +}
120 +
121 +function curveInverse(curve: CrashCurve, m: number): number {
122 + switch (curve.type) {
123 + case "exp":
124 + return Math.log(Math.max(1, m)) / curve.k;
125 + case "power":
126 + return Math.pow(Math.max(0, m - 1) / curve.a, 1 / curve.p);
127 + case "steps":
128 + return Math.ceil(Math.log(Math.max(1, m)) / Math.log(curve.base)) * curve.stepSeconds;
129 + }
130 +}
131 +
132 +/** Multiplier displayed/settled at `elapsedMs` of real time (2-decimal floor). */
133 +export function multiplierAt(def: CrashGameDefinition, events: CrashEvent[], elapsedMs: number): number {
134 + const tau = effectiveTime(events, Math.max(0, elapsedMs));
135 + const m = curveValue(def.curve, tau);
136 + return Math.min(def.maxMultiplier, Math.floor(m * 100) / 100);
137 +}
138 +
139 +/** Real time (ms) at which the curve first reaches `m`. */
140 +export function timeForMultiplier(def: CrashGameDefinition, events: CrashEvent[], m: number): number {
141 + const targetTau = curveInverse(def.curve, m);
142 + // Invert effectiveTime by walking the windows.
143 + let tau = 0;
144 + let cursor = 0;
145 + const sorted = [...events].sort((a, b) => a.startMs - b.startMs);
146 + for (const e of sorted) {
147 + const plain = (e.startMs - cursor) / 1000;
148 + if (tau + plain >= targetTau) return cursor + (targetTau - tau) * 1000;
149 + tau += plain;
150 + const win = ((e.endMs - e.startMs) / 1000) * e.factor;
151 + if (tau + win >= targetTau) return e.startMs + ((targetTau - tau) / e.factor) * 1000;
152 + tau += win;
153 + cursor = e.endMs;
154 + }
155 + return cursor + (targetTau - tau) * 1000;
156 +}
157 +
158 +/** Floor number for the elevator (cosmetic). */
159 +export function floorAt(def: CrashGameDefinition, m: number): number {
160 + return def.curve.type === "steps" ? Math.round(Math.log(m) / Math.log(def.curve.base)) : 0;
161 +}
162 +
added packages/game-core/src/crash/index.ts +228 −0
@@ -0,0 +1,228 @@
1 +/**
2 + * Spinza Risk Games ("cash-out" / crash mechanic).
3 + *
4 + * A round starts, a multiplier grows with time, and the player must cash out
5 + * before the secret crash point. The crash multiplier is drawn server-side at
6 + * round start from a distribution with a fixed RTP for ANY cash-out strategy:
7 + *
8 + * P(crash ≥ x) = rtp / x for x ≥ 1, P(instant crash at 1.00) = 1 − rtp
9 + *
10 + * so the expected return of "cash out at x" is x · rtp / x = rtp. Pacing
11 + * (curve shape, boost/cooling windows) never changes the odds — it only
12 + * changes how the same multiplier value is reached over time.
13 + */
14 +import { createHash, randomBytes } from "node:crypto";
15 +import type { Rng } from "../rng";
16 +import { CryptoRng } from "../rng";
17 +
18 +import type { CrashGameDefinition, CrashEvent, CrashRoundSetup } from "./curve";
19 +import { timeForMultiplier } from "./curve";
20 +export * from "./curve";
21 +
22 +/* -------------------------------------------------------------- drawing */
23 +
24 +export function drawCrashMultiplier(def: CrashGameDefinition, rng: Rng): number {
25 + const u = rng.float();
26 + if (u <= 0 || u > def.rtp) return 1;
27 + const m = def.rtp / u;
28 + return Math.min(def.maxMultiplier, Math.floor(m * 100) / 100);
29 +}
30 +
31 +export function generateEvents(def: CrashGameDefinition, rng: Rng): CrashEvent[] {
32 + const cfg = def.events;
33 + if (!cfg || !rng.chance(cfg.chance)) return [];
34 + const n = 1 + rng.int(cfg.max);
35 + const out: CrashEvent[] = [];
36 + let cursor = cfg.window[0] * 1000;
37 + for (let i = 0; i < n; i++) {
38 + const kind = cfg.kinds[rng.weighted(cfg.kinds.map((k) => k.weight))];
39 + const start = cursor + rng.float() * Math.max(500, (cfg.window[1] * 1000 - cursor) / (n - i));
40 + const dur = (kind.duration[0] + rng.float() * (kind.duration[1] - kind.duration[0])) * 1000;
41 + out.push({ id: kind.id, label: kind.label, startMs: Math.round(start), endMs: Math.round(start + dur), factor: kind.factor });
42 + cursor = start + dur + 400;
43 + if (cursor > cfg.window[1] * 1000) break;
44 + }
45 + return out;
46 +}
47 +
48 +/** Create the secret setup for a round. `seed` must stay private until the round settles. */
49 +export function setupCrashRound(def: CrashGameDefinition, rng: Rng = new CryptoRng()): CrashRoundSetup {
50 + const seed = randomBytes(16).toString("hex");
51 + const crashMultiplier = drawCrashMultiplier(def, rng);
52 + const events = generateEvents(def, rng);
53 + const commitment = createHash("sha256").update(`${seed}:${crashMultiplier.toFixed(2)}`).digest("hex");
54 + return { crashMultiplier, crashAtMs: timeForMultiplier(def, events, crashMultiplier), seed, commitment, events };
55 +}
56 +
57 +export function verifyCommitment(seed: string, crashMultiplier: number, commitment: string): boolean {
58 + return createHash("sha256").update(`${seed}:${crashMultiplier.toFixed(2)}`).digest("hex") === commitment;
59 +}
60 +
61 +/* ----------------------------------------------------------- simulation */
62 +
63 +export interface CrashSimulationResult {
64 + game: string;
65 + version: string;
66 + spins: number;
67 + bet: number;
68 + configuredRtp: number;
69 + observedRtp: number;
70 + deviation: number;
71 + hitRate: number;
72 + bonusRate: number;
73 + freeSpinRate: number;
74 + jackpotRate: number;
75 + wagered: number;
76 + returned: number;
77 + averageWin: number;
78 + medianWin: number;
79 + maxWin: number;
80 + maxWinMultiplier: number;
81 + stdDev: number;
82 + distribution: { label: string; min: number; max: number; count: number; share: number }[];
83 + convergence: { spins: number; rtp: number }[];
84 + featureCounts: Record<string, number>;
85 + cappedRounds: number;
86 + durationMs: number;
87 +}
88 +
89 +/**
90 + * Simulate rounds against a mixed population of players: cash-out targets are
91 + * drawn log-uniformly between 1.05× and 50× (plus 10% "greedy" targets up to 500×).
92 + * RTP is strategy-independent by construction; the simulation verifies the
93 + * implementation end-to-end (drawing, capping, rounding).
94 + */
95 +export function simulateCrash(def: CrashGameDefinition, opts: { spins: number; bet?: number; rng?: Rng }): CrashSimulationResult {
96 + const rng = opts.rng ?? new CryptoRng();
97 + const bet = opts.bet ?? 100;
98 + const started = Date.now();
99 + const n = opts.spins;
100 + let returned = 0;
101 + let hits = 0;
102 + let sum = 0;
103 + let sumSq = 0;
104 + let maxWin = 0;
105 + let capped = 0;
106 + const buckets = [
107 + { label: "0×", min: 0, max: 0 },
108 + { label: "1–1.5×", min: 1, max: 1.5 },
109 + { label: "1.5–2×", min: 1.5, max: 2 },
110 + { label: "2–5×", min: 2, max: 5 },
111 + { label: "5–10×", min: 5, max: 10 },
112 + { label: "10–20×", min: 10, max: 20 },
113 + { label: "20–50×", min: 20, max: 50 },
114 + { label: "50–100×", min: 50, max: 100 },
115 + { label: "100–500×", min: 100, max: 500 },
116 + { label: "500×+", min: 500, max: Infinity },
117 + ];
118 + const counts = new Array(buckets.length).fill(0);
119 + const convergence: { spins: number; rtp: number }[] = [];
120 + const every = Math.max(1, Math.floor(n / 40));
121 + const feature: Record<string, number> = {};
122 + const sample: number[] = [];
123 + for (let i = 0; i < n; i++) {
124 + const crash = drawCrashMultiplier(def, rng);
125 + if (crash >= def.maxMultiplier) capped++;
126 + const greedy = rng.chance(0.1);
127 + const target = Math.floor(Math.exp(Math.log(1.05) + rng.float() * (Math.log(greedy ? 500 : 50) - Math.log(1.05))) * 100) / 100;
128 + const win = crash > target ? Math.round(bet * target) : 0;
129 + const events = generateEvents(def, rng);
130 + for (const e of events) feature[e.label] = (feature[e.label] ?? 0) + 1;
131 + returned += win;
132 + if (win > 0) hits++;
133 + const m = win / bet;
134 + sum += m;
135 + sumSq += m * m;
136 + if (win > maxWin) maxWin = win;
137 + for (let b = 0; b < buckets.length; b++) if ((buckets[b].max === 0 && m === 0) || (m >= buckets[b].min && m < buckets[b].max && buckets[b].max !== 0)) {
138 + counts[b]++;
139 + break;
140 + }
141 + if (sample.length < 100_000) sample.push(m);
142 + if ((i + 1) % every === 0 || i === n - 1) convergence.push({ spins: i + 1, rtp: returned / ((i + 1) * bet) });
143 + }
144 + const mean = sum / n;
145 + const variance = Math.max(0, sumSq / n - mean * mean);
146 + sample.sort((a, b) => a - b);
147 + const observedRtp = returned / (n * bet);
148 + return {
149 + game: def.slug,
150 + version: def.version,
151 + spins: n,
152 + bet,
153 + configuredRtp: def.rtp,
154 + observedRtp,
155 + deviation: observedRtp - def.rtp,
156 + hitRate: hits / n,
157 + bonusRate: 0,
158 + freeSpinRate: 0,
159 + jackpotRate: 0,
160 + wagered: n * bet,
161 + returned,
162 + averageWin: returned / n,
163 + medianWin: (sample[Math.floor(sample.length / 2)] ?? 0) * bet,
164 + maxWin,
165 + maxWinMultiplier: maxWin / bet,
166 + stdDev: Math.sqrt(variance),
167 + distribution: buckets.map((b, i) => ({ ...b, count: counts[i], share: counts[i] / n })),
168 + convergence,
169 + featureCounts: feature,
170 + cappedRounds: capped,
171 + durationMs: Date.now() - started,
172 + };
173 +}
174 +
175 +/* --------------------------------------------------------------- define */
176 +
177 +type CrashInput = Omit<CrashGameDefinition, "kind" | "featureNames" | "tags" | "minBet" | "maxBet"> & Partial<Pick<CrashGameDefinition, "featureNames" | "tags" | "minBet" | "maxBet">>;
178 +
179 +export function defineCrashGame(input: CrashInput): CrashGameDefinition {
180 + return {
181 + kind: "crash",
182 + minBet: 10,
183 + maxBet: 1000,
184 + tags: ["risk", "cash-out"],
185 + featureNames: ["Cash out anytime", "Auto cash-out", "Provably fair", ...(input.events ? ["Live events"] : [])],
186 + ...input,
187 + };
188 +}
189 +
190 +/* ---------------------------------------------------------- certification */
191 +
192 +import { DEFAULT_CERTIFICATION_RULES, type CertificationReport, type CertificationRules } from "../validation";
193 +
194 +/** Certification for crash games: the RTP must match the analytic value for the mixed-strategy population. */
195 +export function certifyCrash(def: CrashGameDefinition, sim: CrashSimulationResult, rules: CertificationRules = DEFAULT_CERTIFICATION_RULES): CertificationReport {
196 + const se = sim.stdDev / Math.sqrt(sim.spins);
197 + const tolerance = Math.min(0.015, Math.max(rules.maxDeviation, 3 * se));
198 + const checks = [
199 + { name: "definition", pass: def.rtp >= 0.94 && def.rtp <= 0.98 && def.maxMultiplier >= 10, detail: `rtp ${def.rtp}, cap ${def.maxMultiplier}×` },
200 + { name: "spins", pass: sim.spins >= rules.minSpins, detail: `${sim.spins.toLocaleString("en-US")} rounds` },
201 + { name: "rtp-deviation", pass: Math.abs(sim.deviation) <= tolerance, detail: `${(sim.deviation * 100).toFixed(3)}% (tolerance ±${(tolerance * 100).toFixed(2)}%)` },
202 + { name: "rtp-band", pass: sim.observedRtp >= rules.rtpBand[0] && sim.observedRtp <= rules.rtpBand[1], detail: `${(sim.observedRtp * 100).toFixed(2)}%` },
203 + { name: "max-win", pass: sim.maxWinMultiplier <= def.maxMultiplier + 1e-9, detail: `${sim.maxWinMultiplier.toFixed(1)}× (cap ${def.maxMultiplier}×)` },
204 + { name: "commitment", pass: (() => { const s = setupCrashRound(def); return verifyCommitment(s.seed, s.crashMultiplier, s.commitment) && s.crashAtMs >= 0; })(), detail: "SHA-256 commit/reveal verified" },
205 + ];
206 + return {
207 + game: def.slug,
208 + name: def.name,
209 + version: def.version,
210 + spins: sim.spins,
211 + configuredRtp: def.rtp,
212 + observedRtp: sim.observedRtp,
213 + deviation: sim.deviation,
214 + hitRate: sim.hitRate,
215 + bonusRate: 0,
216 + freeSpinRate: 0,
217 + maxWinMultiplier: sim.maxWinMultiplier,
218 + stdDev: sim.stdDev,
219 + status: checks.every((c) => c.pass) ? "PASS" : "FAIL",
220 + checks,
221 + certifiedAt: new Date().toISOString(),
222 + rules,
223 + distribution: sim.distribution,
224 + convergence: sim.convergence,
225 + featureCounts: sim.featureCounts,
226 + durationMs: sim.durationMs,
227 + };
228 +}
modified packages/game-core/src/index.ts +1 −0
@@ -8,3 +8,4 @@ export * from "./engine";
8 8 export * from "./simulation";
9 9 export * from "./validation";
10 10 export * from "./define";
11 +export * from "./crash";
11 12