TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import type { Rng } from "../rng";2import type { ArcadeGameDefinition, ArcadeOutcome } from "./types";34/**5 * GRID//BREAK — an 8×8 grid of energy blocks. The player fires a wave down a6 * column; every cluster (≥3 connected blocks of one colour) the wave touches7 * detonates, blocks fall, new ones drop in, and any new cluster detonates in a8 * chain. Bombs clear 3×3, line blocks clear a row, ×2 blocks double the step.9 */1011export interface GridbreakConfig {12 size: number;13 colors: number;14 /** Value per block by colour index, in bet units (× total bet / size²·k). */15 values: number[];16 weights: number[];17 specialChance: { bomb: number; line: number; x2: number };18 chainLadder: number[];19 minCluster: number;20 maxChains: number;21}2223export type Cell = { c: number; s?: "bomb" | "line" | "x2" };2425export interface GridStep {26 grid: Cell[][]; // [col][row], row 0 = top27 destroyed: [number, number][];28 specials: { type: string; at: [number, number] }[];29 chain: number;30 multiplier: number;31 stepValue: number;32 win: number;33}3435function drawCell(cfg: GridbreakConfig, rng: Rng): Cell {36 const c = rng.weighted(cfg.weights);37 const roll = rng.float();38 if (roll < cfg.specialChance.bomb) return { c, s: "bomb" };39 if (roll < cfg.specialChance.bomb + cfg.specialChance.line) return { c, s: "line" };40 if (roll < cfg.specialChance.bomb + cfg.specialChance.line + cfg.specialChance.x2) return { c, s: "x2" };41 return { c };42}4344function clusters(grid: Cell[][], size: number, minCluster: number): [number, number][][] {45 const seen = new Set<string>();46 const out: [number, number][][] = [];47 for (let x = 0; x < size; x++)48 for (let y = 0; y < size; y++) {49 const k = `${x}:${y}`;50 if (seen.has(k)) continue;51 const color = grid[x][y].c;52 const stack: [number, number][] = [[x, y]];53 const group: [number, number][] = [];54 seen.add(k);55 while (stack.length) {56 const [cx, cy] = stack.pop()!;57 group.push([cx, cy]);58 for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {59 const nx = cx + dx;60 const ny = cy + dy;61 if (nx < 0 || ny < 0 || nx >= size || ny >= size) continue;62 const nk = `${nx}:${ny}`;63 if (seen.has(nk) || grid[nx][ny].c !== color) continue;64 seen.add(nk);65 stack.push([nx, ny]);66 }67 }68 if (group.length >= minCluster) out.push(group);69 }70 return out;71}7273export function resolveGridbreak(def: ArcadeGameDefinition, bet: number, rng: Rng, inputRaw: { column?: number }): ArcadeOutcome {74 const cfg = def.config as unknown as GridbreakConfig;75 const size = cfg.size;76 const column = Math.max(0, Math.min(size - 1, Math.round(inputRaw.column ?? Math.floor(size / 2))));77 const grid: Cell[][] = Array.from({ length: size }, () => Array.from({ length: size }, () => drawCell(cfg, rng)));78 const steps: GridStep[] = [];79 const features: string[] = [];80 let chain = 0;81 let totalUnits = 0;82 const unit = 1 / (size * size); // one block of value 1 ≈ 1/64 of the bet83 let firstWave = true;84 while (chain < cfg.maxChains) {85 const all = clusters(grid, size, cfg.minCluster);86 // The first wave only detonates clusters touching the fired column; chains detonate everything.87 const hit = firstWave ? all.filter((g) => g.some(([x]) => x === column)) : all;88 if (hit.length === 0) break;89 const destroyed = new Set<string>();90 const specials: GridStep["specials"] = [];91 let stepMult = 1;92 const push = (x: number, y: number) => {93 if (x >= 0 && y >= 0 && x < size && y < size) destroyed.add(`${x}:${y}`);94 };95 for (const g of hit) for (const [x, y] of g) push(x, y);96 // Specials inside destroyed blocks trigger.97 for (const k of [...destroyed]) {98 const [x, y] = k.split(":").map(Number);99 const cell = grid[x][y];100 if (cell.s === "bomb") {101 specials.push({ type: "bomb", at: [x, y] });102 for (let dx = -1; dx <= 1; dx++) for (let dy = -1; dy <= 1; dy++) push(x + dx, y + dy);103 features.push("Bomb");104 } else if (cell.s === "line") {105 specials.push({ type: "line", at: [x, y] });106 for (let xx = 0; xx < size; xx++) push(xx, y);107 features.push("Line clear");108 } else if (cell.s === "x2") {109 specials.push({ type: "x2", at: [x, y] });110 stepMult *= 2;111 features.push("×2 block");112 }113 }114 const chainMult = cfg.chainLadder[Math.min(chain, cfg.chainLadder.length - 1)];115 let value = 0;116 const destroyedList: [number, number][] = [];117 for (const k of destroyed) {118 const [x, y] = k.split(":").map(Number);119 value += cfg.values[grid[x][y].c] * unit;120 destroyedList.push([x, y]);121 }122 const stepUnits = value * stepMult * chainMult;123 totalUnits += stepUnits;124 steps.push({125 grid: grid.map((c) => c.map((cell) => ({ ...cell }))),126 destroyed: destroyedList,127 specials,128 chain,129 multiplier: stepMult * chainMult,130 stepValue: Math.round(stepUnits * def.payScale * 10000) / 10000,131 win: Math.round(bet * stepUnits * def.payScale),132 });133 // Gravity + refill.134 for (let x = 0; x < size; x++) {135 const kept: Cell[] = [];136 for (let y = 0; y < size; y++) if (!destroyed.has(`${x}:${y}`)) kept.push(grid[x][y]);137 const fresh: Cell[] = [];138 while (fresh.length + kept.length < size) fresh.push(drawCell(cfg, rng));139 grid[x] = [...fresh, ...kept];140 }141 chain++;142 firstWave = false;143 if (chain > 1) features.push("Chain reaction");144 }145 // Final grid for presentation.146 steps.push({ grid: grid.map((c) => c.map((cell) => ({ ...cell }))), destroyed: [], specials: [], chain, multiplier: 1, stepValue: 0, win: 0 });147 const scaled = totalUnits * def.payScale;148 const capped = scaled > def.maxMultiplier;149 const multiplier = Math.min(def.maxMultiplier, scaled);150 const totalWin = Math.round(bet * multiplier);151 return {152 game: def.slug,153 version: def.version,154 bet,155 totalWin,156 multiplier: bet ? totalWin / bet : 0,157 capped,158 features: [...new Set(features)],159 steps,160 summary: { column, chains: chain, blocksDestroyed: steps.reduce((a, s) => a + s.destroyed.length, 0) },161 };162}163164export function randomGridbreakInput(def: ArcadeGameDefinition, rng: Rng): { column: number } {165 return { column: rng.int((def.config as unknown as GridbreakConfig).size) };166}167