export type DifficultyName = 'easy' | 'medium' | 'hard' | 'very-hard' | 'insane' | 'inhuman'; export type Difficulty = DifficultyName | number; const DIGITS = '123456789'; const ROWS = 'ABCDEFGHI'; const COLS = DIGITS; let SQUARES: string[] = []; let UNITS: string[][] = []; let SQUARE_UNITS_MAP: Record = {}; let SQUARE_PEERS_MAP: Record = {}; const MIN_GIVENS = 17; const NR_SQUARES = 81; const DIFFICULTY: Record = { easy: 62, medium: 53, hard: 44, 'very-hard': 35, insane: 26, inhuman: 17, }; const BLANK_CHAR = '.'; const BLANK_BOARD = BLANK_CHAR.repeat(NR_SQUARES); function initialize() { SQUARES = cross(ROWS, COLS); UNITS = getAllUnits(ROWS, COLS); SQUARE_UNITS_MAP = getSquareUnitsMap(SQUARES, UNITS); SQUARE_PEERS_MAP = getSquarePeersMap(SQUARES, SQUARE_UNITS_MAP); } function cross(a: string, b: string) { const res: string[] = []; for (const ai of a) { for (const bi of b) { res.push(ai + bi); } } return res; } function inStr(v: string, seq: string) { return seq.indexOf(v) !== -1; } function shuffle(seq: T[]): T[] { const arr = [...seq]; for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } function randRange(max: number, min = 0) { return Math.floor(Math.random() * (max - min)) + min; } function stripDups(seq: T[]): T[] { return Array.from(new Set(seq)); } function forceRange(nr: number, max: number, min = 0) { return Math.min(Math.max(nr || 0, min), max); } function getAllUnits(rows: string, cols: string) { const units: string[][] = []; for (const r of rows) units.push(cross(r, cols)); for (const c of cols) units.push(cross(rows, c)); const row_blocks = ['ABC', 'DEF', 'GHI']; const col_blocks = ['123', '456', '789']; for (const rb of row_blocks) { for (const cb of col_blocks) { units.push(cross(rb, cb)); } } return units; } function getSquareUnitsMap(squares: string[], units: string[][]) { const map: Record = {}; for (const s of squares) { const list: string[][] = []; for (const u of units) { if (u.includes(s)) list.push(u); } map[s] = list; } return map; } function getSquarePeersMap(squares: string[], units_map: Record) { const map: Record = {}; for (const s of squares) { const peers: string[] = []; for (const unit of units_map[s]) { for (const sq of unit) { if (sq !== s && !peers.includes(sq)) peers.push(sq); } } map[s] = peers; } return map; } function getSquareValsMap(board: string) { const map: Record = {}; if (board.length !== SQUARES.length) throw 'Board length mismatch'; for (let i = 0; i < SQUARES.length; i++) { map[SQUARES[i]] = board[i]; } return map; } function validateBoard(board: string): true | string { if (!board) return 'Empty board'; if (board.length !== NR_SQUARES) return 'Invalid board size'; for (let i = 0; i < board.length; i++) { const c = board[i]; if (!inStr(c, DIGITS) && c !== BLANK_CHAR) { return `Invalid char at ${i}: ${c}`; } } return true; } function getCandidatesMap(board: string): Record | false { const report = validateBoard(board); if (report !== true) throw report; const map: Record = {}; const squares_values = getSquareValsMap(board); for (const s of SQUARES) map[s] = DIGITS; for (const s in squares_values) { const val = squares_values[s]; if (inStr(val, DIGITS)) { const new_candidates = assign(map, s, val); if (!new_candidates) return false; } } return map; } function assign(candidates: Record, square: string, val: string) { const others = candidates[square].replace(val, ''); for (const v of others) { const next = eliminate(candidates, square, v); if (!next) return false; } return candidates; } function eliminate(candidates: Record, square: string, val: string) { if (!inStr(val, candidates[square])) return candidates; candidates[square] = candidates[square].replace(val, ''); const n = candidates[square].length; if (n === 1) { const target_val = candidates[square]; for (const peer of SQUARE_PEERS_MAP[square]) { const next = eliminate(candidates, peer, target_val); if (!next) return false; } } else if (n === 0) return false; for (const unit of SQUARE_UNITS_MAP[square]) { const places: string[] = []; for (const sq of unit) { if (inStr(val, candidates[sq])) places.push(sq); } if (places.length === 0) return false; else if (places.length === 1) { const next = assign(candidates, places[0], val); if (!next) return false; } } return candidates; } function search(candidates: Record, reverse: boolean): any { if (!candidates) return false; reverse = reverse || false; let max_nr_candidates = 0; for (const s of SQUARES) { const n = candidates[s].length; if (n > max_nr_candidates) { max_nr_candidates = n; } } if (max_nr_candidates === 1) return candidates; let min_nr_candidates = 10; let min_square = ''; for (const s of SQUARES) { const n = candidates[s].length; if (n < min_nr_candidates && n > 1) { min_nr_candidates = n; min_square = s; } } const min_candidates = candidates[min_square]; if (!reverse) { for (const v of min_candidates) { const copy = JSON.parse(JSON.stringify(candidates)); const next = search(assign(copy, min_square, v), reverse); if (next) return next; } } else { for (let i = min_candidates.length - 1; i >= 0; --i) { const v = min_candidates[i]; const copy = JSON.parse(JSON.stringify(candidates)); const next = search(assign(copy, min_square, v), reverse); if (next) return next; } } return false; } export class SudokuGenerator { static generate(difficulty: Difficulty, unique?: boolean): string { let numericDifficulty: number; if (typeof difficulty === 'string' || typeof difficulty === 'undefined') { numericDifficulty = DIFFICULTY[difficulty as DifficultyName] || DIFFICULTY.easy; } else { numericDifficulty = difficulty; } numericDifficulty = forceRange(numericDifficulty, NR_SQUARES + 1, MIN_GIVENS); unique = unique ?? true; const blank_board = BLANK_BOARD; let candidates = getCandidatesMap(blank_board); if (!candidates) return SudokuGenerator.generate(numericDifficulty); const shuffled_squares = shuffle(SQUARES); for (const square of shuffled_squares) { const rand_candidate_idx = randRange(candidates[square].length); const rand_candidate = candidates[square][rand_candidate_idx]; if (!assign(candidates, square, rand_candidate)) break; const single_candidates: string[] = []; for (const s of SQUARES) { if (candidates[s].length === 1) single_candidates.push(candidates[s]); } if ( single_candidates.length >= numericDifficulty && stripDups(single_candidates).length >= 8 ) { let board = ''; let givens_idxs: number[] = []; for (let i = 0; i < SQUARES.length; i++) { const s = SQUARES[i]; if (candidates[s].length === 1) { board += candidates[s]; givens_idxs.push(i); } else { board += BLANK_CHAR; } } if (givens_idxs.length > numericDifficulty) { givens_idxs = shuffle(givens_idxs); for (let i = 0; i < givens_idxs.length - numericDifficulty; ++i) { const target = givens_idxs[i]; board = board.substring(0, target) + BLANK_CHAR + board.substring(target + 1); } } if (SudokuGenerator.solve(board)) return board; } } return SudokuGenerator.generate(numericDifficulty); } static solve(board: string, reverse?: boolean): string | false { const report = validateBoard(board); if (report !== true) throw report; let nr_givens = 0; for (const ch of board) { if (ch !== BLANK_CHAR && inStr(ch, DIGITS)) nr_givens++; } if (nr_givens < MIN_GIVENS) throw 'Too few givens'; reverse = reverse || false; const candidates = getCandidatesMap(board); if (!candidates) return false; const result = search(candidates, reverse); if (result) { let solution = ''; for (const s of SQUARES) solution += result[s]; return solution; } return false; } } initialize();