├── .gitignore ├── README.md ├── chessbot ├── code ├── TestImages │ ├── withBoard │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 15.png │ │ ├── 16.png │ │ ├── 17.png │ │ ├── 18.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ └── withoutBoard │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ └── 7.png ├── board_basics.py ├── chessboard_detection.py ├── game_state_classes.py └── main.py └── stockfish /.gitignore: -------------------------------------------------------------------------------- 1 | *.mov 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chessbot 2 | 3 | "*Why create another chessbot ?*" The explanation is simple : I did not find a free bot I liked online : all the bots I saw on internet are parsing the html of the different websites to find the positions. But it creates a big limitation : if there is a new website, or a new html organisation, nothing will work. On the other hand my bot just looks at the screen and work with it to find the chessboard and the pieces. It is much more robust ! 4 | 5 | This chess bot can play automatically as white or black on lichess.com, chess.com, chess24.com and theoretically any website using drag and drop to move pieces. It uses stockfish engine to process moves, mss to do fast screenshots, pyautogui to move the mouse, chess to store and test the moves, and opencv to detect the chessboard. It has been written only with python. 6 | 7 | About the bot level, it beats easily chess.com computer on level 8/10 (around 2000 ELO) when taking 1.2 second per move and crushes every human opponent on any time format longer than 1 minute. 8 | 9 | This bot has been developped on iOS, but all the librairies it is using are compatible on Linux and Windows too. 10 | 11 | 12 | ## Getting Started 13 | 14 | ### Prerequisites 15 | 16 | #### Stockfish: 17 | 18 | This bot uses stockfish to calculate the next best move. Here is the procedure to make it work : 19 | 20 | * Download stockfish for your OS (https://stockfishchess.org/download/), the macOS stockfish I used is already commited. 21 | * Add it to your path with : ```export PATH=$PATH:$(pwd)``` 22 | * Test that stockfish is working well by running the command ```stockfish``` in your terminal. It should output something like this: ```Stockfish 120218 64 by T. Romstad, M. Costalba, J. Kiiski, G. Linscott``` 23 | 24 | #### Python: 25 | 26 | This bot requires python 3 to run 27 | 28 | ### Using the bot: 29 | 30 | The bot runs very easily: 31 | * go in the folder that contains the source code 32 | * Run the command ```python3 main.py``` 33 | 34 | ### Limitations: 35 | 36 | This project is far from perfect yet, it has a few limitations : 37 | * Because of the computer vision algorithm used to detect the chessboard, the square colors should be with plain colors, without having wierd textures. 38 | * The GUI is still quite basic 39 | * One small deviation during a game (the board moved, the user touched the mouse...) and the bot will not work at all. 40 | * It is not possible to stop the chessbot without closing the window 41 | * This project has been tested only on a Mac 42 | 43 | Please feel free to help me improve it 44 | 45 | ## Author 46 | 47 | **Stanislas Heili** - *Initial work* - [myGit](https://github.com/Stanou01260/) 48 | -------------------------------------------------------------------------------- /chessbot: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$(pwd) 2 | python3 ./code/main.py 3 | -------------------------------------------------------------------------------- /code/TestImages/withBoard/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/1.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/10.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/11.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/12.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/13.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/14.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/15.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/16.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/17.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/18.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/2.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/3.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/4.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/5.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/6.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/7.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/8.png -------------------------------------------------------------------------------- /code/TestImages/withBoard/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withBoard/9.png -------------------------------------------------------------------------------- /code/TestImages/withoutBoard/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withoutBoard/1.png -------------------------------------------------------------------------------- /code/TestImages/withoutBoard/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withoutBoard/2.png -------------------------------------------------------------------------------- /code/TestImages/withoutBoard/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withoutBoard/3.png -------------------------------------------------------------------------------- /code/TestImages/withoutBoard/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withoutBoard/4.png -------------------------------------------------------------------------------- /code/TestImages/withoutBoard/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withoutBoard/5.png -------------------------------------------------------------------------------- /code/TestImages/withoutBoard/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withoutBoard/6.png -------------------------------------------------------------------------------- /code/TestImages/withoutBoard/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/code/TestImages/withoutBoard/7.png -------------------------------------------------------------------------------- /code/board_basics.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | #Basic operations on the board: 4 | 5 | def get_square_image(row,column,board_img): #this functions assumes that there are 8*8 squares in the image, and that it is grayscale 6 | height, width = board_img.shape 7 | minX = int(column * width / 8 ) 8 | maxX = int((column + 1) * width / 8 ) 9 | minY = int(row * width / 8 ) 10 | maxY = int((row + 1) * width / 8 ) 11 | square = board_img[minY:maxY, minX:maxX] 12 | square_without_borders = square[3:-3, 3:-3] 13 | return square_without_borders 14 | 15 | def convert_row_column_to_square_name(row,column, is_white_on_bottom): 16 | if is_white_on_bottom == True: 17 | number = repr(8 - row) 18 | letter = str(chr(97 + column)) 19 | return letter+number 20 | else: 21 | number = repr(row + 1) 22 | letter = str(chr(97 + (7 - column))) 23 | return letter+number 24 | 25 | def convert_square_name_to_row_column(square_name,is_white_on_bottom): #Could be optimized 26 | #print("Looking for " + repr(square_name)) 27 | for row in range(8): 28 | for column in range(8): 29 | this_square_name = convert_row_column_to_square_name(row,column,is_white_on_bottom) 30 | #print(this_square_name) 31 | if this_square_name == square_name: 32 | return row,column 33 | return 0,0 34 | 35 | def get_square_center_from_image_and_move(square_name, is_white_on_bottom , minX,minY,maxX,maxY): 36 | row,column = convert_square_name_to_row_column(square_name,is_white_on_bottom) 37 | 38 | centerX = int(minX + (column + 0.5) *(maxX-minX)/8) 39 | centerY = int(minY + (row + 0.5) *(maxY-minY)/8) 40 | return centerX,centerY 41 | 42 | #Basic operation with square images: 43 | def has_square_image_changed(old_square, new_square):#If there has been a change -> the image difference will be non null -> the average intensity will be > treshold 44 | diff = cv2.absdiff(old_square,new_square) 45 | #print(diff.mean()) 46 | if diff.mean() > 8: #8 works pretty nicely but would require optimization 47 | return True 48 | else: 49 | return False 50 | 51 | def is_square_empty(square): # A square is empty if its pixels have no variations 52 | return square.std() < 10 # 10 works pretty well -> the mouse pointer is not enought to disturb (but sometimes it actually does, especially with small chessboards and big pointer) 53 | 54 | def is_white_on_bottom(current_chessboard_image): 55 | #This functions compares the mean intensity from two squares that have the same background (opposite corners) but different pieces on it. 56 | #The one brighter one must be white 57 | m1 = get_square_image(0,0,current_chessboard_image).mean() #Rook on the top left 58 | m2 = get_square_image(7,7,current_chessboard_image).mean() #Rook on the bottom right 59 | if m1 < m2: #If the top is darker than the bottom 60 | return True 61 | else: 62 | return False 63 | 64 | 65 | #This function goes over every square, check if it moves, and detect if the square emptiness on the old vs new 66 | #If the square had a piece previously -> it is a potential starting point 67 | #If the square has a piece now -> it is a potential arrival 68 | def get_potential_moves(old_image,new_image,is_white_on_bottom): 69 | potential_starts = [] 70 | potential_arrivals = [] 71 | for row in range(8): 72 | #print("\nRow",row,"") 73 | for column in range(8): 74 | old_square = get_square_image(row,column,old_image) 75 | new_square = get_square_image(row,column,new_image) 76 | if has_square_image_changed(old_square, new_square): 77 | square_name = convert_row_column_to_square_name(row,column,is_white_on_bottom) 78 | square_was_empty = is_square_empty(old_square) 79 | square_is_empty = is_square_empty(new_square) 80 | if square_was_empty == False: 81 | potential_starts= np.append(potential_starts,square_name) 82 | if square_is_empty== False: 83 | potential_arrivals = np.append(potential_arrivals,square_name) 84 | return potential_starts, potential_arrivals 85 | -------------------------------------------------------------------------------- /code/chessboard_detection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 #OpenCV 3 | import pyautogui #Used to take screenshots and move the mouse 4 | import chess #This is used to deal with the advancement in the game 5 | import chess.uci #This is used to transform uci notations: for instance the uci "e2e4" corresponds to the san : "1. e4" 6 | import random #Use the generate random numbers for processing time 7 | import time #Used to time the executions 8 | import mss #Used to get superfast screenshots 9 | import os 10 | import glob 11 | import game_state_classes 12 | 13 | def find_chessboard(): 14 | #We do a first classical screenshot to see the screen size: 15 | screenshot_shape = np.array(pyautogui.screenshot()).shape 16 | monitor = {'top': 0, 'left': 0, 'width': screenshot_shape[1], 'height': screenshot_shape[0]} 17 | sct = mss.mss() 18 | img = np.array(np.array(sct.grab(monitor))) 19 | img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB) 20 | gray = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) 21 | is_found, current_chessboard_image,minX,minY,maxX,maxY,test_image = find_chessboard_from_image(img) 22 | position = game_state_classes.Board_position(minX,minY,maxX,maxY) 23 | return is_found, position 24 | 25 | def get_chessboard(game_state): 26 | position = game_state.board_position_on_screen 27 | monitor = {'top': 0, 'left': 0, 'width': position.maxX + 10, 'height': position.maxY + 10} 28 | img = np.array(np.array(game_state.sct.grab(monitor))) 29 | #Converting the image in grayscale: 30 | image = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) 31 | dim = (800, 800 ) # perform the actual resizing of the chessboard 32 | resizedChessBoard = cv2.resize(image[position.minY:position.maxY, position.minX:position.maxX], dim, interpolation = cv2.INTER_AREA) 33 | return resizedChessBoard 34 | 35 | def find_chessboard_from_image(img): 36 | #The algorithm here is much faster than the previous one: 37 | #1 Get the horizontal lines by convolving the image with [[-1,1]], get the indexes with the most start and end lines 38 | #2 Get the vertical lines by convolving the image with [[-1],[1]], get the indexes with the most start and end lines 39 | #3 Check if there is only one probable start and end for each dimension, and the board is squared 40 | #4 Resize the image to a std dimension to standardize the treatments 41 | 42 | #Converting the image in grayscale: 43 | image = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) 44 | found_board = False 45 | 46 | kernelH = np.array([[-1,1]]) 47 | kernelV = np.array([[-1],[1]]) 48 | 49 | #Récupération des lignes horizontales : 50 | lignesHorizontales = np.absolute(cv2.filter2D(image.astype('float'),-1,kernelV)) 51 | ret,thresh1 = cv2.threshold(lignesHorizontales,30,255,cv2.THRESH_BINARY) 52 | 53 | kernelSmall = np.ones((1,3), np.uint8) 54 | kernelBig = np.ones((1,50), np.uint8) 55 | 56 | #Remove holes: 57 | imgH1 = cv2.dilate(thresh1, kernelSmall, iterations=1) 58 | imgH2 = cv2.erode(imgH1, kernelSmall, iterations=1) 59 | 60 | #Remove small lines 61 | imgH3 = cv2.erode(imgH2, kernelBig, iterations=1) 62 | imgH4 = cv2.dilate(imgH3, kernelBig, iterations=1) 63 | 64 | linesStarts = cv2.filter2D(imgH4,-1,kernelH) 65 | linesEnds = cv2.filter2D(imgH4,-1,-kernelH) 66 | 67 | lines = linesStarts.sum(axis=0)/255 68 | lineStart = 0 69 | nbLineStart = 0 70 | for idx, val in enumerate(lines): 71 | if val > 6: 72 | nbLineStart += 1 73 | lineStart = idx 74 | 75 | lines = linesEnds.sum(axis=0)/255 76 | lineEnd = 0 77 | nbLineEnd = 0 78 | for idx, val in enumerate(lines): 79 | if val > 6: 80 | nbLineEnd += 1 81 | lineEnd = idx 82 | 83 | #Récupération des lignes verticales: 84 | lignesVerticales = np.absolute(cv2.filter2D(image.astype('float'),-1,kernelH)) 85 | ret,thresh1 = cv2.threshold(lignesVerticales,30,255,cv2.THRESH_BINARY) 86 | 87 | kernelSmall = np.ones((3,1), np.uint8) 88 | kernelBig = np.ones((50,1), np.uint8) 89 | 90 | #Remove holes: 91 | imgV1 = cv2.dilate(thresh1, kernelSmall, iterations=1) 92 | imgV2 = cv2.erode(imgV1, kernelSmall, iterations=1) 93 | 94 | #Remove small lines 95 | imgV3 = cv2.erode(imgV2, kernelBig, iterations=1) 96 | imgV4 = cv2.dilate(imgV3, kernelBig, iterations=1) 97 | 98 | columnStarts = cv2.filter2D(imgV4,-1,kernelV) 99 | columnEnds = cv2.filter2D(imgV4,-1,-kernelV) 100 | 101 | column = columnStarts.sum(axis=1)/255 102 | columnStart = 0 103 | nbColumnStart = 0 104 | for idx, val in enumerate(column): 105 | if val > 6: 106 | columnStart = idx 107 | nbColumnStart += 1 108 | 109 | column = columnEnds.sum(axis=1)/255 110 | columnEnd = 0 111 | nbColumnEnd = 0 112 | for idx, val in enumerate(column): 113 | if val > 6: 114 | columnEnd = idx 115 | nbColumnEnd += 1 116 | 117 | 118 | found_board = False 119 | if (nbLineStart == 1) and (nbLineEnd == 1) and (nbColumnStart == 1) and (nbColumnEnd == 1) : 120 | print("We found a board") 121 | if abs((columnEnd - columnStart) - (lineEnd - lineStart)) > 3: 122 | print ("However, the board is not a square") 123 | else: 124 | print(columnStart,columnEnd,lineStart,lineEnd) 125 | if (columnEnd - columnStart) % 8 == 1: 126 | columnEnd -= 1 127 | if (columnEnd - columnStart) % 8 == 7: 128 | columnEnd += 1 129 | if (lineEnd - lineStart) % 8 == 1: 130 | lineStart += 1 131 | if (lineEnd - lineStart) % 8 == 7: 132 | lineStart -= 1 133 | print(columnStart,columnEnd,lineStart,lineEnd) 134 | 135 | found_board = True 136 | else: 137 | print("We did not found the borders of the board") 138 | 139 | if found_board: 140 | print("Found chessboard sized:" , (columnEnd-columnStart),(lineEnd-lineStart)," x:",columnStart,columnEnd," y: ",lineStart,lineEnd) 141 | dim = (800, 800 ) # perform the actual resizing of the chessboard 142 | print(lineStart,lineEnd,columnStart,columnEnd) 143 | resizedChessBoard = cv2.resize(image[columnStart:columnEnd, lineStart:lineEnd], dim, interpolation = cv2.INTER_AREA) 144 | return True, resizedChessBoard , lineStart, columnStart , lineEnd , columnEnd , resizedChessBoard 145 | 146 | return False, image, 0, 0, 0, 0 , image 147 | 148 | def test_chessboard_detection(imageDir,expectedBoard): 149 | valid_image_extensions = [".jpg", ".jpeg", ".png", ".tif", ".tiff"] 150 | image_count = 0 151 | error_count = 0 152 | for file in os.listdir(imageDir): 153 | print("\n\nTesting new file ", file) 154 | extension = os.path.splitext(file)[1] 155 | if extension.lower() not in valid_image_extensions: 156 | continue 157 | image_count = image_count+1 158 | image = cv2.imread(os.path.join(imageDir, file)) 159 | found_board, resizedChessBoard , minX,minY,maxX,maxY, info_image = find_chessboard_from_image(image) 160 | if found_board != expectedBoard: 161 | print("Error in", file) 162 | error_count = error_count + 1 163 | cv2.imwrite("Errors/Error" + repr(expectedBoard) + file,info_image) 164 | else: 165 | cv2.imwrite("Errors/NoError" + repr(expectedBoard) + file,info_image) 166 | 167 | #print("Had ", error_count, " error_count out of ", image_count, " images") 168 | return image_count,error_count 169 | 170 | def cleanFolder(folder_name): 171 | files = glob.glob(folder_name) 172 | for f in files: 173 | os.remove(f) 174 | 175 | 176 | def global_test_chessboard_detection(): 177 | cleanFolder('Errors/*') 178 | 179 | #Images having a board 180 | print("\nTreating images having a board : ") 181 | image_count,error_count = test_chessboard_detection("TestImages/withBoard/",True) 182 | print("Errors with images having a board : ", 100 * error_count/image_count, "%") 183 | 184 | #Images having no board 185 | print("\nTreating images having no board : ") 186 | image_count,error_count = test_chessboard_detection("TestImages/withoutBoard/",False) 187 | print("Errors with images not having a board : ", 100 * error_count/image_count, "%") 188 | 189 | print("\n Please find outputs in the folder Errors/") 190 | 191 | if __name__ == "__main__": 192 | global_test_chessboard_detection() -------------------------------------------------------------------------------- /code/game_state_classes.py: -------------------------------------------------------------------------------- 1 | import chess #This is used to deal with the advancement in the game 2 | import chess.uci #This is used to transform uci notations: for instance the uci "e2e4" corresponds to the san : "1. e4" 3 | import numpy as np 4 | from board_basics import * 5 | import chessboard_detection 6 | import pyautogui 7 | import cv2 #OpenCV 8 | import mss #Used to get superfast screenshots 9 | import time #Used to time the executions 10 | 11 | 12 | class Board_position: 13 | def __init__(self,minX,minY,maxX,maxY): 14 | self.minX = minX 15 | self.minY = minY 16 | self.maxX = maxX 17 | self.maxY = maxY 18 | 19 | def print_custom(self): 20 | return ("from " + str(self.minX) + "," + str(self.minY) + " to " + str(self.maxX) + ","+ str(self.maxY)) 21 | 22 | class Game_state: 23 | 24 | def __init__(self): 25 | self.we_play_white = True #This store the player color, it will be changed later 26 | self.moves_to_detect_before_use_engine = -1 #The program uses the engine to play move every time that this variable is 0 27 | self.expected_move_to_detect = "" #This variable stores the move we should see next, if we don't see the right one in the next iteration, we wait and try again. This solves the slow transition problem: for instance, starting with e2e4, the screenshot can happen when the pawn is on e3, that is a possible position. We always have to double check that the move is done. 28 | self.previous_chessboard_image = [] #Storing the chessboard image from previous iteration 29 | self.executed_moves = [] #Store the move detected on san format 30 | self.engine = chess.uci.popen_engine("stockfish")#The engine used is stockfish. It requires to have the command stockfish working on the shell 31 | self.board = chess.Board() #This object comes from the "chess" package, the moves are stored inside it (and it has other cool features such as showing all the "legal moves") 32 | self.board_position_on_screen = [] 33 | self.sct = mss.mss() 34 | 35 | #This function checks if the chessboard image we see fits the moves we stored 36 | #The only check done right now is squares have the right emptiness. 37 | def can_image_correspond_to_chessboard(self, move, current_chessboard_image): 38 | self.board.push(move) 39 | squares = chess.SquareSet(chess.BB_ALL) 40 | for square in squares: 41 | row = chess.square_rank(square) 42 | column = chess.square_file(square) 43 | piece = self.board.piece_at(square) 44 | shouldBeEmpty = (piece == None) 45 | 46 | if self.we_play_white == True: 47 | #print("White on bottom",row,column,piece) 48 | rowOnImage = 7-row 49 | columnOnImage = column 50 | else: 51 | #print("White on top",row,7 - column,piece) 52 | rowOnImage = row 53 | columnOnImage = 7-column 54 | 55 | squareImage = get_square_image(rowOnImage,columnOnImage,current_chessboard_image) 56 | 57 | if is_square_empty(squareImage) != shouldBeEmpty: 58 | self.board.pop() 59 | print( "Problem with : ", self.board.uci(move) ," the square ", rowOnImage, columnOnImage, "should ",'be empty' if shouldBeEmpty else 'contain a piece') 60 | return False 61 | print("Accepted move", self.board.uci(move)) 62 | self.board.pop() 63 | return True 64 | 65 | 66 | def get_valid_move(self, potential_starts, potential_arrivals, current_chessboard_image): 67 | print("Starts and arrivals:",potential_starts, potential_arrivals) 68 | 69 | valid_move_string = "" 70 | for start in potential_starts: 71 | for arrival in potential_arrivals: 72 | uci_move = start+arrival 73 | move = chess.Move.from_uci(uci_move) 74 | if move in self.board.legal_moves: 75 | if self.can_image_correspond_to_chessboard(move,current_chessboard_image):#We only keep the move if the current image looks like this move happenned 76 | valid_move_string = uci_move 77 | else: 78 | uci_move_promoted = uci_move + 'q' 79 | promoted_move = chess.Move.from_uci(uci_move_promoted) 80 | if promoted_move in self.board.legal_moves: 81 | if self.can_image_correspond_to_chessboard(move,current_chessboard_image):#We only keep the move if the current image looks like this move happenned 82 | valid_move_string = uci_move_promoted 83 | print("There has been a promotion to queen") 84 | 85 | #Detect castling king side with white 86 | if ("e1" in potential_starts) and ("h1" in potential_starts) and ("f1" in potential_arrivals) and ("g1" in potential_arrivals): 87 | valid_move_string = "e1g1" 88 | 89 | #Detect castling queen side with white 90 | if ("e1" in potential_starts) and ("a1" in potential_starts) and ("c1" in potential_arrivals) and ("d1" in potential_arrivals): 91 | valid_move_string = "e1c1" 92 | 93 | #Detect castling king side with black 94 | if ("e8" in potential_starts) and ("h8" in potential_starts) and ("f8" in potential_arrivals) and ("g8" in potential_arrivals): 95 | valid_move_string = "e8g8" 96 | 97 | #Detect castling queen side with black 98 | if ("e8" in potential_starts) and ("a8" in potential_starts) and ("c8" in potential_arrivals) and ("d8" in potential_arrivals): 99 | valid_move_string = "e8c8" 100 | 101 | return valid_move_string 102 | 103 | def register_move_if_needed(self): 104 | #cv2.imshow('old_image',self.previous_chessboard_image) 105 | #k = cv2.waitKey(10000) 106 | new_board = chessboard_detection.get_chessboard(self) 107 | potential_starts, potential_arrivals = get_potential_moves(self.previous_chessboard_image,new_board,self.we_play_white) 108 | valid_move_string1 = self.get_valid_move(potential_starts,potential_arrivals,new_board) 109 | print("Valid move string 1:" + valid_move_string1) 110 | 111 | if len(valid_move_string1) > 0: 112 | time.sleep(0.1) 113 | 'Check that we were not in the middle of a move animation' 114 | new_board = chessboard_detection.get_chessboard(self) 115 | potential_starts, potential_arrivals = get_potential_moves(self.previous_chessboard_image,new_board,self.we_play_white) 116 | valid_move_string2 = self.get_valid_move(potential_starts,potential_arrivals,new_board) 117 | print("Valid move string 2:" + valid_move_string2) 118 | if valid_move_string2 != valid_move_string1: 119 | return False, "The move has changed" 120 | valid_move_UCI = chess.Move.from_uci(valid_move_string1) 121 | valid_move_registered = self.register_move(valid_move_UCI,new_board) 122 | return True, valid_move_string1 123 | return False, "No move found" 124 | 125 | 126 | 127 | def register_move(self,move,board_image): 128 | if move in self.board.legal_moves: 129 | print("Move has been registered") 130 | self.executed_moves= np.append(self.executed_moves,self.board.san(move)) 131 | self.board.push(move) 132 | self.moves_to_detect_before_use_engine = self.moves_to_detect_before_use_engine - 1 133 | self.previous_chessboard_image = board_image 134 | return True 135 | else: 136 | return False 137 | 138 | def get_square_center(self,square_name): 139 | row,column = convert_square_name_to_row_column(square_name,self.we_play_white) 140 | position = self.board_position_on_screen 141 | centerX = int(position.minX + (column + 0.5) *(position.maxX-position.minX)/8) 142 | centerY = int(position.minY + (row + 0.5) *(position.maxY-position.minY)/8) 143 | return centerX,centerY 144 | 145 | def play_next_move(self): 146 | #This function calculates the next best move with the engine, and play it (by moving the mouse) 147 | print("\nUs to play: Calculating next move") 148 | self.engine.position(self.board) 149 | engine_process = self.engine.go(movetime=200)#random.randint(200,400)) 150 | 151 | best_move = engine_process.bestmove 152 | best_move_string = best_move.uci() 153 | #print("Play next move") 154 | 155 | #print(bestMove) 156 | origin_square = best_move_string[0:2] 157 | destination_square = best_move_string[2:4] 158 | 159 | # I added custom oppenings because I was very annoyed to always see e2e4, feel free to comment or change this 160 | #Potentially, we could even start the bot after N moves allowing the user to see the games he want 161 | #Custom white openning: 162 | #if len(moveHistory) == 0: 163 | # origin_square = "b1" 164 | # destination_square = "c3" 165 | #if len(moveHistory) == 2 : 166 | # origin_square = "c3" 167 | # destination_square = "b1" 168 | #Custom black openning: 169 | #if len(moveHistory) == 1 : 170 | # origin_square = "g8" 171 | # destination_square = "f6" 172 | #if len(moveHistory) == 3 : 173 | # origin_square = "f6" 174 | # destination_square = "g8" 175 | 176 | #From the move we get the positions: 177 | centerXOrigin, centerYOrigin = self.get_square_center(origin_square) 178 | centerXDest, centerYDest = self.get_square_center(destination_square) 179 | 180 | #Having the positions we can drag the piece: 181 | pyautogui.moveTo(centerXOrigin, centerYOrigin, 0.01) 182 | pyautogui.dragTo(centerXOrigin, centerYOrigin + 1, button='left', duration=0.01) #This small click is used to get the focus back on the browser window 183 | pyautogui.dragTo(centerXDest, centerYDest, button='left', duration=0.3) 184 | 185 | if best_move.promotion != None: 186 | print("Promoting to a queen") 187 | #Deal with queen promotion: 188 | cv2.waitKey(100) 189 | pyautogui.dragTo(centerXDest, centerYDest + 1, button='left', duration=0.1) #Always promoting to a queen 190 | 191 | print("Done playing move",origin_square,destination_square) 192 | self.moves_to_detect_before_use_engine = 2 193 | return 194 | 195 | -------------------------------------------------------------------------------- /code/main.py: -------------------------------------------------------------------------------- 1 | #Pour l'export : pyinstaller -w -F -i .\Ressources\DG.ico .\interface_graphique.py 2 | import tkinter as tk 3 | import chessboard_detection 4 | import cv2 #OpenCV 5 | import board_basics 6 | from game_state_classes import * 7 | from tkinter.simpledialog import askstring 8 | 9 | def clear_logs(): 10 | logs_text.delete('1.0', tk.END) 11 | #add_log("Logs have been cleared:") 12 | 13 | def add_log(log): 14 | logs_text.insert(tk.END,log + "\n") 15 | 16 | def stop_playing(): 17 | clear_logs() 18 | button_start = tk.Button(text="Start playing - RESTART NOT WORKING YET", command =start_playing) 19 | button_start.grid(column=0,row =1) 20 | 21 | def start_playing(): 22 | game_state = Game_state() 23 | add_log("Looking for a chessboard...") 24 | 25 | found_chessboard, position = chessboard_detection.find_chessboard() 26 | 27 | if found_chessboard: 28 | add_log("Found the chessboard " + position.print_custom()) 29 | game_state.board_position_on_screen = position 30 | else: 31 | add_log("Could not find the chessboard") 32 | add_log("Please try again when the board is open on the screen\n") 33 | return 34 | 35 | 36 | button_start = tk.Button(text="Stop playing", command =stop_playing) 37 | button_start.grid(column=0,row =1) 38 | 39 | 40 | 41 | add_log("Checking if we are black or white...") 42 | resized_chessboard = chessboard_detection.get_chessboard(game_state) 43 | #cv2.imshow('Resized image',resized_chessboard) 44 | game_state.previous_chessboard_image = resized_chessboard 45 | 46 | we_are_white = board_basics.is_white_on_bottom(resized_chessboard) 47 | game_state.we_play_white = we_are_white 48 | if we_are_white: 49 | add_log("We are white" ) 50 | game_state.moves_to_detect_before_use_engine = 0 51 | else: 52 | add_log("We are black") 53 | game_state.moves_to_detect_before_use_engine = 1 54 | first_move_registered = False 55 | while first_move_registered == False: 56 | first_move_string = askstring('First move', 'What was the first move played by white?') 57 | if len(first_move_string) > 0: 58 | first_move = chess.Move.from_uci(first_move_string) 59 | first_move_registered = game_state.register_move(first_move,resized_chessboard) 60 | 61 | add_log("First move played by white :"+ first_move_string) 62 | 63 | while True: 64 | window.update() 65 | #cv2.imshow('Resized image',game_state.previous_chessboard_image) 66 | #add_log("Moves to detect before use engine" + str(game_state.moves_to_detect_before_use_engine)) 67 | if game_state.moves_to_detect_before_use_engine == 0: 68 | #add_log("Our turn to play:") 69 | game_state.play_next_move() 70 | #add_log("We are done playing") 71 | 72 | found_move, move = game_state.register_move_if_needed() 73 | if found_move: 74 | clear_logs() 75 | add_log("The board :\n" + str(game_state.board) + "\n") 76 | add_log("\nAll moves :\n" + str(game_state.executed_moves)) 77 | 78 | 79 | window = tk.Tk() 80 | #window.geometry("300x300") 81 | window.title("ChessBot by Stanislas Heili") 82 | 83 | label_titre = tk.Label(text="Welcome on my chessbot, hope you will have fun with it",anchor="e", wraplength = 300)#\nThis bot can not work on a game that already started") 84 | label_titre.grid(column = 0,row = 0) 85 | 86 | 87 | button_start = tk.Button(text="Start playing", command =start_playing) 88 | button_start.grid(column=0,row =1) 89 | 90 | logs_text = tk.Text(window,width=40,height=25,background='gray') 91 | logs_text.grid(column = 0,row = 2) 92 | 93 | 94 | window.mainloop() 95 | -------------------------------------------------------------------------------- /stockfish: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanou01260/chessbot_python/d606219751fc0a3386b3051ff73141a4f4017da6/stockfish --------------------------------------------------------------------------------