From a1812aaedfc15b940fe7a2366f340f0a34b8a8c4 Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Sat, 16 Dec 2023 23:02:36 +0100 Subject: [PATCH 1/5] Implement heuristics classes --- src/classes/Engines.py | 16 ++++--------- src/classes/Heuristic.py | 49 ++++++++++++++++++++++++++++------------ src/game.py | 36 +++++++++++++++++++---------- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/classes/Engines.py b/src/classes/Engines.py index 305a199..e19bd1a 100644 --- a/src/classes/Engines.py +++ b/src/classes/Engines.py @@ -1,10 +1,10 @@ import random, math -from .Heuristic import ReversiHeuristic class PlayerEngine: - def __init__(self, logger, options: dict = {}): + def __init__(self, logger, heuristic, options: dict = {}): # init logger do display informations self.logger = logger + self.heuristic = heuristic self.options = options self.logger.info("Init engine {}, options:{}".format( self.__class__.__name__, @@ -91,11 +91,7 @@ class MinmaxPlayerEngine(PlayerEngine): move = '' if depth == 0: leafs +=1 - if self.options['heuristic'] == 'weight': - score = ReversiHeuristic(board).get() - else: - score = board.heuristique() - return score, nodes, leafs + return self.heuristic.get(board), nodes, leafs if friend_move: value = -math.inf @@ -157,11 +153,7 @@ class AlphabetaPlayerEngine(PlayerEngine): leafs = 0 if depth == 0 : leafs +=1 - if self.options['heuristic'] == 'weight': - score = ReversiHeuristic(self.logger).get(board) - else: - score = board.heuristique() - return score, nodes, leafs + return self.heuristic.get(board), nodes, leafs if friend_move: value = -math.inf diff --git a/src/classes/Heuristic.py b/src/classes/Heuristic.py index 13ae8ee..bb20a70 100644 --- a/src/classes/Heuristic.py +++ b/src/classes/Heuristic.py @@ -1,12 +1,30 @@ -class ReversiHeuristic(): - def __init__(self, logger, weight = [1, 2, 10, 20]): +class HeuristicEngine: + def __init__(self, logger, options): self.logger = logger - self.weight = weight + self.options = options + self.logger.info("Heuristic engine {}, options:{}".format( + self.__class__.__name__, + self.options + )) + + def get(): + raise NotImplementedError + +class ScoreHeuristicEngine(HeuristicEngine): + def get(board): + return board.heuristique() + + +class WeightHeuristicEngine(HeuristicEngine): def get(self, board): - size = board.get_board_size() + score = get_weight(board) + return score + + def get_weight(self, board): score = 0 - weights = self._get_weight(size) + 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] == board._nextPlayer: @@ -14,25 +32,28 @@ class ReversiHeuristic(): else: score -= weights[pos_x][pos_y] return score - - def _get_weight(self, size): - w = [[ 0 for _ in range(size)] for _ in range(size)] + def _get_weight_array(self, size): + w = [[ 0 for _ in range(size)] for _ in range(size)] + padding = size // 5 + center = size // 2 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.weight[3] + w[pos_x][pos_y] = self.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.weight[2] + w[pos_x][pos_y] = self.weight[1] # Element the center - elif pos_x in range( int(size // 2 - 2), size // 2 + 2) and pos_y in range( size // 2 - 2, size // 2 + 2): - w[pos_x][pos_y] = self.weight[1] - - else: + elif pos_x in range( center - padding, center + padding) and pos_y in range(center - padding, center + padding): w[pos_x][pos_y] = self.weight[0] return w + +class FullHeuristicEngine(WeightHeuristicEngine): + + def get(self, board): + return self.get_weight(board) + board.heuristique() diff --git a/src/game.py b/src/game.py index 7db3514..9e6f4dc 100755 --- a/src/game.py +++ b/src/game.py @@ -2,6 +2,7 @@ from classes.Reversi import Board from classes.Engines import RandomPlayerEngine, HumanPlayerEngine, MinmaxPlayerEngine, AlphabetaPlayerEngine +from classes.Heuristic import ScoreHeuristicEngine, WeightHeuristicEngine, FullHeuristicEngine import logging as log import argparse as arg from classes.CustomFormater import CustomFormatter @@ -12,7 +13,7 @@ Function to parse command line arguments """ def parse_aguments(): engines_choices=['random', 'human', 'minmax', 'alphabeta'] - heuristic_choices=['weight',] + heuristic_choices=['score', 'weight', 'full'] parser = arg.ArgumentParser('Playing Reversi with (virtual) friend') parser.add_argument('-we', '--white-engine', @@ -41,13 +42,13 @@ def parse_aguments(): parser.add_argument('-bh', '--black-heuristic-engine', help='Black player heutistic engine', - choices=['board', 'weight'], + choices= heuristic_choices, default='board', ) parser.add_argument('-wh', '--white-heuristic-engine', help='White player heutistic engine', - choices=['board', 'weight'], + choices=heuristic_choices, default='board', ) @@ -65,12 +66,17 @@ def parse_aguments(): Main Function """ if __name__ == '__main__': - engines = { + player_engines = { "random": RandomPlayerEngine, "human": HumanPlayerEngine, "minmax": MinmaxPlayerEngine, "alphabeta": AlphabetaPlayerEngine, } + heuristic_engine = { + "score": ScoreHeuristicEngine, + "weight": ScoreHeuristicEngine, + "full": ScoreHeuristicEngine, + } print("Stating PyReverso...") args = parse_aguments() logger = log.getLogger() @@ -93,14 +99,20 @@ if __name__ == '__main__': args.black_engine, args.white_engine )) - wplayer = engines[args.white_engine](logger, { - 'depth': args.white_depth_exploration, - 'heuristic': args.white_heuristic_engine - }) - bplayer = engines[args.black_engine](logger, { - 'depth': args.black_depth_exploration, - 'heuristic': args.black_heuristic_engine - }) + wplayer = player_engines[args.white_engine]( + logger, + heuristic_engine[args.white_heuristic_engine], + { + 'depth': args.white_depth_exploration, + } + ) + bplayer = player_engines[args.black_engine]( + logger, + heuristic_engine[args.black_heuristic_engine], + { + 'depth': args.black_depth_exploration, + } + ) while ( not game.is_game_over()): if game._nextPlayer == 1: move = bplayer.get_move(game) From 66b10f4deb73c3baf48140551c21e441f267ae7a Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Sat, 16 Dec 2023 23:39:46 +0100 Subject: [PATCH 2/5] Weight can be configurable for weight and full heuristic --- src/classes/Heuristic.py | 8 ++++---- src/game.py | 28 ++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/classes/Heuristic.py b/src/classes/Heuristic.py index bb20a70..cbd82d5 100644 --- a/src/classes/Heuristic.py +++ b/src/classes/Heuristic.py @@ -18,7 +18,7 @@ class ScoreHeuristicEngine(HeuristicEngine): class WeightHeuristicEngine(HeuristicEngine): def get(self, board): - score = get_weight(board) + score = self.get_weight(board) return score def get_weight(self, board): @@ -42,15 +42,15 @@ class WeightHeuristicEngine(HeuristicEngine): # Elements in the corner if pos_x in [0, size -1] and pos_y in [0,size - 1]: - w[pos_x][pos_y] = self.weight[2] + 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.weight[1] + 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.weight[0] + w[pos_x][pos_y] = self.options['weight'][0] return w class FullHeuristicEngine(WeightHeuristicEngine): diff --git a/src/game.py b/src/game.py index 9e6f4dc..d600e5a 100755 --- a/src/game.py +++ b/src/game.py @@ -43,13 +43,20 @@ def parse_aguments(): parser.add_argument('-bh', '--black-heuristic-engine', help='Black player heutistic engine', choices= heuristic_choices, - default='board', + default='score', ) parser.add_argument('-wh', '--white-heuristic-engine', help='White player heutistic engine', choices=heuristic_choices, - default='board', + default='score', + ) + + parser.add_argument('--weight', + help='Weight table for weight based heuristic engines', + type=int, + nargs=3, + default=[2,10,25] ) debug_group = parser.add_mutually_exclusive_group() @@ -74,8 +81,8 @@ if __name__ == '__main__': } heuristic_engine = { "score": ScoreHeuristicEngine, - "weight": ScoreHeuristicEngine, - "full": ScoreHeuristicEngine, + "weight": WeightHeuristicEngine, + "full": FullHeuristicEngine, } print("Stating PyReverso...") args = parse_aguments() @@ -99,16 +106,25 @@ if __name__ == '__main__': args.black_engine, args.white_engine )) + logger.debug("Weight value {}".format( args.weight )) wplayer = player_engines[args.white_engine]( logger, - heuristic_engine[args.white_heuristic_engine], + heuristic_engine[args.white_heuristic_engine]( + logger, { + 'weight': args.weight + } + ), { 'depth': args.white_depth_exploration, } ) bplayer = player_engines[args.black_engine]( logger, - heuristic_engine[args.black_heuristic_engine], + heuristic_engine[args.black_heuristic_engine]( + logger, { + 'weight': args.weight + } + ), { 'depth': args.black_depth_exploration, } From c8447b7a6b05be1b606cf8d8fea790eabc3a34c2 Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Sat, 16 Dec 2023 23:54:56 +0100 Subject: [PATCH 3/5] Player is now a parameter og engines classes --- src/classes/Engines.py | 7 ++++--- src/game.py | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/classes/Engines.py b/src/classes/Engines.py index e19bd1a..7f81a44 100644 --- a/src/classes/Engines.py +++ b/src/classes/Engines.py @@ -1,8 +1,9 @@ import random, math class PlayerEngine: - def __init__(self, logger, heuristic, options: dict = {}): + 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 @@ -14,7 +15,7 @@ class PlayerEngine: def get_move(self, board): self.logger.info("engine: {} - player:{}".format( self.__class__.__name__, - self.get_player_name(board._nextPlayer) + self.get_player_name(self.player) )) @staticmethod @@ -33,7 +34,7 @@ class HumanPlayerEngine(PlayerEngine): 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(board._nextPlayer) + self.get_player_name(self.player) )) move = self.validate_input(user_input, board) return move diff --git a/src/game.py b/src/game.py index d600e5a..09a079d 100755 --- a/src/game.py +++ b/src/game.py @@ -108,6 +108,7 @@ if __name__ == '__main__': )) logger.debug("Weight value {}".format( args.weight )) wplayer = player_engines[args.white_engine]( + game._WHITE, logger, heuristic_engine[args.white_heuristic_engine]( logger, { @@ -119,6 +120,7 @@ if __name__ == '__main__': } ) bplayer = player_engines[args.black_engine]( + game._BLACK, logger, heuristic_engine[args.black_heuristic_engine]( logger, { From a64e318c820843917a728bce09a2d5b0482a0edb Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Sun, 17 Dec 2023 01:18:34 +0100 Subject: [PATCH 4/5] Fix: bad parameters --- src/classes/Engines.py | 4 ++-- src/classes/Heuristic.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/classes/Engines.py b/src/classes/Engines.py index 7f81a44..a3d6f54 100644 --- a/src/classes/Engines.py +++ b/src/classes/Engines.py @@ -92,7 +92,7 @@ class MinmaxPlayerEngine(PlayerEngine): move = '' if depth == 0: leafs +=1 - return self.heuristic.get(board), nodes, leafs + return self.heuristic.get(board, self.player), nodes, leafs if friend_move: value = -math.inf @@ -154,7 +154,7 @@ class AlphabetaPlayerEngine(PlayerEngine): leafs = 0 if depth == 0 : leafs +=1 - return self.heuristic.get(board), nodes, leafs + return self.heuristic.get(board, self.player), nodes, leafs if friend_move: value = -math.inf diff --git a/src/classes/Heuristic.py b/src/classes/Heuristic.py index cbd82d5..d7fa3ea 100644 --- a/src/classes/Heuristic.py +++ b/src/classes/Heuristic.py @@ -11,23 +11,23 @@ class HeuristicEngine: raise NotImplementedError class ScoreHeuristicEngine(HeuristicEngine): - def get(board): - return board.heuristique() + def get(self, board, player): + return board.heuristique(player) class WeightHeuristicEngine(HeuristicEngine): - def get(self, board): - score = self.get_weight(board) + def get(self, board, player): + score = self.get_weight(board, player) return score - def get_weight(self, board): + def get_weight(self, board, player): score = 0 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] == board._nextPlayer: + if board._board[pos_x][pos_y] == player: score += weights[pos_x][pos_y] else: score -= weights[pos_x][pos_y] @@ -55,5 +55,5 @@ class WeightHeuristicEngine(HeuristicEngine): class FullHeuristicEngine(WeightHeuristicEngine): - def get(self, board): - return self.get_weight(board) + board.heuristique() + def get(self, board, player): + return self.get_weight(board, player) + board.heuristique(player) From e506bcdab9b38dd757743676713ab3c442060c10 Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Sun, 17 Dec 2023 01:19:58 +0100 Subject: [PATCH 5/5] Add heuristic documentation --- README.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bcae4fa..6a86f74 100644 --- a/README.md +++ b/README.md @@ -44,16 +44,21 @@ Voici la liste des options : * `-wh` | `--black-heuristic-engine`: moteur heuristique utilisé pour l'exploration de l'arbre de jeu du joueur blanc (valable pour les moteur de jeu `minmax` et `alphabeta`) + * `--weight`: scores utilisés pour le calcul des heuristiques pour les moteurs + `weight` et `full`. L'affichage verbeux est activé avec `-V` et les informations de débogage sont 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. + ### Classes PlayerEngine -Définies dans le fichier `./src/classes/Engines.py`, les classes utilisées h -eritent de la classe de base `PlayerEngines` : +Définies dans le fichier `./src/classes/Engines.py`, les classes utilisées +héritent de la classe de base `PlayerEngines` : ```python class PlayerEngine(Object): @@ -63,8 +68,38 @@ class PlayerEngine(Object): class MinmaxPlayerEngine(PlayerEngine): def get_move(board): -class RandomPlayerEngnine(PlayerEngine): +class RandomPlayerEngine(PlayerEngine): def get_move(board): ``` Il est ainsi plus aisé de tester les moteur dans notre programme de base. + +### Classes HeuristicsEngine + +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 +de commande et de façon indépendante pour les joueurs blanc et noir. + +Trois implementation sond disponibles: + + 1. `ScoreHeuristicEngine`: l'heuristique se sert du score (comptage des pièces + sur le tableau) via la méthode `Board.heuristique`; + 2. `WeightHeuristicEngine`: ici on se sert de la place des pièces sur le + tableau. Chaque emplacement vaut un nombre de points; + 3. `FullHeuristicEngine`: c'est la somme de `Board.heuristique()` et du calcul + des poids. + +#### Retour sur le calcul des poids. + +Afin de définir le poids, je me suis servi de la page *Stratégie et bases de +Reversi* sur le site Cool Math Games ([lien][reversi]). Voici les poids par +ordre d'importance : + + 1. Les coins représentent les parties les plus importantes; + 2. Ensuite vient les bords; + 3. Et enfin les coins. + +Les poids affectés sont personnalisable via l'options `--weight`, par défaut +nous avons 2, 10 et 25. + +[reversi]:https://www.coolmathgames.com/blog/how-to-play-reversi-basics-and-best-strategies