import { createRng, shuffle } from "../rng"; // Legacy export — board still imports it but we no longer use it for hints export type ShapeHint = "free"; export interface Region { id: number; cells: [number, number][]; // all cells in grid hintCell: [number, number]; // topmost-leftmost cell (always in solution) relCells: [number, number][]; // shape normalised to (0,0) origin previewRows: number; previewCols: number; size: number; color: string; } export interface PatchesPuzzle { size: number; // 6 regions: Region[]; grid: number[][]; // grid[r][c] = regionId } export const PATCH_COLORS = [ "#e8b040", // gold "#4db86e", // green "#4a9fd4", // blue "#e05050", // red "#e07830", // orange "#30b8b0", // teal "#9060c8", // purple "#d06898", // pink ]; /** Place N seeds spread across the grid (no two adjacent). */ function placeSeeds(size: number, n: number, rng: () => number): [number, number][] { const all: [number, number][] = []; for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) all.push([r, c]); const pool = shuffle(all, rng); const seeds: [number, number][] = []; const banned = new Set(); for (const [r, c] of pool) { if (seeds.length >= n) break; if (banned.has(`${r},${c}`)) continue; seeds.push([r, c]); // Ban cell + its orthogonal neighbours (avoid touching seeds) for (const [dr, dc] of [[0,0],[-1,0],[1,0],[0,-1],[0,1]]) banned.add(`${r+dr},${c+dc}`); } return seeds; } /** * Grow N polyomino regions from seeds, then flood-fill any remaining cells. * Returns a size×size grid where grid[r][c] = regionId. */ function growPolyominos(size: number, seeds: [number, number][], rng: () => number): number[][] { const grid: number[][] = Array.from({ length: size }, () => Array(size).fill(-1)); const sizes: number[] = Array(seeds.length).fill(0); const n = seeds.length; const target = Math.round((size * size) / n); // Place seeds for (let i = 0; i < n; i++) { grid[seeds[i][0]][seeds[i][1]] = i; sizes[i] = 1; } // BFS frontier per region const ADJ: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]]; // Build initial frontiers const frontiers: Set[] = Array.from({ length: n }, (_, id) => { const s = new Set(); const [sr, sc] = seeds[id]; for (const [dr, dc] of ADJ) { const nr = sr+dr, nc = sc+dc; if (nr>=0 && nr=0 && nc i), rng); for (const id of order) { if (sizes[id] >= target) continue; // Pick a random free frontier cell const candidates = [...frontiers[id]].filter(k => { const [r, c] = k.split(",").map(Number); return grid[r][c] === -1; }); if (!candidates.length) continue; const k = candidates[Math.floor(rng() * candidates.length)]; const [nr, nc] = k.split(",").map(Number); if (grid[nr][nc] !== -1) { frontiers[id].delete(k); continue; } // race condition grid[nr][nc] = id; sizes[id]++; frontiers[id].delete(k); anyGrew = true; // Expand frontier for (const [dr, dc] of ADJ) { const nnr = nr+dr, nnc = nc+dc; if (nnr>=0 && nnr=0 && nnc=size || nc<0 || nc>=size) continue; const id = grid[nr][nc]; if (id === -1) continue; if (sizes[id] < bestSz) { bestSz = sizes[id]; best = id; } } if (best !== -1) { grid[r][c] = best; sizes[best]++; changed = true; } } } return grid; } export function generatePatches(date: string): PatchesPuzzle { const seed = date.split("-").reduce((a, n) => a * 1000 + parseInt(n), 0) + 333; const rng = createRng(seed); const size = 6; // 7 or 8 regions → avg 4.5–5 cells each, with interesting shapes const n = 7 + Math.floor(rng() * 2); const seeds = placeSeeds(size, n, rng); const grid = growPolyominos(size, seeds, rng); const numRegions = Math.max(...grid.flat()) + 1; // Collect cells per region const regionCells = new Map(); for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) { const id = grid[r][c]; if (!regionCells.has(id)) regionCells.set(id, []); regionCells.get(id)!.push([r, c]); } const colors = shuffle([...PATCH_COLORS], rng); const regions: Region[] = []; for (let id = 0; id < numRegions; id++) { const cells = regionCells.get(id) ?? []; if (!cells.length) continue; // Hint cell = topmost row, then leftmost col const hintCell = cells.reduce((best, cur) => cur[0] < best[0] || (cur[0] === best[0] && cur[1] < best[1]) ? cur : best ); // Normalised shape (origin at 0,0) const minR = Math.min(...cells.map(([r]) => r)); const minC = Math.min(...cells.map(([, c]) => c)); const relCells: [number, number][] = cells.map(([r, c]) => [r - minR, c - minC]); const previewRows = Math.max(...relCells.map(([r]) => r)) + 1; const previewCols = Math.max(...relCells.map(([, c]) => c)) + 1; regions.push({ id, cells, hintCell, relCells, previewRows, previewCols, size: cells.length, color: colors[id % colors.length], }); } return { size, regions, grid }; }