import { createRng, shuffle } from "../rng"; export type Cell = "sun" | "moon" | null; export type EdgeConstraint = "=" | "x" | null; export interface TangoPuzzle { size: number; // always 6 given: Cell[][]; hEdges: EdgeConstraint[][]; vEdges: EdgeConstraint[][]; solution: Cell[][]; } // Check if placing v at (r,c) is consistent with current partial grid function consistent( grid: Cell[][], hEdges: EdgeConstraint[][], vEdges: EdgeConstraint[][], r: number, c: number, v: Cell, size: number ): boolean { // Row balance let rs = 0, rm = 0; for (let j = 0; j < size; j++) { const cell = j === c ? v : grid[r][j]; if (cell === "sun") rs++; else if (cell === "moon") rm++; } if (rs > size / 2 || rm > size / 2) return false; // Col balance let cs = 0, cm = 0; for (let i = 0; i < size; i++) { const cell = i === r ? v : grid[i][c]; if (cell === "sun") cs++; else if (cell === "moon") cm++; } if (cs > size / 2 || cm > size / 2) return false; // No 3 consecutive in row for (let j = Math.max(0, c - 2); j <= Math.min(size - 3, c); j++) { const a = j === c ? v : grid[r][j]; const b = j + 1 === c ? v : grid[r][j + 1]; const d = j + 2 === c ? v : grid[r][j + 2]; if (a && a === b && b === d) return false; } // No 3 consecutive in col for (let i = Math.max(0, r - 2); i <= Math.min(size - 3, r); i++) { const a = i === r ? v : grid[i][c]; const b = i + 1 === r ? v : grid[i + 1][c]; const d = i + 2 === r ? v : grid[i + 2][c]; if (a && a === b && b === d) return false; } // Edge constraints (only check placed neighbours) const chk = (e: EdgeConstraint, nb: Cell) => { if (!e || !nb) return true; if (e === "=" && nb !== v) return false; if (e === "x" && nb === v) return false; return true; }; if (!chk(c > 0 ? hEdges[r][c - 1] : null, grid[r][c - 1])) return false; if (!chk(c < size - 1 ? hEdges[r][c] : null, grid[r][c + 1])) return false; if (!chk(r > 0 ? vEdges[r - 1][c] : null, grid[r - 1]?.[c] ?? null)) return false; if (!chk(r < size - 1 ? vEdges[r][c] : null, grid[r + 1]?.[c] ?? null)) return false; // Unique rows: if this row is now complete, check it doesn't duplicate an earlier complete row const rowComplete = grid[r].every((cell, j) => (j === c ? v : cell) !== null); if (rowComplete) { const thisRow = grid[r].map((cell, j) => j === c ? v : cell); for (let i = 0; i < r; i++) { if (grid[i].every(cell => cell !== null) && grid[i].every((cell, j) => cell === thisRow[j])) return false; } } // Unique cols: if this col is now complete, check it doesn't duplicate an earlier complete col const colComplete = Array.from({ length: size }, (_, i) => i === r ? v : grid[i][c]).every(cell => cell !== null); if (colComplete) { const thisCol = Array.from({ length: size }, (_, i) => i === r ? v : grid[i][c]); for (let j = 0; j < c; j++) { const other = Array.from({ length: size }, (_, i) => grid[i][j]); if (other.every(cell => cell !== null) && other.every((cell, i) => cell === thisCol[i])) return false; } } return true; } // Count solutions (stops at `limit`) function countSolutions( given: Cell[][], hEdges: EdgeConstraint[][], vEdges: EdgeConstraint[][], size: number, limit = 2 ): number { const grid = given.map(r => [...r]); let count = 0; function bt(pos: number): void { if (count >= limit) return; if (pos === size * size) { count++; return; } const r = Math.floor(pos / size), c = pos % size; if (grid[r][c] !== null) { bt(pos + 1); return; } for (const v of ["sun", "moon"] as Cell[]) { if (consistent(grid, hEdges, vEdges, r, c, v, size)) { grid[r][c] = v; bt(pos + 1); grid[r][c] = null; } } } bt(0); return count; } function generateSolution(size: number, rng: () => number): Cell[][] { const grid: Cell[][] = Array.from({ length: size }, () => Array(size).fill(null)); const noHEdges: EdgeConstraint[][] = Array.from({ length: size }, () => Array(size - 1).fill(null)); const noVEdges: EdgeConstraint[][] = Array.from({ length: size - 1 }, () => Array(size).fill(null)); function bt(pos: number): boolean { if (pos === size * size) return true; const r = Math.floor(pos / size), c = pos % size; const opts: Cell[] = rng() > 0.5 ? ["sun", "moon"] : ["moon", "sun"]; for (const v of opts) { if (consistent(grid, noHEdges, noVEdges, r, c, v, size)) { grid[r][c] = v; if (bt(pos + 1)) return true; grid[r][c] = null; } } return false; } bt(0); return grid; } export function generateTango(date: string): TangoPuzzle { const seed = date.split("-").reduce((a, n) => a * 1000 + parseInt(n), 0) + 999; const rng = createRng(seed); const size = 6; const solution = generateSolution(size, rng); // Start with ALL edges as constraints const hEdges: EdgeConstraint[][] = Array.from({ length: size }, () => Array(size - 1).fill(null)); const vEdges: EdgeConstraint[][] = Array.from({ length: size - 1 }, () => Array(size).fill(null)); for (let r = 0; r < size; r++) for (let c = 0; c < size - 1; c++) hEdges[r][c] = solution[r][c] === solution[r][c + 1] ? "=" : "x"; for (let r = 0; r < size - 1; r++) for (let c = 0; c < size; c++) vEdges[r][c] = solution[r][c] === solution[r + 1][c] ? "=" : "x"; // Build shuffled edge list const edges: Array<{ type: "h" | "v"; r: number; c: number }> = []; for (let r = 0; r < size; r++) for (let c = 0; c < size - 1; c++) edges.push({ type: "h", r, c }); for (let r = 0; r < size - 1; r++) for (let c = 0; c < size; c++) edges.push({ type: "v", r, c }); const given: Cell[][] = Array.from({ length: size }, () => Array(size).fill(null)); // Place given cells first (like LinkedIn: ~9-11 pre-placed sun/moon values) const allCells: [number, number][] = []; for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) allCells.push([r, c]); const cellOrder = shuffle(allCells, rng); const targetGivens = 9 + Math.floor(rng() * 3); // 9–11 given cells for (const [r, c] of cellOrder) { if (given.flat().filter(Boolean).length >= targetGivens) break; given[r][c] = solution[r][c]; if (countSolutions(given, hEdges, vEdges, size) > 1) given[r][c] = null; } // Remove edges greedily while maintaining unique solution for (const e of shuffle(edges, rng)) { if (e.type === "h") { const old = hEdges[e.r][e.c]; hEdges[e.r][e.c] = null; if (countSolutions(given, hEdges, vEdges, size) !== 1) hEdges[e.r][e.c] = old; } else { const old = vEdges[e.r][e.c]; vEdges[e.r][e.c] = null; if (countSolutions(given, hEdges, vEdges, size) !== 1) vEdges[e.r][e.c] = old; } } return { size, given, hEdges, vEdges, solution }; }