Compare commits
18 commits
437f05c8b2
...
eb29f45de2
Author | SHA1 | Date | |
---|---|---|---|
eb29f45de2 | |||
59047be8d5 | |||
1e5f87b5af | |||
ebadd02df7 | |||
928cff6400 | |||
b4a2fd7484 | |||
1a670b59b5 | |||
dd70d575d4 | |||
6979749d5d | |||
935d3b8f23 | |||
9c541c90ea | |||
5dbf82521d | |||
46e83a63ce | |||
aa9d13d587 | |||
2ffd077a26 | |||
8074175ad3 | |||
55f2a29084 | |||
9a4febaaf4 |
5 changed files with 280 additions and 123 deletions
60
README.md
60
README.md
|
@ -34,10 +34,12 @@ Voici la liste des options :
|
||||||
blancs
|
blancs
|
||||||
* `-bd` | `--black-depth-exploration`: niveau d'eploration de l'arbre de jeu
|
* `-bd` | `--black-depth-exploration`: niveau d'eploration de l'arbre de jeu
|
||||||
pour le joueur au pions noirs, valable pour les moteurs `minmax` et
|
pour le joueur au pions noirs, valable pour les moteurs `minmax` et
|
||||||
`alphabeta`
|
`alphabeta`. Utilisé aussi pour définit la profondeur de départ pour
|
||||||
|
l'*iterative deepening*
|
||||||
* `-wd` | `--white-depth-exploration`: niveau d'eploration de l'arbre de jeu
|
* `-wd` | `--white-depth-exploration`: niveau d'eploration de l'arbre de jeu
|
||||||
pour le joueur au pions noirs, valable pour les moteurs `minmax` et
|
pour le joueur au pions noirs, valable pour les moteurs `minmax` et
|
||||||
`alphabeta`
|
`alphabeta`Utilisé aussi pour définit la profondeur de départ pour
|
||||||
|
l'*iterative deepening*
|
||||||
* `-bh` | `--black-heuristic-engine`: moteur heuristique utilisé pour
|
* `-bh` | `--black-heuristic-engine`: moteur heuristique utilisé pour
|
||||||
l'exploration de l'arbre de jeu du joueur noir (valable pour les moteur de
|
l'exploration de l'arbre de jeu du joueur noir (valable pour les moteur de
|
||||||
jeu `minmax` et `alphabeta`)
|
jeu `minmax` et `alphabeta`)
|
||||||
|
@ -53,7 +55,8 @@ affichée avec l'option `-d`.
|
||||||
## Choix d'implémentation
|
## Choix d'implémentation
|
||||||
|
|
||||||
J'ai avant tout privilégié la personnalisation des différentes paramètres des
|
J'ai avant tout privilégié la personnalisation des différentes paramètres des
|
||||||
différents moteurs composant le jeu.
|
différents moteurs composant le jeu. Il,e st ainsi plus aisé de tester le
|
||||||
|
fonctionnement des différents moteurs.
|
||||||
|
|
||||||
### Classes PlayerEngine
|
### Classes PlayerEngine
|
||||||
|
|
||||||
|
@ -85,15 +88,19 @@ Quatre moteur "joueurs" sont implémentés :
|
||||||
maximale définie;
|
maximale définie;
|
||||||
* `AphaBeta` utilise *AlphaBeta* pour déterminer le coup à jouer avec une
|
* `AphaBeta` utilise *AlphaBeta* pour déterminer le coup à jouer avec une
|
||||||
profondeur maximale définie;
|
profondeur maximale définie;
|
||||||
* `IterativeDeepeningMinmax` utilise Minmax avec un temps maximum autorisé
|
* `MinmaxDeepeningMinmax` utilise Minmax avec un temps maximum autorisé;
|
||||||
|
* `AlphaBetaDeepening` utilise AlphaBeta avec un temps maximum autorisé
|
||||||
|
|
||||||
Le choix de ces moteur se fait en ligne de commande avec
|
Le choix de ces moteur se fait en ligne de commande avec les options évoquées
|
||||||
|
plus haut.
|
||||||
|
|
||||||
### Classes HeuristicsEngine
|
### Classes HeuristicsEngine
|
||||||
|
|
||||||
Plusieurs classes impémentent plusieurs méthodes pour le calcul de
|
Plusieurs classes impémentent plusieurs méthodes pour le calcul de
|
||||||
l'heuristique. Comme nous l'avons vu, les moteurs peuvent être choisis en ligne
|
l'heuristique. Toutes les implémentations se trouvent dans le fichier
|
||||||
de commande et de façon indépendante pour les joueurs blanc et noir.
|
`./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.
|
||||||
|
|
||||||
Trois implementation sond disponibles:
|
Trois implementation sond disponibles:
|
||||||
|
|
||||||
|
@ -112,9 +119,44 @@ ordre d'importance :
|
||||||
|
|
||||||
1. Les coins représentent les parties les plus importantes;
|
1. Les coins représentent les parties les plus importantes;
|
||||||
2. Ensuite vient les bords;
|
2. Ensuite vient les bords;
|
||||||
3. Et enfin les coins.
|
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"*
|
||||||
|
|
||||||
Les poids affectés sont personnalisable via l'options `--weight`, par défaut
|
Les poids affectés sont personnalisable via l'options `--weight`, par défaut
|
||||||
nous avons 2, 10 et 25.
|
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.
|
||||||
|
|
||||||
[reversi]:https://www.coolmathgames.com/blog/how-to-play-reversi-basics-and-best-strategies
|
[reversi]:https://www.coolmathgames.com/blog/how-to-play-reversi-basics-and-best-strategies
|
||||||
|
|
|
@ -8,7 +8,7 @@ class CustomFormatter(logging.Formatter):
|
||||||
red = "\x1b[31;20m"
|
red = "\x1b[31;20m"
|
||||||
bold_red = "\x1b[31;1m"
|
bold_red = "\x1b[31;1m"
|
||||||
reset = "\x1b[0m"
|
reset = "\x1b[0m"
|
||||||
format = "%(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
|
format = "%(levelname)s: %(message)s"
|
||||||
|
|
||||||
FORMATS = {
|
FORMATS = {
|
||||||
logging.DEBUG: blue + format + reset,
|
logging.DEBUG: blue + format + reset,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import random, math, time
|
import random, math, time, signal
|
||||||
|
|
||||||
class PlayerEngine:
|
class PlayerEngine:
|
||||||
def __init__(self, player, logger, heuristic, options: dict = {}):
|
def __init__(self, player, logger, heuristic, options: dict = {}):
|
||||||
|
@ -7,6 +7,7 @@ class PlayerEngine:
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.heuristic = heuristic
|
self.heuristic = heuristic
|
||||||
self.options = options
|
self.options = options
|
||||||
|
self.interrupt_search = False
|
||||||
self.logger.info("Init engine {}, options:{}".format(
|
self.logger.info("Init engine {}, options:{}".format(
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self.options
|
self.options
|
||||||
|
@ -18,9 +19,15 @@ class PlayerEngine:
|
||||||
self.get_player_name(self.player)
|
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
|
@staticmethod
|
||||||
def get_player_name(player):
|
def get_player_name(player):
|
||||||
return 'White (O)' if player is 2 else 'Black (X)'
|
return 'White (O)' if player == 2 else 'Black (X)'
|
||||||
|
|
||||||
class RandomPlayerEngine(PlayerEngine):
|
class RandomPlayerEngine(PlayerEngine):
|
||||||
def get_move(self, board):
|
def get_move(self, board):
|
||||||
|
@ -60,19 +67,25 @@ class HumanPlayerEngine(PlayerEngine):
|
||||||
return [board._nextPlayer, x, y]
|
return [board._nextPlayer, x, y]
|
||||||
|
|
||||||
class MinmaxPlayerEngine(PlayerEngine):
|
class MinmaxPlayerEngine(PlayerEngine):
|
||||||
|
|
||||||
def get_move(self, board):
|
def get_move(self, board):
|
||||||
super().get_move(board)
|
super().get_move(board)
|
||||||
|
move, score = self._call(board, self.options['depth'])
|
||||||
|
return move
|
||||||
|
|
||||||
|
def _call(self, board, depth):
|
||||||
value = -math.inf
|
value = -math.inf
|
||||||
nodes = 1
|
nodes = 1
|
||||||
leafs = 0
|
leafs = 0
|
||||||
move = ''
|
move = []
|
||||||
for m in board.legal_moves():
|
moves = self.get_player_moves(board)
|
||||||
|
for m in moves:
|
||||||
board.push(m)
|
board.push(m)
|
||||||
v, n, l = self.checkMinMax(board, False, self.options['depth'] - 1)
|
v, n, l = self.checkMinMax(board, False, depth - 1)
|
||||||
if v > value:
|
if v > value:
|
||||||
value = v
|
value = v
|
||||||
move = m
|
move = m
|
||||||
self.logger.debug("found a better move: {} (heuristic:{})".format(
|
self.logger.debug("\tfound a better move: {} (heuristic:{})".format(
|
||||||
move,
|
move,
|
||||||
value
|
value
|
||||||
))
|
))
|
||||||
|
@ -80,23 +93,25 @@ class MinmaxPlayerEngine(PlayerEngine):
|
||||||
leafs += l
|
leafs += l
|
||||||
board.pop()
|
board.pop()
|
||||||
|
|
||||||
self.logger.debug("Tree statistics:\n\tnodes:{}\n\tleafs:{}".format(
|
self.logger.info("Tree statistics:\n\tnodes:{}\n\tleafs:{}\n\theuristic:{}".format(
|
||||||
nodes,
|
nodes,
|
||||||
leafs
|
leafs,
|
||||||
))
|
value
|
||||||
return move
|
))
|
||||||
|
return move, value
|
||||||
|
|
||||||
def checkMinMax(self, board, friend_move:bool, depth :int = 2):
|
def checkMinMax(self, board, friend_move:bool, depth :int = 2):
|
||||||
nodes = 1
|
nodes = 1
|
||||||
leafs = 0
|
leafs = 0
|
||||||
move = ''
|
move = []
|
||||||
if depth == 0:
|
if depth == 0 or board.is_game_over() or self.interrupt_search:
|
||||||
leafs +=1
|
leafs +=1
|
||||||
return self.heuristic.get(board, self.player), nodes, leafs
|
return self.heuristic.get(board, self.player), nodes, leafs
|
||||||
|
|
||||||
if friend_move:
|
if friend_move:
|
||||||
value = -math.inf
|
value = -math.inf
|
||||||
for m in board.legal_moves():
|
moves = self.get_player_moves(board)
|
||||||
|
for m in moves:
|
||||||
board.push(m)
|
board.push(m)
|
||||||
v, n, l = self.checkMinMax(board, False, depth - 1)
|
v, n, l = self.checkMinMax(board, False, depth - 1)
|
||||||
if v > value:
|
if v > value:
|
||||||
|
@ -107,7 +122,8 @@ class MinmaxPlayerEngine(PlayerEngine):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
value = math.inf
|
value = math.inf
|
||||||
for m in board.legal_moves():
|
moves = self.get_player_moves(board)
|
||||||
|
for m in moves:
|
||||||
board.push(m)
|
board.push(m)
|
||||||
v, n, l = self.checkMinMax(board, True, depth - 1)
|
v, n, l = self.checkMinMax(board, True, depth - 1)
|
||||||
if v < value:
|
if v < value:
|
||||||
|
@ -121,126 +137,148 @@ class AlphabetaPlayerEngine(PlayerEngine):
|
||||||
|
|
||||||
def get_move(self, board):
|
def get_move(self, board):
|
||||||
super().get_move(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
|
alpha = -math.inf
|
||||||
beta = math.inf
|
beta = math.inf
|
||||||
nodes = 1
|
nodes = 1
|
||||||
leafs = 0
|
leafs = 0
|
||||||
move = []
|
move = []
|
||||||
value = -math.inf
|
moves = self.get_player_moves(board)
|
||||||
for m in board.legal_moves():
|
for m in moves:
|
||||||
board.push(m)
|
board.push(m)
|
||||||
v, n, l = self.checkAlphaBeta(board, False, self.options['depth'] - 1, alpha, beta)
|
value, n, l = self.checkAlphaBeta(board, False, depth - 1, alpha, beta)
|
||||||
board.pop()
|
board.pop()
|
||||||
alpha = max(alpha,v)
|
|
||||||
nodes += n
|
nodes += n
|
||||||
leafs += l
|
leafs += l
|
||||||
if alpha >= value:
|
if value >= alpha:
|
||||||
value = alpha
|
alpha = value
|
||||||
move = m
|
move = m
|
||||||
self.logger.debug("found a better move: {} (heuristic:{})".format(
|
self.logger.debug("\t-> found a better move: {} | heuristic:{})".format(
|
||||||
move,
|
move,
|
||||||
alpha
|
alpha
|
||||||
))
|
))
|
||||||
|
|
||||||
self.logger.debug("Tree statistics:\n\tnodes:{}\n\tleafs:{}".format(
|
self.logger.info("Tree statistics:\n\tnodes:{}\n\tleafs:{}\n\theuristic:{}".format(
|
||||||
nodes,
|
nodes,
|
||||||
leafs
|
leafs,
|
||||||
|
alpha
|
||||||
))
|
))
|
||||||
return move
|
return move, alpha
|
||||||
|
|
||||||
|
|
||||||
def checkAlphaBeta(self, board, friend_move : bool, depth, alpha, beta):
|
def checkAlphaBeta(self, board, friend_move : bool, depth, alpha, beta):
|
||||||
nodes = 1
|
nodes = 1
|
||||||
leafs = 0
|
leafs = 0
|
||||||
if depth == 0 :
|
if depth == 0 or board.is_game_over() or self.interrupt_search:
|
||||||
leafs +=1
|
leafs +=1
|
||||||
return self.heuristic.get(board, self.player), nodes, leafs
|
return self.heuristic.get(board, self.player), nodes, leafs
|
||||||
|
|
||||||
if friend_move:
|
if friend_move:
|
||||||
value = -math.inf
|
|
||||||
for m in board.legal_moves():
|
moves = self.get_player_moves(board)
|
||||||
|
for m in moves:
|
||||||
board.push(m)
|
board.push(m)
|
||||||
v, n, l = self.checkAlphaBeta(board, False, depth - 1, alpha, beta)
|
v, n, l = self.checkAlphaBeta(board, False, depth - 1, alpha, beta)
|
||||||
board.pop()
|
board.pop()
|
||||||
alpha = max(value,v)
|
alpha = max(alpha,v)
|
||||||
nodes += n
|
nodes += n
|
||||||
leafs += l
|
leafs += l
|
||||||
if alpha >= beta:
|
if alpha >= beta:
|
||||||
self.logger.debug("Alpha pruning - alpha:{} / beta:{}".format(
|
|
||||||
alpha,
|
|
||||||
beta
|
|
||||||
))
|
|
||||||
return beta, nodes, leafs
|
return beta, nodes, leafs
|
||||||
return alpha, nodes, leafs
|
return alpha, nodes, leafs
|
||||||
|
|
||||||
else:
|
else:
|
||||||
value = math.inf
|
moves = self.get_player_moves(board)
|
||||||
for m in board.legal_moves():
|
for m in moves:
|
||||||
board.push(m)
|
board.push(m)
|
||||||
v, n, l = self.checkAlphaBeta(board, True, depth - 1, alpha, beta)
|
v, n, l = self.checkAlphaBeta(board, True, depth - 1, alpha, beta)
|
||||||
board.pop();
|
board.pop();
|
||||||
beta = min(beta,v)
|
beta = min(beta, v)
|
||||||
nodes += n
|
nodes += n
|
||||||
leafs += l
|
leafs += l
|
||||||
if alpha >= beta:
|
if alpha >= beta:
|
||||||
self.logger.debug("Beta pruning - alpha:{} / beta:{}".format(
|
|
||||||
alpha,
|
|
||||||
beta
|
|
||||||
))
|
|
||||||
return alpha, nodes, leafs
|
return alpha, nodes, leafs
|
||||||
return beta, nodes, leafs
|
return beta, nodes, leafs
|
||||||
|
|
||||||
class MinmaxDeepeningPlayerEngine(PlayerEngine):
|
|
||||||
|
class MinmaxDeepeningPlayerEngine(MinmaxPlayerEngine):
|
||||||
|
|
||||||
def get_move(self, board):
|
def get_move(self, board):
|
||||||
super().get_move(board)
|
super().get_move(board)
|
||||||
value = -math.inf
|
self.interrupt_search = False
|
||||||
nodes = 1
|
|
||||||
leafs = 0
|
# Get an alarm signal to stop iterations
|
||||||
move = ''
|
signal.signal(signal.SIGALRM, self.alarm_handler)
|
||||||
start_time = time.time()
|
signal.alarm(self.options['time_limit'])
|
||||||
for m in board.legal_moves():
|
depth = self.options['depth']
|
||||||
board.push(m)
|
heuristic = -math.inf
|
||||||
v, n, l = self.checkMinMax(board, False, start_time)
|
move = None
|
||||||
if v > value:
|
|
||||||
value = v
|
# We can go deeper than blank place in our board, then we must get
|
||||||
move = m
|
# numbers of avaible place
|
||||||
self.logger.debug("found a better move: {} (heuristic:{})".format(
|
max_depth = (board.get_board_size()**2) - (
|
||||||
move,
|
board.get_nb_pieces()[0] + board.get_nb_pieces()[1])
|
||||||
value
|
|
||||||
))
|
# Iterate depth while our alarm does not trigger and there is enougth
|
||||||
nodes += n
|
# avaiable move to play
|
||||||
leafs += l
|
while not self.interrupt_search and depth <= max_depth:
|
||||||
board.pop()
|
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
|
return move
|
||||||
|
|
||||||
def checkMinMax(self, board, friend_move:bool, start_time):
|
def alarm_handler(self, signal, frame):
|
||||||
nodes = 1
|
self.logger.debug("Raise SIGALMR Signal")
|
||||||
leafs = 0
|
self.interrupt_search = True
|
||||||
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()
|
|
||||||
|
|
||||||
else:
|
|
||||||
value = math.inf
|
class AlphaBetaDeepeningPlayerEngine(AlphabetaPlayerEngine):
|
||||||
for m in board.legal_moves():
|
|
||||||
board.push(m)
|
|
||||||
v, n, l = self.checkMinMax(board, True, start_time)
|
def get_move(self, board):
|
||||||
if v < value:
|
self.interrupt_search = False
|
||||||
value = v
|
|
||||||
board.pop();
|
# Get an alarm signal to stop iterations
|
||||||
nodes += n
|
signal.signal(signal.SIGALRM, self.alarm_handler)
|
||||||
leafs += l
|
signal.alarm(self.options['time_limit'])
|
||||||
return value, nodes, leafs
|
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
|
||||||
|
|
|
@ -17,42 +17,90 @@ class ScoreHeuristicEngine(HeuristicEngine):
|
||||||
|
|
||||||
class WeightHeuristicEngine(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):
|
def get(self, board, player):
|
||||||
score = self.get_weight(board, player)
|
score = self.get_weight(board, player)
|
||||||
return score
|
return score
|
||||||
|
|
||||||
def get_weight(self, board, player):
|
def get_weight(self, board, player):
|
||||||
score = 0
|
score = 0
|
||||||
size = board.get_board_size()
|
size = self.options['size']
|
||||||
weights = self._get_weight_array(size)
|
w = [[ 0 for _ in range(size)] for _ in range(size)]
|
||||||
for pos_x in range(size):
|
for pos_x in range(self.options['size']):
|
||||||
for pos_y in range(size):
|
for pos_y in range(self.options['size']):
|
||||||
if board._board[pos_x][pos_y] == player:
|
p = board._board[pos_x][pos_y]
|
||||||
score += weights[pos_x][pos_y]
|
if p == player:
|
||||||
else:
|
score += self.weights[pos_x][pos_y]
|
||||||
score -= 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]
|
||||||
return score
|
return score
|
||||||
|
|
||||||
def _get_weight_array(self, size):
|
def _get_weight_array(self):
|
||||||
|
size = self.options['size']
|
||||||
w = [[ 0 for _ in range(size)] for _ in range(size)]
|
w = [[ 0 for _ in range(size)] for _ in range(size)]
|
||||||
padding = size // 5
|
padding = size // 5
|
||||||
center = size // 2
|
center = size // 2
|
||||||
for pos_y in range(size):
|
full_range = range(self.options['size'])
|
||||||
for pos_x in range(size):
|
center_range = range(center - padding, center + padding)
|
||||||
|
|
||||||
|
for pos_y in full_range:
|
||||||
|
for pos_x in full_range:
|
||||||
|
|
||||||
# Elements in the corner
|
# Elements in the corner
|
||||||
if pos_x in [0, size -1] and pos_y in [0,size - 1]:
|
if pos_x in [0, size -1] and pos_y in [0,size - 1]:
|
||||||
w[pos_x][pos_y] = self.options['weight'][2]
|
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)
|
||||||
|
|
||||||
# Elements on the border
|
# Elements on the border
|
||||||
elif pos_x in [0, size -1] or pos_y in [0, size -1]:
|
elif pos_x in [0, size -1] or pos_y in [0, size -1]:
|
||||||
w[pos_x][pos_y] = self.options['weight'][1]
|
w[pos_x][pos_y] = self.options['weight'][2]
|
||||||
|
|
||||||
# Element the center
|
# Element the center
|
||||||
elif pos_x in range( center - padding, center + padding) and pos_y in range(center - padding, center + padding):
|
elif pos_x in center_range and pos_y in center_range:
|
||||||
w[pos_x][pos_y] = self.options['weight'][0]
|
w[pos_x][pos_y] = self.options['weight'][1]
|
||||||
|
|
||||||
return w
|
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):
|
class FullHeuristicEngine(WeightHeuristicEngine):
|
||||||
|
|
||||||
def get(self, board, player):
|
def get(self, board, player):
|
||||||
|
|
49
src/game.py
49
src/game.py
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from classes.Reversi import Board
|
from classes.Reversi import Board
|
||||||
from classes.Engines import RandomPlayerEngine, HumanPlayerEngine, MinmaxPlayerEngine, AlphabetaPlayerEngine, MinmaxDeepeningPlayerEngine
|
from classes.Engines import RandomPlayerEngine, HumanPlayerEngine, MinmaxPlayerEngine, AlphabetaPlayerEngine, MinmaxDeepeningPlayerEngine, AlphaBetaDeepeningPlayerEngine
|
||||||
from classes.Heuristic import ScoreHeuristicEngine, WeightHeuristicEngine, FullHeuristicEngine
|
from classes.Heuristic import ScoreHeuristicEngine, WeightHeuristicEngine, FullHeuristicEngine
|
||||||
import logging as log
|
import logging as log
|
||||||
import argparse as arg
|
import argparse as arg
|
||||||
|
@ -12,7 +12,7 @@ from classes.CustomFormater import CustomFormatter
|
||||||
Function to parse command line arguments
|
Function to parse command line arguments
|
||||||
"""
|
"""
|
||||||
def parse_aguments():
|
def parse_aguments():
|
||||||
engines_choices=['random', 'human', 'minmax', 'alphabeta', 'id_minmax']
|
engines_choices=['random', 'human', 'minmax', 'alphabeta', 'id_minmax', 'id_alphabeta']
|
||||||
heuristic_choices=['score', 'weight', 'full']
|
heuristic_choices=['score', 'weight', 'full']
|
||||||
parser = arg.ArgumentParser('Playing Reversi with (virtual) friend')
|
parser = arg.ArgumentParser('Playing Reversi with (virtual) friend')
|
||||||
|
|
||||||
|
@ -52,11 +52,26 @@ def parse_aguments():
|
||||||
default='score',
|
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',
|
parser.add_argument('--weight',
|
||||||
help='Weight table for weight based heuristic engines',
|
help='Weight table for weight based heuristic engines',
|
||||||
type=int,
|
type=int,
|
||||||
nargs=3,
|
nargs=4,
|
||||||
default=[2,10,25]
|
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',
|
||||||
)
|
)
|
||||||
|
|
||||||
debug_group = parser.add_mutually_exclusive_group()
|
debug_group = parser.add_mutually_exclusive_group()
|
||||||
|
@ -79,13 +94,14 @@ if __name__ == '__main__':
|
||||||
"minmax": MinmaxPlayerEngine,
|
"minmax": MinmaxPlayerEngine,
|
||||||
"alphabeta": AlphabetaPlayerEngine,
|
"alphabeta": AlphabetaPlayerEngine,
|
||||||
"id_minmax": MinmaxDeepeningPlayerEngine,
|
"id_minmax": MinmaxDeepeningPlayerEngine,
|
||||||
|
"id_alphabeta": AlphaBetaDeepeningPlayerEngine
|
||||||
}
|
}
|
||||||
heuristic_engine = {
|
heuristic_engine = {
|
||||||
"score": ScoreHeuristicEngine,
|
"score": ScoreHeuristicEngine,
|
||||||
"weight": WeightHeuristicEngine,
|
"weight": WeightHeuristicEngine,
|
||||||
"full": FullHeuristicEngine,
|
"full": FullHeuristicEngine,
|
||||||
}
|
}
|
||||||
print("Stating PyReverso...")
|
print("Starting PyReverso...")
|
||||||
args = parse_aguments()
|
args = parse_aguments()
|
||||||
logger = log.getLogger()
|
logger = log.getLogger()
|
||||||
# Create handler for streaming to tty (stderr / stdout)
|
# Create handler for streaming to tty (stderr / stdout)
|
||||||
|
@ -93,6 +109,16 @@ if __name__ == '__main__':
|
||||||
tty_handler.setFormatter(CustomFormatter())
|
tty_handler.setFormatter(CustomFormatter())
|
||||||
logger.addHandler(tty_handler)
|
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
|
# Activate verbose or debug mode
|
||||||
if args.verbose is True:
|
if args.verbose is True:
|
||||||
logger.setLevel(log.INFO)
|
logger.setLevel(log.INFO)
|
||||||
|
@ -101,7 +127,6 @@ if __name__ == '__main__':
|
||||||
if args.debug is True:
|
if args.debug is True:
|
||||||
logger.setLevel(log.DEBUG)
|
logger.setLevel(log.DEBUG)
|
||||||
logger.debug('DEBUG mode activated')
|
logger.debug('DEBUG mode activated')
|
||||||
|
|
||||||
game = Board(10)
|
game = Board(10)
|
||||||
logger.debug("Init players engines - black:{} / white:{}".format(
|
logger.debug("Init players engines - black:{} / white:{}".format(
|
||||||
args.black_engine,
|
args.black_engine,
|
||||||
|
@ -113,12 +138,14 @@ if __name__ == '__main__':
|
||||||
logger,
|
logger,
|
||||||
heuristic_engine[args.white_heuristic_engine](
|
heuristic_engine[args.white_heuristic_engine](
|
||||||
logger, {
|
logger, {
|
||||||
'weight': args.weight
|
'weight': args.weight,
|
||||||
|
'size': game.get_board_size()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
'depth': args.white_depth_exploration,
|
'depth': args.white_depth_exploration,
|
||||||
'time_limit': 10
|
'time_limit': 10,
|
||||||
|
'randomize_moves': args.white_randomize_moves
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
bplayer = player_engines[args.black_engine](
|
bplayer = player_engines[args.black_engine](
|
||||||
|
@ -126,12 +153,14 @@ if __name__ == '__main__':
|
||||||
logger,
|
logger,
|
||||||
heuristic_engine[args.black_heuristic_engine](
|
heuristic_engine[args.black_heuristic_engine](
|
||||||
logger, {
|
logger, {
|
||||||
'weight': args.weight
|
'weight': args.weight,
|
||||||
|
'size': game.get_board_size()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
'depth': args.black_depth_exploration,
|
'depth': args.black_depth_exploration,
|
||||||
'time_limit': 10
|
'time_limit': 10,
|
||||||
|
'randomize_moves': args.black_randomize_moves
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
while ( not game.is_game_over()):
|
while ( not game.is_game_over()):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue