diff --git a/src/index.ts b/src/index.ts index 07c1a02..111b4fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,67 +7,81 @@ export function analyze(Board: Board): AnalyzeData { const { analyzeBoard } = createSudokuInstance({ initBoard: Board, }); - const analysis = analyzeBoard(); - const isUnique = hasUniqueSolution(Board); - analysis.isUnique = isUnique; - return analysis; + return analyzeBoard(); } export function generate(difficulty: Difficulty): Board { const { getBoard } = createSudokuInstance({ difficulty }); - const board = getBoard(); - const analysis = analyze(board); - if (analysis.isValid && analysis.isUnique) { - return board; - } else { - return generate(difficulty); - } + return getBoard(); } -export function solve(Board: Board): - | { - board: Board; - steps: SolvingStep[]; - } - | undefined { - if (analyze(Board).isValid) { - const solvingSteps: SolvingStep[] = []; - const { solveAll } = createSudokuInstance({ - initBoard: Board, - onUpdate: (solvingStep) => solvingSteps.push(solvingStep), - }); - const board = solveAll(); - return { board, steps: solvingSteps }; - } -} - -export function hint(Board: Board): SolvingStep[] | undefined { +export function solve(Board: Board): { + solved: boolean; + board?: Board; + steps?: SolvingStep[]; + analysis?: AnalyzeData; + error?: string; +} { const solvingSteps: SolvingStep[] = []; - const { solveStep } = createSudokuInstance({ + + const { solveAll, analyzeBoard } = createSudokuInstance({ initBoard: Board, onUpdate: (solvingStep) => solvingSteps.push(solvingStep), }); - const board = solveStep(); - if (board) { - return solvingSteps; + + const analysis = analyzeBoard(); + + if (!analysis.hasSolution) { + return { solved: false, error: "No solution for provided board!" }; + } + + const board = solveAll(); + + if (!analysis.hasUniqueSolution) { + return { + solved: true, + board, + steps: solvingSteps, + analysis, + error: "No unique solution for provided board!", + }; } + + return { solved: true, board, steps: solvingSteps, analysis }; } -export function hasUniqueSolution(Board: Board): boolean { - const { solveAll } = createSudokuInstance({ +export function hint(Board: Board): { + solved: boolean; + board?: Board; + steps?: SolvingStep[]; + analysis?: AnalyzeData; + error?: string; +} { + const solvingSteps: SolvingStep[] = []; + const { solveStep, analyzeBoard } = createSudokuInstance({ initBoard: Board, + onUpdate: (solvingStep) => solvingSteps.push(solvingStep), }); - const solvedBoard = solveAll(); - if (!solvedBoard) { - return false; + const analysis = analyzeBoard(); + + if (!analysis.hasSolution) { + return { solved: false, error: "No solution for provided board!" }; } - const { solveStep, getBoard } = createSudokuInstance({ - initBoard: Board, - }); - while (getBoard().some((item) => !Boolean(item))) { - if (!solveStep()) { - return false; - } + const board = solveStep(); + + if (!board) { + return { solved: false, error: "No solution for provided board!" }; } - return solvedBoard.every((item, index) => getBoard()[index] === item); + + if (!analysis.hasUniqueSolution) { + return { + solved: true, + board, + steps: solvingSteps, + analysis, + error: "No unique solution for provided board!", + }; + } + + return { solved: true, board, steps: solvingSteps, analysis }; } diff --git a/src/sudoku.ts b/src/sudoku.ts index bac692d..34e0ea1 100644 --- a/src/sudoku.ts +++ b/src/sudoku.ts @@ -795,13 +795,13 @@ export function createSudokuInstance(options: Options = {}) { // Function to apply the solving strategies in order const applySolvingStrategies = ({ strategyIndex = 0, - gradingMode = false, + analyzeMode = false, }: { strategyIndex?: number; - gradingMode?: boolean; + analyzeMode?: boolean; } = {}): false | "elimination" | "value" => { if (isBoardFinished(board)) { - if (!gradingMode) { + if (!analyzeMode) { onFinish?.(calculateBoardDifficulty(usedStrategies, strategies)); } return false; @@ -810,11 +810,12 @@ export function createSudokuInstance(options: Options = {}) { strategies[strategyIndex].fn(); strategies[strategyIndex].postFn?.(); + if (effectedCells === false) { if (strategies.length > strategyIndex + 1) { return applySolvingStrategies({ strategyIndex: strategyIndex + 1, - gradingMode, + analyzeMode, }); } else { onError?.({ message: "No More Strategies To Solve The Board" }); @@ -825,7 +826,7 @@ export function createSudokuInstance(options: Options = {}) { return false; } - if (!gradingMode) { + if (!analyzeMode) { onUpdate?.({ strategy: strategies[strategyIndex].title, updates: effectedCells as Update[], @@ -881,14 +882,14 @@ export function createSudokuInstance(options: Options = {}) { function isValidAndEasyEnough(analysis: AnalyzeData, difficulty: Difficulty) { return ( - analysis.isValid && + analysis.hasSolution && analysis.difficulty && - analysis.isUnique && + analysis.hasUniqueSolution && isEasyEnough(difficulty, analysis.difficulty) ); } // Function to prepare the game board - const prepareGameBoard = (boardAnswer: Board) => { + const prepareGameBoard = () => { const cells = Array.from({ length: BOARD_SIZE * BOARD_SIZE }, (_, i) => i); let removalCount = getRemovalCountBasedOnDifficulty(difficulty); while (removalCount > 0 && cells.length > 0) { @@ -899,7 +900,8 @@ export function createSudokuInstance(options: Options = {}) { addValueToCellIndex(board, cellIndex, null); // Reset candidates, only in model. resetCandidates(); - const boardAnalysis = analyzeBoard({ boardAnswer }); + const boardAnalysis = analyzeBoard(); + if (isValidAndEasyEnough(boardAnalysis, difficulty)) { removalCount--; } else { @@ -924,38 +926,50 @@ export function createSudokuInstance(options: Options = {}) { ) .filter(Boolean); } - function analyzeBoard({ boardAnswer }: { boardAnswer?: Board } = {}) { - const usedStrategiesClone = [...usedStrategies]; - const boardClone = JSON.parse(JSON.stringify(board)); - function restoreOriginalState() { - usedStrategies = usedStrategiesClone; - board = boardClone; - } + function analyzeBoard() { + let usedStrategiesClone = usedStrategies.slice(); + let boardClone = JSON.parse(JSON.stringify(board)); + let Continue: boolean | "value" | "elimination" = true; while (Continue) { Continue = applySolvingStrategies({ strategyIndex: Continue === "elimination" ? 1 : 0, - gradingMode: true, + analyzeMode: true, }); } - const data: AnalyzeData = { - isValid: isBoardFinished(board), + hasSolution: isBoardFinished(board), + hasUniqueSolution: false, usedStrategies: filterAndMapStrategies(strategies, usedStrategies), }; - if (data.isValid) { + if (data.hasSolution) { const boardDiff = calculateBoardDifficulty(usedStrategies, strategies); data.difficulty = boardDiff.difficulty; data.score = boardDiff.score; - data.isUnique = boardAnswer - ? board.every((cell, index) => cell.value === boardAnswer[index]) - : false; } + const boardFinishedWithSolveAll = getBoard(); + usedStrategies = usedStrategiesClone.slice(); + board = boardClone; - restoreOriginalState(); + usedStrategiesClone = usedStrategies.slice(); + boardClone = JSON.parse(JSON.stringify(board)); + let solvedBoard: false | Board = [...getBoard()]; + while (solvedBoard && !solvedBoard.every(Boolean)) { + 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; } @@ -963,15 +977,12 @@ export function createSudokuInstance(options: Options = {}) { function generateBoard(): Board { generateBoardAnswerRecursively(0); - const boardAnswer = JSON.parse( - JSON.stringify(board.map((cell) => cell.value)), - ); const slicedBoard = JSON.parse(JSON.stringify(board)); function isBoardTooEasy() { - prepareGameBoard(boardAnswer); + prepareGameBoard(); const data = analyzeBoard(); - if (data.isValid && data.difficulty) { + if (data.hasSolution && data.difficulty) { return !isHardEnough(difficulty, data.difficulty); } return true; @@ -990,21 +1001,26 @@ export function createSudokuInstance(options: Options = {}) { } const MAX_ITERATIONS = 30; // Set your desired maximum number of iterations - const solveStep = (iterationCount: number = 0): Board | false => { + const solveStep = ({ + analyzeMode = false, + iterationCount = 0, + }: { + analyzeMode?: boolean; + iterationCount?: number; + } = {}): Board | false => { if (iterationCount >= MAX_ITERATIONS) { return false; } const initialBoard = getBoard().slice(); - applySolvingStrategies(); + applySolvingStrategies({ analyzeMode }); const stepSolvedBoard = getBoard().slice(); const boardNotChanged = initialBoard.filter(Boolean).length === stepSolvedBoard.filter(Boolean).length; - if (!isBoardFinished(board) && boardNotChanged) { - return solveStep(iterationCount + 1); + return solveStep({ analyzeMode, iterationCount: iterationCount + 1 }); } board = convertInitialBoardToSerializedBoard(stepSolvedBoard); updateCandidatesBasedOnCellsValue(); diff --git a/src/types.ts b/src/types.ts index 4750b15..2732a01 100644 --- a/src/types.ts +++ b/src/types.ts @@ -59,8 +59,8 @@ export type House = Array; export type Houses = Array; export type AnalyzeData = { - isValid?: boolean; - isUnique?: boolean; + hasSolution: boolean; + hasUniqueSolution: boolean; usedStrategies?: ({ title: string; freq: number; diff --git a/tests/sudoku.test.ts b/tests/sudoku.test.ts index 288f4e8..9fd890b 100644 --- a/tests/sudoku.test.ts +++ b/tests/sudoku.test.ts @@ -5,14 +5,8 @@ import { DIFFICULTY_MASTER, DIFFICULTY_MEDIUM, } from "../src/constants"; -import { - generate, - analyze, - solve, - hasUniqueSolution, - Difficulty, - Board, -} from "../src/index"; // Import the createSudokuInstance module (update path as needed) +import { generate, analyze, solve, Difficulty, Board } from "../src/index"; // Import the createSudokuInstance module (update path as needed) +import { createSudokuInstance } from "../src/sudoku"; import { EASY_SUDOKU_BOARD_FOR_TEST, EXPERT_SUDOKU_BOARD_FOR_TEST, @@ -21,6 +15,24 @@ 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", () => { @@ -118,11 +130,11 @@ describe("sudoku-core", () => { const sudokuBoard = [1]; //Act - const { difficulty, isValid } = analyze(sudokuBoard); + const { difficulty, hasSolution } = analyze(sudokuBoard); // Assert expect(difficulty).toBe(undefined); - expect(isValid).toBe(false); + expect(hasSolution).toBe(false); }); it("should validate the easy board", () => { //Arrange