├── .editorconfig ├── .gitignore ├── README.md ├── build.gradle ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── com │ └── wissassblog │ └── sudoku │ ├── Main.java │ ├── SudokuApplication.java │ ├── buildlogic │ └── SudokuBuildLogic.java │ ├── computationlogic │ ├── GameGenerator.java │ ├── GameLogic.java │ ├── SudokuSolver.java │ └── SudokuUtilities.java │ ├── constants │ ├── GameState.java │ ├── Messages.java │ └── Rows.java │ ├── persistence │ └── LocalStorageImpl.java │ ├── problemdomain │ ├── Coordinates.java │ ├── IStorage.java │ └── SudokuGame.java │ └── userinterface │ ├── BadUserInterfaceImpl.java │ ├── IUserInterfaceContract.java │ ├── SudokuTextField.java │ ├── UserInterfaceImpl.java │ └── logic │ └── ControlLogic.java └── test └── java ├── GameGeneratorTest.java ├── GameLogicTest.java └── TestData.java /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | } 4 | repositories { 5 | mavenLocal() 6 | mavenCentral() 7 | } 8 | 9 | println System.properties['java.home'] 10 | } 11 | 12 | plugins { 13 | id 'java' 14 | id 'application' 15 | id 'org.openjfx.javafxplugin' version '0.0.8' 16 | } 17 | 18 | javafx { 19 | version = "13" 20 | modules = [ 'javafx.controls', 'javafx.fxml'] 21 | mainClassName = "com.wiseassblog.sudoku.Main" 22 | 23 | } 24 | 25 | group 'com.wiseassblog' 26 | version '1.0-SNAPSHOT' 27 | 28 | 29 | repositories { 30 | mavenLocal() 31 | mavenCentral() 32 | } 33 | 34 | 35 | 36 | 37 | dependencies { 38 | testImplementation('org.junit.jupiter:junit-jupiter-api:5.4.2') 39 | testImplementation('org.junit.jupiter:junit-jupiter-engine:5.4.2') 40 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sudoku' 2 | 3 | -------------------------------------------------------------------------------- /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/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/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/main/java/com/wissassblog/sudoku/computationlogic/GameGenerator.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 java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Random; 10 | 11 | import static com.wissassblog.sudoku.problemdomain.SudokuGame.GRID_BOUNDARY; 12 | 13 | 14 | class GameGenerator { 15 | public static int[][] getNewGameGrid() { 16 | return unsolveGame(getSolvedGame()); 17 | } 18 | 19 | /** 20 | * 1. Generate a new 9x9 2D Array. 21 | * 2. For each value in the range 1..9, allocate that value 9 times based on the following constraints: 22 | * - A Random coordinate on the grid is selected. If it is empty, a Random value is allocated. 23 | * - The resulting allocation must not produce invalid rows, columns, or squares. 24 | * - If the allocation does produce an invalid game 25 | * 26 | * @return 27 | */ 28 | private static int[][] getSolvedGame() { 29 | Random random = new Random(System.currentTimeMillis()); 30 | int[][] newGrid = new int[GRID_BOUNDARY][GRID_BOUNDARY]; 31 | 32 | //Value represents potential values for each square. Each value must be allocated 9 times. 33 | for (int value = 1; value <= GRID_BOUNDARY; value++) { 34 | //allocations refers to the number of times in which a square has been given a value. 35 | int allocations = 0; 36 | 37 | //If too many allocation attempts are made which end in an invalid game, we grab the most recent 38 | //allocations stored in the List below, and reset them all to 0 (empty). 39 | int interrupt = 0; 40 | 41 | //Keep track of what has been allocated in the current frame of the loop 42 | List allocTracker = new ArrayList<>(); 43 | 44 | //As a failsafe, if we keep rolling back allocations on the most recent frame, and the game still 45 | //keeps breaking, after 500 times we reset the board entirely and start again. 46 | int attempts = 0; 47 | 48 | while (allocations < GRID_BOUNDARY) { 49 | 50 | if (interrupt > 200) { 51 | allocTracker.forEach(coord -> { 52 | newGrid[coord.getX()][coord.getY()] = 0; 53 | }); 54 | 55 | interrupt = 0; 56 | allocations = 0; 57 | allocTracker.clear(); 58 | attempts++; 59 | 60 | if (attempts > 500) { 61 | clearArray(newGrid); 62 | attempts = 0; 63 | value = 1; 64 | } 65 | } 66 | 67 | int xCoordinate = random.nextInt(GRID_BOUNDARY); 68 | int yCoordinate = random.nextInt(GRID_BOUNDARY); 69 | 70 | if (newGrid[xCoordinate][yCoordinate] == 0) { 71 | newGrid[xCoordinate][yCoordinate] = value; 72 | 73 | //if value results in an invalid game, then re-assign that element to 0 and try again 74 | if (GameLogic.sudokuIsInvalid(newGrid)) { 75 | newGrid[xCoordinate][yCoordinate] = 0; 76 | interrupt++; 77 | } 78 | //otherwise, indicate that a value has been allocated, and add it to the allocation tracker. 79 | else { 80 | allocTracker.add(new Coordinates(xCoordinate, yCoordinate)); 81 | allocations++; 82 | } 83 | } 84 | } 85 | } 86 | return newGrid; 87 | } 88 | 89 | /** 90 | * The purpose of this function is to take a game which has already been solved (and thus proven to be solvable), 91 | * and randomly assign a certain number of tiles to be equal to 0. It appears that there is no straight 92 | * forward way to check if a puzzle is still solvable after removing the tiles, beyond using another algorithm 93 | * to attempt to re-solve the problem. 94 | * 95 | * 1. Copy values of solvedGame to a new Array (make into a helper) 96 | * 2. Remove 40 Values randomly from the new Array. 97 | * 3. Test the new Array for solvablility. 98 | * 4a. Solveable -> return new Array 99 | * 4b. return to step 1 100 | * @param solvedGame 101 | * @return 102 | */ 103 | private static int[][] unsolveGame(int[][] solvedGame) { 104 | Random random = new Random(System.currentTimeMillis()); 105 | 106 | boolean solvable = false; 107 | 108 | //note: not actually solvable until the algorithm below finishes! 109 | int[][] solvableArray = new int[GRID_BOUNDARY][GRID_BOUNDARY]; 110 | 111 | while (solvable == false){ 112 | 113 | //Take values from solvedGame and write to new unsolved; i.e. reset to initial state 114 | SudokuUtilities.copySudokuArrayValues(solvedGame, solvableArray); 115 | 116 | //remove 40 random numbers 117 | int index = 0; 118 | while (index < 40) { 119 | int xCoordinate = random.nextInt(GRID_BOUNDARY); 120 | int yCoordinate = random.nextInt(GRID_BOUNDARY); 121 | 122 | if (solvableArray[xCoordinate][yCoordinate] != 0) { 123 | solvableArray[xCoordinate][yCoordinate] = 0; 124 | index++; 125 | } 126 | } 127 | 128 | int[][] toBeSolved = new int[GRID_BOUNDARY][GRID_BOUNDARY]; 129 | SudokuUtilities.copySudokuArrayValues(solvableArray, toBeSolved); 130 | //check if result is solvable 131 | solvable = SudokuSolver.puzzleIsSolvable(toBeSolved); 132 | 133 | //TODO Delete after tests 134 | System.out.println(solvable); 135 | } 136 | 137 | return solvableArray; 138 | } 139 | 140 | private static void clearArray(int[][] newGrid) { 141 | for (int xIndex = 0; xIndex < GRID_BOUNDARY; xIndex++) { 142 | for (int yIndex = 0; yIndex < GRID_BOUNDARY; yIndex++) { 143 | newGrid[xIndex][yIndex] = 0; 144 | } 145 | } 146 | } 147 | 148 | } -------------------------------------------------------------------------------- /src/main/java/com/wissassblog/sudoku/computationlogic/GameLogic.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.computationlogic; 2 | 3 | //Collection of static functions which may be used to determine events and new Game states. 4 | 5 | import com.wissassblog.sudoku.constants.GameState; 6 | import com.wissassblog.sudoku.constants.Rows; 7 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 8 | 9 | import java.util.*; 10 | 11 | import static com.wissassblog.sudoku.problemdomain.SudokuGame.GRID_BOUNDARY; 12 | 13 | /** 14 | * Q: Why isn't this a class hidden behind an interface? 15 | * A: It requires no external libraries, nor do I ever plan to switch to using external libraries. 16 | */ 17 | public class GameLogic { 18 | 19 | public static SudokuGame getNewGame() { 20 | return new SudokuGame( 21 | GameState.NEW, 22 | GameGenerator.getNewGameGrid() 23 | ); 24 | } 25 | 26 | /** 27 | * Check to see if the incoming state (what the values of each square happen to be) of the game is either Active 28 | * (i.e. Unsolved) or Complete (i.e. Solved). 29 | * 30 | * @param grid A virtual representation of a sudoku puzzle, which may or may not be solved. 31 | * @return Either GameState.Active or GameState.Complete, based on analysis of solvedSudoku. 32 | *

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 square = new ArrayList<>(); 153 | 154 | while (yIndex < yIndexEnd) { 155 | 156 | while (xIndex < xIndexEnd) { 157 | square.add( 158 | grid[xIndex][yIndex] 159 | ); 160 | xIndex++; 161 | } 162 | 163 | //reset x to original value by subtracting by 2 164 | xIndex -= 3; 165 | 166 | yIndex++; 167 | } 168 | 169 | //if square has repeats, return true 170 | if (collectionHasRepeats(square)) return true; 171 | return false; 172 | } 173 | 174 | public static boolean columnsAreInvalid(int[][] grid) { 175 | for (int xIndex = 0; xIndex < GRID_BOUNDARY; xIndex++) { 176 | List row = new ArrayList<>(); 177 | for (int yIndex = 0; yIndex < GRID_BOUNDARY; yIndex++) { 178 | row.add(grid[xIndex][yIndex]); 179 | } 180 | 181 | if (collectionHasRepeats(row)) return true; 182 | } 183 | 184 | return false; 185 | } 186 | 187 | public static boolean rowsAreInvalid(int[][] grid) { 188 | for (int yIndex = 0; yIndex < GRID_BOUNDARY; yIndex++) { 189 | List row = new ArrayList<>(); 190 | for (int xIndex = 0; xIndex < GRID_BOUNDARY; xIndex++) { 191 | row.add(grid[xIndex][yIndex]); 192 | } 193 | 194 | if (collectionHasRepeats(row)) return true; 195 | } 196 | 197 | return false; 198 | } 199 | 200 | public static boolean collectionHasRepeats(List collection) { 201 | //count occurrences of integers from 1-GRID_BOUNDARY. If Collections.frequency returns a value greater than 1, 202 | //then the square is invalid (i.e. a non-zero number has been repeated in a square) 203 | for (int index = 1; index <= GRID_BOUNDARY; index++) { 204 | if (Collections.frequency(collection, index) > 1) return true; 205 | } 206 | 207 | return false; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/userinterface/BadUserInterfaceImpl.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.userinterface; 2 | 3 | import com.wissassblog.sudoku.constants.GameState; 4 | import com.wissassblog.sudoku.problemdomain.Coordinates; 5 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 6 | import javafx.beans.value.ChangeListener; 7 | import javafx.beans.value.ObservableValue; 8 | import javafx.event.EventHandler; 9 | import javafx.geometry.Pos; 10 | import javafx.scene.Group; 11 | import javafx.scene.Scene; 12 | import javafx.scene.control.Alert; 13 | import javafx.scene.control.ButtonType; 14 | import javafx.scene.control.TextField; 15 | import javafx.scene.input.KeyCode; 16 | import javafx.scene.input.KeyEvent; 17 | import javafx.scene.layout.Background; 18 | import javafx.scene.paint.Color; 19 | import javafx.scene.shape.Rectangle; 20 | import javafx.scene.text.Font; 21 | import javafx.scene.text.Text; 22 | import javafx.stage.Stage; 23 | 24 | import java.util.HashMap; 25 | 26 | /** 27 | * Manages the window, and displays a pop up notification when the user completes the puzzle. 28 | */ 29 | public class BadUserInterfaceImpl implements IUserInterfaceContract.View, 30 | EventHandler { 31 | private final Stage stage; 32 | private final Group root; 33 | 34 | private HashMap textFieldCoordinates; 35 | 36 | private IUserInterfaceContract.EventListener listener; 37 | 38 | 39 | public BadUserInterfaceImpl(Stage stage) { 40 | this.stage = stage; 41 | this.root = new Group(); 42 | this.textFieldCoordinates = new HashMap<>(); 43 | 44 | Scene scene = new Scene(root, 668, 668); 45 | 46 | Text t = new Text(235, 690, "Sudoku"); 47 | t.setFill(Color.WHITE); 48 | Font tf = new Font(43); 49 | t.setFont(tf); 50 | root.getChildren().add(t); 51 | Color c = Color.rgb(0, 150, 136); 52 | scene.setFill(c); 53 | stage.setScene(scene); 54 | 55 | Rectangle bg = new Rectangle(); 56 | bg.setX(50); 57 | bg.setY(50); 58 | bg.setWidth(576); 59 | bg.setHeight(576); 60 | bg.setFill(Color.rgb(224, 242, 241)); 61 | root.getChildren().add(bg); 62 | 63 | final int xOrigin = 50; 64 | final int yOrigin = 50; 65 | //how much to move the x or y value after each loop 66 | final int xAndYDelta = 64; 67 | 68 | for (int xIndex = 0; xIndex < 9; xIndex++) { 69 | for (int yIndex = 0; yIndex < 9; yIndex++) { 70 | int x = xOrigin + xIndex * xAndYDelta; 71 | int y = yOrigin + yIndex * xAndYDelta; 72 | //draw it 73 | SudokuTextField stf = new SudokuTextField(xIndex, yIndex); 74 | Font numberFont = new Font(32); 75 | stf.setFont(numberFont); 76 | stf.setAlignment(Pos.CENTER); 77 | stf.setLayoutX(x); 78 | stf.setLayoutY(y); 79 | stf.setPrefHeight(64); 80 | stf.setPrefWidth(64); 81 | stf.setBackground(Background.EMPTY); 82 | stf.setOnKeyPressed(this); 83 | textFieldCoordinates.put(new Coordinates(xIndex, yIndex), stf); 84 | root.getChildren().add(stf); 85 | } 86 | } 87 | 88 | int xAndY = 114; 89 | int index = 0; 90 | while (index < 8) { 91 | Rectangle vl = new Rectangle(); 92 | 93 | vl.setX(xAndY + 64 * index); 94 | vl.setY(50); 95 | vl.setHeight(576); 96 | if (index == 2 || index == 5) { 97 | vl.setWidth(3); 98 | } else { 99 | vl.setWidth(2); 100 | } 101 | vl.setFill(Color.BLACK); 102 | 103 | Rectangle hl = new Rectangle(); 104 | hl.setY(xAndY + 64 * index); 105 | hl.setX(50); 106 | hl.setWidth(576); 107 | if (index == 2 || index == 5) { 108 | hl.setHeight(3); 109 | } else { 110 | hl.setHeight(2); 111 | } 112 | hl.setFill(Color.BLACK); 113 | 114 | root.getChildren().addAll( 115 | vl, 116 | hl 117 | ); 118 | 119 | index++; 120 | } 121 | 122 | stage.show(); 123 | 124 | } 125 | 126 | 127 | @Override 128 | public void setListener(IUserInterfaceContract.EventListener listener) { 129 | this.listener = listener; 130 | } 131 | 132 | /** 133 | * Each time the user makes an input (which can be 0 to delete a number), we update the user 134 | * interface appropriately. 135 | */ 136 | @Override 137 | public void updateSquare(int x, int y, int input) { 138 | SudokuTextField tile = textFieldCoordinates.get(new Coordinates(x, y)); 139 | String value = Integer.toString( 140 | input 141 | ); 142 | 143 | if (value.equals("0")) value = ""; 144 | 145 | tile.textProperty().setValue(value); 146 | } 147 | 148 | @Override 149 | public void updateBoard(SudokuGame game) { 150 | for (int xIndex = 0; xIndex < 9; xIndex++) { 151 | for (int yIndex = 0; yIndex < 9; yIndex++) { 152 | TextField tile = textFieldCoordinates.get(new Coordinates(xIndex, yIndex)); 153 | 154 | String value = Integer.toString( 155 | game.getCopyOfGridState()[xIndex][yIndex] 156 | ); 157 | 158 | if (value.equals("0")) value = ""; 159 | tile.setText( 160 | value 161 | ); 162 | 163 | //If a given tile has a non-zero value and the state of the game is GameState.NEW, then mark 164 | //the tile as read only. Otherwise, ensure that it is NOT read only. 165 | if (game.getGameState() == GameState.NEW){ 166 | if (value.equals("")) { 167 | tile.setStyle("-fx-opacity: 1;"); 168 | tile.setDisable(false); 169 | } else { 170 | tile.setStyle("-fx-opacity: 0.8;"); 171 | tile.setDisable(true); 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | @Override 179 | public void showDialog(String message) { 180 | Alert dialog = new Alert(Alert.AlertType.CONFIRMATION, message, ButtonType.OK); 181 | dialog.showAndWait(); 182 | 183 | if (dialog.getResult() == ButtonType.OK) listener.onDialogClick(); 184 | } 185 | 186 | @Override 187 | public void showError(String message) { 188 | Alert dialog = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK); 189 | dialog.showAndWait(); 190 | } 191 | 192 | 193 | @Override 194 | public void handle(KeyEvent event) { 195 | if (event.getEventType() == KeyEvent.KEY_PRESSED) { 196 | if (event.getText().equals("0") 197 | || event.getText().equals("1") 198 | || event.getText().equals("2") 199 | || event.getText().equals("3") 200 | || event.getText().equals("4") 201 | || event.getText().equals("5") 202 | || event.getText().equals("6") 203 | || event.getText().equals("7") 204 | || event.getText().equals("8") 205 | || event.getText().equals("9") 206 | ) { 207 | int value = Integer.parseInt(event.getText()); 208 | listener.onSudokuInput( 209 | ((SudokuTextField) event.getSource()).getX(), 210 | ((SudokuTextField) event.getSource()).getY(), 211 | value 212 | ); 213 | } else if (event.getCode() == KeyCode.BACK_SPACE) { 214 | listener.onSudokuInput( 215 | ((SudokuTextField) event.getSource()).getX(), 216 | ((SudokuTextField) event.getSource()).getY(), 217 | 0 218 | ); 219 | } else { 220 | ((TextField)event.getSource()).setText(""); 221 | } 222 | } 223 | 224 | event.consume(); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/userinterface/UserInterfaceImpl.java: -------------------------------------------------------------------------------- 1 | package com.wissassblog.sudoku.userinterface; 2 | 3 | import com.wissassblog.sudoku.constants.GameState; 4 | import com.wissassblog.sudoku.problemdomain.Coordinates; 5 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 6 | import javafx.event.EventHandler; 7 | import javafx.geometry.Pos; 8 | import javafx.scene.Group; 9 | import javafx.scene.Scene; 10 | import javafx.scene.control.Alert; 11 | import javafx.scene.control.ButtonType; 12 | import javafx.scene.control.TextField; 13 | import javafx.scene.input.KeyCode; 14 | import javafx.scene.input.KeyEvent; 15 | import javafx.scene.layout.Background; 16 | import javafx.scene.paint.Color; 17 | import javafx.scene.shape.Rectangle; 18 | import javafx.scene.text.Font; 19 | import javafx.scene.text.Text; 20 | import javafx.stage.Stage; 21 | 22 | import java.util.HashMap; 23 | 24 | /** 25 | * Manages the window, and displays a pop up notification when the user completes the puzzle. 26 | */ 27 | public class UserInterfaceImpl implements IUserInterfaceContract.View, 28 | EventHandler { 29 | private final Stage stage; 30 | private final Group root; 31 | 32 | //This HashMap stores the Hash Values (a unique identifier which is automatically generated; 33 | // see java.lang.object in the documentation) of each TextField by their Coordinates. When a SudokuGame 34 | //is given to the updateUI method, we iterate through it by X and Y coordinates and assign the values to the 35 | //appropriate TextField therein. This means we don't need to hold a reference variable for every god damn 36 | //text field in this app; which would be awful. 37 | //The Key ( -> ) will be the HashCode of a given InputField for ease of lookup 38 | private HashMap textFieldCoordinates; 39 | 40 | private IUserInterfaceContract.EventListener listener; 41 | 42 | //Size of the window 43 | private static final double WINDOW_Y = 732; 44 | private static final double WINDOW_X = 668; 45 | //distance between window and board 46 | private static final double BOARD_PADDING = 50; 47 | 48 | private static final double BOARD_X_AND_Y = 576; 49 | private static final Color WINDOW_BACKGROUND_COLOR = Color.rgb(0, 150, 136); 50 | private static final Color BOARD_BACKGROUND_COLOR = Color.rgb(224, 242, 241); 51 | private static final String SUDOKU = "Sudoku"; 52 | 53 | /** 54 | * Stage and Group are JavaFX specific classes for modifying the UI. Think of them as containers of various UI 55 | * components. 56 | * 57 | * A HashMap is a data structure which stores key/value pairs. Rather than creating a member variable for every 58 | * SudokuTextField object (all 81 of them), I instead store these references within a HashMap, and I retrieve 59 | * them by using their X and Y Coordinates as a "key" (a unique value used to look something up). 60 | * 61 | * @param stage 62 | */ 63 | public UserInterfaceImpl(Stage stage) { 64 | this.stage = stage; 65 | this.root = new Group(); 66 | this.textFieldCoordinates = new HashMap<>(); 67 | initializeUserInterface(); 68 | } 69 | 70 | 71 | @Override 72 | public void setListener(IUserInterfaceContract.EventListener listener) { 73 | this.listener = listener; 74 | } 75 | 76 | public void initializeUserInterface() { 77 | drawBackground(root); 78 | drawTitle(root); 79 | drawSudokuBoard(root); 80 | drawTextFields(root); 81 | drawGridLines(root); 82 | stage.show(); 83 | } 84 | 85 | /** 86 | * 1. Draw each TextField based on x and y values. 87 | * 2. As each TextField is drawn, add it's coordinates (x, y) based on it's Hash Value to 88 | * to the HashMap. 89 | * 90 | * @param root 91 | */ 92 | private void drawTextFields(Group root) { 93 | //where to start drawing the numbers 94 | final int xOrigin = 50; 95 | final int yOrigin = 50; 96 | //how much to move the x or y value after each loop 97 | final int xAndYDelta = 64; 98 | 99 | 100 | for (int xIndex = 0; xIndex < 9; xIndex++) { 101 | for (int yIndex = 0; yIndex < 9; yIndex++) { 102 | int x = xOrigin + xIndex * xAndYDelta; 103 | int y = yOrigin + yIndex * xAndYDelta; 104 | //draw it 105 | SudokuTextField tile = new SudokuTextField(xIndex, yIndex); 106 | 107 | //encapsulated style information 108 | styleSudokuTile(tile, x, y); 109 | 110 | //Note: Note that UserInterfaceImpl implements EventHandler in the class declaration. 111 | //By passing "this" (which means the current instance of UserInterfaceImpl), when an action occurs, 112 | //it will jump straight to "handle(ActionEvent actionEvent)" down below. 113 | tile.setOnKeyPressed(this); 114 | 115 | textFieldCoordinates.put(new Coordinates(xIndex, yIndex), tile); 116 | 117 | root.getChildren().add(tile); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Helper method for styling a sudoku tile number 124 | * @param tile 125 | * @param x 126 | * @param y 127 | */ 128 | private void styleSudokuTile(SudokuTextField tile, double x, double y) { 129 | Font numberFont = new Font(32); 130 | tile.setFont(numberFont); 131 | tile.setAlignment(Pos.CENTER); 132 | 133 | tile.setLayoutX(x); 134 | tile.setLayoutY(y); 135 | tile.setPrefHeight(64); 136 | tile.setPrefWidth(64); 137 | 138 | tile.setBackground(Background.EMPTY); 139 | } 140 | 141 | 142 | /** 143 | * In order to draw the various lines that make up the Sudoku grid, we use a starting x and y offset 144 | * value (remember, x grows positively from left to right, and y grows positively from top to bottom). 145 | * Each square is meant to be 64x64 units, so we add that number each time a 146 | * @param root 147 | */ 148 | private void drawGridLines(Group root) { 149 | //draw vertical lines starting at 114x and 114y: 150 | int xAndY = 114; 151 | int index = 0; 152 | while (index < 8) { 153 | int thickness; 154 | if (index == 2 || index == 5) { 155 | thickness = 3; 156 | } else { 157 | thickness = 2; 158 | } 159 | 160 | Rectangle verticalLine = getLine( 161 | xAndY + 64 * index, 162 | BOARD_PADDING, 163 | BOARD_X_AND_Y, 164 | thickness 165 | ); 166 | 167 | Rectangle horizontalLine = getLine( 168 | BOARD_PADDING, 169 | xAndY + 64 * index, 170 | thickness, 171 | BOARD_X_AND_Y 172 | ); 173 | 174 | root.getChildren().addAll( 175 | verticalLine, 176 | horizontalLine 177 | ); 178 | 179 | index++; 180 | } 181 | } 182 | 183 | /** 184 | * Convenience method to reduce repetitious code. 185 | * 186 | * X, Y, Height, Width, 187 | * @return A Rectangle to specification 188 | */ 189 | public Rectangle getLine(double x, double y, double height, double width){ 190 | Rectangle line = new Rectangle(); 191 | 192 | line.setX(x); 193 | line.setY(y); 194 | 195 | line.setHeight(height); 196 | line.setWidth(width); 197 | 198 | line.setFill(Color.BLACK); 199 | return line; 200 | 201 | } 202 | 203 | /** 204 | * Background of the primary window 205 | * @param root 206 | */ 207 | private void drawBackground(Group root) { 208 | Scene scene = new Scene(root, WINDOW_X, WINDOW_Y); 209 | scene.setFill(WINDOW_BACKGROUND_COLOR); 210 | stage.setScene(scene); 211 | } 212 | 213 | /** 214 | * Background of the actual sudoku board, offset from the window by BOARD_PADDING 215 | * @param root 216 | */ 217 | private void drawSudokuBoard(Group root) { 218 | Rectangle boardBackground = new Rectangle(); 219 | boardBackground.setX(BOARD_PADDING); 220 | boardBackground.setY(BOARD_PADDING); 221 | boardBackground.setWidth(BOARD_X_AND_Y); 222 | boardBackground.setHeight(BOARD_X_AND_Y); 223 | boardBackground.setFill(BOARD_BACKGROUND_COLOR); 224 | root.getChildren().add(boardBackground); 225 | } 226 | 227 | private void drawTitle(Group root) { 228 | Text title = new Text(235, 690, SUDOKU); 229 | title.setFill(Color.WHITE); 230 | Font titleFont = new Font(43); 231 | title.setFont(titleFont); 232 | root.getChildren().add(title); 233 | } 234 | 235 | /** 236 | * Each time the user makes an input (which can be 0 to delete a number), we update the user 237 | * interface appropriately. 238 | */ 239 | @Override 240 | public void updateSquare(int x, int y, int input) { 241 | SudokuTextField tile = textFieldCoordinates.get(new Coordinates(x, y)); 242 | String value = Integer.toString( 243 | input 244 | ); 245 | 246 | if (value.equals("0")) value = ""; 247 | 248 | tile.textProperty().setValue(value); 249 | } 250 | 251 | @Override 252 | public void updateBoard(SudokuGame game) { 253 | for (int xIndex = 0; xIndex < 9; xIndex++) { 254 | for (int yIndex = 0; yIndex < 9; yIndex++) { 255 | TextField tile = textFieldCoordinates.get(new Coordinates(xIndex, yIndex)); 256 | 257 | String value = Integer.toString( 258 | game.getCopyOfGridState()[xIndex][yIndex] 259 | ); 260 | 261 | if (value.equals("0")) value = ""; 262 | tile.setText( 263 | value 264 | ); 265 | 266 | //If a given tile has a non-zero value and the state of the game is GameState.NEW, then mark 267 | //the tile as read only. Otherwise, ensure that it is NOT read only. 268 | if (game.getGameState() == GameState.NEW){ 269 | if (value.equals("")) { 270 | tile.setStyle("-fx-opacity: 1;"); 271 | tile.setDisable(false); 272 | } else { 273 | tile.setStyle("-fx-opacity: 0.8;"); 274 | tile.setDisable(true); 275 | } 276 | } 277 | } 278 | } 279 | } 280 | 281 | @Override 282 | public void showDialog(String message) { 283 | Alert dialog = new Alert(Alert.AlertType.CONFIRMATION, message, ButtonType.OK); 284 | dialog.showAndWait(); 285 | 286 | if (dialog.getResult() == ButtonType.OK) listener.onDialogClick(); 287 | } 288 | 289 | @Override 290 | public void showError(String message) { 291 | Alert dialog = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK); 292 | dialog.showAndWait(); 293 | } 294 | 295 | 296 | @Override 297 | public void handle(KeyEvent event) { 298 | if (event.getEventType() == KeyEvent.KEY_PRESSED) { 299 | if (event.getText().equals("0") 300 | || event.getText().equals("1") 301 | || event.getText().equals("2") 302 | || event.getText().equals("3") 303 | || event.getText().equals("4") 304 | || event.getText().equals("5") 305 | || event.getText().equals("6") 306 | || event.getText().equals("7") 307 | || event.getText().equals("8") 308 | || event.getText().equals("9") 309 | ) { 310 | int value = Integer.parseInt(event.getText()); 311 | handleInput(value, event.getSource()); 312 | } else if (event.getCode() == KeyCode.BACK_SPACE) { 313 | handleInput(0, event.getSource()); 314 | } else { 315 | ((TextField)event.getSource()).setText(""); 316 | } 317 | } 318 | 319 | event.consume(); 320 | } 321 | 322 | /** 323 | * @param value expected to be an integer from 0-9, inclusive 324 | * @param source the textfield object that was clicked. 325 | */ 326 | private void handleInput(int value, Object source) { 327 | listener.onSudokuInput( 328 | ((SudokuTextField) source).getX(), 329 | ((SudokuTextField) source).getY(), 330 | value 331 | ); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 testList = Arrays.asList(0, 0, 0, 1, 1, 0, 0, 0, 0); 144 | boolean hasRepeats = GameLogic.collectionHasRepeats(testList); 145 | 146 | assert (hasRepeats); 147 | 148 | } 149 | 150 | /** 151 | * Expected value: false 152 | */ 153 | @Test 154 | public void collectionHasNoRepeats() { 155 | List testListOne = Arrays.asList(0, 0, 0, 0, 0, 0, 0, 0, 0); 156 | List testListTwo = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); 157 | boolean hasRepeatsOne = GameLogic.collectionHasRepeats(testListOne); 158 | boolean hasRepeatsTwo = GameLogic.collectionHasRepeats(testListTwo); 159 | 160 | assert (!hasRepeatsOne); 161 | assert (!hasRepeatsTwo); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/java/TestData.java: -------------------------------------------------------------------------------- 1 | import com.wissassblog.sudoku.constants.GameState; 2 | import com.wissassblog.sudoku.problemdomain.SudokuGame; 3 | 4 | class TestData { 5 | //No access modifier means Package-Private access, i.e. only tests within src/test/java 6 | /** 7 | * Note: Arrays which do not use an initializer have all elements assigned to 0 by default. 8 | * This is convenient, as 0 represents an empty square in the sudoku puzzle. 9 | * generate a valid new puzzle with 30 solved squares 10 | * @return 11 | */ 12 | static SudokuGame getValidStart(){ 13 | int[][] validStart = new int[9][9]; 14 | 15 | //first row 16 | validStart[0][0] = 6; 17 | validStart[4][0] = 4; 18 | validStart[7][0] = 1; 19 | 20 | //... 21 | validStart[0][1] = 9; 22 | validStart[2][1] = 3; 23 | validStart[7][1] = 2; 24 | 25 | //... 26 | validStart[1][2] = 5; 27 | validStart[5][2] = 7; 28 | 29 | validStart[1][3] = 3; 30 | validStart[3][3] = 2; 31 | validStart[4][3] = 1; 32 | validStart[5][3] = 9; 33 | validStart[8][3] = 4; 34 | 35 | validStart[0][4] = 1; 36 | validStart[1][4] = 6; 37 | validStart[2][4] = 2; 38 | validStart[7][4] = 9; 39 | 40 | validStart[1][5] = 7; 41 | validStart[4][5] = 8; 42 | validStart[6][5] = 1; 43 | validStart[8][5] = 2; 44 | 45 | validStart[1][6] = 4; 46 | validStart[6][6] = 8; 47 | 48 | validStart[0][7] = 7; 49 | validStart[1][7] = 2; 50 | validStart[2][7] = 6; 51 | validStart[5][7] = 1; 52 | validStart[6][7] = 3; 53 | 54 | validStart[4][8] = 7; 55 | validStart[8][8] = 6; 56 | 57 | SudokuGame validStartGame = new SudokuGame(GameState.ACTIVE, validStart); 58 | return validStartGame; 59 | } 60 | 61 | static SudokuGame getSolved(){ 62 | int[][] solved = new int[9][9]; 63 | 64 | //first group of 9 65 | solved[0][0] = 6; 66 | solved[1][0] = 8; 67 | solved[2][0] = 7; 68 | solved[0][1] = 9; 69 | solved[1][1] = 1; 70 | solved[2][1] = 3; 71 | solved[0][2] = 2; 72 | solved[1][2] = 5; 73 | solved[2][2] = 4; 74 | 75 | 76 | //... 77 | solved[3][0] = 3; 78 | solved[4][0] = 4; 79 | solved[5][0] = 2; 80 | solved[3][1] = 5; 81 | solved[4][1] = 6; 82 | solved[5][1] = 8; 83 | solved[3][2] = 1; 84 | solved[4][2] = 9; 85 | solved[5][2] = 7; 86 | 87 | 88 | //... 89 | solved[6][0] = 9; 90 | solved[7][0] = 1; 91 | solved[8][0] = 5; 92 | solved[6][1] = 4; 93 | solved[7][1] = 2; 94 | solved[8][1] = 7; 95 | solved[6][2] = 6; 96 | solved[7][2] = 8; 97 | solved[8][2] = 3; 98 | 99 | solved[0][3] = 5; 100 | solved[1][3] = 3; 101 | solved[2][3] = 8; 102 | solved[0][4] = 1; 103 | solved[1][4] = 6; 104 | solved[2][4] = 2; 105 | solved[0][5] = 4; 106 | solved[1][5] = 7; 107 | solved[2][5] = 9; 108 | 109 | solved[3][3] = 2; 110 | solved[4][3] = 1; 111 | solved[5][3] = 9; 112 | solved[3][4] = 7; 113 | solved[4][4] = 3; 114 | solved[5][4] = 4; 115 | solved[3][5] = 6; 116 | solved[4][5] = 8; 117 | solved[5][5] = 5; 118 | 119 | solved[6][3] = 7; 120 | solved[7][3] = 6; 121 | solved[8][3] = 4; 122 | solved[6][4] = 5; 123 | solved[7][4] = 9; 124 | solved[8][4] = 8; 125 | solved[6][5] = 1; 126 | solved[7][5] = 3; 127 | solved[8][5] = 2; 128 | 129 | solved[0][6] = 3; 130 | solved[1][6] = 4; 131 | solved[2][6] = 5; 132 | solved[0][7] = 7; 133 | solved[1][7] = 2; 134 | solved[2][7] = 6; 135 | solved[0][8] = 8; 136 | solved[1][8] = 9; 137 | solved[2][8] = 1; 138 | 139 | solved[3][6] = 9; 140 | solved[4][6] = 2; 141 | solved[5][6] = 6; 142 | solved[3][7] = 8; 143 | solved[4][7] = 5; 144 | solved[5][7] = 1; 145 | solved[3][8] = 4; 146 | solved[4][8] = 7; 147 | solved[5][8] = 3; 148 | 149 | solved[6][6] = 8; 150 | solved[7][6] = 7; 151 | solved[8][6] = 1; 152 | solved[6][7] = 3; 153 | solved[7][7] = 4; 154 | solved[8][7] = 9; 155 | solved[6][8] = 2; 156 | solved[7][8] = 5; 157 | solved[8][8] = 6; 158 | 159 | SudokuGame solvedGame = new SudokuGame(GameState.COMPLETE, solved); 160 | 161 | return solvedGame; 162 | } 163 | 164 | static SudokuGame getInvalid(){ 165 | int[][] invalid = new int[9][9]; 166 | 167 | //first group of 9 168 | invalid[0][0] = 2; 169 | invalid[1][0] = 2; 170 | invalid[2][0] = 2; 171 | invalid[0][1] = 2; 172 | invalid[1][1] = 2; 173 | invalid[2][1] = 2; 174 | invalid[0][2] = 2; 175 | invalid[1][2] = 2; 176 | invalid[2][2] = 2; 177 | 178 | SudokuGame invalidGame = new SudokuGame(GameState.ACTIVE, invalid); 179 | 180 | return invalidGame; 181 | } 182 | } 183 | --------------------------------------------------------------------------------