SPB Git

spb/spinza

Public

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 (Sep 8, 2026) parent 51ee542 Browse files

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
Large diff (297 lines) is not loaded by default to keep the page fast.Load diff
added games/certifications/freefall.json +299 −0
Large diff (299 lines) is not loaded by default to keep the page fast.Load diff
added games/certifications/rocket-run.json +299 −0
Large diff (299 lines) is not loaded by default to keep the page fast.Load diff
added games/certifications/skyfall.json +299 −0
Large diff (299 lines) is not loaded by default to keep the page fast.Load diff
added games/certifications/storm-chase.json +299 −0
Large diff (299 lines) is not loaded by default to keep the page fast.Load diff
added games/certifications/volcano.json +299 −0
Large diff (299 lines) is not loaded by default to keep the page fast.Load diff
modified games/src/client.ts +1 −0
Large diff (4 lines) is not loaded by default to keep the page fast.Load diff
added games/src/crash/index.ts +238 −0
Large diff (238 lines) is not loaded by default to keep the page fast.Load diff
modified games/src/index.ts +1 −0
Large diff (4 lines) is not loaded by default to keep the page fast.Load diff
added packages/database/drizzle/0001_lumpy_hulk.sql +27 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added packages/database/drizzle/meta/0001_snapshot.json +2801 −0
Large diff (2,802 lines) is not loaded by default to keep the page fast.Load diff
modified packages/database/drizzle/meta/_journal.json +7 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
modified packages/database/src/schema.ts +31 −0
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
modified packages/database/src/sync-games.ts +50 −2
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
modified packages/game-core/src/client.ts +2 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
added packages/game-core/src/crash/curve.ts +162 −0
Large diff (162 lines) is not loaded by default to keep the page fast.Load diff
added packages/game-core/src/crash/index.ts +228 −0
Large diff (228 lines) is not loaded by default to keep the page fast.Load diff
modified packages/game-core/src/index.ts +1 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme