"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
| Bucket |
Rounds |
Share |
{rows.map((b) => (
| {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"}>
{cert.checks.map((c) => (
-
))}
);
}
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));
}