From 6366a5e0f22a88d7b5bd326364cb0b5d3ff31f70 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 11:53:17 +0100 Subject: [PATCH 01/22] Extending SudokuSolver interface and specifying @overrides in implementation --- app/src/main/java/sudoku/Solver.java | 9 +++++++++ app/src/main/java/sudoku/SudokuSolver.java | 14 +++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/sudoku/Solver.java b/app/src/main/java/sudoku/Solver.java index 4eb977c..f1b9249 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -10,6 +10,7 @@ public class Solver implements SudokuSolver { /** * {@inheritDoc} */ + @Override public void setBoard(int[][] board) { this.board = board; } @@ -17,6 +18,7 @@ public class Solver implements SudokuSolver { /** * {@inheritDoc} */ + @Override public int[][] getBoard() { return board; } @@ -24,6 +26,7 @@ public class Solver implements SudokuSolver { /** * Resets the board to all zeros */ + @Override public void clear() { board = new int[9][9]; } @@ -31,6 +34,7 @@ public class Solver implements SudokuSolver { /** * {@inheritDoc} */ + @Override public boolean solve() { return solve(0, 0); } @@ -79,6 +83,7 @@ public class Solver implements SudokuSolver { /** * Randomizes the board. This guarantees a solvable board. */ + @Override public void randomizeBoard() { this.clear(); for (int i = 0; i < 9; ++i) { @@ -94,6 +99,7 @@ public class Solver implements SudokuSolver { /** * {@inheritDoc} */ + @Override public void set(int row, int col, int val) { if (row < 9 && col < 9) { board[row][col] = val; @@ -103,6 +109,7 @@ public class Solver implements SudokuSolver { /** * {@inheritDoc} */ + @Override public int get(int row, int col) { if (row < 9 && col < 9) { return board[row][col]; @@ -113,6 +120,7 @@ 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) { return false; @@ -153,6 +161,7 @@ public class Solver implements SudokuSolver { * * @return true if solved */ + @Override public boolean isSolved() { return isSolved(0, 0); } diff --git a/app/src/main/java/sudoku/SudokuSolver.java b/app/src/main/java/sudoku/SudokuSolver.java index e77ecdd..f6fd74b 100644 --- a/app/src/main/java/sudoku/SudokuSolver.java +++ b/app/src/main/java/sudoku/SudokuSolver.java @@ -7,7 +7,7 @@ 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; /** * Get a copy of the sudoku board @@ -49,6 +49,18 @@ public interface SudokuSolver { */ void set(int row, int col, int nbr); + /** + * Randomize the board. Guaranteed to be solvable. + */ + void randomizeBoard(); + + /** + * Check if the board is solved + * + * @return true if solved + */ + boolean isSolved(); + /** * Clear the board */ From 196f066281f287f6b2247b2ee28f36a562fde316 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 13:36:51 +0100 Subject: [PATCH 02/22] Recursion limit for bailing on unsolvable, more extensive testing and additions to interface --- app/src/main/java/sudoku/Solver.java | 72 ++++++++++++++++------ app/src/main/java/sudoku/SudokuSolver.java | 16 ++++- app/src/test/java/sudoku/SolverTest.java | 30 +++++++-- 3 files changed, 93 insertions(+), 25 deletions(-) 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()); } } From a6dba79d9d10daa0c43e20d944e5368a4edb066a Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 14:53:50 +0100 Subject: [PATCH 03/22] Inline docstrings where return is void and method takes no parameters --- app/src/main/java/sudoku/Solver.java | 38 ++++++++-------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/sudoku/Solver.java b/app/src/main/java/sudoku/Solver.java index bdfc552..0f4ad33 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -1,40 +1,34 @@ 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} - */ + /** {@inheritDoc} */ @Override public void setBoard(int[][] board) { 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]; } - /** - * {@inheritDoc} - */ + /*{@inheritDoc} */ @Override public boolean solve() { return solve(0, 0); @@ -98,9 +92,7 @@ public class Solver implements SudokuSolver { randomizeBoard(3); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public void randomizeBoard(int difficulty) { int amount_prefilled = (difficulty * 9) + 1; @@ -121,9 +113,7 @@ public class Solver implements SudokuSolver { } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public void set(int row, int col, int val) { if (row < 9 && col < 9) { @@ -131,9 +121,7 @@ public class Solver implements SudokuSolver { } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public int get(int row, int col) { if (row < 9 && col < 9) { @@ -142,9 +130,7 @@ public class Solver implements SudokuSolver { return 0; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public boolean isLegal(int row, int col, int num) { // Sanity check @@ -174,9 +160,7 @@ public class Solver implements SudokuSolver { return true; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isSolvable() { // We want to work on a copy int[][] copy = new int[9][9]; From e7e897912890a7fba4e8be8585039bd3b1183757 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 14:54:34 +0100 Subject: [PATCH 04/22] Set gradle to rerun tasks --- Justfile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index 6ef3ad1..675b9de 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 -c -w app/src "just test && just run" + +watchdoc: + watchexec -c -w app/src "just doc" From 71c43b35c3e5c8474a100bedc566d4b305d9b391 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 14:54:56 +0100 Subject: [PATCH 05/22] MVC architecture user interface implemented with swing --- app/src/main/java/gui/SudokuController.java | 86 +++++++++ app/src/main/java/gui/SudokuView.java | 186 ++++++++++++++++++++ app/src/main/java/sudoku/SolverMain.java | 34 +++- 3 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/gui/SudokuController.java create mode 100644 app/src/main/java/gui/SudokuView.java diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java new file mode 100644 index 0000000..58043d4 --- /dev/null +++ b/app/src/main/java/gui/SudokuController.java @@ -0,0 +1,86 @@ +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 + model.solve(); + + // Update the view + view.updateView(model.getBoard()); + } + }); + + 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.addCellClickListener(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(); + + String inputText = view.getCellValue(row, col); + int value = Integer.parseInt(inputText); + // Check if the input is legal and update the model and view + if (model.isLegal(row, col, value)) { + model.set(row, col, value); + view.updateView(model.getBoard()); + } else { + view.showErrorMessage("Invalid input. Try again."); + } + } + } +} diff --git a/app/src/main/java/gui/SudokuView.java b/app/src/main/java/gui/SudokuView.java new file mode 100644 index 0000000..9570746 --- /dev/null +++ b/app/src/main/java/gui/SudokuView.java @@ -0,0 +1,186 @@ +package gui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +/** + * 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; + + /** 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); + 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"); + + JPanel buttonPanel = new JPanel(); + buttonPanel.add(solveButton); + buttonPanel.add(resetButton); + buttonPanel.add(randomButton); + + 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 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 addCellClickListener(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); + } +} 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(); } } From f449d2343e40ffadbfb3c85e1f64fca617d99414 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 15:03:10 +0100 Subject: [PATCH 06/22] Documentation now passes the gradle doctest --- app/src/main/java/sudoku/SudokuSolver.java | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/sudoku/SudokuSolver.java b/app/src/main/java/sudoku/SudokuSolver.java index edf6ac3..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) throws IllegalArgumentException; + 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,9 +46,9 @@ 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); From 175545d3d593b9d0095a4393632c3a22a176ec8d Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 15:04:04 +0100 Subject: [PATCH 07/22] Suitable exceptions for setBoard with corresponding tests --- app/src/main/java/sudoku/Solver.java | 10 +++++++--- app/src/test/java/sudoku/SolverTest.java | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/sudoku/Solver.java b/app/src/main/java/sudoku/Solver.java index 0f4ad33..2c9bb37 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -12,7 +12,11 @@ public class Solver implements SudokuSolver { /** {@inheritDoc} */ @Override - public void setBoard(int[][] board) { + 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; } @@ -28,7 +32,7 @@ public class Solver implements SudokuSolver { board = new int[9][9]; } - /*{@inheritDoc} */ + /* {@inheritDoc} */ @Override public boolean solve() { return solve(0, 0); @@ -107,7 +111,7 @@ public class Solver implements SudokuSolver { } // 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 + // This is expensive, and there should be a better way to do this if (!isSolvable()) { randomizeBoard(difficulty); } diff --git a/app/src/test/java/sudoku/SolverTest.java b/app/src/test/java/sudoku/SolverTest.java index c8a9dff..af637e1 100644 --- a/app/src/test/java/sudoku/SolverTest.java +++ b/app/src/test/java/sudoku/SolverTest.java @@ -106,4 +106,11 @@ class SolverTest { solver.set(7, 1, 8); assertFalse(solver.solve()); } + + @Test + void setBoardInvalidInputThrowsTest() { + Solver solver = new Solver(); + assertThrows(NullPointerException.class, () -> solver.setBoard(null)); + assertThrows(IllegalArgumentException.class, () -> solver.setBoard(new int[8][8])); + } } From 521b3fb05b0b25af7ff73a15c4eedd04cf45fde2 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 15:54:20 +0100 Subject: [PATCH 08/22] Working parser, file chooser and sample files --- app/sample_sudokus/demo_from_lab.txt | 9 +++ app/src/main/java/gui/SudokuController.java | 18 ++++- app/src/main/java/gui/SudokuView.java | 48 +++++++++++- app/src/main/java/sudoku/Solver.java | 2 - app/src/main/java/sudoku/SudokuParser.java | 73 +++++++++++++++++++ .../test/java/sudoku/SudokuParserTest.java | 17 +++++ 6 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 app/sample_sudokus/demo_from_lab.txt create mode 100644 app/src/main/java/sudoku/SudokuParser.java create mode 100644 app/src/test/java/sudoku/SudokuParserTest.java 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/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java index 58043d4..83b5f2d 100644 --- a/app/src/main/java/gui/SudokuController.java +++ b/app/src/main/java/gui/SudokuController.java @@ -54,7 +54,23 @@ public class SudokuController { } }); - view.addCellClickListener(new CellActionListener()); + 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); + // Update the view + view.updateView(model.getBoard()); + } + } + }); + + view.addCellActionListener(new CellActionListener()); } /** Start the GUI */ diff --git a/app/src/main/java/gui/SudokuView.java b/app/src/main/java/gui/SudokuView.java index 9570746..81d6bcf 100644 --- a/app/src/main/java/gui/SudokuView.java +++ b/app/src/main/java/gui/SudokuView.java @@ -4,6 +4,8 @@ import javax.swing.*; import java.awt.*; import java.awt.event.*; +import sudoku.SudokuParser; + /** * SolverView is a GUI for the SudokuSolver interface */ @@ -17,6 +19,8 @@ public class SudokuView extends JFrame { private JButton resetButton; /** Button for random */ private JButton randomButton; + /** Button for picking a Sudoku-file */ + private JButton fileButton; /** Constructor */ public SudokuView() { @@ -52,11 +56,13 @@ public class SudokuView extends JFrame { 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); } @@ -105,6 +111,15 @@ public class SudokuView extends JFrame { 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 *

@@ -114,7 +129,7 @@ public class SudokuView extends JFrame { * * @param listener the ActionListener to add */ - public void addCellClickListener(ActionListener listener) { + public void addCellActionListener(ActionListener listener) { for (int row = 0; row < 9; row++) { for (int col = 0; col < 9; col++) { grid[row][col].addActionListener(listener); @@ -183,4 +198,35 @@ public class SudokuView extends JFrame { 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 2c9bb37..18f2254 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -47,8 +47,6 @@ public class Solver implements SudokuSolver { */ 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; } 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/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); + } +} From 15d58e52a5f72842ac946e7a991bd227f23615e9 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 16:08:28 +0100 Subject: [PATCH 09/22] Simplifying --- app/src/main/java/gui/SudokuController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java index 83b5f2d..348c997 100644 --- a/app/src/main/java/gui/SudokuController.java +++ b/app/src/main/java/gui/SudokuController.java @@ -88,8 +88,7 @@ public class SudokuController { int row = view.getSelectedRow(); int col = view.getSelectedColumn(); - String inputText = view.getCellValue(row, col); - int value = Integer.parseInt(inputText); + int value = Integer.parseInt(view.getCellValue(row, col)); // Check if the input is legal and update the model and view if (model.isLegal(row, col, value)) { model.set(row, col, value); From 2acf69d466662b4cb0ca420d209f3eb82f557786 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sun, 10 Dec 2023 16:08:51 +0100 Subject: [PATCH 10/22] More clear documentation --- app/src/main/java/sudoku/Solver.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/sudoku/Solver.java b/app/src/main/java/sudoku/Solver.java index 18f2254..89d24cf 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -115,7 +115,11 @@ public class Solver implements SudokuSolver { } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + *

+ * This is not checked for validity + */ @Override public void set(int row, int col, int val) { if (row < 9 && col < 9) { From 3cf7005151eaf13981e85ea45b70bdf9b27aa5b0 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 13:12:50 +0100 Subject: [PATCH 11/22] User feedback on unsolvable --- app/src/main/java/gui/SudokuController.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java index 348c997..69def49 100644 --- a/app/src/main/java/gui/SudokuController.java +++ b/app/src/main/java/gui/SudokuController.java @@ -25,10 +25,16 @@ public class SudokuController { @Override public void actionPerformed(ActionEvent e) { // Solve the board - model.solve(); + boolean solved = model.solve(); + if (!solved) { + view.showErrorMessage("Could not solve the board."); + System.out.println("Could not solve the board."); + System.out.println(model.toString()); + } else { + // Update the view + view.updateView(model.getBoard()); + } - // Update the view - view.updateView(model.getBoard()); } }); From 2209dd7786a499809b69625477b3fbf84b11d88a Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 13:50:58 +0100 Subject: [PATCH 12/22] Ugly fix for invalid input --- app/src/main/java/gui/SudokuController.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java index 69def49..98c2f31 100644 --- a/app/src/main/java/gui/SudokuController.java +++ b/app/src/main/java/gui/SudokuController.java @@ -94,7 +94,22 @@ public class SudokuController { int row = view.getSelectedRow(); int col = view.getSelectedColumn(); - int value = Integer.parseInt(view.getCellValue(row, col)); + int value = 0; + + String cellValue = view.getCellValue(row, col); + if (cellValue == null || cellValue.equals("")) { + value = 0; + } else { + try { + value = Integer.parseInt(cellValue); + } catch (NumberFormatException ex) { + model.set(row, col, 0); + view.updateView(model.getBoard()); + view.showErrorMessage("Invalid input. Try again."); + return; // Bail out if the input is invalid + } + } + // Check if the input is legal and update the model and view if (model.isLegal(row, col, value)) { model.set(row, col, value); From 7e5253fb4b7ac94e1002b05e2c6a68c7bbff2f95 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 13:58:05 +0100 Subject: [PATCH 13/22] Slightly less ugly fix for invalid input --- app/src/main/java/gui/SudokuController.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java index 98c2f31..5528349 100644 --- a/app/src/main/java/gui/SudokuController.java +++ b/app/src/main/java/gui/SudokuController.java @@ -103,20 +103,17 @@ public class SudokuController { try { value = Integer.parseInt(cellValue); } catch (NumberFormatException ex) { - model.set(row, col, 0); - view.updateView(model.getBoard()); - view.showErrorMessage("Invalid input. Try again."); - return; // Bail out if the input is invalid + value = 0; } } - // Check if the input is legal and update the model and view - if (model.isLegal(row, col, value)) { - model.set(row, col, value); - view.updateView(model.getBoard()); - } else { + // If the input is invalid + if (!model.isLegal(row, col, value)) { + value = 0; view.showErrorMessage("Invalid input. Try again."); } + model.set(row, col, value); + view.updateView(model.getBoard()); } } } From d056732addea2ffba64a36b6d1a61aed927fb1b9 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 14:25:04 +0100 Subject: [PATCH 14/22] Polishing invalid input handling --- app/src/main/java/gui/SudokuController.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java index 5528349..a95f1a4 100644 --- a/app/src/main/java/gui/SudokuController.java +++ b/app/src/main/java/gui/SudokuController.java @@ -94,24 +94,31 @@ public class SudokuController { 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 = 0; + value = -1; } } - // If the input is invalid - if (!model.isLegal(row, col, value)) { + // If the input is invalid, value < 0 indicates parse error + if (!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); view.updateView(model.getBoard()); } From f9fa515651d3b7d7909167f090f9ce52fbf54bb9 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Mon, 11 Dec 2023 14:32:51 +0100 Subject: [PATCH 15/22] Extending testcases --- app/sample_sudokus/testfall_3.txt | 9 +++++++++ app/sample_sudokus/testfall_5.txt | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 app/sample_sudokus/testfall_3.txt create mode 100644 app/sample_sudokus/testfall_5.txt 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 From af77468edcbf79d546457d601e2b50b6adbeffbd Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Mon, 11 Dec 2023 14:34:51 +0100 Subject: [PATCH 16/22] Extending testcases --- app/src/test/java/sudoku/SolverTest.java | 33 +++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/sudoku/SolverTest.java b/app/src/test/java/sudoku/SolverTest.java index af637e1..3a5833b 100644 --- a/app/src/test/java/sudoku/SolverTest.java +++ b/app/src/test/java/sudoku/SolverTest.java @@ -86,7 +86,7 @@ class SolverTest { @Test void unsolvableTest() { Solver solver = new Solver(); - + // Simple example solver.clear(); solver.set(0, 0, 1); @@ -107,6 +107,37 @@ class SolverTest { 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(); From e6cd5a2915bacd417708a8b702f54f7f54404e46 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 14:35:25 +0100 Subject: [PATCH 17/22] Added bin directory to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 270a9f381d86cc8755868f00e57dd6a37b5e3ad7 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 14:58:54 +0100 Subject: [PATCH 18/22] File picker now refuses to load unsolvable files --- app/src/main/java/gui/SudokuController.java | 13 ++++++++++--- app/src/main/java/sudoku/Solver.java | 12 +++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java index a95f1a4..a283ab8 100644 --- a/app/src/main/java/gui/SudokuController.java +++ b/app/src/main/java/gui/SudokuController.java @@ -26,13 +26,13 @@ public class SudokuController { 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 { - // Update the view - view.updateView(model.getBoard()); } } @@ -70,6 +70,13 @@ public class SudokuController { 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()); } @@ -113,7 +120,7 @@ public class SudokuController { } // If the input is invalid, value < 0 indicates parse error - if (!model.isLegal(row, col, value) || value < 0) { + if (value != 0 && !model.isLegal(row, col, value) || value < 0) { value = 0; view.showErrorMessage("Invalid input. Try again."); } diff --git a/app/src/main/java/sudoku/Solver.java b/app/src/main/java/sudoku/Solver.java index 89d24cf..95a77b3 100644 --- a/app/src/main/java/sudoku/Solver.java +++ b/app/src/main/java/sudoku/Solver.java @@ -29,7 +29,12 @@ public class Solver implements SudokuSolver { /** 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} */ @@ -144,6 +149,11 @@ public class Solver implements SudokuSolver { return false; } + // Ihe the number is already present in the cell + if (board[row][col] == num) { + return true; + } + // Check both the row and column for (int i = 0; i < 9; i++) { if (board[row][i] == num || board[i][col] == num) { From 26df774ca35d68ba7a937c0f77a8ddf59ce68b81 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 14:59:11 +0100 Subject: [PATCH 19/22] Watchexec targets modified in justfile --- Justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index 675b9de..6ed91e0 100644 --- a/Justfile +++ b/Justfile @@ -11,7 +11,7 @@ clean: fd -td -I build -x rm -r watch: - watchexec -c -w app/src "just test && just run" + watchexec -r -c -w app/src "just test && just run" watchdoc: - watchexec -c -w app/src "just doc" + watchexec -r -c -w app/src "just doc" From 5bf0c92d102e4c3ff8297e56f81ed09b381e5449 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 15:53:22 +0100 Subject: [PATCH 20/22] Error checking inputs and reverting if illegal --- app/src/main/java/gui/SudokuController.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/gui/SudokuController.java b/app/src/main/java/gui/SudokuController.java index a283ab8..0d91eed 100644 --- a/app/src/main/java/gui/SudokuController.java +++ b/app/src/main/java/gui/SudokuController.java @@ -127,6 +127,15 @@ public class SudokuController { // 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()); } } From ffd1d4bd518f3fffea211568f2552a206ca04641 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 11 Dec 2023 16:06:50 +0100 Subject: [PATCH 21/22] Fixing broken test case --- app/src/test/java/sudoku/SolverTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/sudoku/SolverTest.java b/app/src/test/java/sudoku/SolverTest.java index 3a5833b..7000140 100644 --- a/app/src/test/java/sudoku/SolverTest.java +++ b/app/src/test/java/sudoku/SolverTest.java @@ -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)); }); From 9f252703717a7a2a6ef2767480ca8e4e769d21b9 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Tue, 12 Dec 2023 21:01:01 +0100 Subject: [PATCH 22/22] Sudoku board style --- app/src/main/java/gui/SudokuView.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/gui/SudokuView.java b/app/src/main/java/gui/SudokuView.java index 81d6bcf..7cd86eb 100644 --- a/app/src/main/java/gui/SudokuView.java +++ b/app/src/main/java/gui/SudokuView.java @@ -39,17 +39,24 @@ public class SudokuView extends JFrame { 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() {