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.4 KB · 160 lines typescript
Raw Blame History
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