/** * Crash curve math shared by server and browser (pure, no crypto). */ export type CrashScene = "sky" | "ocean" | "rocket" | "bank" | "volcano" | "blackhole" | "freefall" | "reactor" | "storm" | "elevator"; export type CrashCurve = /** m(τ) = e^(k·τ) — classic steady acceleration. */ | { type: "exp"; k: number } /** m(τ) = 1 + a·τ^p — slow start, brutal end (freefall, black hole). */ | { type: "power"; a: number; p: number } /** Discrete floors: m = base^floor, one floor every `stepSeconds` (elevator). */ | { type: "steps"; stepSeconds: number; base: number }; export interface CrashEventKind { id: string; label: string; /** Speed factor applied to the time flow while the window is active (>1 boost, <1 cooling). */ factor: number; /** Window duration in seconds. */ duration: [number, number]; weight: number; } export interface CrashEventsConfig { /** Chance that a round contains at least one event. */ chance: number; /** Max events per round. */ max: number; /** Earliest / latest start (seconds of *real* time). */ window: [number, number]; kinds: CrashEventKind[]; } export interface CrashMilestone { multiplier: number; label: string; } export interface CrashGameDefinition { kind: "crash"; slug: string; name: string; version: string; tagline: string; description: string; theme: string; tags: string[]; rtp: number; minBet: number; maxBet: number; /** Hard cap on the crash multiplier. */ maxMultiplier: number; curve: CrashCurve; events?: CrashEventsConfig; /** Story beats shown as the multiplier passes them (cosmetic). */ milestones: CrashMilestone[]; presentation: { scene: CrashScene; palette: { primary: string; secondary: string; glow: string; bg: string; surface: string }; /** Main button label. */ verb: string; /** Progress unit shown next to the multiplier, e.g. "m", "km", "°C", "floor". */ unit: string; /** Unit value at multiplier x (cosmetic mapping). */ unitScale: number; ambience: string; }; rules: string[]; featureNames: string[]; volatility: "low" | "medium" | "high" | "extreme"; } export interface CrashEvent { id: string; label: string; /** Real-time window in ms since round start. */ startMs: number; endMs: number; factor: number; } export interface CrashRoundSetup { crashMultiplier: number; /** ms of real time at which the crash occurs (derived from the curve + events). */ crashAtMs: number; seed: string; commitment: string; events: CrashEvent[]; } /* ---------------------------------------------------------------- curve */ /** Effective ("game") time after applying event speed windows, in seconds. */ export function effectiveTime(events: CrashEvent[], elapsedMs: number): number { let tau = 0; let cursor = 0; const sorted = [...events].sort((a, b) => a.startMs - b.startMs); for (const e of sorted) { if (elapsedMs <= cursor) break; const plainEnd = Math.min(e.startMs, elapsedMs); if (plainEnd > cursor) tau += (plainEnd - cursor) / 1000; const winEnd = Math.min(e.endMs, elapsedMs); if (winEnd > e.startMs) tau += ((winEnd - e.startMs) / 1000) * e.factor; cursor = Math.max(cursor, e.endMs); } if (elapsedMs > cursor) tau += (elapsedMs - cursor) / 1000; return tau; } function curveValue(curve: CrashCurve, tau: number): number { switch (curve.type) { case "exp": return Math.exp(curve.k * tau); case "power": return 1 + curve.a * Math.pow(tau, curve.p); case "steps": return Math.pow(curve.base, Math.floor(tau / curve.stepSeconds)); } } function curveInverse(curve: CrashCurve, m: number): number { switch (curve.type) { case "exp": return Math.log(Math.max(1, m)) / curve.k; case "power": return Math.pow(Math.max(0, m - 1) / curve.a, 1 / curve.p); case "steps": return Math.ceil(Math.log(Math.max(1, m)) / Math.log(curve.base)) * curve.stepSeconds; } } /** Multiplier displayed/settled at `elapsedMs` of real time (2-decimal floor). */ export function multiplierAt(def: CrashGameDefinition, events: CrashEvent[], elapsedMs: number): number { const tau = effectiveTime(events, Math.max(0, elapsedMs)); const m = curveValue(def.curve, tau); return Math.min(def.maxMultiplier, Math.floor(m * 100) / 100); } /** Real time (ms) at which the curve first reaches `m`. */ export function timeForMultiplier(def: CrashGameDefinition, events: CrashEvent[], m: number): number { const targetTau = curveInverse(def.curve, m); // Invert effectiveTime by walking the windows. let tau = 0; let cursor = 0; const sorted = [...events].sort((a, b) => a.startMs - b.startMs); for (const e of sorted) { const plain = (e.startMs - cursor) / 1000; if (tau + plain >= targetTau) return cursor + (targetTau - tau) * 1000; tau += plain; const win = ((e.endMs - e.startMs) / 1000) * e.factor; if (tau + win >= targetTau) return e.startMs + ((targetTau - tau) / e.factor) * 1000; tau += win; cursor = e.endMs; } return cursor + (targetTau - tau) * 1000; } /** Floor number for the elevator (cosmetic). */ export function floorAt(def: CrashGameDefinition, m: number): number { return def.curve.type === "steps" ? Math.round(Math.log(m) / Math.log(def.curve.base)) : 0; }