diff --git a/app/src/main/java/sudoku/Solver.java b/app/src/main/java/sudoku/Solver.java index f1b9249..bdfc552 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -2,6 +2,7 @@ package sudoku; public class Solver implements SudokuSolver { private int[][] board = null; + private int tries = 0; public Solver() { board = new int[9][9]; @@ -47,6 +48,12 @@ public class Solver implements SudokuSolver { * @return true if solved */ private boolean solve(int row, int col) { + if (++tries >= 10000) { + if (tries == 10000) + System.out.println(String.format("Likely unsolvable. Tries: %d", tries)); + return false; + } + if (row < 0 || row > 9 || col < 0 || col > 9) { return false; } @@ -61,6 +68,7 @@ public class Solver implements SudokuSolver { // If we have a "number" in the current cell // recursively call solve() on the next cell + // until we find a zero (empty cell) if (board[row][col] != 0) { return solve(row + 1, col); } @@ -76,17 +84,28 @@ public class Solver implements SudokuSolver { } } + // Reset the current cell to zero and backtrack board[row][col] = 0; return false; } /** - * Randomizes the board. This guarantees a solvable board. + * {@inheritDoc} + *
+ * Default difficulty is 3 + */ + public void randomizeBoard() { + randomizeBoard(3); + } + + /** + * {@inheritDoc} */ @Override - public void randomizeBoard() { + public void randomizeBoard(int difficulty) { + int amount_prefilled = (difficulty * 9) + 1; this.clear(); - for (int i = 0; i < 9; ++i) { + for (int i = 0; i < amount_prefilled; ++i) { int row = (int) (Math.random() * 9); int col = (int) (Math.random() * 9); int val = (int) (Math.random() * 9) + 1; @@ -94,6 +113,12 @@ public class Solver implements SudokuSolver { board[row][col] = val; } } + + // Recursively call randomizeBoard() until we get a solvable board + // This is expensive, and there should be some voodoo magic that computes this in n^2 time + if (!isSolvable()) { + randomizeBoard(difficulty); + } } /** @@ -121,32 +146,25 @@ public class Solver implements SudokuSolver { * {@inheritDoc} */ @Override - public boolean isLegal(int row, int col, int val) { - if (row < 0 || row >= 9 || col < 0 || col >= 9 || val < 1 || val > 9) { + public boolean isLegal(int row, int col, int num) { + // Sanity check + if (row < 0 || row >= 9 || col < 0 || col >= 9 || num < 1 || num > 9) { return false; } - // Check if val is already in col - for (int i = 0; i < 9; ++i) { - if (val == board[i][col]) { - return false; - } - } - - // Check if val is already in row - for (int j = 0; j < 9; ++j) { - if (val == board[row][j]) { - return false; + // Check both the row and column + for (int i = 0; i < 9; i++) { + if (board[row][i] == num || board[i][col] == num) { + return false; // 'num' is already in the row or column } } // Check the 3x3 box int boxRowOffset = (row / 3) * 3; int boxColOffset = (col / 3) * 3; - for (int k = 0; k < 3; ++k) { for (int m = 0; m < 3; ++m) { - if (val == board[boxRowOffset + k][boxColOffset + m]) { + if (num == board[boxRowOffset + k][boxColOffset + m]) { return false; } } @@ -156,6 +174,22 @@ public class Solver implements SudokuSolver { return true; } + /** + * {@inheritDoc} + */ + public boolean isSolvable() { + // We want to work on a copy + int[][] copy = new int[9][9]; + for (int row = 0; row < 9; row++) { + System.arraycopy(board[row], 0, copy[row], 0, 9); + } + + Solver copyModel = new Solver(); + copyModel.setBoard(copy); + + return copyModel.solve(); + } + /** * Checks if the board is solved * @@ -213,7 +247,7 @@ public class Solver implements SudokuSolver { } } - return true; + return isSolved(row + 1, col); } /** diff --git a/app/src/main/java/sudoku/SudokuSolver.java b/app/src/main/java/sudoku/SudokuSolver.java index f6fd74b..edf6ac3 100644 --- a/app/src/main/java/sudoku/SudokuSolver.java +++ b/app/src/main/java/sudoku/SudokuSolver.java @@ -52,7 +52,14 @@ public interface SudokuSolver { /** * Randomize the board. Guaranteed to be solvable. */ - void randomizeBoard(); + public void randomizeBoard(); + + /** + * Randomize the board. Guaranteed to be solvable. + * + * @param difficulty 0-9, 0 being easiest + */ + void randomizeBoard(int difficulty); /** * Check if the board is solved @@ -61,6 +68,13 @@ public interface SudokuSolver { */ boolean isSolved(); + /** + * Checks if the board is solvable + * + * @return true if solvable + */ + boolean isSolvable(); + /** * Clear the board */ diff --git a/app/src/test/java/sudoku/SolverTest.java b/app/src/test/java/sudoku/SolverTest.java index 2877430..c8a9dff 100644 --- a/app/src/test/java/sudoku/SolverTest.java +++ b/app/src/test/java/sudoku/SolverTest.java @@ -1,6 +1,6 @@ package sudoku; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; import java.util.stream.IntStream; @@ -45,6 +45,7 @@ class SolverTest { } @Test + @RepeatedTest(100) void solverTest() { Solver solver = new Solver(); assertFalse(solver.isSolved()); @@ -52,12 +53,16 @@ class SolverTest { assertTrue(solver.isSolved()); solver.clear(); - solver.randomizeBoard(); assertFalse(solver.isSolved()); assertTrue(solver.solve()); - solver.clear(); + } + + @Test + @RepeatedTest(100) + void randomizeBoardGuaranteeSolvableTest() { + Solver solver = new Solver(); solver.randomizeBoard(); - assertFalse(solver.isSolved()); + assertTrue(solver.solve()); } @Test @@ -79,11 +84,26 @@ class SolverTest { } @Test - @Disabled void unsolvableTest() { Solver solver = new Solver(); + + // Simple example + solver.clear(); solver.set(0, 0, 1); solver.set(0, 1, 1); assertFalse(solver.solve()); + + // More complex example + solver.clear(); + solver.set(0, 5, 7); + solver.set(0, 6, 8); + solver.set(0, 7, 2); + solver.set(1, 5, 3); + solver.set(2, 7, 1); + solver.set(3, 1, 4); + solver.set(5, 2, 8); + solver.set(5, 8, 6); + solver.set(7, 1, 8); + assertFalse(solver.solve()); } }