diff --git a/.gitignore b/.gitignore index 34124a5..722dd18 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build .vscode +app/bin \ No newline at end of file diff --git a/Justfile b/Justfile index 6ef3ad1..6ed91e0 100644 --- a/Justfile +++ b/Justfile @@ -2,10 +2,16 @@ run: ./gradlew run test: - ./gradlew test + ./gradlew test --rerun-tasks + +doc: + ./gradlew javadoc --rerun-tasks clean: fd -td -I build -x rm -r watch: - watchexec -c -w app/src "just test && just run" \ No newline at end of file + watchexec -r -c -w app/src "just test && just run" + +watchdoc: + watchexec -r -c -w app/src "just doc" diff --git a/app/sample_sudokus/demo_from_lab.txt b/app/sample_sudokus/demo_from_lab.txt new file mode 100644 index 0000000..f6b5bbc --- /dev/null +++ b/app/sample_sudokus/demo_from_lab.txt @@ -0,0 +1,9 @@ +0 0 9 0 7 1 3 0 0 +0 0 1 0 0 0 0 0 0 +6 0 0 0 9 0 0 4 7 +5 0 0 9 0 4 0 0 0 +1 0 4 0 0 0 2 0 9 +0 0 0 1 0 8 0 0 4 +7 3 0 0 1 0 0 0 2 +0 0 0 0 0 0 5 0 0 +0 0 8 2 4 0 6 0 0 \ No newline at end of file diff --git a/app/sample_sudokus/testfall_3.txt b/app/sample_sudokus/testfall_3.txt new file mode 100644 index 0000000..3d08a4c --- /dev/null +++ b/app/sample_sudokus/testfall_3.txt @@ -0,0 +1,9 @@ +1 2 3 0 0 0 0 0 0 +4 5 6 0 0 0 0 0 0 +0 0 0 7 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/app/sample_sudokus/testfall_5.txt b/app/sample_sudokus/testfall_5.txt new file mode 100644 index 0000000..e512bf8 --- /dev/null +++ b/app/sample_sudokus/testfall_5.txt @@ -0,0 +1,9 @@ +0 0 8 0 0 9 0 6 2 +0 0 0 0 0 0 0 0 5 +1 0 2 5 0 0 0 0 0 +0 0 0 2 1 0 0 9 0 +0 5 0 0 0 0 6 0 0 +6 0 0 0 0 0 0 2 8 +4 1 0 6 0 8 0 0 0 +8 6 0 0 3 0 1 0 0 +0 0 0 0 0 0 4 0 0 \ No newline at end of file diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java new file mode 100644 index 0000000..0d91eed --- /dev/null +++ b/app/src/main/java/gui/SudokuController.java @@ -0,0 +1,142 @@ +package gui; + +import sudoku.SudokuSolver; +import java.awt.event.*; + +/** + * SolverController is a controller for the SudokuSolver interface + */ +public class SudokuController { + SudokuSolver model; + SudokuView view; + + /** + * Constructor + * + * @param model SudokuSolver model + * @param view SudokuView view + */ + public SudokuController(SudokuSolver model, SudokuView view) { + this.model = model; + this.view = view; + + // Add action listeners to the buttons + view.addSolveButtonListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + // Solve the board + boolean solved = model.solve(); + // Update the view + view.updateView(model.getBoard()); + if (!solved) { + view.showErrorMessage("Could not solve the board."); + System.out.println("Could not solve the board."); + System.out.println(model.toString()); + } else { + } + + } + }); + + view.addResetButtonListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + // Clear the board + model.clear(); + + // Update the view + view.updateView(model.getBoard()); + } + }); + + view.addRandomButtonListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + // Randomize the board + model.randomizeBoard(); + + // Update the view + view.updateView(model.getBoard()); + } + }); + + view.addFileButtonListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + // Open a file, view handles the parsing internally via SudokuParser + int[][] newBoard = view.openFile(); + + // If the file was parsed successfully + if (newBoard != null) { + // Set the model + model.setBoard(newBoard); + + // Warn and clear if the board is not solvable + if(!model.isSolvable()) { + view.showErrorMessage("The board is not solvable."); + model.clear(); + } + + // Update the view + view.updateView(model.getBoard()); + } + } + }); + + view.addCellActionListener(new CellActionListener()); + } + + /** Start the GUI */ + public void start() { + view.setVisible(true); + } + + /** + * CellActionListener is an ActionListener for the Sudoku grid cells + */ + private class CellActionListener implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + // Get the row and column from the clicked cell + int row = view.getSelectedRow(); + int col = view.getSelectedColumn(); + + // The value to be inserted into the cell + // Zero inicates an empty cell + // Negative values are invalid + int value = 0; + + String cellValue = view.getCellValue(row, col); + + // We need to check for null and empty string + if (cellValue == null || cellValue.equals("")) { + value = 0; + } else { + try { + value = Integer.parseInt(cellValue); + } catch (NumberFormatException ex) { + value = -1; + } + } + + // If the input is invalid, value < 0 indicates parse error + if (value != 0 && !model.isLegal(row, col, value) || value < 0) { + value = 0; + view.showErrorMessage("Invalid input. Try again."); + } + + // Update the model and view + model.set(row, col, value); + + // Warn if the board is not solvable (e.g. if the user has made a mistake) + // This is very messy, error prone and computationally expensive + if(!model.isSolvable()) { + model.set(row, col, 0); + view.showErrorMessage("Illegal move. The board is not solvable."); + } + + // Sync the view with the model + view.updateView(model.getBoard()); + } + } +} diff --git a/app/src/main/java/gui/SudokuView.java b/app/src/main/java/gui/SudokuView.java new file mode 100644 index 0000000..7cd86eb --- /dev/null +++ b/app/src/main/java/gui/SudokuView.java @@ -0,0 +1,239 @@ +package gui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +import sudoku.SudokuParser; + +/** + * SolverView is a GUI for the SudokuSolver interface + */ +public class SudokuView extends JFrame { + /** The grid of text fields */ + private JTextField[][] grid; + + /** Button for solve */ + private JButton solveButton; + /** Button for reset */ + private JButton resetButton; + /** Button for random */ + private JButton randomButton; + /** Button for picking a Sudoku-file */ + private JButton fileButton; + + /** Constructor */ + public SudokuView() { + setTitle("Sudoku Solver"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setLayout(new BorderLayout()); + + initializeGrid(); + initializeButtons(); + + pack(); + setLocationRelativeTo(null); + } + + /** Initialize the grid, called by the constructor */ + private void initializeGrid() { + grid = new JTextField[9][9]; + JPanel gridPanel = new JPanel(new GridLayout(9, 9)); + + for (int row = 0; row < 9; row++) { + for (int col = 0; col < 9; col++) { + grid[row][col] = new JTextField(2); + grid[row][col].setHorizontalAlignment(JTextField.CENTER); + + // Set background color to gray for every third JTextField + if ((row / 3 + col / 3) % 2 == 1) { + grid[row][col].setBackground(Color.LIGHT_GRAY); + } + + gridPanel.add(grid[row][col]); + } + } + + add(gridPanel, BorderLayout.CENTER); + } + + + /** Initialize the buttons, called by the constructor */ + private void initializeButtons() { + solveButton = new JButton("Solve"); + resetButton = new JButton("Reset"); + randomButton = new JButton("Randomize"); + fileButton = new JButton("Open file"); + + JPanel buttonPanel = new JPanel(); + buttonPanel.add(solveButton); + buttonPanel.add(resetButton); + buttonPanel.add(randomButton); + buttonPanel.add(fileButton); + + add(buttonPanel, BorderLayout.SOUTH); + } + + /** + * Update the view with a new grid + * + * @param newGrid the new grid to display + */ + public void updateView(int[][] newGrid) { + for (int row = 0; row < 9; row++) { + for (int col = 0; col < 9; col++) { + if (newGrid[row][col] != 0) { + grid[row][col].setText(String.valueOf(newGrid[row][col])); + } else { + grid[row][col].setText(""); + } + } + } + } + + /** + * Method to add ActionListener to solve button + * + * @param listener the ActionListener to add + */ + public void addSolveButtonListener(ActionListener listener) { + solveButton.addActionListener(listener); + } + + /** + * Method to add ActionListener to reset button + * + * @param listener the ActionListener to add + */ + public void addResetButtonListener(ActionListener listener) { + resetButton.addActionListener(listener); + } + + /** + * Method to add ActionListener to randomize button + * + * @param listener the ActionListener to add + */ + public void addRandomButtonListener(ActionListener listener) { + randomButton.addActionListener(listener); + } + + /** + * Method to add ActionListener to file button + * + * @param listener the ActionListener to add + */ + public void addFileButtonListener(ActionListener listener) { + fileButton.addActionListener(listener); + } + + /** + * Method to add ActionListener to individual cells in the grid + *
+ * Assumes that the ActionListener will be the same for all cells + * and that the listener will be capable of determining which cell + * was clicked + * + * @param listener the ActionListener to add + */ + public void addCellActionListener(ActionListener listener) { + for (int row = 0; row < 9; row++) { + for (int col = 0; col < 9; col++) { + grid[row][col].addActionListener(listener); + } + } + } + + /** + * Getter method to retrieve the values from the text fields + * + * @param row the row of the cell + * @param col the column of the cell + * @return the value of the cell + */ + public String getCellValue(int row, int col) { + return grid[row][col].getText(); + } + + /** + * Method to get the selected row (example implementation) + * + * @return the selected row, or -1 if no cell is selected + */ + public int getSelectedRow() { + for (int row = 0; row < 9; row++) { + for (int col = 0; col < 9; col++) { + if (grid[row][col].isFocusOwner()) { + return row; + } + } + } + return -1; // Return -1 if no cell is selected + } + + /** + * Method to get the selected column (example implementation) + * + * @return the selected row, or -1 if no cell is selected + */ + public int getSelectedColumn() { + for (int row = 0; row < 9; row++) { + for (int col = 0; col < 9; col++) { + if (grid[row][col].isFocusOwner()) { + return col; + } + } + } + return -1; // Return -1 if no cell is selected + } + + /** + * Methods to show dialogs + * + * @param message the message to display + * @return the user input + */ + public String showInputDialog(String message) { + return JOptionPane.showInputDialog(this, message); + } + + /** + * Method to show error messages + * + * @param message the message to display + */ + public void showErrorMessage(String message) { + JOptionPane.showMessageDialog(this, message); + } + + /** + * Method to open a file picker dialog + * + * @return 2D array of integers representing the Sudoku board + */ + public int[][] openFile() { + // Create a file chooser and set all related options + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setCurrentDirectory(new java.io.File(".")); + fileChooser.setDialogTitle("Select a Sudoku file"); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + fileChooser.setAcceptAllFileFilterUsed(false); + + // Show the file chooser and return if the user cancels + int returnValue = fileChooser.showOpenDialog(this); + if (returnValue != JFileChooser.APPROVE_OPTION) { + return null; + } + + // Get the path + String filepath = fileChooser.getSelectedFile().getAbsolutePath(); + + // Try to parse it + try { + return SudokuParser.parseSudoku(filepath); + } catch (Exception e) { + showErrorMessage(e.getMessage()); + return null; + } + } +} diff --git a/app/src/main/java/sudoku/Solver.java b/app/src/main/java/sudoku/Solver.java index 4eb977c..95a77b3 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -1,36 +1,44 @@ package sudoku; +/** Solver is a class that implements the SudokuSolver interface */ public class Solver implements SudokuSolver { private int[][] board = null; + private int tries = 0; + /** Constructor */ public Solver() { board = new int[9][9]; } - /** - * {@inheritDoc} - */ - public void setBoard(int[][] board) { + /** {@inheritDoc} */ + @Override + public void setBoard(int[][] board) throws IllegalArgumentException, NullPointerException { + if (board == null) + throw new NullPointerException("Board cannot be null"); + if (board.length != 9 || board[0].length != 9) + throw new IllegalArgumentException("Board must be 9x9"); this.board = board; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ + @Override public int[][] getBoard() { return board; } - /** - * Resets the board to all zeros - */ + /** Resets the board to all zeros */ + @Override public void clear() { - board = new int[9][9]; + for (int[] row : board) { + for (int i = 0; i < row.length; ++i) { + row[i] = 0; + } + } + // board = new int[9][9]; } - /** - * {@inheritDoc} - */ + /* {@inheritDoc} */ + @Override public boolean solve() { return solve(0, 0); } @@ -43,6 +51,10 @@ public class Solver implements SudokuSolver { * @return true if solved */ private boolean solve(int row, int col) { + if (++tries >= 10000) { + return false; + } + if (row < 0 || row > 9 || col < 0 || col > 9) { return false; } @@ -57,6 +69,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); } @@ -72,16 +85,26 @@ 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(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; @@ -89,20 +112,28 @@ 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 a better way to do this + if (!isSolvable()) { + randomizeBoard(difficulty); + } } - /** - * {@inheritDoc} + /** + * {@inheritDoc} + *
+ * This is not checked for validity */ + @Override public void set(int row, int col, int val) { if (row < 9 && col < 9) { board[row][col] = val; } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ + @Override public int get(int row, int col) { if (row < 9 && col < 9) { return board[row][col]; @@ -110,35 +141,32 @@ public class Solver implements SudokuSolver { return 0; } - /** - * {@inheritDoc} - */ - public boolean isLegal(int row, int col, int val) { - if (row < 0 || row >= 9 || col < 0 || col >= 9 || val < 1 || val > 9) { + /** {@inheritDoc} */ + @Override + 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; - } + // Ihe the number is already present in the cell + if (board[row][col] == num) { + return true; } - // 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; } } @@ -148,11 +176,26 @@ 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 * * @return true if solved */ + @Override public boolean isSolved() { return isSolved(0, 0); } @@ -204,7 +247,7 @@ public class Solver implements SudokuSolver { } } - return true; + return isSolved(row + 1, col); } /** diff --git a/app/src/main/java/sudoku/SolverMain.java b/app/src/main/java/sudoku/SolverMain.java index 7077608..ea5ed7c 100644 --- a/app/src/main/java/sudoku/SolverMain.java +++ b/app/src/main/java/sudoku/SolverMain.java @@ -1,12 +1,34 @@ package sudoku; +import gui.SudokuController; +import gui.SudokuView; + +/** SolverMain is the main class for the Sudoku Solver */ public class SolverMain { + + private Solver model; + private SudokuView view; + private SudokuController controller; + + /** Constructor */ + SolverMain() { + model = new Solver(); + view = new SudokuView(); + controller = new SudokuController(model, view); + } + + /** Start the GUI */ + void start() { + controller.start(); + } + + /** + * Main method + * + * @param args command line arguments + */ public static void main(String[] args) { - Solver s = new Solver(); - System.out.println(s.toString()); - s.randomizeBoard(); - System.out.println(s.toString()); - s.solve(); - System.out.println(s.toString()); + SolverMain main = new SolverMain(); + main.start(); } } diff --git a/app/src/main/java/sudoku/SudokuParser.java b/app/src/main/java/sudoku/SudokuParser.java new file mode 100644 index 0000000..bbe7ac9 --- /dev/null +++ b/app/src/main/java/sudoku/SudokuParser.java @@ -0,0 +1,73 @@ +package sudoku; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.FileNotFoundException; + +/** Helpers class for parsing Sudoku files */ +public class SudokuParser { + private static final int BOARD_SIZE = 9; + + // * Empty private constructor */ + private SudokuParser() { + // Empty + }; + + /** + * Parses a Sudoku file and returns a 2D array of integers + * + * @param filePath Path to the Sudoku file + * @return 2D array of integers representing the Sudoku board + * @throws IOException If an IO error occurs + * @throws FileNotFoundException When the file cannot be found + * @throws NumberFormatException When the file contains invalid characters + * @throws IllegalArgumentException When the file contains an invalid number of + * rows or columns + */ + public static int[][] parseSudoku(String filePath) + throws IOException, FileNotFoundException, NumberFormatException, IllegalArgumentException { + int[][] sudokuBoard = new int[BOARD_SIZE][BOARD_SIZE]; + + // In practice we could just split the entire file into a single string and then + // parse it into an array of integers, which is then partitioned into a 2D + // array. + // However, this is how the assignment is specified, so we will do it this way. + + // Try to read the file with a BufferedReader + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String line; + int row = 0; + + // While there are lines to read and we haven't reached the end of the board + while ((line = reader.readLine()) != null && row < BOARD_SIZE) { + // Split it into an array of strings + String[] values = line.trim().split("\\s+"); + + // Check that the number of columns is correct + if (values.length != BOARD_SIZE) { + throw new IllegalArgumentException("Invalid number of columns in the Sudoku file."); + } + + // Parse the strings into integers and add them to the board + for (int col = 0; col < BOARD_SIZE; col++) { + sudokuBoard[row][col] = Integer.parseInt(values[col]); // Throws NumberFormatException + } + + row++; + } + + if (row != BOARD_SIZE) { + throw new IllegalArgumentException("Invalid number of rows in the Sudoku file."); + } + } catch (FileNotFoundException e) { + throw new FileNotFoundException("The Sudoku file could not be found."); + } catch (IOException e) { + throw new IOException("An error occurred while reading the Sudoku file."); + } catch (NumberFormatException e) { + throw new NumberFormatException("The Sudoku file contains invalid characters."); + } + + return sudokuBoard; + } +} diff --git a/app/src/main/java/sudoku/SudokuSolver.java b/app/src/main/java/sudoku/SudokuSolver.java index e77ecdd..758e5b5 100644 --- a/app/src/main/java/sudoku/SudokuSolver.java +++ b/app/src/main/java/sudoku/SudokuSolver.java @@ -1,5 +1,6 @@ package sudoku; +/** SudokuSolver is an interface for implementing Sudoku solvers */ public interface SudokuSolver { /** * Set sudoku board, numbers 1-9 are fixed values, 0 is unsolved. @@ -7,10 +8,12 @@ public interface SudokuSolver { * @param board a board to copy values from * @throws IllegalArgumentException if board is invalid, e.g. not 9x9 */ - void setBoard(int[][] board); + void setBoard(int[][] board) throws IllegalArgumentException, NullPointerException; /** * Get a copy of the sudoku board + * + * @return a copy of the sudoku board */ int[][] getBoard(); @@ -24,9 +27,9 @@ public interface SudokuSolver { /** * Check if digit is legal on the current board * - * @param row - * @param col - * @param nbr + * @param row row + * @param col column + * @param nbr number to check * @return true if legal */ boolean isLegal(int row, int col, int nbr); @@ -34,8 +37,8 @@ public interface SudokuSolver { /** * Get number on board * - * @param row - * @param col + * @param row row + * @param col column * @return number on board */ int get(int row, int col); @@ -43,12 +46,38 @@ public interface SudokuSolver { /** * Set number on board, numbers 1-9 are fixed values, 0 is unsolved. * - * @param row - * @param col - * @param nbr + * @param row row + * @param col column + * @param nbr number to set */ void set(int row, int col, int nbr); + /** + * Randomize the board. Guaranteed to be solvable. + */ + 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 + * + * @return true if solved + */ + 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..7000140 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; @@ -34,7 +34,8 @@ class SolverTest { assertTrue(solver.isLegal(0, 0, 1)); solver.set(0, 0, 1); - IntStream.range(0, 9).forEach(i -> { + // Start from one, since setting the same value is legal + IntStream.range(1, 9).forEach(i -> { assertFalse(solver.isLegal(0, i, 1)); assertFalse(solver.isLegal(i, 0, 1)); }); @@ -45,6 +46,7 @@ class SolverTest { } @Test + @RepeatedTest(100) void solverTest() { Solver solver = new Solver(); assertFalse(solver.isSolved()); @@ -52,12 +54,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 +85,64 @@ 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()); + } + + @Test + void unsolvableTestCase3() { + Solver solver = new Solver(); + + // More complex example + solver.clear(); + solver.set(0, 0, 1); + solver.set(0, 1, 2); + solver.set(0, 2, 3); + solver.set(1, 0, 4); + solver.set(1, 1, 5); + solver.set(1, 2, 6); + solver.set(2, 3, 7); + assertFalse(solver.isSolvable()); + } + + @Test + void solvableTestCase3() { + Solver solver = new Solver(); + + // More complex example + solver.clear(); + solver.set(0, 0, 1); + solver.set(0, 1, 2); + solver.set(0, 2, 3); + solver.set(1, 0, 4); + solver.set(1, 1, 5); + solver.set(1, 2, 6); + assertTrue(solver.isSolvable()); + } + + @Test + void setBoardInvalidInputThrowsTest() { + Solver solver = new Solver(); + assertThrows(NullPointerException.class, () -> solver.setBoard(null)); + assertThrows(IllegalArgumentException.class, () -> solver.setBoard(new int[8][8])); } } diff --git a/app/src/test/java/sudoku/SudokuParserTest.java b/app/src/test/java/sudoku/SudokuParserTest.java new file mode 100644 index 0000000..b63be11 --- /dev/null +++ b/app/src/test/java/sudoku/SudokuParserTest.java @@ -0,0 +1,17 @@ +package sudoku; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class SudokuParserTest { + @Test + void constructorTest() { + int[][] board; + try { + board = SudokuParser.parseSudoku("sample_sudokus/demo_from_lab.txt"); + } catch (Exception e) { + board = null; + } + assertNotNull(board); + } +}