import random, math, time, signal class PlayerEngine: 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.__class__.__name__, self.options )) def get_move(self, board): self.logger.info("engine: {} - player:{}".format( self.__class__.__name__, self.get_player_name(self.player) )) def get_player_moves(self, board): moves = board.legal_moves() if self.options['randomize_moves'] is True: random.shuffle(moves) return moves @staticmethod def get_player_name(player): return 'White (O)' if player == 2 else 'Black (X)' class RandomPlayerEngine(PlayerEngine): def get_move(self, board): super().get_move(board) moves = board.legal_moves() return random.choice(moves) class HumanPlayerEngine(PlayerEngine): 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 @staticmethod def validate_input(input, board): if input == 'print': print(board.show_board()) return None if input == 'help': print('{}'.format(board.legal_moves())) return None if len(input) != 2: return None x = int(input[0]) y = int(input[1]) if not board.is_valid_move(board._nextPlayer, x, y): return None return [board._nextPlayer, x, y] class MinmaxPlayerEngine(PlayerEngine): def get_move(self, board): super().get_move(board) move, score = self._call(board, self.options['depth']) return move 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.logger.debug("\tfound a better move: {} (heuristic:{})".format( move, value )) nodes += n leafs += l board.pop() self.logger.info("Tree statistics:\n\tnodes:{}\n\tleafs:{}\n\theuristic:{}".format( nodes, leafs, value )) return move, value def checkMinMax(self, board, friend_move:bool, depth :int = 2): nodes = 1 leafs = 0 move = [] 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, l = self.checkMinMax(board, False, depth - 1) if v > value: value = v nodes += n leafs += l board.pop() else: value = math.inf moves = self.get_player_moves(board) for m in moves: board.push(m) v, n, l = self.checkMinMax(board, True, depth - 1) if v < value: value = v board.pop(); nodes += n leafs += l 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 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, l = self.checkAlphaBeta(board, False, depth - 1, alpha, beta) board.pop() nodes += n leafs += l if value >= alpha: alpha = value move = m self.logger.debug("\t-> found a better move: {} | heuristic:{})".format( move, alpha )) self.logger.info("Tree statistics:\n\tnodes:{}\n\tleafs:{}\n\theuristic:{}".format( nodes, leafs, alpha )) return move, alpha 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, l = self.checkAlphaBeta(board, False, depth - 1, alpha, beta) board.pop() alpha = max(alpha,v) nodes += n leafs += l 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, l = self.checkAlphaBeta(board, True, depth - 1, alpha, beta) board.pop(); beta = min(beta, v) nodes += n leafs += l if alpha >= beta: return alpha, nodes, leafs return beta, nodes, leafs class MinmaxDeepeningPlayerEngine(MinmaxPlayerEngine): 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']) depth = self.options['depth'] 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]) # 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: move = current_move depth = depth + 1 self.logger.debug("id_minmax - depth reached: {} | max depth : {}".format( depth - 1, max_depth )) return move def alarm_handler(self, signal, frame): self.logger.debug("Raise SIGALMR Signal") self.interrupt_search = True class AlphaBetaDeepeningPlayerEngine(AlphabetaPlayerEngine): 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']) depth = self.options['depth'] 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]) # 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: move = current_move depth = depth + 1 self.logger.debug("id_minmax - depth reached: {} | max depth : {}".format( depth - 1, max_depth )) return move def alarm_handler(self, signal, frame): self.logger.debug("Raise SIGALMR Signal") self.interrupt_search = True