import type { GameDefinition, GameSymbol, Grid } from "../types"; import type { Rng } from "../rng"; export interface ReelTables { /** Per reel: symbol ids and cumulative weights (base game). */ base: { ids: string[]; weights: Float64Array }[]; /** Per reel: free-spin tables. */ free: { ids: string[]; weights: Float64Array }[]; byId: Map; regularIds: string[]; } const tableCache = new WeakMap(); function weightFor(sym: GameSymbol, reel: number, free: boolean): number { const w = free && sym.freeSpinWeight !== undefined ? sym.freeSpinWeight : sym.weight; if (Array.isArray(w)) return w[Math.min(reel, w.length - 1)] ?? 0; return w; } /** Build (and cache) weighted draw tables for a definition, for up to 8 reels. */ export function reelTables(def: GameDefinition): ReelTables { const cached = tableCache.get(def); if (cached) return cached; const maxReels = Math.max(def.grid.reels, ...(def.dynamicGrid?.sequence.map((s) => s[0]) ?? [0])); const build = (free: boolean) => Array.from({ length: maxReels }, (_, reel) => { const ids: string[] = []; const ws: number[] = []; for (const s of def.symbols) { const w = weightFor(s, reel, free); if (w > 0) { ids.push(s.id); ws.push(w); } } return { ids, weights: Float64Array.from(ws) }; }); const tables: ReelTables = { base: build(false), free: build(true), byId: new Map(def.symbols.map((s) => [s.id, s])), regularIds: def.symbols.filter((s) => s.kind === "regular").map((s) => s.id), }; tableCache.set(def, tables); return tables; } export function drawSymbol(tables: ReelTables, reel: number, free: boolean, rng: Rng): string { const t = (free ? tables.free : tables.base)[reel]; return t.ids[rng.weighted(t.weights)]; } /** Draw a fresh grid of `reels × rows`. */ export function drawGrid(def: GameDefinition, rng: Rng, free: boolean, reels = def.grid.reels, rows = def.grid.rows): Grid { const tables = reelTables(def); const grid: Grid = []; for (let r = 0; r < reels; r++) { const col: string[] = new Array(rows); for (let y = 0; y < rows; y++) col[y] = drawSymbol(tables, r, free, rng); grid.push(col); } return grid; } /** Apply stacked-wild expansion: with probability p a landed wild fills its reel. */ export function applyStackedWilds(def: GameDefinition, grid: Grid, rng: Rng): number[] { const w = def.wild; if (!w?.stacked) return []; const stacked: number[] = []; for (let r = 0; r < grid.length; r++) { if (grid[r].includes(w.id) && rng.chance(w.stacked)) { for (let y = 0; y < grid[r].length; y++) grid[r][y] = w.id; stacked.push(r); } } return stacked; } export function cloneGrid(g: Grid): Grid { return g.map((c) => c.slice()); } export function countSymbol(grid: Grid, id: string): [number, number][] { const out: [number, number][] = []; 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]); return out; }