├── settings.gradle ├── .editorconfig ├── src ├── main │ └── java │ │ └── com │ │ └── wissassblog │ │ └── sudoku │ │ ├── constants │ │ ├── GameState.java │ │ ├── Messages.java │ │ └── Rows.java │ │ ├── Main.java │ │ ├── problemdomain │ │ ├── IStorage.java │ │ ├── Coordinates.java │ │ └── SudokuGame.java │ │ ├── userinterface │ │ ├── SudokuTextField.java │ │ ├── IUserInterfaceContract.java │ │ ├── logic │ │ │ └── ControlLogic.java │ │ ├── BadUserInterfaceImpl.java │ │ └── UserInterfaceImpl.java │ │ ├── SudokuApplication.java │ │ ├── computationlogic │ │ ├── SudokuUtilities.java │ │ ├── SudokuSolver.java │ │ ├── GameGenerator.java │ │ └── GameLogic.java │ │ ├── buildlogic │ │ └── SudokuBuildLogic.java │ │ └── persistence │ │ └── LocalStorageImpl.java └── test │ └── java │ ├── GameGeneratorTest.java │ ├── GameLogicTest.java │ └── TestData.java ├── README.md ├── .gitignore ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sudoku' 2 | 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | root = true 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 4 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/constants/GameState.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.constants; 2 | 3 | public enum GameState { 4 | COMPLETE, 5 | ACTIVE, 6 | NEW 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/Main.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku; 2 | 3 | 4 | public class Main { 5 | public static void main(String[] args){ 6 | SudokuApplication.main(new String[]{}); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/constants/Messages.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.constants; 2 | 3 | public class Messages { 4 | public static final String GAME_COMPLETE = "Congratulations, you have won! New Game?"; 5 | public static final String ERROR = "An error has occurred."; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/problemdomain/IStorage.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.problemdomain; 2 | 3 | import java.io.IOException; 4 | 5 | //Interfaces are great for keeping concerns for the back and front ends separate. 6 | //If you do not use them anywhere else, this is a great place to start. 7 | public interface IStorage { 8 | void updateGameData(SudokuGame game) throws IOException; 9 | SudokuGame getGameData() throws IOException; 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is used as a learning resource for my course, Working Class Java. 2 | 3 | In order to actually build and launch this project, you will need to configure some kind of build tool. I have included the build.gradle and settings.gradle files, which should allow you to set the project up using Gradle, should that be your preferred tool. 4 | 5 | The easiest solution would be to create a new Gradle project in your preferred IDE, and then copy and paste the source files, and the gradle files form this repo over to the new project. -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/constants/Rows.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.constants; 2 | 3 | /** 4 | * This enum exists to provide better legibility for the logic required to check if each Square in the 5 | * sudoku puzzle contains a valid value. See GameLogic.java for usage. 6 | * 7 | * Top, Middle, and Bottom rows for each square (a square consists of 3x3 "tiles", with 9 squares total in a 8 | * sudoku puzzle). 9 | * 10 | * The values represent the Y coordinates of each tile. 11 | */ 12 | public enum Rows { 13 | TOP, 14 | MIDDLE, 15 | BOTTOM 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/problemdomain/Coordinates.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.problemdomain; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Convenience class for storing the location of a given tile in the Sudoku puzzle in a Hashmap. 7 | */ 8 | public class Coordinates { 9 | private final int x; 10 | private final int y; 11 | 12 | public Coordinates(int x, int y) { 13 | this.x = x; 14 | this.y = y; 15 | } 16 | 17 | public int getX() { 18 | return x; 19 | } 20 | 21 | public int getY() { 22 | return y; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | Coordinates that = (Coordinates) o; 30 | return x == that.x && 31 | y == that.y; 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return Objects.hash(x, y); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/userinterface/SudokuTextField.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.userinterface; 2 | 3 | import javafx.scene.control.TextField; 4 | 5 | public class SudokuTextField extends TextField { 6 | private final int x; 7 | private final int y; 8 | 9 | public SudokuTextField(int x, int y) { 10 | this.x = x; 11 | this.y = y; 12 | 13 | } 14 | 15 | public int getX() { 16 | return x; 17 | } 18 | 19 | public int getY() { 20 | return y; 21 | } 22 | 23 | /* 24 | For some reason, when I override these two functions, the TextFields stop duplicating numeric inputs... 25 | */ 26 | @Override 27 | public void replaceText(int i, int i1, String s) { 28 | if (!s.matches("[0-9]")) { 29 | super.replaceText(i, i1, s); 30 | } 31 | } 32 | 33 | 34 | @Override 35 | public void replaceSelection(String s) { 36 | if (!s.matches("[0-9]")) { 37 | super.replaceSelection(s); 38 | } 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/SudokuApplication.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku; 2 | 3 | import com.wissassblog.sudoku.buildlogic.SudokuBuildLogic; 4 | import com.wissassblog.sudoku.userinterface.UserInterfaceImpl; 5 | import javafx.application.Application; 6 | import javafx.stage.Stage; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * This class is the Root Container (the thing which attends to all of the primary objects which must communicate when 12 | * the program is running (a running program is called a "process"). 13 | */ 14 | public class SudokuApplication extends Application { 15 | private UserInterfaceImpl uiImpl; 16 | 17 | @Override 18 | public void start(Stage primaryStage) throws IOException { 19 | //Get SudokuGame object for a new game 20 | uiImpl = new UserInterfaceImpl(primaryStage); 21 | 22 | try { 23 | SudokuBuildLogic.build(uiImpl); 24 | } catch (IOException e) { 25 | e.printStackTrace(); 26 | throw e; 27 | } 28 | } 29 | 30 | public static void main(String[] args) { 31 | launch(args); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/computationlogic/SudokuUtilities.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.computationlogic; 2 | 3 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 4 | 5 | public class SudokuUtilities { 6 | 7 | /** 8 | * Copy the values from one sudoku grid into another 9 | * 10 | * Note: O(n^2) Runtime Complexity 11 | * @param oldArray 12 | * @param newArray 13 | */ 14 | public static void copySudokuArrayValues(int[][] oldArray, int[][] newArray) { 15 | for (int xIndex = 0; xIndex < SudokuGame.GRID_BOUNDARY; xIndex++){ 16 | for (int yIndex = 0; yIndex < SudokuGame.GRID_BOUNDARY; yIndex++ ){ 17 | newArray[xIndex][yIndex] = oldArray[xIndex][yIndex]; 18 | } 19 | } 20 | } 21 | 22 | /** 23 | * Creates and returns a new Array with the same values as the inputted Array. 24 | * 25 | * @param oldArray 26 | */ 27 | public static int[][] copyToNewArray(int[][] oldArray) { 28 | int[][] newArray = new int[SudokuGame.GRID_BOUNDARY][SudokuGame.GRID_BOUNDARY]; 29 | copySudokuArrayValues(oldArray,newArray); 30 | return newArray; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/buildlogic/SudokuBuildLogic.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.buildlogic; 2 | 3 | import com.sun.javafx.iio.ios.IosDescriptor; 4 | import com.wissassblog.sudoku.computationlogic.GameLogic; 5 | import com.wissassblog.sudoku.persistence.LocalStorageImpl; 6 | import com.wissassblog.sudoku.problemdomain.IStorage; 7 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 8 | import com.wissassblog.sudoku.userinterface.IUserInterfaceContract; 9 | import com.wissassblog.sudoku.userinterface.logic.ControlLogic; 10 | 11 | import java.io.IOException; 12 | 13 | public class SudokuBuildLogic { 14 | 15 | /** 16 | * This class takes in the uiImpl object which is tightly-coupled to the JavaFX framework, 17 | * and binds that object to the various other objects necessary for the application to function. 18 | */ 19 | public static void build(IUserInterfaceContract.View userInterface) throws IOException { 20 | SudokuGame initialState; 21 | IStorage storage = new LocalStorageImpl(); 22 | 23 | try { 24 | //will throw if no game data is found in local storage 25 | 26 | initialState = storage.getGameData(); 27 | } catch (IOException e) { 28 | 29 | initialState = GameLogic.getNewGame(); 30 | //this method below will also throw an IOException 31 | //if we cannot update the game data. At this point 32 | //the application is considered unrecoverable 33 | storage.updateGameData(initialState); 34 | } 35 | 36 | IUserInterfaceContract.EventListener uiLogic = new ControlLogic(storage, userInterface); 37 | userInterface.setListener(uiLogic); 38 | userInterface.updateBoard(initialState); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/GameGeneratorTest.java: -------------------------------------------------------------------------------- 1 | import com.wissassblog.sudoku.computationlogic.GameLogic; 2 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class GameGeneratorTest { 6 | 7 | /** 8 | * Generate a new puzzle based on the appropriate rules, with 30 numbers initially completed. 9 | */ 10 | @Test 11 | public void onGenerateNewPuzzle() { 12 | int[][] newPuzzle = GameLogic.getNewGame().getCopyOfGridState(); 13 | 14 | int numberOfFilledSquares = 0; 15 | 16 | //Traverse array 17 | for (int xIndex = 0; xIndex < 9; xIndex++){ 18 | for (int yIndex = 0; yIndex < 9; yIndex++ ){ 19 | if (newPuzzle[xIndex][yIndex] != 0) numberOfFilledSquares++; 20 | } 21 | } 22 | 23 | //Check of invalid set up 24 | assert (!GameLogic.rowsAreInvalid(newPuzzle)); 25 | assert (!GameLogic.columnsAreInvalid(newPuzzle)); 26 | assert (!GameLogic.squaresAreInvalid(newPuzzle)); 27 | assert (numberOfFilledSquares == 81); 28 | 29 | } 30 | 31 | /** 32 | * After spending several days sorting out how to generate a new valid sudoku puzzle, this test 33 | * will confirm if my algorithm works. 34 | */ 35 | @Test 36 | public void test100NewPuzzles(){ 37 | for (int testIndex = 0; testIndex < 100; testIndex++){ 38 | 39 | int[][] newPuzzle = GameLogic.getNewGame().getCopyOfGridState(); 40 | 41 | assert (!GameLogic.rowsAreInvalid(newPuzzle)); 42 | assert (!GameLogic.columnsAreInvalid(newPuzzle)); 43 | assert (!GameLogic.squaresAreInvalid(newPuzzle)); 44 | assert (!GameLogic.tilesAreNotFilled(newPuzzle)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/persistence/LocalStorageImpl.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.persistence; 2 | 3 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 4 | import com.wissassblog.sudoku.problemdomain.IStorage; 5 | 6 | import java.io.*; 7 | import java.nio.charset.Charset; 8 | import java.nio.file.Files; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | 12 | 13 | /** 14 | * JSON is a simple language which is commonly used for storage and data transfer in Desktop, Web, and Mobile 15 | * programming. By having one simple language which can be understood by a wide variety of different platforms and 16 | * operating systems, this makes life easier for us programmers to have our programs communicate with each other, and 17 | * work on more devices. 18 | */ 19 | public class LocalStorageImpl implements IStorage { 20 | 21 | private static File GAME_DATA = new File( 22 | System.getProperty("user.home"), 23 | "gamedata.txt" 24 | ); 25 | 26 | @Override 27 | public void updateGameData(SudokuGame game) throws IOException { 28 | try { 29 | 30 | 31 | FileOutputStream fileOutputStream = 32 | new FileOutputStream(GAME_DATA); 33 | ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); 34 | objectOutputStream.writeObject(game); 35 | objectOutputStream.close(); 36 | } catch (IOException e) { 37 | throw new IOException("Unable to access Game Data"); 38 | } 39 | } 40 | 41 | @Override 42 | public SudokuGame getGameData() throws IOException { 43 | 44 | FileInputStream fileInputStream = 45 | new FileInputStream(GAME_DATA); 46 | ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); 47 | try { 48 | SudokuGame gameState = (SudokuGame) objectInputStream.readObject(); 49 | objectInputStream.close(); 50 | return gameState; 51 | } catch (ClassNotFoundException e) { 52 | throw new IOException("File Not Found"); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/userinterface/IUserInterfaceContract.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.userinterface; 2 | 3 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 4 | 5 | /** 6 | * Contract is really just another word for interface, which is another word for a Protocol. 7 | * All of these words mean: "something (a file in this case) which describes how one or more objects 8 | * may interact with each other without having to know too much about each other." 9 | * 10 | * For example, let's say you order food to be delivered to your house. You expect the delivery person to show 11 | * (hopefully soon) to accept your payment and give you food. Both you and the driver are aware that this is how 12 | * you can interact with each other successfully. The driver must give you food, and you must pay the driver. 13 | * 14 | * If I was to describe that Contract/Interface/Protocol/Abstraction (English unfortunately has many words for one 15 | * thing) in Java code, it might look like: 16 | * 17 | * interface FoodService { 18 | * interface Customer { 19 | * void acceptFood(Food food); 20 | * } 21 | * 22 | * interface DeliveryPerson { 23 | * void acceptPayment(Money money); 24 | * } 25 | * } 26 | * 27 | * 28 | */ 29 | public interface IUserInterfaceContract { 30 | 31 | //Short is just a smaller version of an "int". Although computers have become very powerful, 32 | //it is still good practice to use the smallest possible data structure, unless legibility (such as an enum) 33 | //is a more important concern for the problem in front of you. 34 | interface EventListener { 35 | void onSudokuInput(int x, int y, int input); 36 | void onDialogClick(); 37 | } 38 | 39 | 40 | //View refers to what the user can "View", or "See". In English, the word is both a noun and a verb. 41 | interface View { 42 | void setListener(IUserInterfaceContract.EventListener listener); 43 | //update a single square after user input 44 | void updateSquare(int x, int y, int input); 45 | 46 | //update the entire board, such as after game completion or initial execution of the program 47 | void updateBoard(SudokuGame game); 48 | void showDialog(String message); 49 | void showError(String message); 50 | } 51 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | 26 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 27 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 28 | 29 | # User-specific stuff 30 | .idea/**/workspace.xml 31 | .idea/**/tasks.xml 32 | .idea/**/usage.statistics.xml 33 | .idea/**/dictionaries 34 | .idea/**/shelf 35 | 36 | # AWS User-specific 37 | .idea/**/aws.xml 38 | 39 | # Generated files 40 | .idea/**/contentModel.xml 41 | 42 | # Sensitive or high-churn files 43 | .idea/**/dataSources/ 44 | .idea/**/dataSources.ids 45 | .idea/**/dataSources.local.xml 46 | .idea/**/sqlDataSources.xml 47 | .idea/**/dynamic.xml 48 | .idea/**/uiDesigner.xml 49 | .idea/**/dbnavigator.xml 50 | 51 | # Gradle 52 | .idea/**/gradle.xml 53 | .idea/**/libraries 54 | 55 | # Gradle and Maven with auto-import 56 | # When using Gradle or Maven with auto-import, you should exclude module files, 57 | # since they will be recreated, and may cause churn. Uncomment if using 58 | # auto-import. 59 | .idea/artifacts 60 | .idea/compiler.xml 61 | .idea/jarRepositories.xml 62 | .idea/modules.xml 63 | .idea/*.iml 64 | .idea/modules 65 | *.iml 66 | *.iml 67 | *.ipr 68 | 69 | # CMake 70 | cmake-build-*/ 71 | 72 | # Mongo Explorer plugin 73 | .idea/**/mongoSettings.xml 74 | 75 | # File-based project format 76 | *.iws 77 | 78 | # IntelliJ 79 | out/ 80 | 81 | # mpeltonen/sbt-idea plugin 82 | .idea_modules/ 83 | 84 | # JIRA plugin 85 | atlassian-ide-plugin.xml 86 | 87 | # Cursive Clojure plugin 88 | .idea/replstate.xml 89 | 90 | # Crashlytics plugin (for Android Studio and IntelliJ) 91 | com_crashlytics_export_strings.xml 92 | crashlytics.properties 93 | crashlytics-build.properties 94 | fabric.properties 95 | 96 | # Editor-based Rest Client 97 | .idea/httpRequests 98 | 99 | # Android studio 3.1+ serialized cache file 100 | .idea/caches/build_file_checksums.ser 101 | 102 | .gradle 103 | **/build/ 104 | !src/**/build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 110 | !gradle-wrapper.jar 111 | 112 | # Cache of project 113 | .gradletasknamecache 114 | .idea/modules/* 115 | 116 | .idea/* 117 | gradle/wrapper/gradle-wrapper.jar 118 | gradle/wrapper/gradle-wrapper.properties 119 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/userinterface/logic/ControlLogic.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.userinterface.logic; 2 | 3 | 4 | import com.wissassblog.sudoku.constants.GameState; 5 | import com.wissassblog.sudoku.constants.Messages; 6 | import com.wissassblog.sudoku.computationlogic.GameLogic; 7 | import com.wissassblog.sudoku.problemdomain.IStorage; 8 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 9 | import com.wissassblog.sudoku.userinterface.IUserInterfaceContract; 10 | 11 | import java.io.IOException; 12 | 13 | /** 14 | * Since this is a single screen application, just one container (class) for the logic of the user interface is 15 | * necessary. Break these things up when building applications with more screens/features. Don't build God Classes! 16 | */ 17 | 18 | public class ControlLogic implements IUserInterfaceContract.EventListener { 19 | 20 | private IStorage storage; 21 | //Remember, this could be the real UserInterfaceImpl, or it could be a test class 22 | //which implements the same interface! 23 | private IUserInterfaceContract.View view; 24 | 25 | public ControlLogic(IStorage storage, IUserInterfaceContract.View view) { 26 | this.storage = storage; 27 | this.view = view; 28 | } 29 | 30 | /** 31 | * Use Case: 32 | * 1. Retrieve current "state" of the data from IStorage 33 | * 2. Update it according to the input 34 | * 3. Write the result to IStorage 35 | * @param x X coordinate of the selected input 36 | * @param y Y ... 37 | * @param input Which key was entered, One of: 38 | * - Numbers 0-9 39 | * 40 | */ 41 | @Override 42 | public void onSudokuInput(int x, int y, int input) { 43 | try { 44 | SudokuGame gameData = storage.getGameData(); 45 | int[][] newGridState = gameData.getCopyOfGridState(); 46 | newGridState[x][y] = input; 47 | 48 | gameData = new SudokuGame( 49 | GameLogic.checkForCompletion(newGridState), 50 | newGridState 51 | ); 52 | 53 | storage.updateGameData(gameData); 54 | 55 | //either way, update the view 56 | view.updateSquare(x, y, input); 57 | 58 | //if game is complete, show dialog 59 | if (gameData.getGameState() == GameState.COMPLETE) view.showDialog(Messages.GAME_COMPLETE); 60 | } catch (IOException e) { 61 | e.printStackTrace(); 62 | view.showError(Messages.ERROR); 63 | } 64 | } 65 | 66 | @Override 67 | public void onDialogClick() { 68 | try { 69 | storage.updateGameData( 70 | GameLogic.getNewGame() 71 | ); 72 | 73 | view.updateBoard(storage.getGameData()); 74 | } catch (IOException e) { 75 | view.showError(Messages.ERROR); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/problemdomain/SudokuGame.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.problemdomain; 2 | 3 | import com.wissassblog.sudoku.computationlogic.SudokuUtilities; 4 | import com.wissassblog.sudoku.constants.GameState; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Remember, a program contains representations of real world objects (i.e. Money, User, Game). These things 10 | * contain real world information (although they may also contain useless/stub/test information), and are necessary 11 | * for a program which does anything practical. 12 | * 13 | * A game of sudoku is a board, which contain 81 squares. 14 | * 15 | */ 16 | public class SudokuGame implements Serializable { 17 | private final GameState gameState; 18 | private final int[][] gridState; 19 | 20 | /** 21 | * To make it easier to work with Arrays (where the first index position is 0 instead of 1, and so on), 22 | * Grid coordinates will be represented with x and y index values ranging from 0 (inclusive) to 8 (inclusive). 23 | */ 24 | public static final int GRID_BOUNDARY = 9; 25 | 26 | /** 27 | * I suppose that the most fundamental states required to represent a sudoku game, are an active state and a 28 | * complete state. The game will start in Active state, and when a Complete state is achieved (based on GameLogic), 29 | * then a special UI screen will be displayed by the user interface. 30 | * 31 | * To avoid Shared Mutable State (Shared change-able data), which causes many problems, I have decided to make this 32 | * class Immutable (meaning that once I created an instance of it, the values may only be read via getGameState() 33 | * and getGridState() functions, a.k.a. methods. Each time the gridState changes, a new SudokuGame object is created 34 | * by taking the old one, applying some functions to each, and generated a new one. 35 | * 36 | * @param gameState I have decided to make the initial potential states of the game to be an ENUM (a set of custom 37 | * constant values which I give legible names to), one of: 38 | * - GameState.Complete 39 | * - GameState.Active 40 | * 41 | * @param gridState The state of the sudoku game. If certain conditions are met (all locations in the gridstate 42 | * are filled in with the proper value), GameLogic must change gameState. 43 | * Examples: 44 | * - gridState[1,1] Top left square 45 | * - gridState[3,9] 3rd from the left, bottom row 46 | * - gridState[9,9] Bottom right square 47 | */ 48 | public SudokuGame(GameState gameState, int[][] gridState) { 49 | this.gameState = gameState; 50 | this.gridState = gridState; 51 | } 52 | 53 | public GameState getGameState() { 54 | return gameState; 55 | } 56 | 57 | public int[][] getCopyOfGridState() { 58 | return SudokuUtilities.copyToNewArray(gridState); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/computationlogic/SudokuSolver.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.computationlogic; 2 | 3 | import com.wissassblog.sudoku.constants.GameState; 4 | import com.wissassblog.sudoku.problemdomain.Coordinates; 5 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 6 | 7 | import static com.wissassblog.sudoku.problemdomain.SudokuGame.GRID_BOUNDARY; 8 | 9 | /** 10 | * Note: Algorithm based on "Simple Solving Algorithm" from the link below. I will look at more complex and efficient 11 | * algorithms in the future, they key with this algo is that it will tell me if the puzzle is solveable. 12 | *
13 | *
14 | * http://pi.math.cornell.edu/~mec/Summer2009/meerkamp/Site/Solving_any_Sudoku_I.html 15 | */ 16 | public class SudokuSolver { 17 | 18 | /** 19 | * 1.Enumerate all empty cells in typewriter order (left to right, top to bottom) 20 | *
21 | * 2.Our “current cell” is the first cell in the enumeration. 22 | *
23 | * 3.Enter a 1 into the current cell. If this violates the Sudoku condition, try entering a 2, then a 3, and so forth, until 24 | * a. the entry does not violate the Sudoku condition, or until 25 | * b. you have reached 9 and still violate the Sudoku condition. 26 | *
27 | *
28 | * 4.In case a: if the current cell was the last enumerated one, then the puzzle is solved. 29 | * If not, then go back to step 2 with the “current cell” being the next cell. 30 | * In case b: if the current cell is the first cell in the enumeration, then the Sudoku puzzle does not have a solution. 31 | * If not, then erase the 9 from the current cell, call the previous cell in the enumeration the new “current cell”, and 32 | * continue with step 3. 33 | */ 34 | public static boolean puzzleIsSolvable(int[][] puzzle) { 35 | 36 | //step 1: 37 | Coordinates[] emptyCells = typeWriterEnumerate(puzzle); 38 | 39 | //I would like to stress that using lots of nested loops is only appropriate if you are certain that 40 | //the size of input O(n) is small. 41 | int index = 0; 42 | int input = 1; 43 | while (index < 10) { 44 | Coordinates current = emptyCells[index]; 45 | input = 1; 46 | while (input < 40) { 47 | puzzle[current.getX()][current.getY()] = input; 48 | //if puzzle is invalid.... 49 | if (GameLogic.sudokuIsInvalid(puzzle)) { 50 | //if we hit GRID_BOUNDARY and it is still invalid, move to step 4b 51 | if (index == 0 && input == GRID_BOUNDARY) { 52 | //first cell can't be solved 53 | return false; 54 | } else if (input == GRID_BOUNDARY) { 55 | //decrement by 2 since the outer loop will increment by 1 when it finishes; we want the previous 56 | //cell 57 | index--; 58 | } 59 | 60 | input++; 61 | } else { 62 | index++; 63 | 64 | if (index == 39) { 65 | //last cell, puzzle solved 66 | return true; 67 | } 68 | 69 | //input = 10 to break the loop 70 | input = 10; 71 | } 72 | //move to next cell over 73 | } 74 | } 75 | 76 | return false; 77 | } 78 | 79 | /** 80 | * Enumerate all empty cells in typewriter order (left to right, top to bottom) 81 | *
82 | * 1. Traverse x from from 0-8 for each y, from 0-8, adding to a 1 dimensional array. 83 | *
84 | * NOTE: Assume that the maximum number of empty cells is 40, as per GameGenerator
85 | *
86 | * @param puzzle
87 | * @return
88 | */
89 | private static Coordinates[] typeWriterEnumerate(int[][] puzzle) {
90 | Coordinates[] emptyCells = new Coordinates[40];
91 | int iterator = 0;
92 | for (int y = 0; y < GRID_BOUNDARY; y++) {
93 | for (int x = 0; x < GRID_BOUNDARY; x++) {
94 | if (puzzle[x][y] == 0) {
95 | emptyCells[iterator] = new Coordinates(x, y);
96 | if (iterator == 39) return emptyCells;
97 | iterator++;
98 | }
99 | }
100 | }
101 | return emptyCells;
102 | }
103 |
104 |
105 | }
--------------------------------------------------------------------------------
/src/test/java/GameLogicTest.java:
--------------------------------------------------------------------------------
1 | import com.wissassblog.sudoku.computationlogic.GameLogic;
2 | import com.wissassblog.sudoku.computationlogic.SudokuUtilities;
3 | import com.wissassblog.sudoku.constants.GameState;
4 | import org.junit.jupiter.api.Assertions;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.util.Arrays;
8 | import java.util.List;
9 |
10 |
11 | public class GameLogicTest {
12 |
13 | /**
14 | * Start with the basic logic to validate a valid Sudoku puzzle
15 | */
16 | @Test
17 | public void onValidateValidPuzzle() {
18 | assert (GameState.COMPLETE ==
19 | GameLogic.checkForCompletion(
20 | TestData.getSolved().getCopyOfGridState()
21 | )
22 | );
23 | }
24 |
25 | @Test
26 | public void onValidateActivePuzzle() {
27 | assert (GameState.ACTIVE ==
28 | GameLogic.checkForCompletion(
29 | TestData.getValidStart().getCopyOfGridState()
30 | )
31 | );
32 | }
33 |
34 | @Test
35 | public void canCopyArrayValuesCorrectly()
36 | {
37 | int[][] oldArray = TestData.getInvalid().getCopyOfGridState();
38 | int[][] newArray = SudokuUtilities.copyToNewArray(oldArray);
39 | int[][] newSudokuArray = new int[9][9];
40 | SudokuUtilities.copySudokuArrayValues(oldArray,newSudokuArray);
41 | for(int i = 0; i < oldArray.length - 1; i++)
42 | {
43 | Assertions.assertArrayEquals(newArray[i],newSudokuArray[i]);
44 | Assertions.assertArrayEquals(oldArray[i],newSudokuArray[i]);
45 | }
46 | }
47 |
48 | /**
49 | * Expected value: True (i.e. squares are indeed not all filled
50 | */
51 | @Test
52 | public void gameSquaresAreNotFilled() {
53 | assert (GameLogic.tilesAreNotFilled(TestData.getValidStart().getCopyOfGridState()));
54 | }
55 |
56 | /**
57 | * Expected value: false
58 | */
59 | @Test
60 | public void gameSquaresAreFilled() {
61 | assert (!GameLogic.tilesAreNotFilled(TestData.getSolved().getCopyOfGridState()));
62 | }
63 |
64 | /**
65 | * Expected value: true
66 | */
67 | @Test
68 | public void gameSquaresAreInvalid() {
69 | int[][] invalid = TestData.getInvalid().getCopyOfGridState();
70 |
71 | boolean isInvalid = GameLogic.squaresAreInvalid(invalid);
72 | assert (isInvalid);
73 | }
74 |
75 | /**
76 | * Expected value: false
77 | */
78 | @Test
79 | public void gameSquaresAreValid() {
80 | int[][] valid = TestData.getSolved()
81 | .getCopyOfGridState();
82 |
83 | boolean isInvalid = GameLogic.squaresAreInvalid(
84 | valid
85 | );
86 |
87 | assert (!isInvalid);
88 | }
89 |
90 | /**
91 | * Expected value: true
92 | */
93 | @Test
94 | public void gameColumnsAreInvalid() {
95 | int[][] invalid = TestData.getInvalid()
96 | .getCopyOfGridState();
97 |
98 | boolean isInvalid = GameLogic.columnsAreInvalid(
99 | invalid
100 | );
101 | assert (isInvalid);
102 | }
103 |
104 | /**
105 | * Expected value: false
106 | */
107 | @Test
108 | public void gameColumnsAreValid() {
109 | int[][] valid = TestData.getSolved().getCopyOfGridState();
110 |
111 | boolean isInvalid = GameLogic.columnsAreInvalid(valid);
112 | assert (!isInvalid);
113 | }
114 |
115 | /**
116 | * Expected value: true
117 | */
118 | @Test
119 | public void gameRowsAreInvalid() {
120 | int[][] invalid = TestData.getInvalid().getCopyOfGridState();
121 |
122 | boolean isInvalid = GameLogic.rowsAreInvalid(invalid);
123 | assert (isInvalid);
124 | }
125 |
126 | /**
127 | * Expected value: false
128 | */
129 | @Test
130 | public void gameRowsAreValid() {
131 | int[][] valid = TestData.getSolved().getCopyOfGridState();
132 |
133 | boolean isInvalid = GameLogic.rowsAreInvalid(valid);
134 | assert (!isInvalid);
135 | }
136 |
137 | /**
138 | * Collection does have repeated integer values (this will be either a row or a column)
139 | * Expected value: true
140 | */
141 | @Test
142 | public void collectionHasRepeats() {
143 | List
33 | * Rules:
34 | * - A number may not be repeated among Rows, e.g.:
35 | * - [0, 0] == [0-8, 1] not allowed
36 | * - [0, 0] == [3, 4] allowed
37 | * - A number may not be repeated among Columns, e.g.:
38 | * - [0-8, 1] == [0, 0] not allowed
39 | * - [0, 0] == [3, 4] allowed
40 | * - A number may not be repeated within respective GRID_BOUNDARYxGRID_BOUNDARY regions within the Sudoku Puzzle
41 | * - [0, 0] == [1, 2] not allowed
42 | * - [0, 0] == [3, 4] allowed
43 | */
44 | public static GameState checkForCompletion(int[][] grid) {
45 | if (sudokuIsInvalid(grid)) return GameState.ACTIVE;
46 | if (tilesAreNotFilled(grid)) return GameState.ACTIVE;
47 | return GameState.COMPLETE;
48 | }
49 |
50 | /**
51 | * Traverse all tiles and determine if any all are not 0.
52 | * Note: GRID_BOUNDARY = GRID_BOUNDARY
53 | *
54 | * @param grid
55 | * @return
56 | */
57 | public static boolean tilesAreNotFilled(int[][] grid) {
58 | for (int xIndex = 0; xIndex < GRID_BOUNDARY; xIndex++) {
59 | for (int yIndex = 0; yIndex < GRID_BOUNDARY; yIndex++) {
60 | if (grid[xIndex][yIndex] == 0) return true;
61 | }
62 | }
63 | return false;
64 | }
65 |
66 | /**
67 | * Checks the if the current complete or incomplete state of the game is still a valid state of a Sudoku game,
68 | * relative to columns, rows, and squares.
69 | *
70 | * @param grid
71 | * @return
72 | */
73 | public static boolean sudokuIsInvalid(int[][] grid) {
74 | if (rowsAreInvalid(grid)) return true;
75 | if (columnsAreInvalid(grid)) return true;
76 | if (squaresAreInvalid(grid)) return true;
77 | else return false;
78 | }
79 |
80 |
81 | /**
82 | * For the purposes of giving specific names to specific things, a "Square" is one of the 3x3 portions of the
83 | * Sudoku puzzle, containing GRID_BOUNDARY "Tiles".
84 | *
85 | * Example square:
86 | * [0][0], [1][0], [2][0]
87 | * [0][1], [1][1], [2][1]
88 | * [0][2], [1][2], [2][2]
89 | *
90 | * How can I solve this problem elegantly?
91 | * 1. Compare every single element in the array to every other element in the array? (hell no)
92 | * 2. Use some dope problem solving skills to select for each square
93 | * and compare them individually. (sounds much better to me)
94 | *
95 | * Ranges:
96 | * [0][0] - [2][2], [3][0] - [5][2], [6][0] - [8][2]
97 | *
98 | * [0][3] - [2][2], [3][3] - [5][5], [6][3] - [8][5]
99 | *
100 | * [0][6] - [2][2], [3][0] - [5][2], [6][0] - [8][8]
101 | *
102 | * @param grid A copy of the Sudoku Game's grid state to compare against
103 | * @return
104 | */
105 | public static boolean squaresAreInvalid(int[][] grid) {
106 | //top three squares
107 | if (rowOfSquaresIsInvalid(Rows.TOP, grid)) return true;
108 |
109 | //middle three
110 | if (rowOfSquaresIsInvalid(Rows.MIDDLE, grid)) return true;
111 |
112 | //bottom three
113 | if (rowOfSquaresIsInvalid(Rows.BOTTOM, grid)) return true;
114 |
115 | return false;
116 | }
117 |
118 | private static boolean rowOfSquaresIsInvalid(Rows value, int[][] grid) {
119 | switch (value) {
120 | case TOP:
121 | //x FIRST = 0
122 | if (squareIsInvalid(0, 0, grid)) return true;
123 | //x SECOND = 3
124 | if (squareIsInvalid(0, 3, grid)) return true;
125 | //x THIRD = 6
126 | if (squareIsInvalid(0, 6, grid)) return true;
127 |
128 | //Otherwise squares appear to be valid
129 | return false;
130 |
131 | case MIDDLE:
132 | if (squareIsInvalid(3, 0, grid)) return true;
133 | if (squareIsInvalid(3, 3, grid)) return true;
134 | if (squareIsInvalid(3, 6, grid)) return true;
135 | return false;
136 |
137 | case BOTTOM:
138 | if (squareIsInvalid(6, 0, grid)) return true;
139 | if (squareIsInvalid(6, 3, grid)) return true;
140 | if (squareIsInvalid(6, 6, grid)) return true;
141 | return false;
142 |
143 | default:
144 | return false;
145 | }
146 | }
147 |
148 | public static boolean squareIsInvalid(int yIndex, int xIndex, int[][] grid) {
149 | int yIndexEnd = yIndex + 3;
150 | int xIndexEnd = xIndex + 3;
151 |
152 | List