export type Difficulty = 'easy' | 'medium' | 'hard'; const CELLS_TO_REMOVE = { easy: 25, medium: 40, hard: 50, } as const; export class SudokuGenerator { private solution: string = ''; private puzzle: string = ''; constructor() { this.generate('medium'); } generate(difficulty: Difficulty = 'medium', customRemovedCells?: number): void { this.solution = this.generateSolvedBoard(); if (!this.validateFullBoard(this.solution)) { throw new Error('Generated invalid solved board'); } this.puzzle = this.makePuzzle(this.solution, difficulty, customRemovedCells); } getSolution(): string { return this.solution; } getPuzzle(): string { return this.puzzle; } private generateSolvedBoard(): string { const size = 9; const box = 3; const pattern = (r: number, c: number) => (box * (r % box) + Math.floor(r / box) + c) % size; const baseBoard: number[][] = Array.from({ length: size }, (_, r) => Array.from({ length: size }, (_, c) => pattern(r, c) + 1) ); const shuffle = (arr: T[]): T[] => { const copy = [...arr]; for (let i = copy.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [copy[i], copy[j]] = [copy[j], copy[i]]; } return copy; }; const rows = shuffle([0, 1, 2]) .concat(shuffle([3, 4, 5])) .concat(shuffle([6, 7, 8])); const cols = shuffle([0, 1, 2]) .concat(shuffle([3, 4, 5])) .concat(shuffle([6, 7, 8])); const rowBands = shuffle([0, 1, 2]); const colStacks = shuffle([0, 1, 2]); const shuffledRows = rowBands.flatMap(band => rows.slice(band * 3, band * 3 + 3)); const shuffledCols = colStacks.flatMap(stack => cols.slice(stack * 3, stack * 3 + 3)); const digits = shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9]); const board: number[][] = shuffledRows.map(r => shuffledCols.map(c => digits[baseBoard[r][c] - 1]) ); return board.flat().join(''); } private makePuzzle(solution: string, difficulty: Difficulty, customRemovedCells?: number): string { if (solution.length !== 81 || !/^[1-9]+$/.test(solution)) { throw new Error('Invalid solved board: must be 81 digits 1–9'); } const removeCount = customRemovedCells ?? CELLS_TO_REMOVE[difficulty]; const board = solution.split(''); const indices = Array.from({ length: 81 }, (_, i) => i); for (let i = indices.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [indices[i], indices[j]] = [indices[j], indices[i]]; } for (let i = 0; i < removeCount; i++) { const idx = indices[i]; board[idx] = '.'; } return board.join(''); } private validateFullBoard(board: string): boolean { if (board.length !== 81) return false; if (!/^[1-9]+$/.test(board)) return false; const getCell = (r: number, c: number) => Number(board[r * 9 + c]); for (let r = 0; r < 9; r++) { const sum = Array.from({ length: 9 }, (_, c) => getCell(r, c)) .reduce((a, b) => a + b, 0); if (sum !== 45) return false; } for (let c = 0; c < 9; c++) { const sum = Array.from({ length: 9 }, (_, r) => getCell(r, c)) .reduce((a, b) => a + b, 0); if (sum !== 45) return false; } for (let br = 0; br < 3; br++) { for (let bc = 0; bc < 3; bc++) { const sum = Array.from({ length: 9 }, (_, i) => { const r = br * 3 + Math.floor(i / 3); const c = bc * 3 + (i % 3); return getCell(r, c); }).reduce((a, b) => a + b, 0); if (sum !== 45) return false; } } return true; } }