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
|
||||
|
||||
.vscode
|
||||
app/bin
|
10
Justfile
10
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"
|
||||
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;
|
||||
|
||||
/** Solver is a class that implements the SudokuSolver interface */
|
||||
public class Solver implements SudokuSolver {
|
||||
private int[][] board = null;
|
||||
private int tries = 0;
|
||||
|
||||
/** Constructor */
|
||||
public Solver() {
|
||||
board = new int[9][9];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void setBoard(int[][] board) {
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void setBoard(int[][] board) throws IllegalArgumentException, NullPointerException {
|
||||
if (board == null)
|
||||
throw new NullPointerException("Board cannot be null");
|
||||
if (board.length != 9 || board[0].length != 9)
|
||||
throw new IllegalArgumentException("Board must be 9x9");
|
||||
this.board = board;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public int[][] getBoard() {
|
||||
return board;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the board to all zeros
|
||||
*/
|
||||
/** Resets the board to all zeros */
|
||||
@Override
|
||||
public void clear() {
|
||||
board = new int[9][9];
|
||||
for (int[] row : board) {
|
||||
for (int i = 0; i < row.length; ++i) {
|
||||
row[i] = 0;
|
||||
}
|
||||
}
|
||||
// board = new int[9][9];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
/* {@inheritDoc} */
|
||||
@Override
|
||||
public boolean solve() {
|
||||
return solve(0, 0);
|
||||
}
|
||||
|
@ -43,6 +51,10 @@ public class Solver implements SudokuSolver {
|
|||
* @return true if solved
|
||||
*/
|
||||
private boolean solve(int row, int col) {
|
||||
if (++tries >= 10000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (row < 0 || row > 9 || col < 0 || col > 9) {
|
||||
return false;
|
||||
}
|
||||
|
@ -57,6 +69,7 @@ public class Solver implements SudokuSolver {
|
|||
|
||||
// If we have a "number" in the current cell
|
||||
// recursively call solve() on the next cell
|
||||
// until we find a zero (empty cell)
|
||||
if (board[row][col] != 0) {
|
||||
return solve(row + 1, col);
|
||||
}
|
||||
|
@ -72,16 +85,26 @@ public class Solver implements SudokuSolver {
|
|||
}
|
||||
}
|
||||
|
||||
// Reset the current cell to zero and backtrack
|
||||
board[row][col] = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomizes the board. This guarantees a solvable board.
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* Default difficulty is 3
|
||||
*/
|
||||
public void randomizeBoard() {
|
||||
randomizeBoard(3);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void randomizeBoard(int difficulty) {
|
||||
int amount_prefilled = (difficulty * 9) + 1;
|
||||
this.clear();
|
||||
for (int i = 0; i < 9; ++i) {
|
||||
for (int i = 0; i < amount_prefilled; ++i) {
|
||||
int row = (int) (Math.random() * 9);
|
||||
int col = (int) (Math.random() * 9);
|
||||
int val = (int) (Math.random() * 9) + 1;
|
||||
|
@ -89,20 +112,28 @@ public class Solver implements SudokuSolver {
|
|||
board[row][col] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively call randomizeBoard() until we get a solvable board
|
||||
// This is expensive, and there should be a better way to do this
|
||||
if (!isSolvable()) {
|
||||
randomizeBoard(difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* This is <b>not</b> checked for validity
|
||||
*/
|
||||
@Override
|
||||
public void set(int row, int col, int val) {
|
||||
if (row < 9 && col < 9) {
|
||||
board[row][col] = val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public int get(int row, int col) {
|
||||
if (row < 9 && col < 9) {
|
||||
return board[row][col];
|
||||
|
@ -110,35 +141,32 @@ public class Solver implements SudokuSolver {
|
|||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public boolean isLegal(int row, int col, int val) {
|
||||
if (row < 0 || row >= 9 || col < 0 || col >= 9 || val < 1 || val > 9) {
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean isLegal(int row, int col, int num) {
|
||||
// Sanity check
|
||||
if (row < 0 || row >= 9 || col < 0 || col >= 9 || num < 1 || num > 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if val is already in col
|
||||
for (int i = 0; i < 9; ++i) {
|
||||
if (val == board[i][col]) {
|
||||
return false;
|
||||
}
|
||||
// Ihe the number is already present in the cell
|
||||
if (board[row][col] == num) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if val is already in row
|
||||
for (int j = 0; j < 9; ++j) {
|
||||
if (val == board[row][j]) {
|
||||
return false;
|
||||
// Check both the row and column
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (board[row][i] == num || board[i][col] == num) {
|
||||
return false; // 'num' is already in the row or column
|
||||
}
|
||||
}
|
||||
|
||||
// Check the 3x3 box
|
||||
int boxRowOffset = (row / 3) * 3;
|
||||
int boxColOffset = (col / 3) * 3;
|
||||
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
for (int m = 0; m < 3; ++m) {
|
||||
if (val == board[boxRowOffset + k][boxColOffset + m]) {
|
||||
if (num == board[boxRowOffset + k][boxColOffset + m]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -148,11 +176,26 @@ public class Solver implements SudokuSolver {
|
|||
return true;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
public boolean isSolvable() {
|
||||
// We want to work on a copy
|
||||
int[][] copy = new int[9][9];
|
||||
for (int row = 0; row < 9; row++) {
|
||||
System.arraycopy(board[row], 0, copy[row], 0, 9);
|
||||
}
|
||||
|
||||
Solver copyModel = new Solver();
|
||||
copyModel.setBoard(copy);
|
||||
|
||||
return copyModel.solve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the board is solved
|
||||
*
|
||||
* @return true if solved
|
||||
*/
|
||||
@Override
|
||||
public boolean isSolved() {
|
||||
return isSolved(0, 0);
|
||||
}
|
||||
|
@ -204,7 +247,7 @@ public class Solver implements SudokuSolver {
|
|||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return isSolved(row + 1, col);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
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;
|
||||
|
||||
/** SudokuSolver is an interface for implementing Sudoku solvers */
|
||||
public interface SudokuSolver {
|
||||
/**
|
||||
* Set sudoku board, numbers 1-9 are fixed values, 0 is unsolved.
|
||||
|
@ -7,10 +8,12 @@ public interface SudokuSolver {
|
|||
* @param board a board to copy values from
|
||||
* @throws IllegalArgumentException if board is invalid, e.g. not 9x9
|
||||
*/
|
||||
void setBoard(int[][] board);
|
||||
void setBoard(int[][] board) throws IllegalArgumentException, NullPointerException;
|
||||
|
||||
/**
|
||||
* Get a copy of the sudoku board
|
||||
*
|
||||
* @return a <b>copy</b> of the sudoku board
|
||||
*/
|
||||
int[][] getBoard();
|
||||
|
||||
|
@ -24,9 +27,9 @@ public interface SudokuSolver {
|
|||
/**
|
||||
* Check if digit is legal on the current board
|
||||
*
|
||||
* @param row
|
||||
* @param col
|
||||
* @param nbr
|
||||
* @param row row
|
||||
* @param col column
|
||||
* @param nbr number to check
|
||||
* @return true if legal
|
||||
*/
|
||||
boolean isLegal(int row, int col, int nbr);
|
||||
|
@ -34,8 +37,8 @@ public interface SudokuSolver {
|
|||
/**
|
||||
* Get number on board
|
||||
*
|
||||
* @param row
|
||||
* @param col
|
||||
* @param row row
|
||||
* @param col column
|
||||
* @return number on board
|
||||
*/
|
||||
int get(int row, int col);
|
||||
|
@ -43,12 +46,38 @@ public interface SudokuSolver {
|
|||
/**
|
||||
* Set number on board, numbers 1-9 are fixed values, 0 is unsolved.
|
||||
*
|
||||
* @param row
|
||||
* @param col
|
||||
* @param nbr
|
||||
* @param row row
|
||||
* @param col column
|
||||
* @param nbr number to set
|
||||
*/
|
||||
void set(int row, int col, int nbr);
|
||||
|
||||
/**
|
||||
* Randomize the board. Guaranteed to be solvable.
|
||||
*/
|
||||
public void randomizeBoard();
|
||||
|
||||
/**
|
||||
* Randomize the board. Guaranteed to be solvable.
|
||||
*
|
||||
* @param difficulty 0-9, 0 being easiest
|
||||
*/
|
||||
void randomizeBoard(int difficulty);
|
||||
|
||||
/**
|
||||
* Check if the board is solved
|
||||
*
|
||||
* @return true if solved
|
||||
*/
|
||||
boolean isSolved();
|
||||
|
||||
/**
|
||||
* Checks if the board is solvable
|
||||
*
|
||||
* @return true if solvable
|
||||
*/
|
||||
boolean isSolvable();
|
||||
|
||||
/**
|
||||
* Clear the board
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package sudoku;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.RepeatedTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import java.util.stream.IntStream;
|
||||
|
@ -34,7 +34,8 @@ class SolverTest {
|
|||
assertTrue(solver.isLegal(0, 0, 1));
|
||||
solver.set(0, 0, 1);
|
||||
|
||||
IntStream.range(0, 9).forEach(i -> {
|
||||
// Start from one, since setting the same value is legal
|
||||
IntStream.range(1, 9).forEach(i -> {
|
||||
assertFalse(solver.isLegal(0, i, 1));
|
||||
assertFalse(solver.isLegal(i, 0, 1));
|
||||
});
|
||||
|
@ -45,6 +46,7 @@ class SolverTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@RepeatedTest(100)
|
||||
void solverTest() {
|
||||
Solver solver = new Solver();
|
||||
assertFalse(solver.isSolved());
|
||||
|
@ -52,12 +54,16 @@ class SolverTest {
|
|||
assertTrue(solver.isSolved());
|
||||
|
||||
solver.clear();
|
||||
solver.randomizeBoard();
|
||||
assertFalse(solver.isSolved());
|
||||
assertTrue(solver.solve());
|
||||
solver.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@RepeatedTest(100)
|
||||
void randomizeBoardGuaranteeSolvableTest() {
|
||||
Solver solver = new Solver();
|
||||
solver.randomizeBoard();
|
||||
assertFalse(solver.isSolved());
|
||||
assertTrue(solver.solve());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -79,11 +85,64 @@ class SolverTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void unsolvableTest() {
|
||||
Solver solver = new Solver();
|
||||
|
||||
// Simple example
|
||||
solver.clear();
|
||||
solver.set(0, 0, 1);
|
||||
solver.set(0, 1, 1);
|
||||
assertFalse(solver.solve());
|
||||
|
||||
// More complex example
|
||||
solver.clear();
|
||||
solver.set(0, 5, 7);
|
||||
solver.set(0, 6, 8);
|
||||
solver.set(0, 7, 2);
|
||||
solver.set(1, 5, 3);
|
||||
solver.set(2, 7, 1);
|
||||
solver.set(3, 1, 4);
|
||||
solver.set(5, 2, 8);
|
||||
solver.set(5, 8, 6);
|
||||
solver.set(7, 1, 8);
|
||||
assertFalse(solver.solve());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsolvableTestCase3() {
|
||||
Solver solver = new Solver();
|
||||
|
||||
// More complex example
|
||||
solver.clear();
|
||||
solver.set(0, 0, 1);
|
||||
solver.set(0, 1, 2);
|
||||
solver.set(0, 2, 3);
|
||||
solver.set(1, 0, 4);
|
||||
solver.set(1, 1, 5);
|
||||
solver.set(1, 2, 6);
|
||||
solver.set(2, 3, 7);
|
||||
assertFalse(solver.isSolvable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void solvableTestCase3() {
|
||||
Solver solver = new Solver();
|
||||
|
||||
// More complex example
|
||||
solver.clear();
|
||||
solver.set(0, 0, 1);
|
||||
solver.set(0, 1, 2);
|
||||
solver.set(0, 2, 3);
|
||||
solver.set(1, 0, 4);
|
||||
solver.set(1, 1, 5);
|
||||
solver.set(1, 2, 6);
|
||||
assertTrue(solver.isSolvable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setBoardInvalidInputThrowsTest() {
|
||||
Solver solver = new Solver();
|
||||
assertThrows(NullPointerException.class, () -> solver.setBoard(null));
|
||||
assertThrows(IllegalArgumentException.class, () -> solver.setBoard(new int[8][8]));
|
||||
}
|
||||
}
|
||||
|
|
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