First commit: minmax and alphabeta implemented

This commit is contained in:
Yorick Barbanneau 2023-12-14 21:58:35 +01:00
commit 45abd8c7eb
7 changed files with 593 additions and 0 deletions

View file

@ -0,0 +1,24 @@
import logging
class CustomFormatter(logging.Formatter):
grey = "\x1b[0;35m"
blue = "\x1b[34;20m"
yellow = "\x1b[33;20m"
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
format = "%(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
FORMATS = {
logging.DEBUG: blue + format + reset,
logging.INFO: grey + format + reset,
logging.WARNING: yellow + format + reset,
logging.ERROR: red + format + reset,
logging.CRITICAL: bold_red + format + reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)

187
src/classes/Engines.py Normal file
View file

@ -0,0 +1,187 @@
import random, math
class PlayerEngine:
def __init__(self, logger, options: dict = {}):
# init logger do display informations
self.logger = logger
self.options = options
self.logger.info("Init engine {}".format(self.__class__.__name__))
def get_move(self, board):
self.logger.info("engine: {} - player:{}".format(
self.__class__.__name__,
self.get_player_name(board._nextPlayer)
))
@staticmethod
def get_player_name(player):
return 'White (O)' if player is 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: ".format(
self.get_player_name(board._nextPlayer)
))
move = self.validate_input(user_input, board, player)
print("{}".format(move))
return move
@staticmethod
def validate_input(input, board):
if input == 'print':
print("\n{}".format(board.__str__))
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(int(player), x, y):
return None
return [board._nextPlayer, x, y]
class MinmaxPlayerEngine(PlayerEngine):
def get_move(self, board):
super().get_move(board)
value = -math.inf
nodes = 1
leafs = 0
move = ''
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkMinMax(board, False, self.options['depth'] - 1)
if v > value:
value = v
move = m
self.logger.debug("found a better move: {} (heuristic:{})".format(
move,
value
))
nodes += n
leafs += l
board.pop()
self.logger.debug("Tree statistics:\n\tnodes:{}\n\tleafs:{}".format(
nodes,
leafs
))
return move
def checkMinMax(self, board, friend_move:bool, depth :int = 2):
nodes = 1
leafs = 0
move = ''
if depth == 0:
leafs +=1
return board.heuristique(), nodes, leafs
if friend_move:
value = -math.inf
for m in board.legal_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
for m in board.legal_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)
alpha = -math.inf
beta = math.inf
nodes = 1
leafs = 0
move = []
value = -math.inf
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkAlphaBeta(board, False, self.options['depth'] - 1, alpha, beta)
board.pop()
alpha = max(alpha,v)
nodes += n
leafs += l
if alpha >= value:
value = alpha
move = m
self.logger.debug("found a better move: {} (heuristic:{})".format(
move,
alpha
))
self.logger.debug("Tree statistics:\n\tnodes:{}\n\tleafs:{}".format(
nodes,
leafs
))
return move
def checkAlphaBeta(self, board, friend_move : bool, depth, alpha, beta):
nodes = 1
leafs = 0
if depth == 0 :
leafs +=1
return board.heuristique(), nodes, leafs
if friend_move:
value = -math.inf
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkAlphaBeta(board, False, depth - 1, alpha, beta)
board.pop()
alpha = max(value,v)
nodes += n
leafs += l
if alpha >= beta:
self.logger.debug("Alpha pruning - alpha:{} / beta:{}".format(
alpha,
beta
))
return beta, nodes, leafs
return alpha, nodes, leafs
else:
value = math.inf
for m in board.legal_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:
self.logger.debug("Beta pruning - alpha:{} / beta:{}".format(
alpha,
beta
))
return alpha, nodes, leafs
return beta, nodes, leafs

232
src/classes/Reversi.py Normal file
View file

@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
''' Fichier de règles du Reversi
Certaines parties de ce code sont fortement inspirée de
https://inventwithpython.com/chapter15.html
'''
class Board:
_BLACK = 1
_WHITE = 2
_EMPTY = 0
# Attention, la taille du plateau est donnée en paramètre
def __init__(self, boardsize = 8):
self._nbWHITE = 2
self._nbBLACK = 2
self._nextPlayer = self._BLACK
self._boardsize = boardsize
self._board = []
for x in range(self._boardsize):
self._board.append([self._EMPTY]* self._boardsize)
_middle = int(self._boardsize / 2)
self._board[_middle-1][_middle-1] = self._BLACK
self._board[_middle-1][_middle] = self._WHITE
self._board[_middle][_middle-1] = self._WHITE
self._board[_middle][_middle] = self._BLACK
self._stack= []
self._successivePass = 0
def reset(self):
self.__init__()
# Donne la taille du plateau
def get_board_size(self):
return self._boardsize
# Donne le nombre de pieces de blanc et noir sur le plateau
# sous forme de tuple (blancs, noirs)
# Peut être utilisé si le jeu est terminé pour déterminer le vainqueur
def get_nb_pieces(self):
return (self._nbWHITE, self._nbBLACK)
# Vérifie si player a le droit de jouer en (x,y)
def is_valid_move(self, player, x, y):
if x == -1 and y == -1:
return not self.at_least_one_legal_move(player)
return self.lazyTest_ValidMove(player,x,y)
def _isOnBoard(self,x,y):
return x >= 0 and x < self._boardsize and y >= 0 and y < self._boardsize
# Renvoie la liste des pieces a retourner si le coup est valide
# Sinon renvoie False
# Ce code est très fortement inspiré de https://inventwithpython.com/chapter15.html
# y faire référence dans tous les cas
def testAndBuild_ValidMove(self, player, xstart, ystart):
if self._board[xstart][ystart] != self._EMPTY or not self._isOnBoard(xstart, ystart):
return False
self._board[xstart][ystart] = player # On pourra remettre _EMPTY ensuite
otherPlayer = self._flip(player)
tilesToFlip = [] # Si au moins un coup est valide, on collecte ici toutes les pieces a retourner
for xdirection, ydirection in [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]]:
x, y = xstart, ystart
x += xdirection
y += ydirection
if self._isOnBoard(x, y) and self._board[x][y] == otherPlayer:
# There is a piece belonging to the other player next to our piece.
x += xdirection
y += ydirection
if not self._isOnBoard(x, y):
continue
while self._board[x][y] == otherPlayer:
x += xdirection
y += ydirection
if not self._isOnBoard(x, y): # break out of while loop, then continue in for loop
break
if not self._isOnBoard(x, y):
continue
if self._board[x][y] == player: # We are sure we can at least build this move. Let's collect
while True:
x -= xdirection
y -= ydirection
if x == xstart and y == ystart:
break
tilesToFlip.append([x, y])
self._board[xstart][ystart] = self._EMPTY # restore the empty space
if len(tilesToFlip) == 0: # If no tiles were flipped, this is not a valid move.
return False
return tilesToFlip
# Pareil que ci-dessus mais ne revoie que vrai / faux (permet de tester plus rapidement)
def lazyTest_ValidMove(self, player, xstart, ystart):
if self._board[xstart][ystart] != self._EMPTY or not self._isOnBoard(xstart, ystart):
return False
self._board[xstart][ystart] = player # On pourra remettre _EMPTY ensuite
otherPlayer = self._flip(player)
for xdirection, ydirection in [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]]:
x, y = xstart, ystart
x += xdirection
y += ydirection
if self._isOnBoard(x, y) and self._board[x][y] == otherPlayer:
# There is a piece belonging to the other player next to our piece.
x += xdirection
y += ydirection
if not self._isOnBoard(x, y):
continue
while self._board[x][y] == otherPlayer:
x += xdirection
y += ydirection
if not self._isOnBoard(x, y): # break out of while loop, then continue in for loop
break
if not self._isOnBoard(x, y): # On a au moins
continue
if self._board[x][y] == player: # We are sure we can at least build this move.
self._board[xstart][ystart] = self._EMPTY
return True
self._board[xstart][ystart] = self._EMPTY # restore the empty space
return False
def _flip(self, player):
if player == self._BLACK:
return self._WHITE
return self._BLACK
def is_game_over(self):
if self.at_least_one_legal_move(self._nextPlayer):
return False
if self.at_least_one_legal_move(self._flip(self._nextPlayer)):
return False
return True
def push(self, move):
[player, x, y] = move
assert player == self._nextPlayer
if x==-1 and y==-1: # pass
self._nextPlayer = self._flip(player)
self._stack.append([move, self._successivePass, []])
self._successivePass += 1
return
toflip = self.testAndBuild_ValidMove(player,x,y)
self._stack.append([move, self._successivePass, toflip])
self._successivePass = 0
self._board[x][y] = player
for xf,yf in toflip:
self._board[xf][yf] = self._flip(self._board[xf][yf])
if player == self._BLACK:
self._nbBLACK += 1 + len(toflip)
self._nbWHITE -= len(toflip)
self._nextPlayer = self._WHITE
else:
self._nbWHITE += 1 + len(toflip)
self._nbBLACK -= len(toflip)
self._nextPlayer = self._BLACK
def pop(self):
[move, self._successivePass, toflip] = self._stack.pop()
[player,x,y] = move
self._nextPlayer = player
if len(toflip) == 0: # pass
assert x == -1 and y == -1
return
self._board[x][y] = self._EMPTY
for xf,yf in toflip:
self._board[xf][yf] = self._flip(self._board[xf][yf])
if player == self._BLACK:
self._nbBLACK -= 1 + len(toflip)
self._nbWHITE += len(toflip)
else:
self._nbWHITE -= 1 + len(toflip)
self._nbBLACK += len(toflip)
# Est-ce que on peut au moins jouer un coup ?
# Note: cette info pourrait être codée plus efficacement
def at_least_one_legal_move(self, player):
for x in range(0,self._boardsize):
for y in range(0,self._boardsize):
if self.lazyTest_ValidMove(player, x, y):
return True
return False
# Renvoi la liste des coups possibles
# Note: cette méthode pourrait être codée plus efficacement
def legal_moves(self):
moves = []
for x in range(0,self._boardsize):
for y in range(0,self._boardsize):
if self.lazyTest_ValidMove(self._nextPlayer, x, y):
moves.append([self._nextPlayer,x,y])
if len(moves) is 0:
moves = [[self._nextPlayer, -1, -1]] # We shall pass
return moves
# Exemple d'heuristique tres simple : compte simplement les pieces
def heuristique(self, player=None):
if player is None:
player = self._nextPlayer
if player is self._WHITE:
return self._nbWHITE - self._nbBLACK
return self._nbBLACK - self._nbWHITE
def _piece2str(self, c):
if c==self._WHITE:
return 'O'
elif c==self._BLACK:
return 'X'
else:
return '.'
def __str__(self):
toreturn=""
for l in self._board:
for c in l:
toreturn += self._piece2str(c)
toreturn += "\n"
toreturn += "Next player: " + ("BLACK" if self._nextPlayer == self._BLACK else "WHITE") + "\n"
toreturn += str(self._nbBLACK) + " blacks and " + str(self._nbWHITE) + " whites on board\n"
toreturn += "(successive pass: " + str(self._successivePass) + " )"
return toreturn
__repr__ = __str__