Skip to content

Commit

Permalink
fix: all board has unique solution now
Browse files Browse the repository at this point in the history
  • Loading branch information
komeilmehranfar committed Apr 15, 2024
1 parent 0b7692b commit d84f5c8
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 224 deletions.
20 changes: 12 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSudokuInstance } from "./sudoku";
import { isUniqueSolution } from "./sudoku-solver";
import {
type AnalyzeData,
type Board,
Expand All @@ -11,25 +12,28 @@ export { type AnalyzeData, type Board, type Difficulty, type SolvingStep };

export function analyze(Board: Board): AnalyzeData {
const { analyzeBoard } = createSudokuInstance({
initBoard: Board,
initBoard: Board.slice(),
});
return analyzeBoard();
return { ...analyzeBoard(), hasUniqueSolution: isUniqueSolution(Board) };
}

export function generate(difficulty: Difficulty): Board {
const { getBoard } = createSudokuInstance({ difficulty });
if (!analyze(getBoard()).hasUniqueSolution) {
return generate(difficulty);
}
return getBoard();
}

export function solve(Board: Board): SolvingResult {
const solvingSteps: SolvingStep[] = [];

const { solveAll, analyzeBoard } = createSudokuInstance({
initBoard: Board,
const { solveAll } = createSudokuInstance({
initBoard: Board.slice(),
onUpdate: (solvingStep) => solvingSteps.push(solvingStep),
});

const analysis = analyzeBoard();
const analysis = analyze(Board);

if (!analysis.hasSolution) {
return { solved: false, error: "No solution for provided board!" };
Expand All @@ -52,11 +56,11 @@ export function solve(Board: Board): SolvingResult {

export function hint(Board: Board): SolvingResult {
const solvingSteps: SolvingStep[] = [];
const { solveStep, analyzeBoard } = createSudokuInstance({
initBoard: Board,
const { solveStep } = createSudokuInstance({
initBoard: Board.slice(),
onUpdate: (solvingStep) => solvingSteps.push(solvingStep),
});
const analysis = analyzeBoard();
const analysis = analyze(Board);

if (!analysis.hasSolution) {
return { solved: false, error: "No solution for provided board!" };
Expand Down
54 changes: 54 additions & 0 deletions src/sudoku-solver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
type Board = (number | null)[];

function isValid(board: Board, index: number, num: number): boolean {
const row = Math.floor(index / 9);
const col = index % 9;
// Check if number already exists in row or column
for (let i = 0; i < 9; i++) {
if (board[row * 9 + i] === num || board[col + 9 * i] === num) {
return false;
}
}
// Check if number already exists in 3x3 box
const startRow = row - (row % 3);
const startCol = col - (col % 3);
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[(startRow + i) * 9 + startCol + j] === num) {
return false;
}
}
}
return true;
}

let solutionCount = 0;

function solveSudoku(board: Board): boolean {
for (let i = 0; i < 81; i++) {
if (!board[i]) {
for (let num = 1; num <= 9; num++) {
if (isValid(board, i, num)) {
board[i] = num;
solveSudoku(board);
if (solutionCount > 1) {
return false;
}
board[i] = null;
}
}
return false;
}
}
solutionCount++;
if (solutionCount > 1) {
return false;
}
return true;
}

export function isUniqueSolution(board: Board): boolean {
solutionCount = 0;
solveSudoku([...board]);
return solutionCount === 1;
}
90 changes: 42 additions & 48 deletions src/sudoku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CANDIDATES,
NULL_CANDIDATE_LIST,
} from "./constants";
import { isUniqueSolution } from "./sudoku-solver";

// Importing necessary types
import {
Expand Down Expand Up @@ -104,30 +105,30 @@ export function createSudokuInstance(options: Options = {}) {
score: 90,
type: "elimination",
},
{
title: "Naked Triplet Strategy",
fn: nakedTripletStrategy,
score: 100,
type: "elimination",
},
{
title: "Hidden Triplet Strategy",
fn: hiddenTripletStrategy,
score: 140,
type: "elimination",
},
{
title: "Naked Quadruple Strategy",
fn: nakedQuadrupleStrategy,
score: 150,
type: "elimination",
},
{
title: "Hidden Quadruple Strategy",
fn: hiddenQuadrupleStrategy,
score: 280,
type: "elimination",
},
// {
// title: "Naked Triplet Strategy",
// fn: nakedTripletStrategy,
// score: 100,
// type: "elimination",
// },
// {
// title: "Hidden Triplet Strategy",
// fn: hiddenTripletStrategy,
// score: 140,
// type: "elimination",
// },
// {
// title: "Naked Quadruple Strategy",
// fn: nakedQuadrupleStrategy,
// score: 150,
// type: "elimination",
// },
// {
// title: "Hidden Quadruple Strategy",
// fn: hiddenQuadrupleStrategy,
// score: 280,
// type: "elimination",
// },
];

// Function to initialize the Sudoku board
Expand Down Expand Up @@ -629,17 +630,17 @@ export function createSudokuInstance(options: Options = {}) {
* --------------
* These strategies look for a group of 2, 3, or 4 cells in the same house that between them have exactly 2, 3, or 4 candidates. Since those candidates have to go in some cell in that group, they can be eliminated as candidates from other cells in the house. For example, if in a column two cells can only contain the numbers 2 and 3, then in the rest of that column, 2 and 3 can be removed from the candidate lists.
* -----------------------------------------------------------------*/
function nakedTripletStrategy() {
return nakedCandidatesStrategy(3);
}
// function nakedTripletStrategy() {
// return nakedCandidatesStrategy(3);
// }

/* nakedQuadrupleStrategy
* --------------
* These strategies look for a group of 2, 3, or 4 cells in the same house that between them have exactly 2, 3, or 4 candidates. Since those candidates have to go in some cell in that group, they can be eliminated as candidates from other cells in the house. For example, if in a column two cells can only contain the numbers 2 and 3, then in the rest of that column, 2 and 3 can be removed from the candidate lists.
* -----------------------------------------------------------------*/
function nakedQuadrupleStrategy() {
return nakedCandidatesStrategy(4);
}
// function nakedQuadrupleStrategy() {
// return nakedCandidatesStrategy(4);
// }

/* hiddenLockedCandidates
* These strategies are similar to the naked ones, but instead of looking for cells that only contain the group of candidates, they look for candidates that only appear in the group of cells. For example, if in a box, the numbers 2 and 3 only appear in two cells, then even if those cells have other candidates, you know that one of them has to be 2 and the other has to be 3, so you can remove any other candidates from those cells.
Expand Down Expand Up @@ -780,17 +781,17 @@ export function createSudokuInstance(options: Options = {}) {
* --------------
* These strategies are similar to the naked ones, but instead of looking for cells that only contain the group of candidates, they look for candidates that only appear in the group of cells. For example, if in a box, the numbers 2 and 3 only appear in two cells, then even if those cells have other candidates, you know that one of them has to be 2 and the other has to be 3, so you can remove any other candidates from those cells.
* -----------------------------------------------------------------*/
function hiddenTripletStrategy() {
return hiddenLockedCandidates(3);
}
// function hiddenTripletStrategy() {
// return hiddenLockedCandidates(3);
// }

/* hiddenQuadrupleStrategy
* --------------
* These strategies are similar to the naked ones, but instead of looking for cells that only contain the group of candidates, they look for candidates that only appear in the group of cells. For example, if in a box, the numbers 2 and 3 only appear in two cells, then even if those cells have other candidates, you know that one of them has to be 2 and the other has to be 3, so you can remove any other candidates from those cells.
* -----------------------------------------------------------------*/
function hiddenQuadrupleStrategy() {
return hiddenLockedCandidates(4);
}
// function hiddenQuadrupleStrategy() {
// return hiddenLockedCandidates(4);
// }

// Function to apply the solving strategies in order
const applySolvingStrategies = ({
Expand Down Expand Up @@ -884,7 +885,6 @@ export function createSudokuInstance(options: Options = {}) {
return (
analysis.hasSolution &&
analysis.difficulty &&
analysis.hasUniqueSolution &&
isEasyEnough(difficulty, analysis.difficulty)
);
}
Expand All @@ -901,8 +901,11 @@ export function createSudokuInstance(options: Options = {}) {
// Reset candidates, only in model.
resetCandidates();
const boardAnalysis = analyzeBoard();

if (isValidAndEasyEnough(boardAnalysis, difficulty)) {
console.log({ removalCount, score: boardAnalysis.score });
if (
isValidAndEasyEnough(boardAnalysis, difficulty) &&
isUniqueSolution(getBoard())
) {
removalCount--;
} else {
// Reset - don't dig this cell
Expand Down Expand Up @@ -940,7 +943,6 @@ export function createSudokuInstance(options: Options = {}) {
}
const data: AnalyzeData = {
hasSolution: isBoardFinished(board),
hasUniqueSolution: false,
usedStrategies: filterAndMapStrategies(strategies, usedStrategies),
};

Expand All @@ -949,7 +951,6 @@ export function createSudokuInstance(options: Options = {}) {
data.difficulty = boardDiff.difficulty;
data.score = boardDiff.score;
}
const boardFinishedWithSolveAll = getBoard();
usedStrategies = usedStrategiesClone.slice();
board = boardClone;

Expand All @@ -961,13 +962,6 @@ export function createSudokuInstance(options: Options = {}) {
solvedBoard = solveStep({ analyzeMode: true, iterationCount: 0 });
}

if (data.hasSolution && typeof solvedBoard !== "boolean") {
data.hasUniqueSolution =
solvedBoard &&
solvedBoard.every(
(item, index) => item === boardFinishedWithSolveAll[index],
);
}
usedStrategies = usedStrategiesClone.slice();
board = boardClone;
return data;
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export type Houses = Array<House>;

export type AnalyzeData = {
hasSolution: boolean;
hasUniqueSolution: boolean;
hasUniqueSolution?: boolean;
usedStrategies?: ({
title: string;
freq: number;
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export const calculateBoardDifficulty = (
: DIFFICULTY_HARD;

if (totalScore > 750) difficulty = DIFFICULTY_EXPERT;
if (totalScore > 2000) difficulty = DIFFICULTY_MASTER;
if (totalScore > 1000) difficulty = DIFFICULTY_MASTER;

return {
difficulty,
Expand Down
Loading

0 comments on commit d84f5c8

Please sign in to comment.