├── README.md ├── build.gradle ├── chess_kotlin ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew └── gradlew.bat ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── main ├── kotlin └── chess │ ├── core │ ├── Pieces │ │ └── Pieces.kt │ ├── game │ │ ├── Board.kt │ │ ├── ChessGame.kt │ │ └── GameInfo.kt │ └── history │ │ └── History.kt │ └── view │ ├── App.kt │ ├── PromotionDialog.kt │ └── WelcomeView.kt └── resources ├── chess_bd.png ├── chess_bl.png ├── chess_kd.png ├── chess_kl.png ├── chess_nd.png ├── chess_nl.png ├── chess_pd.png ├── chess_pl.png ├── chess_qd.png ├── chess_ql.png ├── chess_rd.png ├── chess_rl.png ├── dark_background.jpg ├── wood_dark.png └── wood_light.png /README.md: -------------------------------------------------------------------------------- 1 | # Chess-Kotlin-Tornadofx 2 | 2 player chess in kotlin tornadofx 3 | 4 | Simple Chess using Forsyth edwards notation (Fen) , Standard algebraic notation (SAN) and Figurine algebraic notation (FAN) in Kotlin Tornadofx 5 | 6 | you can see what is Fen here : 7 | https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation 8 | 9 | and other notations in chess : 10 | https://en.wikipedia.org/wiki/Chess_notation 11 | 12 | Game capabilities : 13 | * En passant 14 | * Castles 15 | * Undo/Redo 16 | * Timer 17 | * Save in file 18 | * Read from file 19 | * Switch between game moves 20 | * Highlight valid movements 21 | 22 | 23 | ![GIF](https://user-images.githubusercontent.com/89391453/151761718-3ac990aa-b6c1-43a4-9f66-b2f0c1efc6fb.gif) 24 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version "1.4.32" 3 | id 'application' 4 | } 5 | group = 'com.test' 6 | version = '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | application { 13 | mainClassName = "com.example.MainKt" 14 | } 15 | 16 | dependencies { 17 | implementation "no.tornado:tornadofx:$tornadofx_version" 18 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit" 19 | } 20 | 21 | compileKotlin { 22 | kotlinOptions.jvmTarget = "1.8" 23 | } 24 | compileTestKotlin { 25 | kotlinOptions.jvmTarget = "1.8" 26 | } -------------------------------------------------------------------------------- /chess_kotlin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version "1.4.32" 3 | id 'application' 4 | } 5 | group = 'com.test' 6 | version = '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | application { 13 | mainClassName = "com.example.MainKt" 14 | } 15 | 16 | dependencies { 17 | implementation "no.tornado:tornadofx:$tornadofx_version" 18 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit" 19 | } 20 | 21 | compileKotlin { 22 | kotlinOptions.jvmTarget = "1.8" 23 | } 24 | compileTestKotlin { 25 | kotlinOptions.jvmTarget = "1.8" 26 | } -------------------------------------------------------------------------------- /chess_kotlin/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | tornadofx_version=1.7.20 -------------------------------------------------------------------------------- /chess_kotlin/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amir-devs/Chess-Kotlin-Tornadofx/51f46223abc0320af871dbae43d566a127394508/chess_kotlin/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chess_kotlin/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /chess_kotlin/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 | 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 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /chess_kotlin/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 init 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 init 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 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | tornadofx_version=1.7.20 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amir-devs/Chess-Kotlin-Tornadofx/51f46223abc0320af871dbae43d566a127394508/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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 | 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 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /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 init 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 init 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 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /src/main/kotlin/chess/core/Pieces/Pieces.kt: -------------------------------------------------------------------------------- 1 | package chess.core.Pieces 2 | 3 | import chess.core.game.* 4 | 5 | interface Promotable 6 | 7 | abstract class PromotablePiece(override val whitePlayer: Boolean) : ChessPiece(whitePlayer), Promotable 8 | 9 | sealed class ChessPiece(open val whitePlayer: Boolean) { 10 | companion object { 11 | fun fenToPiece(pieceFen: Char): ChessPiece? { 12 | return when(pieceFen){ 13 | 'P' -> Pawn(true) 14 | 'p' -> Pawn(false) 15 | 'N' -> Knight(true) 16 | 'n' -> Knight(false) 17 | 'B' -> Bishop(true) 18 | 'b' -> Bishop(false) 19 | 'R' -> Rook(true) 20 | 'r' -> Rook(false) 21 | 'Q' -> Queen(true) 22 | 'q' -> Queen(false) 23 | 'K' -> King(true) 24 | 'k' -> King(false) 25 | else -> null 26 | } 27 | } 28 | 29 | fun thereIsBlockadeBetweenStartAndTarget(deltaFile: Int, deltaRank: Int, game: ChessGame, startSquare: Coordinates): Boolean { 30 | val squaresBetweenTargetAndStart = arrayListOf() 31 | val deltaFileSign = if (deltaFile == 0) 0 else deltaFile / Math.abs(deltaFile) 32 | val deltaRankSign = if (deltaRank == 0) 0 else deltaRank / Math.abs(deltaRank) 33 | if (deltaFileSign == 0 && deltaRankSign == 0) return false 34 | val numSquares = if (deltaFileSign == 0) Math.abs(deltaRank) - 1 else Math.abs(deltaFile) - 1 35 | (1..numSquares).forEach { 36 | val soughtFile = startSquare.file + it * deltaFileSign 37 | val soughtRank = startSquare.rank + it * deltaRankSign 38 | if (soughtFile !in 0..7 || soughtRank !in 0..7) return@forEach 39 | squaresBetweenTargetAndStart.add( 40 | Coordinates(soughtRank, soughtFile)) 41 | } 42 | if (squaresBetweenTargetAndStart.any { 43 | game.board[it.rank, it.file] != null 44 | }) return true 45 | return false 46 | } 47 | } 48 | 49 | abstract fun toFEN() : Char 50 | 51 | // With pseudo-legal moves, we still can leave our own king in chess 52 | abstract fun isValidPseudoLegalMove(game: ChessGame, move: Move): Boolean 53 | } 54 | 55 | data class Pawn(override val whitePlayer: Boolean) : ChessPiece(whitePlayer){ 56 | override fun isValidPseudoLegalMove(game: ChessGame, move: Move): Boolean { 57 | val deltaFile = move.to.file - move.from.file 58 | val deltaRank = move.to.rank - move.from.rank 59 | 60 | val isValidTwoCellsJumpAsWhite = (game.info.whiteTurn && deltaFile == 0 && deltaRank == 2 && move.from.rank == ChessBoard.RANK_2 61 | && game.board[ChessBoard.RANK_3, move.from.file] == null 62 | && game.board[ChessBoard.RANK_4, move.from.file] == null) 63 | val isValidTwoCellsJumpAsBlack = (!game.info.whiteTurn && deltaFile == 0 && deltaRank == -2 && move.from.rank == ChessBoard.RANK_7 64 | && game.board[ChessBoard.RANK_6, move.from.file] == null 65 | && game.board[ChessBoard.RANK_5, move.from.file] == null) 66 | 67 | val isValidForwardMoveAsWhite = (game.info.whiteTurn && deltaRank == 1 && deltaFile == 0 68 | && game.board[move.from.rank+1, move.from.file] == null) 69 | 70 | val isValidForwardMoveAsBlack = (!game.info.whiteTurn && deltaRank == -1 && deltaFile == 0 71 | && game.board[move.from.rank-1, move.from.file] == null) 72 | 73 | val isValidCaptureMoveAsWhite = (game.info.whiteTurn && deltaRank == 1 && Math.abs(deltaFile) == 1 74 | && game.board[move.to.rank, move.to.file] != null 75 | && !game.board[move.to.rank, move.to.file]!!.whitePlayer) 76 | val isValidCaptureMoveAsBlack = (!game.info.whiteTurn && deltaRank == -1 && Math.abs(deltaFile) == 1 77 | && game.board[move.to.rank, move.to.file] != null 78 | && game.board[move.to.rank, move.to.file]!!.whitePlayer) 79 | 80 | val isValidEnPassantCaptureAsWhite = (game.info.whiteTurn 81 | && move.from.rank == ChessBoard.RANK_5 && move.to.rank == ChessBoard.RANK_6 82 | && Math.abs(deltaFile) == 1 83 | && game.board[move.to.rank, move.to.file] == null 84 | && game.info.enPassantFile == move.to.file) 85 | 86 | val isValidEnPassantCaptureAsBlack = (!game.info.whiteTurn 87 | && move.from.rank == ChessBoard.RANK_4 && move.to.rank == ChessBoard.RANK_3 88 | && Math.abs(deltaFile) == 1 89 | && game.board[move.to.rank, move.to.file] == null 90 | && game.info.enPassantFile == move.to.file) 91 | 92 | val ownerPlayer = whitePlayer == game.info.whiteTurn 93 | val followValidLine = isValidTwoCellsJumpAsWhite || isValidTwoCellsJumpAsBlack 94 | || isValidForwardMoveAsWhite || isValidForwardMoveAsBlack 95 | || isValidCaptureMoveAsWhite || isValidCaptureMoveAsBlack 96 | || isValidEnPassantCaptureAsWhite || isValidEnPassantCaptureAsBlack 97 | 98 | return followValidLine && ownerPlayer 99 | } 100 | 101 | override fun toFEN(): Char { 102 | return if (whitePlayer) 'P' else 'p' 103 | } 104 | 105 | 106 | } 107 | data class Knight(override val whitePlayer: Boolean) : PromotablePiece(whitePlayer) { 108 | override fun isValidPseudoLegalMove(game: ChessGame, move: Move): Boolean { 109 | val deltaFile = move.to.file - move.from.file 110 | val deltaRank = move.to.rank - move.from.rank 111 | 112 | val absDeltaFile = Math.abs(deltaFile) 113 | val absDeltaRank = Math.abs(deltaRank) 114 | 115 | val pieceAtEndCell = game.board[move.to.rank, move.to.file] 116 | val endSquarePieceIsEnemy = pieceAtEndCell?.whitePlayer != whitePlayer 117 | val followValidLine = (absDeltaFile == 1 && absDeltaRank == 2) || (absDeltaFile == 2 && absDeltaRank == 1) 118 | 119 | val ownerPlayer = whitePlayer == game.info.whiteTurn 120 | 121 | return followValidLine && endSquarePieceIsEnemy && ownerPlayer 122 | } 123 | 124 | override fun toFEN(): Char { 125 | return if (whitePlayer) 'N' else 'n' 126 | } 127 | } 128 | data class Bishop(override val whitePlayer: Boolean) : PromotablePiece(whitePlayer) { 129 | override fun isValidPseudoLegalMove(game: ChessGame, move: Move): Boolean { 130 | val deltaFile = move.to.file - move.from.file 131 | val deltaRank = move.to.rank - move.from.rank 132 | 133 | val absDeltaFile = Math.abs(deltaFile) 134 | val absDeltaRank = Math.abs(deltaRank) 135 | 136 | val pieceAtEndCell = game.board[move.to.rank, move.to.file] 137 | val endSquarePieceIsEnemy = pieceAtEndCell?.whitePlayer != whitePlayer 138 | val followValidLine = absDeltaFile == absDeltaRank 139 | 140 | val ownerPlayer = whitePlayer == game.info.whiteTurn 141 | 142 | if (thereIsBlockadeBetweenStartAndTarget(deltaFile, deltaRank, game, move.from)) return false 143 | 144 | return followValidLine && endSquarePieceIsEnemy && ownerPlayer 145 | } 146 | 147 | override fun toFEN(): Char { 148 | return if (whitePlayer) 'B' else 'b' 149 | } 150 | } 151 | data class Rook(override val whitePlayer: Boolean) : PromotablePiece(whitePlayer) { 152 | override fun isValidPseudoLegalMove(game: ChessGame, move: Move): Boolean { 153 | val deltaFile = move.to.file - move.from.file 154 | val deltaRank = move.to.rank - move.from.rank 155 | 156 | val absDeltaFile = Math.abs(deltaFile) 157 | val absDeltaRank = Math.abs(deltaRank) 158 | 159 | val pieceAtEndCell = game.board[move.to.rank, move.to.file] 160 | val endSquarePieceIsEnemy = pieceAtEndCell?.whitePlayer != whitePlayer 161 | val followValidLine = (absDeltaFile == 0 && absDeltaRank > 0) || (absDeltaFile > 0 && absDeltaRank == 0) 162 | 163 | if (thereIsBlockadeBetweenStartAndTarget(deltaFile, deltaRank, game, move.from)) return false 164 | 165 | val ownerPlayer = whitePlayer == game.info.whiteTurn 166 | 167 | return followValidLine && endSquarePieceIsEnemy && ownerPlayer 168 | } 169 | 170 | override fun toFEN(): Char { 171 | return if (whitePlayer) 'R' else 'r' 172 | } 173 | } 174 | data class Queen(override val whitePlayer: Boolean) : PromotablePiece(whitePlayer) { 175 | override fun isValidPseudoLegalMove(game: ChessGame, move: Move): Boolean { 176 | val deltaFile = move.to.file - move.from.file 177 | val deltaRank = move.to.rank - move.from.rank 178 | 179 | val absDeltaFile = Math.abs(deltaFile) 180 | val absDeltaRank = Math.abs(deltaRank) 181 | 182 | val pieceAtEndCell = game.board[move.to.rank, move.to.file] 183 | val endSquarePieceIsEnemy = pieceAtEndCell?.whitePlayer != whitePlayer 184 | val followValidLine = (absDeltaFile == 0 && absDeltaRank > 0) || (absDeltaFile > 0 && absDeltaRank == 0) || 185 | absDeltaFile == absDeltaRank 186 | 187 | if (thereIsBlockadeBetweenStartAndTarget(deltaFile, deltaRank, game, move.from)) return false 188 | 189 | val ownerPlayer = whitePlayer == game.info.whiteTurn 190 | 191 | return followValidLine && endSquarePieceIsEnemy && ownerPlayer 192 | } 193 | 194 | override fun toFEN(): Char { 195 | return if (whitePlayer) 'Q' else 'q' 196 | } 197 | } 198 | data class King(override val whitePlayer: Boolean) : ChessPiece(whitePlayer){ 199 | override fun isValidPseudoLegalMove(game: ChessGame, move: Move): Boolean { 200 | val deltaFile = move.to.file - move.from.file 201 | val deltaRank = move.to.rank - move.from.rank 202 | 203 | val absDeltaFile = Math.abs(deltaFile) 204 | val absDeltaRank = Math.abs(deltaRank) 205 | 206 | val pieceAtEndCell = game.board[move.to.rank, move.to.file] 207 | val followValidLine = absDeltaFile <= 1 && absDeltaRank <= 1 208 | val endPieceIsEnemy = pieceAtEndCell?.whitePlayer != whitePlayer 209 | 210 | val whiteKingCrossingF1Pieces = copyGamePiecesIntoArray(game) 211 | whiteKingCrossingF1Pieces[ChessBoard.RANK_1][ChessBoard.FILE_E] = null 212 | whiteKingCrossingF1Pieces[ChessBoard.RANK_1][ChessBoard.FILE_F] = King(whitePlayer = true) 213 | val whiteKingCrossingF1Info = game.info.copy(whiteTurn = true) 214 | val whiteKingCrossingF1Position = game.copy(board = ChessBoard(whiteKingCrossingF1Pieces), info = whiteKingCrossingF1Info) 215 | val isLegalKingSideCastleAsWhite = game.info.whiteTurn 216 | && WhiteKingSideCastle in game.info.castles 217 | && deltaFile == 2 && deltaRank == 0 218 | && move.from == Coordinates(rank = ChessBoard.RANK_1, file = ChessBoard.FILE_E) 219 | && game.board[ChessBoard.RANK_1, ChessBoard.FILE_H] == Rook(whitePlayer = true) 220 | && game.board[ChessBoard.RANK_1, ChessBoard.FILE_F] == null 221 | && game.board[ChessBoard.RANK_1, ChessBoard.FILE_G] == null 222 | && !game.playerKingIsAttacked() 223 | && !whiteKingCrossingF1Position.playerKingIsAttacked() 224 | 225 | val whiteKingCrossingD1Pieces = copyGamePiecesIntoArray(game) 226 | whiteKingCrossingD1Pieces[ChessBoard.RANK_1][ChessBoard.FILE_E] = null 227 | whiteKingCrossingD1Pieces[ChessBoard.RANK_1][ChessBoard.FILE_D] = King(whitePlayer = true) 228 | val whiteKingCrossingD1Info = game.info.copy(whiteTurn = true) 229 | val whiteKingCrossingD1Position = game.copy(board = ChessBoard(whiteKingCrossingD1Pieces), info = whiteKingCrossingD1Info) 230 | val isLegalQueenSideCastleAsWhite = game.info.whiteTurn 231 | && WhiteQueenSideCastle in game.info.castles 232 | && deltaFile == -2 && deltaRank == 0 233 | && move.from == Coordinates(rank = ChessBoard.RANK_1, file = ChessBoard.FILE_E) 234 | && game.board[ChessBoard.RANK_1, ChessBoard.FILE_A] == Rook(whitePlayer = true) 235 | && game.board[ChessBoard.RANK_1, ChessBoard.FILE_D] == null 236 | && game.board[ChessBoard.RANK_1, ChessBoard.FILE_C] == null 237 | && game.board[ChessBoard.RANK_1, ChessBoard.FILE_B] == null 238 | && !game.playerKingIsAttacked() 239 | && !whiteKingCrossingD1Position.playerKingIsAttacked() 240 | 241 | val blackKingCrossingF8Pieces = copyGamePiecesIntoArray(game) 242 | blackKingCrossingF8Pieces[ChessBoard.RANK_8][ChessBoard.FILE_E] = null 243 | blackKingCrossingF8Pieces[ChessBoard.RANK_8][ChessBoard.FILE_F] = King(whitePlayer = false) 244 | val blackKingCrossingF8Info = game.info.copy(whiteTurn = false) 245 | val blackKingCrossingF8Position = game.copy(board = ChessBoard(blackKingCrossingF8Pieces), info = blackKingCrossingF8Info) 246 | val isLegalKingSideCastleAsBlack = !game.info.whiteTurn 247 | && BlackKingSideCastle in game.info.castles 248 | && deltaFile == 2 && deltaRank == 0 249 | && move.from == Coordinates(rank = ChessBoard.RANK_8, file = ChessBoard.FILE_E) 250 | && game.board[ChessBoard.RANK_8, ChessBoard.FILE_H] == Rook(whitePlayer = false) 251 | && game.board[ChessBoard.RANK_8, ChessBoard.FILE_F] == null 252 | && game.board[ChessBoard.RANK_8, ChessBoard.FILE_G] == null 253 | && !game.playerKingIsAttacked() 254 | && !blackKingCrossingF8Position.playerKingIsAttacked() 255 | 256 | val blackKingCrossingD8Pieces = copyGamePiecesIntoArray(game) 257 | blackKingCrossingD8Pieces[ChessBoard.RANK_8][ChessBoard.FILE_E] = null 258 | blackKingCrossingD8Pieces[ChessBoard.RANK_8][ChessBoard.FILE_D] = King(whitePlayer = false) 259 | val blackKingCrossingD8Info = game.info.copy(whiteTurn = false) 260 | val blackKingCrossingD8Position = game.copy(board = ChessBoard((blackKingCrossingD8Pieces)), info = blackKingCrossingD8Info) 261 | val isLegalQueenSideCastleAsBlack = !game.info.whiteTurn 262 | && BlackQueenSideCastle in game.info.castles 263 | && deltaFile == -2 && deltaRank == 0 264 | && move.from == Coordinates(rank = ChessBoard.RANK_8, file = ChessBoard.FILE_E) 265 | && game.board[ChessBoard.RANK_8, ChessBoard.FILE_A] == Rook(whitePlayer = false) 266 | && game.board[ChessBoard.RANK_8, ChessBoard.FILE_D] == null 267 | && game.board[ChessBoard.RANK_8, ChessBoard.FILE_C] == null 268 | && game.board[ChessBoard.RANK_8, ChessBoard.FILE_B] == null 269 | && !game.playerKingIsAttacked() 270 | && !blackKingCrossingD8Position.playerKingIsAttacked() 271 | 272 | val isLegalCastle = isLegalKingSideCastleAsWhite || isLegalQueenSideCastleAsWhite 273 | || isLegalKingSideCastleAsBlack || isLegalQueenSideCastleAsBlack 274 | 275 | val ownerPlayer = whitePlayer == game.info.whiteTurn 276 | 277 | return ((followValidLine && endPieceIsEnemy) || isLegalCastle) && ownerPlayer 278 | } 279 | 280 | private fun copyGamePiecesIntoArray(game: ChessGame): Array> { 281 | val array = Array(8, { Array(8, { null }) }) 282 | for (rank in 0..7) { 283 | for (file in 0..7) { 284 | array[rank][file] = game.board[rank, file] 285 | } 286 | } 287 | return array 288 | } 289 | 290 | override fun toFEN(): Char { 291 | return if (whitePlayer) 'K' else 'k' 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/kotlin/chess/core/game/Board.kt: -------------------------------------------------------------------------------- 1 | package chess.core.game 2 | 3 | import chess.core.Pieces.ChessPiece 4 | 5 | class ChessBoard(val pieces: Array>) { 6 | 7 | companion object { 8 | val FILE_A = 0 9 | val FILE_B = 1 10 | val FILE_C = 2 11 | val FILE_D = 3 12 | val FILE_E = 4 13 | val FILE_F = 5 14 | val FILE_G = 6 15 | val FILE_H = 7 16 | 17 | val RANK_1 = 0 18 | val RANK_2 = 1 19 | val RANK_3 = 2 20 | val RANK_4 = 3 21 | val RANK_5 = 4 22 | val RANK_6 = 5 23 | val RANK_7 = 6 24 | val RANK_8 = 7 25 | 26 | 27 | fun fenToChessBoard(fen: String):ChessBoard{ 28 | val pieces = Array(8, { Array(8, {null})}) 29 | 30 | val boardPart = fen.split("""\s+""".toRegex())[0] 31 | val lines = boardPart.split("/").reversed() 32 | 33 | for (rank in 0..7){ 34 | val currentLine = lines[rank] 35 | var file = 0 36 | for (currentChar in currentLine){ 37 | if (currentChar.isDigit()){ 38 | file += currentChar.toInt() - '0'.toInt() 39 | } 40 | else { 41 | pieces[rank][file++] = ChessPiece.fenToPiece(currentChar) 42 | } 43 | } 44 | } 45 | 46 | return ChessBoard(pieces) 47 | } 48 | } 49 | 50 | operator fun get(rank: Int, file: Int):ChessPiece? = pieces[rank][file] 51 | 52 | fun toFEN():String { 53 | val builder = StringBuilder() 54 | 55 | for (rank in 7.downTo(0)){ 56 | var currentGap = 0 57 | (0..7).forEach{ file -> 58 | val currentPiece = pieces[rank][file] 59 | if (currentPiece == null){ 60 | currentGap++ 61 | } 62 | else { 63 | if (currentGap > 0) builder.append("$currentGap") 64 | currentGap = 0 65 | builder.append(currentPiece.toFEN()) 66 | } 67 | } 68 | if (currentGap > 0) builder.append("$currentGap") 69 | if (rank > 0) builder.append('/') 70 | } 71 | 72 | return builder.toString() 73 | } 74 | 75 | override fun equals(other: Any?): Boolean { 76 | if (other is ChessBoard){ 77 | return other.toFEN() == toFEN() 78 | } 79 | return false 80 | } 81 | 82 | override fun hashCode(): Int { 83 | return toFEN().hashCode() 84 | } 85 | 86 | override fun toString(): String { 87 | return toFEN() 88 | } 89 | } 90 | 91 | data class Coordinates(val rank: Int, val file: Int){ 92 | init { 93 | require(rank in 0..7) 94 | require(file in 0..7) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/chess/core/game/ChessGame.kt: -------------------------------------------------------------------------------- 1 | package chess.core.game 2 | 3 | import chess.core.Pieces.* 4 | 5 | data class ChessGame(val board: ChessBoard, val info: GameInfo){ 6 | companion object { 7 | fun fenToGame(fen: String): ChessGame { 8 | return ChessGame(board = ChessBoard.fenToChessBoard(fen), 9 | info = GameInfo.fenToGameInfo(fen)) 10 | } 11 | var INITIAL_POSITION = ChessGame.fenToGame("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") 12 | } 13 | 14 | fun toFEN(): String = "${board.toFEN()} ${info.toFEN()}" 15 | 16 | fun getSANForMove(move: Move, promotionPiece: PromotablePiece = Queen(info.whiteTurn)) : String { 17 | if (!isValidMove(move)) throw IllegalMoveException() 18 | 19 | val builder = StringBuilder() 20 | 21 | if (isWhiteKingSideCastle(move) || isBlackKingSideCastle(move)) builder.append("O-O") 22 | else if (isWhiteQueenSideCastle(move) || isBlackQueenSideCastle(move)) builder.append("O-O-O") 23 | else { 24 | val pieceAtStartSquare = board[move.from.rank, move.from.file] 25 | if (pieceAtStartSquare == null) throw NoPieceAtStartCellException() 26 | 27 | if (pieceAtStartSquare is Pawn) { 28 | val isCapturingMove = move.from.file != move.to.file 29 | val isPromotionMove = isPromotionMove(move) 30 | 31 | if (isCapturingMove) { 32 | builder.append(('a'.toInt() + move.from.file).toChar()) 33 | builder.append('x') 34 | } 35 | builder.append(('a'.toInt() + move.to.file).toChar()) 36 | builder.append(('1'.toInt() + move.to.rank).toChar()) 37 | 38 | if (isPromotionMove){ 39 | builder.append('=') 40 | builder.append(promotionPiece.toFEN().toUpperCase()) 41 | } 42 | } 43 | else { 44 | val allMovesWithSameEndSquareAndSameMovingPiece = getAllPossibleMoves().filter { it.to == move.to }.filter { 45 | val soughtMovePieceAtStartSquare = board[it.from.rank, it.from.file] 46 | soughtMovePieceAtStartSquare == pieceAtStartSquare 47 | }.filter { it != move } 48 | 49 | val samePieceWithSameRank = allMovesWithSameEndSquareAndSameMovingPiece.any{ it.from.rank == move.from.rank } 50 | val samePieceWithSameFile = allMovesWithSameEndSquareAndSameMovingPiece.any{ it.from.file == move.from.file } 51 | 52 | builder.append(pieceAtStartSquare.toFEN().toUpperCase()) 53 | if (allMovesWithSameEndSquareAndSameMovingPiece.isNotEmpty()){ 54 | if (samePieceWithSameFile || samePieceWithSameRank){ 55 | if (samePieceWithSameRank) builder.append(('a'.toInt() + move.from.file).toChar()) 56 | if (samePieceWithSameFile) builder.append(('1'.toInt() + move.from.rank).toChar()) 57 | } 58 | else { 59 | builder.append(('a'.toInt() + move.from.file).toChar()) 60 | } 61 | } 62 | 63 | val pieceAtEndSquare = board[move.to.rank, move.to.file] 64 | val isCapturingMove = pieceAtEndSquare?.whitePlayer == !info.whiteTurn 65 | if (isCapturingMove) builder.append('x') 66 | 67 | builder.append(('a'.toInt() + move.to.file).toChar()) 68 | builder.append(('1'.toInt() + move.to.rank).toChar()) 69 | } 70 | } 71 | 72 | val positionAfterMove = doMoveWithValidation(move) 73 | if (positionAfterMove.playerIsMate()) builder.append('#') 74 | else if (positionAfterMove.playerKingIsAttacked()) builder.append('+') 75 | 76 | return builder.toString() 77 | } 78 | 79 | fun getFANForMove(move: Move, promotionPiece: PromotablePiece = Queen(info.whiteTurn)) : String { 80 | val moveSAN = getSANForMove(move = move, promotionPiece = promotionPiece) 81 | return moveSAN.replace('N', if (info.whiteTurn) '\u2658' else '\u265E') 82 | .replace('B', if (info.whiteTurn) '\u2657' else '\u265D') 83 | .replace('R', if (info.whiteTurn) '\u2656' else '\u265C') 84 | .replace('Q', if (info.whiteTurn) '\u2655' else '\u265B') 85 | .replace('K', if (info.whiteTurn) '\u2654' else '\u265A') 86 | } 87 | 88 | fun searchForKingCoordinates(whiteTurn: Boolean) : Coordinates? { 89 | var playerKingPosition:Coordinates? = null 90 | 91 | kingPositionOuterLoop@ for (rank in 0..7){ 92 | for (file in 0..7){ 93 | if (board[rank, file] == King(whiteTurn)) { 94 | playerKingPosition = Coordinates(rank, file) 95 | break@kingPositionOuterLoop 96 | } 97 | } 98 | } 99 | 100 | return playerKingPosition 101 | } 102 | 103 | fun playerKingIsAttacked() : Boolean { 104 | val gameWithTurnInverse = copy(info = info.copy(whiteTurn = !this.info.whiteTurn, enPassantFile = null)) 105 | var isAttacked = false 106 | 107 | val currentPlayerTurnKingPosition:Coordinates? = gameWithTurnInverse.searchForKingCoordinates(info.whiteTurn) 108 | if (currentPlayerTurnKingPosition == null) throw PlayerHasNoKingException() 109 | 110 | attackSearchOuterLoop@ for(rank in 0..7){ 111 | for (file in 0..7){ 112 | /* 113 | We can't test with kings because of infinite recursive calls between methods 114 | But anyway, that does not matter as a king can't attack the other king. 115 | */ 116 | if (gameWithTurnInverse.board[rank, file] !is King 117 | && gameWithTurnInverse.isValidPseudoMove( 118 | Move(from = Coordinates(rank = rank, file = file), to = currentPlayerTurnKingPosition))){ 119 | isAttacked = true 120 | break@attackSearchOuterLoop 121 | } 122 | } 123 | } 124 | 125 | return isAttacked 126 | } 127 | 128 | fun playerIsMate(): Boolean { 129 | if (!playerKingIsAttacked()) return false 130 | 131 | val possiblesMoves = getAllPossibleMoves() 132 | return possiblesMoves.all { 133 | val positionAfterMove = doMoveWithValidation(it) 134 | val positionAfterMoveButWithCurrentTurn = positionAfterMove.copy(info = info.copy(whiteTurn = this.info.whiteTurn) ) 135 | return positionAfterMoveButWithCurrentTurn.playerKingIsAttacked() 136 | } 137 | } 138 | 139 | fun getAllPossibleMoves(): List { 140 | val movesList = mutableListOf() 141 | 142 | for (startCellRank in 0..7){ 143 | for (startCellFile in 0..7){ 144 | val startCell = Coordinates(rank = startCellRank, file = startCellFile) 145 | for (endCellRank in 0..7){ 146 | for (endCellFile in 0..7){ 147 | val endCell = Coordinates(rank = endCellRank, file = endCellFile) 148 | if (startCell == endCell) continue 149 | 150 | val move = Move(from = startCell, to = endCell) 151 | 152 | if (isValidMove(move)) movesList.add(move) 153 | } 154 | } 155 | } 156 | } 157 | 158 | return movesList 159 | } 160 | 161 | fun playerKingIsUnderAttackAfterMove(move: Move) : Boolean { 162 | val positionAfterMove = this.copy().doMoveWithoutValidation(move) 163 | val positionAfterMoveButWithCurrentPlayerTurn_GameInfo = positionAfterMove.info.copy(whiteTurn = this.info.whiteTurn) 164 | val positionAfterMoveButWithCurrentPlayerTurn = positionAfterMove.copy(info = positionAfterMoveButWithCurrentPlayerTurn_GameInfo) 165 | 166 | return positionAfterMoveButWithCurrentPlayerTurn.playerKingIsAttacked() 167 | } 168 | 169 | fun isValidMove(move: Move): Boolean { 170 | return isValidPseudoMove(move) 171 | && !playerKingIsUnderAttackAfterMove(move) 172 | } 173 | 174 | fun isValidPseudoMove(move: Move): Boolean { 175 | val pieceAtStartSquare = board[move.from.rank, move.from.file] 176 | if (pieceAtStartSquare?.whitePlayer != info.whiteTurn) return false 177 | return pieceAtStartSquare.isValidPseudoLegalMove(this, move) 178 | } 179 | 180 | fun isWhiteKingSideCastle(move: Move): Boolean { 181 | val pieceAtStartSquare = board[move.from.rank, move.from.file] ?: return false 182 | 183 | return info.whiteTurn 184 | && WhiteKingSideCastle in info.castles 185 | && pieceAtStartSquare == King(whitePlayer = true) 186 | && board[ChessBoard.RANK_1, ChessBoard.FILE_H] == Rook(whitePlayer = true) 187 | && move.from == Coordinates(rank = ChessBoard.RANK_1, file = ChessBoard.FILE_E) 188 | && move.to == Coordinates(rank = ChessBoard.RANK_1, file = ChessBoard.FILE_G) 189 | && board[ChessBoard.RANK_1, ChessBoard.FILE_F] == null 190 | && board[ChessBoard.RANK_1, ChessBoard.FILE_G] == null 191 | } 192 | 193 | fun isWhiteQueenSideCastle(move: Move): Boolean { 194 | val pieceAtStartSquare = board[move.from.rank, move.from.file] ?: return false 195 | 196 | return info.whiteTurn 197 | && WhiteQueenSideCastle in info.castles 198 | && pieceAtStartSquare == King(whitePlayer = true) 199 | && board[ChessBoard.RANK_1, ChessBoard.FILE_A] == Rook(whitePlayer = true) 200 | && move.from == Coordinates(rank = ChessBoard.RANK_1, file = ChessBoard.FILE_E) 201 | && move.to == Coordinates(rank = ChessBoard.RANK_1, file = ChessBoard.FILE_C) 202 | && board[ChessBoard.RANK_1, ChessBoard.FILE_D] == null 203 | && board[ChessBoard.RANK_1, ChessBoard.FILE_C] == null 204 | && board[ChessBoard.RANK_1, ChessBoard.FILE_B] == null 205 | } 206 | 207 | fun isBlackKingSideCastle(move: Move): Boolean { 208 | val pieceAtStartSquare = board[move.from.rank, move.from.file] ?: return false 209 | 210 | return !info.whiteTurn 211 | && BlackKingSideCastle in info.castles 212 | && pieceAtStartSquare == King(whitePlayer = false) 213 | && board[ChessBoard.RANK_8, ChessBoard.FILE_H] == Rook(whitePlayer = false) 214 | && move.from == Coordinates(rank = ChessBoard.RANK_8, file = ChessBoard.FILE_E) 215 | && move.to == Coordinates(rank = ChessBoard.RANK_8, file = ChessBoard.FILE_G) 216 | && board[ChessBoard.RANK_8, ChessBoard.FILE_F] == null 217 | && board[ChessBoard.RANK_8, ChessBoard.FILE_G] == null 218 | } 219 | 220 | fun isBlackQueenSideCastle(move: Move): Boolean { 221 | val pieceAtStartSquare = board[move.from.rank, move.from.file] ?: return false 222 | 223 | return !info.whiteTurn 224 | && BlackQueenSideCastle in info.castles 225 | && pieceAtStartSquare == King(whitePlayer = false) 226 | && board[ChessBoard.RANK_8, ChessBoard.FILE_A] == Rook(whitePlayer = false) 227 | && move.from == Coordinates(rank = ChessBoard.RANK_8, file = ChessBoard.FILE_E) 228 | && move.to == Coordinates(rank = ChessBoard.RANK_8, file = ChessBoard.FILE_C) 229 | && board[ChessBoard.RANK_8, ChessBoard.FILE_D] == null 230 | && board[ChessBoard.RANK_8, ChessBoard.FILE_C] == null 231 | && board[ChessBoard.RANK_8, ChessBoard.FILE_B] == null 232 | } 233 | 234 | fun isEnPassantMove(move: Move): Boolean { 235 | val pieceAtStartSquare = board[move.from.rank, move.from.file] ?: return false 236 | val pieceAtEndSquare = board[move.to.rank, move.to.file] 237 | val isWhiteEnPassantMove = info.whiteTurn && pieceAtStartSquare == Pawn(whitePlayer = true) 238 | && pieceAtEndSquare == null && info.enPassantFile == move.to.file 239 | && move.from.rank == ChessBoard.RANK_5 && move.to.rank == ChessBoard.RANK_6 240 | val isBlackEnPassantMove = !info.whiteTurn && pieceAtStartSquare == Pawn(whitePlayer = false) 241 | && pieceAtEndSquare == null && info.enPassantFile == move.to.file 242 | && move.from.rank == ChessBoard.RANK_4 && move.to.rank == ChessBoard.RANK_3 243 | 244 | return isWhiteEnPassantMove || isBlackEnPassantMove 245 | } 246 | 247 | fun isPromotionMove(move: Move): Boolean { 248 | val pieceAtStartSquare = board[move.from.rank, move.from.file] ?: return false 249 | 250 | val promotionAsWhite = info.whiteTurn && pieceAtStartSquare == Pawn(whitePlayer = true) 251 | && move.to.rank == ChessBoard.RANK_8 252 | val promotionAsBlack = !info.whiteTurn && pieceAtStartSquare == Pawn(whitePlayer = false) 253 | && move.to.rank == ChessBoard.RANK_1 254 | 255 | return promotionAsWhite || promotionAsBlack 256 | } 257 | 258 | fun doMoveWithValidation(move: Move, 259 | promotionPiece: PromotablePiece = Queen(info.whiteTurn)): ChessGame { 260 | val pieceAtStartSquare = board[move.from.rank, move.from.file] ?: throw NoPieceAtStartCellException() 261 | if (!pieceAtStartSquare.isValidPseudoLegalMove(this, move)) throw IllegalMoveException() 262 | 263 | return doMoveWithoutValidation(move, promotionPiece) 264 | } 265 | 266 | fun doMoveWithoutValidation(move: Move, 267 | promotionPiece: PromotablePiece = Queen(info.whiteTurn)): ChessGame { 268 | val pieceAtStartSquare = board[move.from.rank, move.from.file] ?: throw NoPieceAtStartCellException() 269 | 270 | val capturingMove = board[move.to.rank, move.to.file] != null 271 | 272 | val deltaFile = move.to.file - move.from.file 273 | val deltaRank = move.to.rank - move.from.rank 274 | 275 | val modifiedBoardArray = copyBoardIntoArray() 276 | val newMoveNumber = if (!info.whiteTurn) info.moveNumber+1 else info.moveNumber 277 | var modifiedGameInfo = info.copy(whiteTurn = !info.whiteTurn, moveNumber = newMoveNumber) 278 | 279 | if (isEnPassantMove(move)) { 280 | modifiedGameInfo = modifiedGameInfo.copy(enPassantFile = null, nullityCount = 0) 281 | modifiedBoardArray[move.from.rank][move.from.file] = null 282 | modifiedBoardArray[move.to.rank][move.to.file] = pieceAtStartSquare 283 | modifiedBoardArray[if (info.whiteTurn) (move.to.rank-1) else (move.to.rank+1)][move.to.file] = null 284 | } 285 | else if (isPromotionMove(move)) { 286 | modifiedGameInfo = modifiedGameInfo.copy(enPassantFile = null, nullityCount = 0) 287 | if (promotionPiece.whitePlayer != info.whiteTurn) throw WrongPromotionPieceColor() 288 | modifiedBoardArray[move.from.rank][move.from.file] = null 289 | modifiedBoardArray[move.to.rank][move.to.file] = promotionPiece 290 | } 291 | else if (isWhiteKingSideCastle(move)) { 292 | modifiedGameInfo = modifiedGameInfo.copy(enPassantFile = null, nullityCount = modifiedGameInfo.nullityCount+1) 293 | val pathEmpty = board[ChessBoard.RANK_1, ChessBoard.FILE_F] == null 294 | && board[ChessBoard.RANK_1, ChessBoard.FILE_G] == null 295 | if (pathEmpty){ 296 | // update king 297 | modifiedBoardArray[move.from.rank][move.from.file] = null 298 | modifiedBoardArray[move.to.rank][move.to.file] = pieceAtStartSquare 299 | 300 | // update rook 301 | modifiedBoardArray[ChessBoard.RANK_1][ChessBoard.FILE_H] = null 302 | modifiedBoardArray[ChessBoard.RANK_1][ChessBoard.FILE_F] = Rook(whitePlayer = true) 303 | 304 | // update game info 305 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 306 | newCastlesRight.remove(WhiteKingSideCastle) 307 | newCastlesRight.remove(WhiteQueenSideCastle) 308 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 309 | } 310 | else throw IllegalMoveException() 311 | } else if (isWhiteQueenSideCastle(move)) { 312 | modifiedGameInfo = modifiedGameInfo.copy(enPassantFile = null, nullityCount = modifiedGameInfo.nullityCount+1) 313 | val pathEmpty = board[ChessBoard.RANK_1, ChessBoard.FILE_D] == null 314 | && board[ChessBoard.RANK_1, ChessBoard.FILE_C] == null 315 | && board[ChessBoard.RANK_1, ChessBoard.FILE_B] == null 316 | if (pathEmpty){ 317 | // update king 318 | modifiedBoardArray[move.from.rank][move.from.file] = null 319 | modifiedBoardArray[move.to.rank][move.to.file] = pieceAtStartSquare 320 | 321 | // update rook 322 | modifiedBoardArray[ChessBoard.RANK_1][ChessBoard.FILE_A] = null 323 | modifiedBoardArray[ChessBoard.RANK_1][ChessBoard.FILE_D] = Rook(whitePlayer = true) 324 | 325 | // update game info 326 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 327 | newCastlesRight.remove(WhiteKingSideCastle) 328 | newCastlesRight.remove(WhiteQueenSideCastle) 329 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 330 | } 331 | else throw IllegalMoveException() 332 | } else if (isBlackKingSideCastle(move)) { 333 | modifiedGameInfo = modifiedGameInfo.copy(enPassantFile = null, nullityCount = modifiedGameInfo.nullityCount+1) 334 | val pathEmpty = board[ChessBoard.RANK_8, ChessBoard.FILE_F] == null 335 | && board[ChessBoard.RANK_8, ChessBoard.FILE_G] == null 336 | if (pathEmpty){ 337 | // update king 338 | modifiedBoardArray[move.from.rank][move.from.file] = null 339 | modifiedBoardArray[move.to.rank][move.to.file] = pieceAtStartSquare 340 | 341 | // update rook 342 | modifiedBoardArray[ChessBoard.RANK_8][ChessBoard.FILE_H] = null 343 | modifiedBoardArray[ChessBoard.RANK_8][ChessBoard.FILE_F] = Rook(whitePlayer = false) 344 | 345 | // update game info 346 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 347 | newCastlesRight.remove(BlackKingSideCastle) 348 | newCastlesRight.remove(BlackQueenSideCastle) 349 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 350 | } 351 | else throw IllegalMoveException() 352 | } else if (isBlackQueenSideCastle(move)) { 353 | modifiedGameInfo = modifiedGameInfo.copy(enPassantFile = null, nullityCount = modifiedGameInfo.nullityCount+1) 354 | val pathEmpty = board[ChessBoard.RANK_8, ChessBoard.FILE_D] == null 355 | && board[ChessBoard.RANK_8, ChessBoard.FILE_C] == null 356 | && board[ChessBoard.RANK_8, ChessBoard.FILE_B] == null 357 | if (pathEmpty){ 358 | // update king 359 | modifiedBoardArray[move.from.rank][move.from.file] = null 360 | modifiedBoardArray[move.to.rank][move.to.file] = pieceAtStartSquare 361 | 362 | // update rook 363 | modifiedBoardArray[ChessBoard.RANK_8][ChessBoard.FILE_A] = null 364 | modifiedBoardArray[ChessBoard.RANK_8][ChessBoard.FILE_D] = Rook(whitePlayer = false) 365 | 366 | // update game info 367 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 368 | newCastlesRight.remove(BlackKingSideCastle) 369 | newCastlesRight.remove(BlackQueenSideCastle) 370 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 371 | } 372 | else throw IllegalMoveException() 373 | } else { // regular move 374 | modifiedBoardArray[move.from.rank][move.from.file] = null 375 | modifiedBoardArray[move.to.rank][move.to.file] = pieceAtStartSquare 376 | 377 | val isPawnTwoCellsJump = (pieceAtStartSquare == Pawn(whitePlayer = true) 378 | && deltaFile == 0 && deltaRank == 2 && move.from.rank == ChessBoard.RANK_2) 379 | || (pieceAtStartSquare == Pawn(whitePlayer = false) 380 | && deltaFile == 0 && deltaRank == -2 && move.from.rank == ChessBoard.RANK_7) 381 | 382 | if (isPawnTwoCellsJump) { 383 | modifiedGameInfo = modifiedGameInfo.copy(enPassantFile = move.from.file, nullityCount = 0) 384 | } else { 385 | modifiedGameInfo = modifiedGameInfo.copy(enPassantFile = null) 386 | } 387 | 388 | if (pieceAtStartSquare::class == Pawn::class || capturingMove){ 389 | modifiedGameInfo = modifiedGameInfo.copy(nullityCount = 0) 390 | } else { 391 | modifiedGameInfo = modifiedGameInfo.copy(nullityCount = modifiedGameInfo.nullityCount+1) 392 | } 393 | 394 | if (pieceAtStartSquare == King(whitePlayer = true)) { 395 | // update game info 396 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 397 | newCastlesRight.remove(WhiteKingSideCastle) 398 | newCastlesRight.remove(WhiteQueenSideCastle) 399 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 400 | } else if (pieceAtStartSquare == King(whitePlayer = false)) { 401 | // update game info 402 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 403 | newCastlesRight.remove(BlackKingSideCastle) 404 | newCastlesRight.remove(BlackQueenSideCastle) 405 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 406 | } else if (pieceAtStartSquare == Rook(whitePlayer = true) 407 | && move.from == Coordinates(rank = ChessBoard.RANK_1, file = ChessBoard.FILE_H)) { 408 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 409 | newCastlesRight.remove(WhiteKingSideCastle) 410 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 411 | } else if (pieceAtStartSquare == Rook(whitePlayer = true) 412 | && move.from == Coordinates(rank = ChessBoard.RANK_1, file = ChessBoard.FILE_A)) { 413 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 414 | newCastlesRight.remove(WhiteQueenSideCastle) 415 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 416 | } else if (pieceAtStartSquare == Rook(whitePlayer = false) 417 | && move.from == Coordinates(rank = ChessBoard.RANK_8, file = ChessBoard.FILE_H)) { 418 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 419 | newCastlesRight.remove(BlackKingSideCastle) 420 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 421 | } else if (pieceAtStartSquare == Rook(whitePlayer = false) 422 | && move.from == Coordinates(rank = ChessBoard.RANK_8, file = ChessBoard.FILE_A)) { 423 | val newCastlesRight = mutableListOf(*info.castles.toTypedArray()) 424 | newCastlesRight.remove(BlackQueenSideCastle) 425 | modifiedGameInfo = modifiedGameInfo.copy(castles = newCastlesRight) 426 | } 427 | } 428 | 429 | val modifiedBoard = ChessBoard(modifiedBoardArray) 430 | 431 | return ChessGame(modifiedBoard, modifiedGameInfo) 432 | } 433 | 434 | fun copyBoardIntoArray() : Array> { 435 | val pieces = Array(8, { Array(8, {null})}) 436 | 437 | for (rank in 0..7){ 438 | for (file in 0..7){ 439 | pieces[rank][file] = board[rank, file] 440 | } 441 | } 442 | 443 | return pieces 444 | } 445 | } 446 | 447 | data class Move(val from: Coordinates, val to: Coordinates) 448 | 449 | class NoPieceAtStartCellException : Exception() 450 | class IllegalMoveException : Exception() 451 | class WrongPromotionPieceColor: Exception() 452 | class PlayerHasNoKingException: Exception() -------------------------------------------------------------------------------- /src/main/kotlin/chess/core/game/GameInfo.kt: -------------------------------------------------------------------------------- 1 | package chess.core.game 2 | 3 | sealed class Castle 4 | object WhiteKingSideCastle : Castle() 5 | object WhiteQueenSideCastle: Castle() 6 | object BlackKingSideCastle: Castle() 7 | object BlackQueenSideCastle: Castle() 8 | 9 | 10 | data class GameInfo(val whiteTurn: Boolean, val castles: List, 11 | val enPassantFile: Int?, val nullityCount: Int, val moveNumber: Int) { 12 | companion object{ 13 | fun fenToGameInfo(fen: String) : GameInfo { 14 | val parts = fen.split("""\s+""".toRegex()) 15 | 16 | val whiteTurn = parts[1] == "w" 17 | 18 | var castles = listOf() 19 | if (parts[2].contains('K')) castles += WhiteKingSideCastle 20 | if (parts[2].contains('Q')) castles += WhiteQueenSideCastle 21 | if (parts[2].contains('k')) castles += BlackKingSideCastle 22 | if (parts[2].contains('q')) castles += BlackQueenSideCastle 23 | 24 | val enPassantFile = if (parts[3][0].toInt() in ('a'.toInt()..'h'.toInt())) 25 | parts[3][0].toInt() - 'a'.toInt() 26 | else null 27 | 28 | val nullityCount = Integer.parseInt(parts[4]) 29 | if (nullityCount < 0) throw IllegalArgumentException("nullityCount($nullityCount) must be >= 0.") 30 | 31 | val moveNumber = Integer.parseInt(parts[5]) 32 | if (moveNumber < 1) throw IllegalArgumentException("moveNumber($moveNumber) must be >= 1.") 33 | 34 | return GameInfo(whiteTurn, castles, enPassantFile, nullityCount, moveNumber) 35 | } 36 | } 37 | 38 | fun toFEN() : String { 39 | val builder = StringBuilder() 40 | 41 | if (whiteTurn) builder.append("w ") else builder.append("b ") 42 | 43 | val castlesBuilder = StringBuilder() 44 | if (castles.contains(WhiteKingSideCastle)) castlesBuilder.append("K") 45 | if (castles.contains(WhiteQueenSideCastle)) castlesBuilder.append("Q") 46 | if (castles.contains(BlackKingSideCastle)) castlesBuilder.append("k") 47 | if (castles.contains(BlackQueenSideCastle)) castlesBuilder.append("q") 48 | if (castlesBuilder.isEmpty()) builder.append("- ") else builder.append("$castlesBuilder ") 49 | 50 | if (enPassantFile == null){ 51 | builder.append("- ") 52 | } 53 | else { 54 | val enPassantBuilder = StringBuilder() 55 | val fileASCII = enPassantFile + 'a'.toInt() 56 | enPassantBuilder.append(fileASCII.toChar()) 57 | enPassantBuilder.append(if (whiteTurn) "6 " else "3 ") 58 | builder.append(enPassantBuilder) 59 | } 60 | 61 | builder.append("$nullityCount $moveNumber") 62 | 63 | return builder.toString() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/chess/core/history/History.kt: -------------------------------------------------------------------------------- 1 | package chess.core.history 2 | 3 | import chess.core.game.ChessGame 4 | 5 | 6 | class HistoryNode(val relatedPosition: ChessGame, val parentNode: HistoryNode?, 7 | val moveLeadingToThisNodeFAN: String?) { 8 | init { 9 | parentNode?.addChild(this) 10 | if (parentNode != null) require(moveLeadingToThisNodeFAN != null) 11 | {"Only the root node can bypass the moveLeadingToThisNodeFAN parameter."} 12 | } 13 | 14 | private var _comment: String? = null 15 | 16 | private var _mainLineChild : HistoryNode? = null 17 | 18 | private val _variantsChildren = mutableListOf() 19 | 20 | fun setComment(comment: String) { 21 | _comment = comment 22 | } 23 | 24 | fun removeComment() { 25 | _comment = null 26 | } 27 | 28 | val comment: String? 29 | get() = _comment 30 | 31 | 32 | fun addChild(child: HistoryNode) { 33 | if (_mainLineChild == null) { 34 | _mainLineChild = child 35 | return 36 | } 37 | val childMoveNotAlreadyAdded = _mainLineChild!!.moveLeadingToThisNodeFAN != child.moveLeadingToThisNodeFAN 38 | && _variantsChildren.all { it.moveLeadingToThisNodeFAN != child.moveLeadingToThisNodeFAN } 39 | 40 | if (childMoveNotAlreadyAdded) _variantsChildren.add(child) 41 | } 42 | 43 | 44 | fun deleteThisLine() { 45 | val lineRoot = findLineRoot(this) 46 | val belongsToRootMainLine = lineRoot.parentNode == null 47 | if (belongsToRootMainLine) { 48 | lineRoot._variantsChildren.clear() 49 | lineRoot._mainLineChild = null 50 | } else { 51 | val lineRootChildIndexForThisLine = findLineRootChildIndexContainingThisNode() 52 | lineRoot._variantsChildren.removeAt(lineRootChildIndexForThisLine!!) 53 | } 54 | } 55 | 56 | 57 | private fun findLineRootChildIndexContainingThisNode(): Int? { 58 | fun searchForThisNodeInMainLineOf(place: HistoryNode): Boolean { 59 | if (place == this) return true 60 | if (place._mainLineChild == this) return true 61 | if (place._mainLineChild == null) return false 62 | return searchForThisNodeInMainLineOf(place._mainLineChild!!) 63 | } 64 | 65 | val lineRoot = findLineRoot() 66 | if (lineRoot._variantsChildren.isEmpty()) return null 67 | val lineRootChildIndexForThisLine = lineRoot._variantsChildren.map { searchForThisNodeInMainLineOf(it) } 68 | .withIndex().filter { (_, value) -> value }[0].index 69 | return lineRootChildIndexForThisLine 70 | } 71 | 72 | fun findLineRoot(): HistoryNode = findLineRoot(this) 73 | 74 | private fun findLineRoot(node: HistoryNode): HistoryNode { 75 | if (node.parentNode == null) return node 76 | if (node in node.parentNode._variantsChildren) return node.parentNode 77 | return findLineRoot(node.parentNode) 78 | } 79 | 80 | fun promoteThisLine() { 81 | val lineRoot = findLineRoot() 82 | if (lineRoot.parentNode == null) return 83 | 84 | val lineRootChildContainingThisNodeIndex = findLineRootChildIndexContainingThisNode() 85 | if (lineRootChildContainingThisNodeIndex == null) return 86 | 87 | 88 | val temp = lineRoot._variantsChildren[lineRootChildContainingThisNodeIndex] 89 | lineRoot._variantsChildren[lineRootChildContainingThisNodeIndex] = lineRoot._mainLineChild!! 90 | lineRoot._mainLineChild = temp 91 | } 92 | 93 | val mainLine: HistoryNode? 94 | get() = _mainLineChild 95 | 96 | val variants: List 97 | get() = _variantsChildren 98 | } -------------------------------------------------------------------------------- /src/main/kotlin/chess/view/App.kt: -------------------------------------------------------------------------------- 1 | package chess.view 2 | 3 | import chess.core.Pieces.* 4 | import chess.core.game.ChessGame 5 | import chess.core.game.Coordinates 6 | import chess.core.game.GameInfo 7 | import chess.core.game.Move 8 | import chess.core.history.HistoryNode 9 | import javafx.animation.KeyFrame 10 | import javafx.animation.KeyValue 11 | import javafx.animation.Timeline 12 | import javafx.application.Application 13 | import javafx.application.Platform 14 | import javafx.geometry.Pos 15 | import javafx.scene.Cursor 16 | import javafx.scene.Group 17 | import javafx.scene.control.Hyperlink 18 | import javafx.scene.control.Label 19 | import javafx.scene.control.TextInputDialog 20 | import javafx.scene.image.ImageView 21 | import javafx.scene.input.MouseEvent 22 | import javafx.scene.paint.Color 23 | import javafx.scene.text.Font 24 | import javafx.scene.text.Text 25 | import javafx.scene.text.TextFlow 26 | import javafx.util.Duration 27 | import tornadofx.* 28 | import java.io.File 29 | import java.io.FileWriter 30 | import java.lang.Exception 31 | import java.util.* 32 | 33 | data class FenUpdatingEvent(val fen: String) : FXEvent() 34 | data class AddFANToHistory(val fan: String, val historyNode: HistoryNode) : FXEvent() 35 | data class ChangeChessBoardPosition(val historyNode: HistoryNode) : FXEvent() 36 | class HistoryNeedUpdatingEvent : FXEvent() 37 | 38 | fun Stack.clearTopItem() { 39 | if (this.isEmpty()) return 40 | 41 | val lastItemIndex = this.size - 1 42 | this[lastItemIndex] = 0 43 | } 44 | 45 | fun Stack.incrementTopItem() { 46 | if (this.isEmpty()) return 47 | 48 | val lastItemIndex = this.size - 1 49 | this[lastItemIndex]++ 50 | } 51 | 52 | fun Stack.topItem(): Int = this.peek() 53 | 54 | fun main(args: Array) { 55 | Application.launch(MyApp::class.java, *args) 56 | } 57 | 58 | class MyApp: App(MainView::class) 59 | 60 | class MainView : View() { 61 | init { 62 | subscribe { 63 | fenZone.text = it.fen 64 | } 65 | 66 | subscribe { 67 | historyZone.updateMovesFromRootNode(historyRootNode) 68 | } 69 | 70 | subscribe { 71 | val currentHistoryNode = it.historyNode 72 | val currentFEN = currentHistoryNode.relatedPosition.toFEN() 73 | chessBoard.setHistoryNode(currentHistoryNode) 74 | fenZone.text = currentFEN 75 | } 76 | 77 | subscribe { 78 | historyZone.updateMovesFromRootNode(historyRootNode) 79 | } 80 | } 81 | 82 | companion object{ 83 | val fenFile = File("Log.txt") 84 | var clicked = 0 85 | fun setTimer(clicked: Int) { 86 | val timer = Timer() 87 | var interval = 31 88 | val check = clicked 89 | timer.scheduleAtFixedRate(object : TimerTask() { 90 | override fun run() { 91 | if (interval > 0 && check == MainView.clicked ) { 92 | Platform.runLater { 93 | ChessBoard.turnComponent?.text = interval.toString() 94 | } 95 | interval-- 96 | } 97 | else { 98 | timer.cancel() 99 | } 100 | } 101 | }, 0, 1000) 102 | } 103 | } 104 | 105 | val historyRootNode = HistoryNode(relatedPosition = ChessGame.INITIAL_POSITION, parentNode = null, 106 | moveLeadingToThisNodeFAN = null) 107 | val fenZone = Text(historyRootNode.relatedPosition.toFEN()) 108 | val historyZone = MovesHistory() 109 | val chessBoard = ChessBoard(historyRootNode) 110 | 111 | override val root = borderpane { 112 | title = "Chess" 113 | center = chessBoard.root 114 | right = historyZone.root 115 | bottom = fenZone 116 | } 117 | } 118 | 119 | class ChessBoard(startHistoryNode: HistoryNode) : View() { 120 | 121 | companion object{ 122 | var turnComponent: Label? = null 123 | } 124 | 125 | private val cellsSize = 50.0 126 | private val picturesSize = 75.0 127 | private val cursorOffset = cellsSize * 0.50 128 | private val picturesScale = cellsSize / picturesSize 129 | 130 | private val piecesGroup = Group() 131 | private var currentHistoryNode = startHistoryNode 132 | 133 | private var currentHighlighter: Label? = null 134 | private var dragStartHighlighter: Label? = null 135 | private var dragStartCoordinates: Coordinates? = null 136 | private var movedPieceCursor: ImageView? = null 137 | private var possibleMoveList = mutableListOf