TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import type { GameDefinition, GameSymbol, Grid } from "../types";2import type { Rng } from "../rng";34export interface ReelTables {5 /** Per reel: symbol ids and cumulative weights (base game). */6 base: { ids: string[]; weights: Float64Array }[];7 /** Per reel: free-spin tables. */8 free: { ids: string[]; weights: Float64Array }[];9 byId: Map<string, GameSymbol>;10 regularIds: string[];11}1213const tableCache = new WeakMap<GameDefinition, ReelTables>();1415function weightFor(sym: GameSymbol, reel: number, free: boolean): number {16 const w = free && sym.freeSpinWeight !== undefined ? sym.freeSpinWeight : sym.weight;17 if (Array.isArray(w)) return w[Math.min(reel, w.length - 1)] ?? 0;18 return w;19}2021/** Build (and cache) weighted draw tables for a definition, for up to 8 reels. */22export function reelTables(def: GameDefinition): ReelTables {23 const cached = tableCache.get(def);24 if (cached) return cached;25 const maxReels = Math.max(def.grid.reels, ...(def.dynamicGrid?.sequence.map((s) => s[0]) ?? [0]));26 const build = (free: boolean) =>27 Array.from({ length: maxReels }, (_, reel) => {28 const ids: string[] = [];29 const ws: number[] = [];30 for (const s of def.symbols) {31 const w = weightFor(s, reel, free);32 if (w > 0) {33 ids.push(s.id);34 ws.push(w);35 }36 }37 return { ids, weights: Float64Array.from(ws) };38 });39 const tables: ReelTables = {40 base: build(false),41 free: build(true),42 byId: new Map(def.symbols.map((s) => [s.id, s])),43 regularIds: def.symbols.filter((s) => s.kind === "regular").map((s) => s.id),44 };45 tableCache.set(def, tables);46 return tables;47}4849export function drawSymbol(tables: ReelTables, reel: number, free: boolean, rng: Rng): string {50 const t = (free ? tables.free : tables.base)[reel];51 return t.ids[rng.weighted(t.weights)];52}5354/** Draw a fresh grid of `reels × rows`. */55export function drawGrid(def: GameDefinition, rng: Rng, free: boolean, reels = def.grid.reels, rows = def.grid.rows): Grid {56 const tables = reelTables(def);57 const grid: Grid = [];58 for (let r = 0; r < reels; r++) {59 const col: string[] = new Array(rows);60 for (let y = 0; y < rows; y++) col[y] = drawSymbol(tables, r, free, rng);61 grid.push(col);62 }63 return grid;64}6566/** Apply stacked-wild expansion: with probability p a landed wild fills its reel. */67export function applyStackedWilds(def: GameDefinition, grid: Grid, rng: Rng): number[] {68 const w = def.wild;69 if (!w?.stacked) return [];70 const stacked: number[] = [];71 for (let r = 0; r < grid.length; r++) {72 if (grid[r].includes(w.id) && rng.chance(w.stacked)) {73 for (let y = 0; y < grid[r].length; y++) grid[r][y] = w.id;74 stacked.push(r);75 }76 }77 return stacked;78}7980export function cloneGrid(g: Grid): Grid {81 return g.map((c) => c.slice());82}8384export function countSymbol(grid: Grid, id: string): [number, number][] {85 const out: [number, number][] = [];86 for (let r = 0; r < grid.length; r++) for (let y = 0; y < grid[r].length; y++) if (grid[r][y] === id) out.push([r, y]);87 return out;88}89