93 lines
2.4 KiB
TypeScript
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 };
|
|
}
|