├── .gitattributes ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── README.md ├── src ├── test │ ├── resources │ │ └── com │ │ │ └── abhyudayasharma │ │ │ └── sudoku │ │ │ └── sudoku1.csv │ └── java │ │ └── com │ │ └── abhyudayasharma │ │ └── sudoku │ │ ├── AppTest.java │ │ └── SudokuBoardTest.java └── main │ └── java │ └── com │ └── abhyudayasharma │ └── sudoku │ ├── core │ ├── MoveType.java │ ├── AbstractMove.java │ ├── Result.java │ ├── BacktrackingMove.java │ ├── AssignmentMove.java │ └── SudokuSolver.java │ ├── Main.java │ ├── ui │ ├── SudokuTableCellRenderer.java │ ├── SudokuTable.java │ ├── SudokuCellEditor.java │ └── SudokuTableModel.java │ ├── SudokuBoard.java │ └── Sudoku.java ├── .gitignore ├── .editorconfig ├── .github └── workflows │ └── gradle.yml ├── LICENSE ├── settings.gradle ├── gradlew.bat └── gradlew /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bat eol=crlf 2 | *.csv eol=crlf # RFC4180 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhyudayaSharma/sudoku/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sudoku 2 | 3 | A Sudoku solver for a college project. 4 | 5 | 6 | 7 | To run the application: 8 | 9 | ```bash 10 | ./gradlew run 11 | ``` 12 | -------------------------------------------------------------------------------- /src/test/resources/com/abhyudayasharma/sudoku/sudoku1.csv: -------------------------------------------------------------------------------- 1 | 1,,3,4,5,6,7,8,9 2 | 1,,3,4,5,6,7,8,9 3 | 1,,3,4,5,6,7,8,9 4 | 1,,3,4,5,6,7,8,9 5 | 1,,3,4,5,6,7,8,9 6 | 1,,3,4,5,6,7,8,9 7 | 1,,3,4,5,6,7,8,9 8 | 1,,3,4,5,6,7,8,9 9 | 1,,3,4,5,6,7,8,9 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | 4 | /build/ 5 | 6 | # Ignore Gradle GUI config 7 | gradle-app.setting 8 | 9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 10 | !gradle-wrapper.jar 11 | 12 | # Cache of project 13 | .gradletasknamecache 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | max_line_length = 120 10 | insert_final_newline = true 11 | tab_width = 4 12 | 13 | [*.bat] 14 | end_of_line = crlf 15 | 16 | [*.csv] # RFC4180 17 | end_of_line = crlf 18 | 19 | [*.properties] 20 | charset = latin1 21 | 22 | [*.{yml, yaml}] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macOS-latest] 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Set up JDK 11 15 | uses: actions/setup-java@v1 16 | with: 17 | java-version: 11 18 | - name: Build with Gradle 19 | run: ./gradlew build 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Abhyudaya Sharma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | rootProject.name = 'sudoku' 26 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/core/MoveType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.core; 26 | 27 | public enum MoveType { 28 | ASSIGNMENT, 29 | BACKTRACK 30 | } 31 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # 2 | # MIT License 3 | # 4 | # Copyright (c) 2019 Abhyudaya Sharma 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | #Wed Aug 28 17:23:11 IST 2019 24 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip 25 | distributionBase=GRADLE_USER_HOME 26 | distributionPath=wrapper/dists 27 | zipStorePath=wrapper/dists 28 | zipStoreBase=GRADLE_USER_HOME 29 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/core/AbstractMove.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.core; 26 | 27 | import lombok.AllArgsConstructor; 28 | import lombok.Getter; 29 | 30 | @AllArgsConstructor 31 | public abstract class AbstractMove { 32 | @Getter 33 | final MoveType moveType; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/core/Result.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.core; 26 | 27 | import com.abhyudayasharma.sudoku.SudokuBoard; 28 | import lombok.NonNull; 29 | import lombok.Value; 30 | 31 | @Value 32 | @SuppressWarnings("WeakerAccess") 33 | public class Result { 34 | @NonNull 35 | final SudokuBoard board; 36 | final int backTrackCount; 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/abhyudayasharma/sudoku/AppTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku; 26 | 27 | import org.junit.jupiter.api.Test; 28 | 29 | import static org.junit.jupiter.api.Assertions.assertTrue; 30 | 31 | class AppTest { 32 | /** 33 | * Sample test for the application. 34 | */ 35 | @Test 36 | void test() { 37 | assertTrue(true); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku; 26 | 27 | import lombok.extern.slf4j.Slf4j; 28 | 29 | import javax.swing.SwingUtilities; 30 | import javax.swing.UIManager; 31 | import javax.swing.UnsupportedLookAndFeelException; 32 | import javax.swing.plaf.nimbus.NimbusLookAndFeel; 33 | 34 | @Slf4j 35 | public class Main { 36 | public static void main(String[] args) { 37 | try { 38 | UIManager.setLookAndFeel(new NimbusLookAndFeel()); 39 | } catch (UnsupportedLookAndFeelException e) { 40 | log.warn("Unable to set Nimbus Look and Feel for the application.", e); 41 | } 42 | 43 | SwingUtilities.invokeLater(() -> new Sudoku().start()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/core/BacktrackingMove.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.core; 26 | 27 | import lombok.Getter; 28 | import lombok.ToString; 29 | import lombok.experimental.FieldDefaults; 30 | 31 | @Getter 32 | @ToString 33 | @FieldDefaults(makeFinal = true) 34 | class BacktrackingMove extends AbstractMove { 35 | private final int fromRow; 36 | private final int fromCol; 37 | private final int toRow; 38 | private final int toCol; 39 | 40 | BacktrackingMove(int fromRow, int fromCol, int toRow, int toCol) { 41 | super(MoveType.BACKTRACK); 42 | this.fromRow = fromRow; 43 | this.fromCol = fromCol; 44 | this.toRow = toRow; 45 | this.toCol = toCol; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/core/AssignmentMove.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.core; 26 | 27 | import lombok.EqualsAndHashCode; 28 | import lombok.Getter; 29 | import lombok.ToString; 30 | 31 | @ToString 32 | @EqualsAndHashCode(callSuper = false) 33 | public 34 | class AssignmentMove extends AbstractMove { 35 | @Getter 36 | private final int row; 37 | @Getter 38 | private final int col; 39 | @Getter 40 | private final int oldValue; 41 | @Getter 42 | private final int newValue; 43 | 44 | AssignmentMove(int row, int col, int oldValue, int newValue) { 45 | super(MoveType.ASSIGNMENT); 46 | this.row = row; 47 | this.col = col; 48 | this.oldValue = oldValue; 49 | this.newValue = newValue; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/test/java/com/abhyudayasharma/sudoku/SudokuBoardTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku; 26 | 27 | import org.junit.jupiter.api.Test; 28 | 29 | import static org.junit.jupiter.api.Assertions.assertEquals; 30 | import static org.junit.jupiter.api.Assertions.assertTrue; 31 | 32 | class SudokuBoardTest { 33 | @Test 34 | void sizeTest() { 35 | assertEquals(9, SudokuBoard.SIZE); 36 | var sqrt = Math.sqrt(SudokuBoard.SIZE); 37 | 38 | // should be a perfect square 39 | assertTrue(sqrt == Math.floor(sqrt) && !Double.isInfinite(sqrt)); 40 | } 41 | 42 | @Test 43 | void csvLoadTest1() throws Exception { 44 | final int[][] expected = new int[][]{ 45 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 46 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 47 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 48 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 49 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 50 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 51 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 52 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 53 | {1, 0, 3, 4, 5, 6, 7, 8, 9}, 54 | }; 55 | 56 | SudokuBoard board = SudokuBoard.load(getClass().getResource("sudoku1.csv").toURI()); 57 | for (int i = 0; i < SudokuBoard.SIZE; i++) { 58 | for (int j = 0; j < SudokuBoard.SIZE; j++) { 59 | assertEquals(expected[i][j], board.get(i, j).orElse(0)); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/ui/SudokuTableCellRenderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.ui; 26 | 27 | import com.abhyudayasharma.sudoku.SudokuBoard; 28 | 29 | import javax.swing.BorderFactory; 30 | import javax.swing.JComponent; 31 | import javax.swing.JTable; 32 | import javax.swing.SwingConstants; 33 | import javax.swing.table.DefaultTableCellRenderer; 34 | import java.awt.Color; 35 | import java.awt.Component; 36 | 37 | public class SudokuTableCellRenderer extends DefaultTableCellRenderer { 38 | private final int sqrt = (int) Math.rint(Math.sqrt(SudokuBoard.SIZE)); 39 | private final Color borderColor = Color.BLACK; 40 | 41 | @Override 42 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, 43 | boolean hasFocus, int row, int column) { 44 | final var component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 45 | var border = BorderFactory.createEmptyBorder(); 46 | 47 | final var borderThickness = 3; 48 | if (row % sqrt == sqrt - 1 && row != SudokuBoard.SIZE - 1) { 49 | border = BorderFactory.createCompoundBorder(border, 50 | BorderFactory.createMatteBorder(0, 0, borderThickness, 0, borderColor)); 51 | } 52 | 53 | if (column % sqrt == sqrt - 1 && column != SudokuBoard.SIZE - 1) { 54 | border = BorderFactory.createCompoundBorder(border, 55 | BorderFactory.createMatteBorder(0, 0, 0, borderThickness, borderColor)); 56 | } 57 | 58 | if (component instanceof JComponent) { 59 | ((JComponent) component).setBorder(border); 60 | } else { 61 | throw new IllegalArgumentException("component should be a JComponent"); 62 | } 63 | 64 | return component; 65 | } 66 | 67 | @Override 68 | public int getHorizontalAlignment() { 69 | return SwingConstants.CENTER; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/ui/SudokuTable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.ui; 26 | 27 | import com.abhyudayasharma.sudoku.Sudoku; 28 | import com.abhyudayasharma.sudoku.SudokuBoard; 29 | 30 | import javax.swing.BorderFactory; 31 | import javax.swing.JTable; 32 | import javax.swing.ListSelectionModel; 33 | import javax.swing.table.DefaultTableCellRenderer; 34 | import javax.swing.table.TableCellEditor; 35 | import javax.swing.table.TableCellRenderer; 36 | import java.awt.Color; 37 | import java.net.URI; 38 | 39 | public class SudokuTable extends JTable { 40 | private static final int CELL_SIZE = 60; 41 | 42 | private static final DefaultTableCellRenderer defaultRenderer = new SudokuTableCellRenderer(); 43 | 44 | @Override 45 | public SudokuTableModel getModel() { 46 | return (SudokuTableModel) super.getModel(); 47 | } 48 | 49 | @Override 50 | public TableCellEditor getCellEditor(int row, int column) { 51 | return new SudokuCellEditor(); 52 | } 53 | 54 | public SudokuTable() { 55 | super(new SudokuTableModel()); 56 | cellSelectionEnabled = rowSelectionAllowed = false; 57 | selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 58 | showVerticalLines = true; 59 | showHorizontalLines = true; 60 | autoResizeMode = JTable.AUTO_RESIZE_OFF; 61 | rowHeight = CELL_SIZE; 62 | tableHeader = null; 63 | 64 | setColumnCellEditors(); 65 | setFont(Sudoku.BOARD_FONT); 66 | setBorder(BorderFactory.createLineBorder(Color.BLACK, 2, false)); 67 | } 68 | 69 | @Override 70 | public TableCellRenderer getCellRenderer(int row, int column) { 71 | return defaultRenderer; 72 | } 73 | 74 | public SudokuBoard getBoard() { 75 | return getModel().asBoard(); 76 | } 77 | 78 | private void setColumnCellEditors() { 79 | for (var it = columnModel.getColumns().asIterator(); it.hasNext(); ) { 80 | var column = it.next(); 81 | column.setPreferredWidth(CELL_SIZE); 82 | column.setCellEditor(new SudokuCellEditor()); 83 | } 84 | } 85 | 86 | /** 87 | * Load the table from a CSV file 88 | * 89 | * @param uri uri to a CSV fil 90 | * @throws Exception if unable to load the file 91 | */ 92 | public void load(URI uri) throws Exception { 93 | setModel(new SudokuTableModel(uri)); 94 | setColumnCellEditors(); 95 | } 96 | 97 | public void clear() { 98 | getModel().clear(); 99 | repaint(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/ui/SudokuCellEditor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.ui; 26 | 27 | import com.abhyudayasharma.sudoku.Sudoku; 28 | import com.abhyudayasharma.sudoku.SudokuBoard; 29 | 30 | import javax.swing.DefaultCellEditor; 31 | import javax.swing.JOptionPane; 32 | import javax.swing.JTextField; 33 | import javax.swing.SwingUtilities; 34 | import javax.swing.event.DocumentEvent; 35 | import javax.swing.event.DocumentListener; 36 | 37 | /** 38 | * A {@link javax.swing.CellEditor} that validates input for a sudoku cell and sets the font specified by 39 | * {@link Sudoku#BOARD_FONT}. 40 | * 41 | * @author Abhyudaya Sharma 42 | */ 43 | class SudokuCellEditor extends DefaultCellEditor { 44 | SudokuCellEditor() { 45 | super(new SudokuTextField()); 46 | } 47 | 48 | private static class SudokuTextField extends JTextField { 49 | SudokuTextField() { 50 | setFont(Sudoku.BOARD_FONT); 51 | setHorizontalAlignment(CENTER); 52 | 53 | getDocument().addDocumentListener(new DocumentListener() { 54 | @Override 55 | public void insertUpdate(DocumentEvent documentEvent) { 56 | checkAndWarn(); 57 | } 58 | 59 | @Override 60 | public void removeUpdate(DocumentEvent documentEvent) { 61 | checkAndWarn(); 62 | } 63 | 64 | @Override 65 | public void changedUpdate(DocumentEvent documentEvent) { 66 | checkAndWarn(); 67 | } 68 | 69 | /** 70 | * Validates the input as a possible number for a sudoku square. 71 | *

72 | * If the integer parsed from the text of the {@link JTextField} is valid, the text is left unmodified. 73 | * Otherwise, the text is set to an empty string. If the parsed number was not in range for the puzzle, 74 | * a message box is generated. 75 | */ 76 | private void checkAndWarn() { 77 | final var text = SudokuTextField.this.getText(); 78 | 79 | // blank input is invalid; empty is fine 80 | if (text.isEmpty()) { 81 | return; 82 | } 83 | 84 | var parsedInt = 0; 85 | var doClearTextField = false; 86 | try { 87 | parsedInt = Integer.parseInt(text); 88 | } catch (NumberFormatException e) { 89 | doClearTextField = true; 90 | } 91 | 92 | if (!doClearTextField && (parsedInt > SudokuBoard.SIZE || parsedInt <= 0)) { 93 | SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog( 94 | SudokuTextField.this.getTopLevelAncestor(), 95 | String.format("Please enter an integer between %d and %d", 1, SudokuBoard.SIZE), 96 | "Invalid input", JOptionPane.WARNING_MESSAGE)); 97 | 98 | doClearTextField = true; 99 | } 100 | 101 | if (doClearTextField) { 102 | SwingUtilities.invokeLater(() -> SudokuTextField.this.setText("")); 103 | } 104 | } 105 | }); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/ui/SudokuTableModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.ui; 26 | 27 | import com.abhyudayasharma.sudoku.SudokuBoard; 28 | import lombok.Getter; 29 | import lombok.Setter; 30 | import org.apache.commons.csv.CSVFormat; 31 | import org.apache.commons.csv.CSVPrinter; 32 | import org.apache.commons.io.FilenameUtils; 33 | 34 | import javax.swing.event.TableModelEvent; 35 | import javax.swing.event.TableModelListener; 36 | import javax.swing.table.TableModel; 37 | import java.io.BufferedWriter; 38 | import java.io.File; 39 | import java.io.FileWriter; 40 | import java.io.IOException; 41 | import java.net.URI; 42 | import java.nio.charset.StandardCharsets; 43 | import java.util.ArrayList; 44 | import java.util.Collections; 45 | import java.util.List; 46 | import java.util.Vector; 47 | 48 | public class SudokuTableModel implements TableModel { 49 | private final List> data; 50 | private final List listeners = new Vector<>(); 51 | 52 | @Getter 53 | @Setter 54 | private boolean isEditable = true; 55 | 56 | SudokuTableModel() { 57 | data = new ArrayList<>(); 58 | for (int i = 0; i < SudokuBoard.SIZE; i++) { 59 | var row = new ArrayList<>(Collections.nCopies(SudokuBoard.SIZE, "")); 60 | data.add(row); 61 | } 62 | } 63 | 64 | public SudokuTableModel(SudokuBoard board) { 65 | data = board.asList(); 66 | } 67 | 68 | SudokuTableModel(URI uri) throws Exception { 69 | SudokuBoard board = SudokuBoard.load(uri); 70 | data = board.asList(); 71 | } 72 | 73 | @Override 74 | public int getRowCount() { 75 | return SudokuBoard.SIZE; 76 | } 77 | 78 | @Override 79 | public int getColumnCount() { 80 | return SudokuBoard.SIZE; 81 | } 82 | 83 | @Override 84 | public String getColumnName(int i) { 85 | // columns have no name 86 | return ""; 87 | } 88 | 89 | @Override 90 | public Class getColumnClass(int i) { 91 | return String.class; 92 | } 93 | 94 | @Override 95 | public boolean isCellEditable(int i, int i1) { 96 | return isEditable; 97 | } 98 | 99 | @Override 100 | public synchronized Object getValueAt(int row, int col) { 101 | return data.get(row).get(col); 102 | } 103 | 104 | @Override 105 | public synchronized void setValueAt(Object o, int row, int col) { 106 | if (!(o instanceof String)) { 107 | throw new IllegalArgumentException("The object passed should be a String"); 108 | } 109 | data.get(row).set(col, (String) o); 110 | listeners.forEach(listener -> listener.tableChanged(new TableModelEvent(this, row))); 111 | } 112 | 113 | @Override 114 | public void addTableModelListener(TableModelListener tableModelListener) { 115 | listeners.add(tableModelListener); 116 | } 117 | 118 | @Override 119 | public void removeTableModelListener(TableModelListener tableModelListener) { 120 | listeners.remove(tableModelListener); 121 | } 122 | 123 | synchronized SudokuBoard asBoard() { 124 | return new SudokuBoard(data); 125 | } 126 | 127 | /** 128 | * Save the current table model as a CSV 129 | * 130 | * @param file the file to be written to 131 | * @throws IOException when unable to write the file 132 | */ 133 | public void save(File file) throws IOException { 134 | if (!FilenameUtils.getExtension(file.getName()).equalsIgnoreCase("csv")) { 135 | file = new File(file.toString() + ".csv"); 136 | } 137 | try (CSVPrinter printer = new CSVPrinter(new BufferedWriter(new FileWriter( 138 | file, StandardCharsets.UTF_8)), CSVFormat.RFC4180)) { 139 | printer.printRecords(data); 140 | } 141 | } 142 | 143 | /** 144 | * Clear all data from the model. 145 | */ 146 | synchronized void clear() { 147 | data.forEach(List::clear); 148 | data.forEach(row -> row.addAll(Collections.nCopies(SudokuBoard.SIZE, ""))); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/core/SudokuSolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku.core; 26 | 27 | import com.abhyudayasharma.sudoku.SudokuBoard; 28 | import lombok.extern.slf4j.Slf4j; 29 | import lombok.val; 30 | import org.apache.commons.lang3.tuple.Pair; 31 | 32 | import javax.swing.SwingWorker; 33 | import java.util.ArrayList; 34 | import java.util.Arrays; 35 | import java.util.EmptyStackException; 36 | import java.util.HashSet; 37 | import java.util.List; 38 | import java.util.Set; 39 | import java.util.Stack; 40 | import java.util.concurrent.ExecutionException; 41 | import java.util.stream.Collectors; 42 | 43 | import static com.abhyudayasharma.sudoku.SudokuBoard.SIZE; 44 | 45 | /** 46 | * Artificial Intelligence for solving a sudoku puzzle. 47 | * 48 | * @author Abhyudaya Sharma 49 | */ 50 | @Slf4j 51 | public abstract class SudokuSolver extends SwingWorker { 52 | private static final int sqrt = (int) Math.rint(Math.sqrt(SIZE)); 53 | 54 | private final int delay; 55 | 56 | protected SudokuSolver(SudokuBoard board, int delay) { 57 | initialBoard = board; 58 | matrix = initialBoard.asMatrix(); 59 | if (delay < 0) { 60 | throw new IllegalArgumentException("Delay should be greater than 0"); 61 | } 62 | this.delay = delay; 63 | } 64 | 65 | private final SudokuBoard initialBoard; 66 | private final int[][] matrix; 67 | private final Stack moves = new Stack<>(); 68 | 69 | /** 70 | * Set of values contained by each row. 71 | */ 72 | private final List> rowValues = new ArrayList<>(SIZE); 73 | /** 74 | * Set of values contained by each column. 75 | */ 76 | private final List> colValues = new ArrayList<>(SIZE); 77 | /** 78 | * Set of values contained by the small squares inside the board. 79 | * The top left has index 0. The bottom right has index {@link SudokuBoard#SIZE} {@code - 1}. 80 | */ 81 | private final List> boxValues = new ArrayList<>(SIZE); 82 | private boolean isSolved = false; 83 | private int backtrackCount = 0; 84 | 85 | @Override 86 | protected abstract void done(); 87 | 88 | @Override 89 | protected abstract void process(List chunks); 90 | 91 | /** 92 | * Solves a {@link SudokuBoard} and returns a new fully solved one. 93 | */ 94 | private Result solve() throws ExecutionException, InterruptedException { 95 | if (isSolved) { 96 | return get(); 97 | } 98 | 99 | isSolved = true; 100 | 101 | if (!initialBoard.isValid()) { 102 | throw new IllegalArgumentException("The sudoku board is not valid."); 103 | } 104 | 105 | initSets(); 106 | try { 107 | for (int i = 0; i < matrix.length; i++) { 108 | for (int j = 0; j < matrix.length; j++) { 109 | if (initialBoard.get(i, j).isPresent()) { 110 | continue; 111 | } 112 | 113 | boolean wasAdded = false; 114 | val oldValue = matrix[i][j]; 115 | val boxIndex = getBoxIndex(i, j); 116 | 117 | for (var cellValue = oldValue + 1; cellValue <= SIZE; cellValue++) { 118 | if (!(rowValues.get(i).contains(cellValue) || colValues.get(j).contains(cellValue) || 119 | boxValues.get(boxIndex).contains(cellValue))) { 120 | matrix[i][j] = cellValue; 121 | rowValues.get(i).add(cellValue); 122 | colValues.get(j).add(cellValue); 123 | boxValues.get(boxIndex).add(cellValue); 124 | wasAdded = true; 125 | val move = new AssignmentMove(i, j, oldValue, cellValue); 126 | moves.push(move); 127 | publish(move); 128 | // make it slower 129 | Thread.sleep(delay); 130 | break; 131 | } 132 | } 133 | 134 | // go to the previous cell and try to increment the value 135 | if (!wasAdded) { 136 | matrix[i][j] = 0; 137 | val previousCell = backtrack(); 138 | var row = previousCell.getLeft(); 139 | var col = previousCell.getRight(); 140 | val value = matrix[row][col]; 141 | publish(new BacktrackingMove(i, j, row, col)); 142 | 143 | // remove values because the incremented value would be set up in the next iteration 144 | rowValues.get(row).remove(value); 145 | colValues.get(col).remove(value); 146 | boxValues.get(getBoxIndex(row, col)).remove(value); 147 | 148 | // set up i and j to become equal to row and col for the next iteration 149 | i = row; 150 | j = col - 1; 151 | } 152 | } 153 | } 154 | } catch (EmptyStackException e) { 155 | // moves stack becomes empty when there is no possible value to be put in the puzzle 156 | // and throws the EmptyStackException. This means that the puzzle is invalid. 157 | // for example, consider the puzzle 158 | // 1 2 3 4 5 6 7 8 X 159 | // X X X X X X X X 2 160 | // X X X X X X X X 3 161 | // X X X X X X X X 4 162 | // X X X X X X X X 5 163 | // X X X X X X X X 6 164 | // X X X X X X X X 7 165 | // X X X X X X X X 8 166 | // X X X X X X X X 9 167 | // taken from https://boards.straightdope.com/sdmb/archive/index.php/t-458783.html which 168 | // is valid but not a correct sudoku puzzle. 169 | throw new IllegalArgumentException("The entered pattern is not a valid sudoku puzzle.", e); 170 | } 171 | 172 | val resultBoard = new SudokuBoard( 173 | Arrays.stream(matrix).map(row -> Arrays.stream(row).mapToObj(String::valueOf).collect(Collectors.toUnmodifiableList())) 174 | .collect(Collectors.toUnmodifiableList())); 175 | 176 | return new Result(resultBoard, backtrackCount); 177 | } 178 | 179 | /** 180 | * Find the cell least recently visited cell whose value can still be increased. 181 | * In the process, reset the cells that were visited. 182 | * 183 | * @return which row and column to iterate from in the next loop, respectively 184 | */ 185 | private Pair backtrack() { 186 | backtrackCount++; 187 | var move = moves.pop(); 188 | 189 | while (matrix[move.getRow()][move.getCol()] > SIZE) { 190 | val row = move.getRow(); 191 | val col = move.getCol(); 192 | val value = matrix[row][col]; 193 | 194 | rowValues.get(row).remove(value); 195 | colValues.get(col).remove(value); 196 | boxValues.get(getBoxIndex(row, col)).remove(value); 197 | matrix[row][col] = 0; 198 | publish(new AssignmentMove(row, col, value, 0)); 199 | 200 | move = moves.pop(); 201 | } 202 | 203 | return Pair.of(move.getRow(), move.getCol()); 204 | } 205 | 206 | private int getBoxIndex(int row, int col) { 207 | val boxRow = row / sqrt; 208 | val boxCol = col / sqrt; 209 | return boxRow * sqrt + boxCol; 210 | } 211 | 212 | private void initSets() { 213 | for (int i = 0; i < SIZE; i++) { 214 | rowValues.add(new HashSet<>()); 215 | colValues.add(new HashSet<>()); 216 | boxValues.add(new HashSet<>()); 217 | } 218 | 219 | for (int i = 0; i < matrix.length; i++) { 220 | for (int j = 0; j < matrix.length; j++) { 221 | val row = i; 222 | val col = j; 223 | initialBoard.get(i, j).ifPresent(x -> { 224 | rowValues.get(row).add(x); 225 | colValues.get(col).add(x); 226 | boxValues.get(getBoxIndex(row, col)).add(x); 227 | }); 228 | } 229 | } 230 | } 231 | 232 | @Override 233 | protected Result doInBackground() throws Exception { 234 | return solve(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/SudokuBoard.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku; 26 | 27 | import lombok.extern.slf4j.Slf4j; 28 | import org.apache.commons.csv.CSVFormat; 29 | import org.apache.commons.csv.CSVParser; 30 | import org.apache.commons.csv.CSVRecord; 31 | 32 | import java.io.IOException; 33 | import java.net.URI; 34 | import java.nio.charset.StandardCharsets; 35 | import java.util.ArrayList; 36 | import java.util.Arrays; 37 | import java.util.HashSet; 38 | import java.util.List; 39 | import java.util.Optional; 40 | import java.util.stream.Collectors; 41 | 42 | /** 43 | * A 9x9 matrix that can be used as a Sudoku board. 44 | *

45 | * Internally, {@code 0} values are considered empty values. Values less than {@code 0} or greater than 46 | * {@link SudokuBoard#SIZE} are not valid. 47 | * 48 | * @author Abhyudaya Sharma 49 | */ 50 | @Slf4j 51 | public class SudokuBoard { 52 | public static final int SIZE = 9; 53 | private final int[][] matrix; 54 | 55 | /** 56 | * Creates a sudoku board from a list of list os {@link String}s. 57 | * Empty strings are considered as empty sudoku cells. 58 | * 59 | * @param strings raw strings 60 | * @throws IllegalArgumentException if any of the {@link String} values is not valid in the Sudoku. 61 | */ 62 | public SudokuBoard(List> strings) { 63 | matrix = new int[SIZE][SIZE]; 64 | for (int i = 0; i < SIZE; i++) { 65 | for (int j = 0; j < SIZE; j++) { 66 | matrix[i][j] = parseValue(strings.get(i).get(j)); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Creates a SudokuBoard from the given matrix. 73 | * 74 | * @param matrix initialize the sudoku board from the given matrix 75 | */ 76 | private SudokuBoard(int[][] matrix) { 77 | var isInvalid = false; 78 | if (matrix.length != SIZE) { 79 | isInvalid = true; 80 | } 81 | 82 | isInvalid = isInvalid || !Arrays.stream(matrix).mapToInt(row -> row.length).allMatch(length -> length == SIZE); 83 | isInvalid = isInvalid || !Arrays.stream(matrix).allMatch( 84 | row -> Arrays.stream(row).allMatch(x -> x >= 0 && x <= SIZE)); 85 | 86 | if (isInvalid) { 87 | throw new IllegalArgumentException(String.format("The size of the matrix should be %d x %d", SIZE, SIZE)); 88 | } 89 | 90 | this.matrix = matrix; 91 | } 92 | 93 | /** 94 | * Creates a {@link SudokuBoard} from the CSV file. 95 | * 96 | * @param uri the {@link URI} to the CSV file that will be used to initialize the sudoku board. 97 | * @return A {@link SudokuBoard} with values read from the CSV file 98 | * @throws IOException if an I/O error takes place when trying to read the file 99 | * @throws IllegalArgumentException if any entry of the CSV is not a positive integer 100 | */ 101 | public static SudokuBoard load(URI uri) throws IOException { 102 | var parser = CSVParser.parse(uri.toURL(), StandardCharsets.UTF_8, CSVFormat.RFC4180); 103 | int[][] matrix = new int[SIZE][SIZE]; 104 | 105 | List records = parser.getRecords(); 106 | 107 | for (CSVRecord record : records) { 108 | long recordNumber = record.getRecordNumber() - 1; // make the record number zero indexed 109 | if (recordNumber >= SIZE) { 110 | log.warn("The CSV file contains {} records, will consider only the first {} records.", 111 | records.size(), SIZE); 112 | break; 113 | } 114 | 115 | int valueCount = 0; 116 | for (String value : record) { 117 | if (valueCount >= SIZE) { 118 | log.warn("The record number {} contains {} values, will consider only the first {} values.", 119 | recordNumber, record.size(), SIZE); 120 | break; 121 | } 122 | 123 | matrix[(int) recordNumber][valueCount] = parseValue(value); 124 | 125 | valueCount++; 126 | } 127 | } 128 | 129 | return new SudokuBoard(matrix); 130 | } 131 | 132 | /** 133 | * Parse a valid sudoku integer from the given string value. 134 | * 135 | * @param value the string value to be parsed into a valid sudoku integer. 136 | * @return parsed integer value valid for the Sudoku 137 | * @throws IllegalArgumentException when string is invalid for the value of a sudoku cell 138 | */ 139 | private static int parseValue(String value) { 140 | var parsedInt = 0; 141 | 142 | if (!value.isBlank()) { // blank values should mean empty sudoku squares 143 | try { 144 | parsedInt = Integer.parseInt(value); 145 | } catch (NumberFormatException e) { 146 | throw new IllegalArgumentException( 147 | String.format("The value \"%s\" cannot be parsed as an integer", value), e); 148 | } 149 | 150 | if (parsedInt < 1 || parsedInt > SIZE) { 151 | throw new IllegalArgumentException( 152 | String.format("The number \"%d\" is not valid as the value of a sudoku block", parsedInt)); 153 | } 154 | } 155 | 156 | return parsedInt; 157 | } 158 | 159 | /** 160 | * Return the value at a particular row or a column. 161 | * 162 | * @param row the row index 163 | * @param col the column index 164 | * @return {@link Optional#empty()} if no element is present at that cell. 165 | * @throws IndexOutOfBoundsException if the row and column indices are out of bounds. 166 | */ 167 | public Optional get(int row, int col) { 168 | if (row < 0 || row >= SIZE || col < 0 || col >= SIZE) { 169 | throw new IndexOutOfBoundsException(String.format("Matrix Index (%d, %d) is out of bounds", row, col)); 170 | } 171 | 172 | var value = matrix[row][col]; 173 | if (value == 0) { 174 | return Optional.empty(); 175 | } else { 176 | return Optional.of(value); 177 | } 178 | } 179 | 180 | /** 181 | * Return the sudoku as a list of list of strings. 182 | *

183 | * The returned list is modifiable and a new list is generated on every call. 184 | * 185 | * @return the sudoku as a list of list of strings 186 | */ 187 | public List> asList() { 188 | var ret = new ArrayList>(); 189 | for (int[] ints : matrix) { 190 | ret.add(Arrays.stream(ints).mapToObj(x -> x == 0 ? "" : String.valueOf(x)).collect(Collectors.toList())); 191 | } 192 | return ret; 193 | } 194 | 195 | /** 196 | * Return true if the {@link SudokuBoard} is valid. 197 | * 198 | * @return true if the {@link SudokuBoard} is valid. 199 | */ 200 | public boolean isValid() { 201 | final var rowInts = new HashSet(); 202 | final var colInts = new HashSet(); 203 | 204 | // check for for duplicates in every row and column 205 | for (int i = 0; i < SIZE; i++) { 206 | for (int j = 0; j < SIZE; j++) { 207 | final var rowValue = matrix[i][j]; 208 | if ((rowValue != 0 && rowInts.contains(rowValue))) { 209 | return false; 210 | } else { 211 | rowInts.add(rowValue); 212 | } 213 | 214 | final var colValue = matrix[j][i]; 215 | if (colValue != 0 && colInts.contains(colValue)) { 216 | return false; 217 | } else { 218 | colInts.add(colValue); 219 | } 220 | } 221 | 222 | rowInts.clear(); 223 | colInts.clear(); 224 | } 225 | 226 | // now check the squares inside the big square. 227 | // for an n * n sudoku, there are n small squares inside it. 228 | // each sqrt(n) wide row contains sqrt(n) small squares. 229 | final var intsInSquare = new HashSet(); 230 | final var sqrt = (int) Math.rint(Math.sqrt(SIZE)); 231 | 232 | for (int i = 0; i < sqrt; i++) { 233 | final var rowStart = sqrt * i; 234 | final var rowEnd = rowStart + sqrt; // non-inclusive 235 | 236 | for (int j = 0; j < sqrt; j++) { 237 | final var colStart = sqrt * j; 238 | final var colEnd = colStart + sqrt; // non-inclusive, again 239 | 240 | // go over every element of this square 241 | for (int k = rowStart; k < rowEnd; k++) { 242 | for (int l = colStart; l < colEnd; l++) { 243 | final var value = matrix[k][l]; 244 | 245 | if (value == 0) { 246 | continue; 247 | } 248 | 249 | if (intsInSquare.contains(value)) { 250 | return false; 251 | } else { 252 | intsInSquare.add(value); 253 | } 254 | } 255 | } 256 | 257 | intsInSquare.clear(); 258 | } 259 | } 260 | 261 | return true; 262 | } 263 | 264 | /** 265 | * Return a deep-copy of the internal matrix used by the board. 266 | * 267 | * @return copy of the internal matrix used by this {@link SudokuBoard} 268 | */ 269 | public int[][] asMatrix() { 270 | return Arrays.stream(matrix).map(int[]::clone).toArray(int[][]::new); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/main/java/com/abhyudayasharma/sudoku/Sudoku.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Abhyudaya Sharma 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.abhyudayasharma.sudoku; 26 | 27 | import com.abhyudayasharma.sudoku.core.AbstractMove; 28 | import com.abhyudayasharma.sudoku.core.AssignmentMove; 29 | import com.abhyudayasharma.sudoku.core.SudokuSolver; 30 | import com.abhyudayasharma.sudoku.ui.SudokuTable; 31 | import com.abhyudayasharma.sudoku.ui.SudokuTableModel; 32 | import lombok.extern.slf4j.Slf4j; 33 | import net.miginfocom.swing.MigLayout; 34 | 35 | import javax.swing.JButton; 36 | import javax.swing.JFileChooser; 37 | import javax.swing.JFrame; 38 | import javax.swing.JLabel; 39 | import javax.swing.JMenu; 40 | import javax.swing.JMenuBar; 41 | import javax.swing.JMenuItem; 42 | import javax.swing.JOptionPane; 43 | import javax.swing.JSeparator; 44 | import javax.swing.JSlider; 45 | import javax.swing.WindowConstants; 46 | import javax.swing.filechooser.FileNameExtensionFilter; 47 | import java.awt.Font; 48 | import java.awt.event.ActionEvent; 49 | import java.awt.event.ActionListener; 50 | import java.io.IOException; 51 | import java.util.List; 52 | import java.util.concurrent.CancellationException; 53 | 54 | /** 55 | * A Sudoku puzzle emulation which can solve all valid Sudoku puzzles. 56 | * 57 | * @author Abhyudaya Sharma - 1710110019 58 | */ 59 | @Slf4j 60 | public class Sudoku { 61 | /** 62 | * The {@link Font} that must be used for the sudoku board. 63 | */ 64 | public static final Font BOARD_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 36); 65 | 66 | private final JFrame frame = new JFrame("Sudoku"); 67 | private final SudokuTable table = new SudokuTable(); 68 | private final JButton solveButton = new JButton("Solve"); 69 | private final JButton clearButton = new JButton("Clear"); 70 | private final JButton stopButton = new JButton("Stop"); 71 | private final JLabel solvedLabel = new JLabel("Ready..."); 72 | private SudokuSolver solver = null; 73 | 74 | void start() { 75 | initFrame(); 76 | } 77 | 78 | /** 79 | * Initializes a {@link JFrame} for display. 80 | */ 81 | private void initFrame() { 82 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 83 | frame.setLayout(new MigLayout()); 84 | frame.add(table, "grow, wrap, span"); 85 | frame.setJMenuBar(createMenuBar()); 86 | 87 | var slider = new JSlider(0, 10); 88 | slider.setSize(slider.getPreferredSize()); 89 | stopButton.setEnabled(false); 90 | 91 | solveButton.addActionListener(new TableActionListener() { 92 | @Override 93 | void actionPerformed() { 94 | var board = table.getBoard(); 95 | stopButton.setEnabled(true); 96 | slider.setEnabled(false); 97 | solver = new SudokuSolver(board, 10 - slider.getValue()) { 98 | @Override 99 | protected void done() { 100 | try { 101 | final var result = get(); 102 | final var newModel = new SudokuTableModel(result.getBoard()); 103 | newModel.setEditable(false); 104 | table.setModel(newModel); 105 | solvedLabel.setText("Solved"); 106 | } catch (CancellationException e) { 107 | clearButton.doClick(); 108 | } catch (Exception e) { 109 | JOptionPane.showMessageDialog(frame, "Unable to solve: " + e.getCause().getMessage(), 110 | "Error", JOptionPane.ERROR_MESSAGE); 111 | table.getModel().setEditable(true); 112 | solveButton.setEnabled(true); 113 | slider.setEnabled(true); 114 | solvedLabel.setText("Ready..."); 115 | } finally { 116 | clearButton.setEnabled(true); 117 | stopButton.setEnabled(false); 118 | } 119 | } 120 | 121 | @Override 122 | protected void process(List moves) { 123 | for (var move : moves) { 124 | if (move instanceof AssignmentMove) { 125 | var assignmentMove = (AssignmentMove) move; 126 | var newValue = assignmentMove.getNewValue(); 127 | var strValue = newValue == 0 ? "" : String.valueOf(newValue); 128 | table.getModel().setValueAt(strValue, assignmentMove.getRow(), assignmentMove.getCol()); 129 | } 130 | } 131 | } 132 | }; 133 | 134 | clearButton.setEnabled(false); 135 | solveButton.setEnabled(false); 136 | solvedLabel.setText("Solving..."); 137 | table.getModel().setEditable(false); 138 | solver.execute(); 139 | } 140 | }); 141 | 142 | clearButton.addActionListener(new TableActionListener() { 143 | @Override 144 | void actionPerformed() { 145 | table.clear(); 146 | slider.setEnabled(true); 147 | table.getModel().setEditable(true); 148 | solveButton.setEnabled(true); 149 | solvedLabel.setText("Ready..."); 150 | } 151 | }); 152 | 153 | stopButton.addActionListener(e -> { 154 | if (solver != null) { 155 | solver.cancel(true); 156 | solvedLabel.setText("Stopped"); 157 | } 158 | }); 159 | 160 | frame.add(new JLabel("Solving Speed:")); 161 | frame.add(slider); 162 | frame.add(solveButton); 163 | frame.add(clearButton); 164 | frame.add(stopButton); 165 | frame.add(solvedLabel); 166 | 167 | frame.pack(); 168 | frame.setVisible(true); 169 | frame.setResizable(false); 170 | } 171 | 172 | /** 173 | * Creates a {@link JMenuBar} for the frame. 174 | * 175 | * @return a new {@link JMenuBar} object. 176 | */ 177 | private JMenuBar createMenuBar() { 178 | var menuBar = new JMenuBar(); 179 | 180 | var fileMenu = new JMenu("File"); 181 | fileMenu.setMnemonic('F'); 182 | 183 | var loadFromFile = new JMenuItem("Load..."); 184 | var saveToFile = new JMenuItem("Save..."); 185 | var exitMenuItem = new JMenuItem("Exit"); 186 | 187 | loadFromFile.addActionListener(new TableActionListener() { 188 | @Override 189 | void actionPerformed() { 190 | var fileChooser = new JFileChooser(); 191 | var filter = new FileNameExtensionFilter("CSV files", "csv"); 192 | fileChooser.addChoosableFileFilter(filter); 193 | fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 194 | fileChooser.setMultiSelectionEnabled(false); 195 | fileChooser.setFileFilter(filter); 196 | 197 | var response = fileChooser.showOpenDialog(frame); 198 | if (response == JFileChooser.APPROVE_OPTION) { 199 | try { 200 | table.load(fileChooser.getSelectedFile().toURI()); 201 | } catch (Exception ex) { 202 | JOptionPane.showMessageDialog(frame, "Error while trying to read the file:\n" + ex.getMessage(), 203 | "Error reading file", JOptionPane.ERROR_MESSAGE); 204 | } 205 | } 206 | 207 | table.getModel().setEditable(true); 208 | solveButton.setEnabled(true); 209 | } 210 | }); 211 | 212 | saveToFile.addActionListener(new TableActionListener() { 213 | @Override 214 | void actionPerformed() { 215 | var fileChooser = new JFileChooser(); 216 | var filter = new FileNameExtensionFilter("CSV files", "csv"); 217 | fileChooser.addChoosableFileFilter(filter); 218 | fileChooser.setFileFilter(filter); 219 | fileChooser.setMultiSelectionEnabled(false); 220 | fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 221 | var response = fileChooser.showSaveDialog(frame); 222 | if (response == JFileChooser.APPROVE_OPTION) { 223 | var selectedFile = fileChooser.getSelectedFile(); 224 | if (selectedFile.exists()) { 225 | var option = JOptionPane.showConfirmDialog(frame, "The selected file will be overwritten. Do you want to continue?", 226 | "Overwrite File?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); 227 | if (option != JOptionPane.YES_OPTION) { 228 | return; 229 | } 230 | } 231 | 232 | var model = table.getModel(); 233 | try { 234 | model.save(fileChooser.getSelectedFile()); 235 | } catch (IOException ex) { 236 | JOptionPane.showMessageDialog(frame, "Unable to save the file: " + ex.getMessage(), 237 | "Unable to save", JOptionPane.ERROR_MESSAGE); 238 | } 239 | } 240 | } 241 | }); 242 | 243 | exitMenuItem.addActionListener(e -> frame.dispose()); 244 | exitMenuItem.setMnemonic('x'); 245 | 246 | fileMenu.add(loadFromFile); 247 | fileMenu.add(saveToFile); 248 | fileMenu.add(new JSeparator()); 249 | fileMenu.add(exitMenuItem); 250 | menuBar.add(fileMenu); 251 | return menuBar; 252 | } 253 | 254 | private abstract class TableActionListener implements ActionListener { 255 | @Override 256 | public final void actionPerformed(ActionEvent actionEvent) { 257 | var editor = table.getCellEditor(); 258 | if (editor != null) { 259 | editor.stopCellEditing(); 260 | } 261 | actionPerformed(); 262 | } 263 | 264 | abstract void actionPerformed(); 265 | } 266 | } 267 | --------------------------------------------------------------------------------