From d28377eb14e57a374ec26ebeb97c86f884b4afc1 Mon Sep 17 00:00:00 2001 From: Josh Ashton Date: Wed, 20 Mar 2024 22:06:41 -0600 Subject: [PATCH] init commit (imported from personal account) --- resources/input.sdku | 9 + resources/master-difficulty.sdku | 9 + resources/test.sdku | 9 + src/Cell.java | 330 +++++++++++++++++++++++++++ src/Nav.java | 173 +++++++++++++++ src/Settings.java | 52 +++++ src/SudokuChecker.java | 370 +++++++++++++++++++++++++++++++ src/run.ps1 | 3 + 8 files changed, 955 insertions(+) create mode 100644 resources/input.sdku create mode 100644 resources/master-difficulty.sdku create mode 100644 resources/test.sdku create mode 100644 src/Cell.java create mode 100644 src/Nav.java create mode 100644 src/Settings.java create mode 100644 src/SudokuChecker.java create mode 100644 src/run.ps1 diff --git a/resources/input.sdku b/resources/input.sdku new file mode 100644 index 0000000..a42d0df --- /dev/null +++ b/resources/input.sdku @@ -0,0 +1,9 @@ +53..7.... +6..195... +.98....6. +8...6...3 +4..8.3..1 +7...2...6 +.6....28. +...419..5 +....8..79 \ No newline at end of file diff --git a/resources/master-difficulty.sdku b/resources/master-difficulty.sdku new file mode 100644 index 0000000..c9e2857 --- /dev/null +++ b/resources/master-difficulty.sdku @@ -0,0 +1,9 @@ +.9...2.7. +.....3..6 +..196..8. +4......1. +.....6... +.3.75...8 +...2..... +.7.89...5 +..3...7.. \ No newline at end of file diff --git a/resources/test.sdku b/resources/test.sdku new file mode 100644 index 0000000..1f695ae --- /dev/null +++ b/resources/test.sdku @@ -0,0 +1,9 @@ +8.14..... +.4.83...6 +6..5921.4 +27.1.9538 +...6..4.9 +59.3...7. +.2.....65 +36.95..1. +........3 \ No newline at end of file diff --git a/src/Cell.java b/src/Cell.java new file mode 100644 index 0000000..72a5c8f --- /dev/null +++ b/src/Cell.java @@ -0,0 +1,330 @@ +package src; + +/** + * A Cell represents a single cell in the Sudoku grid. + * This helper class is used to store the row, column, value, and possible values for a cell. + * The possible values are stored in an ArrayList of Integers, and are sorted in ascending order. + * + * The Cell class also contains a method to add a possible value to the cell, and a method to get the possible values. + */ +public class Cell { + private int row; + private int col; + private int box; + private int value; + private List possibleValues; + + /** + * Create a new Cell with the given row and column. + * + * The value of the cell is initially -2, and the possible values are initially empty. + * + * @param row + * @param col + */ + public Cell(int row, int col) { + this.row = row; + this.col = col; + setBox(); + possibleValues = new List(); + } + + /** + * Create a new Cell with the given row, column, and value. + * + * The possible values are initially empty. + * + * @param row + * @param col + * @param value + */ + public Cell(int row, int col, int value) { + this.row = row; + this.col = col; + this.value = value; + setBox(); + possibleValues = new List(); + } + + /** + * Create a new Cell with the given row, column, and possible values. + * + * The value of the cell is initially -2. + * + * @param row + * @param col + * @param possibleValues + */ + public Cell(int row, int col, int[] possibleValues) { + this.row = row; + this.col = col; + setBox(); + this.possibleValues = new List(possibleValues); + } + + /** + * Set the value of a cell. + * + * The value must be between 1 and 9, inclusive. + * + * @param value + */ + public void setValue(int value) { + possibleValues.clear(); + this.value = value; + } + + /** + * Get the value of a cell. + * + * @return + */ + public int getValue() { + return value; + } + + /** + * Get the box a cell is in. + * + * @return box number + */ + public int getBox() { + return box; + } + + /** + * Add a value to the list of possible values. + * + * If the value is already possible for the cell, or if the value is not a valid value for the cell, then the value is not added. + * + * @param value + */ + public void addPossibleValue(int value) { + if(possibleValues.contains(value)) { + return; + } else if(value < 1 || value > 9) { + return; + } + + possibleValues.add(value); + } + + /** + * Set the list of possible values for the cell. + * + * @param possibleValues + */ + public void setPossibleValues(int[] possibleValues) { + this.possibleValues = new List(possibleValues); + } + + /** + * Remove a value from the list of possible values. + * + * @return + */ + public void removePossibleValue(int value) { + if(!possibleValues.contains(value)) { + return; + } + + possibleValues.remove(value); + } + + /** + * Get a list of possible values for the cell. + * + * This list is a copy of the cell's possible values, and is sorted in ascending order. + * + * @return list of possible values for the cell + */ + public int[] getPossibleValues() { + return possibleValues.toArray(); + } + + /** + * Given the cell's row and column, set the box number for the cell. + */ + private void setBox() { + int boxRow = row / 3; + int boxCol = col / 3; + box = boxRow * 3 + boxCol; + } + + /** + * This class is used to store the possible values for a cell in a sorted list. + * The list is sorted in ascending order, and the values are stored in a linked list. + * + * The List class also contains a method to add a value to the list, and a method to get the list of values as an array. + */ + private class List { + + private Value head; + private Value tail; + private int size; + private int max; + private int min; + + /** + * Create a new empty List. + * + * By default, the head and tail are null, the size is 0, and the max and min are 0. + */ + public List() { + head = null; + tail = null; + size = 0; + max = 0; + min = 0; + } + + /** + * Create a new List with the given initial value. + * + * By default, the head and tail are the same, the size is 1, and the max and min are the initial value. + * @param initValue + */ + public List(int initValue) { + head = new Value(initValue); + tail = head; + size = 1; + max = initValue; + min = initValue; + } + + /** + * Create a new List with the given initial values. + * + * @param initValues + */ + public List(int[] initValues) { + for(int i = 0; i < initValues.length; i++) add(initValues[i]); + } + + /** + * Add a new value to the list. + * + * By default, the value will be inserted at it's sorted place in the list. + * + * @param newValue + */ + public void add(int newValue) { + if(size == 0) { + head = new Value(newValue); + tail = head; + size = 1; + max = newValue; + min = newValue; + } else { + if(newValue > max) { + max = newValue; + tail.next = new Value(newValue); + tail = tail.next; + } else if(newValue < min) { + min = newValue; + Value tmp = new Value(newValue); + tmp.setNext(head); + head = tmp; + } else { + Value current = head; + while(current.next != null && current.next.value < newValue) { + current = current.next; + } + Value tmp = new Value(newValue); + tmp.setNext(current.next); + current.setNext(tmp); + } + size++; + } + } + + /** + * Remove a value from the list. + * + * @param value + */ + public void remove(int value) { + if(size == 0) { + System.out.println("List is empty."); + return; + } + + if(head.value == value) { + head = head.next; + size--; + return; + } + + Value current = head; + while(current.next != null) { + if(current.next.value == value) { + current.setNext(current.next.next); + size--; + return; + } + current = current.next; + } + + System.out.println("Value " + value + " is not in the list."); + } + + /** + * Get the list of values as an array. + * + * @return array of values in the list + */ + public int[] toArray() { + int[] array = new int[size]; + Value current = head; + for(int i = 0; i < size; i++) { + array[i] = current.value; + current = current.next; + } + return array; + } + + /** + * Check if the list contains the given value. + * + * @param value + * @return + */ + public boolean contains(int value) { + Value current = head; + while(current != null) { + if(current.value == value) return true; + current = current.next; + } + return false; + } + + /** + * Clear the list of all values. + */ + public void clear() { + head = null; + tail = null; + size = 0; + max = 0; + min = 0; + } + + /** + * Value class is a Node for the List class. + */ + private class Value { + + private int value; + private Value next; + + public Value(int value) { + this.value = value; + next = null; + } + + public void setNext(Value next) { + this.next = next; + } + } + } +} diff --git a/src/Nav.java b/src/Nav.java new file mode 100644 index 0000000..9de8886 --- /dev/null +++ b/src/Nav.java @@ -0,0 +1,173 @@ +package src; + +import java.util.Scanner; +import java.io.File; + +import javax.swing.JPanel; +import javax.swing.JComboBox; + +/** + * The Nav class represents the Navigation bar at the top of the JFrame window. + * + * The Nav class is responsible for basic File IO operations. The most basic + * functionality is the following: + * - creating a new, completely empty .sdku file for a user to create + * their own puzzle with; + * - open an existing .sdku file in the user's file system (a standard + * or default location is TBD); + * - save the currently open .sdku file with any changes the user has made. + * + * In the future, the Nav class would ideally incorporate additional elements, + * such as the difficulty of the current problem, providing hints, displaying + * the length of time since a puzzle was started, etc. + */ +public class Nav extends JPanel { + private Settings s; + private File f; + private Cell[][] grid; + + /** + * Create a new Nav in a command-line interface. This is intended for + * debugging rather than practical use. + * + * Additionally, even for debugging, this should be used since default + * file I/O behavior is dictated by the values in the Settings object. + */ + public Nav() { + System.out.println("Welcome to the Sudoku Solver!"); + + Scanner in = new Scanner(System.in); + System.out.print( + "Enter the name of the input file (do not " + + "include the file extension): " + ); + String inputFilename = in.nextLine(); + in.close(); + + open(inputFilename); + } + + /** + * Create a new Nav object for use in the command-line. This is intended + * for debugging rather than practical use. + * + * @param Settings + */ + public Nav(Settings s) { + this.s = s; + System.out.println("Welcome to the Sudoku Solver!"); + + Scanner in = new Scanner(System.in); + System.out.print( + "Enter the name of the input file (do not " + + "include the file extension): " + ); + String inputFilename = in.nextLine(); + in.close(); + + open(inputFilename); + } + + private void new() { + int default = s.getNewFileProperties(); + + // See Settings.getNewFileProperties for possible values and + // the expected behavior of each value. + switch(default) { + case 0: + case 1: + case 2: + default: + } + // TODO: Create a new .sdku file in the default directory. + } + + /** + * Given an input filename, open the file and populate the grid. + * + * @param String + */ + private void open(String filename) { + File f = new File(/*"spring24/sudoku/" + */filename + ".sdku"); + if(f == null || !f.exists() || f.isDirectory() || !f.canRead()) { + System.out.println( + "The file " + filename + + ".sdku does not exist or cannot be read." + ); + + System.out.println("The exact path to the file is: " + + f.getAbsolutePath() + ); + + return null; + } + + createGrid(f); + } + + private void save() { + // TODO: Get the current Cell[][] from the Board JPanel + // TODO: Write to a file. + } + + /** + * Get the currently loaded Cell[][] grid. + * + * @return the grid + */ + public Cell[][] getLoadedGrid() { + return grid; + } + + /** + * Update the loaded grid with the current version with any changes the + * user has made. + * + * For future auto-save functionality, this could simply be called after + * each change the user makes on the Sudoku board. + * + * In the future, an optimization could be made where a different data + * structure is used to pass only the changes the player has made within + * the last x minutes, for eg. {row, col, value, noteValues[]}. Given + * the size of a standard Sudoku board however, this may be an irrelevant + * and unnecessary optimization. + * + * @param Cell[][] + */ + public void updateLoadedGrid(Cell[][] grid) { + this.grid = grid; + } + + /** + * To be called when a File is opened or created. This will populate the + * Cell[][] grid with values read in from the file. + * + * @param File + */ + private void createGrid(File f) { + try (Scanner in = new Scanner(f)) { + Cell[][] grid = new Cell[9][9]; + for(int i = 0; i < 9; i++) { + String line = in.nextLine(); + for(int j = 0; j < 9; j++) { + if(line.charAt(j) == '.') { + grid[i][j] = new Cell(i, j, 0); + continue; + } + + int value = Character.getNumericValue(line.charAt(j)); + grid[i][j] = new Cell(i, j, value); + } + } + + this.grid = grid; + } catch (FileNotFoundException e) { + System.err.println("An error occurred while reading the file " + + filename + ".sdku." + ); + + e.printStackTrace(); + return null; + } + } +} diff --git a/src/Settings.java b/src/Settings.java new file mode 100644 index 0000000..432432e --- /dev/null +++ b/src/Settings.java @@ -0,0 +1,52 @@ +package src; +import java.util.Scanner; +import java.io.File; + +/** + * A class solely to store user settings relating to default behaviors, + * difficulty, filepaths, and more. + * + * These user settings are stored in a {TBD} file located at {TBD}. + */ +public class Settings { + private int newFileProperties; + private String defaultDirectory; + + public Settings() { + // TODO: Read in a file from the master default directory and populate + // settings, effectively toggles. If the file does not exist, + // populate it with the following values (possibly in .json). + newFileProperties = 0; + defaultDirectory = "~/AppData/Local/sudoku/"; + } + + /** + * New File Properties is a configurable setting, where the following + * values are allowable: + * 0 - Load the last opened .sdku file, default behavior + * 1 - Create a new, blank .sdku file + * 2 - Create a new, random .sdku file + * + * @return int + */ + public int getNewFileProperties() { + return newFileProperties; + } + + /** + * The Default Directory is a configurable setting, where the user can + * specify a default directory to store .sdku files. This is useful for + * eliminating the need for user input for file operations. + * + * By default, the value will be "~/.config/sudoku/" on MacOS and Linux. + * On Windows, the value will be "~/AppData/Local/sudoku/". + * + * NOTE: This does not change the location that the settings file may + * be located in. This must be located at {TBD}. + * + * @return String + */ + public String getDefaultDirectory() { + return defaultDirectory; + } +} diff --git a/src/SudokuChecker.java b/src/SudokuChecker.java new file mode 100644 index 0000000..43823c9 --- /dev/null +++ b/src/SudokuChecker.java @@ -0,0 +1,370 @@ +package src; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Scanner; + + +/** + * A Sudoku puzzle is a 9x9 grid of numbers, where each row, column, and 3x3 subgrid contains the numbers 1-9 exactly once. + * + * The goal of this challenge is to write a program that can solve Sudoku puzzles. + * + * Given an input .sdku file (a plain-text file containing the initial state of a Sudoku puzzle), find the solution to the puzzle and write the solution to a new .sdku file. + * + * The input file will contain 9 lines, each containing 9 characters. Each character will be a digit (1-9) or a period (.), representing an empty cell. + * + * The output file should contain the same 9x9 grid, with the empty cells filled in with the correct numbers. + * + * For example, given the following input file: + * + * 53..7.... + * 6..195... + * .98....6. + * 8...6...3 + * 4..8.3..1 + * 7...2...6 + * .6....28. + * ...419..5 + * ....8..79 + * + * The output file should be: + * + * 534678912 + * 672195348 + * 198342567 + * 859761423 + * 426853791 + * 713924856 + * 961537284 + * 287419635 + * 345286179 + * + * This challenge is broken up into parts. In Programming Club, we will split into teams of 3-4 people to work on each method. + */ +public class SudokuChecker { + + public static void main(String[] args) { + + Cell[][] grid = readPuzzle(inputFilename); + solve(grid); + System.out.println("The solution to the Sudoku puzzle is:"); + for(int i = 0; i < 9; i++) { + for(int j = 0; j < 9; j++) { + System.out.print(grid[i][j].getValue()); + } + System.out.println(); + } + //app.saveSolution(outputFilename, grid); + } + + /** + * Given an input filename, read the Sudoku puzzle from the file and return it as a 9x9 grid numbers, with no value for spaces in the file. + * + * @param filename + * @return + */ + public Cell[][] readPuzzle(String filename) { + } + + /** + * Given an output filename and a 9x9 grid of numbers, write the Sudoku puzzle to the file. + * + * @param grid + * @return + */ + public void saveSolution(String filename, Cell[][] grid) { + System.out.println("TODO: Write sudoku puzzle solution to " + filename + ".sdku"); + // TODO: Account for possible invalid solution from isSolved and handle an error. + // TODO: Write the solution to the file. + } + + /** + * Given two arrays of numbers, return the intersection of the two arrays. + * + * @param a + * @param b + * @return + */ + private ArrayList intersection(ArrayList a, ArrayList b) { + ArrayList intersection = new ArrayList(); + for(int i = 0; i < a.size(); i++) { + if(b.contains(a.get(i))) { + intersection.add(a.get(i)); + } + } + + return intersection; + } + + /** + * Determine what numbers are available to be placed in the given cell of the grid. + * This is effectively an intersection of the numbers available in the row, column, and box of the cell. + * + * @param grid + * @param row + * @param col + * @return an array of numbers that are available to be placed in the given cell + */ + private void getAvailableNumbers(Cell[][] grid, int row, int col) { + ArrayList intersection = intersection(getRowRemainingNumbers(grid, row), getColRemainingNumbers(grid, col)); + intersection = intersection(intersection, getBoxRemainingNumbers(grid, row, col)); + + if(intersection.size() == 0) { + return; + } else if(intersection.size() == 1) { + int value = intersection.get(0); + + grid[row][col].setValue(value); + updatePossibleValues(grid, row, col); + return; + } else { + // Join the arrays and find the intersection of the three arrays. + ArrayList availableNumbers = new ArrayList(); + for(int i = 0; i < intersection.size(); i++) { + availableNumbers.add(intersection.get(i)); + } + + grid[row][col].setPossibleValues(arrayListToArray(availableNumbers)); + } + } + + /** + * Update the possible values for cells in the same row, column, and box as the given cell. + * This should always be called once a cell's value has been set, to remove that value from the possible values of other cells. + * + * @param grid + * @param row + * @param col + */ + private void updatePossibleValues(Cell[][] grid, int row, int col) { + int value = grid[row][col].getValue(); + + for(int i = 0; i < 9; i++) { + if(grid[row][i].getValue() == 0) { + grid[row][i].removePossibleValue(value); + if(grid[row][i].getPossibleValues().length == 1) { + grid[row][i].setValue(grid[row][i].getPossibleValues()[0]); + updatePossibleValues(grid, row, i); + } + } + if(grid[i][col].getValue() == 0) { + grid[i][col].removePossibleValue(value); + if(grid[i][col].getPossibleValues().length == 1) { + grid[i][col].setValue(grid[i][col].getPossibleValues()[0]); + updatePossibleValues(grid, i, col); + } + } + } + + int boxRow = row / 3; + int boxCol = col / 3; + for(int i = 0; i < 3; i++) { + for(int j = 0; j < 3; j++) { + if(grid[boxRow * 3 + i][boxCol * 3 + j].getValue() == 0) { + grid[boxRow * 3 + i][boxCol * 3 + j].removePossibleValue(value); + } + } + } + } + + /** + * Solve the Sudoku puzzle. + * + * @param grid + */ + public void solve(Cell[][] grid) { + // Continue until a valid solution is reached. + while(!isValidSolution(grid)) { + for(int i = 0; i < 9; i++) { + for(int j = 0; j < 9; j++) { + if(grid[i][j].getValue() == 0) { + getAvailableNumbers(grid, i, j); + } + } + } + } + } + + // + + /** + * Get any number between 1 and 9 that is not in the row. + * + * @param grid + * @param row + * @return + */ + private ArrayList getRowRemainingNumbers(Cell[][] grid, int row) { + ArrayList remainingNumbers = new ArrayList(); + for(int i = 1; i <= 9; i++) { + boolean found = false; + for(int j = 0; j < 9; j++) { + if(grid[row][j].getValue() == i) { + found = true; + break; + } + } + if(!found) { + remainingNumbers.add(i); + } + } + + return remainingNumbers; + } + + /** + * Get any number between 1 and 9 that is not in the column. + * + * @param grid + * @param col + * @return + */ + private ArrayList getColRemainingNumbers(Cell[][] grid, int col) { + ArrayList remainingNumbers = new ArrayList(); + for(int i = 1; i <= 9; i++) { + boolean found = false; + for(int j = 0; j < 9; j++) { + if(grid[j][col].getValue() == i) { + found = true; + break; + } + } + if(!found) { + remainingNumbers.add(i); + } + } + + return remainingNumbers; + } + + /** + * Get any number between 1 and 9 that is not in the box. + * + * A box is a 3x3 subgrid of the 9x9 grid. + * + * @param grid + * @param box + * @return + */ + private ArrayList getBoxRemainingNumbers(Cell[][] grid, int row, int col) { + ArrayList remainingNumbers = new ArrayList(); + int boxRow = row / 3; + int boxCol = col / 3; + for(int i = 1; i <= 9; i++) { + boolean found = false; + for(int j = 0; j < 3; j++) { + for(int k = 0; k < 3; k++) { + if(grid[boxRow * 3 + j][boxCol * 3 + k].getValue() == i) { + found = true; + break; + } + } + } + if(!found) { + remainingNumbers.add(i); + } + } + + return remainingNumbers; + } + + /** + * Given a list of numbers, return an array of the numbers. + * + * A helper method to keep the code using arrays instead of lists whenever possible. + * + * @param list + * @return + */ + private int[] arrayListToArray(ArrayList list) { + int[] array = new int[list.size()]; + for(int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + /** + * Given a 9x9 grid of numbers, return true if the grid is a valid Sudoku puzzle solution, and false otherwise. + * + * A valid Sudoku puzzle is one where each row, column, and 3x3 subgrid contains the numbers 1-9 exactly once. + * + * @param grid a 9x9 grid of numbers + * @return true if the grid is a valid Sudoku puzzle, and false otherwise + */ + private boolean isValidSolution(Cell[][] grid) { + // Check that every cell has a value between 1 and 9. + for(int i = 0; i < 9; i++) { + for(int j = 0; j < 9; j++) { + if(grid[i][j].getValue() < 1 || grid[i][j].getValue() > 9) { + //System.out.println("Cell at row " + i + " and column " + j + " has an invalid value of " + grid[i][j].getValue() + "."); + return false; + } + } + } + + // Check that every row contains the numbers 1-9 exactly once. + for(int i = 0; i < 9; i++) { + int[] row = new int[9]; + for(int j = 0; j < 9; j++) { + row[j] = grid[i][j].getValue(); + } + if(!isValidSet(row)) { + System.out.println("Row " + i + " is invalid."); + return false; + } + } + + // Check that every column contains the numbers 1-9 exactly once. + for(int i = 0; i < 9; i++) { + int[] col = new int[9]; + for(int j = 0; j < 9; j++) { + col[j] = grid[j][i].getValue(); + } + if(!isValidSet(col)) { + System.out.println("Column " + i + " is invalid."); + return false; + } + } + + // Check that every 3x3 subgrid contains the numbers 1-9 exactly once. + for(int i = 0; i < 3; i++) { + for(int j = 0; j < 3; j++) { + int[] box = new int[9]; + for(int k = 0; k < 3; k++) { + for(int l = 0; l < 3; l++) { + box[k * 3 + l] = grid[i * 3 + k][j * 3 + l].getValue(); + } + } + if(!isValidSet(box)) { + System.out.println("Box at row " + i + " and column " + j + " is invalid."); + return false; + } + } + } + + return true; + } + + /** + * Given an array of 9 numbers, return true if the array contains the numbers 1-9 exactly once, and false otherwise. + * + * @param set an array of 9 numbers + * @return true if the array contains the numbers 1-9 exactly once, and false otherwise + */ + private boolean isValidSet(int[] set) { + boolean[] found = new boolean[9]; + for(int i = 0; i < 9; i++) { + if(set[i] < 1 || set[i] > 9) { + return false; + } else if(found[set[i] - 1]) { + return false; + } else { + found[set[i] - 1] = true; + } + } + return true; + } +} diff --git a/src/run.ps1 b/src/run.ps1 new file mode 100644 index 0000000..9d21467 --- /dev/null +++ b/src/run.ps1 @@ -0,0 +1,3 @@ +javac *.java +java Sudoku +rm *.class