Skip to content

Commit

Permalink
refactor code base
Browse files Browse the repository at this point in the history
  • Loading branch information
komeilmehranfar committed Dec 9, 2023
1 parent 0b6077a commit ba89a3f
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 91 deletions.
106 changes: 60 additions & 46 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
82 changes: 49 additions & 33 deletions src/sudoku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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" });
Expand All @@ -825,7 +826,7 @@ export function createSudokuInstance(options: Options = {}) {
return false;
}

if (!gradingMode) {
if (!analyzeMode) {
onUpdate?.({
strategy: strategies[strategyIndex].title,
updates: effectedCells as Update[],
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -924,54 +926,63 @@ 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;
}

// Function to generate the Sudoku board
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;
Expand All @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export type House = Array<number>;
export type Houses = Array<House>;

export type AnalyzeData = {
isValid?: boolean;
isUnique?: boolean;
hasSolution: boolean;
hasUniqueSolution: boolean;
usedStrategies?: ({
title: string;
freq: number;
Expand Down
32 changes: 22 additions & 10 deletions tests/sudoku.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ba89a3f

Please sign in to comment.