SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
5.2 KB · 163 lines typescript
Raw Blame History
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