diff --git a/pyalapin/__init__.py b/pyalapin/__init__.py new file mode 100644 index 0000000..33749f8 --- /dev/null +++ b/pyalapin/__init__.py @@ -0,0 +1,6 @@ +""" +pyalapin, chess python package. +""" + +__version__ = "0.0.1" +__author__ = "Vincent Auriau" diff --git a/pyalapin/engine/__init__.py b/pyalapin/engine/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/engine/color.py b/pyalapin/engine/color.py similarity index 100% rename from python/engine/color.py rename to pyalapin/engine/color.py diff --git a/python/engine/engine.py b/pyalapin/engine/engine.py similarity index 92% rename from python/engine/engine.py rename to pyalapin/engine/engine.py index eaeea17..2b91814 100644 --- a/python/engine/engine.py +++ b/pyalapin/engine/engine.py @@ -7,11 +7,11 @@ import copy -from engine.move import Move -from player.player import Player, AIRandomPlayer -from player.ai_player import EasyAIPlayer -from player.my_player import MyPlayer -import engine.material as material +from pyalapin.engine.move import Move +from pyalapin.player.player import Player, AIRandomPlayer +from pyalapin.player.ai_player import EasyAIPlayer +from pyalapin.player.my_player import MyPlayer +import pyalapin.engine.material as material class Color: @@ -292,7 +292,6 @@ def is_threatened( return True elif piece_to_check is None: - print("def") keep_going = True x_to_check += i y_to_check += j @@ -414,10 +413,9 @@ def __init__(self, empty_init=False): 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={}): + def deepcopy(self, light=True): """Method to create an uncorrelated clone of the board. Returns @@ -428,12 +426,21 @@ def deepcopy(self, memodict={}): 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 - copied_material = self.deep_copy_material() - white_king = copied_material["white"]["alive"]["king"][0] - black_king = copied_material["black"]["alive"]["king"][0] + if light: + copied_material = self.light_deep_copy_material() + else: + copied_material = self.deep_copy_material() + + assert ( + len(copied_material["black"]["alive"]["king"]) > 0 + ), "Black king is dead ?" + assert ( + len(copied_material["white"]["alive"]["king"]) > 0 + ), "White king is dead ?" + copied_object.white_king = copied_material["white"]["alive"]["king"][0] + copied_object.black_king = copied_material["black"]["alive"]["king"][0] copied_object.all_material = copied_material - copied_object.white_king = white_king - copied_object.black_king = black_king + for piece_list in copied_material["white"]["alive"].values(): for piece in piece_list: copied_object.get_cell(piece.x, piece.y).set_piece(piece) @@ -443,6 +450,63 @@ def deepcopy(self, memodict={}): return copied_object + def light_deep_copy_material(self): + """Method to create an uncorrelated clone of all the pieces on the board. Light version + where only alive pieces are returned. + + Returns + ------- + dict of Pieces + Exact copy of self.all_material. + """ + material = { + "white": { + "alive": { + "pawn": [], + "knight": [], + "bishop": [], + "rook": [], + "queen": [], + "king": [], + }, + "killed": { + "pawn": [], + "knight": [], + "bishop": [], + "rook": [], + "queen": [], + "king": [], + }, + }, + "black": { + "alive": { + "pawn": [], + "knight": [], + "bishop": [], + "rook": [], + "queen": [], + "king": [], + }, + "killed": { + "pawn": [], + "knight": [], + "bishop": [], + "rook": [], + "queen": [], + "king": [], + }, + }, + } + + for color in ["white", "black"]: + for status in ["alive"]: + for piece_type in ["pawn", "knight", "bishop", "rook", "queen", "king"]: + for piece in self.all_material[color][status][piece_type]: + material[color][status][piece_type].append( + piece.piece_deepcopy() + ) + return material + def deep_copy_material(self): """Method to create an uncorrelated clone of all the pieces on the board, killed and not killed. @@ -499,7 +563,7 @@ def deep_copy_material(self): ) return material - def __deepcopy__(self, memodict={}): + def _deepcopy__(self, memodict={}): """Method to create an uncorrelated clone of the board. Returns @@ -904,7 +968,7 @@ class Game: game_status = [] - def __init__(self, automatic_draw=True, ai=False): + def __init__(self, player1=None, player2=None, automatic_draw=True, ai=False): """Initialization of the cell. Parameters @@ -914,14 +978,30 @@ def __init__(self, automatic_draw=True, ai=False): 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: - # self.player2 = AIRandomPlayer(False) - self.player2 = EasyAIPlayer(False) - # self.player2 = MyPlayer(white_side=False, path_to_model="./test1") + + # If ai = True and both players are None, AI plays by default black pieces + if player2 is None: + if ai: + self.player2 = EasyAIPlayer(False) + else: + self.player2 = Player(False) + + if player1 is None: + self.player1 = Player(True) + else: + self.player1 = player1 + + elif player1 is None: + if ai: + self.player1 = EasyAIPlayer(True) + else: + self.player1 = Player(True) + self.player2 = player2 else: - self.player2 = Player(False) + self.player1 = player1 + self.player2 = player2 + + self.ai = ai self.to_play_player = self.player1 self.board = Board() diff --git a/python/engine/material.py b/pyalapin/engine/material.py similarity index 95% rename from python/engine/material.py rename to pyalapin/engine/material.py index 3cbf33e..a76941c 100644 --- a/python/engine/material.py +++ b/pyalapin/engine/material.py @@ -1,6 +1,6 @@ from abc import abstractmethod -from engine.color import Color +from pyalapin.engine.color import Color class Piece(object): @@ -158,6 +158,24 @@ def get_potential_moves(self, x, y): """ return None + @abstractmethod + def get_threatened_cells_on_board(self, board): + """ + Method to list which cells are threatened by the piece (authorized movement + conditioned on other pieces on board). + + Parameters + ---------- + board: Board + game board self belong to + + Returns + ------- + list + List of threatened cells + + """ + return [] + @abstractmethod def get_str(self): """Method to represent the piece as a string. @@ -371,13 +389,14 @@ def get_potential_moves(self, x, y): possible_moves = [] if self.is_white(): # Front cell - possible_moves.append((x + 1, y)) + if x < 7: + possible_moves.append((x + 1, y)) - # Diagonal cells - if y - 1 >= 0: - possible_moves.append((x + 1, y - 1)) - if y + 1 <= 7: - possible_moves.append((x + 1, y + 1)) + # Diagonal cells + if y - 1 >= 0: + possible_moves.append((x + 1, y - 1)) + if y + 1 <= 7: + possible_moves.append((x + 1, y + 1)) # Double front cell if x == 1: @@ -385,17 +404,54 @@ def get_potential_moves(self, x, y): # Symmetric for black pawns else: - possible_moves.append((x - 1, y)) + if x > 0: + possible_moves.append((x - 1, y)) - if y - 1 >= 0: - possible_moves.append((x - 1, y - 1)) - if y + 1 <= 7: - possible_moves.append((x - 1, y + 1)) + if y - 1 >= 0: + possible_moves.append((x - 1, y - 1)) + if y + 1 <= 7: + possible_moves.append((x - 1, y + 1)) if x == 6: possible_moves.append((x - 2, y)) return possible_moves + @abstractmethod + def get_threatened_cells_on_board(self, board): + """ + Method to list which cells are threatened by the piece (authorized movement + conditioned on other pieces on board). + + Parameters + ---------- + board: Board + game board self belong to + + Returns + ------- + list + List of threatened cells + + """ + + cells_threatened = [] + if self.is_white(): + if x < 7: + # Diagonal cells + if y - 1 >= 0: + cells_threatened.append((x + 1, y - 1)) + if y + 1 <= 7: + cells_threatened.append((x + 1, y + 1)) + + # Symmetric for black pawns + else: + if x > 0: + if y - 1 >= 0: + cells_threatened.append((x - 1, y - 1)) + if y + 1 <= 7: + cells_threatened.append((x - 1, y + 1)) + + return cells_threatened + def promote(self, promote_into="Queen"): """Method to promote a pawn to other material type. Only happens if the pawn reaches the other side of the board. The player can choose which type of material he wants its pawn to be promoted into. diff --git a/python/engine/move.py b/pyalapin/engine/move.py similarity index 98% rename from python/engine/move.py rename to pyalapin/engine/move.py index 235e6d0..718cd65 100644 --- a/python/engine/move.py +++ b/pyalapin/engine/move.py @@ -1,7 +1,7 @@ import copy import pickle -import engine.material as material +import pyalapin.engine.material as material class Move: @@ -337,7 +337,7 @@ def move_pieces(self): # Sets the different movement related attributes of Pieces self._set_moved_attribute() - def is_possible_move(self): + def is_possible_move(self, check_chess=True): # REFONDRE, particulièrement, faire en sorte qu'on ne vérifie chaque condition qu'une seule fois # Why castling is checked here ? """ @@ -439,9 +439,10 @@ 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: - return False + if check_chess: + is_king_threatened_in_future = self._work_future_to_check_chess() + if is_king_threatened_in_future: + return False return True diff --git a/python/interface/interface.py b/pyalapin/interface/interface.py similarity index 54% rename from python/interface/interface.py rename to pyalapin/interface/interface.py index cf99006..5e1d0e7 100644 --- a/python/interface/interface.py +++ b/pyalapin/interface/interface.py @@ -12,11 +12,14 @@ from kivy.graphics import Rectangle, Color, Canvas -from engine.engine import Game +from pyalapin.engine.engine import Game class LoginScreen(GridLayout): def __init__(self, **kwargs): + """ + Base class for a Login Screen, not used yet + """ super(LoginScreen, self).__init__(**kwargs) self.cols = 8 self.add_widget(Label(text="")) @@ -34,14 +37,58 @@ def __init__(self, **kwargs): class DisplayableCell(Button): + """Base class to represent a Cell as Button""" + def __init__(self, row, column, **kwargs): + """ + Initialization of the representation of the cell. + + Parameters + ---------- + + row: int + row coordinate of the Cell + column: int + column coordinate of the Cell + """ super(DisplayableCell, self).__init__(**kwargs) self.row = row self.column = column class TableScreen(GridLayout): + """ + Main class to represent and display the board, as well as to play a chess game. + + Attributes + ---------- + path_to_illustrations: str + Path to the images to use to display cells & pieces + game: engine.Game + game to actually represent + ai_playing: bool + whether or not an AI is playing (only one of the players for now) + cols: int + number of columns of the board + rows: int + number of rows of the board + to_play_player: player.Player + player who should play next + first_cell_clicked: engince.Cell or None + First part of a piece movement, used to memorize the first cell selected by user who would like to move a piece. + cells: list of DisplayableCells + List of cells constituting the board + """ + def __init__(self, game, **kwargs): + """ + Initialization of the board display. + + Parameters + ---------- + game: engine.Game to represent + + """ super(TableScreen, self).__init__(**kwargs) self.path_to_illustrations = "illustrations" self.game = game @@ -60,9 +107,11 @@ def __init__(self, game, **kwargs): self.cells = [] - for i in range(8): + # Initialization of the display of the board + for i in range(self.rows): line = [] - for j in range(8): + for j in range(self.cols): + # Alternate cells color for better board perception if (i % 2 == 0 and j % 2 == 0) or (i % 2 == 1 and j % 2 == 1): color = (0.4, 0.4, 0.8, 1) c_img = "b" @@ -78,7 +127,7 @@ def __init__(self, game, **kwargs): path_to_img = c_img if piece.is_white(): - piece_color = (1, 1, 1, 1) + piece_color = (1, 1, 1, 1) # For text color, could be removed path_to_img += "w" else: piece_color = (0, 0, 0, 1) @@ -93,7 +142,7 @@ def __init__(self, game, **kwargs): piece = piece.get_str() button = DisplayableCell( - text=piece, + # text=piece, # Remove it for prettier results :) on_press=self.click_cell, row=i, column=j, @@ -103,9 +152,11 @@ def __init__(self, game, **kwargs): background_down=path_to_down_img, ) else: + # No piece to display piece = "" - piece_color = (1, 1, 1, 1) + piece_color = (1, 1, 1, 1) # For text color could be removed path_to_img = c_img + ".png" + # Unclicked path_to_down_img = "down_" + path_to_img path_to_img = os.path.join(self.path_to_illustrations, path_to_img) @@ -114,7 +165,7 @@ def __init__(self, game, **kwargs): ) button = DisplayableCell( - text=piece, + # text=piece, # Remove for prettier results :) background_normal=path_to_img, on_press=self.click_cell, row=i, @@ -128,14 +179,25 @@ def __init__(self, game, **kwargs): self.cells.append(line) def reset_game(self, button): + """ + Method used to reset a game when clicked on the reset button + + Parameters + ---------- + button: Button + button used to click for reset + """ print("On click, Reset", button) self.game.reset_game() self.update() def update(self): + """ + Method used to update the display of the board. Actually redraws everythin from start. + """ board = self.game.board - for i in range(8): - for j in range(8): + for i in range(self.rows): + for j in range(self.cols): if (i % 2 == 0 and j % 2 == 0) or (i % 2 == 1 and j % 2 == 1): c_img = "b" else: @@ -162,12 +224,20 @@ def update(self): path_to_down_img = os.path.join( self.path_to_illustrations, path_to_down_img ) - self.cells[i][j].text = piece - self.cells[i][j].color = piece_color + # self.cells[i][j].text = piece + # self.cells[i][j].color = piece_color self.cells[i][j].background_normal = path_to_img self.cells[i][j].background_down = path_to_down_img def finish_game(self, winner): + """ + Method used to trigger a display of the end of a game. + + Parameters + ---------- + winner: player.Player + Winner of self.Game if the game is finished. + """ popup = Popup(title="Game finished", auto_dismiss=False) popup.bind(on_dismiss=self.reset_game) @@ -182,6 +252,18 @@ def finish_game(self, winner): popup.open() def click_cell(self, event): + """ + Main trigger when a cell is clicked. + Works in a serie of two clicks: + - First click is to define start cell (or cell of the piece we want to move) + - Second click is to define the landing cell (or cell we want to move the piece to) + + Parameters + ---------- + event: Event + Click on a Board's cell event triggering the method. + """ + # Reverse the backgrounds when a cell is clicked, to indicate it has been clicked. ( self.cells[event.row][event.column].background_normal, self.cells[event.row][event.column].background_down, @@ -189,81 +271,137 @@ def click_cell(self, event): self.cells[event.row][event.column].background_down, self.cells[event.row][event.column].background_normal, ) + # If no previous cell has been clicked, then it's the start cell that has been clicked, + # In this case it is store, waiting fot the click on the landinc cell. if self.first_cell_clicked is None: self.first_cell_clicked = (event.row, event.column) + # In this case the player has clicked twice on the same cell. + # It is considered as a cancellation of the first click elif ( self.first_cell_clicked[0] == event.row and self.first_cell_clicked[1] == event.column ): print("Selection Aborted") self.first_cell_clicked = None + # In this cas, actually move the piece. else: start_x = self.first_cell_clicked[0] start_y = self.first_cell_clicked[1] end_x = event.row end_y = event.column - print(self.game.player1, self.game.to_play_player) + # Try and move if possible the piece validated_move, winner = self.game.move_from_coordinates( self.game.to_play_player, start_x, start_y, end_x, end_y ) - print( - "Validated move ?", - validated_move, - self.game.to_play_player, - start_x, - start_y, - end_x, - end_y, - winner, - ) + # In case move is ok if validated_move: self.update() + # If AI is playing, then trigger its next move with time_to_play method. if self.ai_playing: print("Time for AI") + + # Resets background colors of player ai_move = self.game.player2.time_to_play(self.game.board) self.game.board.draw() game_is_on = self.game.move(ai_move, self.game.player2) + # Verify game is still going on + # Actually we consider that an AI cannot trigger an impossible move here + # Maybe should be modified ? + if game_is_on[0]: self.update() + ( + self.cells[ai_move.start.x][ + ai_move.start.y + ].background_normal, + self.cells[ai_move.start.x][ + ai_move.start.y + ].background_down, + ) = ( + self.cells[ai_move.start.x][ + ai_move.start.y + ].background_down, + self.cells[ai_move.start.x][ + ai_move.start.y + ].background_normal, + ) + ( + self.cells[ai_move.end.x][ai_move.end.y].background_normal, + self.cells[ai_move.end.x][ai_move.end.y].background_down, + ) = ( + self.cells[ai_move.end.x][ai_move.end.y].background_down, + self.cells[ai_move.end.x][ai_move.end.y].background_normal, + ) else: if isinstance(game_is_on[1], str): self.finish_game(game_is_on[1]) else: + # Can we be here ? pass + # Verify is there is or not a winner and if game is finished. elif isinstance(winner, str): print("WINNER", winner) self.finish_game(winner) return None - row, col = self.first_cell_clicked - ( - self.cells[row][col].background_normal, - self.cells[row][col].background_down, - ) = ( - self.cells[row][event.column].background_down, - self.cells[event.row][col].background_normal, - ) + # In this case, game was not possible, reset last clicks so that the player can restart + # and redefine its move. + else: + popup = Popup( + title="Unable Move", + content=Label( + text="Your selected move is not possible, please, select another one." + ), + size_hint=(None, None), + size=(15, 15), + ) + popup.open() + + if not self.ai_playing: + # Resets values befor next move + # If AI is playing, it is handled with self.update() + row, col = self.first_cell_clicked + ( + self.cells[row][col].background_normal, + self.cells[row][col].background_down, + ) = ( + self.cells[row][col].background_down, + self.cells[row][col].background_normal, + ) + ( + self.cells[event.row][event.column].background_normal, + self.cells[event.row][event.column].background_down, + ) = ( + self.cells[event.row][event.column].background_down, + self.cells[event.row][event.column].background_normal, + ) + self.first_cell_clicked = None - ( - self.cells[event.row][event.column].background_normal, - self.cells[event.row][event.column].background_down, - ) = ( - self.cells[event.row][event.column].background_down, - self.cells[event.row][event.column].background_normal, - ) + if not validated_move: + self.update() class MyApp(App): + """ + Main app to use to play game, by calling MyApp().buil() and then player. + """ + def __init__(self, play_with_ai=False, **kwargs): + """ + Initialization, with precision whether or not playing with AI. + """ super().__init__(**kwargs) self.play_with_ai = play_with_ai def build(self): + """ + Builds the game and the display board along with it. + """ game = Game(automatic_draw=False, ai=self.play_with_ai) print("game created") return TableScreen(game) diff --git a/python/player/ai_player.py b/pyalapin/player/ai_player.py similarity index 65% rename from python/player/ai_player.py rename to pyalapin/player/ai_player.py index 076819e..a24775e 100644 --- a/python/player/ai_player.py +++ b/pyalapin/player/ai_player.py @@ -2,12 +2,27 @@ import pickle import numpy as np -from player.player import Player -import engine.material as material -import engine.move as move +from pyalapin.player.player import Player +import pyalapin.engine.material as material +import pyalapin.engine.move as move class EasyAIPlayer(Player): + """ + AI Player class with simple rules and alpha/beta pruning to speed up research of the best move in the future. + + Attributes + ---------- + is_white_side : bool + Whether the player plays with white or black Pieces. + piece_weights: dict + Values of the different pieces. + pieces_positions_weights: dict + Values for each piece to be on a certain position. + random_coeff: int + Coefficient of randomness that will be added to the move score. + """ + piece_weights = { "pawn": 10, "knight": 30, @@ -79,10 +94,21 @@ class EasyAIPlayer(Player): ], } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, white_side, random_coeff=0, *args, **kwargs): + """Initialization of the player. + + Parameters + ---------- + white_side : bool + Whether the player plays with white or black pieces. + random_coeff: int + Coefficient of randomness that will be added to the move score. + """ + super().__init__(white_side=white_side, *args, **kwargs) self.color = "white" if self.white_side else "black" - if self.color == "white": + self.random_coeff = random_coeff + if self.white_side: + # Reverse position values for white pieces player for key, values in self.piece_positions_weights.items(): new_values = [] for i in range(len(values)): @@ -90,9 +116,25 @@ def __init__(self, *args, **kwargs): self.piece_positions_weights[key] = new_values def __str__(self): + """Initialization of the player. + + Returns + ------- + str + String representation of the player + """ return "EasyAIPlayer" def _get_possible_moves(self, board, is_white=None): + """Initialization of the player. + + Parameters + ---------- + board: Board + Board on which to look for the possible moves. + is_white : bool or None + If we want the possible moves for a different player, can be used. + """ if is_white is None: is_white = self.white_side() @@ -109,48 +151,72 @@ def is_white_side(self): else: player = self color = {True: "white", False: "black"}[is_white] + possible_moves = [] - ###print("LOOKING FOR MOVES FOR COLOR", color) + # Iterate of pieces for type_piece in board.all_material[color]["alive"].keys(): - ###print(' >>>>>>>>>>>>>>>>> TYPE PCE', type_piece) for piece in board.all_material[color]["alive"][type_piece]: + # Get potential moves as coordinates piece_available_moves = piece.get_potential_moves(piece.x, piece.y) + + # Iterate over piece possible moves for mv in piece_available_moves: - if isinstance(piece, material.Pawn): - ###print("POSSIBLE MOVES FOR PAWN", piece_available_moves) - pass + # Verify that the move is actually possible selected_move = move.Move( player, board, board.get_cell(piece.x, piece.y), board.get_cell(mv[0], mv[1]), ) - if selected_move.is_possible_move(): + # Keep only of possible + # Test letting this test in _alpha_beta + if selected_move.is_possible_move(check_chess=False): possible_moves.append(selected_move) - ###print("possible move +1") - ###print("NB possible moves", len(possible_moves)) + return possible_moves def _select_move_from_score(self, moves, method="max"): - all_scores = {} + """Method to select a move according to the score on the board after the move. + Maximum or minimum score can be selected. + + Parameters + ---------- + moves : list + list of moves among which to chose the best one. + method: str in {"min", "max"} + Whether to select the move leading to minimum or maximum score + + Returns + ------- + move.Move + Selected move + float + Score corresponding to the selected move + """ + # Compute scores + scores = {} for mv in moves: - mv_ = pickle.loads(pickle.dumps(mv, -1)) + mv_ = mv.deepcopy() + # mv_ = pickle.loads(pickle.dumps(mv, -1)) # mv_ = copy.deepcopy(mv) mv_.move_pieces() score = self._score_board(mv_.board) - all_scores[mv] = score + scores.append(score) - scores = list(all_scores.values()) + # Selection os scores if method == "max": all_indexes = np.where(scores == np.max(scores))[0] elif method == "min": all_indexes = np.where(scores == np.min(scores))[0] else: raise ValueError("MIN OR MAX ALGO, selected %s" % method) + # If several moves lead to the same score, select randomly + if len(all_indexes) > 1: + perm = np.random.permutation(len(all_indexes))[0] + final_index = all_indexes[perm] - perm = np.random.permutation(len(all_indexes))[0] - final_index = all_indexes[perm] - return list(all_scores.keys())[int(final_index)], scores[final_index] + # Return + return moves[int(final_index)], scores[final_index] # def _def_get_best_score(self, moves, method="max"): # all_scores = [] @@ -194,7 +260,6 @@ def _search_tree(self, init_board, depth=2, method="max"): best_indexes = np.where(np.array(scores) == best_score)[0] final_index = best_indexes[np.random.permutation(len(best_indexes))[0]] - ###print(final_index) return possible_moves[int(final_index)], best_score def _score_move(self, move): @@ -215,30 +280,58 @@ def _alpha_beta( alpha=-10000, beta=10000, is_white=None, + draw_board=False, ): + """Method to reccursively look for the best move. Implements alpha-beta pruning to test most possible moves. + + Parameters + ---------- + init_board : engine.Board + Current state of the game as board + init_move: move.Move or None + Not sure yet + depth: int + How many turns to look into the future (also used for recursion). + alpha: int + Max score value for alpha pruning + beta: int + Max score value for beta pruning + is_white: None or bool + Can be used if we want to use the method for other color than self + draw_board: bool + Whether or not to draw the board in terminal + + Returns + ------- + int + score of "best" selected move + move + "best" selected move + """ + # If want to use other color than self. if is_white is None: is_white = self.white_side - ###print('ALPHA BETA FOR BOARD:', "with depth", depth) - init_board.draw() - if depth == 0: - ###print("SCORING BOARD") + + if draw_board: init_board.draw() + + # End of recursion + if depth == 0: + # Score current board and return it score = self._score_board(init_board) - ###print("SCORE FOUND:", score) return score, init_move elif is_white == self.is_white_side(): + # Get moves possible_moves = self._get_possible_moves(init_board, is_white=is_white) - ###print(depth, "nb moves:", len(possible_moves)) + # Iterate over moves and keep the best one. best_score = -10000 best_move = None i = 0 for p_mv in possible_moves: - ###print("Move", i, "on", len(possible_moves), "for depth", depth, p_mv.end.x) i += 1 - # p_mv_ = pickle.loads(pickle.dumps(p_mv, -1)) - # p_mv_ = copy.deepcopy(p_mv) p_mv_ = p_mv.deepcopy() p_mv_.move_pieces() + # Get best move if p_mv is actually made score, _ = self._alpha_beta( p_mv_.board, init_move=p_mv_, @@ -247,24 +340,24 @@ def _alpha_beta( beta=beta, is_white=not is_white, ) - ###print(score, p_mv.start.x, p_mv.start.y, p_mv.end.x, p_mv.end.y) - best_move = [best_move, p_mv][np.argmax([best_score, score])] - best_score = np.max([best_score, score]) - ###print("BEST SCORE", best_score) - ###print("BEST MOVE", best_move, best_move.start.x, best_move.start.y, best_move.end.x, best_move.end.y) + random_noise = np.random.randint(0, self.random_coeff) + best_move = [best_move, p_mv][ + np.argmax([best_score, score + random_noise]) + ] + best_score = np.max([best_score, score + random_noise]) + if best_score >= beta: return best_score, best_move alpha = np.max((alpha, best_score)) - ###print("BBBBESTTT MOOVEEE", best_move, best_move.start.x, best_move.start.y, best_move.end.x, best_move.end.y, best_score) + return best_score, best_move else: possible_moves = self._get_possible_moves(init_board, is_white=is_white) - ###print(depth, "nb moves:", len(possible_moves)) + best_score = 10000 best_move = None for p_mv in possible_moves: - # p_mv_ = pickle.loads(pickle.dumps(p_mv, -1)) p_mv_ = p_mv.deepcopy() p_mv_.move_pieces() score, _ = self._alpha_beta( @@ -275,16 +368,13 @@ def _alpha_beta( beta=beta, is_white=is_white, ) - ###print(score, p_mv.start.x, p_mv.start.y, p_mv.end.x, p_mv.end.y) + best_move = [best_move, p_mv][np.argmin([best_score, score])] best_score = np.min([best_score, score]) - ###print("BEST SCORE", best_score) - ###print(np.argmax([best_score, score])) - ###print("BEST MOVE", best_move, best_move.start.x, best_move.start.y, best_move.end.x, best_move.end.y) + if best_score <= alpha: return best_score, best_move beta = np.min([beta, best_score]) - ###print("BBBBESTTT MOOVEEE", best_move, best_move.start.x, best_move.start.y, best_move.end.x, best_move.end.y, best_score) return best_score, best_move def random_move(self, board): @@ -319,34 +409,73 @@ def random_move(self, board): return selected_move ###print('No moved found, aborting...') - def time_to_play(self, board, depth=3): - board.draw() - current_score = self._score_board(board) - ###print("SCORE:", current_score) + def time_to_play(self, board, depth=3, draw_board=False): + """Method that must be called to ask AI player to move. - # all_possible_moves = self._get_possible_moves(board) - # sel_move, sel_score = self._select_move_from_score(all_possible_moves) - # sel_move, sel_score = self._search_tree(board, depth=3) - board.draw() + Parameters + ---------- + board : engine.Board + board on which to play + depth: int + Tree best move search depth + draw_board: bool + Whether or not to draw the board in terminal + + Returns + ------- + move.Move + Best move according to board and parameters + """ + if draw_board: + board.draw() + # current_score = self._score_board(board) sel_score, sel_move = self._alpha_beta(board, depth=depth) - board.draw() - ###print("future score:", sel_score) - ###print(sel_move.start.x, sel_move.start.y, sel_move.end.x, sel_move.end.y, self.color) return sel_move def _score_board(self, board): + """Method to score a board according to player policy. + + Parameters + ---------- + board : engine.Board + board to score + + Returns + ------- + float + Score corresponding to the board and the player's parameters + """ score = 0 + # Positive score for player pieces for piece_type in board.all_material[self.color]["alive"].keys(): for piece in board.all_material[self.color]["alive"][piece_type]: score += self.piece_weights[piece_type] - ###print(piece_type, piece.x, piece.y) score += self.piece_positions_weights[piece_type][piece.x][piece.y] + own_king = board.all_material[self.color]["alive"]["king"] + if len(own_king) == 0: + score -= 1000 + else: + own_king = own_king[0] + if board.get_cell(own_king.x, own_king.y).is_threatened( + board, own_king.is_white() + ): + score -= 1000 adv_color = "white" if self.color == "black" else "black" - + # Negative score for opponent pieces. for piece_type in board.all_material[adv_color]["alive"].keys(): for piece in board.all_material[adv_color]["alive"][piece_type]: score -= self.piece_weights[piece_type] score -= self.piece_positions_weights[piece_type][piece.x][piece.y] + adv_king = board.all_material[adv_color]["alive"]["king"] + + if len(adv_king) == 0: + score -= 1000 + else: + adv_king = adv_king[0] + if board.get_cell(adv_king.x, adv_king.y).is_threatened( + board, adv_king.is_white() + ): + score += 1000 return score diff --git a/python/player/my_player.py b/pyalapin/player/my_player.py similarity index 98% rename from python/player/my_player.py rename to pyalapin/player/my_player.py index 7aafbe6..b9fc20b 100644 --- a/python/player/my_player.py +++ b/pyalapin/player/my_player.py @@ -4,9 +4,9 @@ # import tensorflow as tf -from player.player import Player -import engine.material as material -import engine.move as move +from pyalapin.player.player import Player +import pyalapin.engine.material as material +import pyalapin.engine.move as move class Memory(object): diff --git a/python/player/player.py b/pyalapin/player/player.py similarity index 99% rename from python/player/player.py rename to pyalapin/player/player.py index 2f19212..d775a02 100644 --- a/python/player/player.py +++ b/pyalapin/player/player.py @@ -1,6 +1,6 @@ import numpy as np -from engine.move import Move +from pyalapin.engine.move import Move class Player: diff --git a/pyalapin/setup.py b/pyalapin/setup.py new file mode 100644 index 0000000..db26aab --- /dev/null +++ b/pyalapin/setup.py @@ -0,0 +1,30 @@ +from setuptools import setup, find_packages + +VERSION = "0.0.1" +DESCRIPTION = "pyalapin is a customized chess engine, specifically to work with AIs." +LONG_DESCRIPTION = """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 ❤️""" + +# Setting up +setup( + name="pyalapin", + version=VERSION, + author="Vincent Auriau", + author_email="", + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + packages=find_packages(), + install_requires=["numpy"], + keywords=["python", "first package"], + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Anyone", + "Programming Language :: Python :: 3", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: Linux :: Linux", + ], +) diff --git a/pyproject.toml b/pyproject.toml index 4a9b9ba..195e4d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,5 +14,5 @@ requires-python = ">=3.8" [tool.setuptools] packages = [ "tests", - "python" + "pyalapin" ] diff --git a/run_app.py b/run_app.py index 5557084..51d23d4 100644 --- a/run_app.py +++ b/run_app.py @@ -2,7 +2,7 @@ sys.path.append("python/") -from interface.interface import MyApp +from pyalapin.interface.interface import MyApp if __name__ == "__main__": MyApp(play_with_ai=True).run() diff --git a/tests/prev_engine_test.py b/tests/prev_engine_test.py index e641acd..18cd34a 100644 --- a/tests/prev_engine_test.py +++ b/tests/prev_engine_test.py @@ -1,14 +1,11 @@ import sys -sys.path.append("../python") -sys.path.append("python") +sys.path.append("pyalapin") -import engine.engine as engine -import importlib -import engine.move as move +import pyalapin.engine.engine as engine +import pyalapin.engine.move as move import time -importlib.reload(engine) import player.ai_player as ai_player diff --git a/tests/unit_test/engine_test.py b/tests/unit_test/engine_test.py index b9ec56d..a019586 100644 --- a/tests/unit_test/engine_test.py +++ b/tests/unit_test/engine_test.py @@ -1,9 +1,8 @@ import sys -sys.path.append("../../python") -sys.path.append("python") +sys.path.append("pyalapin") -import engine.engine as engine +import pyalapin.engine.engine as engine def test_blocked_moves(): diff --git a/tests/unit_test/test_material.py b/tests/unit_test/test_material.py index 8573d98..6aef5f2 100644 --- a/tests/unit_test/test_material.py +++ b/tests/unit_test/test_material.py @@ -1,10 +1,9 @@ import sys -sys.path.append("../../python") -sys.path.append("python") +sys.path.append("pyalapin") -import engine.engine as engine -import engine.material as material +import pyalapin.engine.engine as engine +import pyalapin.engine.material as material # Add verifications about own color of piece on end cell # diff --git a/python/utils/images_creation.py b/utils/images_creation.py similarity index 100% rename from python/utils/images_creation.py rename to utils/images_creation.py diff --git a/python/utils/profile_game.py b/utils/profile_game.py similarity index 95% rename from python/utils/profile_game.py rename to utils/profile_game.py index 26223aa..2f8c633 100644 --- a/python/utils/profile_game.py +++ b/utils/profile_game.py @@ -1,7 +1,7 @@ import copy import sys -sys.path.append("../") +sys.path.append("pyalapin/") import engine.engine as engine @@ -17,6 +17,7 @@ ai_move = game.player2.time_to_play(game.board) game_is_on = game.move(ai_move, game.player2) +""" from player.my_player import MyPlayer my_player = MyPlayer(white_side=False) @@ -26,3 +27,4 @@ game_is_on = game.move(ai_move, game.player2) score = my_player._score_board(game.board) print(my_player.model.summary()) +""" diff --git a/python/utils/profiling.png b/utils/profiling.png similarity index 100% rename from python/utils/profiling.png rename to utils/profiling.png diff --git a/python/utils/training_ai.py b/utils/training_ai.py similarity index 100% rename from python/utils/training_ai.py rename to utils/training_ai.py