429 lines
12 KiB
Python
429 lines
12 KiB
Python
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
|