diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f93364c..4c6f921 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { // Apply a specific Java toolchain to ease working on different environments. java { toolchain { - languageVersion.set(JavaLanguageVersion.of(11)) + languageVersion.set(JavaLanguageVersion.of(21)) } } diff --git a/app/src/main/java/sudoku/Solver.java b/app/src/main/java/sudoku/Solver.java index bfc1d7c..13631c9 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -1,5 +1,7 @@ package sudoku; +import java.util.stream.IntStream; + public class Solver implements SudokuSolver { private int[][] board; @@ -16,17 +18,200 @@ public class Solver implements SudokuSolver { } public boolean solve() { + return solve(0, 0); } - public Boolean isSolved() { + /** + * Resets the board to all zeros + */ + public void reset() { + board = new int[9][9]; } + /** + * Recursive helper method for solve() + * + * @param row row to solve + * @param col column to solve + * @return true if solved + */ private boolean solve(int row, int col) { + if (row < 0 || row > 9 || col < 0 || col > 9) { + return false; + } + + if (row == 9) { + row = 0; + if (++col == 9) { + return true; + } + } + + if (board[row][col] != 0) { + return solve(row + 1, col); + } + + for (int val = 1; val <= 9; ++val) { + if (legal(row, col, val)) { + board[row][col] = val; + if (solve(row + 1, col)) { + return true; + } + } + } + + board[row][col] = 0; + return false; } - public boolean legal(int row, int col, int nbr) { + /** + * Randomizes the board. This guarantees a solvable board. + */ + public void randomizeBoard() { + this.reset(); + for (int i = 0; i < 9; ++i) { + int row = (int) (Math.random() * 9); + int col = (int) (Math.random() * 9); + int val = (int) (Math.random() * 9) + 1; + if (legal(row, col, val)) { + board[row][col] = val; + } + } } + /** + * Sets the value of the board at the given position + * + * @param row row to set + * @param col column to set + * @param val value to set + */ + public void setPos(int row, int col, int val) { + if (row < 9 && col < 9) { + board[row][col] = val; + } + } + + /** + * Checks if val is legal in the given row, column, and 3x3 box + * + * @param row row to check + * @param col column to check + * @param val value to check + * @return true if val is legal + */ + public boolean legal(int row, int col, int val) { + if (row < 0 || row >= 9 || col < 0 || col >= 9 || val < 1 || val > 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 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]) { + return false; + } + } + } + + // None of the above failed, so it is legal + return true; + } + + public boolean isSolved() { + return isSolved(0, 0); + } + + /** + * Recursive helper method for isSolved() + * + * @param row + * @param col + * @return + */ + private boolean isSolved(int row, int col) { + // If we are at the 9th row and 0th column (the last cell), we are done + if (row == 9) { + row = 0; + if (++col == 9) { + return true; + } + } + + // If we find a zero, the board is not solved + if (board[row][col] == 0) { + return false; + } + + for (int i = 0; i < 9; ++i) { + // Checks if there is a duplicate in the row + if (i != row && board[i][col] == board[row][col]) { + return false; + } + + // Checks if there is a duplicate in the column + if (i != col && board[row][i] == board[row][col]) { + return false; + } + } + + // Checks if there is a duplicate in 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) { + int r = boxRowOffset + k; + int c = boxColOffset + m; + if ((r != row || c != col) && board[r][c] == board[row][col]) { + return false; + } + } + } + + return true; + } + + /** + * Returns a string representation of the board + * + * @return String representation of the board + */ + // Not particularly pretty, but it works public String toString() { + final String divider = "-------+--------+--------\n"; + StringBuilder sb = new StringBuilder(); + sb.append(divider); + int rowcount = 0; + for (int[] row : board) { + int colcount = 0; + sb.append("| "); + for (int val : row) { + colcount++; + sb.append(val); + sb.append(colcount % 3 == 0 ? " | " : " "); + } + rowcount++; + sb.append(rowcount % 3 == 0 ? "\n" : ""); + sb.append(rowcount % 3 == 0 ? divider : "\n"); + } + return sb.toString(); } } \ No newline at end of file diff --git a/app/src/main/java/sudoku/SolverMain.java b/app/src/main/java/sudoku/SolverMain.java index 76acdf9..7077608 100644 --- a/app/src/main/java/sudoku/SolverMain.java +++ b/app/src/main/java/sudoku/SolverMain.java @@ -2,6 +2,11 @@ package sudoku; public class SolverMain { public static void main(String[] args) { - System.out.println("Hello world!"); + Solver s = new Solver(); + System.out.println(s.toString()); + s.randomizeBoard(); + System.out.println(s.toString()); + s.solve(); + System.out.println(s.toString()); } } diff --git a/app/src/test/java/sudoku/SolverTest.java b/app/src/test/java/sudoku/SolverTest.java index ae16b4b..618f20a 100644 --- a/app/src/test/java/sudoku/SolverTest.java +++ b/app/src/test/java/sudoku/SolverTest.java @@ -1,12 +1,86 @@ package sudoku; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; +import java.util.stream.IntStream; class SolverTest { - @Test void boardTest() { + @Test + void constructorTest() { Solver solver = new Solver(); - solver.solve(); + assertNotNull(solver); + } + + @Test + void setBoardTest() { + Solver solver = new Solver(); + int[][] board = new int[9][9]; + solver.setBoard(board); + assertEquals(board, solver.getBoard()); + } + + @Test + void randomizeBoardTest() { + int[][] board = new int[9][9]; + Solver solver = new Solver(); + solver.randomizeBoard(); + assertNotEquals(board, solver.getBoard()); + } + + @Test + void legalTest() { + Solver solver = new Solver(); + assertTrue(solver.legal(0, 0, 1)); + solver.setPos(0, 0, 1); + + IntStream.range(0, 9).forEach(i -> { + assertFalse(solver.legal(0, i, 1)); + assertFalse(solver.legal(i, 0, 1)); + }); + + assertTrue(solver.legal(5, 5, 1)); + assertTrue(solver.legal(8, 8, 9)); + assertTrue(solver.legal(8, 8, 1)); + } + + @Test + void solverTest() { + Solver solver = new Solver(); + assertFalse(solver.isSolved()); + assertTrue(solver.solve()); assertTrue(solver.isSolved()); + + solver.reset(); + solver.randomizeBoard(); + assertFalse(solver.isSolved()); + assertTrue(solver.solve()); + } + + @Test + void resetTest() { + Solver solver = new Solver(); + assertTrue(solver.solve()); + solver.randomizeBoard(); + solver.reset(); + assertFalse(solver.isSolved()); + } + + @Test + void legalWithInvalidInputsTest() { + Solver solver = new Solver(); + assertFalse(solver.legal(-1, 0, 1)); + assertFalse(solver.legal(0, -1, 1)); + assertFalse(solver.legal(0, 0, -1)); + assertFalse(solver.legal(0, 0, 10)); + } + + @Test + @Disabled + void unsolvableTest() { + Solver solver = new Solver(); + solver.setPos(0, 0, 1); + solver.setPos(0, 1, 1); + assertFalse(solver.solve()); } }