feat: implement Phase 21 - replace with pattern-based generation
Replace Robatron's complex algorithm with simpler, always-valid approach:
Core Changes:
- src/useBoard/SudokuGenerator.ts: Complete rewrite
- Pattern-based generation (base pattern + structural shuffles)
- Generate complete solution first, then remove cells
- Validates all boards before returning (defensive)
- Class-based API: generate(), getPuzzle(), getSolution()
- Configurable difficulty: CELLS_TO_REMOVE = {easy: 40, medium: 50, hard: 60}
- ~130 lines vs 372 lines (much simpler)
- src/useBoard/SudokuRenderer.ts: NEW - Clean separation
- render(board, initialBoard): Displays current state
- renderEmpty(): Shows empty 9x9 grid
- Distinguishes 'given' vs 'user-entered' cells with CSS classes
- src/useBoard/useBoard.ts: Updated for class architecture
- Refs: generatorRef, rendererRef, boardRef, solutionRef
- Uses class instances for generation and rendering
- Clean separation of concerns
- src/useBoard/SudokuGenerator.test.ts: Unit tests
- Tests valid board generation (100x)
- Tests correct cell removal counts per difficulty
- Tests puzzle/solution consistency
Algorithm Benefits:
1. Always valid - starts with mathematically valid board
2. Always solvable - we keep the solution reference
3. Fast - no backtracking, just pattern + shuffle
4. Simple - easy to understand and maintain
5. Predictable - difficulty = cell count, not complexity
Pattern-Based Approach:
- Base pattern ensures valid structure
- Shuffle rows/cols within bands/stacks (preserves 3x3 boxes)
- Shuffle band/stack order
- Shuffle digit mapping (1-9)
- Result: Valid, random, unique board every time
Phase 21 complete. Next: Phase 30 (useGameStatus hook).
This commit is contained in:
+31
-33
@@ -1,56 +1,54 @@
|
||||
import { useRef } from 'react';
|
||||
import { sudoku } from './SudokuGenerator';
|
||||
import { SudokuGenerator } from './SudokuGenerator';
|
||||
import { SudokuRenderer } from './SudokuRenderer';
|
||||
import type { Difficulty } from './SudokuGenerator';
|
||||
|
||||
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>('');
|
||||
|
||||
function generateBoard(difficulty: string = 'easy') {
|
||||
const newBoard = sudoku.generate(difficulty);
|
||||
boardRef.current = newBoard;
|
||||
initialBoardRef.current = newBoard;
|
||||
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;
|
||||
|
||||
const board = boardRef.current;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
const sudokuDiv = document.createElement('div');
|
||||
sudokuDiv.id = 'sudoku';
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'sudoku-grid';
|
||||
|
||||
for (let i = 0; i < 81; i++) {
|
||||
const r = Math.floor(i / 9);
|
||||
const c = i % 9;
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'sudoku-cell';
|
||||
cell.dataset.r = String(r);
|
||||
cell.dataset.c = String(c);
|
||||
|
||||
const value = board[i];
|
||||
if (value && value !== '.') {
|
||||
cell.textContent = value;
|
||||
} else {
|
||||
cell.textContent = '';
|
||||
}
|
||||
|
||||
grid.appendChild(cell);
|
||||
if (!rendererRef.current) {
|
||||
rendererRef.current = new SudokuRenderer(container);
|
||||
}
|
||||
|
||||
sudokuDiv.appendChild(grid);
|
||||
container.appendChild(sudokuDiv);
|
||||
const board = boardRef.current;
|
||||
const initialBoard = initialBoardRef.current;
|
||||
|
||||
if (board) {
|
||||
rendererRef.current.render(board, initialBoard);
|
||||
} else {
|
||||
rendererRef.current.renderEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
generatorRef,
|
||||
rendererRef,
|
||||
boardRef,
|
||||
solutionRef,
|
||||
initialBoardRef,
|
||||
generateBoard,
|
||||
renderBoard,
|
||||
|
||||
Reference in New Issue
Block a user