import random import math import signal """ Base player engine """ class PlayerEngine: """ init @param player: black or white player @param logger: loggig object (display verbose / debug messages @param heuristic: HeuristicClass object to calculate tree heuristic @param options: hashtable containing options """ def __init__(self, player, logger, heuristic, options: dict = {}): # init logger do display informations self.player = player self.logger = logger self.heuristic = heuristic self.options = options self.interrupt_search = False self.logger.info("Init engine {}, options:{}".format( self._get_class_name(), self.options )) """ get move @param board: Board """ def get_move(self, board): self.logger.info("engine: {} - player:{}".format( self._get_class_name(), self._get_player_name(self.player) )) """ Get possibles player move an apply a Random.shuffle on it (if needed) @param none @return: array """ def get_player_moves(self, board): moves = board.legal_moves() if self.options['randomize_moves'] is True: random.shuffle(moves) return moves """ Get player name based on his number @param player: int @return: string """ def _get_player_name(self, player): return 'White (O)' if self.player == 2 else 'Black (X)' def _show_stats_info(self, depth, nodes, leafs, heuristic): self.logger.info(" -> stats: depth:{:.>2} | node:{:.>6} | leafs:{:.>6} | heuristic:{:.>4}".format( depth, nodes, leafs, heuristic )) def _show_better_move(self, move, heuristic): self.logger.debug(" -> Found a better move: {},{} | heuristic:{}".format( move[1], chr(move[2] + 65), heuristic )) def _get_class_name(self): return self.__class__.__name__ """ Random game engine """ class RandomPlayerEngine(PlayerEngine): """ Get move return a random move based on Board.legal_moves @param player: int @return: array """ def get_move(self, board): super().get_move(board) moves = board.legal_moves() return random.choice(moves) """ Human player engine. """ class HumanPlayerEngine(PlayerEngine): """ Get move return a move based on user input @param board: Board @return: array """ def get_move(self, board): super() move = None while move is None: user_input = input("Please enter player {} move, `print` to display board and `help` possible moves : ".format( self._get_player_name(self.player) )) move = self.validate_input(user_input, board) return move """ Validate user input an verify than the move is possible @param input: string @return: array """ def validate_input(self, input, board): if input == 'print': print("\n{}".format(board.show_board())) return None if input == 'help': text = "Possible move:" for m in board.legal_moves(): text += " {}{}".format(m[1], chr(65+m[2])) print(text) return None if len(input) != 2: self.logger.error("Input coordinate (A1 for example), help or print") return None x = ord(input[0]) - 65 y = int(input[1]) try: if not board.is_valid_move(board._nextPlayer, x, y): self.logger.error("Move is not possible at this place") return None except IndexError: self.logger.error("Invalid input must be [A-J][0-9] (was {})".format(input)) return None return [board._nextPlayer, x, y] """ MinMax player engine """ class MinmaxPlayerEngine(PlayerEngine): """ Get move based on minmax algorithm @param board: Board @return: array """ def get_move(self, board): super().get_move(board) move, score = self._call(board, self.options['depth']) return move """ First part of the minmax algorithm, it get the best player move based on max value @param board: Board @param depth: search depth @return: move and max heuristic """ def _call(self, board, depth): value = -math.inf nodes = 1 leafs = 0 move = [] moves = self.get_player_moves(board) for m in moves: board.push(m) v, n, l = self.checkMinMax(board, False, depth - 1) if v > value: value = v move = m self._show_better_move(move, value) nodes += n leafs += l board.pop() self._show_stats_info(depth, nodes, leafs, value) return move, value """ recursive function to apply minmax @param board: Board @param friend_move: boolean does function maximise (player turn) or not (opponent turn) @param depth: search depth @return: heuristic score, nodes ans leafs processed """ def checkMinMax(self, board, friend_move: bool, depth: int = 2): nodes = 1 leafs = 0 if depth == 0 or board.is_game_over() or self.interrupt_search: leafs +=1 return self.heuristic.get(board, self.player), nodes, leafs if friend_move: value = -math.inf moves = self.get_player_moves(board) for m in moves: board.push(m) v, n, le = self.checkMinMax(board, False, depth - 1) if v > value: value = v nodes += n leafs += le board.pop() else: value = math.inf moves = self.get_player_moves(board) for m in moves: board.push(m) v, n, le = self.checkMinMax(board, True, depth - 1) if v < value: value = v board.pop() nodes += n leafs += le return value, nodes, leafs class AlphabetaPlayerEngine(PlayerEngine): def get_move(self, board): super().get_move(board) move, heuristic = self._call(board, self.options['depth']) return move """ First part of the alphabeta algorithm, it get the best player move based on max value @param board: Board @param depth: search depth @return: move and max heuristic """ def _call(self, board, depth): self.logger.debug("Enter AlphaBeta function") alpha = -math.inf beta = math.inf nodes = 1 leafs = 0 move = [] moves = self.get_player_moves(board) for m in moves: board.push(m) value, n, le = self.checkAlphaBeta(board, False, depth - 1, alpha, beta) board.pop() nodes += n leafs += le if value >= alpha: alpha = value move = m self._show_better_move(move, alpha) self._show_stats_info(depth, nodes, leafs, value) return move, alpha """ recursive function to apply alphabeta @param board: Board @param friend_move: boolean does function maximise (player turn) or not (opponent turn) @param depth: search depth @return: heuristic score, nodes ans leafs processed """ def checkAlphaBeta(self, board, friend_move: bool, depth, alpha, beta): nodes = 1 leafs = 0 if depth == 0 or board.is_game_over() or self.interrupt_search: leafs += 1 return self.heuristic.get(board, self.player), nodes, leafs if friend_move: moves = self.get_player_moves(board) for m in moves: board.push(m) v, n, le = self.checkAlphaBeta(board, False, depth - 1, alpha, beta) board.pop() alpha = max(alpha, v) nodes += n leafs += le if alpha >= beta: return beta, nodes, leafs return alpha, nodes, leafs else: moves = self.get_player_moves(board) for m in moves: board.push(m) v, n, le = self.checkAlphaBeta(board, True, depth - 1, alpha, beta) board.pop() beta = min(beta, v) nodes += n leafs += le if alpha >= beta: return alpha, nodes, leafs return beta, nodes, leafs class MinmaxDeepeningPlayerEngine(MinmaxPlayerEngine): """ Get move based on minmax algorithm with iterative deepening @param board: Board @return: array """ def get_move(self, board): super().get_move(board) self.interrupt_search = False # Get an alarm signal to stop iterations signal.signal(signal.SIGALRM, self.alarm_handler) signal.alarm(self.options['time_limit']) heuristic = -math.inf move = None # We can go deeper than blank place in our board, then we must get # numbers of avaible place max_depth = (board.get_board_size()**2) - ( board.get_nb_pieces()[0] + board.get_nb_pieces()[1]) depth = self.options['depth'] if self.options['depth'] <= max_depth else max_depth # Iterate depth while our alarm does not trigger and there is enougth # avaiable move to play # Iterate depth while our alarm does not trigger and there is enougth # avaiable move to play while not self.interrupt_search and depth <= max_depth: current_move, current_heuristic = self._call(board, depth) # return the current move onli if heuristic is better than previous # iteration if current_heuristic > heuristic: heuristic = current_heuristic move = current_move depth = depth + 1 self.logger.info("Iterative Minmax - depth: {}/{} | heuristic: {}".format( depth - 1, max_depth, heuristic )) return move def alarm_handler(self, signal, frame): self.logger.debug("Raise SIGALMR Signal") self.interrupt_search = True class AlphaBetaDeepeningPlayerEngine(AlphabetaPlayerEngine): """ Get move based on alphabeta algorithm with iterative deepening @param board: Board @return: array """ def get_move(self, board): self.interrupt_search = False # Get an alarm signal to stop iterations signal.signal(signal.SIGALRM, self.alarm_handler) signal.alarm(self.options['time_limit']) heuristic = -math.inf move = None # We can go deeper than blank place in our board, then we must get # numbers of avaible place max_depth = (board.get_board_size()**2) - ( board.get_nb_pieces()[0] + board.get_nb_pieces()[1]) depth = self.options['depth'] if self.options['depth'] <= max_depth else max_depth # Iterate depth while our alarm does not trigger and there is enougth # avaiable move to play while not self.interrupt_search and depth <= max_depth: current_move, current_heuristic = self._call(board, depth) # return the current move only if heuristic is better than previous # iteration can be possible id iteration is stopped by timer if current_heuristic > heuristic: heuristic = current_heuristic move = current_move depth = depth + 1 self.logger.info("Iterative Alphabeta - depth: {}/{} | heuristic: {}".format( depth - 1, max_depth, heuristic )) return move """ define an handler for the alarm signal """ def alarm_handler(self, signal, frame): self.logger.debug("Raise SIGALMR Signal") self.interrupt_search = True