- Add getHint() method to BoardManager with two-tier priority: 1. Find incorrect user-entered cells → flash red 2. Find empty cells → show correct digit green temporarily - Add Shift+H key handler in useCursor (normal mode only) - Visual feedback: - Red flash (hint-flash-error): incorrect cells, 1s duration, red bg + border - Green flash (hint-flash-correct): correct digit shown, 1s duration, green bg + border - Distinct from invalid-move (no background, 500ms) - Score penalties based on difficulty: - Easy: 25 points per hint - Medium: 50 points per hint - Hard: 100 points per hint - Track hints used in scoreboard display - Correct digit appears temporarily (1s) then disappears (player still does the work) - Update example app to show hints counter and Shift+H instruction
109 lines
3.1 KiB
TypeScript
109 lines
3.1 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { SudokuGenerator } from './SudokuGenerator';
|
|
import { SudokuRenderer } from './SudokuRenderer';
|
|
import type { Difficulty } from './SudokuGenerator';
|
|
import type { HintResult } from './types';
|
|
|
|
export function useBoard() {
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
const generatorRef = useRef<SudokuGenerator | null>(null);
|
|
const rendererRef = useRef<SudokuRenderer | null>(null);
|
|
const boardRef = useRef<string>('');
|
|
const solutionRef = useRef<string>('');
|
|
const initialBoardRef = useRef<string>('');
|
|
const [boardRendered, setBoardRendered] = useState(0);
|
|
|
|
function generateBoard(difficulty: Difficulty = 'easy') {
|
|
if (!generatorRef.current) {
|
|
generatorRef.current = new SudokuGenerator();
|
|
}
|
|
|
|
generatorRef.current.generate(difficulty);
|
|
const puzzle = generatorRef.current.getPuzzle();
|
|
const solution = generatorRef.current.getSolution();
|
|
|
|
boardRef.current = puzzle;
|
|
solutionRef.current = solution;
|
|
initialBoardRef.current = puzzle;
|
|
}
|
|
|
|
function renderBoard() {
|
|
const container = containerRef.current;
|
|
if (!container) return;
|
|
|
|
if (!rendererRef.current) {
|
|
rendererRef.current = new SudokuRenderer(container);
|
|
}
|
|
|
|
const board = boardRef.current;
|
|
const initialBoard = initialBoardRef.current;
|
|
|
|
if (board) {
|
|
rendererRef.current.render(board, initialBoard);
|
|
} else {
|
|
rendererRef.current.renderEmpty();
|
|
}
|
|
|
|
setBoardRendered(prev => prev + 1);
|
|
}
|
|
|
|
function setCell(row: number, col: number, value: string): void {
|
|
const board = boardRef.current.split('');
|
|
const index = row * 9 + col;
|
|
board[index] = value;
|
|
boardRef.current = board.join('');
|
|
renderBoard();
|
|
}
|
|
|
|
function getHint(): HintResult {
|
|
const board = boardRef.current;
|
|
const solution = solutionRef.current;
|
|
const initialBoard = initialBoardRef.current;
|
|
|
|
if (!board || !solution) return null;
|
|
|
|
const incorrectCells: Array<{ row: number; col: number }> = [];
|
|
const emptyCells: Array<{ row: number; col: number }> = [];
|
|
|
|
for (let i = 0; i < 81; i++) {
|
|
const row = Math.floor(i / 9);
|
|
const col = i % 9;
|
|
const currentValue = board[i];
|
|
const correctValue = solution[i];
|
|
const initialValue = initialBoard[i];
|
|
|
|
if (currentValue !== '.' && initialValue === '.' && currentValue !== correctValue) {
|
|
incorrectCells.push({ row, col });
|
|
}
|
|
|
|
if (currentValue === '.') {
|
|
emptyCells.push({ row, col });
|
|
}
|
|
}
|
|
|
|
if (incorrectCells.length > 0) {
|
|
const randomCell = incorrectCells[Math.floor(Math.random() * incorrectCells.length)];
|
|
return { ...randomCell, type: 'error' };
|
|
}
|
|
|
|
if (emptyCells.length > 0) {
|
|
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
|
|
const index = randomCell.row * 9 + randomCell.col;
|
|
const correctValue = solution[index];
|
|
return { ...randomCell, type: 'correct', value: correctValue };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
containerRef,
|
|
boardRef,
|
|
boardRendered,
|
|
generateBoard,
|
|
renderBoard,
|
|
setCell,
|
|
getHint,
|
|
};
|
|
}
|