import { defineArcadeGame, type ArcadeGameDefinition } from "@spinza/game-core"; import calibration from "../calibration.json"; /** * Spinza Originals — Beyond Slots. Five interactive originals sharing the * arcade engine: two ladder games (analytic RTP) and three instant games * (payScale calibrated by simulation, see calibration.json). */ export const dropzone = defineArcadeGame({ slug: "dropzone", name: "Dropzone", version: "1.0.0", tagline: "Release the capsule. Pray for the edge.", description: "Drop a capsule from the top of a vertical tower. It ricochets through pegs, energy gates and portals down to a row of buckets whose values depend on the risk profile you pick. Some descents trigger Deep Drop: the tower extends and the capsule keeps falling into a far riskier zone where the multipliers stack — or vanish.", theme: "arcade tower drop", mode: "instant", rtp: 0.96, maxMultiplier: 3000, volatility: "high", presentation: { scene: "dropzone", palette: { primary: "#22d3ee", secondary: "#f472b6", glow: "#67e8f9", bg: "#050a14", surface: "#0b1a2e" }, ambience: "arcade", verb: "DROP" }, config: { rows: 12, lanes: 13, buckets: { low: [8, 3, 1.6, 1.2, 1, 0.7, 0.5, 0.7, 1, 1.2, 1.6, 3, 8], medium: [25, 6, 2.5, 1.4, 0.8, 0.4, 0.2, 0.4, 0.8, 1.4, 2.5, 6, 25], high: [120, 18, 4, 1.2, 0.4, 0.1, 0, 0.1, 0.4, 1.2, 4, 18, 120], }, gateChancePerRow: 0.035, gateValues: [1.5, 2, 3], portalChancePerRow: 0.012, deepDropChance: 0.08, deepRows: 8, deepBuckets: [10, 4, 2, 1, 0, 1, 2, 4, 10], }, featureNames: ["3 risk profiles", "Energy gates", "Portals", "Deep Drop"], rules: [ "Choose a risk profile (Low / Medium / High) and a release lane, then press DROP.", "The capsule bounces left or right at every peg row (12 rows) and lands in one of 13 buckets.", "Energy gates multiply the result by ×1.5, ×2 or ×3; portals hop the capsule one to three lanes sideways.", "Bucket values are normalised per release lane, so every lane has the same expected value — edges are just wilder.", "Deep Drop (8% of drops): the tower extends by 8 rows and the capsule falls into a second bucket row that multiplies the result by 0× to 10×.", "Bucket values shown are already scaled to the game's certified RTP. Maximum win: 3,000× the bet.", ], }); export const gridBreak = defineArcadeGame({ slug: "grid-break", name: "Grid//Break", version: "1.0.0", tagline: "One wave. Infinite chains.", description: "An 8×8 grid of energy blocks. Fire a wave down a column: every cluster it touches detonates, blocks fall, fresh ones drop in and new clusters detonate in a chain. Longer chains climb the multiplier ladder. Bombs clear 3×3, line blocks wipe a row, ×2 blocks double the step. It plays like a lightning-fast puzzle game.", theme: "futuristic puzzle grid", mode: "instant", rtp: 0.96, maxMultiplier: 2500, volatility: "medium", presentation: { scene: "gridbreak", palette: { primary: "#a3e635", secondary: "#38bdf8", glow: "#bef264", bg: "#05080c", surface: "#0e1620" }, ambience: "quantum", verb: "FIRE WAVE" }, config: { size: 8, colors: 6, values: [1, 1.5, 2.5, 4, 7, 12], weights: [26, 22, 19, 15, 11, 7], specialChance: { bomb: 0.02, line: 0.012, x2: 0.02 }, chainLadder: [1, 2, 4, 8, 16, 32, 64, 128], minCluster: 4, maxChains: 40, }, featureNames: ["Chain reactions", "Bombs", "Line clears", "×2 blocks"], rules: [ "Pick a column and fire the wave. Clusters of 4+ connected same-colour blocks touching that column detonate.", "Blocks fall, new blocks drop in; any new cluster of 4+ detonates automatically — a chain.", "Each chain step doubles the multiplier: ×1, ×2, ×4, ×8, ×16, ×32, ×64, ×128.", "Bomb blocks clear the 3×3 around them, line blocks clear their whole row, ×2 blocks double the step.", "Block values: the rarer the colour, the higher the value. Maximum win: 2,500× the bet.", ], }); export const theVault = defineArcadeGame({ slug: "the-vault", name: "The Vault", version: "1.0.0", tagline: "Five layers. One decision at each.", description: "Face a colossal vault with five locked layers. Each layer reveals its digits one by one; when a layer opens, secure your multiplier or attempt the next lock. The deeper you go, the more spectacular the vault — and the higher the risk. Open all five in one run to trigger the fictional jackpot.", theme: "monumental vault", mode: "ladder", rtp: 0.96, maxMultiplier: 50, volatility: "high", presentation: { scene: "vault", palette: { primary: "#c9a961", secondary: "#38bdf8", glow: "#e8cf8f", bg: "#08070a", surface: "#16131a" }, ambience: "heist", verb: "OPEN NEXT LOCK", secondaryVerb: "SECURE" }, config: { layers: [0.8, 0.7, 0.55, 0.4, 0.25], digitsPerLayer: 3, layerNames: ["Outer door", "Time lock", "Biometric ring", "Pressure chamber", "The core"], }, featureNames: ["Secure or push", "5 layers", "Fictional jackpot"], rules: [ "Place a bet. The first layer opens automatically — its 3 digits are revealed one by one.", "After each opened layer, SECURE your multiplier or OPEN the next lock.", "Chances the lock yields: 80% → 70% → 55% → 40% → 25%. Multipliers after each layer: 1.20× → 1.71× → 3.12× → 7.79× → 31.2×.", "A failed digit triggers the alarm: the run ends and the bet is lost.", "Opening all five layers in one run pays the fictional jackpot (31.2×). Expected return is 96% whatever your stopping point.", ], }); export const orbit = defineArcadeGame({ slug: "orbit", name: "Orbit", version: "1.0.0", tagline: "Fire the impulse. Ride the chaos.", description: "Planets, satellites and debris circle a burning core. Aim an impulse through the orbits: every object it touches adds value, may deflect the impulse or spawn a brand-new orbit. Some rounds cascade into fifteen objects spinning at once. Supernova collapses everything and turns the survivors into multipliers.", theme: "orbital mechanics", mode: "instant", rtp: 0.96, maxMultiplier: 2000, volatility: "high", presentation: { scene: "orbit", palette: { primary: "#f0abfc", secondary: "#facc15", glow: "#f5d0fe", bg: "#07030f", surface: "#150a26" }, ambience: "space", verb: "FIRE IMPULSE" }, config: { orbits: 3, objectsPerOrbit: [3, 6], beamHalfWidth: 14, objectTypes: [ { id: "debris", label: "Debris", value: 0.3, weight: 34 }, { id: "satellite", label: "Satellite", value: 0.8, weight: 26, deflect: 40 }, { id: "planet", label: "Planet", value: 2, weight: 18, deflect: 20 }, { id: "comet", label: "Comet", value: 4, weight: 10, spawn: true }, { id: "gasgiant", label: "Gas giant", value: 8, weight: 5, deflect: 60 }, { id: "quasar", label: "Quasar", value: 25, weight: 1, spawn: true }, ], supernovaChance: 0.04, maxOrbits: 7, }, featureNames: ["Aim the impulse", "Deflections", "New orbits", "Supernova"], rules: [ "Choose an angle and fire. The impulse travels outward through every orbit.", "An object within the beam is collected: Debris 0.3×, Satellite 0.8×, Planet 2×, Comet 4×, Gas giant 8×, Quasar 25× (values scaled to the certified RTP).", "Satellites, planets and gas giants deflect the impulse; comets and quasars spawn a new orbit (up to 7).", "Supernova (4% of scoring rounds): every remaining object becomes +0.5× on the collected total.", "Maximum win: 2,000× the bet.", ], }); export const escape99 = defineArcadeGame({ slug: "escape-99", name: "Escape 99", version: "1.0.0", tagline: "Ninety-nine floors. Cash out on any of them.", description: "A lightning-fast roguelike. Start on floor 1 of a 99-floor tower and climb room by room: chests, guardians, traps, runes, portals and forks between a safe corridor and a risky one. Cash out on any floor, or push for the summit — floor 99 triggers the final sequence.", theme: "roguelike tower", mode: "ladder", rtp: 0.96, maxMultiplier: 5000, volatility: "extreme", presentation: { scene: "escape", palette: { primary: "#34d399", secondary: "#f97316", glow: "#6ee7b7", bg: "#06090a", surface: "#0f1a16" }, ambience: "jungle", verb: "CLIMB", secondaryVerb: "CASH OUT" }, config: { floors: 99, bands: [ [10, 0.97], [25, 0.95], [50, 0.93], [75, 0.91], [99, 0.89], ], checkpoints: [10, 25, 50, 75, 99], rooms: [ { kind: "chest", label: "Treasure room", weight: 26, detail: "a chest glints in the dark" }, { kind: "enemy", label: "Guardian hall", weight: 22, detail: "something stirs" }, { kind: "trap", label: "Trap corridor", weight: 18, detail: "pressure plates everywhere" }, { kind: "multiplier", label: "Rune chamber", weight: 16, detail: "runes pulse with energy" }, { kind: "room", label: "Quiet stairwell", weight: 18, detail: "nothing but echoes" }, ], forkChance: 0.18, portalChance: 0.06, }, featureNames: ["99 floors", "Forks", "Portals", "Checkpoints", "Final sequence"], rules: [ "Place a bet: you enter floor 1 automatically. Each CLIMB advances one floor (three through a portal).", "Floor safety: 97% up to floor 10, 95% to 25, 93% to 50, 91% to 75, 89% beyond. Each survived floor multiplies by 1 ÷ safety.", "Forks offer a safe corridor (+6% safety, smaller jump) or a risky one (−18%, bigger jump) — equal expected value.", "CASH OUT at any floor after the first. Checkpoints (10, 25, 50, 75, 99) are marked on the tower.", "Floor 99 ends the run with the final sequence. Expected return is 96% for any stopping strategy; maximum win 5,000×.", ], }); type Calibration = Record; const withCalibration = (def: ArcadeGameDefinition): ArcadeGameDefinition => { const c = (calibration as Calibration)[def.slug]; return c && c.version === def.version ? { ...def, payScale: c.payScale } : def; }; export const RAW_ARCADE_GAMES: ArcadeGameDefinition[] = [dropzone, gridBreak, theVault, orbit, escape99]; export const ARCADE_GAMES: ArcadeGameDefinition[] = RAW_ARCADE_GAMES.map(withCalibration); export const ARCADE_BY_SLUG = new Map(ARCADE_GAMES.map((g) => [g.slug, g]));