Compare commits

..

No commits in common. "eb29f45de2382d76ccd38accddb562a306df171b" and "437f05c8b2bc0db7020347c71396ebaeb767464d" have entirely different histories.

5 changed files with 123 additions and 280 deletions

View file

@ -34,12 +34,10 @@ Voici la liste des options :
blancs
* `-bd` | `--black-depth-exploration`: niveau d'eploration de l'arbre de jeu
pour le joueur au pions noirs, valable pour les moteurs `minmax` et
`alphabeta`. Utilisé aussi pour définit la profondeur de départ pour
l'*iterative deepening*
`alphabeta`
* `-wd` | `--white-depth-exploration`: niveau d'eploration de l'arbre de jeu
pour le joueur au pions noirs, valable pour les moteurs `minmax` et
`alphabeta`Utilisé aussi pour définit la profondeur de départ pour
l'*iterative deepening*
`alphabeta`
* `-bh` | `--black-heuristic-engine`: moteur heuristique utilisé pour
l'exploration de l'arbre de jeu du joueur noir (valable pour les moteur de
jeu `minmax` et `alphabeta`)
@ -55,8 +53,7 @@ affichée avec l'option `-d`.
## Choix d'implémentation
J'ai avant tout privilégié la personnalisation des différentes paramètres des
différents moteurs composant le jeu. Il,e st ainsi plus aisé de tester le
fonctionnement des différents moteurs.
différents moteurs composant le jeu.
### Classes PlayerEngine
@ -88,19 +85,15 @@ Quatre moteur "joueurs" sont implémentés :
maximale définie;
* `AphaBeta` utilise *AlphaBeta* pour déterminer le coup à jouer avec une
profondeur maximale définie;
* `MinmaxDeepeningMinmax` utilise Minmax avec un temps maximum autorisé;
* `AlphaBetaDeepening` utilise AlphaBeta avec un temps maximum autorisé
* `IterativeDeepeningMinmax` utilise Minmax avec un temps maximum autorisé
Le choix de ces moteur se fait en ligne de commande avec les options évoquées
plus haut.
Le choix de ces moteur se fait en ligne de commande avec
### Classes HeuristicsEngine
Plusieurs classes impémentent plusieurs méthodes pour le calcul de
l'heuristique. Toutes les implémentations se trouvent dans le fichier
`./src/classes/Heuristic.py` Comme nous l'avons vu, les moteurs peuvent être
choisis en ligne de commande et de façon indépendante pour les joueurs blanc et
noir.
l'heuristique. Comme nous l'avons vu, les moteurs peuvent être choisis en ligne
de commande et de façon indépendante pour les joueurs blanc et noir.
Trois implementation sond disponibles:
@ -119,44 +112,9 @@ ordre d'importance :
1. Les coins représentent les parties les plus importantes;
2. Ensuite vient les bords;
3. Et enfin le centre.
Cependant certaines parties du plateau de jeu sont à éviter :
* Les cases autour des coins, car elle laisserai la possibilité au joueur
adverse de placer un de ses pions dans le coin. La case en diagonale du coin
est particulièrement sensible.
* Les lignes juste avant les bords, placer un pion à cet endroit permettrai à
l'adversaire de placer un pion sur le bord. Ce ion sera alors p[lus
difficilement *"capturable"*
3. Et enfin les coins.
Les poids affectés sont personnalisable via l'options `--weight`, par défaut
nous avons -5, 2, 10 et 25.
Voici le tableau des poinds par défaut, il peut être affiché avec l'option
`--show-weights-table`:
```text
./game.py --show-weights-table
Starting PyReverso...
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
--------------------------------------------
0 |25 |-5 |10 | 8 | 8 | 8 | 8 |10 |-5 |25 |
1 |-5 |-7 |-3 |-3 |-3 |-3 |-3 |-3 |-7 |-5 |
2 |10 |-3 | 0 | 0 | 0 | 0 | 0 | 0 |-3 |10 |
3 | 8 |-3 | 0 | 2 | 2 | 2 | 2 | 0 |-3 | 8 |
4 | 8 |-3 | 0 | 2 | 2 | 2 | 2 | 0 |-3 | 8 |
5 | 8 |-3 | 0 | 2 | 2 | 2 | 2 | 0 |-3 | 8 |
6 | 8 |-3 | 0 | 2 | 2 | 2 | 2 | 0 |-3 | 8 |
7 |10 |-3 | 0 | 0 | 0 | 0 | 0 | 0 |-3 |10 |
8 |-5 |-7 |-3 |-3 |-3 |-3 |-3 |-3 |-7 |-5 |
9 |25 |-5 |10 | 8 | 8 | 8 | 8 |10 |-5 |25 |
```
### À savoir:
Les pois utilisé pour les heuristiques sont important.
nous avons 2, 10 et 25.
[reversi]:https://www.coolmathgames.com/blog/how-to-play-reversi-basics-and-best-strategies

View file

@ -8,7 +8,7 @@ class CustomFormatter(logging.Formatter):
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
format = "%(levelname)s: %(message)s"
format = "%(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
FORMATS = {
logging.DEBUG: blue + format + reset,

View file

@ -1,4 +1,4 @@
import random, math, time, signal
import random, math, time
class PlayerEngine:
def __init__(self, player, logger, heuristic, options: dict = {}):
@ -7,7 +7,6 @@ class PlayerEngine:
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
@ -19,15 +18,9 @@ class PlayerEngine:
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)'
return 'White (O)' if player is 2 else 'Black (X)'
class RandomPlayerEngine(PlayerEngine):
def get_move(self, board):
@ -67,25 +60,19 @@ class HumanPlayerEngine(PlayerEngine):
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:
move = ''
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkMinMax(board, False, depth - 1)
v, n, l = self.checkMinMax(board, False, self.options['depth'] - 1)
if v > value:
value = v
move = m
self.logger.debug("\tfound a better move: {} (heuristic:{})".format(
self.logger.debug("found a better move: {} (heuristic:{})".format(
move,
value
))
@ -93,25 +80,23 @@ class MinmaxPlayerEngine(PlayerEngine):
leafs += l
board.pop()
self.logger.info("Tree statistics:\n\tnodes:{}\n\tleafs:{}\n\theuristic:{}".format(
self.logger.debug("Tree statistics:\n\tnodes:{}\n\tleafs:{}".format(
nodes,
leafs,
value
))
return move, value
leafs
))
return move
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:
move = ''
if depth == 0:
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:
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkMinMax(board, False, depth - 1)
if v > value:
@ -122,8 +107,7 @@ class MinmaxPlayerEngine(PlayerEngine):
else:
value = math.inf
moves = self.get_player_moves(board)
for m in moves:
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkMinMax(board, True, depth - 1)
if v < value:
@ -137,148 +121,126 @@ 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:
value = -math.inf
for m in board.legal_moves():
board.push(m)
value, n, l = self.checkAlphaBeta(board, False, depth - 1, alpha, beta)
v, n, l = self.checkAlphaBeta(board, False, self.options['depth'] - 1, alpha, beta)
board.pop()
alpha = max(alpha,v)
nodes += n
leafs += l
if value >= alpha:
alpha = value
if alpha >= value:
value = alpha
move = m
self.logger.debug("\t-> found a better move: {} | heuristic:{})".format(
self.logger.debug("found a better move: {} (heuristic:{})".format(
move,
alpha
))
self.logger.info("Tree statistics:\n\tnodes:{}\n\tleafs:{}\n\theuristic:{}".format(
self.logger.debug("Tree statistics:\n\tnodes:{}\n\tleafs:{}".format(
nodes,
leafs,
alpha
leafs
))
return move, alpha
return move
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:
if depth == 0 :
leafs +=1
return self.heuristic.get(board, self.player), nodes, leafs
if friend_move:
moves = self.get_player_moves(board)
for m in moves:
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(alpha,v)
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:
moves = self.get_player_moves(board)
for m in moves:
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)
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
class MinmaxDeepeningPlayerEngine(MinmaxPlayerEngine):
class MinmaxDeepeningPlayerEngine(PlayerEngine):
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
))
value = -math.inf
nodes = 1
leafs = 0
move = ''
start_time = time.time()
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkMinMax(board, False, start_time)
if v > value:
value = v
move = m
self.logger.debug("found a better move: {} (heuristic:{})".format(
move,
value
))
nodes += n
leafs += l
board.pop()
return move
def alarm_handler(self, signal, frame):
self.logger.debug("Raise SIGALMR Signal")
self.interrupt_search = True
def checkMinMax(self, board, friend_move:bool, start_time):
nodes = 1
leafs = 0
move = ''
if time.time() >= start_time + self.options['time_limit'] or board.is_game_over():
leafs +=1
return self.heuristic.get(board, self.player), nodes, leafs
if friend_move:
value = -math.inf
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkMinMax(board, False, start_time)
if v > value:
value = v
nodes += n
leafs += l
board.pop()
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
else:
value = math.inf
for m in board.legal_moves():
board.push(m)
v, n, l = self.checkMinMax(board, True, start_time)
if v < value:
value = v
board.pop();
nodes += n
leafs += l
return value, nodes, leafs

View file

@ -17,90 +17,42 @@ class ScoreHeuristicEngine(HeuristicEngine):
class WeightHeuristicEngine(HeuristicEngine):
def __init__(self, logger, options):
super().__init__(logger, options)
self.weights = self._get_weight_array()
self.logger.debug("{}".format(self.show_weights()))
def get(self, board, player):
score = self.get_weight(board, player)
return score
def get_weight(self, board, player):
score = 0
size = self.options['size']
w = [[ 0 for _ in range(size)] for _ in range(size)]
for pos_x in range(self.options['size']):
for pos_y in range(self.options['size']):
p = board._board[pos_x][pos_y]
if p == player:
score += self.weights[pos_x][pos_y]
w[pos_x][pos_y] = self.weights[pos_x][pos_y]
elif p != player and p != board._EMPTY:
score -= self.weights[pos_x][pos_y]
w[pos_x][pos_y] = -self.weights[pos_x][pos_y]
size = board.get_board_size()
weights = self._get_weight_array(size)
for pos_x in range(size):
for pos_y in range(size):
if board._board[pos_x][pos_y] == player:
score += weights[pos_x][pos_y]
else:
score -= weights[pos_x][pos_y]
return score
def _get_weight_array(self):
size = self.options['size']
def _get_weight_array(self, size):
w = [[ 0 for _ in range(size)] for _ in range(size)]
padding = size // 5
center = size // 2
full_range = range(self.options['size'])
center_range = range(center - padding, center + padding)
for pos_y in full_range:
for pos_x in full_range:
for pos_y in range(size):
for pos_x in range(size):
# Elements in the corner
if pos_x in [0, size -1] and pos_y in [0,size - 1]:
w[pos_x][pos_y] = self.options['weight'][3]
# corners are a bad place!
elif (pos_x in [0, size - 1] and pos_y in [size - 2, 1]) or \
(pos_x in [1, size -2] and pos_y in [0, size - 1]):
w[pos_x][pos_y] = self.options['weight'][0]
# in diagonale of the corner too
elif pos_x in [size - 2, 1] and pos_y in [size - 2, 1]:
w[pos_x][pos_y] = int(self.options['weight'][0] * 1.5)
elif pos_x in [1,size - 2] and pos_y in range(2, size - 2) or \
pos_y in [1,size - 2] and pos_x in range(2, size - 2) :
w[pos_x][pos_y] = int(self.options['weight'][0] * 0.75)
# center border : cool but not so...
elif (pos_x in center_range and pos_y in [0, size-1]) or \
pos_y in center_range and pos_x in [0, size-1]:
w[pos_x][pos_y] = int(self.options['weight'][2] // 1.25)
w[pos_x][pos_y] = self.options['weight'][2]
# Elements on the border
elif pos_x in [0, size -1] or pos_y in [0, size -1]:
w[pos_x][pos_y] = self.options['weight'][2]
# Element the center
elif pos_x in center_range and pos_y in center_range:
w[pos_x][pos_y] = self.options['weight'][1]
# Element the center
elif pos_x in range( center - padding, center + padding) and pos_y in range(center - padding, center + padding):
w[pos_x][pos_y] = self.options['weight'][0]
return w
def show_weights(self):
display = "\n |"
sep = "\n----"
for x in range(self.options['size']):
display += "{:^3}|".format(x)
sep += '----'
display += sep + "\n"
for x in range(self.options['size']):
display += "{:^3}|".format(str(x))
for y in range(self.options['size']):
display += "{:^3}|".format(self.weights[x][y])
display += "\n"
return display
class FullHeuristicEngine(WeightHeuristicEngine):
def get(self, board, player):

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3
from classes.Reversi import Board
from classes.Engines import RandomPlayerEngine, HumanPlayerEngine, MinmaxPlayerEngine, AlphabetaPlayerEngine, MinmaxDeepeningPlayerEngine, AlphaBetaDeepeningPlayerEngine
from classes.Engines import RandomPlayerEngine, HumanPlayerEngine, MinmaxPlayerEngine, AlphabetaPlayerEngine, MinmaxDeepeningPlayerEngine
from classes.Heuristic import ScoreHeuristicEngine, WeightHeuristicEngine, FullHeuristicEngine
import logging as log
import argparse as arg
@ -12,7 +12,7 @@ from classes.CustomFormater import CustomFormatter
Function to parse command line arguments
"""
def parse_aguments():
engines_choices=['random', 'human', 'minmax', 'alphabeta', 'id_minmax', 'id_alphabeta']
engines_choices=['random', 'human', 'minmax', 'alphabeta', 'id_minmax']
heuristic_choices=['score', 'weight', 'full']
parser = arg.ArgumentParser('Playing Reversi with (virtual) friend')
@ -52,26 +52,11 @@ def parse_aguments():
default='score',
)
parser.add_argument('-br', '--black-randomize-moves',
help='Apply a random function on moves list before explore the game tree - black player',
type=bool,
default=True,
)
parser.add_argument('-wr', '--white-randomize-moves',
help='Apply a random function on moves list before explore the game tree - white player',
type=bool,
default=True,
)
parser.add_argument('--weight',
help='Weight table for weight based heuristic engines',
type=int,
nargs=4,
default=[-5, 2, 10,25]
)
parser.add_argument('--show-weights-table',
help='Display weight table used in \'weight\' and \'full\' heuristic calculation and exit',
action='store_true',
nargs=3,
default=[2,10,25]
)
debug_group = parser.add_mutually_exclusive_group()
@ -94,14 +79,13 @@ if __name__ == '__main__':
"minmax": MinmaxPlayerEngine,
"alphabeta": AlphabetaPlayerEngine,
"id_minmax": MinmaxDeepeningPlayerEngine,
"id_alphabeta": AlphaBetaDeepeningPlayerEngine
}
heuristic_engine = {
"score": ScoreHeuristicEngine,
"weight": WeightHeuristicEngine,
"full": FullHeuristicEngine,
}
print("Starting PyReverso...")
print("Stating PyReverso...")
args = parse_aguments()
logger = log.getLogger()
# Create handler for streaming to tty (stderr / stdout)
@ -109,16 +93,6 @@ if __name__ == '__main__':
tty_handler.setFormatter(CustomFormatter())
logger.addHandler(tty_handler)
# IT shoud be better implemented but no time to make it clean
if args.show_weights_table:
print("{}".format(
heuristic_engine['weight'](logger,{
'weight': args.weight,
'size': 10
}).show_weights()
))
exit(0)
# Activate verbose or debug mode
if args.verbose is True:
logger.setLevel(log.INFO)
@ -127,6 +101,7 @@ if __name__ == '__main__':
if args.debug is True:
logger.setLevel(log.DEBUG)
logger.debug('DEBUG mode activated')
game = Board(10)
logger.debug("Init players engines - black:{} / white:{}".format(
args.black_engine,
@ -138,14 +113,12 @@ if __name__ == '__main__':
logger,
heuristic_engine[args.white_heuristic_engine](
logger, {
'weight': args.weight,
'size': game.get_board_size()
'weight': args.weight
}
),
{
'depth': args.white_depth_exploration,
'time_limit': 10,
'randomize_moves': args.white_randomize_moves
'time_limit': 10
}
)
bplayer = player_engines[args.black_engine](
@ -153,14 +126,12 @@ if __name__ == '__main__':
logger,
heuristic_engine[args.black_heuristic_engine](
logger, {
'weight': args.weight,
'size': game.get_board_size()
'weight': args.weight
}
),
{
'depth': args.black_depth_exploration,
'time_limit': 10,
'randomize_moves': args.black_randomize_moves
'time_limit': 10
}
)
while ( not game.is_game_over()):