Compare commits
No commits in common. "9f252703717a7a2a6ef2767480ca8e4e769d21b9" and "fda5ef3e938d0defd31b7af496052fc15524af25" have entirely different histories.
9f25270371
...
fda5ef3e93
13 changed files with 61 additions and 719 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,4 +5,3 @@
|
||||||
build
|
build
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
app/bin
|
|
10
Justfile
10
Justfile
|
@ -2,16 +2,10 @@ run:
|
||||||
./gradlew run
|
./gradlew run
|
||||||
|
|
||||||
test:
|
test:
|
||||||
./gradlew test --rerun-tasks
|
./gradlew test
|
||||||
|
|
||||||
doc:
|
|
||||||
./gradlew javadoc --rerun-tasks
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
fd -td -I build -x rm -r
|
fd -td -I build -x rm -r
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
watchexec -r -c -w app/src "just test && just run"
|
watchexec -c -w app/src "just test && just run"
|
||||||
|
|
||||||
watchdoc:
|
|
||||||
watchexec -r -c -w app/src "just doc"
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,142 +0,0 @@
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,239 +0,0 @@
|
||||||
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
|
|
||||||
* <p>
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +1,36 @@
|
||||||
package sudoku;
|
package sudoku;
|
||||||
|
|
||||||
/** Solver is a class that implements the SudokuSolver interface */
|
|
||||||
public class Solver implements SudokuSolver {
|
public class Solver implements SudokuSolver {
|
||||||
private int[][] board = null;
|
private int[][] board = null;
|
||||||
private int tries = 0;
|
|
||||||
|
|
||||||
/** Constructor */
|
|
||||||
public Solver() {
|
public Solver() {
|
||||||
board = new int[9][9];
|
board = new int[9][9];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/**
|
||||||
@Override
|
* {@inheritDoc}
|
||||||
public void setBoard(int[][] board) throws IllegalArgumentException, NullPointerException {
|
*/
|
||||||
if (board == null)
|
public void setBoard(int[][] board) {
|
||||||
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;
|
this.board = board;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/**
|
||||||
@Override
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
public int[][] getBoard() {
|
public int[][] getBoard() {
|
||||||
return board;
|
return board;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resets the board to all zeros */
|
/**
|
||||||
@Override
|
* Resets the board to all zeros
|
||||||
|
*/
|
||||||
public void clear() {
|
public void clear() {
|
||||||
for (int[] row : board) {
|
board = new int[9][9];
|
||||||
for (int i = 0; i < row.length; ++i) {
|
|
||||||
row[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// board = new int[9][9];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* {@inheritDoc} */
|
/**
|
||||||
@Override
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
public boolean solve() {
|
public boolean solve() {
|
||||||
return solve(0, 0);
|
return solve(0, 0);
|
||||||
}
|
}
|
||||||
|
@ -51,10 +43,6 @@ public class Solver implements SudokuSolver {
|
||||||
* @return true if solved
|
* @return true if solved
|
||||||
*/
|
*/
|
||||||
private boolean solve(int row, int col) {
|
private boolean solve(int row, int col) {
|
||||||
if (++tries >= 10000) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row < 0 || row > 9 || col < 0 || col > 9) {
|
if (row < 0 || row > 9 || col < 0 || col > 9) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -69,7 +57,6 @@ public class Solver implements SudokuSolver {
|
||||||
|
|
||||||
// If we have a "number" in the current cell
|
// If we have a "number" in the current cell
|
||||||
// recursively call solve() on the next cell
|
// recursively call solve() on the next cell
|
||||||
// until we find a zero (empty cell)
|
|
||||||
if (board[row][col] != 0) {
|
if (board[row][col] != 0) {
|
||||||
return solve(row + 1, col);
|
return solve(row + 1, col);
|
||||||
}
|
}
|
||||||
|
@ -85,26 +72,16 @@ public class Solver implements SudokuSolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the current cell to zero and backtrack
|
|
||||||
board[row][col] = 0;
|
board[row][col] = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* Randomizes the board. This guarantees a solvable board.
|
||||||
* <p>
|
|
||||||
* Default difficulty is 3
|
|
||||||
*/
|
*/
|
||||||
public void randomizeBoard() {
|
public void randomizeBoard() {
|
||||||
randomizeBoard(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
|
||||||
@Override
|
|
||||||
public void randomizeBoard(int difficulty) {
|
|
||||||
int amount_prefilled = (difficulty * 9) + 1;
|
|
||||||
this.clear();
|
this.clear();
|
||||||
for (int i = 0; i < amount_prefilled; ++i) {
|
for (int i = 0; i < 9; ++i) {
|
||||||
int row = (int) (Math.random() * 9);
|
int row = (int) (Math.random() * 9);
|
||||||
int col = (int) (Math.random() * 9);
|
int col = (int) (Math.random() * 9);
|
||||||
int val = (int) (Math.random() * 9) + 1;
|
int val = (int) (Math.random() * 9) + 1;
|
||||||
|
@ -112,28 +89,20 @@ public class Solver implements SudokuSolver {
|
||||||
board[row][col] = val;
|
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}
|
||||||
* <p>
|
|
||||||
* This is <b>not</b> checked for validity
|
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
public void set(int row, int col, int val) {
|
public void set(int row, int col, int val) {
|
||||||
if (row < 9 && col < 9) {
|
if (row < 9 && col < 9) {
|
||||||
board[row][col] = val;
|
board[row][col] = val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/**
|
||||||
@Override
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
public int get(int row, int col) {
|
public int get(int row, int col) {
|
||||||
if (row < 9 && col < 9) {
|
if (row < 9 && col < 9) {
|
||||||
return board[row][col];
|
return board[row][col];
|
||||||
|
@ -141,32 +110,35 @@ public class Solver implements SudokuSolver {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/**
|
||||||
@Override
|
* {@inheritDoc}
|
||||||
public boolean isLegal(int row, int col, int num) {
|
*/
|
||||||
// Sanity check
|
public boolean isLegal(int row, int col, int val) {
|
||||||
if (row < 0 || row >= 9 || col < 0 || col >= 9 || num < 1 || num > 9) {
|
if (row < 0 || row >= 9 || col < 0 || col >= 9 || val < 1 || val > 9) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ihe the number is already present in the cell
|
// Check if val is already in col
|
||||||
if (board[row][col] == num) {
|
for (int i = 0; i < 9; ++i) {
|
||||||
return true;
|
if (val == board[i][col]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check both the row and column
|
// Check if val is already in row
|
||||||
for (int i = 0; i < 9; i++) {
|
for (int j = 0; j < 9; ++j) {
|
||||||
if (board[row][i] == num || board[i][col] == num) {
|
if (val == board[row][j]) {
|
||||||
return false; // 'num' is already in the row or column
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the 3x3 box
|
// Check the 3x3 box
|
||||||
int boxRowOffset = (row / 3) * 3;
|
int boxRowOffset = (row / 3) * 3;
|
||||||
int boxColOffset = (col / 3) * 3;
|
int boxColOffset = (col / 3) * 3;
|
||||||
|
|
||||||
for (int k = 0; k < 3; ++k) {
|
for (int k = 0; k < 3; ++k) {
|
||||||
for (int m = 0; m < 3; ++m) {
|
for (int m = 0; m < 3; ++m) {
|
||||||
if (num == board[boxRowOffset + k][boxColOffset + m]) {
|
if (val == board[boxRowOffset + k][boxColOffset + m]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,26 +148,11 @@ public class Solver implements SudokuSolver {
|
||||||
return true;
|
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
|
* Checks if the board is solved
|
||||||
*
|
*
|
||||||
* @return true if solved
|
* @return true if solved
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
public boolean isSolved() {
|
public boolean isSolved() {
|
||||||
return isSolved(0, 0);
|
return isSolved(0, 0);
|
||||||
}
|
}
|
||||||
|
@ -247,7 +204,7 @@ public class Solver implements SudokuSolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isSolved(row + 1, col);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,34 +1,12 @@
|
||||||
package sudoku;
|
package sudoku;
|
||||||
|
|
||||||
import gui.SudokuController;
|
|
||||||
import gui.SudokuView;
|
|
||||||
|
|
||||||
/** SolverMain is the main class for the Sudoku Solver */
|
|
||||||
public class SolverMain {
|
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) {
|
public static void main(String[] args) {
|
||||||
SolverMain main = new SolverMain();
|
Solver s = new Solver();
|
||||||
main.start();
|
System.out.println(s.toString());
|
||||||
|
s.randomizeBoard();
|
||||||
|
System.out.println(s.toString());
|
||||||
|
s.solve();
|
||||||
|
System.out.println(s.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
package sudoku;
|
package sudoku;
|
||||||
|
|
||||||
/** SudokuSolver is an interface for implementing Sudoku solvers */
|
|
||||||
public interface SudokuSolver {
|
public interface SudokuSolver {
|
||||||
/**
|
/**
|
||||||
* Set sudoku board, numbers 1-9 are fixed values, 0 is unsolved.
|
* Set sudoku board, numbers 1-9 are fixed values, 0 is unsolved.
|
||||||
|
@ -8,12 +7,10 @@ public interface SudokuSolver {
|
||||||
* @param board a board to copy values from
|
* @param board a board to copy values from
|
||||||
* @throws IllegalArgumentException if board is invalid, e.g. not 9x9
|
* @throws IllegalArgumentException if board is invalid, e.g. not 9x9
|
||||||
*/
|
*/
|
||||||
void setBoard(int[][] board) throws IllegalArgumentException, NullPointerException;
|
void setBoard(int[][] board);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a copy of the sudoku board
|
* Get a copy of the sudoku board
|
||||||
*
|
|
||||||
* @return a <b>copy</b> of the sudoku board
|
|
||||||
*/
|
*/
|
||||||
int[][] getBoard();
|
int[][] getBoard();
|
||||||
|
|
||||||
|
@ -27,9 +24,9 @@ public interface SudokuSolver {
|
||||||
/**
|
/**
|
||||||
* Check if digit is legal on the current board
|
* Check if digit is legal on the current board
|
||||||
*
|
*
|
||||||
* @param row row
|
* @param row
|
||||||
* @param col column
|
* @param col
|
||||||
* @param nbr number to check
|
* @param nbr
|
||||||
* @return true if legal
|
* @return true if legal
|
||||||
*/
|
*/
|
||||||
boolean isLegal(int row, int col, int nbr);
|
boolean isLegal(int row, int col, int nbr);
|
||||||
|
@ -37,8 +34,8 @@ public interface SudokuSolver {
|
||||||
/**
|
/**
|
||||||
* Get number on board
|
* Get number on board
|
||||||
*
|
*
|
||||||
* @param row row
|
* @param row
|
||||||
* @param col column
|
* @param col
|
||||||
* @return number on board
|
* @return number on board
|
||||||
*/
|
*/
|
||||||
int get(int row, int col);
|
int get(int row, int col);
|
||||||
|
@ -46,38 +43,12 @@ public interface SudokuSolver {
|
||||||
/**
|
/**
|
||||||
* Set number on board, numbers 1-9 are fixed values, 0 is unsolved.
|
* Set number on board, numbers 1-9 are fixed values, 0 is unsolved.
|
||||||
*
|
*
|
||||||
* @param row row
|
* @param row
|
||||||
* @param col column
|
* @param col
|
||||||
* @param nbr number to set
|
* @param nbr
|
||||||
*/
|
*/
|
||||||
void set(int row, int col, int nbr);
|
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
|
* Clear the board
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package sudoku;
|
package sudoku;
|
||||||
|
|
||||||
import org.junit.jupiter.api.RepeatedTest;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
@ -34,8 +34,7 @@ class SolverTest {
|
||||||
assertTrue(solver.isLegal(0, 0, 1));
|
assertTrue(solver.isLegal(0, 0, 1));
|
||||||
solver.set(0, 0, 1);
|
solver.set(0, 0, 1);
|
||||||
|
|
||||||
// Start from one, since setting the same value is legal
|
IntStream.range(0, 9).forEach(i -> {
|
||||||
IntStream.range(1, 9).forEach(i -> {
|
|
||||||
assertFalse(solver.isLegal(0, i, 1));
|
assertFalse(solver.isLegal(0, i, 1));
|
||||||
assertFalse(solver.isLegal(i, 0, 1));
|
assertFalse(solver.isLegal(i, 0, 1));
|
||||||
});
|
});
|
||||||
|
@ -46,7 +45,6 @@ class SolverTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@RepeatedTest(100)
|
|
||||||
void solverTest() {
|
void solverTest() {
|
||||||
Solver solver = new Solver();
|
Solver solver = new Solver();
|
||||||
assertFalse(solver.isSolved());
|
assertFalse(solver.isSolved());
|
||||||
|
@ -54,16 +52,12 @@ class SolverTest {
|
||||||
assertTrue(solver.isSolved());
|
assertTrue(solver.isSolved());
|
||||||
|
|
||||||
solver.clear();
|
solver.clear();
|
||||||
|
solver.randomizeBoard();
|
||||||
assertFalse(solver.isSolved());
|
assertFalse(solver.isSolved());
|
||||||
assertTrue(solver.solve());
|
assertTrue(solver.solve());
|
||||||
}
|
solver.clear();
|
||||||
|
|
||||||
@Test
|
|
||||||
@RepeatedTest(100)
|
|
||||||
void randomizeBoardGuaranteeSolvableTest() {
|
|
||||||
Solver solver = new Solver();
|
|
||||||
solver.randomizeBoard();
|
solver.randomizeBoard();
|
||||||
assertTrue(solver.solve());
|
assertFalse(solver.isSolved());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -85,64 +79,11 @@ class SolverTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Disabled
|
||||||
void unsolvableTest() {
|
void unsolvableTest() {
|
||||||
Solver solver = new Solver();
|
Solver solver = new Solver();
|
||||||
|
|
||||||
// Simple example
|
|
||||||
solver.clear();
|
|
||||||
solver.set(0, 0, 1);
|
solver.set(0, 0, 1);
|
||||||
solver.set(0, 1, 1);
|
solver.set(0, 1, 1);
|
||||||
assertFalse(solver.solve());
|
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]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue