diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..152304f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.7 + +#by default, source code is not obfuscated +ARG OBF=false + +COPY pyarmor-regcode-1.txt ./pyarmor-regcode-1.txt + +RUN pip install pyarmor +RUN pyarmor register pyarmor-regcode-1.txt + +ADD /src /src +# obfuscate the source code ("src" folder). It creates "dist" folder with obfuscated source code that is copied again to "src" folder. +RUN if [ "$OBF" = "true" ]; then pyarmor obfuscate -r --src="src" sunfish.py;cp -R /dist/. /src; fi; + +# remove file license python obfuscator +RUN find pyarmor-regcode-1.txt -delete + +CMD python -u /src/sunfish.py "e2e4" \ No newline at end of file diff --git a/README.md b/README.md index 9e2b957..a2a6663 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ![Sunfish logo](https://raw.github.com/thomasahle/sunfish/master/logo/sunfish_large.png) - + ## Introduction Sunfish is a simple, but strong chess engine, written in Python, mostly for teaching purposes. Without tables and its simple interface, it takes up just 111 lines of code! (see [`compressed.py`](https://github.com/thomasahle/sunfish/blob/master/compressed.py)) Yet [it plays at ratings above 2000 at Lichess](https://lichess.org/@/sunfish-engine). @@ -64,3 +64,4 @@ In terms of Heritage, Sunfish borrows much more from [Micro-Max by Geert Muller] # License [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..6c7a08d --- /dev/null +++ b/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'com.bmuschko.docker-remote-api' version '6.4.0' +} + +version='0.1' + +repositories { + mavenCentral() +} + + +// Import task types +import com.bmuschko.gradle.docker.tasks.image.* + + +task buildImage(type: DockerBuildImage) { + inputDir = file('.') + images.add("docker.io/sebastianrevuelta/sunfish:${project.version}") +} + + +task pushImage(type: DockerPushImage, dependsOn: buildImage){ + images.add("docker.io/sebastianrevuelta/sunfish:${project.version}") +} + +task buildImageObf(type: DockerBuildImage) { + inputDir = file('.') + buildArgs = ['OBF':'true'] + images.add("docker.io/sebastianrevuelta/sunfish:obf-${project.version}") + +} + +task pushImageObf(type: DockerPushImage, dependsOn: buildImageObf) { + dependsOn buildImageObf + images.add("docker.io/sebastianrevuelta/sunfish:obf-${project.version}") +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..12d38de --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..fcb1861 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.6.1/userguide/multi_project_builds.html + */ + +rootProject.name = 'sunfish' diff --git a/compressed.py b/src/compressed.py similarity index 100% rename from compressed.py rename to src/compressed.py diff --git a/logo/sunfish_large.png b/src/logo/sunfish_large.png similarity index 100% rename from logo/sunfish_large.png rename to src/logo/sunfish_large.png diff --git a/logo/sunfish_small.bmp b/src/logo/sunfish_small.bmp similarity index 100% rename from logo/sunfish_small.bmp rename to src/logo/sunfish_small.bmp diff --git a/sunfish.6 b/src/sunfish.6 similarity index 100% rename from sunfish.6 rename to src/sunfish.6 diff --git a/sunfish.py b/src/sunfish.py similarity index 83% rename from sunfish.py rename to src/sunfish.py index c6875f3..82c321c 100644 --- a/sunfish.py +++ b/src/sunfish.py @@ -1,451 +1,556 @@ -#!/usr/bin/env pypy -# -*- coding: utf-8 -*- - -from __future__ import print_function -import re, sys, time -from itertools import count -from collections import namedtuple - -############################################################################### -# Piece-Square tables. Tune these to change sunfish's behaviour -############################################################################### - -piece = { 'P': 100, 'N': 280, 'B': 320, 'R': 479, 'Q': 929, 'K': 60000 } -pst = { - 'P': ( 0, 0, 0, 0, 0, 0, 0, 0, - 78, 83, 86, 73, 102, 82, 85, 90, - 7, 29, 21, 44, 40, 31, 44, 7, - -17, 16, -2, 15, 14, 0, 15, -13, - -26, 3, 10, 9, 6, 1, 0, -23, - -22, 9, 5, -11, -10, -2, 3, -19, - -31, 8, -7, -37, -36, -14, 3, -31, - 0, 0, 0, 0, 0, 0, 0, 0), - 'N': ( -66, -53, -75, -75, -10, -55, -58, -70, - -3, -6, 100, -36, 4, 62, -4, -14, - 10, 67, 1, 74, 73, 27, 62, -2, - 24, 24, 45, 37, 33, 41, 25, 17, - -1, 5, 31, 21, 22, 35, 2, 0, - -18, 10, 13, 22, 18, 15, 11, -14, - -23, -15, 2, 0, 2, 0, -23, -20, - -74, -23, -26, -24, -19, -35, -22, -69), - 'B': ( -59, -78, -82, -76, -23,-107, -37, -50, - -11, 20, 35, -42, -39, 31, 2, -22, - -9, 39, -32, 41, 52, -10, 28, -14, - 25, 17, 20, 34, 26, 25, 15, 10, - 13, 10, 17, 23, 17, 16, 0, 7, - 14, 25, 24, 15, 8, 25, 20, 15, - 19, 20, 11, 6, 7, 6, 20, 16, - -7, 2, -15, -12, -14, -15, -10, -10), - 'R': ( 35, 29, 33, 4, 37, 33, 56, 50, - 55, 29, 56, 67, 55, 62, 34, 60, - 19, 35, 28, 33, 45, 27, 25, 15, - 0, 5, 16, 13, 18, -4, -9, -6, - -28, -35, -16, -21, -13, -29, -46, -30, - -42, -28, -42, -25, -25, -35, -26, -46, - -53, -38, -31, -26, -29, -43, -44, -53, - -30, -24, -18, 5, -2, -18, -31, -32), - 'Q': ( 6, 1, -8,-104, 69, 24, 88, 26, - 14, 32, 60, -10, 20, 76, 57, 24, - -2, 43, 32, 60, 72, 63, 43, 2, - 1, -16, 22, 17, 25, 20, -13, -6, - -14, -15, -2, -5, -1, -10, -20, -22, - -30, -6, -13, -11, -16, -11, -16, -27, - -36, -18, 0, -19, -15, -15, -21, -38, - -39, -30, -31, -13, -31, -36, -34, -42), - 'K': ( 4, 54, 47, -99, -99, 60, 83, -62, - -32, 10, 55, 56, 56, 55, 10, 3, - -62, 12, -57, 44, -67, 28, 37, -31, - -55, 50, 11, -4, -19, 13, 0, -49, - -55, -43, -52, -28, -51, -47, -8, -50, - -47, -42, -43, -79, -64, -32, -29, -32, - -4, 3, -14, -50, -57, -18, 13, 4, - 17, 30, -3, -14, 6, -1, 40, 18), -} -# Pad tables and join piece and pst dictionaries -for k, table in pst.items(): - padrow = lambda row: (0,) + tuple(x+piece[k] for x in row) + (0,) - pst[k] = sum((padrow(table[i*8:i*8+8]) for i in range(8)), ()) - pst[k] = (0,)*20 + pst[k] + (0,)*20 - -############################################################################### -# Global constants -############################################################################### - -# Our board is represented as a 120 character string. The padding allows for -# fast detection of moves that don't stay within the board. -A1, H1, A8, H8 = 91, 98, 21, 28 -initial = ( - ' \n' # 0 - 9 - ' \n' # 10 - 19 - ' rnbqkbnr\n' # 20 - 29 - ' pppppppp\n' # 30 - 39 - ' ........\n' # 40 - 49 - ' ........\n' # 50 - 59 - ' ........\n' # 60 - 69 - ' ........\n' # 70 - 79 - ' PPPPPPPP\n' # 80 - 89 - ' RNBQKBNR\n' # 90 - 99 - ' \n' # 100 -109 - ' \n' # 110 -119 -) - -# Lists of possible moves for each piece type. -N, E, S, W = -10, 1, 10, -1 -directions = { - 'P': (N, N+N, N+W, N+E), - 'N': (N+N+E, E+N+E, E+S+E, S+S+E, S+S+W, W+S+W, W+N+W, N+N+W), - 'B': (N+E, S+E, S+W, N+W), - 'R': (N, E, S, W), - 'Q': (N, E, S, W, N+E, S+E, S+W, N+W), - 'K': (N, E, S, W, N+E, S+E, S+W, N+W) -} - -# Mate value must be greater than 8*queen + 2*(rook+knight+bishop) -# King value is set to twice this value such that if the opponent is -# 8 queens up, but we got the king, we still exceed MATE_VALUE. -# When a MATE is detected, we'll set the score to MATE_UPPER - plies to get there -# E.g. Mate in 3 will be MATE_UPPER - 6 -MATE_LOWER = piece['K'] - 10*piece['Q'] -MATE_UPPER = piece['K'] + 10*piece['Q'] - -# The table size is the maximum number of elements in the transposition table. -TABLE_SIZE = 1e7 - -# Constants for tuning search -QS_LIMIT = 219 -EVAL_ROUGHNESS = 13 -DRAW_TEST = True - - -############################################################################### -# Chess logic -############################################################################### - -class Position(namedtuple('Position', 'board score wc bc ep kp')): - """ A state of a chess game - board -- a 120 char representation of the board - score -- the board evaluation - wc -- the castling rights, [west/queen side, east/king side] - bc -- the opponent castling rights, [west/king side, east/queen side] - ep - the en passant square - kp - the king passant square - """ - - def gen_moves(self): - # For each of our pieces, iterate through each possible 'ray' of moves, - # as defined in the 'directions' map. The rays are broken e.g. by - # captures or immediately in case of pieces such as knights. - for i, p in enumerate(self.board): - if not p.isupper(): continue - for d in directions[p]: - for j in count(i+d, d): - q = self.board[j] - # Stay inside the board, and off friendly pieces - if q.isspace() or q.isupper(): break - # Pawn move, double move and capture - if p == 'P' and d in (N, N+N) and q != '.': break - if p == 'P' and d == N+N and (i < A1+N or self.board[i+N] != '.'): break - if p == 'P' and d in (N+W, N+E) and q == '.' \ - and j not in (self.ep, self.kp, self.kp-1, self.kp+1): break - # Move it - yield (i, j) - # Stop crawlers from sliding, and sliding after captures - if p in 'PNK' or q.islower(): break - # Castling, by sliding the rook next to the king - if i == A1 and self.board[j+E] == 'K' and self.wc[0]: yield (j+E, j+W) - if i == H1 and self.board[j+W] == 'K' and self.wc[1]: yield (j+W, j+E) - - def rotate(self): - ''' Rotates the board, preserving enpassant ''' - return Position( - self.board[::-1].swapcase(), -self.score, self.bc, self.wc, - 119-self.ep if self.ep else 0, - 119-self.kp if self.kp else 0) - - def nullmove(self): - ''' Like rotate, but clears ep and kp ''' - return Position( - self.board[::-1].swapcase(), -self.score, - self.bc, self.wc, 0, 0) - - def move(self, move): - i, j = move - p, q = self.board[i], self.board[j] - put = lambda board, i, p: board[:i] + p + board[i+1:] - # Copy variables and reset ep and kp - board = self.board - wc, bc, ep, kp = self.wc, self.bc, 0, 0 - score = self.score + self.value(move) - # Actual move - board = put(board, j, board[i]) - board = put(board, i, '.') - # Castling rights, we move the rook or capture the opponent's - if i == A1: wc = (False, wc[1]) - if i == H1: wc = (wc[0], False) - if j == A8: bc = (bc[0], False) - if j == H8: bc = (False, bc[1]) - # Castling - if p == 'K': - wc = (False, False) - if abs(j-i) == 2: - kp = (i+j)//2 - board = put(board, A1 if j < i else H1, '.') - board = put(board, kp, 'R') - # Pawn promotion, double move and en passant capture - if p == 'P': - if A8 <= j <= H8: - board = put(board, j, 'Q') - if j - i == 2*N: - ep = i + N - if j == self.ep: - board = put(board, j+S, '.') - # We rotate the returned position, so it's ready for the next player - return Position(board, score, wc, bc, ep, kp).rotate() - - def value(self, move): - i, j = move - p, q = self.board[i], self.board[j] - # Actual move - score = pst[p][j] - pst[p][i] - # Capture - if q.islower(): - score += pst[q.upper()][119-j] - # Castling check detection - if abs(j-self.kp) < 2: - score += pst['K'][119-j] - # Castling - if p == 'K' and abs(i-j) == 2: - score += pst['R'][(i+j)//2] - score -= pst['R'][A1 if j < i else H1] - # Special pawn stuff - if p == 'P': - if A8 <= j <= H8: - score += pst['Q'][j] - pst['P'][j] - if j == self.ep: - score += pst['P'][119-(j+S)] - return score - -############################################################################### -# Search logic -############################################################################### - -# lower <= s(pos) <= upper -Entry = namedtuple('Entry', 'lower upper') - -class Searcher: - def __init__(self): - self.tp_score = {} - self.tp_move = {} - self.history = set() - self.nodes = 0 - - def bound(self, pos, gamma, depth, root=True): - """ returns r where - s(pos) <= r < gamma if gamma > s(pos) - gamma <= r <= s(pos) if gamma <= s(pos)""" - self.nodes += 1 - - # Depth <= 0 is QSearch. Here any position is searched as deeply as is needed for - # calmness, and from this point on there is no difference in behaviour depending on - # depth, so so there is no reason to keep different depths in the transposition table. - depth = max(depth, 0) - - # Sunfish is a king-capture engine, so we should always check if we - # still have a king. Notice since this is the only termination check, - # the remaining code has to be comfortable with being mated, stalemated - # or able to capture the opponent king. - if pos.score <= -MATE_LOWER: - return -MATE_UPPER - - # We detect 3-fold captures by comparing against previously - # _actually played_ positions. - # Note that we need to do this before we look in the table, as the - # position may have been previously reached with a different score. - # This is what prevents a search instability. - # FIXME: This is not true, since other positions will be affected by - # the new values for all the drawn positions. - if DRAW_TEST: - if not root and pos in self.history: - return 0 - - # Look in the table if we have already searched this position before. - # We also need to be sure, that the stored search was over the same - # nodes as the current search. - entry = self.tp_score.get((pos, depth, root), Entry(-MATE_UPPER, MATE_UPPER)) - if entry.lower >= gamma and (not root or self.tp_move.get(pos) is not None): - return entry.lower - if entry.upper < gamma: - return entry.upper - - # Here extensions may be added - # Such as 'if in_check: depth += 1' - - # Generator of moves to search in order. - # This allows us to define the moves, but only calculate them if needed. - def moves(): - # First try not moving at all. We only do this if there is at least one major - # piece left on the board, since otherwise zugzwangs are too dangerous. - if depth > 0 and not root and any(c in pos.board for c in 'RBNQ'): - yield None, -self.bound(pos.nullmove(), 1-gamma, depth-3, root=False) - # For QSearch we have a different kind of null-move, namely we can just stop - # and not capture anythign else. - if depth == 0: - yield None, pos.score - # Then killer move. We search it twice, but the tp will fix things for us. - # Note, we don't have to check for legality, since we've already done it - # before. Also note that in QS the killer must be a capture, otherwise we - # will be non deterministic. - killer = self.tp_move.get(pos) - if killer and (depth > 0 or pos.value(killer) >= QS_LIMIT): - yield killer, -self.bound(pos.move(killer), 1-gamma, depth-1, root=False) - # Then all the other moves - for move in sorted(pos.gen_moves(), key=pos.value, reverse=True): - #for val, move in sorted(((pos.value(move), move) for move in pos.gen_moves()), reverse=True): - # If depth == 0 we only try moves with high intrinsic score (captures and - # promotions). Otherwise we do all moves. - if depth > 0 or pos.value(move) >= QS_LIMIT: - yield move, -self.bound(pos.move(move), 1-gamma, depth-1, root=False) - - # Run through the moves, shortcutting when possible - best = -MATE_UPPER - for move, score in moves(): - best = max(best, score) - if best >= gamma: - # Clear before setting, so we always have a value - if len(self.tp_move) > TABLE_SIZE: self.tp_move.clear() - # Save the move for pv construction and killer heuristic - self.tp_move[pos] = move - break - - # Stalemate checking is a bit tricky: Say we failed low, because - # we can't (legally) move and so the (real) score is -infty. - # At the next depth we are allowed to just return r, -infty <= r < gamma, - # which is normally fine. - # However, what if gamma = -10 and we don't have any legal moves? - # Then the score is actaully a draw and we should fail high! - # Thus, if best < gamma and best < 0 we need to double check what we are doing. - # This doesn't prevent sunfish from making a move that results in stalemate, - # but only if depth == 1, so that's probably fair enough. - # (Btw, at depth 1 we can also mate without realizing.) - if best < gamma and best < 0 and depth > 0: - is_dead = lambda pos: any(pos.value(m) >= MATE_LOWER for m in pos.gen_moves()) - if all(is_dead(pos.move(m)) for m in pos.gen_moves()): - in_check = is_dead(pos.nullmove()) - best = -MATE_UPPER if in_check else 0 - - # Clear before setting, so we always have a value - if len(self.tp_score) > TABLE_SIZE: self.tp_score.clear() - # Table part 2 - if best >= gamma: - self.tp_score[pos, depth, root] = Entry(best, entry.upper) - if best < gamma: - self.tp_score[pos, depth, root] = Entry(entry.lower, best) - - return best - - def search(self, pos, history=()): - """ Iterative deepening MTD-bi search """ - self.nodes = 0 - if DRAW_TEST: - self.history = set(history) - # print('# Clearing table due to new history') - self.tp_score.clear() - - # In finished games, we could potentially go far enough to cause a recursion - # limit exception. Hence we bound the ply. - for depth in range(1, 1000): - # The inner loop is a binary search on the score of the position. - # Inv: lower <= score <= upper - # 'while lower != upper' would work, but play tests show a margin of 20 plays - # better. - lower, upper = -MATE_UPPER, MATE_UPPER - while lower < upper - EVAL_ROUGHNESS: - gamma = (lower+upper+1)//2 - score = self.bound(pos, gamma, depth) - if score >= gamma: - lower = score - if score < gamma: - upper = score - # We want to make sure the move to play hasn't been kicked out of the table, - # So we make another call that must always fail high and thus produce a move. - self.bound(pos, lower, depth) - # If the game hasn't finished we can retrieve our move from the - # transposition table. - yield depth, self.tp_move.get(pos), self.tp_score.get((pos, depth, True)).lower - - -############################################################################### -# User interface -############################################################################### - -# Python 2 compatability -if sys.version_info[0] == 2: - input = raw_input - - -def parse(c): - fil, rank = ord(c[0]) - ord('a'), int(c[1]) - 1 - return A1 + fil - 10*rank - - -def render(i): - rank, fil = divmod(i - A1, 10) - return chr(fil + ord('a')) + str(-rank + 1) - - -def print_pos(pos): - print() - uni_pieces = {'R':'♜', 'N':'♞', 'B':'♝', 'Q':'♛', 'K':'♚', 'P':'♟', - 'r':'♖', 'n':'♘', 'b':'♗', 'q':'♕', 'k':'♔', 'p':'♙', '.':'·'} - for i, row in enumerate(pos.board.split()): - print(' ', 8-i, ' '.join(uni_pieces.get(p, p) for p in row)) - print(' a b c d e f g h \n\n') - - -def main(): - hist = [Position(initial, 0, (True,True), (True,True), 0, 0)] - searcher = Searcher() - while True: - print_pos(hist[-1]) - - if hist[-1].score <= -MATE_LOWER: - print("You lost") - break - - # We query the user until she enters a (pseudo) legal move. - move = None - while move not in hist[-1].gen_moves(): - match = re.match('([a-h][1-8])'*2, input('Your move: ')) - if match: - move = parse(match.group(1)), parse(match.group(2)) - else: - # Inform the user when invalid input (e.g. "help") is entered - print("Please enter a move like g8f6") - hist.append(hist[-1].move(move)) - - # After our move we rotate the board and print it again. - # This allows us to see the effect of our move. - print_pos(hist[-1].rotate()) - - if hist[-1].score <= -MATE_LOWER: - print("You won") - break - - # Fire up the engine to look for a move. - start = time.time() - for _depth, move, score in searcher.search(hist[-1], hist): - if time.time() - start > 1: - break - - if score == MATE_UPPER: - print("Checkmate!") - - # The black player moves from a rotated position, so we have to - # 'back rotate' the move before printing it. - print("My move:", render(119-move[0]) + render(119-move[1])) - hist.append(hist[-1].move(move)) - - -if __name__ == '__main__': - main() - +#!/usr/bin/env pypy +# -*- coding: utf-8 -*- + +from __future__ import print_function +import re, sys, time +from itertools import count +from collections import namedtuple +import configparser +import os.path + +############################################################################### +# Piece-Square tables. Tune these to change sunfish's behaviour +############################################################################### + +piece = { 'P': 100, 'N': 280, 'B': 320, 'R': 479, 'Q': 929, 'K': 60000 } +pst = { + 'P': ( 0, 0, 0, 0, 0, 0, 0, 0, + 78, 83, 86, 73, 102, 82, 85, 90, + 7, 29, 21, 44, 40, 31, 44, 7, + -17, 16, -2, 15, 14, 0, 15, -13, + -26, 3, 10, 9, 6, 1, 0, -23, + -22, 9, 5, -11, -10, -2, 3, -19, + -31, 8, -7, -37, -36, -14, 3, -31, + 0, 0, 0, 0, 0, 0, 0, 0), + 'N': ( -66, -53, -75, -75, -10, -55, -58, -70, + -3, -6, 100, -36, 4, 62, -4, -14, + 10, 67, 1, 74, 73, 27, 62, -2, + 24, 24, 45, 37, 33, 41, 25, 17, + -1, 5, 31, 21, 22, 35, 2, 0, + -18, 10, 13, 22, 18, 15, 11, -14, + -23, -15, 2, 0, 2, 0, -23, -20, + -74, -23, -26, -24, -19, -35, -22, -69), + 'B': ( -59, -78, -82, -76, -23,-107, -37, -50, + -11, 20, 35, -42, -39, 31, 2, -22, + -9, 39, -32, 41, 52, -10, 28, -14, + 25, 17, 20, 34, 26, 25, 15, 10, + 13, 10, 17, 23, 17, 16, 0, 7, + 14, 25, 24, 15, 8, 25, 20, 15, + 19, 20, 11, 6, 7, 6, 20, 16, + -7, 2, -15, -12, -14, -15, -10, -10), + 'R': ( 35, 29, 33, 4, 37, 33, 56, 50, + 55, 29, 56, 67, 55, 62, 34, 60, + 19, 35, 28, 33, 45, 27, 25, 15, + 0, 5, 16, 13, 18, -4, -9, -6, + -28, -35, -16, -21, -13, -29, -46, -30, + -42, -28, -42, -25, -25, -35, -26, -46, + -53, -38, -31, -26, -29, -43, -44, -53, + -30, -24, -18, 5, -2, -18, -31, -32), + 'Q': ( 6, 1, -8,-104, 69, 24, 88, 26, + 14, 32, 60, -10, 20, 76, 57, 24, + -2, 43, 32, 60, 72, 63, 43, 2, + 1, -16, 22, 17, 25, 20, -13, -6, + -14, -15, -2, -5, -1, -10, -20, -22, + -30, -6, -13, -11, -16, -11, -16, -27, + -36, -18, 0, -19, -15, -15, -21, -38, + -39, -30, -31, -13, -31, -36, -34, -42), + 'K': ( 4, 54, 47, -99, -99, 60, 83, -62, + -32, 10, 55, 56, 56, 55, 10, 3, + -62, 12, -57, 44, -67, 28, 37, -31, + -55, 50, 11, -4, -19, 13, 0, -49, + -55, -43, -52, -28, -51, -47, -8, -50, + -47, -42, -43, -79, -64, -32, -29, -32, + -4, 3, -14, -50, -57, -18, 13, 4, + 17, 30, -3, -14, 6, -1, 40, 18), +} +# Pad tables and join piece and pst dictionaries +for k, table in pst.items(): + padrow = lambda row: (0,) + tuple(x+piece[k] for x in row) + (0,) + pst[k] = sum((padrow(table[i*8:i*8+8]) for i in range(8)), ()) + pst[k] = (0,)*20 + pst[k] + (0,)*20 + +############################################################################### +# Global constants +############################################################################### + +# Our board is represented as a 120 character string. The padding allows for +# fast detection of moves that don't stay within the board. +A1, H1, A8, H8 = 91, 98, 21, 28 +initial = ( + ' \n' # 0 - 9 + ' \n' # 10 - 19 + ' rnbqkbnr\n' # 20 - 29 + ' pppppppp\n' # 30 - 39 + ' ........\n' # 40 - 49 + ' ........\n' # 50 - 59 + ' ........\n' # 60 - 69 + ' ........\n' # 70 - 79 + ' PPPPPPPP\n' # 80 - 89 + ' RNBQKBNR\n' # 90 - 99 + ' \n' # 100 -109 + ' \n' # 110 -119 +) + +# Lists of possible moves for each piece type. +N, E, S, W = -10, 1, 10, -1 +directions = { + 'P': (N, N+N, N+W, N+E), + 'N': (N+N+E, E+N+E, E+S+E, S+S+E, S+S+W, W+S+W, W+N+W, N+N+W), + 'B': (N+E, S+E, S+W, N+W), + 'R': (N, E, S, W), + 'Q': (N, E, S, W, N+E, S+E, S+W, N+W), + 'K': (N, E, S, W, N+E, S+E, S+W, N+W) +} + +# Mate value must be greater than 8*queen + 2*(rook+knight+bishop) +# King value is set to twice this value such that if the opponent is +# 8 queens up, but we got the king, we still exceed MATE_VALUE. +# When a MATE is detected, we'll set the score to MATE_UPPER - plies to get there +# E.g. Mate in 3 will be MATE_UPPER - 6 +MATE_LOWER = piece['K'] - 10*piece['Q'] +MATE_UPPER = piece['K'] + 10*piece['Q'] + +# The table size is the maximum number of elements in the transposition table. +TABLE_SIZE = 1e7 + +# Constants for tuning search +QS_LIMIT = 219 +EVAL_ROUGHNESS = 13 +DRAW_TEST = True + + +############################################################################### +# Chess logic +############################################################################### + +class Position(namedtuple('Position', 'board score wc bc ep kp')): + """ A state of a chess game + board -- a 120 char representation of the board + score -- the board evaluation + wc -- the castling rights, [west/queen side, east/king side] + bc -- the opponent castling rights, [west/king side, east/queen side] + ep - the en passant square + kp - the king passant square + """ + + def gen_moves(self): + # For each of our pieces, iterate through each possible 'ray' of moves, + # as defined in the 'directions' map. The rays are broken e.g. by + # captures or immediately in case of pieces such as knights. + for i, p in enumerate(self.board): + if not p.isupper(): continue + for d in directions[p]: + for j in count(i+d, d): + q = self.board[j] + # Stay inside the board, and off friendly pieces + if q.isspace() or q.isupper(): break + # Pawn move, double move and capture + if p == 'P' and d in (N, N+N) and q != '.': break + if p == 'P' and d == N+N and (i < A1+N or self.board[i+N] != '.'): break + if p == 'P' and d in (N+W, N+E) and q == '.' \ + and j not in (self.ep, self.kp, self.kp-1, self.kp+1): break + # Move it + yield (i, j) + # Stop crawlers from sliding, and sliding after captures + if p in 'PNK' or q.islower(): break + # Castling, by sliding the rook next to the king + if i == A1 and self.board[j+E] == 'K' and self.wc[0]: yield (j+E, j+W) + if i == H1 and self.board[j+W] == 'K' and self.wc[1]: yield (j+W, j+E) + + def rotate(self): + ''' Rotates the board, preserving enpassant ''' + return Position( + self.board[::-1].swapcase(), -self.score, self.bc, self.wc, + 119-self.ep if self.ep else 0, + 119-self.kp if self.kp else 0) + + def nullmove(self): + ''' Like rotate, but clears ep and kp ''' + return Position( + self.board[::-1].swapcase(), -self.score, + self.bc, self.wc, 0, 0) + + def move(self, move): + i, j = move + p, q = self.board[i], self.board[j] + put = lambda board, i, p: board[:i] + p + board[i+1:] + # Copy variables and reset ep and kp + board = self.board + wc, bc, ep, kp = self.wc, self.bc, 0, 0 + score = self.score + self.value(move) + # Actual move + board = put(board, j, board[i]) + board = put(board, i, '.') + # Castling rights, we move the rook or capture the opponent's + if i == A1: wc = (False, wc[1]) + if i == H1: wc = (wc[0], False) + if j == A8: bc = (bc[0], False) + if j == H8: bc = (False, bc[1]) + # Castling + if p == 'K': + wc = (False, False) + if abs(j-i) == 2: + kp = (i+j)//2 + board = put(board, A1 if j < i else H1, '.') + board = put(board, kp, 'R') + # Pawn promotion, double move and en passant capture + if p == 'P': + if A8 <= j <= H8: + board = put(board, j, 'Q') + if j - i == 2*N: + ep = i + N + if j == self.ep: + board = put(board, j+S, '.') + # We rotate the returned position, so it's ready for the next player + return Position(board, score, wc, bc, ep, kp).rotate() + + def value(self, move): + i, j = move + p, q = self.board[i], self.board[j] + # Actual move + score = pst[p][j] - pst[p][i] + # Capture + if q.islower(): + score += pst[q.upper()][119-j] + # Castling check detection + if abs(j-self.kp) < 2: + score += pst['K'][119-j] + # Castling + if p == 'K' and abs(i-j) == 2: + score += pst['R'][(i+j)//2] + score -= pst['R'][A1 if j < i else H1] + # Special pawn stuff + if p == 'P': + if A8 <= j <= H8: + score += pst['Q'][j] - pst['P'][j] + if j == self.ep: + score += pst['P'][119-(j+S)] + return score + +############################################################################### +# Search logic +############################################################################### + +# lower <= s(pos) <= upper +Entry = namedtuple('Entry', 'lower upper') + +class Searcher: + def __init__(self): + self.tp_score = {} + self.tp_move = {} + self.history = set() + self.nodes = 0 + + def bound(self, pos, gamma, depth, root=True): + """ returns r where + s(pos) <= r < gamma if gamma > s(pos) + gamma <= r <= s(pos) if gamma <= s(pos)""" + self.nodes += 1 + + # Depth <= 0 is QSearch. Here any position is searched as deeply as is needed for + # calmness, and from this point on there is no difference in behaviour depending on + # depth, so so there is no reason to keep different depths in the transposition table. + depth = max(depth, 0) + + # Sunfish is a king-capture engine, so we should always check if we + # still have a king. Notice since this is the only termination check, + # the remaining code has to be comfortable with being mated, stalemated + # or able to capture the opponent king. + if pos.score <= -MATE_LOWER: + return -MATE_UPPER + + # We detect 3-fold captures by comparing against previously + # _actually played_ positions. + # Note that we need to do this before we look in the table, as the + # position may have been previously reached with a different score. + # This is what prevents a search instability. + # FIXME: This is not true, since other positions will be affected by + # the new values for all the drawn positions. + if DRAW_TEST: + if not root and pos in self.history: + return 0 + + # Look in the table if we have already searched this position before. + # We also need to be sure, that the stored search was over the same + # nodes as the current search. + entry = self.tp_score.get((pos, depth, root), Entry(-MATE_UPPER, MATE_UPPER)) + if entry.lower >= gamma and (not root or self.tp_move.get(pos) is not None): + return entry.lower + if entry.upper < gamma: + return entry.upper + + # Here extensions may be added + # Such as 'if in_check: depth += 1' + + # Generator of moves to search in order. + # This allows us to define the moves, but only calculate them if needed. + def moves(): + # First try not moving at all. We only do this if there is at least one major + # piece left on the board, since otherwise zugzwangs are too dangerous. + if depth > 0 and not root and any(c in pos.board for c in 'RBNQ'): + yield None, -self.bound(pos.nullmove(), 1-gamma, depth-3, root=False) + # For QSearch we have a different kind of null-move, namely we can just stop + # and not capture anythign else. + if depth == 0: + yield None, pos.score + # Then killer move. We search it twice, but the tp will fix things for us. + # Note, we don't have to check for legality, since we've already done it + # before. Also note that in QS the killer must be a capture, otherwise we + # will be non deterministic. + killer = self.tp_move.get(pos) + if killer and (depth > 0 or pos.value(killer) >= QS_LIMIT): + yield killer, -self.bound(pos.move(killer), 1-gamma, depth-1, root=False) + # Then all the other moves + for move in sorted(pos.gen_moves(), key=pos.value, reverse=True): + #for val, move in sorted(((pos.value(move), move) for move in pos.gen_moves()), reverse=True): + # If depth == 0 we only try moves with high intrinsic score (captures and + # promotions). Otherwise we do all moves. + if depth > 0 or pos.value(move) >= QS_LIMIT: + yield move, -self.bound(pos.move(move), 1-gamma, depth-1, root=False) + + # Run through the moves, shortcutting when possible + best = -MATE_UPPER + for move, score in moves(): + best = max(best, score) + if best >= gamma: + # Clear before setting, so we always have a value + if len(self.tp_move) > TABLE_SIZE: self.tp_move.clear() + # Save the move for pv construction and killer heuristic + self.tp_move[pos] = move + break + + # Stalemate checking is a bit tricky: Say we failed low, because + # we can't (legally) move and so the (real) score is -infty. + # At the next depth we are allowed to just return r, -infty <= r < gamma, + # which is normally fine. + # However, what if gamma = -10 and we don't have any legal moves? + # Then the score is actaully a draw and we should fail high! + # Thus, if best < gamma and best < 0 we need to double check what we are doing. + # This doesn't prevent sunfish from making a move that results in stalemate, + # but only if depth == 1, so that's probably fair enough. + # (Btw, at depth 1 we can also mate without realizing.) + if best < gamma and best < 0 and depth > 0: + is_dead = lambda pos: any(pos.value(m) >= MATE_LOWER for m in pos.gen_moves()) + if all(is_dead(pos.move(m)) for m in pos.gen_moves()): + in_check = is_dead(pos.nullmove()) + best = -MATE_UPPER if in_check else 0 + + # Clear before setting, so we always have a value + if len(self.tp_score) > TABLE_SIZE: self.tp_score.clear() + # Table part 2 + if best >= gamma: + self.tp_score[pos, depth, root] = Entry(best, entry.upper) + if best < gamma: + self.tp_score[pos, depth, root] = Entry(entry.lower, best) + + return best + + def search(self, pos, history=()): + """ Iterative deepening MTD-bi search """ + self.nodes = 0 + if DRAW_TEST: + self.history = set(history) + # print('# Clearing table due to new history') + self.tp_score.clear() + + # In finished games, we could potentially go far enough to cause a recursion + # limit exception. Hence we bound the ply. + for depth in range(1, 1000): + # The inner loop is a binary search on the score of the position. + # Inv: lower <= score <= upper + # 'while lower != upper' would work, but play tests show a margin of 20 plays + # better. + lower, upper = -MATE_UPPER, MATE_UPPER + while lower < upper - EVAL_ROUGHNESS: + gamma = (lower+upper+1)//2 + score = self.bound(pos, gamma, depth) + if score >= gamma: + lower = score + if score < gamma: + upper = score + # We want to make sure the move to play hasn't been kicked out of the table, + # So we make another call that must always fail high and thus produce a move. + self.bound(pos, lower, depth) + # If the game hasn't finished we can retrieve our move from the + # transposition table. + yield depth, self.tp_move.get(pos), self.tp_score.get((pos, depth, True)).lower + + +############################################################################### +# User interface +############################################################################### + +# Python 2 compatability +if sys.version_info[0] == 2: + input = raw_input + + +def parse(c): + fil, rank = ord(c[0]) - ord('a'), int(c[1]) - 1 + return A1 + fil - 10*rank + + +def render(i): + rank, fil = divmod(i - A1, 10) + return chr(fil + ord('a')) + str(-rank + 1) + + +def print_pos(pos): + print() + uni_pieces = {'R':'♜', 'N':'♞', 'B':'♝', 'Q':'♛', 'K':'♚', 'P':'♟', + 'r':'♖', 'n':'♘', 'b':'♗', 'q':'♕', 'k':'♔', 'p':'♙', '.':'·'} + for i, row in enumerate(pos.board.split()): + print(' ', 8-i, ' '.join(uni_pieces.get(p, p) for p in row)) + print(' a b c d e f g h \n\n') + + +def main_init(): + hist = [Position(initial, 0, (True,True), (True,True), 0, 0)] + searcher = Searcher() + while True: + print_pos(hist[-1]) + + if hist[-1].score <= -MATE_LOWER: + print("You lost!") + break + + # We query the user until she enters a (pseudo) legal move. + move = None + while move not in hist[-1].gen_moves(): + match = re.match('([a-h][1-8])'*2, input('Your move: ')) + if match: + move = parse(match.group(1)), parse(match.group(2)) + else: + # Inform the user when invalid input (e.g. "help") is entered + print("Please enter a move like g8f6") + hist.append(hist[-1].move(move)) + + # After our move we rotate the board and print it again. + # This allows us to see the effect of our move. + print_pos(hist[-1].rotate()) + + if hist[-1].score <= -MATE_LOWER: + print("You won") + break + + # Fire up the engine to look for a move. + start = time.time() + for _depth, move, score in searcher.search(hist[-1], hist): + if time.time() - start > 1: + break + + if score == MATE_UPPER: + print("Checkmate!") + + # The black player moves from a rotated position, so we have to + # 'back rotate' the move before printing it. + print("My move:", render(119-move[0]) + render(119-move[1])) + hist.append(hist[-1].move(move)) + +############################################################################### +# convert list to string +############################################################################### +def listToString(s): + + # initialize an empty string + str1 = "" + + # traverse in the string + for ele in s: + str1 += ele + + # return string + return str1 + +############################################################################### +# main +############################################################################### +def main(argv): + + #read history + hist = readMatch() + searcher = Searcher() + + if hist[-1].score <= -MATE_LOWER: + print("You lost") + + match = re.match('([a-h][1-8])'*2, argv) + move = parse(match.group(1)), parse(match.group(2)) + hist.append(hist[-1].move(move)) + + # After our move we rotate the board and print it again. + # This allows us to see the effect of our move. + print_pos(hist[-1].rotate()) + + if hist[-1].score <= -MATE_LOWER: + print("You won") + + # Fire up the engine to look for a move. + start = time.time() + for _depth, move, score in searcher.search(hist[-1], hist): + if time.time() - start > 1: + break + + if score == MATE_UPPER: + print("Checkmate!") + + # The black player moves from a rotated position, so we have to + # 'back rotate' the move before printing it. + print("My move:", render(119-move[0]) + render(119-move[1])) + hist.append(hist[-1].move(move)) + lastHist = hist[len(hist)-1] + #Save history + saveMatch(lastHist) + +############################################################################### +# saveMatch +############################################################################### +def saveMatch(lastHist): + + print("Saving match") + ## save board + f = open("match.txt", "w") + f.write(lastHist.board) + f.close() + + ## save properties of the match + config = configparser.ConfigParser() + config.add_section('match') + config['match']['score'] = str(lastHist.score) + config['match']['wc'] = str(lastHist.wc) + config['match']['bc'] = str(lastHist.bc) + config['match']['ep'] = str(lastHist.ep) + config['match']['kp'] = str(lastHist.kp) + + with open('match.ini', 'w') as configfile: + config.write(configfile) + +############################################################################### +# readMatch +############################################################################### +def readMatch(): + + ## read board + if os.path.isfile('match.txt'): + f = open("match.txt", "r") + board = f.read() + f.close() + + ## read properties of the file + config = configparser.ConfigParser() + config.read('match.ini') + + score = int(config['match']['score']) + wc = config['match']['wc'] + bc = config['match']['bc'] + ep = int(config['match']['ep']) + kp = int(config['match']['kp']) + #print(board) + hist = [Position(board, score, (True,True), (True,True), ep, kp)] + else: + hist = [Position(initial, 0, (True,True), (True,True), 0, 0)] + return hist + +if __name__ == '__main__': + main(sys.argv[1]) + diff --git a/test.py b/src/test.py similarity index 99% rename from test.py rename to src/test.py index d05426b..eec44d7 100644 --- a/test.py +++ b/src/test.py @@ -139,6 +139,7 @@ def test_3fold(self): if move == last_move: print('Score was', score, pos.score) print('Failed at', i) + print('Hey') print(_pgn) self.assertNotEqual(move, last_move) diff --git a/tests/3fold.pgn b/src/tests/3fold.pgn similarity index 100% rename from tests/3fold.pgn rename to src/tests/3fold.pgn diff --git a/tests/3fold_do.pgn b/src/tests/3fold_do.pgn similarity index 100% rename from tests/3fold_do.pgn rename to src/tests/3fold_do.pgn diff --git a/tests/3fold_dont.pgn b/src/tests/3fold_dont.pgn similarity index 100% rename from tests/3fold_dont.pgn rename to src/tests/3fold_dont.pgn diff --git a/tests/avoid_mate.epd b/src/tests/avoid_mate.epd similarity index 100% rename from tests/avoid_mate.epd rename to src/tests/avoid_mate.epd diff --git a/tests/bratko_kopec_test.epd b/src/tests/bratko_kopec_test.epd similarity index 100% rename from tests/bratko_kopec_test.epd rename to src/tests/bratko_kopec_test.epd diff --git a/tests/ccr_one_hour_test.epd b/src/tests/ccr_one_hour_test.epd similarity index 100% rename from tests/ccr_one_hour_test.epd rename to src/tests/ccr_one_hour_test.epd diff --git a/tests/chessathome_openings.fen b/src/tests/chessathome_openings.fen similarity index 100% rename from tests/chessathome_openings.fen rename to src/tests/chessathome_openings.fen diff --git a/tests/clean_draws.py b/src/tests/clean_draws.py similarity index 100% rename from tests/clean_draws.py rename to src/tests/clean_draws.py diff --git a/tests/mate1.fen b/src/tests/mate1.fen similarity index 100% rename from tests/mate1.fen rename to src/tests/mate1.fen diff --git a/tests/mate2.fen b/src/tests/mate2.fen similarity index 100% rename from tests/mate2.fen rename to src/tests/mate2.fen diff --git a/tests/mate3.fen b/src/tests/mate3.fen similarity index 100% rename from tests/mate3.fen rename to src/tests/mate3.fen diff --git a/tests/mate4.epd b/src/tests/mate4.epd similarity index 100% rename from tests/mate4.epd rename to src/tests/mate4.epd diff --git a/tests/nullmove_mates.fen b/src/tests/nullmove_mates.fen similarity index 100% rename from tests/nullmove_mates.fen rename to src/tests/nullmove_mates.fen diff --git a/tests/pgns.pgn b/src/tests/pgns.pgn similarity index 100% rename from tests/pgns.pgn rename to src/tests/pgns.pgn diff --git a/tests/queen.fen b/src/tests/queen.fen similarity index 100% rename from tests/queen.fen rename to src/tests/queen.fen diff --git a/tests/stalemate2.fen b/src/tests/stalemate2.fen similarity index 100% rename from tests/stalemate2.fen rename to src/tests/stalemate2.fen diff --git a/tests/unstable1 b/src/tests/unstable1 similarity index 100% rename from tests/unstable1 rename to src/tests/unstable1 diff --git a/tests/unstable_positions b/src/tests/unstable_positions similarity index 100% rename from tests/unstable_positions rename to src/tests/unstable_positions diff --git a/tests/unstable_positions2 b/src/tests/unstable_positions2 similarity index 100% rename from tests/unstable_positions2 rename to src/tests/unstable_positions2 diff --git a/tools.py b/src/tools.py similarity index 100% rename from tools.py rename to src/tools.py diff --git a/uci.py b/src/uci.py similarity index 100% rename from uci.py rename to src/uci.py diff --git a/xboard.py b/src/xboard.py similarity index 100% rename from xboard.py rename to src/xboard.py