TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1/**2 * Crash curve math shared by server and browser (pure, no crypto).3 */4export type CrashScene = "sky" | "ocean" | "rocket" | "bank" | "volcano" | "blackhole" | "freefall" | "reactor" | "storm" | "elevator";56export 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 };1314export 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}2324export 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}3334export interface CrashMilestone {35 multiplier: number;36 label: string;37}3839export 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}7273export 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}8182export 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}9091/* ---------------------------------------------------------------- curve */9293/** Effective ("game") time after applying event speed windows, in seconds. */94export 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}109110function 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}120121function 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}131132/** Multiplier displayed/settled at `elapsedMs` of real time (2-decimal floor). */133export 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}138139/** Real time (ms) at which the curve first reaches `m`. */140export 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}157158/** Floor number for the elevator (cosmetic). */159export 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}162163