"use client"; import * as React from "react"; import type { CertificationReport, SimulationResult } from "@spinza/game-core"; import { Bars, ChartFrame, CHART, Histogram, Lines } from "./charts"; import { KV, Panel, PassFail, SegmentedControl, StatGrid, StatTile } from "./primitives"; import { durationMs, int, multiplier, pct, sc, signedPct } from "./format"; import { cn } from "@/lib/utils"; /** KPI tiles + charts for a finished simulation. Shared by the simulator and the game detail page. */ export function SimulationResults({ sim, cert, compactTiles }: { sim: SimulationResult; cert: CertificationReport | null; compactTiles?: boolean }) { const [scale, setScale] = React.useState<"log" | "linear">("log"); const devTone = Math.abs(sim.deviation) <= 0.004 ? "success" : Math.abs(sim.deviation) <= 0.015 ? "neutral" : "danger"; const features = Object.entries(sim.featureCounts) .map(([feature, count]) => ({ feature, count, rate: count / sim.spins })) .sort((a, b) => b.count - a.count); const variance = sim.stdDev * sim.stdDev; const convDomain: [number, number] = [Math.max(0, Math.min(sim.configuredRtp, ...sim.convergence.map((c) => c.rtp)) - 0.02), Math.max(sim.configuredRtp, ...sim.convergence.map((c) => c.rtp)) + 0.02]; return (
{sc(sim.medianWin)} / {sc(Math.round(sim.averageWin))}} sub={`bet ${sc(sim.bet)}`} compact={compactTiles} /> 0.0005 ? "danger" : "neutral"} sub={pct(sim.cappedRounds / sim.spins, 4)} compact={compactTiles} />
}> ({ ...b }))} x="label" y="count" logScale={scale === "log"} yFormat={(v) => (v >= 1_000_000 ? `${(v / 1_000_000).toFixed(1)}M` : v >= 1000 ? `${(v / 1000).toFixed(0)}K` : String(Math.round(v)))} /> ({ ...c }))} x="spins" series={[{ key: "rtp", label: "Observed RTP", color: CHART.accent }]} xFormat={(v) => shortNum(Number(v))} yFormat={(v) => `${(v * 100).toFixed(1)}%`} yDomain={convDomain} reference={{ y: sim.configuredRtp, label: `target ${pct(sim.configuredRtp)}` }} tooltipLabel={(l) => `${int(l)} spins`} /> {features.length ? shortNum(v)} tooltipLabel={(l, row) => `${String(l)} · ${pct(Number(row?.rate ?? 0), 3)} of rounds`} /> :
No feature fired during this run.
}
{cert ? : null}
); } function DistributionTable({ rows }: { rows: SimulationResult["distribution"] }) { return (
Table view {rows.map((b) => ( ))}
Bucket Rounds Share
{b.label} {int(b.count)} {pct(b.share, 3)}
); } export function CertificationBlock({ cert, title = "Certification report" }: { cert: CertificationReport; title?: string }) { return ( } tone={cert.status === "PASS" ? "success" : "danger"}> ); } function shortNum(v: number): string { if (Math.abs(v) >= 1_000_000) return `${(v / 1_000_000).toFixed(v % 1_000_000 === 0 ? 0 : 1)}M`; if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}K`; return String(Math.round(v)); }