Compare commits
23 commits
fda5ef3e93
...
9f25270371
Author | SHA1 | Date | |
---|---|---|---|
|
9f25270371 | ||
|
ffd1d4bd51 | ||
|
5bf0c92d10 | ||
|
26df774ca3 | ||
|
270a9f381d | ||
|
e6cd5a2915 | ||
|
af77468edc | ||
|
f9fa515651 | ||
|
d056732add | ||
|
7e5253fb4b | ||
|
2209dd7786 | ||
|
3cf7005151 | ||
|
2acf69d466 | ||
|
15d58e52a5 | ||
|
521b3fb05b | ||
|
175545d3d5 | ||
|
f449d2343e | ||
|
71c43b35c3 | ||
|
e7e8979128 | ||
|
a6dba79d9d | ||
|
196f066281 | ||
|
6366a5e0f2 | ||
383af5be58 |
13 changed files with 719 additions and 61 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@
|
||||||
build
|
build
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
|
app/bin
|
10
Justfile
10
Justfile
|
@ -2,10 +2,16 @@ run:
|
||||||
./gradlew run
|
./gradlew run
|
||||||
|
|
||||||
test:
|
test:
|
||||||
./gradlew test
|
./gradlew test --rerun-tasks
|
||||||
|
|
||||||
|
doc:
|
||||||
|
./gradlew javadoc --rerun-tasks
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
fd -td -I build -x rm -r
|
fd -td -I build -x rm -r
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
watchexec -c -w app/src "just test && just run"
|
watchexec -r -c -w app/src "just test && just run"
|
||||||
|
|
||||||
|
watchdoc:
|
||||||
|
watchexec -r -c -w app/src "just doc"
|
||||||
|
|
9
app/sample_sudokus/demo_from_lab.txt
Normal file
9
app/sample_sudokus/demo_from_lab.txt
Normal file
|
@ -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
|
9
app/sample_sudokus/testfall_3.txt
Normal file
9
app/sample_sudokus/testfall_3.txt
Normal file
|
@ -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
|
9
app/sample_sudokus/testfall_5.txt
Normal file
9
app/sample_sudokus/testfall_5.txt
Normal file
|
@ -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
|
142
app/src/main/java/gui/SudokuController.java
Normal file
142
app/src/main/java/gui/SudokuController.java
Normal file
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
239
app/src/main/java/gui/SudokuView.java
Normal file
239
app/src/main/java/gui/SudokuView.java
Normal file
|
@ -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
|
||||||
|
* <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,36 +1,44 @@
|
||||||
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} */
|
||||||
* {@inheritDoc}
|
@Override
|
||||||
*/
|
public void setBoard(int[][] board) throws IllegalArgumentException, NullPointerException {
|
||||||
public void setBoard(int[][] board) {
|
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;
|
this.board = board;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** {@inheritDoc} */
|
||||||
* {@inheritDoc}
|
@Override
|
||||||
*/
|
|
||||||
public int[][] getBoard() {
|
public int[][] getBoard() {
|
||||||
return board;
|
return board;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Resets the board to all zeros */
|
||||||
* Resets the board to all zeros
|
@Override
|
||||||
*/
|
|
||||||
public void clear() {
|
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() {
|
public boolean solve() {
|
||||||
return solve(0, 0);
|
return solve(0, 0);
|
||||||
}
|
}
|
||||||
|
@ -43,6 +51,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -57,6 +69,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -72,16 +85,26 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Randomizes the board. This guarantees a solvable board.
|
* {@inheritDoc}
|
||||||
|
* <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 < 9; ++i) {
|
for (int i = 0; i < amount_prefilled; ++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;
|
||||||
|
@ -89,20 +112,28 @@ 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} */
|
||||||
* {@inheritDoc}
|
@Override
|
||||||
*/
|
|
||||||
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];
|
||||||
|
@ -110,35 +141,32 @@ public class Solver implements SudokuSolver {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** {@inheritDoc} */
|
||||||
* {@inheritDoc}
|
@Override
|
||||||
*/
|
public boolean isLegal(int row, int col, int num) {
|
||||||
public boolean isLegal(int row, int col, int val) {
|
// Sanity check
|
||||||
if (row < 0 || row >= 9 || col < 0 || col >= 9 || val < 1 || val > 9) {
|
if (row < 0 || row >= 9 || col < 0 || col >= 9 || num < 1 || num > 9) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if val is already in col
|
// Ihe the number is already present in the cell
|
||||||
for (int i = 0; i < 9; ++i) {
|
if (board[row][col] == num) {
|
||||||
if (val == board[i][col]) {
|
return true;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if val is already in row
|
// Check both the row and column
|
||||||
for (int j = 0; j < 9; ++j) {
|
for (int i = 0; i < 9; i++) {
|
||||||
if (val == board[row][j]) {
|
if (board[row][i] == num || board[i][col] == num) {
|
||||||
return false;
|
return false; // 'num' is already in the row or column
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (val == board[boxRowOffset + k][boxColOffset + m]) {
|
if (num == board[boxRowOffset + k][boxColOffset + m]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,11 +176,26 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -204,7 +247,7 @@ public class Solver implements SudokuSolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return isSolved(row + 1, col);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,12 +1,34 @@
|
||||||
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) {
|
||||||
Solver s = new Solver();
|
SolverMain main = new SolverMain();
|
||||||
System.out.println(s.toString());
|
main.start();
|
||||||
s.randomizeBoard();
|
|
||||||
System.out.println(s.toString());
|
|
||||||
s.solve();
|
|
||||||
System.out.println(s.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
73
app/src/main/java/sudoku/SudokuParser.java
Normal file
73
app/src/main/java/sudoku/SudokuParser.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
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.
|
||||||
|
@ -7,10 +8,12 @@ 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);
|
void setBoard(int[][] board) throws IllegalArgumentException, NullPointerException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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();
|
||||||
|
|
||||||
|
@ -24,9 +27,9 @@ public interface SudokuSolver {
|
||||||
/**
|
/**
|
||||||
* Check if digit is legal on the current board
|
* Check if digit is legal on the current board
|
||||||
*
|
*
|
||||||
* @param row
|
* @param row row
|
||||||
* @param col
|
* @param col column
|
||||||
* @param nbr
|
* @param nbr number to check
|
||||||
* @return true if legal
|
* @return true if legal
|
||||||
*/
|
*/
|
||||||
boolean isLegal(int row, int col, int nbr);
|
boolean isLegal(int row, int col, int nbr);
|
||||||
|
@ -34,8 +37,8 @@ public interface SudokuSolver {
|
||||||
/**
|
/**
|
||||||
* Get number on board
|
* Get number on board
|
||||||
*
|
*
|
||||||
* @param row
|
* @param row row
|
||||||
* @param col
|
* @param col column
|
||||||
* @return number on board
|
* @return number on board
|
||||||
*/
|
*/
|
||||||
int get(int row, int col);
|
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.
|
* Set number on board, numbers 1-9 are fixed values, 0 is unsolved.
|
||||||
*
|
*
|
||||||
* @param row
|
* @param row row
|
||||||
* @param col
|
* @param col column
|
||||||
* @param nbr
|
* @param nbr number to set
|
||||||
*/
|
*/
|
||||||
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.Disabled;
|
import org.junit.jupiter.api.RepeatedTest;
|
||||||
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,7 +34,8 @@ class SolverTest {
|
||||||
assertTrue(solver.isLegal(0, 0, 1));
|
assertTrue(solver.isLegal(0, 0, 1));
|
||||||
solver.set(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(0, i, 1));
|
||||||
assertFalse(solver.isLegal(i, 0, 1));
|
assertFalse(solver.isLegal(i, 0, 1));
|
||||||
});
|
});
|
||||||
|
@ -45,6 +46,7 @@ class SolverTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@RepeatedTest(100)
|
||||||
void solverTest() {
|
void solverTest() {
|
||||||
Solver solver = new Solver();
|
Solver solver = new Solver();
|
||||||
assertFalse(solver.isSolved());
|
assertFalse(solver.isSolved());
|
||||||
|
@ -52,12 +54,16 @@ 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();
|
||||||
assertFalse(solver.isSolved());
|
assertTrue(solver.solve());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -79,11 +85,64 @@ 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]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
app/src/test/java/sudoku/SudokuParserTest.java
Normal file
17
app/src/test/java/sudoku/SudokuParserTest.java
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue