- Add removedCells to GameOptions for custom puzzle sizes - removedCells overrides difficulty default when provided - Maintain backward compatibility with all defaults - Complete IMPLEMENTATION.md with Iteration 16 and summary - Completely rewrite README.md with comprehensive documentation: - Complete API reference (GameOptions, GameManager, CursorManager, ScoreManager) - Game states and state machine flow - Full VIM controls reference (movement, editing, hints, game control) - Cell state rules and visual feedback - Detailed scoring formula with examples - Game-over conditions (time and hint limits) - Hint system priority and penalties - Configuration examples for different play styles - Platform hook integration guide - Mark implementation as COMPLETE and PRODUCTION READY
131 lines
3.6 KiB
TypeScript
131 lines
3.6 KiB
TypeScript
export type Difficulty = 'easy' | 'medium' | 'hard';
|
||
|
||
const CELLS_TO_REMOVE = {
|
||
easy: 25,
|
||
medium: 40,
|
||
hard: 50,
|
||
} as const;
|
||
|
||
export class SudokuGenerator {
|
||
private solution: string = '';
|
||
private puzzle: string = '';
|
||
|
||
constructor() {
|
||
this.generate('medium');
|
||
}
|
||
|
||
generate(difficulty: Difficulty = 'medium', customRemovedCells?: number): void {
|
||
this.solution = this.generateSolvedBoard();
|
||
|
||
if (!this.validateFullBoard(this.solution)) {
|
||
throw new Error('Generated invalid solved board');
|
||
}
|
||
|
||
this.puzzle = this.makePuzzle(this.solution, difficulty, customRemovedCells);
|
||
}
|
||
|
||
getSolution(): string {
|
||
return this.solution;
|
||
}
|
||
|
||
getPuzzle(): string {
|
||
return this.puzzle;
|
||
}
|
||
|
||
private generateSolvedBoard(): string {
|
||
const size = 9;
|
||
const box = 3;
|
||
|
||
const pattern = (r: number, c: number) =>
|
||
(box * (r % box) + Math.floor(r / box) + c) % size;
|
||
|
||
const baseBoard: number[][] = Array.from({ length: size }, (_, r) =>
|
||
Array.from({ length: size }, (_, c) => pattern(r, c) + 1)
|
||
);
|
||
|
||
const shuffle = <T>(arr: T[]): T[] => {
|
||
const copy = [...arr];
|
||
for (let i = copy.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[copy[i], copy[j]] = [copy[j], copy[i]];
|
||
}
|
||
return copy;
|
||
};
|
||
|
||
const rows = shuffle([0, 1, 2])
|
||
.concat(shuffle([3, 4, 5]))
|
||
.concat(shuffle([6, 7, 8]));
|
||
const cols = shuffle([0, 1, 2])
|
||
.concat(shuffle([3, 4, 5]))
|
||
.concat(shuffle([6, 7, 8]));
|
||
|
||
const rowBands = shuffle([0, 1, 2]);
|
||
const colStacks = shuffle([0, 1, 2]);
|
||
|
||
const shuffledRows = rowBands.flatMap(band => rows.slice(band * 3, band * 3 + 3));
|
||
const shuffledCols = colStacks.flatMap(stack => cols.slice(stack * 3, stack * 3 + 3));
|
||
|
||
const digits = shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||
|
||
const board: number[][] = shuffledRows.map(r =>
|
||
shuffledCols.map(c => digits[baseBoard[r][c] - 1])
|
||
);
|
||
|
||
return board.flat().join('');
|
||
}
|
||
|
||
private makePuzzle(solution: string, difficulty: Difficulty, customRemovedCells?: number): string {
|
||
if (solution.length !== 81 || !/^[1-9]+$/.test(solution)) {
|
||
throw new Error('Invalid solved board: must be 81 digits 1–9');
|
||
}
|
||
|
||
const removeCount = customRemovedCells ?? CELLS_TO_REMOVE[difficulty];
|
||
const board = solution.split('');
|
||
|
||
const indices = Array.from({ length: 81 }, (_, i) => i);
|
||
for (let i = indices.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[indices[i], indices[j]] = [indices[j], indices[i]];
|
||
}
|
||
|
||
for (let i = 0; i < removeCount; i++) {
|
||
const idx = indices[i];
|
||
board[idx] = '.';
|
||
}
|
||
|
||
return board.join('');
|
||
}
|
||
|
||
private validateFullBoard(board: string): boolean {
|
||
if (board.length !== 81) return false;
|
||
if (!/^[1-9]+$/.test(board)) return false;
|
||
|
||
const getCell = (r: number, c: number) => Number(board[r * 9 + c]);
|
||
|
||
for (let r = 0; r < 9; r++) {
|
||
const sum = Array.from({ length: 9 }, (_, c) => getCell(r, c))
|
||
.reduce((a, b) => a + b, 0);
|
||
if (sum !== 45) return false;
|
||
}
|
||
|
||
for (let c = 0; c < 9; c++) {
|
||
const sum = Array.from({ length: 9 }, (_, r) => getCell(r, c))
|
||
.reduce((a, b) => a + b, 0);
|
||
if (sum !== 45) return false;
|
||
}
|
||
|
||
for (let br = 0; br < 3; br++) {
|
||
for (let bc = 0; bc < 3; bc++) {
|
||
const sum = Array.from({ length: 9 }, (_, i) => {
|
||
const r = br * 3 + Math.floor(i / 3);
|
||
const c = bc * 3 + (i % 3);
|
||
return getCell(r, c);
|
||
}).reduce((a, b) => a + b, 0);
|
||
if (sum !== 45) return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|