diff --git a/src/index.ts b/src/index.ts index c5e3884..b812185 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { createSudokuInstance } from "./sudoku"; +import { isUniqueSolution } from "./sudoku-solver"; import { type AnalyzeData, type Board, @@ -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!" }; @@ -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!" }; diff --git a/src/sudoku-solver.ts b/src/sudoku-solver.ts new file mode 100644 index 0000000..1f24738 --- /dev/null +++ b/src/sudoku-solver.ts @@ -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; +} diff --git a/src/sudoku.ts b/src/sudoku.ts index 34e0ea1..bbb433c 100644 --- a/src/sudoku.ts +++ b/src/sudoku.ts @@ -5,6 +5,7 @@ import { CANDIDATES, NULL_CANDIDATE_LIST, } from "./constants"; +import { isUniqueSolution } from "./sudoku-solver"; // Importing necessary types import { @@ -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 @@ -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. @@ -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 = ({ @@ -884,7 +885,6 @@ export function createSudokuInstance(options: Options = {}) { return ( analysis.hasSolution && analysis.difficulty && - analysis.hasUniqueSolution && isEasyEnough(difficulty, analysis.difficulty) ); } @@ -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 @@ -940,7 +943,6 @@ export function createSudokuInstance(options: Options = {}) { } const data: AnalyzeData = { hasSolution: isBoardFinished(board), - hasUniqueSolution: false, usedStrategies: filterAndMapStrategies(strategies, usedStrategies), }; @@ -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; @@ -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; diff --git a/src/types.ts b/src/types.ts index 7293035..55abc96 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,7 +68,7 @@ export type Houses = Array; export type AnalyzeData = { hasSolution: boolean; - hasUniqueSolution: boolean; + hasUniqueSolution?: boolean; usedStrategies?: ({ title: string; freq: number; diff --git a/src/utils.ts b/src/utils.ts index 5adb94b..6873147 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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, diff --git a/tests/__snapshots__/sudoku.test.ts.snap b/tests/__snapshots__/sudoku.test.ts.snap index f544246..5a5dd25 100644 --- a/tests/__snapshots__/sudoku.test.ts.snap +++ b/tests/__snapshots__/sudoku.test.ts.snap @@ -103,75 +103,74 @@ exports[`sudoku-core hint method should solve the easy board 2`] = ` exports[`sudoku-core hint method should solve the expert board 1`] = ` [ - null, + 4, 7, null, null, - 9, null, null, null, - 3, null, - 5, + 1, + 9, null, + 1, + 7, null, null, null, - 4, null, null, null, null, + 6, null, - 3, - 8, null, null, + 9, null, null, null, null, null, - 9, null, null, - 1, - 3, null, + 5, + null, + 6, null, null, - 8, null, + 5, + 9, null, + 2, null, null, - 5, null, - 6, null, - 7, + 3, + 4, + null, null, - 1, - 5, - 2, null, - 8, null, - 6, - 9, null, + 6, null, + 9, 1, null, + 3, null, - 5, - 4, null, + 4, 2, null, null, null, + 6, null, null, null, @@ -179,9 +178,10 @@ exports[`sudoku-core hint method should solve the expert board 1`] = ` null, null, null, + 8, + 7, 2, null, - 9, null, null, ] @@ -194,8 +194,8 @@ exports[`sudoku-core hint method should solve the expert board 2`] = ` "type": "value", "updates": [ { - "filledValue": 2, - "index": 51, + "filledValue": 1, + "index": 57, }, ], }, @@ -204,87 +204,87 @@ exports[`sudoku-core hint method should solve the expert board 2`] = ` exports[`sudoku-core hint method should solve the hard board 1`] = ` [ - null, - null, - 6, + 2, null, 8, - 4, null, + 3, null, - 9, + 6, null, null, null, - 7, null, + 1, null, null, + 6, null, null, + 7, null, 5, - null, + 4, + 8, null, 1, null, null, - 8, null, + 5, null, null, null, null, null, null, + 3, + 1, + 9, null, null, - 6, null, + 5, + 4, null, null, null, - 2, - 9, - 1, - 3, - 5, + 7, null, - 6, null, null, null, null, null, + 9, null, 8, null, null, null, null, - 3, + 9, null, - 2, null, null, null, - 8, null, null, + 2, null, null, null, - 1, null, - 9, null, null, - null, - 5, + 6, null, null, + 4, null, null, + 5, + 2, ] `; @@ -295,8 +295,8 @@ exports[`sudoku-core hint method should solve the hard board 2`] = ` "type": "value", "updates": [ { - "filledValue": 8, - "index": 4, + "filledValue": 2, + "index": 0, }, ], }, @@ -305,87 +305,87 @@ exports[`sudoku-core hint method should solve the hard board 2`] = ` exports[`sudoku-core hint method should solve the master board 1`] = ` [ - null, - 7, - 6, null, null, - 1, null, null, - 3, null, null, + 5, null, null, null, null, null, + 5, + 7, + 1, null, null, + 8, null, null, + 2, null, - 7, - 6, + 8, null, null, + 4, + 1, null, + 6, + 5, + 3, null, null, 1, null, null, + 1, null, null, - 7, - null, null, null, null, null, - 3, 8, + 4, null, null, + 4, null, null, null, - 8, - 4, + 6, null, + 9, null, + 1, null, - 9, null, - 6, null, + 3, null, null, - 6, null, + 7, null, - 8, - 5, - 2, - 4, null, - 2, null, - 3, null, + 2, null, null, null, - 8, null, null, null, null, - 2, + 5, + null, + 4, null, null, - 9, ] `; @@ -396,8 +396,8 @@ exports[`sudoku-core hint method should solve the master board 2`] = ` "type": "value", "updates": [ { - "filledValue": 2, - "index": 62, + "filledValue": 1, + "index": 26, }, ], }, diff --git a/tests/constants.ts b/tests/constants.ts index 092f7c5..44f1cf2 100644 --- a/tests/constants.ts +++ b/tests/constants.ts @@ -169,157 +169,156 @@ export const MEDIUM_SUDOKU_BOARD_FOR_TEST = [ export const HARD_SUDOKU_BOARD_FOR_TEST = [ null, null, - 6, - null, - null, - 4, + 8, null, + 3, null, - 9, + 6, null, null, null, - 7, null, + 1, null, null, + 6, null, null, + 7, null, 5, - null, + 4, + 8, null, 1, null, null, - 8, null, + 5, null, null, null, null, null, null, + 3, + 1, + 9, null, null, - 6, null, + 5, + 4, null, null, null, - 2, - 9, - 1, - 3, - 5, + 7, null, - 6, null, null, null, null, null, + 9, null, 8, null, null, null, null, - 3, - null, - 2, + 9, null, null, null, - 8, null, null, null, + 2, null, null, - 1, null, - 9, null, null, null, - 5, + 6, null, null, + 4, null, null, + 5, + 2, ]; export const EXPERT_SUDOKU_BOARD_FOR_TEST = [ - null, + 4, 7, null, null, - 9, null, null, null, - 3, null, - 5, + 1, + 9, null, + 1, + 7, null, null, null, - 4, null, null, null, null, + 6, null, - 3, - 8, null, null, + 9, null, null, null, null, null, - 9, null, null, - 1, - 3, null, + 5, + null, + 6, null, null, - 8, null, + 5, + 9, null, + 2, null, null, - 5, null, - 6, null, - 7, + 3, + 4, + null, null, - 1, - 5, null, null, - 8, null, 6, + null, 9, null, null, - 1, + 3, null, null, - 5, 4, - null, 2, null, null, null, + 6, null, null, null, @@ -327,93 +326,94 @@ export const EXPERT_SUDOKU_BOARD_FOR_TEST = [ null, null, null, + 8, + 7, 2, null, - 9, null, null, ]; export const MASTER_SUDOKU_BOARD_FOR_TEST = [ null, - 7, - 6, null, null, - 1, null, null, - 3, null, + 5, null, null, null, null, null, + 5, + 7, + 1, null, null, + 8, null, null, + 2, null, + 8, null, - 7, - 6, null, + 4, null, null, + 6, + 5, + 3, null, null, 1, null, null, + 1, null, null, - 7, - null, null, null, null, null, - 3, 8, + 4, null, null, + 4, null, null, null, - 8, - 4, + 6, + null, + 9, null, + 1, null, null, - 9, null, - 6, + 3, null, null, null, - 6, + 7, null, null, - 8, - 5, null, - 4, null, 2, null, - 3, - null, null, null, null, - 8, null, null, null, + 5, null, - 2, + 4, null, null, - 9, ]; diff --git a/tests/sudoku.test.ts b/tests/sudoku.test.ts index e1429a6..00c53eb 100644 --- a/tests/sudoku.test.ts +++ b/tests/sudoku.test.ts @@ -13,7 +13,7 @@ import { Board, hint, } from "../src/index"; // Import the createSudokuInstance module (update path as needed) -import { createSudokuInstance } from "../src/sudoku"; +import { isUniqueSolution } from "../src/sudoku-solver"; import { EASY_SUDOKU_BOARD_FOR_TEST, EXPERT_SUDOKU_BOARD_FOR_TEST, @@ -22,24 +22,6 @@ import { MEDIUM_SUDOKU_BOARD_FOR_TEST, } from "./constants"; -function hasUniqueSolution(Board: Board): boolean { - const { solveAll } = createSudokuInstance({ - initBoard: Board, - }); - const solvedBoard = solveAll(); - if (!solvedBoard) { - return false; - } - const { solveStep, getBoard } = createSudokuInstance({ - initBoard: Board, - }); - while (getBoard().some((item) => !Boolean(item))) { - if (!solveStep()) { - return false; - } - } - return solvedBoard.every((item, index) => getBoard()[index] === item); -} describe("sudoku-core", () => { describe("generate method", () => { it("should generate a valid easy difficulty board", () => { @@ -51,7 +33,7 @@ describe("sudoku-core", () => { // Assert expect(data.difficulty).toBe("easy"); - expect(hasUniqueSolution(sudokuBoard)).toBe(true); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); it("should generate a valid medium difficulty board", () => { //Arrange @@ -61,7 +43,7 @@ describe("sudoku-core", () => { const data = analyze(sudokuBoard); // Assert expect(data.difficulty).toBe("medium"); - expect(hasUniqueSolution(sudokuBoard)).toBe(true); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); it("should generate a valid hard difficulty board", () => { //Arrange @@ -72,11 +54,10 @@ describe("sudoku-core", () => { // Assert expect(data.difficulty).toBe("hard"); - expect(hasUniqueSolution(sudokuBoard)).toBe(true); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); it("should generate a valid expert difficulty board", () => { //Arrange - const sudokuBoard = generate("expert"); //Act @@ -84,7 +65,7 @@ describe("sudoku-core", () => { // Assert expect(data.difficulty).toBe("expert"); - expect(hasUniqueSolution(sudokuBoard)).toBe(true); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); it("should generate a valid master difficulty board", () => { //Arrange @@ -94,8 +75,9 @@ describe("sudoku-core", () => { const data = analyze(sudokuBoard); // Assert + console.log(sudokuBoard); expect(data.difficulty).toBe("master"); - expect(hasUniqueSolution(sudokuBoard)).toBe(true); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); }); @@ -167,11 +149,13 @@ describe("sudoku-core", () => { const sudokuBoard = [1]; //Act - const { difficulty, hasSolution } = analyze(sudokuBoard); + const { difficulty, hasSolution, hasUniqueSolution } = + analyze(sudokuBoard); // Assert expect(difficulty).toBe(undefined); expect(hasSolution).toBe(false); + expect(hasUniqueSolution).toBe(false); }); it("should validate the easy board", () => { //Arrange @@ -182,6 +166,7 @@ describe("sudoku-core", () => { // Assert expect(difficulty).toBe("easy"); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); it("should validate the medium board", () => { //Arrange @@ -192,6 +177,7 @@ describe("sudoku-core", () => { // Assert expect(difficulty).toBe("medium"); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); it("should validate the hard board", () => { //Arrange @@ -202,6 +188,7 @@ describe("sudoku-core", () => { // Assert expect(difficulty).toBe("hard"); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); it("should validate the expert board", () => { //Arrange @@ -212,6 +199,7 @@ describe("sudoku-core", () => { // Assert expect(difficulty).toBe("expert"); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); it("should validate the master board", () => { //Arrange @@ -222,6 +210,7 @@ describe("sudoku-core", () => { // Assert expect(difficulty).toBe("master"); + expect(isUniqueSolution(sudokuBoard)).toBe(true); }); }); });