diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa968c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/bin/ +.classpath +.project diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..e69de29 diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..e69de29 diff --git a/src/App.java b/src/App.java index 065bcc9..594cbba 100644 --- a/src/App.java +++ b/src/App.java @@ -1,29 +1,44 @@ +// GUI imports +import javax.swing.BoxLayout; import javax.swing.JFrame; -import javax.swing.JPanel; import javax.swing.UIManager; - import java.awt.BorderLayout; -import java.awt.Dimension; + +// Project imports +import gui.*; +import gui.backend.*; + +// Unused imports +//import java.awt.event.KeyEvent; +//import java.awt.event.KeyListener; /** - * Runner class for the Sudoku App, managing the GUI and coordinating internal logic. + * Runner class for the Sudoku App, managing the GUI and coordinating + * internal logic. */ public class App extends JFrame { // GUI fields private Nav nav; + private Board board; // Data fields - private Settings settings; - private SudokuChecker checker; + private Settings s; + private SudokuChecker sc; /** * Create a new App object, initializing the GUI and internal logic. */ public App() { super("Sudoku"); - settings = new Settings(); - nav = new Nav(settings, false); + s = new Settings(); initSetup(); + nav = new Nav(s, false); + sc = new SudokuChecker(nav.getLoadedGrid()); + board = new Board(s, nav.getLoadedGrid(), sc); + + nav.setBoard(board); + nav.setChecker(sc); + add(createApp()); } @@ -31,30 +46,43 @@ public class App extends JFrame { * Basic JFrame settings and setup for the GUI. */ private void initSetup() { - // TODO: These are just some default values, these should use the values from Settings. + // TODO: These are just some default values, these should use the + // values from Settings. setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(new BorderLayout()); - setPreferredSize(new Dimension(750, 550)); + setPreferredSize(s.getDimension()); setSize(getPreferredSize()); - setResizable(false); + setResizable(s.getResizable()); setLocationRelativeTo(null); + // Set the look and feel for the app to be cross-platform. try { - UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + UIManager.setLookAndFeel( + UIManager.getCrossPlatformLookAndFeelClassName() + ); } catch (Exception e) { - System.out.println("Error with cross platform look/feel in frameSetup()."); + System.out.println( + "Error with cross platform look/feel in initSetup()." + ); } } /** - * Create the root JPanel holding all other GUI components. + * Create the Root JPanel for the Sudoku App. This will contain all other + * GUI components. * - * @return JPanel + * Settings must be passed since, in the future, the user may be able to + * customize the layout of the app. + * + * @param settings + * @return Root */ - private JPanel createApp() { - // TODO: Create the root JPanel holding all other GUI components. - JPanel root = new JPanel(); + private Root createApp() { + Root root = new Root(s); + root.setLayout(new BoxLayout(root, BoxLayout.Y_AXIS)); root.add(nav); + root.add(board); + return root; } @@ -63,7 +91,7 @@ public class App extends JFrame { */ public static void cli() { Nav nav = new Nav(new Settings(), true); - SudokuChecker check = new SudokuChecker(nav.getLoadedGrid()); + new SudokuChecker(nav.getLoadedGrid()); } /** @@ -74,9 +102,10 @@ public class App extends JFrame { public static void main(String[] args) { if(args.length == 0) new App().setVisible(true); - else if(args[0].equals("-c") || args[0].equals("--cli")) + else if(args[0].equals("-c") || + args[0].equals("--cli")) cli(); else new App().setVisible(true); } -} +} \ No newline at end of file diff --git a/src/Nav.java b/src/Nav.java deleted file mode 100644 index 8bbede3..0000000 --- a/src/Nav.java +++ /dev/null @@ -1,216 +0,0 @@ -import java.util.Scanner; -import java.io.File; -import java.io.FileNotFoundException; - -import javax.swing.JPanel; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JButton; - -/** - * 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() { - super(); - 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(); - - openFile(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, boolean cli) { - super(); - this.s = s; - - if(cli) { - 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(); - - openFile(inputFilename); - } else { - // TODO: Create the GUI for the Nav bar. - createGUI(); - } - } - - /** - * Create the GUI for the Nav bar. - * - * This method is called when the Nav object is created in the App - * class if the program is not being run in the command-line for - * debugging purposes. - */ - private void createGUI() { - JComboBox fileOptions = new JComboBox(); - fileOptions.addItem("New"); - fileOptions.addItem("Open"); - fileOptions.addItem("Save"); - fileOptions.addItem("Exit"); - - JLabel elapsedTime = new JLabel("Elapsed Time: 00:00:00"); - - JButton solve = new JButton("Solve"); - - add(fileOptions); - add(elapsedTime); - add(solve); - } - - /** - * Given the user's selection from the JComboBox, perform the appropriate - * action. - * - * @param String - */ - private void newFile() { - int defaultOption = s.getNewFileProperties(); - - // See Settings.getNewFileProperties for possible values and - // the expected behavior of each value. - switch(defaultOption) { - 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 openFile(String filename) { - File f = new File("resources/" + 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; - } - - createGrid(f); - } - - /** - * Save the currently open .sdku file with any changes the user has made. - * This needs to pull the current state of the grid from the Board JPanel. - */ - private void saveFile() { - // 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) { - String filename = f.getName(); - System.err.println("An error occurred while reading the file " + - filename + ".sdku." - ); - - e.printStackTrace(); - return; - } - } -} diff --git a/src/Settings.java b/src/Settings.java deleted file mode 100644 index 4490228..0000000 --- a/src/Settings.java +++ /dev/null @@ -1,55 +0,0 @@ -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; - - /** - * Create a new Settings object, initializing the default settings - * or reading in the settings from a file if it exists. - */ - 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 deleted file mode 100644 index e23aa91..0000000 --- a/src/SudokuChecker.java +++ /dev/null @@ -1,314 +0,0 @@ -import java.io.File; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Scanner; - - -/** - * The SudokuChecker class is responsible for checking the validity of a given Sudoku puzzle solution. - * - * The SudokuChecker class is also responsible for solving a given Sudoku puzzle. - */ -public class SudokuChecker { - private Cell[][] grid; - - /** - * Create a new SudokuChecker object, initializing the grid to the given 9x9 grid of numbers. - * This should either check each cell as the user inputs a value, or be used to check the validity of a - * puzzle when the user requests it. - * - * TODO: Currently, this class is not used in the GUI. It is only used in the command-line interface. - * @param grid - */ - public SudokuChecker(Cell[][] grid) { - this.grid = grid; - solve(); - 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(); - } - } - - /** - * 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 row - * @param col - * @return an array of numbers that are available to be placed in the given cell - */ - private void getAvailableNumbers(int row, int col) { - ArrayList intersection = intersection(getRowRemainingNumbers(row), getColRemainingNumbers(col)); - intersection = intersection(intersection, getBoxRemainingNumbers(row, col)); - - if(intersection.size() == 0) { - return; - } else if(intersection.size() == 1) { - int value = intersection.get(0); - - grid[row][col].setValue(value); - updatePossibleValues(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 row - * @param col - */ - private void updatePossibleValues(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(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(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. - */ - public void solve() { - // Continue until a valid solution is reached. - while(!isValidSolution()) { - for(int i = 0; i < 9; i++) { - for(int j = 0; j < 9; j++) { - if(grid[i][j].getValue() == 0) { - getAvailableNumbers(i, j); - } - } - } - } - } - - // - - /** - * Get any number between 1 and 9 that is not in the row. - * - * @param row - * @return - */ - private ArrayList getRowRemainingNumbers(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 col - * @return - */ - private ArrayList getColRemainingNumbers(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 box - * @return - */ - private ArrayList getBoxRemainingNumbers(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. - * - * @return true if the grid is a valid Sudoku puzzle, and false otherwise - */ - private boolean isValidSolution() { - // 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/bin/App.class b/src/bin/App.class new file mode 100644 index 0000000..7692a29 Binary files /dev/null and b/src/bin/App.class differ diff --git a/src/bin/gui/Board$1.class b/src/bin/gui/Board$1.class new file mode 100644 index 0000000..27a1656 Binary files /dev/null and b/src/bin/gui/Board$1.class differ diff --git a/src/bin/gui/Board$2.class b/src/bin/gui/Board$2.class new file mode 100644 index 0000000..b372525 Binary files /dev/null and b/src/bin/gui/Board$2.class differ diff --git a/src/bin/gui/Board.class b/src/bin/gui/Board.class new file mode 100644 index 0000000..c3a2388 Binary files /dev/null and b/src/bin/gui/Board.class differ diff --git a/src/bin/gui/CellGUI.class b/src/bin/gui/CellGUI.class new file mode 100644 index 0000000..2e70d97 Binary files /dev/null and b/src/bin/gui/CellGUI.class differ diff --git a/src/bin/gui/ComboBox.class b/src/bin/gui/ComboBox.class new file mode 100644 index 0000000..ed7bc32 Binary files /dev/null and b/src/bin/gui/ComboBox.class differ diff --git a/src/bin/gui/FileChooser$Filter.class b/src/bin/gui/FileChooser$Filter.class new file mode 100644 index 0000000..ea0f5c2 Binary files /dev/null and b/src/bin/gui/FileChooser$Filter.class differ diff --git a/src/bin/gui/FileChooser.class b/src/bin/gui/FileChooser.class new file mode 100644 index 0000000..9c0486e Binary files /dev/null and b/src/bin/gui/FileChooser.class differ diff --git a/src/bin/gui/Label.class b/src/bin/gui/Label.class new file mode 100644 index 0000000..c3a1594 Binary files /dev/null and b/src/bin/gui/Label.class differ diff --git a/src/bin/gui/Nav.class b/src/bin/gui/Nav.class new file mode 100644 index 0000000..e986806 Binary files /dev/null and b/src/bin/gui/Nav.class differ diff --git a/src/bin/gui/Root.class b/src/bin/gui/Root.class new file mode 100644 index 0000000..aa2a216 Binary files /dev/null and b/src/bin/gui/Root.class differ diff --git a/src/bin/gui/backend/Cell$List$Value.class b/src/bin/gui/backend/Cell$List$Value.class new file mode 100644 index 0000000..cad821e Binary files /dev/null and b/src/bin/gui/backend/Cell$List$Value.class differ diff --git a/src/bin/gui/backend/Cell$List.class b/src/bin/gui/backend/Cell$List.class new file mode 100644 index 0000000..a8755a3 Binary files /dev/null and b/src/bin/gui/backend/Cell$List.class differ diff --git a/src/bin/gui/backend/Cell.class b/src/bin/gui/backend/Cell.class new file mode 100644 index 0000000..ccc6f9f Binary files /dev/null and b/src/bin/gui/backend/Cell.class differ diff --git a/src/bin/gui/backend/Settings.class b/src/bin/gui/backend/Settings.class new file mode 100644 index 0000000..36b7744 Binary files /dev/null and b/src/bin/gui/backend/Settings.class differ diff --git a/src/bin/gui/backend/SudokuChecker.class b/src/bin/gui/backend/SudokuChecker.class new file mode 100644 index 0000000..fd6b027 Binary files /dev/null and b/src/bin/gui/backend/SudokuChecker.class differ diff --git a/src/bin/gui/backend/Theme.class b/src/bin/gui/backend/Theme.class new file mode 100644 index 0000000..314e3ba Binary files /dev/null and b/src/bin/gui/backend/Theme.class differ diff --git a/src/gui/Board.java b/src/gui/Board.java new file mode 100644 index 0000000..7823814 --- /dev/null +++ b/src/gui/Board.java @@ -0,0 +1,323 @@ +package gui; + +// GUI imports +import javax.swing.JPanel; +import java.awt.GridLayout; + +// Event & action imports +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +// Processing & backend imports +import gui.backend.Cell; +import gui.backend.Settings; +import gui.backend.SudokuChecker; + +/** + * The Board class represents the Sudoku board in the GUI. + * + * It contains all other related GUI components, and is the main JPanel for + * the board. Accessible information from the Board includes: + * - the current state of the board, including filled values and notes; + * + * TODO: Future features: + * - the current difficulty; + * - the current time since the puzzle was started; + * - the current hints available; and + * - the current number of mistakes. + */ +public class Board extends JPanel { + private Settings s; + private Cell[][] grid; + private Cell[][] solvedGrid; + private CellGUI[][] gridGUI; + private SudokuChecker sc; + private CellGUI selected; + + /** + * Create a new Board object. + * + * Board requires a Settings object for styling, sizing, and for different + * toggleable features. + * + * A 2D Cell array is required to abstract the data away from the Board, + * and the GUI in general. + * + * @param s + * @param grid + */ + public Board(Settings s, Cell[][] grid) { + super(new GridLayout(9, 9)); + this.s = s; + this.grid = grid; + solvedGrid = new SudokuChecker(Cell.copyGrid(grid)).getSolution(); + + style(); + createBoard(); + } + + /** + * Create a new Board object. + * + * Board requires a Settings object for styling, sizing, and for different + * toggleable features. + * + * A 2D Cell array is required to abstract the data away from the Board, + * and the GUI in general. + * + * A SudokuChecker is required if Settings.getAutoCheckValues() is true. + * + * @param s + * @param grid + * @param sc + */ + public Board(Settings s, Cell[][] grid, SudokuChecker sc) { + super(new GridLayout(9, 9)); + this.s = s; + this.grid = grid; + this.sc = sc; + solvedGrid = new SudokuChecker(Cell.copyGrid(grid)).getSolution(); + + style(); + createBoard(); + } + + /** + * Get the grid of the Board. + * + * @return + */ + public Cell[][] getGrid() { + return grid; + } + + /** + * Set the grid of the Board to the given grid. + * + * Intended to be used when loading a new puzzle. + * + * @param grid + */ + public void setGrid(Cell[][] grid) { + this.grid = grid; + gridGUI = null; + selected = null; + + removeAll(); + createBoard(); + repaint(); + revalidate(); + } + + /** + * Set up the Board Panel with the appropriate styling. + */ + private void style() { + setBackground(s.getTheme().getPrimaryBackground()); + setForeground(s.getTheme().getPrimaryText()); + } + + /** + * Create the Board Panel with the appropriate cells. + * + * Every CellGUI object is created with the appropriate Cell object and + * Settings object. The CellGUI objects are then added to the Board Panel. + * + * Each CellGUI object is also given a MouseListener to handle user input, + * and a KeyListener to handle keyboard input if/when the cell is selected. + */ + private void createBoard() { + if(s.getAutoFillNotes()) grid = sc.getPossibleValues(grid); + gridGUI = new CellGUI[9][9]; + + for(int i = 0; i < 9; i++) { + for(int j = 0; j < 9; j++) { + // Create a new CellGUI object with the appropriate Cell object. + CellGUI cell = new CellGUI(grid[i][j], s); + + // Add a MouseListener to the cell. + cell.addMouseListener(createMouseListener(cell)); + + // Add the CellGUI object to the Board Panel. + gridGUI[i][j] = cell; + add(gridGUI[i][j]); + } + } + } + + /** + * Select the given cell and highlight all cells in the same row, column, + * and box. + * + * Intended to be used in conjunction with a MouseListener. + * + * @param cell + */ + private void select(Cell cell) { + // Select the CellGUI objects in the same row and column, but not in + // the same box. + for(int n = 0; n < 9; n++) { + int row = cell.getRow(); + int col = cell.getCol(); + boolean colInSameBox = + (gridGUI[n][col].cell.getBox() == + cell.getBox()); + + boolean rowInSameBox = + (gridGUI[row][n].cell.getBox() == + cell.getBox()); + + // Handling edge cases where the column or row are in the box. + if(colInSameBox && !rowInSameBox) { + gridGUI[row][n].select(); + continue; + } else if(!colInSameBox && rowInSameBox) { + gridGUI[n][col].select(); + continue; + + // Skip the cell if it is in the same box. + } else if(colInSameBox && rowInSameBox) continue; + + gridGUI[n][col].select(); + gridGUI[row][n].select(); + } + + // Select every cell in the box. + int boxRow = cell.getRow() / 3; + int boxCol = cell.getCol() / 3; + for(int n = 0; n < 3; n++) { + for(int m = 0; m < 3; m++) { + int row = boxRow * 3 + n; + int col = boxCol * 3 + m; + + // Skip the user-selected cell for clearer highlighting. + if(row == cell.getRow() && col == cell.getCol()) continue; + + gridGUI[row][col].select(); + } + } + } + + /** + * Create a MouseListener for CellGUI objects. + * + * This MouseListener listens for mouse clicks, and selects the cell + * if it is clicked. If the cell is already selected, it is deselected. + * + * Additionally, adds hover highlighting for the cell. + * + * mouseClicked() is implemented for cell selection functionality. + * mouseEntered() and mouseExited() are implemented for hover functionality. + * Other methods are not implemented. + * + * @param cell + * @return MouseListener + */ + private MouseListener createMouseListener(CellGUI cell) { + return new MouseListener() { + @Override + public void mouseClicked(MouseEvent e) { + // Left click + if(e.getButton() == 1) { + // Deselect the already selected cell. + if(selected != null) select(selected.cell); + + // Deselect the cell if it is already selected. + if(selected == cell) { + selected = null; + return; + } + + // Select the cell and add a KeyListener for input. + select(cell.cell); + selected = cell; + selected.requestFocusInWindow(); + selected.addKeyListener(createKeyListener()); + + // Right click + } else if(e.getButton() == 3) { + cell.setNoteMode(); + return; + } + } + @Override + public void mousePressed(MouseEvent e) {} + @Override + public void mouseReleased(MouseEvent e) {} + @Override + public void mouseEntered(MouseEvent e) { + cell.select(); + } + @Override + public void mouseExited(MouseEvent e) { + cell.select(); + } + }; + } + + /** + * Create a KeyListener for CellGUI objects. + * + * This KeyListener listens for key releases, and updates the + * selected cell with additional possible values or a new value. + * + * Only keyReleased is implemented, as keyTyped does not allow for + * certain features regarding key codes. + * + * @return KeyListener + */ + private KeyListener createKeyListener() { + return new KeyListener() { + @Override + public void keyTyped(KeyEvent e) {} + + @Override + public void keyPressed(KeyEvent e) {} + + @Override + public void keyReleased(KeyEvent e) { + // If a CellGUI is not selected, do nothing. + if(selected == null) return; + + char key = e.getKeyChar(); + Cell c = selected.cell; + + // If the Cell is an initial value read from the starting file + // then skip it. + if(c.isInitValue()) return; + + // Backspace support for removing values. + if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) { + selected.removeValue(); + selected.repaint(); + selected.revalidate(); + return; + } + + // Only allow digits to be entered. + if(!Character.isDigit(key)) return; + + // Enter the value from the keyboard. + if(!selected.isInNotesMode()) { + + // Set the value of the cell and check if it is correct. + if(s.getAutoCheckValues()) { + int row = c.getRow(); + int col = c.getCol(); + int expected = solvedGrid[row][col].getValue(); + selected.setValue(Character.getNumericValue(key), expected); + + // Set the value of the cell. + } else selected.setValue(Character.getNumericValue(key)); + + // Add the possible value to the cell. + } else selected.addPossibleValue(Character.getNumericValue(key)); + + selected.repaint(); + selected.revalidate(); + } + }; + } +} \ No newline at end of file diff --git a/src/gui/CellGUI.java b/src/gui/CellGUI.java new file mode 100644 index 0000000..2c30d5a --- /dev/null +++ b/src/gui/CellGUI.java @@ -0,0 +1,316 @@ +package gui; + +// GUI imports +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.LineBorder; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridLayout; + +// Processing & backend imports +import gui.backend.Cell; +import gui.backend.Settings; +import gui.backend.Theme; + +/** + * GUI representation of a single cell in the Sudoku grid. + */ +class CellGUI extends JPanel { + // GUI fields + private GridLayout noteLayout = new GridLayout(3, 3); + private GridLayout valueLayout = new GridLayout(0, 1); + private JPanel internalPanel = new JPanel(valueLayout); + private JLabel valueLabel; + private Label[] notesLabels = new Label[9]; + + // GUI data fields read from a Settings object. + private Dimension size; + private Theme theme; + private Font font; + + // Data fields + Cell cell; + private boolean noteMode; // true to display notes, or display value + private boolean selected = false; // true if the cell is selected + private boolean incorrect = false; // true if the cell has an invalid value + + /** + * Create a new CellGUI object with the given single cell. + * + * Start blank cells in notes mode if startInNotesOrValueMode is true. + * + * @param cell + * @param startInNotesOrValueMode + */ + public CellGUI(Cell cell, Settings s) { + super(); + this.cell = cell; + + // Set the initial state of the cell based off the settings. + noteMode = s.getCellGUIStartMode(); + theme = s.getTheme(); + font = s.getFont(); + selected = false; + size = s.getCellDimensions(); + + // Set the layout and size of the cell. + setFocusable(true); + setLayout(valueLayout); + valueLabel = new Label("", theme, font); + setSize(size); + internalPanel.setSize(size); + defaultStyle(); + + // Populate the cell with the appropriate value or notes. + if(cell.getValue() == 0) { + internalPanel.setLayout(noteLayout); + generateNotes(true); + } else { + internalPanel.setLayout(valueLayout); + valueLabel.setText(Integer.toString(cell.getValue())); + internalPanel.add(valueLabel); + } + add(internalPanel); + } + + /** + * Get the value of the underlying Cell object. + * + * @return int + */ + public int getValue() { + return cell.getValue(); + } + + /** + * Set the value of the underlying Cell object. + * + * This implementation is to be used when Settings.getAutoCheckValues + * is false. It will assume that every value the user enters is incorrect, + * and thus the underlying List of possible values is never cleared. + * + * @param value + */ + public void setValue(int value) { + if(!selected) return; + + cell.setValue(value, false); + // Update the GUI with the actual Cell value (unchanged if invalid) + valueLabel.setText(Integer.toString(cell.getValue())); + refresh(); + } + + /** + * Set the value of the underlying Cell object. + * + * This implementation is to be used when Settings.getAutoCheckValues + * is true. It allows for incorrect values to be highlighted. And the + * underlying List of possible values to be cleared if the entered + * value is correct. + * + * @param value + */ + public void setValue(int value, int expected) { + if(!selected) return; + + cell.setValue(value, value == expected); + valueLabel.setText(Integer.toString(cell.getValue())); + + if(value != expected) { + incorrect = true; + valueLabel.setText(Integer.toString(value)); + errorStyle(); + } else refresh(); + } + + /** + * Remove the value of the underlying Cell object. + */ + public void removeValue() { + if(!selected) return; + + cell.setValue(0, false); + if(!cell.isInitValue()) + valueLabel.setText(""); + + incorrect = false; + highlightedStyle(); + } + + /** + * Add a possible value to the cell. + * + * @param value + */ + public void addPossibleValue(int value) { + if(!selected) return; + + cell.addPossibleValue(value); + internalPanel.removeAll(); + internalPanel.setLayout(noteLayout); + noteMode = true; + generateNotes(true); + highlightedStyle(); + } + + /** + * Remove a possible value from the cell. + * + * @return + */ + public void removePossibleValue(int value) { + cell.removePossibleValue(value); + } + + /** + * Get the status of the cell in notes mode. + * + * @return + */ + public boolean isInNotesMode() { + return noteMode; + } + + /** + * Toggle the mode of the cell between value and note, including the + * layout and visual content of the cell. + */ + public void setNoteMode() { + if(cell.isInitValue()) return; + + if(noteMode) { + internalPanel.removeAll(); + internalPanel.setLayout(valueLayout); + internalPanel.add(valueLabel); + } else { + internalPanel.setLayout(noteLayout); + generateNotes(true); + } + + refresh(); + noteMode = !noteMode; + } + + /** + * Select the cell, and style it for error, highlight, or default. + */ + public void select() { + selected = !selected; + if(incorrect) { + errorStyle(); + return; + } else if(!selected) + defaultStyle(); + else highlightedStyle(); + } + + /** + * Style the CellGUI with the primary colors from the Theme. + * + * @see Theme.java + */ + private void defaultStyle() { + setBackground(theme.getPrimaryBackground()); + setForeground(theme.getPrimaryText()); + internalPanel.setBackground(theme.getPrimaryBackground()); + internalPanel.setForeground(theme.getPrimaryText()); + valueLabel.setForeground(theme.getPrimaryText()); + setBorder(new LineBorder(theme.getPrimaryBorder(), 2)); + + for(int i = 0; i < 9; i++) { + if(notesLabels[i] != null) { + notesLabels[i].setBackground(theme.getPrimaryBackground()); + notesLabels[i].setForeground(theme.getPrimaryText()); + } + } + + refresh(); + } + + /** + * Style the CellGUI with the secondary colors from the Theme. + * + * @see Theme.java + */ + private void highlightedStyle() { + setBackground(theme.getSecondaryBackground()); + setForeground(theme.getSecondaryText()); + internalPanel.setBackground(theme.getSecondaryBackground()); + internalPanel.setForeground(theme.getSecondaryText()); + valueLabel.setForeground(theme.getSecondaryText()); + setBorder(new LineBorder(theme.getSecondaryBorder(), 2)); + + for(int i = 0; i < 9; i++) { + if(notesLabels[i] != null) { + notesLabels[i].setBackground(theme.getSecondaryBackground()); + notesLabels[i].setForeground(theme.getSecondaryText()); + } + } + + refresh(); + } + + /** + * Style the CellGUI with the error colors from the Theme. + * + * @see Theme.java + */ + private void errorStyle() { + valueLabel.setForeground(theme.getErrorText()); + internalPanel.setBackground(theme.getErrorBackground()); + setBorder(new LineBorder(theme.getErrorBorder(), 2)); + + refresh(); + } + + /** + * Helper method to repaint and revalidate the internal panel and CellGUI. + */ + private void refresh() { + internalPanel.repaint(); + internalPanel.revalidate(); + repaint(); + revalidate(); + } + + /** + * Create the GUI components for the cell's notes. + * + * If autoFill is true, then the CellGUI will automatically switch to + * noteMode. + * + * @param autoFill + */ + private void generateNotes(boolean autoFill) { + int[] possibleValues = cell.getPossibleValues(); + + // Prepare the internalPanel for noteMode. + if(autoFill) internalPanel.removeAll(); + + // Handle cases where there are no possible values stored. + if(possibleValues.length == 0) { + for(int i = 0; i < 9; i++) { + notesLabels[i] = new Label("", theme, font); + + if(autoFill) internalPanel.add(notesLabels[i]); + } + return; + } + + int index = 0; + // Add the noted possible values to the cell. + for(int i = 1; i <= 9 && index < possibleValues.length; i++) { + // Add the possible value if it exists. + if(possibleValues[index] == i) { + notesLabels[i - 1] = new Label(Integer.toString(i), theme, font); + index++; + + // Otherwise, add a blank value. + } else + notesLabels[i - 1] = new Label("", theme, font); + + if(autoFill) internalPanel.add(notesLabels[i - 1]); + } + } +} \ No newline at end of file diff --git a/src/gui/ComboBox.java b/src/gui/ComboBox.java new file mode 100644 index 0000000..75d4d9d --- /dev/null +++ b/src/gui/ComboBox.java @@ -0,0 +1,39 @@ +package gui; + +import java.awt.Font; + +import javax.swing.JComboBox; + +import gui.backend.Settings; +import gui.backend.Theme; + +/** + * A custom JComboBox with the appropriate styling for the Sudoku app. + */ +public class ComboBox extends JComboBox { + private Theme theme; + private Font font; + + /** + * Create a new ComboBox with the default styling and without initial items. + */ + public ComboBox() { + super(); + } + + public ComboBox(Settings s) { + super(); + theme = s.getTheme(); + font = s.getFont(); + style(); + } + + /** + * Set up the ComboBox with the appropriate styling. + */ + private void style() { + setFont(font); + setBackground(theme.getPrimaryBackground()); + setForeground(theme.getPrimaryText()); + } +} diff --git a/src/gui/FileChooser.java b/src/gui/FileChooser.java new file mode 100644 index 0000000..d14cb62 --- /dev/null +++ b/src/gui/FileChooser.java @@ -0,0 +1,66 @@ +package gui; + +// GUI imports +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; + +// Processing & backend imports +import java.io.File; + +/** + * A custom FileChooser with the appropriate styling for the Sudoku app. + * Additionally, this FileChooser only allows .sdku files to be selected. + */ +public class FileChooser extends JFileChooser { + /** + * Create a new FileChooser with the default directory set to the user's + * Sudoku directory. + * + * @param defaultDirectory + * @param s + */ + public FileChooser(File defaultDirectory) { + super(defaultDirectory); + + for (File f: defaultDirectory.listFiles()) { + ensureFileIsVisible(f); + } + + style(); + setupSettings(); + } + + /** + * Set up the FileChooser with the appropriate styling. + */ + private void style() { + // TODO: Style the FileChooser. + } + + /** + * Set up the FileChooser with the appropriate settings. + */ + private void setupSettings() { + setFileHidingEnabled(false); + setFileSelectionMode(JFileChooser.FILES_ONLY); + setFileFilter(new Filter()); + } + + /** + * A custom FileFilter to only allow .sdku files to be selected. + */ + private class Filter extends FileFilter { + public boolean accept(File f) { + String name = f.getName().toLowerCase(); + if(name.length() > 5) { + return name.substring(name.length() - 5).equals(".sdku") + || !f.isDirectory(); + } + return false; + } + + public String getDescription() { + return "Sudoku Puzzle Files (*.sdku)"; + } + } +} \ No newline at end of file diff --git a/src/gui/Label.java b/src/gui/Label.java new file mode 100644 index 0000000..bf1a587 --- /dev/null +++ b/src/gui/Label.java @@ -0,0 +1,41 @@ +package gui; + +import java.awt.Font; + +import javax.swing.JLabel; +import javax.swing.SwingConstants; + +import gui.backend.Theme; + +/** + * The Note class represents a single note of a cell in the Sudoku board. + * + * This is the GUI abstraction for the underlying Cell object, and is used to + * display and manage the note or value for a cell. + */ +public class Label extends JLabel { + private Theme t; + private Font f; + + /** + * Create a new Note object with the given text and theme. + * + * @param text + * @param t + */ + public Label(String text, Theme t, Font f) { + super(text, SwingConstants.CENTER); + this.t = t; + this.f = f; + style(); + } + + /** + * Style the Note component with the current theme. + */ + private void style() { + setBackground(t.getPrimaryBackground()); + setForeground(t.getPrimaryText()); + setFont(f); + } +} diff --git a/src/gui/Nav.java b/src/gui/Nav.java new file mode 100644 index 0000000..db9d466 --- /dev/null +++ b/src/gui/Nav.java @@ -0,0 +1,413 @@ +package gui; + +// File IO imports +import java.util.Scanner; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +// GUI imports +import javax.swing.JPanel; + +// Processing & backend imports +import gui.backend.Cell; +import gui.backend.Settings; +import gui.backend.SudokuChecker; + +// Unused imports +//import javax.swing.JLabel; +//import javax.swing.JButton; + +/** + * 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: + * - open an existing .sdku file in the user's file system (a standard + * or default location is TBD); + * + * NOTE: Needed features + * - default open state from Settings + * - creating a new, completely empty .sdku file for a user to create + * their own puzzle with; + * - save the currently open .sdku file with any changes the user has made; + * + * NOTE: Future features + * - auto-save + * - display elapsed time; + * - open a settings GUI interface; + * - display the number of mistakes made; + * - display how many hints have been used; + * - button to globally enable noteMode in CellGUI objects contained in + * the Board; and + * - toggleable number pad instead of using keyboard inputs. + */ +public class Nav extends JPanel { + @SuppressWarnings("unused") + private SudokuChecker sc; + + private Settings s; + + private File f; + private Board b; + 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 not be used since default + * file I/O behavior is dictated by the values in the Settings object. + * + * @Deprecated + */ + public Nav() { + super(); + 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(); + + openFile(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, boolean cli) { + super(); + this.s = s; + + // If the program is being run in the command-line, prompt the user + // for the name of the input file. + if(cli) { + 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(); + + openFile(inputFilename); + + // Otherwise, create the GUI for the Nav bar. + } else { + createGUI(); + + // TODO: Need to implement the default file options in Settings. + switch(s.getDefaultOpenState()) { + case 0: + openFile("input"); + break; + case 1: + openFile("medium"); + break; + case 2: + openFile("hard"); + break; + default: + System.out.println("Invalid default file option."); + } + } + } + + /** + * Set the Board object for the Nav bar. + * + * This is necessary for the Nav bar to be able to interact with the + * Board object and update the grid with file I/O operations. This + * shouldn't be necessary to change once the program is running. + * + * @param b + */ + public void setBoard(Board b) { + this.b = b; + } + + /** + * Set the SudokuChecker object for the Nav bar. + * + * This is necessary for the Nav bar to be able to interact with the + * SudokuChecker object for the solve button. + * + * In the future, the SudokuChecker object is also used for hints. + * + * This shouldn't be necessary to change once the program is running. + */ + public void setChecker(SudokuChecker sc) { + this.sc = sc; + } + + /** + * Style the Nav with the primary colors from Theme. + */ + private void style() { + setBackground(s.getTheme().getPrimaryBackground()); + setForeground(s.getTheme().getPrimaryText()); + } + + /** + * Create the GUI for the Nav bar. + * + * This method is called when the Nav object is created in the App + * class and if the program is not being run in the command-line for + * debugging purposes. + */ + private void createGUI() { + style(); + + /* + // TODO: Make custom Label class. + JLabel elapsedTime = new JLabel("Elapsed Time: 00:00:00"); + elapsedTime.setFont(s.getFont()); + + // TODO: Make custom Button class. + JButton solve = new JButton("Solve"); + solve.setFont(s.getFont()); + */ + + // Add the ComboBox for the file options, the elapsed time label, and + // the solve button to the Nav Panel. + add(createFileOptions()); + //add(elapsedTime); + //add(solve); + } + + /** + * Create the ComboBox for the file options. + * + * @return ComboBox + */ + private ComboBox createFileOptions() { + // Create a new ComboBox with the default styling. + ComboBox fileOptions = new ComboBox<>(s); + fileOptions.addItem("New"); + fileOptions.addItem("Open"); + fileOptions.addItem("Save"); + fileOptions.addItem("Save As"); + fileOptions.addItem("Exit"); + + fileOptions.addActionListener(e -> { + int selected = fileOptions.getSelectedIndex(); + switch(selected) { + case 0: + newFile(); + break; + case 1: + openFile(); + break; + case 2: + saveFile(false); + break; + case 3: + saveFile(true); + break; + case 4: + System.exit(0); + break; + default: + System.out.println("Invalid selection."); + } + }); + return fileOptions; + } + + /** + * Given the user's selection from the ComboBox, perform the appropriate + * action. + * + * See Settings.getNewFileProperties() for more information on the expected + * actions. + * + * @see Settings.java + */ + private void newFile() { + int defaultOption = s.getNewFileProperties(); + + switch(defaultOption) { + case 0: + f = new File("new.sdku"); + try { + f.createNewFile(); + + // Create a new, blank .sdku file. + for(int i = 0; i < 9; i++) + for(int j = 0; j < 9; j++) + grid[i][j] = new Cell(i, j, 0); + + // Update the Board with the new, blank .sdku puzzle. + b.setGrid(grid); + + // Save the file if auto-save is on. + if(s.getAutoSave()) saveFile(true); + } catch (IOException e1) { + System.err.println("Unable to create new file " + f.getName()); + // TODO: Add a dialog box GUI to notify. + + // Open the default, pre-downloaded default.sdku file. + openFile(s.getDefaultDirectory() + "default"); + } + case 1: + case 2: + default: + } + } + + /** + * Open an existing .sdku file in the user's file system, selected using + * a GUI. + * + * Create the Grid and update the Board with the loaded Grid. + */ + private void openFile() { + // No error handling is needed here, since the Settings class ensures + // that the default directory is always a valid directory. + File defaultDirectory = new File(s.getDefaultDirectory()); + + // Custom FileChooser class for style and to ensure the user can only + // select .sdku files. + FileChooser fc = new FileChooser(defaultDirectory); + int result = fc.showOpenDialog(this); + + // If the user selects a file, open it. + if(result == FileChooser.APPROVE_OPTION) { + f = fc.getSelectedFile(); + createGrid(f); + } + b.setGrid(grid); + } + + /** + * Given an input filename, open the file and populate the grid. + * + * This implementation is for use only in the command-line interface. + * + * @param String + */ + private void openFile(String filename) { + //file path is not working for everyone had to append src/ to run + File f = new File("resources/" + filename + ".sdku"); + // If the file does not exist, print an error message and return. + 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() + ); + + grid = new Cell[9][9]; + for(int i = 0; i < 9; i++) + for(int j = 0; j < 9; j++) + grid[i][j] = new Cell(i, j, 0); + + return; + } + + // Otherwise, open the file and populate the grid. + createGrid(f); + } + + /** + * Save the currently open .sdku file with any changes the user has made. + * This needs to pull the current state of the grid from the Board JPanel. + * + * If called from 'Save' option, then saveAs should be false. + * If called from 'Save As' option, then saveAs should be true. + * + * 'Save' will simply overwrite the existing file that was opened, + * while 'Save As' while prompt the user to select or create a file. + * + * 'Save As' functionality is the default behavior for new .sdku files + * created. Therefore, if the filename is 'new.sdku', then saveAs will + * be made true. + * + * @param boolean + */ + private void saveFile(boolean saveAs) { + if(f.getName().equals("new.sdku")) + saveAs = true; + // 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. + * + * If the user has enabled auto-save in Settings, then save the file. + * + * 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; + if(s.getAutoSave()) saveFile(true); + } + + /** + * 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)) { + // Create a new 9x9 grid of Cells. + Cell[][] grid = new Cell[9][9]; + + // Read in the file line by line. + for(int i = 0; i < 9; i++) { + + // Read in the line and create a new Cell for each character. + String line = in.nextLine(); + for(int j = 0; j < 9; j++) { + + // If the character is a period, the cell is empty. + if(line.charAt(j) == '.') { + grid[i][j] = new Cell(i, j, 0); + continue; + } + + // Otherwise, the character is a digit. + int value = Character.getNumericValue(line.charAt(j)); + grid[i][j] = new Cell(i, j, value); + } + } + + this.grid = grid; + + // If the file is not found, print an error message and return. + } catch (FileNotFoundException e) { + newFile(); + return; + } + } +} diff --git a/src/gui/Root.java b/src/gui/Root.java new file mode 100644 index 0000000..2f80f41 --- /dev/null +++ b/src/gui/Root.java @@ -0,0 +1,36 @@ +package gui; + +import javax.swing.JPanel; + +import gui.backend.Settings; +import gui.backend.Theme; + +/** + * The Root class represents the root JPanel for the Sudoku app. + * + * It contains all other GUI components, and is the main JPanel for the app. + * To create a new Root object, a Settings object must be passed in for + * styling purposes. + */ +public class Root extends JPanel { + private Theme theme; + + /** + * Create a new Root object with the default styling from settings. + * + * @param s + */ + public Root(Settings settings) { + super(); + this.theme = settings.getTheme(); + style(); + } + + /** + * Set up the Root Panel with the appropriate styling. + */ + private void style() { + setBackground(theme.getPrimaryBackground()); + setForeground(theme.getPrimaryText()); + } +} diff --git a/src/Cell.java b/src/gui/backend/Cell.java similarity index 84% rename from src/Cell.java rename to src/gui/backend/Cell.java index 0f242cc..040ed19 100644 --- a/src/Cell.java +++ b/src/gui/backend/Cell.java @@ -1,3 +1,4 @@ +package gui.backend; /** * 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. @@ -10,6 +11,8 @@ public class Cell { private int col; private int box; private int value; + private boolean initValue; + private List possibleValues; /** @@ -40,6 +43,8 @@ public class Cell { this.row = row; this.col = col; this.value = value; + initValue = value != 0; + setBox(); possibleValues = new List(); } @@ -65,10 +70,16 @@ public class Cell { * * The value must be between 1 and 9, inclusive. * + * Additionally if the value is incorrect, then do not clear the + * possible values. + * * @param value + * @param isCorrect */ - public void setValue(int value) { - possibleValues.clear(); + public void setValue(int value, boolean isCorrect) { + if(initValue) return; + if(isCorrect) possibleValues.clear(); + this.value = value; } @@ -81,6 +92,24 @@ public class Cell { return value; } + /** + * Get the row of the cell. + * + * @return row number + */ + public int getRow() { + return row; + } + + /** + * Get the column of the cell. + * + * @return column number + */ + public int getCol() { + return col; + } + /** * Get the box a cell is in. * @@ -98,7 +127,10 @@ public class Cell { * @param value */ public void addPossibleValue(int value) { + if(initValue) return; + if(possibleValues.contains(value)) { + removePossibleValue(value); return; } else if(value < 1 || value > 9) { return; @@ -113,6 +145,8 @@ public class Cell { * @param possibleValues */ public void setPossibleValues(int[] possibleValues) { + if(initValue) return; + this.possibleValues = new List(possibleValues); } @@ -132,14 +166,37 @@ public class Cell { /** * 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. + * 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(); } - + + /** + * Check if the cell has an initial value. + * + * An initial value is a value that is set when the cell is created, + * and cannot be changed. + * @return + */ + public boolean isInitValue() { + return initValue; + } + + public static Cell[][] copyGrid(Cell[][] grid) { + Cell[][] newGrid = new Cell[9][9]; + for(int row = 0; row < 9; row++) { + for(int col = 0; col < 9; col++) { + newGrid[row][col] = new Cell(row, col, grid[row][col].getValue()); + } + } + + return newGrid; + } + /** * Given the cell's row and column, set the box number for the cell. */ @@ -150,10 +207,12 @@ public class Cell { } /** - * 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. + * 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. + * 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 { @@ -175,20 +234,6 @@ public class Cell { 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. diff --git a/src/gui/backend/Settings.java b/src/gui/backend/Settings.java new file mode 100644 index 0000000..7831755 --- /dev/null +++ b/src/gui/backend/Settings.java @@ -0,0 +1,543 @@ +package gui.backend; + +import java.io.File; +import java.io.IOException; + +import java.awt.GraphicsEnvironment; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontFormatException; + +/** + * A class solely to store user settings relating to default behaviors, + * difficulty, filepaths, and more. + * + * These user settings are stored in a settings file that must be stored + * either in "~/.config/sudoku/settings.json" or + * "%APPDATA%/Local/Sudoku/settings.json" depending on the user's operating + * system. + * + * (Currently) Configurable settings include: + * - Default puzzles directory for .sdku files; + * - New file Properties; + * - Font; + * - Window size; + * - Window resizing (experimental); + * - Cell start mode, meaning if a cell is blank and selected, input + * is either going to the notes or entering a value; + * - Application default open behavior; + * - Theme, specifically by providing a default.theme file formatted for JSON; + * - Auto-fill notes with possible values; + * - Auto-check user-entered values with the correct values; + * - Auto-save and auto-save frequency; + */ +public class Settings { + private final String os; + + // The directory where the settings file is located. + private String appDirectory; + + // The default directory for .sdku files. + private String defaultDirectory; + + // The properties for creating a new .sdku file. + private int newFileProperties; + + // The font to be used throughout the application. + private Font font; + + // The default dimension of the application window. + private Dimension dimension; + + // The default dimension of the cells in the application window. + private Dimension cellDimension; + + // Resizable setting + private boolean resizable; + + // Cell GUI Start Mode + private boolean cellGUIStartMode; + + // Default open state for the application. + private int defaultOpenState; + + // Theme object storing the colors for the GUI components. + private Theme theme; + + // Auto-fill notes + private boolean autoFillNotes; + + // Auto check values + private boolean autoCheckValues; + + // Auto-save whenever a new note or value is changed. + private boolean autoSave; + + // Auto-save frequency + private int autoSaveFrequency; + + /** + * Create a new Settings object, initializing the default settings + * or reading in the settings from settings file if it exists. + * + * While the defaultDirectory can be modified, it will not change where + * the settings file is located. The settings file is always located in + * either "~/.config/sudoku/" or "%APPDATA%/Local/Sudoku/", depending + * on the user's operating system. + * + * The settings file is settings.json. + */ + public Settings() { + os = System.getProperty("os.name").toLowerCase(); + if(os.startsWith("windows")) + appDirectory = System.getProperty("user.home") + + "\\AppData\\Local\\Sudoku\\"; + else + appDirectory = System.getProperty("user.home") + + "/.config/sudoku/"; + + File dir = new File(appDirectory); + + // If the app directory does not exist, create it and populate a + // settings.json file with the default settings. + if (!dir.exists() || !dir.isDirectory()) { + dir.mkdirs(); + defaultSettings(); + updateSettingsFile(); + + } else readSettings(); + } + + /** + * Set the default settings for the application and write them to the + * settings file. + */ + private void defaultSettings() { + // Setup the default directory for .sdku puzzle files. + defaultDirectory = appDirectory + "puzzles"; + File dir = new File(defaultDirectory); + + // Create the puzzles directory. + if(!dir.exists() || !dir.isDirectory()) + dir.mkdirs(); + + // Load the font from the system directory. + if(loadFont("HackNerdFont-Regular") == 0) + font = new Font("Hack Nerd Font", Font.PLAIN, 16); + + // If the font does not exist, use Arial as the default font. + else + font = new Font("Arial", Font.PLAIN, 16); + + // Set other default settings. + newFileProperties = 0; + dimension = new Dimension(600, 800); + resizable = false; + cellGUIStartMode = true; + defaultOpenState = 0; + theme = new Theme(new File(appDirectory + "default.theme")); + autoFillNotes = false; + autoCheckValues = true; + autoSave = false; + autoSaveFrequency = 0; + setCellDimensions(); + + // Write the default settings to the settings.json file. + updateSettingsFile(); + } + + /** + * Open and write the settings to the settings file. + * + * This method is called whenever a setting is updated. + */ + private void updateSettingsFile() { + // TODO: Write the settings to the settings file. + } + + /** + * Read the settings from the settings file. + * + * If the settings file does not exist, the default settings will be used + * to populate the Settings file. + */ + private void readSettings() { + File settingsFile = new File(appDirectory + "/settings.json"); + if (!settingsFile.exists() && !settingsFile.isDirectory()) { + defaultSettings(); + return; + } + + // TODO: Read the settings from the settings file. + } + + /** + * Load the font from the resources folder in the user's system directory. + * + * @return int 0 if successful, 1 if the file does not exist. + */ + private int loadFont(String fontName) { + // Load the font from the system directory. + String fontFilepath; + if(os.startsWith("windows")) + fontFilepath = System.getProperty("user.home") + + "\\AppData\\Local\\Microsoft\\Windows\\Fonts\\" + + fontName + ".ttf"; + else if(os.startsWith("mac")) + fontFilepath = "/Library/Fonts/" + fontName + ".ttf"; + else { + fontName = "HackNerdFontMono-Regular"; + fontFilepath = "/usr/share/fonts/TTF/" + fontName + ".ttf"; + } + + // Register the font with the GraphicsEnvironment. + try { + GraphicsEnvironment ge = GraphicsEnvironment. + getLocalGraphicsEnvironment(); + + ge.registerFont(Font.createFont( + Font.TRUETYPE_FONT, + new File(fontFilepath) + )); + + return 0; + + } catch(IOException | FontFormatException e) { + System.out.println("Filepath of font not found: " + + fontFilepath + " does not exist."); + return 1; + } + } + + /** + * 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 default directory is either "~/.config/sudoku/puzzles" + * or "%APPDATA%/Local/Sudoku/Puzzles", depending on the user's operating + * system. + * + * @return String + */ + public String getDefaultDirectory() { + return defaultDirectory; + } + + /** + * Set the default directory and write it to the settings file. + * + * @param defaultDirectory + */ + public void setDefaultDirectory(String defaultDirectory) { + this.defaultDirectory = defaultDirectory; + updateSettingsFile(); + } + + /** + * New File Properties is a configurable setting, where the following + * values are allowable: + * 0 - Create a new, blank .sdku file + * 1 - Create a new, random .sdku file + * + * Integer is being used instead of boolean since additional properties + * may be added in the future. + * + * @return int + */ + public int getNewFileProperties() { + return newFileProperties; + } + + /** + * Set the new file properties and write them to the settings file. + * + * @param newFileProperties + */ + public void setNewFileProperties(int newFileProperties) { + this.newFileProperties = newFileProperties; + updateSettingsFile(); + } + + /** + * The Font is a configurable setting, where the user can specify a + * font to be used throughout the application. + * + * Fonts are loaded from the system directory. In Windows, the font + * is loaded from C:\Windows\Fonts\. In MacOS, the font is loaded + * from /Library/Fonts/. In Linux, the font is loaded from + * /usr/share/fonts/. + * + * @return Font + */ + public Font getFont() { + return font; + } + + /** + * Set the font and write it to the settings file. + * + * @param font + */ + public void setFont(Font font) { + this.font = font; + updateSettingsFile(); + } + + /** + * The default dimension of the application window. + * + * The user can specify both the starting width and height of the app, + * and the app will remember the last used dimensions as well. + * + * @return Dimension + */ + public Dimension getDimension() { + return dimension; + } + + /** + * Set the default dimension and write it to the settings file. + * + * @param dimension + */ + public void setDimension(Dimension dimension) { + this.dimension = dimension; + updateSettingsFile(); + } + + /** + * The resizable setting for the application window. + * + * By default, the application window is not resizable and is considered + * an experimental feature. Dynamic layout is not yet supported. + * + * @return boolean + */ + public boolean getResizable() { + return resizable; + } + + /** + * Set the resizable setting and write it to the settings file. + * + * @param resizable + */ + public void setResizable(boolean resizable) { + this.resizable = resizable; + updateSettingsFile(); + } + + /** + * The Cell GUI Start Mode is a configurable setting, where the user can + * specify whether the cell GUI should start in notes mode or value mode. + * + * By default, the cell GUI starts in value mode, ie. returns false. + * Set to true to start in notes mode. + * + * @return boolean + */ + public boolean getCellGUIStartMode() { + return cellGUIStartMode; + } + + /** + * Set the cell GUI start mode and write it to the settings file. + * + * @param cellGUIStartMode + */ + public void setCellGUIStartMode(boolean cellGUIStartMode) { + this.cellGUIStartMode = cellGUIStartMode; + updateSettingsFile(); + } + + /** + * The default open state for the application. The following values + * are accepted: + * 0 - Open the default easy.sdku file, default behavior; + * 1 - Open the last opened .sdku file; + * 2 - Open a new, blank .sdku file; and + * 3 - Open a new, random .sdku file. + * + * NOTE: The value 2 is not yet supported. + * + * @return + */ + public int getDefaultOpenState() { + return defaultOpenState; + } + + /** + * Set the default open state for the application and write it to the + * settings file. + * + * @param defaultOpenState + */ + public void setDefaultOpenState(int defaultOpenState) { + this.defaultOpenState = defaultOpenState; + updateSettingsFile(); + } + + /** + * The Theme object stores the colors for the GUI components. + * + * The available colors for customization are: + * - primaryBackground + * - secondaryBackground + * - primaryText + * - secondaryText + * - primaryHighlight + * - secondaryHighlight + * - primaryBorder + * - secondaryBorder + * - primaryButton + * - secondaryButton + * - primaryButtonHighlight + * - secondaryButtonHighlight + * - primaryButtonBorder + * - secondaryButtonBorder + * + * @see Theme + * @return + */ + public Theme getTheme() { + return theme; + } + + /** + * Set the theme and write it to the settings file. + * + * @param theme + */ + public void setTheme(Theme theme) { + this.theme = theme; + updateSettingsFile(); + } + + /** + * The Auto-fill notes setting is a configurable setting, where the user + * can specify whether notes should be automatically filled in when a + * value is placed in a cell. + * + * By default, the auto-fill notes setting is set to false. + * + * @return boolean + */ + public boolean getAutoFillNotes() { + return autoFillNotes; + } + + /** + * Set the auto-fill notes setting and write it to the settings file. + * + * @param autoFillNotes + */ + public void setAutoFillNotes(boolean autoFillNotes) { + this.autoFillNotes = autoFillNotes; + setCellGUIStartMode(autoFillNotes); + updateSettingsFile(); + } + + /** + * The Auto-check values setting is a configurable setting, where the user + * can specify whether values should be automatically checked when placed + * in a cell. + * + * By default, the auto-check values setting is set to false. + * + * @return + */ + public boolean getAutoCheckValues() { + return autoCheckValues; + } + + /** + * Set the auto-check values setting and write it to the settings file. + * + * @param autoCheckValues + */ + public void setAutoCheckValues(boolean autoCheckValues) { + this.autoCheckValues = autoCheckValues; + updateSettingsFile(); + } + + /** + * The default dimensions of the cells in the application window. + * + * The default dimensions are set to 50x50 pixels. + * + * @return + */ + public Dimension getCellDimensions() { + return cellDimension; + } + + private void setCellDimensions() { + int height; + int width; + + if(dimension.height < 500) height = 50; + else height = dimension.height / 10; + + if(dimension.width < 500) width = 50; + else width = dimension.width / 10; + + cellDimension = new Dimension(width, height); + } + + /** + * Auto-save is a user-configurable setting to automatically save the + * puzzle anytime they make a change. Additionally, auto-save will + * result in the file being automatically saved every 60 seconds, to + * preserve the elapsed time tracking. + * + * @return boolean + */ + public boolean getAutoSave() { + return autoSave; + } + + /** + * Set the auto-save feature to a specified state. + * + * @param autoSave + */ + public void setAutoSave(boolean autoSave) { + this.autoSave = autoSave; + updateSettingsFile(); + } + + /** + * The auto save frequnecy, in seconds. + * + * By default, auto-save will save whenever the user makes changes on the + * puzzle. However, since elapsed time and other future features are not + * user-controlled events, the frequency is used to specify in a separate + * thread how often the .sdku file should be saved. + * + * For performance and realistic necessity for preserving this kind of + * information, the frequency should (not required) be following: + * - 0: off; + * - 5: 5 second intervals; + * - 30: 30 second intervals; + * - 60: 60 second intervals. + * + * If auto-save is off, then this value is 0 automatically. When auto-save + * is turned on, by default the value is 30. + * + * @return auto-save frequency + */ + public int getAutoSaveFrequency() { + return autoSaveFrequency; + } + + /** + * Set the auto-save frequency, in seconds. + * + * @param autoSaveFrequency + */ + public void setAutoSaveFrequency(int autoSaveFrequency) { + this.autoSaveFrequency = autoSaveFrequency; + updateSettingsFile(); + } +} diff --git a/src/gui/backend/SudokuChecker.java b/src/gui/backend/SudokuChecker.java new file mode 100644 index 0000000..bd7d772 --- /dev/null +++ b/src/gui/backend/SudokuChecker.java @@ -0,0 +1,457 @@ +package gui.backend; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + + + +/** + * The SudokuChecker class is responsible for calculating the solution to a + * given Sudoku puzzle. + * + * TODO: The Sudoku algorithm used is efficient and effective for solving easy + * and medium puzzles, but it is not optimized for hard puzzles. It is + * recommended to use a different algorithm for hard puzzles, likely a tree & + * back-track approach. + */ +public class SudokuChecker { + private Cell[][] grid; + private Cell[][] origGrid; + + /** + * Create a new SudokuChecker object, initializing the grid to the given 9x9 + * grid of numbers. This should either check each cell as the user inputs a + * value, or be used to check the validity of a puzzle when the user requests + * it. + * + * @param grid + */ + public SudokuChecker(Cell[][] grid) { + this.grid = grid; + + // Create a copy of the original grid to be used for resetting the + // grid to its original state. + origGrid = new Cell[9][9]; + for (int i = 0; i < 9; i++) { + for (int j = 0; j < 9; j++) { + origGrid[i][j] = new Cell(i, j, grid[i][j].getValue()); + } + } + } + + /** + * Check if the given value can be placed in the given cell of the grid. + * + * False means that the value is incorrect, and true means that the value is + * correct. + * + * @param row + * @param col + * @param value + * @return boolean + */ + public boolean checkValue(int row, int col, int value) { + // Check the row + for (int i = 0; i < 9; i++) { + if (grid[row][i].getValue() == value && i != col) + return false; + } + + // Check the column + for (int i = 0; i < 9; i++) { + if (grid[i][col].getValue() == value && i != row) + return false; + } + + // Check the box + 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() == value + && (boxRow * 3 + i != row || boxCol * 3 + j != col)) { + return false; + } + } + } + + return true; + } + + /** + * Get the possible values for each cell in the Sudoku puzzle. + * + * Intended to be used for auto-filling in possible values in the GUI. + * + * @return + */ + public Cell[][] getPossibleValues(Cell[][] grid) { + for (int row = 0; row < grid.length; row++) { + for (int col = 0; col < grid[row].length; col++) { + if (grid[row][col].getValue() != 0) + continue; + + // Replace intersection with union? + ArrayList intersection = intersection(getRowRemainingNumbers(row), + getColRemainingNumbers(col)); + + intersection = intersection(intersection, getBoxRemainingNumbers(row, col)); + + // 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)); + } + } + + this.grid = grid; + return grid; + } + + /** + * Get the solution to the Sudoku puzzle. + * + * @return Cell[][] + */ + public Cell[][] getSolution() { + solve(); + return grid; + } + + /** + * 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 row + * @param col + * @return an array of numbers that are available to be placed in the given cell + */ + private void getAvailableNumbers(int row, int col) { + ArrayList intersection = intersection(getRowRemainingNumbers(row), getColRemainingNumbers(col)); + + intersection = intersection(intersection, getBoxRemainingNumbers(row, col)); + + if (intersection.size() == 0) + return; + else if (intersection.size() == 1) { + int value = intersection.get(0); + + grid[row][col].setValue(value, true); + updatePossibleValues(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 row + * @param col + */ + private void updatePossibleValues(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], true); + updatePossibleValues(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], true); + updatePossibleValues(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 + */ + private boolean solve() { + return solve(0, 0); + } + + /** + * Overloaded method that solves the Sudoku puzzle moving from the position given to the end + * + * @param row + * @param col + * @return + */ + private boolean solve(int row, int col) { + int nextCol = (col + 1) % 9; + int nextRow = (nextCol == 0) ? row + 1 : row; + // Base case - progressed past the last row and column. + if (row == 9) { +// grid.printTable(); + return true; + } + // If the cell has a value it is skipped. + if (grid[row][col].getValue() != 0) { + solve(nextRow, nextCol); + } else { + // An empty cell prompts a generation of possible numbers + List possibleNumbers = getAllRemainingNumbers(row, col); + if (possibleNumbers.size() == 0) // If there are no available numbers the solve() returns false + return false; + // each possible number is given in ascending order + for (Integer el : possibleNumbers) { + grid[row][col].setValue(el.intValue(), true); + + // here is the check to see if the next possible number needs to be tested + if (solve(nextRow, nextCol)) + return true; + // reset the cell so that it is not assumed to be solved after failing the + // current tested values } + } + } + return false; + } + + /** + * This may be redundant but I needed an method that could be called to get a list of all + * remaining numbers after eliminating 1-9 by standard Sudoku rules. + * This method calls the 3 methods already in this class to build a HashSet of + * known values which are used to verify which numbers remain as possible solutions. + * @param row + * @param col + * @return ArrayList results; + */ + private ArrayList getAllRemainingNumbers(int row, int col) { + HashSet nums = new HashSet<>(); + ArrayList results = new ArrayList<>(); + nums.addAll(getColRemainingNumbers(col)); + nums.addAll(getRowRemainingNumbers(row)); + nums.addAll(getBoxRemainingNumbers(row, col)); + for (int i = 1; i <= 9; i++) { + if (!nums.contains(i)) { + results.add(i); + } + } + return results; + } + + /** + * Get any number between 1 and 9 that is not in the row. + * + * @param row + * @return + */ + private ArrayList getRowRemainingNumbers(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 col + * @return + */ + private ArrayList getColRemainingNumbers(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 box + * @return + */ + private ArrayList getBoxRemainingNumbers(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. + * + * @return true if the grid is a valid Sudoku puzzle, and false otherwise + */ + private boolean isValidSolution() { + // 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) + 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; + } +} \ No newline at end of file diff --git a/src/gui/backend/Theme.java b/src/gui/backend/Theme.java new file mode 100644 index 0000000..eae087f --- /dev/null +++ b/src/gui/backend/Theme.java @@ -0,0 +1,386 @@ +package gui.backend; + +import java.awt.Color; +import java.io.File; + +/** + * The Theme class represents the color scheme for the GUI components. + * + * The Theme class reads a .theme file and sets the colors for the GUI components. + * .theme files are .json files for standardized data processing. + * + * The Theme class provides the following color customizations: + * - primaryBackground + * - secondaryBackground + * - primaryText + * - secondaryText + * - primaryHighlight + * - secondaryHighlight + * - primaryBorder + * - secondaryBorder + * - primaryButton + * - secondaryButton + * - primaryButtonHighlight + * - secondaryButtonHighlight + * - primaryButtonBorder + * - secondaryButtonBorder + */ +public class Theme { + private Color primaryBackground; + private Color secondaryBackground; + private Color primaryText; + private Color secondaryText; + private Color primaryHighlight; + private Color secondaryHighlight; + private Color primaryBorder; + private Color secondaryBorder; + private Color primaryButton; + private Color secondaryButton; + private Color primaryButtonHighlight; + private Color secondaryButtonHighlight; + private Color primaryButtonBorder; + private Color secondaryButtonBorder; + private Color errorBackground; + private Color errorText; + private Color errorBorder; + + /** + * Constructs a new Theme object with the given theme file. + * This object provides the colors for the GUI components. + * + * @param theme the theme file + */ + public Theme(File file) { + readTheme(file); + } + + /** + * Reads the .theme file and sets the colors for the GUI components. + * Once complete, sets the theme for the GUI components. + */ + private void readTheme(File file) { + // TODO: Read the .theme file + setTheme(); + } + + /** + * Sets the theme for the GUI components. + */ + private void setTheme() { + primaryBackground = Color.decode("#4A245E"); + secondaryBackground = Color.decode("#DADFF7"); + primaryText = Color.decode("#DBABF7"); + secondaryText = Color.decode("#4A245E"); + primaryBorder = Color.decode("#A76BCA"); + secondaryBorder = Color.decode("#DBABF7"); + errorBackground = Color.decode("#FF0000"); + errorText = Color.decode("#FFFFFF"); + errorBorder = Color.decode("#FF0000"); + } + + /** + * Gets the primary background color. + * + * @return the primary background color + */ + public Color getPrimaryBackground() { + return primaryBackground; + } + + /** + * Sets the primary background color. + * + * @param primaryBackground the primary background color + */ + public void setPrimaryBackground(Color primaryBackground) { + this.primaryBackground = primaryBackground; + } + + /** + * Gets the secondary background color. + * + * @return the secondary background color + */ + public Color getSecondaryBackground() { + return secondaryBackground; + } + + /** + * Sets the secondary background color. + * + * @param secondaryBackground the secondary background color + */ + public void setSecondaryBackground(Color secondaryBackground) { + this.secondaryBackground = secondaryBackground; + } + + /** + * Gets the primary text color. + * + * @return the primary text color + */ + public Color getPrimaryText() { + return primaryText; + } + + /** + * Sets the primary text color. + * + * @param primaryText the primary text color + */ + public void setPrimaryText(Color primaryText) { + this.primaryText = primaryText; + } + + /** + * Gets the secondary text color. + * + * @return the secondary text color + */ + public Color getSecondaryText() { + return secondaryText; + } + + /** + * Sets the secondary text color. + * + * @param secondaryText the secondary text color + */ + public void setSecondaryText(Color secondaryText) { + this.secondaryText = secondaryText; + } + + /** + * Gets the primary highlight color. + * + * @return the primary highlight color + */ + public Color getPrimaryHighlight() { + return primaryHighlight; + } + + /** + * Sets the primary highlight color. + * + * @param primaryHighlight the primary highlight color + */ + public void setPrimaryHighlight(Color primaryHighlight) { + this.primaryHighlight = primaryHighlight; + } + + /** + * Gets the secondary highlight color. + * + * @return the secondary highlight color + */ + public Color getSecondaryHighlight() { + return secondaryHighlight; + } + + /** + * Sets the secondary highlight color. + * + * @param secondaryHighlight the secondary highlight color + */ + public void setSecondaryHighlight(Color secondaryHighlight) { + this.secondaryHighlight = secondaryHighlight; + } + + /** + * Gets the primary border color. + * + * @return the primary border color + */ + public Color getPrimaryBorder() { + return primaryBorder; + } + + /** + * Sets the primary border color. + * + * @param primaryBorder the primary border color + */ + public void setPrimaryBorder(Color primaryBorder) { + this.primaryBorder = primaryBorder; + } + + /** + * Gets the secondary border color. + * + * @return the secondary border color + */ + public Color getSecondaryBorder() { + return secondaryBorder; + } + + /** + * Sets the secondary border color. + * + * @param secondaryBorder the secondary border color + */ + public void setSecondaryBorder(Color secondaryBorder) { + this.secondaryBorder = secondaryBorder; + } + + /** + * Gets the primary button color. + * + * @return the primary button color + */ + public Color getPrimaryButton() { + return primaryButton; + } + + /** + * Sets the primary button color. + * + * @param primaryButton the primary button color + */ + public void setPrimaryButton(Color primaryButton) { + this.primaryButton = primaryButton; + } + + /** + * Gets the secondary button color. + * + * @return the secondary button color + */ + public Color getSecondaryButton() { + return secondaryButton; + } + + /** + * Sets the secondary button color. + * + * @param secondaryButton the secondary button color + */ + public void setSecondaryButton(Color secondaryButton) { + this.secondaryButton = secondaryButton; + } + + /** + * Gets the primary button highlight color. + * + * @return the primary button highlight color + */ + public Color getPrimaryButtonHighlight() { + return primaryButtonHighlight; + } + + /** + * Sets the primary button highlight color. + * + * @param primaryButtonHighlight the primary button highlight color + */ + public void setPrimaryButtonHighlight(Color primaryButtonHighlight) { + this.primaryButtonHighlight = primaryButtonHighlight; + } + + /** + * Gets the secondary button highlight color. + * + * @return the secondary button highlight color + */ + public Color getSecondaryButtonHighlight() { + return secondaryButtonHighlight; + } + + /** + * Sets the secondary button highlight color. + * + * @param secondaryButtonHighlight the secondary button highlight color + */ + public void setSecondaryButtonHighlight(Color secondaryButtonHighlight) { + this.secondaryButtonHighlight = secondaryButtonHighlight; + } + + /** + * Gets the primary button border color. + * + * @return the primary button border color + */ + public Color getPrimaryButtonBorder() { + return primaryButtonBorder; + } + + /** + * Sets the primary button border color. + * + * @param primaryButtonBorder the primary button border color + */ + public void setPrimaryButtonBorder(Color primaryButtonBorder) { + this.primaryButtonBorder = primaryButtonBorder; + } + + /** + * Gets the secondary button border color. + * + * @return the secondary button border color + */ + public Color getSecondaryButtonBorder() { + return secondaryButtonBorder; + } + + /** + * Sets the secondary button border color. + * + * @param secondaryButtonBorder the secondary button border color + */ + public void setSecondaryButtonBorder(Color secondaryButtonBorder) { + this.secondaryButtonBorder = secondaryButtonBorder; + } + + /** + * Gets the error background color. + * + * @return the error background color + */ + public Color getErrorBackground() { + return errorBackground; + } + + /** + * Sets the error background color. + * + * @param errorBackground the error background color + */ + public void setErrorBackground(Color errorBackground) { + this.errorBackground = errorBackground; + } + + /** + * Gets the error text color. + * + * @return the error text color + */ + public Color getErrorText() { + return errorText; + } + + /** + * Sets the error text color. + * + * @param errorText the error text color + */ + public void setErrorText(Color errorText) { + this.errorText = errorText; + } + + /** + * Gets the error border color. + * + * @return the error border color + */ + public Color getErrorBorder() { + return errorBorder; + } + + /** + * Sets the error border color. + * + * @param errorBorder the error border color + */ + public void setErrorBorder(Color errorBorder) { + this.errorBorder = errorBorder; + } +} diff --git a/src/run.ps1 b/src/run.ps1 index 28ea8b4..96c3ae1 100644 --- a/src/run.ps1 +++ b/src/run.ps1 @@ -2,13 +2,12 @@ function Run { param( [string]$flag ) - javac *.java - java App $flag - rm *.class + javac -d bin *.java + java -cp bin App $flag } if ($args.Length -eq 1) { $flag = $args[0] } -Run $flag \ No newline at end of file +Run $flag diff --git a/src/run.sh b/src/run.sh old mode 100644 new mode 100755 index 448bcb8..bb3119f --- a/src/run.sh +++ b/src/run.sh @@ -1,11 +1,32 @@ -run() { - javac *.java - java App $1 - rm *.class +function run() { + if [[ $# -eq 0 ]]; then + # Compile the program + javac -d bin *.java + java -cp bin App + else + case $1 in + "-c" | "--cli") + # Run the program in the CLI + # Compile the program + javac -d bin *.java + java -cp bin App -c + ;; + + "-b" | "--build") + # Compile the program + javac -d bin *.java + ;; + + "-j" | "--jar") + # Create a JAR file + jar cfe sudoku.jar App -C bin . + ;; + + *) + echo "Invalid argument" + ;; + esac + fi } -if($# -eq 1) then - FLAG=$1 -fi - -run $FLAG \ No newline at end of file +run $@ diff --git a/src/sudoku.jar b/src/sudoku.jar new file mode 100644 index 0000000..83744b0 Binary files /dev/null and b/src/sudoku.jar differ