diff --git a/.gitignore b/.gitignore index d5bd4fb..11a3155 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ venv/ .idea/ +.DS_Store __pycache__/ diff --git a/README.md b/README.md index 9219c1e..525278e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ +## PyAlapin, your customized chesse engine +Is it the best, most efficient and state of the art chess engine ? I'm pretty sure not. +However, driven by passion and madness, I have developed my own chess game in Python. +For your pretty eyes and your devilish smile, I share it with you. But only with you. + +Special thanks and dedication to LeMerluche, crushing its opponents on chess.com with alapin openings ❤️ + +## How to play with interface ```python from interface.interface import MyApp @@ -9,7 +17,7 @@ if __name__ == '__main__': ![](interface_example.PNG) -Python methods to play a game: +## How to play with Python commands ```python import sys diff --git a/python/engine/engine.py b/python/engine/engine.py index 2862a5c..eaeea17 100644 --- a/python/engine/engine.py +++ b/python/engine/engine.py @@ -21,7 +21,31 @@ class Color: class Cell: + """ + Cell class representing a base element of a board. + + Attributes + ---------- + x : int + x coordinate of the cell on the board. + y : int + y coordinate of the cell on the board + piece: material.Piece or None + Piece that is on the cell (or None if no Piece is on the cell) + """ + def __init__(self, x, y, piece): + """Initialization of the cell. + + Parameters + ---------- + x : int + x coordinate of the cell on the board. + y : int + y coordinate of the cell on the board + piece: material.Piece or None + Piece that is on the cell (or None if no Piece is on the cell) + """ self.x = x self.y = y self.piece = piece @@ -30,27 +54,80 @@ def __init__(self, x, y, piece): self.piece.y = y def __deepcopy__(self, memodict={}): + """Method to create an uncorrelated clone of the cell. + + Returns + ------- + Cell + Exact copy of self. + """ copy_object = Cell(self.x, self.y, copy.deepcopy(self.piece)) return copy_object def set_piece(self, piece): + """Sets a Piece in the Cell. + + Parameters + ---------- + piece: material.Piece + Piece to set up on self. + """ self.piece = piece if piece is not None: self.piece.x = self.x self.piece.y = self.y def get_piece(self): + """Method to access the piece on the Cell. + + Returns + ------- + Piece + Piece on the self. + """ return self.piece def get_x(self): + """Method to acces Cell x coordinate. + + Returns + ------- + int + x-axis coordinate of self. + """ return self.x def get_y(self): + """Method to acces Cell y coordinate. + + Returns + ------- + int + y-axis coordinate of self. + """ return self.y def is_threatened( self, board, threaten_color ): # change threaten_color par #white_threatened + """ + Method to check if the Cell is threatened by a given color. + + Parameters + ---------- + board : Board + Board to which self belongs to. + threaten_color : str + Color of that wants to know if cell is threatened by opponent. + + Returns + ------- + bool + Whether the celle is threatened or not. + """ + # One way that could be more efficient would be to keep at every step the list of threatened cell by each piece + # And update it at each move. + # Check Knights threatening for i, j in [ (2, 1), @@ -65,7 +142,7 @@ def is_threatened( x_to_check = self.x + i y_to_check = self.y + j - if 0 < x_to_check < 8 and 0 < y_to_check < 8: + if 0 <= x_to_check < 8 and 0 <= y_to_check < 8: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -74,11 +151,12 @@ def is_threatened( return True # King + Rook + Queen + # Checking direct surroundings for i, j in [(1, 0), (0, -1), (-1, 0), (0, -1)]: x_to_check = self.x + i y_to_check = self.y + j - if 0 < x_to_check < 8 and 0 < y_to_check < 8: + if 0 <= x_to_check < 8 and 0 <= y_to_check < 8: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -90,9 +168,30 @@ def is_threatened( if piece_to_check.is_white() != threaten_color: return True + elif piece_to_check is None: + keep_going = True + x_to_check += i + y_to_check += j + while 0 <= x_to_check < 8 and 0 <= y_to_check < 8 and keep_going: + cell_to_check = board.get_cell(x_to_check, y_to_check) + piece_to_check = cell_to_check.get_piece() + if isinstance(piece_to_check, material.Rook) or isinstance( + piece_to_check, material.Queen + ): + keep_going = False + if piece_to_check.is_white() != threaten_color: + return True + elif piece_to_check is not None: + keep_going = False + else: + x_to_check += i + y_to_check += j + + """ # Rook + Queen + # Going further keep_going = True - x_to_check = self.x + 1 + x_to_check = self.x + 2 y_to_check = self.y while x_to_check < 8 and keep_going: cell_to_check = board.get_cell(x_to_check, y_to_check) @@ -109,7 +208,7 @@ def is_threatened( x_to_check += 1 keep_going = True - x_to_check = self.x - 1 + x_to_check = self.x - 2 y_to_check = self.y while x_to_check >= 0 and keep_going: cell_to_check = board.get_cell(x_to_check, y_to_check) @@ -127,7 +226,7 @@ def is_threatened( keep_going = True x_to_check = self.x - y_to_check = self.y + 1 + y_to_check = self.y + 2 while y_to_check < 8 and keep_going: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -144,7 +243,7 @@ def is_threatened( keep_going = True x_to_check = self.x - y_to_check = self.y - 1 + y_to_check = self.y - 2 while y_to_check >= 0 and keep_going: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -158,13 +257,15 @@ def is_threatened( elif piece_to_check is not None: keep_going = False y_to_check -= 1 + """ # King + Queen + Bishop + Pawn + # Checking direct surroundings for i, j in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: x_to_check = self.x + i y_to_check = self.y + j - if 0 < x_to_check < 8 and 0 < y_to_check < 8: + if 0 <= x_to_check < 8 and 0 <= y_to_check < 8: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -190,10 +291,31 @@ def is_threatened( if piece_to_check.is_white() != threaten_color: return True + elif piece_to_check is None: + print("def") + keep_going = True + x_to_check += i + y_to_check += j + while 0 <= x_to_check < 8 and 0 <= y_to_check < 8 and keep_going: + cell_to_check = board.get_cell(x_to_check, y_to_check) + piece_to_check = cell_to_check.get_piece() + + if isinstance(piece_to_check, material.Bishop) or isinstance( + piece_to_check, material.Queen + ): + keep_going = False + if piece_to_check.is_white() != threaten_color: + return True + elif piece_to_check is not None: + keep_going = False + x_to_check += i + y_to_check += j + + """ # Queen + Bishop keep_going = True - x_to_check = self.x + 1 - y_to_check = self.y + 1 + x_to_check = self.x + 2 + y_to_check = self.y + 2 while x_to_check < 8 and y_to_check < 8 and keep_going: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -210,8 +332,8 @@ def is_threatened( y_to_check += 1 keep_going = True - x_to_check = self.x - 1 - y_to_check = self.y + 1 + x_to_check = self.x - 2 + y_to_check = self.y + 2 while x_to_check >= 0 and y_to_check < 8 and keep_going: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -228,8 +350,8 @@ def is_threatened( y_to_check += 1 keep_going = True - x_to_check = self.x + 1 - y_to_check = self.y - 1 + x_to_check = self.x + 2 + y_to_check = self.y - 2 while x_to_check < 8 and y_to_check >= 0 and keep_going: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -246,8 +368,8 @@ def is_threatened( y_to_check -= 1 keep_going = True - x_to_check = self.x - 1 - y_to_check = self.y - 1 + x_to_check = self.x - 2 + y_to_check = self.y - 2 while x_to_check >= 0 and y_to_check >= 0 and keep_going: cell_to_check = board.get_cell(x_to_check, y_to_check) piece_to_check = cell_to_check.get_piece() @@ -262,17 +384,47 @@ def is_threatened( keep_going = False x_to_check -= 1 y_to_check -= 1 + """ return False class Board: + """ + Board class representing the chess board. + + Attributes + ---------- + board : list of Cells + Represents all cells of a chess board from x coordinates [0, 7] and y coordinates [0, 7] + white_king : material.King + King piece of white color. + black_king : material.King + King piece of black color. + all_material: dict + Dictionnary containing all the pieces on the board, killed and not killed. + """ + def __init__(self, empty_init=False): + """Initialization of the board. + + Parameters + ---------- + empty_init: bool + True if you want to start from an existing board. + """ if not empty_init: self.board = None self.white_king, self.black_king, self.all_material = self._reset_board() def deepcopy(self, memodict={}): + """Method to create an uncorrelated clone of the board. + + Returns + ------- + Cell + Exact copy of self. + """ copied_object = Board(empty_init=True) board = [[Cell(i, j, None) for j in range(8)] for i in range(8)] copied_object.board = board @@ -292,6 +444,13 @@ def deepcopy(self, memodict={}): return copied_object def deep_copy_material(self): + """Method to create an uncorrelated clone of all the pieces on the board, killed and not killed. + + Returns + ------- + dict of Pieces + Exact copy of self.all_material. + """ material = { "white": { "alive": { @@ -341,6 +500,13 @@ def deep_copy_material(self): return material def __deepcopy__(self, memodict={}): + """Method to create an uncorrelated clone of the board. + + Returns + ------- + Cell + Exact copy of self. + """ copied_object = Board(empty_init=True) board = [[Cell(i, j, None) for j in range(8)] for i in range(8)] copied_object.board = board @@ -361,6 +527,13 @@ def __deepcopy__(self, memodict={}): return copied_object def to_fen(self): + """Method to generate a fen representation of the current state of the board + + Returns + ------- + tuple of str + fen representation and 'KQkq' + """ fen = "" for line in self.board: no_piece_count = 0 @@ -382,6 +555,22 @@ def to_fen(self): return fen[:-1], "KQkq" def one_hot_encode(self, white_side=True): + """Method to create a representation of the board with OneHot encode of the pieces. + + Parameters + ---------- + white_sied : bool + Whether we want to represent the board from the White side point of view or not. Point of view sees its pieces + represented by +1 OneHot and opponent side by -1. + + Returns + ------- + list of list + 8x8 list representing the board with full zeros list when cell is empty or OneHot representation of the piece on + the cell otherwise. + """ + + # Dict of OneHot transformations material_to_one_hot = { "pawn": [1, 0, 0, 0, 0, 0], "bishop": [0, 1, 0, 0, 0, 0], @@ -390,15 +579,19 @@ def one_hot_encode(self, white_side=True): "queen": [0, 0, 0, 0, 1, 0], "king": [0, 0, 0, 0, 0, 1], } + # Iterating over cells and add OneHot representations to the list. one_hot_board = [] for line in self.board: one_hot_line = [] for cell in line: piece = cell.get_piece() + # Empty cell if piece is None: one_hot_line.append([0] * 6) + # Piece on the cell else: one_hot_piece = material_to_one_hot[piece.type] + # Negative OneHot if opponent side if piece.is_white() != white_side: one_hot_piece = [-1 * val for val in one_hot_piece] one_hot_line.append(one_hot_piece) @@ -406,20 +599,50 @@ def one_hot_encode(self, white_side=True): return one_hot_board def get_cell(self, x, y): + """Method to access a cell on the board from its coordinates. + + Parameters + ---------- + x : int + x-coordinate of the cell. + y : int + y-coordinate of the cell. + + Returns + ------- + cell + Cell at coordinates (x, y) + """ return self.board[x][y] - def copy(self): - return self.deepcopy() - def reset(self): + """ + Resets the board, all the pieces, everything. + """ self.white_king, self.black_king, self.all_material = self._reset_board() def create_board_from_string(self, string): - return None + """ + Method to set up a sepecific board with Pieces on specific Celss from a string. + """ + raise NotImplementedError def _reset_board(self): + """Method to create the board. Creates Pieces and place them on their original cells. + + Returns + ------- + material.King + White King on the board + material.King + black King on the board + dict + Dictionnary with all the board pieces + """ + # List of cells board = [] + # Dictionnary to access easily the pieces pieces = { "white": { "alive": { @@ -459,6 +682,7 @@ def _reset_board(self): }, } + # Initialize white pieces white_king = material.King(True, 0, 4) pieces["white"]["alive"]["king"].append(white_king) black_king = material.King(False, 7, 4) @@ -514,6 +738,7 @@ def _reset_board(self): line.append(Cell(6, i, p)) board.append(line) + # Initialize black pieces b_rook_1 = material.Rook(False, 7, 0) b_rook_2 = material.Rook(False, 7, 7) pieces["black"]["alive"]["rook"].append(b_rook_1) @@ -548,31 +773,49 @@ def _reset_board(self): return white_king, black_king, pieces def move_piece_from_coordinates(self, start_coordinates, end_coordinates): + """Method to move a piece on the board from start and landing coordinates. + + Parameters + ---------- + start_coordinates : tuple of int + (x, y) coordinates of move starting cell + end_coordinates : tuple of int + (x, y) coordinates of move landing cell + """ start_cell = self.get_cell(start_coordinates[0], start_coordinates[1]) end_cell = self.get_cell(end_coordinates[0], end_coordinates[1]) piece_to_move = start_cell.get_piece() if piece_to_move is None: - ###print(start_coordinates, end_coordinates) - ###print(start_cell, start_cell.piece) raise ValueError("Empty cells chosen as moved piece") end_cell.set_piece(piece_to_move) start_cell.set_piece(None) def kill_piece_from_coordinates(self, coordinates): + """Method to kill a piece from its coordinates on the board. + + Parameters + ---------- + coordinates : tuple of ints + (x, y) coordinates of cell on which is the piece to kill + """ to_kill_piece = self.get_cell(coordinates[0], coordinates[1]).get_piece() to_kill_piece.set_killed() color = "white" if to_kill_piece.is_white() else "black" - ###print(color) - ###print(self.all_material[color]['alive']) - ###print(self.all_material[color]["alive"][to_kill_piece.type]) - ###print(to_kill_piece) - ###print(to_kill_piece.type) self.all_material[color]["alive"][to_kill_piece.type].remove(to_kill_piece) self.all_material[color]["killed"][to_kill_piece.type].append(to_kill_piece) def transform_pawn(self, coordinates): + """Method to promote a pawn from its coordinates. + + Parameters + ---------- + coordinates : tuple of ints + (x, y) coordinates of the cell on which is Pawn to promote + promote_into : str + type of piece to promote the Pawn into. Default is "Queen" can also be "Rool", "Bishop" and "Knigh" + """ pawn = self.get_cell(coordinates[0], coordinates[1]).get_piece() if not isinstance(pawn, material.Pawn): raise ValueError("Transforming piece that is not a Pawn") @@ -585,6 +828,15 @@ def transform_pawn(self, coordinates): self.all_material[color]["alive"]["queen"].append(new_queen) def promote_pawn(self, coordinates, promote_into="Queen"): + """Method to promote a pawn from its coordinates. + + Parameters + ---------- + coordinates : tuple of ints + (x, y) coordinates of the cell on which is Pawn to promote + promote_into : str + type of piece to promote the Pawn into. Default is "Queen" can also be "Rool", "Bishop" and "Knigh" + """ pawn = self.get_cell(coordinates[0], coordinates[1]).get_piece() if not isinstance(pawn, material.Pawn): raise ValueError("Transforming piece that is not a Pawn") @@ -597,10 +849,15 @@ def promote_pawn(self, coordinates, promote_into="Queen"): self.all_material[color]["alive"]["queen"].append(new_piece) def draw(self, printing=True): + """Method to draw the board as a string and potentially print it in the terminal. + + Parameters + ---------- + printing : bool + Whether or not to print it in the terminal + """ whole_text = " | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |" - # ###print(' | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |') boarder_line = "+---+-----+-----+-----+-----+-----+-----+-----+-----+" - # ###print(boarder_line) whole_text += "\n" whole_text += boarder_line for i in range(8): @@ -613,19 +870,50 @@ def draw(self, printing=True): current_line += cell.get_piece().draw() current_line += "|" whole_text += "\n" - # ###print(current_line) whole_text += current_line - # ###print(boarder_line) whole_text += "\n" whole_text += boarder_line - print(whole_text + "\n") + if printing: + print(whole_text + "\n") return whole_text class Game: + """ + Game class, used to play a chess game, interact with the board and move pieces. + + Attributes + ---------- + player1 : player.Player + player object that will be the one to play white pieces. For now, has to be a human player + ai : bool + Whether or not to play with AI. Is set to True, AI will play black pieces. + player2 : player.Player + player object that will play the black pieces. Can be human or AI player. + to_player_player: player1 or player2 + Argument pointing to the player that has to play next. Initialized to white pieces player. + board: Board + Board object on which to play. + status: str + String indicating if the game is still active or if there is mat or pat. + played_moves: list + List storing all the played move during the game. + automatic draw: bool + Whether to draw the board in the terminal at each round. + """ + game_status = [] def __init__(self, automatic_draw=True, ai=False): + """Initialization of the cell. + + Parameters + ---------- + automatic_draw : bool + Whether to draw the board in the terminal at each round. + ai : bool + Whether or not to play with AI. Is set to True, AI will play black pieces. + """ self.player1 = Player(True) self.ai = ai if ai: @@ -643,37 +931,103 @@ def __init__(self, automatic_draw=True, ai=False): self.automatic_draw = automatic_draw def reset_game(self): + """Method to reset the game. Recreates the borad, the pieces and restarts the game.""" self.board.reset() self.played_moves = [] self.to_play_player = self.player1 def to_fen(self): + """ + Writes the board in fen. + + Returns + ------- + str + fen representation of the board. + """ pieces, castling = self.board.to_fen() color_playing = "w" if self.to_play_player.is_white_side() else "b" return pieces + " " + color_playing + " " + castling + " - 0 1" def is_finished(self): + """ + Method to know if the game is still active or finished (i.e. pat or mat) + + Returns + ------- + bool + Whether the game is finished or not. + """ return self.status != "ACTIVE" def move_from_coordinates(self, player, start_x, start_y, end_x, end_y, extras={}): + """ + Method to move a piece on the board from its coordinates. Creates the Move object from the coordinates and + calls the .move() method. + + Parameters + ---------- + player: player.Player + player that wants to move a piece + start_x: int + x-coordinate of the piece to move + start_y: int + y-coordinate of the piece to move + end_x: int + x-coordinate of the cell to move the piece to + end_y: int + x-coordinate of the cell to move the piece to + extras: dict + Dictionnary used to add additional data such as which type a piece a Pawn should be promoted to + if it reaches the other side of the board. + + Returns + ------- + self.move() + Method move of self. + """ + # Get the cells from the coordinates start_cell = self.board.get_cell(start_x, start_y) end_cell = self.board.get_cell(end_x, end_y) + # Create the Move object move = Move(player, self.board, start_cell, end_cell, extras=extras) + # Move return self.move(move, player) def draw_board(self): + """ + Draw the game's borad as a string in the terminal. + """ return self.board.draw() def can_player_move(self, player): - ###print('CHECK IF PLAYER CAN MOVE') + """ + Methods that verifies if a player can still move at least one piece. + + Parameters + ---------- + player: player.Player + player we want to check can still move at least one Piece. + + Returns + ------- + bool + Whether player can still move at least a piece. + """ + # Checks all cells for i in range(8): for j in range(8): + # Checks Pieces on cells selected_piece = self.board.get_cell(i, j).get_piece() if selected_piece is not None: + # Checks color of pices if selected_piece.is_white() == player.is_white_side(): + # Checks if piece can move possible_moves = selected_piece.get_potential_moves(i, j) + + # Verifies if the move is authorized for k in range(len(possible_moves)): selected_move = possible_moves[k] selected_move = Move( @@ -685,22 +1039,34 @@ def can_player_move(self, player): verified_move = selected_move.is_possible_move() if verified_move: - ###print('==== CHECK FINISHED ====') return True - ###print('==== CHECK FINISHED ====') return False def check_pat_mat(self, player): + """ + Method to check if a player is in PAT or MAT situation. + + Parameters + ---------- + player: player.Player + player that needs to be checked + + Returns + ------- + int + 0 if the player can move, 1 if PAT, 2 if MAT + """ can_player_move = self.can_player_move(player) - print("CAN MOVE", can_player_move) if can_player_move: return 0 + # If player cannot move any piece else: if player.is_white_side(): king = self.board.white_king else: king = self.board.black_king + # If King is threatened, is mat otherwise, is pat is_mat = self.board.get_cell(king.x, king.y).is_threatened( self.board, not player.is_white_side ) @@ -710,6 +1076,24 @@ def check_pat_mat(self, player): return 1 def move(self, move, player): + """ + Method to move a piece on the board from player and move objects. + + Parameters + ---------- + move: move.Move + move object ready to move a piece. + player: player.Player + player that wants to move a piece. + + Returns + ------- + bool + Whether the move has happened or not (if not means that it has been blocked by a rule). & Whether the + game keeps going. (Actually False when not moving should be True) + int or str + Status of the game: 0 nothing has happened or no winner, winner otherwise + """ moved_piece = move.moved_piece # List of checks @@ -718,20 +1102,23 @@ def move(self, move, player): return False, 0 assert moved_piece is not None + # Check that right player is playing if player != self.to_play_player: return False, 0 assert player == self.to_play_player + # Check that the move is authorized allowed_move = move.is_possible_move() if not allowed_move: return False, 0 elif moved_piece.is_white() != player.is_white_side(): return False, 0 - else: assert moved_piece.is_white() == player.is_white_side() + # Actually move pieces move.move_pieces() + # Store move self.played_moves.append(move) # Change player @@ -740,9 +1127,11 @@ def move(self, move, player): else: self.to_play_player = self.player1 + # Draw if self.automatic_draw: self.board.draw() - # self.save() + + # Check status of Kings if self.board.white_king.is_killed(): print("END OF THE GAME, BLACK HAS WON") return False, "black" @@ -756,26 +1145,46 @@ def move(self, move, player): return check_status, winner def update_status(self): + """ + Checks the status of the game (on going, pat, mat) and returns it. + + Returns + ------- + bool + Whether the game keeps going or not. + int or str + Status of the game: 0 nothing has happened or no winner, winner otherwise + + """ game_status = self.check_pat_mat(self.player1) + # Pat if game_status == 1: - ###print('PAT, white & black do not differentiate each other') return False, "black&white" + # Mat elif game_status == 2: - ###print('END OF THE GAME, MAT DETECTED, BLACK HAS WON') return False, "black" else: game_status = self.check_pat_mat(self.player2) + # Pat if game_status == 1: - ###print('PAT, white & black do not differentiate each other') return False, "black&white" + # Mat elif game_status == 2: - ###print('END OF THE GAME, MAT DETECTED WHITE HAS WON') return False, "white" + # Keeps going else: - ###print('Game keeps going') return True, "" def save(self, directory="debug_files"): + """ + Method to save the state of the game as matplotlib figure. + Uses a str representation of the game moves as figure title. + + Parameters + ---------- + directory: str + directory in which to save the figure. + """ draw_text = self.draw_board() draw_text = draw_text.replace("\x1b[32m", "") draw_text = draw_text.replace("\033[0m", "") diff --git a/python/engine/move.py b/python/engine/move.py index 1bcf377..235e6d0 100644 --- a/python/engine/move.py +++ b/python/engine/move.py @@ -5,23 +5,76 @@ class Move: + """Base class for material movement. + + Implements various checks and is able to operate a piece movement along additional actions such as taking an + adversary piece, castling, en passant. + + Attributes + ---------- + player : player.Player + Which player wants to play the move. + board : Engine.Board + board of the game on which the move is operated. + start : engine.Cell + Current cell on which the moved piece is. + end: engine.Cell + Cell on which the piece is moved to. + moved_piece: material.Piece + Piece of the board that is moved. + killed_piece: [None or material.Piece] + If a piece is killed during the move (meaning that there is a piece on cell end) otherwise is None. + is_castling: bool + Whether the move it castling. + complementary_castling: [None or material.Piece] + If is_castling, holds additional information => To be moved in extras ? + is_en_passant: bool + Whether or not the move is taking a Pawn 'En Passant' + extras: dict + Used to introduce additional information such as piece for promotion. + + """ + def __init__(self, player, board, start, end, extras={}): + """Initialization of the piece. + + Parameters + ---------- + player : player.Player + Which player wants to play the move. + board : Engine.Board + board of the game on which the move is operated. + start : engine.Cell + Current cell on which the moved piece is. + end: engine.Cell + Cell on which the piece is moved to. + extras: dict + Used to introduce additional information such as piece for promotion. + + """ self.player = player self.board = board - self.start = start - self.end = end + self.start = start # Should we take only starting coordinates ? + self.end = end # Should we take only landing coordinates ? self.extras = extras - self.moved_piece = start.get_piece() - if self.moved_piece is None: - ###print("Empty cell selected as start of a move") - pass + self.moved_piece = start.get_piece() # Check that moved_piece is not None ? + self.killed_piece = self.end.get_piece() self.is_castling = False self.complementary_castling = None self.en_passant = False def deepcopy(self): + """Method to create an uncorrelated clone of the move. + + Returns + ------- + Move + Exact copy of self. + + """ + # Rethink what needs to be copied and what needs not. copied_board = self.board.deepcopy() copied_move = Move( self.player, @@ -35,26 +88,53 @@ def deepcopy(self): return copied_move def _set_moved_attribute(self): - if hasattr(self.moved_piece, "has_moved"): + """Method to set the 'has_moved' attributes for the pieces that need to check whether + they have already moved or not. + Also, if the piece is a Pawn, changes the last_move_is_double attribute so that it can + be taken 'En Passant' if this is the case. + """ + if hasattr( + self.moved_piece, "has_moved" + ): # Check if the moved piece has the attribute self.moved_piece.has_moved = True - ###print('PIECE', self.moved_piece.is_white(), self.moved_piece, "set to moved") - ###print(self.start.x, self.start.y, self.end.x, self.end.y) - if hasattr(self.moved_piece, "last_move_is_double"): - if abs(self.start.get_x() - self.end.get_x()) > 1: + + if hasattr(self.moved_piece, "last_move_is_double"): # Check if it is a Pawn + if ( + abs(self.start.get_x() - self.end.get_x()) > 1 + ): # If the move is a double forward self.moved_piece.last_move_is_double = True else: self.moved_piece.last_move_is_double = False def _set_castling_done(self): + """ + If self is a castling move, then when it is done this function sets the castling_done attribute + of the concerned King to True, so that it cannot use castling twice. + """ assert isinstance(self.moved_piece, material.King) self.moved_piece.castling_done = True def _is_castling(self): + """ + Checks if the current move is castling. In particular verifies that: + - the moved piece is a King + - the moved King has not already used its castling + - the King has not already moved + - the comparnion Rook is on the right Cell + - the companion Rook has not already moved + - the Cells between the King and the Rook are empty + - the King landing Cell is not threatened by adversary Pieces + Also if it is a castling move sets in self.extra the Rook that needs to be moved along the King + + Returns + ------- + bool + Whether self is castling or not. + """ if not isinstance(self.moved_piece, material.King): ###print("not castling becasuse king not moved") return False elif self.moved_piece.castling_done or self.moved_piece.has_moved: - ###print("castling already done or king has already moved") ###print(self.moved_piece.castling_done) ###print(self.moved_piece.has_moved) return False @@ -63,10 +143,8 @@ def _is_castling(self): if self.end.y == 6: # Castling in the right rook_to_move = self.board.get_cell(self.start.x, 7).get_piece() if not isinstance(rook_to_move, material.Rook): - ###print("no rook to move") return False elif rook_to_move.has_moved: - ###print("rook has already moved") return False else: rook_starting_coordinates = (self.start.x, 7) @@ -84,10 +162,8 @@ def _is_castling(self): elif self.end.y == 2: # Castling on the left rook_to_move = self.board.get_cell(self.start.x, 0).get_piece() if not isinstance(rook_to_move, material.Rook): - ###print('no rook to move') return False elif rook_to_move.has_moved: - ###print('rook has already moved') return False else: rook_starting_coordinates = (self.start.x, 0) @@ -106,6 +182,8 @@ def _is_castling(self): ###print('king did not move to a castling position') return False + # Verifying that the cells between rook and king are empty and that starting + # and landing cells for the king are not threatened. empty_cells_check = True not_threatened_cells = True for cll in must_be_empty_cells: @@ -114,11 +192,20 @@ def _is_castling(self): for cll in must_not_be_threatened_cells: if cll.is_threatened(self.board, self.moved_piece.is_white()): not_threatened_cells = False - ###print("CELL THREATENED: ", cll.get_x(), cll.get_y()) + # Verifies that both conditions are met conditions_to_castling = [empty_cells_check, not_threatened_cells] if all(conditions_to_castling): - self.complementary_castling = ( + self.complementary_castling = ( # To store in self.extras :) + rook_to_move, + self.board.get_cell( + rook_starting_coordinates[0], rook_starting_coordinates[1] + ), + self.board.get_cell( + rook_ending_coordinates[0], rook_ending_coordinates[1] + ), + ) + self.extras["complementary_castling"] = ( rook_to_move, self.board.get_cell( rook_starting_coordinates[0], rook_starting_coordinates[1] @@ -137,16 +224,35 @@ def _is_castling(self): return False def _is_en_passant(self): - if isinstance(self.moved_piece, material.Pawn): + """ + Checks if the current move is an 'En passant' capture. In particular verifies that: + - the moved piece is a Pawn + - the movement of the pices is in diagonal + - the move crosses another Pawn + - this crossed Pawn is of different color + - this crossed Pawn last move is a double forward advance + Also if it is an 'En passant' capture sets the crossed Pawn as killed_piece + Returns + ------- + bool + Whether self is an 'En passant' capture or not. + """ + if isinstance( + self.moved_piece, material.Pawn + ): # Only a Pawn can take 'En Passant' dx = self.start.get_x() - self.end.get_x() dy = self.start.get_y() - self.end.get_y() + # Needs the movement to be in diagonal and that no piece is on the landing Cell if dy == 0 or self.killed_piece is not None: return False else: + # Retrieving crossed Piece crossed_cell = self.board.get_cell(self.start.get_x(), self.end.get_y()) crossed_piece = crossed_cell.get_piece() + # Verifying the crossed Piece is a Pawn if isinstance(crossed_piece, material.Pawn): + # Verifying color and last move of crossed_piece if ( crossed_piece.last_move_is_double and crossed_piece.is_white() != self.moved_piece.is_white() @@ -163,50 +269,91 @@ def _is_en_passant(self): return False def _is_pawn_promotion(self): + """ + Checks if the current move is should ends up with a Pawn promotion, meaning that: + - the moved piece is a Pawn + - the Pawn reaches the other side of the board according to its color + If the Piece it should be promote into is not specified in self.extras then sets Queen as promotion. + + Returns + ------- + bool + Whether self should ends up by a Pawn promotion or not. + """ + # Checks the piece if not isinstance(self.moved_piece, material.Pawn): return False else: - if self.end.get_x() == 7 and self.moved_piece.is_white(): + # Checks if the Pawn has reached the other side of the board. + if self.end.get_x() == 7 and self.moved_piece.is_white(): # White Piece + # Standard is to promote into a Queen if not specified self.promote_into = self.extras.get("promote_into", "queen") - print(self.extras) return True - elif self.end.get_x() == 0 and not self.moved_piece.is_white(): + + elif ( + self.end.get_x() == 0 and not self.moved_piece.is_white() + ): # Black Piece + # Standard is to promote into a Queen if not specified self.promote_into = self.extras.get("promote_into", "queen") return True else: return False def _promote_pawn(self): + """ + Organizes the Pawn promotion. + """ coordinates = (self.end.get_x(), self.end.get_y()) - print("promote into", self.promote_into) self.board.promote_pawn(coordinates=coordinates, promote_into=self.promote_into) def move_pieces(self): """ Effectively moves pieces on board """ + # Do everything from coordinates so that only board needs to be copied in self.deepcopy() ? + # Kills Piece on landing Cell if needed. if self.killed_piece is not None: self.board.kill_piece_from_coordinates((self.end.x, self.end.y)) + # Moves Piece on the Board self.board.move_piece_from_coordinates( (self.start.x, self.start.y), (self.end.x, self.end.y) ) - # ADD CASTLING + # Executes castling if needed if self.complementary_castling is not None and self.is_castling: castling_rook, rook_start, rook_end = self.complementary_castling self.board.move_piece_from_coordinates( (rook_start.x, rook_start.y), (rook_end.x, rook_end.y) ) - ###print("CASTLING DETECTED PPPPPPPPP") + + # Sets castling to done self._set_castling_done() + # Promotes Pawn if needed if self._is_pawn_promotion(): self._promote_pawn() + + # Sets the different movement related attributes of Pieces self._set_moved_attribute() - def is_possible_move(self): # REFONDRE - # To be implemented + def is_possible_move(self): + # REFONDRE, particulièrement, faire en sorte qu'on ne vérifie chaque condition qu'une seule fois + # Why castling is checked here ? + """ + Checks if move is possible. In particular checks that: + - Landing Cell is different than current Cell + - Checks the color of the potential Piece on the landing Cell + - Movement on the Board is authorized according to the Piece type + - If moved Piece is a King, does not land on a threatened Cell + + If the Piece it should be promote into is not specified in self.extras then sets Queen as promotion. + + Returns + ------- + bool + Whether the move is legal or not. + """ # Should be kept ? dx = self.start.get_x() - self.end.get_x() @@ -227,22 +374,18 @@ def is_possible_move(self): # REFONDRE # check color of receiving piece if self.killed_piece is not None: if self.moved_piece.is_white() == self.killed_piece.is_white(): - ###print("Move is not legal, trying to move piece toward a cell with piece of same color") return False if self.killed_piece.is_white() == self.player.is_white_side(): - ###print('Player cannot take his own material') return False - # CHECK NOT THREATENED CELL IF KING - # good moving for type of piece selected is_legal_move = self.moved_piece.can_move(self.board, self) + + # Why here ? self.is_castling = self._is_castling() if not is_legal_move: if not self._is_en_passant() or not self.is_castling: - ###print('Move is not legal, %s authorized movements not allowing it to go on this cell (%i, %i) from cell (%i, %i)' % - ### (self.moved_piece.__str__(), self.end.get_x(), self.end.get_y(), self.start.get_x(), self.start.get_y())) return False """ @@ -295,21 +438,29 @@ def work_future(curr_move, curr_board): return future_board.get_cell(king.x, king.y).is_threatened(future_board, king.is_white()) """ + # Checks if the player's King is threatened after the move. is_king_threatened_in_future = self._work_future_to_check_chess() if is_king_threatened_in_future: - ###print('King will be threatened / checked if this move is operated') return False return True - def _work_future_to_check_chess(self): - """TO BE DONE LAST""" + def _work_future_to_check_chess(self): # Can it be done better ? + """ + Effectively move the Piece and check if the King is then threatened. In this case, move cannot happen. + + Returns + ------- + bool + Whether the player's King is threatened once the move done. + """ - # move = copy.deepcopy(self) + # Deep Copy everything move = self.deepcopy() - # move = pickle.loads(pickle.dumps(self, -1)) + # Move Piece move.move_pieces() + # Select player's King and check if threatened. if move.player.is_white_side(): king = move.board.white_king else: diff --git a/python/player/player.py b/python/player/player.py index c5f7542..2f19212 100644 --- a/python/player/player.py +++ b/python/player/player.py @@ -4,43 +4,119 @@ class Player: + """Base class players. If human player only here to ensure that right player is playing right Pieces. + For AI-based players can implement more operations. + + Attributes + ---------- + is_white_side : bool + Whether the player plays with white or black Pieces. + + """ + def __init__(self, white_side): + """Initialization of the player. + + Parameters + ---------- + white_side : bool + Whether the player plays with white or black pieces. + """ self.white_side = white_side self.random_number = np.random.randint(0, 1000, 1) def __str__(self): + """Creates a string representation of the player. + + Returns + ------- + str + String representation of the player + + """ return "NormalPlayer%i" % self.random_number def is_white_side(self): + """Method to access the player's side. + + Returns + ------- + bool + color of pieces played by player. + + """ return self.white_side def time_to_play(self, board): + """Potential method that returns a move. + + + Parameters + ---------- + board : engine.Board on which to play. + + Returns + ------- + move.Move move to operate on board. + + """ pass class AIRandomPlayer(Player): + """ + A first AI that plays totally randomly. Selects one move among all possibles and plays it. + """ + def __str__(self): + """Creates a string representation of the player. + + Returns + ------- + str + String representation of the player + """ + return "AIRandomPlayer" def time_to_play(self, board): + """Potential method that returns a move. + + + Parameters + ---------- + board : engine.Board on which to play. + + Returns + ------- + move.Move move to operate on board. + """ + + # Random selection of Piece to play for i in np.random.permutation(8): for j in np.random.permutation(8): + # Verfies there is a Piece to play. if board.get_cell(i, j).get_piece() is not None: if ( board.get_cell(i, j).get_piece().is_white() == self.is_white_side() ): + # Get potential moves selected_piece = board.get_cell(i, j).get_piece() print("AI Selected Piece", selected_piece) possible_moves = selected_piece.get_potential_moves(i, j) + # We can must verify that a move is playable verified_move = False + # Random selection of move random_move = np.random.permutation(len(possible_moves)) index = 0 print( "Verifying Moves,", len(possible_moves), "Moves Possibles" ) + # Stop only with a move that can be played. while not verified_move and index < len(random_move): + # Select and check move. selected_move = possible_moves[random_move[index]] selected_move = Move( self, @@ -51,6 +127,7 @@ def time_to_play(self, board): verified_move = selected_move.is_possible_move() index += 1 + # If move is ok, break loop and return it if verified_move: print("Move is verified, ") return selected_move diff --git a/tests/unit_test/engine_test.py b/tests/unit_test/engine_test.py index 9e8be94..51840fd 100644 --- a/tests/unit_test/engine_test.py +++ b/tests/unit_test/engine_test.py @@ -15,7 +15,7 @@ def test_blocked_moves(): _, winner = game.move_from_coordinates(game.player2, 7, 0, 5, 0) assert winner == 0 - + def test_promotion_to_rook(): """ Test that the promotion works well @@ -32,7 +32,9 @@ def test_promotion_to_rook(): game.move_from_coordinates( game.player1, 6, 6, 7, 6, extras={"promote_into": "rook"} ) - assert game.board.to_fen()[0] == "rnbqkbnr/pppp1ppp/8/8/P7/7N/1PPPP2P/RNBQKBrR" + assert ( + game.board.to_fen()[0] == "rnbqkbnr/pppp1ppp/8/8/P7/7N/1PPPP2P/RNBQKBrR" + ), game.board.to_fen()[0] def test_default_promotion(): @@ -49,10 +51,13 @@ def test_default_promotion(): game.move_from_coordinates(game.player1, 5, 5, 6, 6) game.move_from_coordinates(game.player2, 5, 0, 4, 0) game.move_from_coordinates(game.player1, 6, 6, 7, 6) - assert game.board.to_fen()[0] == "rnbqkbnr/pppp1ppp/8/8/P7/7N/1PPPP2P/RNBQKBqR" + assert ( + game.board.to_fen()[0] == "rnbqkbnr/pppp1ppp/8/8/P7/7N/1PPPP2P/RNBQKBqR" + ), game.board.to_fen()[0] + def test_working_castling(): - """ Tests that small and big castling work. """ + """Tests that small and big castling work.""" game = engine.Game(automatic_draw=False) game.move_from_coordinates(game.player1, 1, 4, 3, 4) game.move_from_coordinates(game.player2, 6, 4, 4, 4) @@ -71,10 +76,14 @@ def test_working_castling(): # big castling move game.move_from_coordinates(game.player2, 7, 4, 7, 2) - assert game.board.to_fen()[0] == "r1b2rk1/ppppqppp/2n2n2/2b1p3/4P1Q1/2NP4/PPPB1PPP/2KR1BNR" + assert ( + game.board.to_fen()[0] + == "r1b2rk1/ppppqppp/2n2n2/2b1p3/4P1Q1/2NP4/PPPB1PPP/2KR1BNR" + ), game.board.to_fen()[0] + def test_failing_castling(): - """ Tests conditions where castling cannot be done. """ + """Tests conditions where castling cannot be done.""" game = engine.Game(automatic_draw=False) game.move_from_coordinates(game.player1, 1, 4, 3, 4) game.move_from_coordinates(game.player2, 6, 4, 4, 4) @@ -92,20 +101,27 @@ def test_failing_castling(): # small castling move _, status = game.move_from_coordinates(game.player1, 0, 4, 0, 6) assert status == 0 - assert game.board.to_fen()[0] == "rnbqk2r/pppp1ppp/5n2/2b1p3/4P1Q1/2N5/PPPP1PPP/R1B1KBNR" + assert ( + game.board.to_fen()[0] + == "rnbqk2r/pppp1ppp/5n2/2b1p3/4P1Q1/2N5/PPPP1PPP/R1B1KBNR" + ), game.board.to_fen()[0] + def test_en_passant(): - """ Tests that prise en passant can be done. """ + """Tests that prise en passant can be done.""" game = engine.Game(automatic_draw=False) game.move_from_coordinates(game.player1, 1, 4, 3, 4) game.move_from_coordinates(game.player2, 6, 0, 5, 0) game.move_from_coordinates(game.player1, 3, 4, 4, 4) game.move_from_coordinates(game.player2, 6, 5, 4, 5) game.move_from_coordinates(game.player1, 4, 4, 5, 5) - assert game.board.to_fen()[0] == "rnbqkbnr/pppp1ppp/8/8/5P2/P4p2/1PPPP1PP/RNBQKBNR" + assert ( + game.board.to_fen()[0] == "rnbqkbnr/pppp1ppp/8/8/5P2/P4p2/1PPPP1PP/RNBQKBNR" + ), game.board.to_fen()[0] + def test_blocked_by_mat(): - """ Tests that if the king is checked cannot move unless it unchecks the king. """ + """Tests that if the king is checked cannot move unless it unchecks the king.""" game = engine.Game(automatic_draw=True) game.move_from_coordinates(game.player1, 1, 4, 3, 4) game.move_from_coordinates(game.player2, 6, 5, 4, 5) @@ -113,8 +129,9 @@ def test_blocked_by_mat(): _, status = game.move_from_coordinates(game.player2, 4, 5, 3, 4) assert status == 0 + def test_end_game(): - """ Tests what happens when check & mat happens. """ + """Tests what happens when check & mat happens.""" game = engine.Game(automatic_draw=True) game.move_from_coordinates(game.player1, 1, 4, 3, 4) game.move_from_coordinates(game.player2, 6, 5, 4, 5) @@ -125,6 +142,7 @@ def test_end_game(): keep_going, status = game.move_from_coordinates(game.player1, 4, 7, 5, 6) print(keep_going, status) + if __name__ == "__main__": test_en_passant() test_end_game()