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

93 lines
2.4 KiB
TypeScript

import { createRng, shuffle } from "../rng";
export interface SudokuPuzzle {
size: 6;
// 0 = empty, 1-6 = given
given: number[][];
solution: number[][];
}
function isValid(grid: number[][], r: number, c: number, val: number): boolean {
for (let i = 0; i < 6; i++) {
if (grid[r][i] === val || grid[i][c] === val) return false;
}
// 2x3 box
const br = Math.floor(r / 2) * 2, bc = Math.floor(c / 3) * 3;
for (let dr = 0; dr < 2; dr++)
for (let dc = 0; dc < 3; dc++)
if (grid[br + dr][bc + dc] === val) return false;
return true;
}
function solve(grid: number[][], rng: () => number): boolean {
for (let r = 0; r < 6; r++) {
for (let c = 0; c < 6; c++) {
if (grid[r][c] !== 0) continue;
const nums = shuffle([1, 2, 3, 4, 5, 6], rng);
for (const n of nums) {
if (!isValid(grid, r, c, n)) continue;
grid[r][c] = n;
if (solve(grid, rng)) return true;
grid[r][c] = 0;
}
return false;
}
}
return true;
}
function countSolutions(grid: number[][], limit = 2): number {
let count = 0;
function bt(): boolean {
for (let r = 0; r < 6; r++) {
for (let c = 0; c < 6; c++) {
if (grid[r][c] !== 0) continue;
for (let n = 1; n <= 6; n++) {
if (!isValid(grid, r, c, n)) continue;
grid[r][c] = n;
bt();
grid[r][c] = 0;
if (count >= limit) return true;
}
return false;
}
}
count++;
return count >= limit;
}
bt();
return count;
}
export function generateSudoku(date: string): SudokuPuzzle {
const seed = date.split("-").reduce((a, n) => a * 1000 + parseInt(n), 0) + 555;
const rng = createRng(seed);
const grid: number[][] = Array.from({ length: 6 }, () => Array(6).fill(0));
solve(grid, rng);
const solution = grid.map(r => [...r]);
// Remove cells while puzzle remains uniquely solvable
const positions = shuffle(
Array.from({ length: 36 }, (_, i) => [Math.floor(i / 6), i % 6] as [number, number]),
rng
);
const given = solution.map(r => [...r]);
let removed = 0;
for (const [r, c] of positions) {
if (removed >= 22) break; // keep ~14 givens in a 6x6
const val = given[r][c];
given[r][c] = 0;
// Quick check: still solvable
const test = given.map(row => [...row]);
if (countSolutions(test) !== 1) {
given[r][c] = val; // restore
} else {
removed++;
}
}
return { size: 6, given, solution };
}