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%
6.5 KB · 155 lines typescript
Raw Blame History
1import type { CoinCell, GameDefinition, Grid, PickBonusConfig } from "../types";2import type { Rng } from "../rng";3import { reelTables } from "../reels";4import { credits } from "../evaluate";56/** Mystery symbols: all reveal into the same random symbol. */7export function revealMystery(def: GameDefinition, grid: Grid, rng: Rng, forcedPool?: string[]): { symbol: string; positions: [number, number][] } | null {8  const m = def.mystery;9  if (!m) return null;10  const positions: [number, number][] = [];11  for (let r = 0; r < grid.length; r++) for (let y = 0; y < grid[r].length; y++) if (grid[r][y] === m.symbolId) positions.push([r, y]);12  if (positions.length === 0) return null;13  const tables = reelTables(def);14  const pool = forcedPool ?? m.revealPool ?? tables.regularIds;15  const weights = forcedPool ? pool.map(() => 1) : (m.revealWeights ?? pool.map(() => 1));16  const symbol = pool[rng.weighted(weights)];17  for (const [r, y] of positions) grid[r][y] = symbol;18  return { symbol, positions };19}2021/** Add extra mystery symbols to the grid (mysteryBoost feature). */22export function boostMystery(def: GameDefinition, grid: Grid, rng: Rng, count: number): number {23  const m = def.mystery;24  if (!m) return 0;25  const tables = reelTables(def);26  const cells: [number, number][] = [];27  for (let r = 0; r < grid.length; r++)28    for (let y = 0; y < grid[r].length; y++) if (tables.byId.get(grid[r][y])?.kind === "regular") cells.push([r, y]);29  let n = 0;30  for (let i = 0; i < count && cells.length > 0; i++) {31    const [r, y] = cells.splice(rng.int(cells.length), 1)[0];32    grid[r][y] = m.symbolId;33    n++;34  }35  return n;36}3738/** Quantum split: some regular cells gain multiplicity, multiplying ways. */39export function quantumSplit(def: GameDefinition, grid: Grid, rng: Rng): { multiplicity: number[][]; splits: { position: [number, number]; multiplicity: number }[] } | null {40  const q = def.quantum;41  if (!q || !rng.chance(q.chance)) return null;42  const tables = reelTables(def);43  const cells: [number, number][] = [];44  for (let r = 0; r < grid.length; r++)45    for (let y = 0; y < grid[r].length; y++) {46      const k = tables.byId.get(grid[r][y])?.kind;47      if (k === "regular" || k === "wild") cells.push([r, y]);48    }49  const n = Math.min(cells.length, q.cells.min + rng.int(q.cells.max - q.cells.min + 1));50  const multiplicity = grid.map((c) => c.map(() => 1));51  const splits: { position: [number, number]; multiplicity: number }[] = [];52  for (let i = 0; i < n; i++) {53    const [r, y] = cells.splice(rng.int(cells.length), 1)[0];54    const m = q.splits.values[rng.weighted(q.splits.weights)];55    multiplicity[r][y] = m;56    splits.push({ position: [r, y], multiplicity: m });57  }58  return { multiplicity, splits };59}6061/** Resolve a pick bonus. Prizes are multiples of bet. */62export function resolvePickBonus(def: GameDefinition, cfg: PickBonusConfig, bet: number, rng: Rng): { picks: { cell: number; value: number; amount: number }[]; total: number } {63  const cells = Array.from({ length: cfg.cells }, (_, i) => i);64  const picks: { cell: number; value: number; amount: number }[] = [];65  let total = 0;66  for (let i = 0; i < cfg.picks && cells.length > 0; i++) {67    const cell = cells.splice(rng.int(cells.length), 1)[0];68    const value = cfg.prizes.values[rng.weighted(cfg.prizes.weights)];69    const amount = Math.round(credits(bet, value, def.payScale));70    picks.push({ cell, value, amount });71    total += amount;72    if (cfg.endOn !== undefined && value === cfg.endOn) break;73  }74  return { picks, total };75}7677export interface HoldRespinResult {78  steps: { coins: CoinCell[]; respinsLeft: number; grid: Grid }[];79  total: number;80  jackpot: { tier: string; amount: number } | null;81  fullGrid: boolean;82}8384function drawCoin(def: GameDefinition, bet: number, rng: Rng, reel: number, row: number): CoinCell {85  const cfg = def.holdRespin!;86  if (cfg.jackpotTagChance) {87    const t = cfg.jackpotTagChance;88    const roll = rng.float();89    let acc = 0;90    for (const tier of ["grand", "major", "minor", "mini"] as const) {91      const p = t[tier] ?? 0;92      acc += p;93      if (roll < acc) return { reel, row, value: null, jackpot: tier, isNew: true };94    }95  }96  const v = cfg.values.values[rng.weighted(cfg.values.weights)];97  return { reel, row, value: Math.round(credits(bet, v, def.payScale)), isNew: true };98}99100/** Lock-and-respin feature, fully resolved. */101export function resolveHoldRespin(def: GameDefinition, grid: Grid, bet: number, rng: Rng): HoldRespinResult | null {102  const cfg = def.holdRespin;103  if (!cfg) return null;104  const reels = grid.length;105  const rows = grid[0].length;106  const coins: CoinCell[] = [];107  for (let r = 0; r < reels; r++) for (let y = 0; y < rows; y++) if (grid[r][y] === cfg.symbolId) coins.push(drawCoin(def, bet, rng, r, y));108  if (coins.length < cfg.minToTrigger) return null;109110  const steps: HoldRespinResult["steps"] = [];111  const blank = "__";112  const makeGrid = (): Grid => {113    const g: Grid = Array.from({ length: reels }, () => new Array(rows).fill(blank));114    for (const c of coins) g[c.reel][c.row] = cfg.symbolId;115    return g;116  };117  let respins = cfg.respins;118  steps.push({ coins: coins.map((c) => ({ ...c })), respinsLeft: respins, grid: makeGrid() });119  for (const c of coins) c.isNew = false;120  const capacity = reels * rows;121  let guard = 0;122  while (respins > 0 && coins.length < capacity && guard++ < 200) {123    let landed = false;124    const occupied = new Set(coins.map((c) => `${c.reel}:${c.row}`));125    for (let r = 0; r < reels; r++)126      for (let y = 0; y < rows; y++) {127        if (occupied.has(`${r}:${y}`)) continue;128        if (rng.chance(cfg.landChance)) {129          coins.push(drawCoin(def, bet, rng, r, y));130          landed = true;131        }132      }133    respins = landed ? cfg.respins : respins - 1;134    steps.push({ coins: coins.map((c) => ({ ...c })), respinsLeft: respins, grid: makeGrid() });135    for (const c of coins) c.isNew = false;136  }137  let total = 0;138  let best: { tier: string; amount: number } | null = null;139  const fullGrid = coins.length >= capacity;140  for (const c of coins) {141    if (c.value !== null) total += c.value;142    else if (c.jackpot) {143      const amt = Math.round(cfg.jackpots[c.jackpot] * bet);144      total += amt;145      if (!best || amt > best.amount) best = { tier: c.jackpot, amount: amt };146    }147  }148  if (fullGrid && cfg.fullGridJackpot) {149    const amt = Math.round(cfg.jackpots[cfg.fullGridJackpot] * bet);150    total += amt;151    best = { tier: cfg.fullGridJackpot, amount: amt };152  }153  return { steps, total, jackpot: best, fullGrid };154}155