Files
vim-sudoku/src/useBoard/SudokuGenerator.test.ts
T
pastilhas 07f07376be 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).
2025-10-22 18:39:55 +01:00

63 lines
2.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, test, expect } from "bun:test";
import { SudokuGenerator } from "./SudokuGenerator";
describe("SudokuGenerator", () => {
test("generates an 81-character string of only digits 1–9 for solution", () => {
const generator = new SudokuGenerator();
const solution = generator.getSolution();
expect(solution).toHaveLength(81);
expect(/^[1-9]+$/.test(solution)).toBe(true);
});
test("generates 10 valid boards", () => {
for (let i = 0; i < 10; i++) {
const generator = new SudokuGenerator();
const solution = generator.getSolution();
expect(solution).toHaveLength(81);
expect(/^[1-9]+$/.test(solution)).toBe(true);
}
});
test("generates 100 valid boards", () => {
for (let i = 0; i < 100; i++) {
const generator = new SudokuGenerator();
const solution = generator.getSolution();
expect(solution).toHaveLength(81);
expect(/^[1-9]+$/.test(solution)).toBe(true);
}
});
test("generates puzzle with correct number of empty cells", () => {
const easyGen = new SudokuGenerator();
easyGen.generate('easy');
const easyPuzzle = easyGen.getPuzzle();
const easyEmpty = (easyPuzzle.match(/\./g) || []).length;
expect(easyEmpty).toBe(40);
const mediumGen = new SudokuGenerator();
mediumGen.generate('medium');
const mediumPuzzle = mediumGen.getPuzzle();
const mediumEmpty = (mediumPuzzle.match(/\./g) || []).length;
expect(mediumEmpty).toBe(50);
const hardGen = new SudokuGenerator();
hardGen.generate('hard');
const hardPuzzle = hardGen.getPuzzle();
const hardEmpty = (hardPuzzle.match(/\./g) || []).length;
expect(hardEmpty).toBe(60);
});
test("puzzle and solution have same givens in same positions", () => {
const generator = new SudokuGenerator();
generator.generate('medium');
const puzzle = generator.getPuzzle();
const solution = generator.getSolution();
for (let i = 0; i < 81; i++) {
if (puzzle[i] !== '.') {
expect(puzzle[i]).toBe(solution[i]);
}
}
});
});