- 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).
240 lines
7.0 KiB
TypeScript
240 lines
7.0 KiB
TypeScript
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
|
|
};
|
|
} |