puzzle-trainer/generators/tango.ts
2026-05-23 01:05:21 +00:00

184 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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); // 911 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 };
}