project_reversi/src/classes/Engines.py

284 lines
8.9 KiB
Python

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