TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
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