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.2 KB · 194 lines typescript
Raw Blame History
1import type { GameDefinition, Grid, WinLine } from "../types";2import { reelTables } from "../reels";34export interface EvalContext {5  bet: number;6  /** Cell multiplicity (quantum splits); undefined = 1 everywhere. */7  multiplicity?: number[][];8  /** Wild multiplier per cell (from multiplier wilds). */9  wildMultipliers?: Map<string, number>;10}1112const key = (r: number, y: number) => `${r}:${y}`;1314function isWild(def: GameDefinition, id: string): boolean {15  return def.wild?.id === id;16}1718/** Exact (unrounded) credit amount for a pay multiple; rounding happens once per step. */19export function credits(bet: number, payMultiple: number, scale: number): number {20  return bet * payMultiple * scale;21}2223function combineWildMultipliers(def: GameDefinition, positions: [number, number][], ctx: EvalContext): number {24  if (!ctx.wildMultipliers || ctx.wildMultipliers.size === 0 || !def.wild?.multiplier) return 1;25  let acc = def.wild.multiplier.combine === "add" ? 0 : 1;26  let any = false;27  for (const [r, y] of positions) {28    const m = ctx.wildMultipliers.get(key(r, y));29    if (m && m > 1) {30      any = true;31      acc = def.wild.multiplier.combine === "add" ? acc + m : acc * m;32    }33  }34  if (!any) return 1;35  const cap = def.wild.maxCombinedMultiplier ?? Infinity;36  return Math.min(acc, cap);37}3839/** Ways evaluation (left to right, adjacent reels). */40export function evaluateWays(def: GameDefinition, grid: Grid, ctx: EvalContext): WinLine[] {41  const tables = reelTables(def);42  const wins: WinLine[] = [];43  const reels = grid.length;44  const wildId = def.wild?.id;4546  // Candidate symbols = regular symbols present on reel 0 (or wild on reel 0 → any symbol).47  const candidates = new Set<string>();48  for (const id of grid[0]) {49    if (id === wildId) {50      for (const s of tables.regularIds) candidates.add(s);51    } else if (tables.byId.get(id)?.pays) candidates.add(id);52  }5354  for (const symbol of candidates) {55    const sym = tables.byId.get(symbol);56    if (!sym?.pays) continue;57    let ways = 1;58    let count = 0;59    const positions: [number, number][] = [];60    for (let r = 0; r < reels; r++) {61      let matches = 0;62      for (let y = 0; y < grid[r].length; y++) {63        const id = grid[r][y];64        if (id === symbol || (wildId && id === wildId)) {65          matches += ctx.multiplicity?.[r]?.[y] ?? 1;66          positions.push([r, y]);67        }68      }69      if (matches === 0) break;70      ways *= matches;71      count++;72    }73    const pay = sym.pays[count];74    if (count >= 2 && pay) {75      const wm = combineWildMultipliers(def, positions, ctx);76      const raw = (credits(ctx.bet, pay, def.payScale) * ways) / def.betDivisor;77      wins.push({ symbol, count, ways, positions, base: Math.round(raw), wildMultiplier: wm, payout: Math.round(raw * wm), raw: raw * wm });78    }79  }80  return wins;81}8283/** Payline evaluation. `lines[i][reel] = row`. */84export function evaluateLines(def: GameDefinition, grid: Grid, ctx: EvalContext): WinLine[] {85  if (def.payModel.type !== "lines") return [];86  const tables = reelTables(def);87  const wildId = def.wild?.id;88  const wins: WinLine[] = [];89  const rows = grid[0].length;90  def.payModel.lines.forEach((line, li) => {91    // Determine target symbol: first non-wild along the line.92    let symbol: string | null = null;93    for (let r = 0; r < grid.length; r++) {94      const row = Math.min(line[r] ?? 0, rows - 1);95      const id = grid[r][row];96      if (id !== wildId) {97        symbol = id;98        break;99      }100    }101    // All-wild line pays as the best regular symbol.102    const evaluateFor = (sym: string): WinLine | null => {103      const s = tables.byId.get(sym);104      if (!s?.pays) return null;105      let count = 0;106      const positions: [number, number][] = [];107      for (let r = 0; r < grid.length; r++) {108        const row = Math.min(line[r] ?? 0, rows - 1);109        const id = grid[r][row];110        if (id === sym || id === wildId) {111          count++;112          positions.push([r, row]);113        } else break;114      }115      const pay = s.pays[count];116      if (!pay) return null;117      const wm = combineWildMultipliers(def, positions, ctx);118      const raw = credits(ctx.bet, pay, def.payScale) / def.betDivisor;119      return { symbol: sym, count, line: li, positions, base: Math.round(raw), wildMultiplier: wm, payout: Math.round(raw * wm), raw: raw * wm };120    };121    if (symbol === null) {122      let best: WinLine | null = null;123      for (const sym of tables.regularIds) {124        const w = evaluateFor(sym);125        if (w && (!best || w.payout > best.payout)) best = w;126      }127      if (best) wins.push(best);128    } else {129      const w = evaluateFor(symbol);130      if (w) wins.push(w);131    }132  });133  return wins;134}135136export function evaluate(def: GameDefinition, grid: Grid, ctx: EvalContext): WinLine[] {137  return def.payModel.type === "ways" ? evaluateWays(def, grid, ctx) : evaluateLines(def, grid, ctx);138}139140/** Scatter pays (anywhere on the grid). */141export function scatterWin(def: GameDefinition, grid: Grid, bet: number): { count: number; positions: [number, number][]; win: number } {142  const sid = def.scatter?.id;143  const positions: [number, number][] = [];144  if (!sid) return { count: 0, positions, win: 0 };145  for (let r = 0; r < grid.length; r++) for (let y = 0; y < grid[r].length; y++) if (grid[r][y] === sid) positions.push([r, y]);146  const sym = reelTables(def).byId.get(sid);147  const pay = sym?.scatterPays?.[positions.length] ?? 0;148  return { count: positions.length, positions, win: pay ? credits(bet, pay, def.payScale) : 0 };149}150151/** Exact sum of wins (unrounded). */152export function sumWins(wins: WinLine[]): number {153  let t = 0;154  for (const w of wins) t += w.raw;155  return t;156}157158/** Standard payline sets. */159export const LINES_10: number[][] = [160  [1, 1, 1, 1, 1],161  [0, 0, 0, 0, 0],162  [2, 2, 2, 2, 2],163  [0, 1, 2, 1, 0],164  [2, 1, 0, 1, 2],165  [0, 0, 1, 2, 2],166  [2, 2, 1, 0, 0],167  [1, 0, 1, 2, 1],168  [1, 2, 1, 0, 1],169  [0, 1, 1, 1, 0],170];171172export const LINES_20: number[][] = [173  ...LINES_10,174  [2, 1, 1, 1, 2],175  [0, 1, 0, 1, 0],176  [2, 1, 2, 1, 2],177  [1, 1, 0, 1, 1],178  [1, 1, 2, 1, 1],179  [0, 0, 1, 0, 0],180  [2, 2, 1, 2, 2],181  [1, 0, 0, 0, 1],182  [1, 2, 2, 2, 1],183  [0, 2, 0, 2, 0],184];185186export const LINES_25: number[][] = [187  ...LINES_20,188  [2, 0, 2, 0, 2],189  [0, 2, 2, 2, 0],190  [2, 0, 0, 0, 2],191  [1, 0, 2, 0, 1],192  [1, 2, 0, 2, 1],193];194