TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import type { GameDefinition, Grid } from "../types";2import type { Rng } from "../rng";3import { drawSymbol, reelTables } from "../reels";45const key = (r: number, y: number) => `${r}:${y}`;67/** Expanding wilds: every reel containing a wild becomes all-wild. Returns expanded reel indexes. */8export function applyExpandingWilds(def: GameDefinition, grid: Grid, free: boolean): number[] {9 const w = def.wild;10 if (!w?.expanding) return [];11 if (w.expanding === "freeSpins" && !free) return [];12 const out: number[] = [];13 for (let r = 0; r < grid.length; r++) {14 if (grid[r].includes(w.id) && !grid[r].every((s) => s === w.id)) {15 for (let y = 0; y < grid[r].length; y++) grid[r][y] = w.id;16 out.push(r);17 } else if (grid[r].includes(w.id) && grid[r].length > 1 && grid[r].every((s) => s === w.id)) {18 out.push(r);19 }20 }21 return out;22}2324/** Floating / random wilds dropped before evaluation. Never overwrite scatters or existing wilds. */25export function applyFloatingWilds(def: GameDefinition, grid: Grid, rng: Rng, free: boolean, forced?: { min: number; max: number }): [number, number][] {26 const w = def.wild;27 if (!w) return [];28 let cfg = forced;29 if (!cfg) {30 if (!w.floating) return [];31 const chance = free && w.floating.freeSpinsChance !== undefined ? w.floating.freeSpinsChance : w.floating.chance;32 if (!rng.chance(chance)) return [];33 cfg = { min: w.floating.min, max: w.floating.max };34 }35 const count = cfg.min + rng.int(cfg.max - cfg.min + 1);36 const cells: [number, number][] = [];37 for (let r = 0; r < grid.length; r++)38 for (let y = 0; y < grid[r].length; y++) {39 const id = grid[r][y];40 if (id !== w.id && id !== def.scatter?.id && id !== def.holdRespin?.symbolId) cells.push([r, y]);41 }42 const added: [number, number][] = [];43 for (let i = 0; i < count && cells.length > 0; i++) {44 const idx = rng.int(cells.length);45 const [r, y] = cells.splice(idx, 1)[0];46 grid[r][y] = w.id;47 added.push([r, y]);48 }49 return added;50}5152/** Assign multipliers to wilds present on the grid (only cells not yet assigned). */53export function assignWildMultipliers(54 def: GameDefinition,55 grid: Grid,56 rng: Rng,57 free: boolean,58 existing: Map<string, number>,59): { position: [number, number]; value: number }[] {60 const w = def.wild;61 if (!w?.multiplier) return [];62 if (w.multiplier.freeSpinsOnly && !free) return [];63 const out: { position: [number, number]; value: number }[] = [];64 for (let r = 0; r < grid.length; r++)65 for (let y = 0; y < grid[r].length; y++) {66 if (grid[r][y] !== w.id) continue;67 const k = key(r, y);68 if (existing.has(k)) continue;69 const v = w.multiplier.values[rng.weighted(w.multiplier.weights)];70 existing.set(k, v);71 out.push({ position: [r, y], value: v });72 }73 return out;74}7576/** Exploding wilds: the 8 neighbours of each *new* wild that are regular symbols are destroyed. */77export function explodeWilds(def: GameDefinition, grid: Grid, newWilds: [number, number][]): [number, number][] {78 const w = def.wild;79 if (!w?.exploding || newWilds.length === 0) return [];80 const tables = reelTables(def);81 const removed = new Set<string>();82 const out: [number, number][] = [];83 for (const [r, y] of newWilds) {84 for (let dr = -1; dr <= 1; dr++)85 for (let dy = -1; dy <= 1; dy++) {86 if (dr === 0 && dy === 0) continue;87 const rr = r + dr;88 const yy = y + dy;89 if (rr < 0 || rr >= grid.length || yy < 0 || yy >= grid[rr].length) continue;90 const id = grid[rr][yy];91 const sym = tables.byId.get(id);92 if (sym?.kind === "regular" && !removed.has(key(rr, yy))) {93 removed.add(key(rr, yy));94 out.push([rr, yy]);95 }96 }97 }98 return out;99}100101/** Positions of wilds on the grid. */102export function wildPositions(def: GameDefinition, grid: Grid): [number, number][] {103 const w = def.wild?.id;104 const out: [number, number][] = [];105 if (!w) return out;106 for (let r = 0; r < grid.length; r++) for (let y = 0; y < grid[r].length; y++) if (grid[r][y] === w) out.push([r, y]);107 return out;108}109110/** Moving wilds: shift every tracked wild one reel to the left; drop the ones that leave the grid. */111export function shiftMovingWilds(positions: [number, number][]): [number, number][] {112 return positions.filter(([r]) => r - 1 >= 0).map(([r, y]) => [r - 1, y] as [number, number]);113}114115/** Overwrite grid cells with wilds (sticky / moving). */116export function stampWilds(def: GameDefinition, grid: Grid, positions: [number, number][]): void {117 const w = def.wild?.id;118 if (!w) return;119 for (const [r, y] of positions) if (r < grid.length && y < grid[r].length) grid[r][y] = w;120}121122/** Upgrade one low-tier symbol type present on the grid to a random mid/high symbol ("upgradeLow" feature). */123export function upgradeLowSymbols(def: GameDefinition, grid: Grid, rng: Rng): number {124 const tables = reelTables(def);125 const targets = def.symbols.filter((s) => s.kind === "regular" && (s.tier === "mid" || s.tier === "high")).map((s) => s.id);126 if (targets.length === 0) return 0;127 const lowsPresent = [...new Set(grid.flat().filter((id) => tables.byId.get(id)?.tier === "low"))];128 if (lowsPresent.length === 0) return 0;129 const from = lowsPresent[rng.int(lowsPresent.length)];130 const pick = targets[rng.int(targets.length)];131 let n = 0;132 for (let r = 0; r < grid.length; r++)133 for (let y = 0; y < grid[r].length; y++) {134 if (grid[r][y] === from) {135 grid[r][y] = pick;136 n++;137 }138 }139 return n;140}141142/** Refill a reel column after removals by dropping surviving symbols down and drawing new ones on top. */143export function refillGrid(def: GameDefinition, grid: Grid, removed: [number, number][], rng: Rng, free: boolean, protectedCells?: Set<string>): void {144 const tables = reelTables(def);145 const removedSet = new Set(removed.filter(([r, y]) => !protectedCells?.has(key(r, y))).map(([r, y]) => key(r, y)));146 for (let r = 0; r < grid.length; r++) {147 const col = grid[r];148 const rows = col.length;149 const kept: string[] = [];150 // Row 0 is the top. Survivors fall to the bottom, keeping order.151 for (let y = 0; y < rows; y++) if (!removedSet.has(key(r, y))) kept.push(col[y]);152 const missing = rows - kept.length;153 if (missing === 0) continue;154 const fresh: string[] = [];155 for (let i = 0; i < missing; i++) fresh.push(drawSymbol(tables, r, free, rng));156 const next = [...fresh, ...kept];157 for (let y = 0; y < rows; y++) col[y] = next[y];158 }159}160