feat: prepare vim-sudoku for unified API implementation
- Archive previous implementation attempt (src -> src_failed) - Create comprehensive IMPLEMENTATION_PLAN.md with phased approach - Create IMPLEMENTATION.md to track implementation progress - Update example app to match vim-maze patterns: - Simplify UI to desktop-only (no mobile support) - Add score metrics display (time, keystrokes, final score) - Update instructions for sudoku-specific keys (hjkl, i for edit) - Remove unnecessary mobile hooks Phase 5 complete. Ready to begin Phase 10 (Setup & Core Types).
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
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<string, string[][]> = {};
|
||||
let SQUARE_PEERS_MAP: Record<string, string[]> = {};
|
||||
|
||||
const MIN_GIVENS = 17;
|
||||
const NR_SQUARES = 81;
|
||||
|
||||
const DIFFICULTY: Record<DifficultyName, number> = {
|
||||
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<T>(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<T>(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<string, string[][]> = {};
|
||||
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<string, string[][]>) {
|
||||
const map: Record<string, string[]> = {};
|
||||
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<string, string> = {};
|
||||
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<string, string> | false {
|
||||
const report = validateBoard(board);
|
||||
if (report !== true) throw report;
|
||||
|
||||
const map: Record<string, string> = {};
|
||||
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<string, string>, 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<string, string>, 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<string, string>, 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();
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { SudokuBoard, SudokuCell } from './types';
|
||||
|
||||
export class SudokuRenderer {
|
||||
static renderBoard(container: HTMLElement, board: SudokuBoard): void {
|
||||
const sudokuDiv = document.createElement('div');
|
||||
sudokuDiv.id = 'sudoku';
|
||||
sudokuDiv.className = 'sudoku-board';
|
||||
|
||||
for (let row = 0; row < 9; row++) {
|
||||
const rowDiv = document.createElement('div');
|
||||
rowDiv.className = 'sudoku-row';
|
||||
|
||||
for (let col = 0; col < 9; col++) {
|
||||
const cell = board[row][col];
|
||||
const cellDiv = document.createElement('div');
|
||||
cellDiv.className = 'sudoku-cell';
|
||||
cellDiv.dataset.row = row.toString();
|
||||
cellDiv.dataset.col = col.toString();
|
||||
cellDiv.textContent = cell.value || '';
|
||||
|
||||
if (cell.isGiven) {
|
||||
cellDiv.classList.add('given');
|
||||
}
|
||||
|
||||
if (cell.hasConflict) {
|
||||
cellDiv.classList.add('conflict');
|
||||
}
|
||||
|
||||
if (col === 2 || col === 5) {
|
||||
cellDiv.classList.add('border-right');
|
||||
}
|
||||
|
||||
if (row === 2 || row === 5) {
|
||||
cellDiv.classList.add('border-bottom');
|
||||
}
|
||||
|
||||
rowDiv.appendChild(cellDiv);
|
||||
}
|
||||
|
||||
sudokuDiv.appendChild(rowDiv);
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(sudokuDiv);
|
||||
}
|
||||
|
||||
static renderEmptyBoard(container: HTMLElement): void {
|
||||
const emptyBoard: SudokuBoard = Array(9).fill(null).map(() =>
|
||||
Array(9).fill(null).map(() => ({
|
||||
value: '',
|
||||
isGiven: false,
|
||||
hasConflict: false
|
||||
}))
|
||||
);
|
||||
|
||||
SudokuRenderer.renderBoard(container, emptyBoard);
|
||||
}
|
||||
|
||||
static updateCell(container: HTMLElement, row: number, col: number, cell: SudokuCell): void {
|
||||
const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement;
|
||||
if (!cellEl) return;
|
||||
|
||||
cellEl.textContent = cell.value || '';
|
||||
cellEl.classList.toggle('given', cell.isGiven);
|
||||
cellEl.classList.toggle('conflict', cell.hasConflict);
|
||||
}
|
||||
|
||||
static highlightCursor(container: HTMLElement, row: number, col: number, editMode: boolean): void {
|
||||
const cells = container.querySelectorAll('.sudoku-cell');
|
||||
cells.forEach(cell => cell.classList.remove('cursor', 'edit-mode'));
|
||||
|
||||
const cursorCell = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement;
|
||||
if (cursorCell) {
|
||||
cursorCell.classList.add('cursor');
|
||||
if (editMode) {
|
||||
cursorCell.classList.add('edit-mode');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static flashCell(container: HTMLElement, row: number, col: number, className: string, duration: number = 300): void {
|
||||
const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement;
|
||||
if (!cellEl) return;
|
||||
|
||||
cellEl.classList.add(className);
|
||||
setTimeout(() => cellEl.classList.remove(className), duration);
|
||||
}
|
||||
|
||||
static highlightConflicts(container: HTMLElement, conflictIndices: number[], duration: number = 2000): void {
|
||||
conflictIndices.forEach((idx) => {
|
||||
const row = Math.floor(idx / 9);
|
||||
const col = idx % 9;
|
||||
const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement;
|
||||
if (cellEl) {
|
||||
cellEl.classList.add('conflict');
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
conflictIndices.forEach((idx) => {
|
||||
const row = Math.floor(idx / 9);
|
||||
const col = idx % 9;
|
||||
const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement;
|
||||
if (cellEl) {
|
||||
cellEl.classList.remove('conflict');
|
||||
}
|
||||
});
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { useBoard } from './useBoard';
|
||||
export type { BoardManager, SudokuBoard, SudokuCell, Coord, Difficulty } from './types';
|
||||
export { SudokuGenerator } from './SudokuGenerator';
|
||||
export { SudokuRenderer } from './SudokuRenderer';
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { RefObject } from 'react';
|
||||
|
||||
export type Coord = { row: number; col: number };
|
||||
|
||||
export type SudokuCell = {
|
||||
value: string;
|
||||
isGiven: boolean;
|
||||
hasConflict: boolean;
|
||||
};
|
||||
|
||||
export type SudokuBoard = SudokuCell[][];
|
||||
|
||||
export type Difficulty = 'easy' | 'medium' | 'hard' | 'very-hard' | 'insane' | 'inhuman';
|
||||
|
||||
export type BoardManager = {
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
boardRef: RefObject<SudokuBoard>;
|
||||
initialBoardRef: RefObject<string>;
|
||||
solutionRef: RefObject<string | null>;
|
||||
renderBoard: () => void;
|
||||
generateBoard: (difficulty: Difficulty) => void;
|
||||
getCell: (row: number, col: number) => SudokuCell | null;
|
||||
setCell: (row: number, col: number, value: string) => boolean;
|
||||
canEditCell: (row: number, col: number) => boolean;
|
||||
isValidPlacement: (row: number, col: number, digit: string) => boolean;
|
||||
getConflicts: (row: number, col: number) => number[];
|
||||
isComplete: () => boolean;
|
||||
isFullyValid: () => boolean;
|
||||
solveBoard: () => boolean;
|
||||
resetBoard: () => void;
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
import { useRef, useState, useCallback } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
import type { BoardManager, SudokuBoard, SudokuCell, Difficulty } from './types';
|
||||
import { SudokuGenerator } from './SudokuGenerator';
|
||||
import { SudokuRenderer } from './SudokuRenderer';
|
||||
|
||||
export function useBoard(): BoardManager {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const boardRef = useRef<SudokuBoard>([]);
|
||||
const initialBoardRef = useRef<string>('');
|
||||
const solutionRef = useRef<string | null>(null);
|
||||
|
||||
const initializeBoard = useCallback((boardString: string): SudokuBoard => {
|
||||
const board: SudokuBoard = [];
|
||||
for (let row = 0; row < 9; row++) {
|
||||
const boardRow: SudokuCell[] = [];
|
||||
for (let col = 0; col < 9; col++) {
|
||||
const index = row * 9 + col;
|
||||
const value = boardString[index];
|
||||
boardRow.push({
|
||||
value: value === '.' ? '' : value,
|
||||
isGiven: value !== '.',
|
||||
hasConflict: false
|
||||
});
|
||||
}
|
||||
board.push(boardRow);
|
||||
}
|
||||
return board;
|
||||
}, []);
|
||||
|
||||
const getCell = useCallback((row: number, col: number): SudokuCell | null => {
|
||||
const board = boardRef.current;
|
||||
if (row < 0 || row >= 9 || col < 0 || col >= 9) return null;
|
||||
return board[row]?.[col] || null;
|
||||
}, []);
|
||||
|
||||
const canEditCell = useCallback((row: number, col: number): boolean => {
|
||||
const cell = getCell(row, col);
|
||||
return cell ? !cell.isGiven : false;
|
||||
}, [getCell]);
|
||||
|
||||
const isValidPlacement = useCallback((boardString: string, row: number, col: number, digit: string): boolean => {
|
||||
if (digit === '.') return true;
|
||||
|
||||
for (let c = 0; c < 9; c++) {
|
||||
const checkIdx = row * 9 + c;
|
||||
if (c !== col && boardString[checkIdx] === digit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let r = 0; r < 9; r++) {
|
||||
const checkIdx = r * 9 + col;
|
||||
if (r !== row && boardString[checkIdx] === digit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const boxRow = Math.floor(row / 3) * 3;
|
||||
const boxCol = Math.floor(col / 3) * 3;
|
||||
for (let r = boxRow; r < boxRow + 3; r++) {
|
||||
for (let c = boxCol; c < boxCol + 3; c++) {
|
||||
const checkIdx = r * 9 + c;
|
||||
if ((r !== row || c !== col) && boardString[checkIdx] === digit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const getConflicts = useCallback((boardString: string, row: number, col: number): number[] => {
|
||||
const conflicts: number[] = [];
|
||||
const digit = boardString[row * 9 + col];
|
||||
|
||||
if (digit === '.') return conflicts;
|
||||
|
||||
for (let c = 0; c < 9; c++) {
|
||||
if (c !== col && boardString[row * 9 + c] === digit) {
|
||||
conflicts.push(row * 9 + c);
|
||||
}
|
||||
}
|
||||
|
||||
for (let r = 0; r < 9; r++) {
|
||||
if (r !== row && boardString[r * 9 + col] === digit) {
|
||||
conflicts.push(r * 9 + col);
|
||||
}
|
||||
}
|
||||
|
||||
const boxRow = Math.floor(row / 3) * 3;
|
||||
const boxCol = Math.floor(col / 3) * 3;
|
||||
for (let r = boxRow; r < boxRow + 3; r++) {
|
||||
for (let c = boxCol; c < boxCol + 3; c++) {
|
||||
const idx = r * 9 + c;
|
||||
if ((r !== row || c !== col) && boardString[idx] === digit) {
|
||||
if (!conflicts.includes(idx)) {
|
||||
conflicts.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}, []);
|
||||
|
||||
const isFullyValid = useCallback((boardString: string): boolean => {
|
||||
for (let r = 0; r < 9; r++) {
|
||||
let rowSum = 0;
|
||||
for (let c = 0; c < 9; c++) {
|
||||
const digit = boardString[r * 9 + c];
|
||||
if (digit === '.') return false;
|
||||
rowSum += parseInt(digit, 10);
|
||||
}
|
||||
if (rowSum !== 45) return false;
|
||||
}
|
||||
|
||||
for (let c = 0; c < 9; c++) {
|
||||
let colSum = 0;
|
||||
for (let r = 0; r < 9; r++) {
|
||||
const digit = boardString[r * 9 + c];
|
||||
colSum += parseInt(digit, 10);
|
||||
}
|
||||
if (colSum !== 45) return false;
|
||||
}
|
||||
|
||||
for (let boxRow = 0; boxRow < 3; boxRow++) {
|
||||
for (let boxCol = 0; boxCol < 3; boxCol++) {
|
||||
let boxSum = 0;
|
||||
for (let r = boxRow * 3; r < boxRow * 3 + 3; r++) {
|
||||
for (let c = boxCol * 3; c < boxCol * 3 + 3; c++) {
|
||||
const digit = boardString[r * 9 + c];
|
||||
boxSum += parseInt(digit, 10);
|
||||
}
|
||||
}
|
||||
if (boxSum !== 45) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const isComplete = useCallback((boardString: string): boolean => {
|
||||
return !boardString.includes('.');
|
||||
}, []);
|
||||
|
||||
const boardToString = useCallback((board: SudokuBoard): string => {
|
||||
return board.map(row =>
|
||||
row.map(cell => cell.value || '.').join('')
|
||||
).join('');
|
||||
}, []);
|
||||
|
||||
const setCell = useCallback((row: number, col: number, value: string): boolean => {
|
||||
if (!canEditCell(row, col)) return false;
|
||||
|
||||
const board = boardRef.current;
|
||||
const boardString = boardToString(board);
|
||||
const newBoardString = boardString.substring(0, row * 9 + col) + value + boardString.substring(row * 9 + col + 1);
|
||||
|
||||
if (!isValidPlacement(newBoardString, row, col, value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
board[row][col] = {
|
||||
value: value === '.' ? '' : value,
|
||||
isGiven: board[row][col].isGiven,
|
||||
hasConflict: false
|
||||
};
|
||||
|
||||
return true;
|
||||
}, [canEditCell, isValidPlacement, boardToString]);
|
||||
|
||||
const generateBoard = useCallback((difficulty: Difficulty): void => {
|
||||
const newBoardString = SudokuGenerator.generate(difficulty);
|
||||
const solution = SudokuGenerator.solve(newBoardString);
|
||||
|
||||
initialBoardRef.current = newBoardString;
|
||||
solutionRef.current = solution;
|
||||
boardRef.current = initializeBoard(newBoardString);
|
||||
}, [initializeBoard]);
|
||||
|
||||
const solveBoard = useCallback((): boolean => {
|
||||
const solution = solutionRef.current;
|
||||
if (!solution) return false;
|
||||
|
||||
boardRef.current = initializeBoard(solution);
|
||||
return true;
|
||||
}, [initializeBoard]);
|
||||
|
||||
const resetBoard = useCallback((): void => {
|
||||
const initialBoard = initialBoardRef.current;
|
||||
if (initialBoard) {
|
||||
boardRef.current = initializeBoard(initialBoard);
|
||||
}
|
||||
}, [initializeBoard]);
|
||||
|
||||
const renderBoard = useCallback((): void => {
|
||||
const container = containerRef.current;
|
||||
const board = boardRef.current;
|
||||
|
||||
if (!container || board.length === 0) {
|
||||
if (container) {
|
||||
SudokuRenderer.renderEmptyBoard(container);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SudokuRenderer.renderBoard(container, board);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
boardRef,
|
||||
initialBoardRef,
|
||||
solutionRef,
|
||||
renderBoard,
|
||||
generateBoard,
|
||||
getCell,
|
||||
setCell,
|
||||
canEditCell,
|
||||
isValidPlacement: (row, col, digit) => {
|
||||
const boardString = boardToString(boardRef.current);
|
||||
return isValidPlacement(boardString, row, col, digit);
|
||||
},
|
||||
getConflicts: (row, col) => {
|
||||
const boardString = boardToString(boardRef.current);
|
||||
return getConflicts(boardString, row, col);
|
||||
},
|
||||
isComplete: () => {
|
||||
const boardString = boardToString(boardRef.current);
|
||||
return isComplete(boardString);
|
||||
},
|
||||
isFullyValid: () => {
|
||||
const boardString = boardToString(boardRef.current);
|
||||
return isFullyValid(boardString);
|
||||
},
|
||||
solveBoard,
|
||||
resetBoard
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user