From d846549bb7577d6e93679a584fc12fb5d4ecde16 Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Thu, 21 Dec 2023 22:27:50 +0100 Subject: [PATCH 1/7] Fix error in variable name for AlphaBeta with ID --- src/classes/Engines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Engines.py b/src/classes/Engines.py index ef92c94..ff88784 100644 --- a/src/classes/Engines.py +++ b/src/classes/Engines.py @@ -250,7 +250,7 @@ class AlphabetaPlayerEngine(PlayerEngine): if value >= alpha: alpha = value move = m - self._show_stats_info(move, alpha) + self._show_better_move(move, alpha) self._show_stats_info(depth, nodes, leafs, value) return move, alpha From 507964cd6bd384cfb6296ae3282501023c69978e Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Thu, 21 Dec 2023 23:09:04 +0100 Subject: [PATCH 2/7] Rework board display and human interaction --- src/classes/Engines.py | 26 ++++++++++++++++++-------- src/classes/Reversi.py | 16 +++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/classes/Engines.py b/src/classes/Engines.py index ff88784..bfb5e78 100644 --- a/src/classes/Engines.py +++ b/src/classes/Engines.py @@ -103,7 +103,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(self.player) + self._get_player_name(self.player) )) move = self.validate_input(user_input, board) return move @@ -114,23 +114,33 @@ class HumanPlayerEngine(PlayerEngine): @param input: string @return: array """ - @staticmethod - def validate_input(input, board): + def validate_input(self, input, board): if input == 'print': - print(board.show_board()) + print("\n{}".format(board.show_board())) return None if input == 'help': - print('{}'.format(board.legal_moves())) + text = "Possible move:" + for m in board.legal_moves(): + text += " {}{}".format(chr(65+m[1]), m[2]) + print(text) + return None if len(input) != 2: + self.logger.error("Input coordinate (A1 for example), help or print") return None - x = int(input[0]) + x = ord(input[0]) - 65 y = int(input[1]) - if not board.is_valid_move(board._nextPlayer, x, y): - return None + 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] diff --git a/src/classes/Reversi.py b/src/classes/Reversi.py index 208dbcc..e489efe 100644 --- a/src/classes/Reversi.py +++ b/src/classes/Reversi.py @@ -217,16 +217,18 @@ class Board: return '.' def show_board(self): - display = " |" + display = " |" + sep = "----" for x in range(self.get_board_size()): - display += "{}|".format(str(x)) - display += "\n" + display += " {} |".format(chr(65+x)) + sep += '----' + display += "\n" + sep + "\n" for x in range(self.get_board_size()): - display += "{}|".format(str(x)) + display += " {} |".format(str(x)) for y in range(self.get_board_size()): - display += "{}|".format(self._piece2str(self._board[x][y])) - display += "\n" - return display + display += " {} |".format(self._piece2str(self._board[x][y])) + display += "\n"#+sep+"\n" + return display + sep + '\n' def __str__(self): toreturn="" From f8cf9e6bff287d6f534cdcc020dd1b38ceff5cfc Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Fri, 22 Dec 2023 01:12:44 +0100 Subject: [PATCH 3/7] Fix reset when boardsize is not 8 --- src/classes/Reversi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Reversi.py b/src/classes/Reversi.py index e489efe..1a65e62 100644 --- a/src/classes/Reversi.py +++ b/src/classes/Reversi.py @@ -30,7 +30,7 @@ class Board: self._successivePass = 0 def reset(self): - self.__init__() + self.__init__(self.get_board_size()) # Donne la taille du plateau def get_board_size(self): From 4a8d97c4edc1767eacfa256fa9e7070836c5621e Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Fri, 22 Dec 2023 01:15:12 +0100 Subject: [PATCH 4/7] Move message display coordinate in [A-J][0-9] form --- src/classes/Engines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Engines.py b/src/classes/Engines.py index bfb5e78..cd9bc90 100644 --- a/src/classes/Engines.py +++ b/src/classes/Engines.py @@ -63,7 +63,7 @@ class PlayerEngine: def _show_better_move(self, move, heuristic): self.logger.debug(" -> Found a better move: {},{} | heuristic:{}".format( - move[1],move[2], + chr(move[1] + 65),move[2], heuristic )) From 23468ffe0597388a1fcc2ba5731f00220f00906a Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Fri, 22 Dec 2023 01:16:55 +0100 Subject: [PATCH 5/7] Add recurse game mode And display statistics after --- src/game.py | 86 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 12 deletions(-) diff --git a/src/game.py b/src/game.py index 4285146..58ecafd 100755 --- a/src/game.py +++ b/src/game.py @@ -83,6 +83,12 @@ def parse_aguments(): default=[-5, 2, 10,25] ) + parser.add_argument('-r', '--recursions', + help='Number parties to play', + type=int, + default=1 + ) + parser.add_argument('--show-weights-table', help='Display weight table used in \'weight\' and \'full\' heuristic calculation and exit', action='store_true', @@ -177,16 +183,72 @@ if __name__ == '__main__': 'randomize_moves': args.black_randomize_moves } ) - while ( not game.is_game_over()): - if game._nextPlayer == 1: - move = bplayer.get_move(game) + recursions = args.recursions + parties = [] + while recursions > 0: + while ( not game.is_game_over()): + if game._nextPlayer == 1: + move = bplayer.get_move(game) + else: + move = wplayer.get_move(game) + # Display informations only id we are not in recurse mode + if args.recursions == 1: + print("Player {} move: {},{}".format( + "Black (X)" if move[0] == 2 else "White (O)", + move[1], + move[2] + )) + game.push(move) + + parties.append([recursions, game._nbBLACK, game._nbWHITE]) + score = game._nbBLACK - game._nbWHITE + if score == 0: + winner = "No winner" + elif score > 0: + winner = "Black" else: - move = wplayer.get_move(game) - print("Player {} move: {},{}".format( - "Black (X)" if move[0] == 2 else "White (O)", - move[1], - move[2] - )) - game.push(move) - print("Game end - score black:{}, white:{}\n".format(game._nbBLACK, game._nbWHITE)) - print(game.show_board()) + winner = "White" + + print("\nGAME OVER\n---\nWINNER: {} | black:{} | white:{}".format( + winner, + game._nbBLACK, + game._nbWHITE + )) + print("\n{}".format(game.show_board())) + game.reset() + recursions -= 1 + + # Make somes statistics + if args.recursions > 1: + numbers = len(parties) + black = 0 + white = 0 + null = 0 + for p in parties: + black += 1 if p[1] > p[2] else 0 + white += 1 if p[1] < p[2] else 0 + null += 1 if p[1] == p[2] else 0 + print("Stats\n---") + print("Parties: {}".format(numbers)) + print("Black: {:>2} | ratio: {:>6} | engine: {}".format( + black, + black * 100 / numbers, + bplayer._get_class_name(), + + )) + + print("White: {:>2} | ratio: {:>6} | engine: {}".format( + white, + white * 100 / numbers, + wplayer._get_class_name(), + )) + + print("Null: {:>2} | ratio: {:>6}".format( + null, + bplayer._get_class_name(), + null * 100 / numbers + )) + print("---\nBlack player options: {}\nWhite player options: {}".format( + bplayer.options, wplayer.options + )) + From 9d792187dc63121c484406d062a452dad7f34cd7 Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Fri, 22 Dec 2023 01:55:56 +0100 Subject: [PATCH 6/7] Update documentation --- README.md | 125 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 88 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index abf62f3..72d2802 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,25 @@ IA: jeu de Reversi ------------------ -Le but de ce projet est d'implémenter plusieurs mécanisme de jeu (humain et -intelligence artificielle) pour le jeu de Reversi +Le but de ce projet est d'implémenter plusieurs mécanismes de jeu (humain et +intelligence artificielle) pour le Reversi. ## Installation -Le programme utilise des outils standard de Python installé de base : `random`, -`math`, `argpase` et `logging`. Le project est fourni avec un shell *Nix* dans -le répertoire `src` +Le programme utilise des outils standard de Python installés de base : `random`, +`math`, `argpase` et `logging`. Le projet est fourni avec un shell *Nix* dans +le répertoire `src`. ## Utilisation -le programme propose un emsemble d'options en ligne de commande afin de définir -les options du jeu comme le choix des implementations de jeu (aléatoine, MinMax -etc.) ou encore les paramètres (profondeur de recherche). Une aide est intégrée -au programme via la commande `./game.py -h`. Voici quelques exemple de -lancement: +J'ai choisi de créer un programme en *Python* utilisable depuis un terminal +(testé uniquement sous Linux). + +Le programme propose un ensemble d'options afin de définir les paramètres des +différentes implémentations présentes dans le jeu comme le choix des moteurs de +jeu (aléatoire, MinMax etc.), les paramètres (profondeur de recherche, +temps imparti) etc. Une aide est intégrée au programme via la commande `./game.py +-h`. Voici quelques exemple de lancement: ```shell # Lancement de base: les deux joueurs jouent avec le moteur aléatoire et les @@ -24,38 +27,50 @@ options de base: ./game.py # joueur noir humain et joueur blanc MinMax avec une profondeur de 5 -./game.py -be human -we minmax --white-depth-exploration 5 +./game.py -be human -we minmax -wd 5 ``` -Voici la liste des options : + +Voyons maintenant quelques paramètres. ### Moteur de jeu -Il est possible de définir le moteur de jeu indépedamment pour chaque joueur et -ainsi faire des match: +Il est possible de définir le moteur de jeu indépendamment pour chaque joueur et +ainsi faire des matches: * `-be` | `--black-player-engine`: moteur utilisé par le joueur avec les pions noirs * `-we` | `--white-player-engine`: moteur utilisé par le joueur avec les pions blancs -Le moteur de jeux par défaut est random. +Le moteur de jeux par défaut est `random`. ### Profondeur d'exploration - Il est aussi possible de définir la profindeur d'exploration de l'arbre de jeu + Il est aussi possible de définir la profondeur d'exploration de l'arbre de jeu pour chacun des joueurs: - * `-bd` | `--black-depth-exploration`: niveau d'eploration de l'arbre de jeu + * `-bd` | `--black-depth-exploration`: niveau d'exploration 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* - * `-wd` | `--white-depth-exploration`: niveau d'eploration de l'arbre de jeu - pour le joueur au pions noirs, valable pour les moteurs `minmax` et + * `-wd` | `--white-depth-exploration`: niveau d'exploration de l'arbre de jeu + pour le joueur au pions blancs, valable pour les moteurs `minmax` et `alphabeta`Utilisé aussi pour définit la profondeur de départ pour l'*iterative deepening* La profondeur par défaut est 3. +### Temps d'exploration pour l'*Iterative Deepening* + +Lorsque le choix est fait d'utiliser les algorithmes utilisant l'*iterative +deepening. Il est possible de régler les temps d'exploration indépendamment pour +les deux joueurs: + + * `-bt` | `--black-player-deepening-time`: temps maximum en seconde + d'exploration de l'arbre pour le joueur noir + * `-wt` | `--white-player-deepening-time`: temps maximum en seconde + d'exploration de l'arbre pour le joueur blanc + ### Heuristique Il est possible de choisir entre les 3 moteur de calcul d'heuristique inclus à @@ -75,14 +90,19 @@ Pour l'utilisation des poids, il est possible de les paramétrer : * `--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 +### Debug et mode verbeux + +L'affichage verbeux est activé avec `-V` et les informations de débogages 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. Il,e st ainsi plus aisé de tester le -fonctionnement des différents moteurs. +J'ai avant tout privilégié la personnalisation des paramètres des +différents moteurs composant le jeu. Il est ainsi plus aisé de tester le +fonctionnement suivants différents scénarios. + +Tout est implémenté suivant une logique objet facilitant le développement des +composants et leurs tests. ### Classes PlayerEngine @@ -107,30 +127,31 @@ class AlphabetaPlayerEngine(PlayerEngine): Quatre moteur "joueurs" sont implémentés : - * `Human` pour gérer des joueurs humain, une saisir utilisateur est demandée + * `Human` pour gérer des joueurs humain, une saisie utilisateur est demandée sous la forme ``. Il est aussi possible d'afficher le plateau avec la commande `print` ou les coups possibles avec `help`; - * `Ramdom` va choisir aléatoirement le coup à jouer en fonction des coups; + * `Ramdom` va choisir aléatoirement le coup à jouer en fonction de ceux possibles; * `Minmax` utilise *MinMax* pour déterminer le coup à jouer avec une profondeur 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é + * `MinmaxDeepeningMinmax` utilise Minmax avec un temps maximum autorisé en + itérant sur la profondeur; + * `AlphaBetaDeepening` utilise AlphaBeta avec un temps maximum autorisé. Le choix de ces moteur se fait en ligne de commande avec les options évoquées plus haut. ### Classes HeuristicsEngine -Plusieurs classes impémentent plusieurs méthodes pour le calcul de +Plusieurs classes implé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. -Trois implementation sond disponibles: +Trois implementations sont disponibles: 1. `ScoreHeuristicEngine`: l'heuristique se sert du score (comptage des pièces sur le tableau) via la méthode `Board.heuristique`; @@ -155,17 +176,19 @@ Cependant certaines parties du plateau de jeu sont à éviter : 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 + l'adversaire de placer un pion sur le bord. Ce pion sera alors plus difficilement *"capturable"* Les poids affectés sont personnalisable via l'options `--weight`, par défaut -nous avons -5, 2, 10 et 25. +nous avons `[-5, 2, 10, 25]`. Ces quatre chiffres servent de base pour le +calcul de l'ensemble des poids Une étude autour de l'heuristique de l'Othello menée par Vaishnavi Sannidhanam -et Muthukaruppan Annamalai de l'université de Washingtown propose d'autre piste -pour maéliorer l'heuristique. [télécharger le pdf][etude] +et Muthukaruppan Annamalai de l'université de Washingtown propose d'autre pistes +pour améliorer son calcul. [télécharger le pdf][etude]. Mon calcul des poinds +s'en inspire grandement. -Voici le tableau des poinds par défaut, il peut être affiché avec l'option +Voici le tableau des poids par défaut, il peut être affiché avec l'option `--show-weights-table`: ```text @@ -186,11 +209,39 @@ Starting PyReverso... 9 |25 |-5 |10 | 8 | 8 | 8 | 8 |10 |-5 |25 | ``` - - ### À savoir: -Les pois utilisé pour les heuristiques sont important. +Les poids utilisé pour les heuristiques sont importants, ils ont été trouvés en +effectuant plusieurs tests mais peuvent être améliorés de mon point. + +## mode récursions + +Le programme principal inclus un mode *recursion* permettant l'exécutions de +plusieurs parties les unes à la suite des autres afin de tester les paramètres. Le +paramètre pour la ligne de commande est `-r` | `--recursions` suivi d'un nombre +entier positif. + +À la fin de la passe, un récapitulatif est affiché montrant statistiques, +moteurs utilisés et leurs options: + +```text +Stats +--- +Parties: 10 +Black: 8 | ratio: 80.0 | engine: MinmaxDeepeningPlayerEngine +White: 2 | ratio: 20.0 | engine: AlphaBetaDeepeningPlayerEngine +Null: 0 | ratio: 0.0 +``` +## Pour conclure + +Pour mon implémentation, le moteur **MinMax** avec l'**Iterative Deepening** se +montre plus performant. La logique voudrait que se soit le moteur *AlphaBeta* +avec *Iterative Deepening* le plus performant car il explore l'arbre de jeu +plus en profondeur. C'est d'ailleurs ce qui apparait dans les données affichées +en mode debug (option `-d`). + +Il est donc fort à parier que mon heuristique ne soit pas encore au point. Mais +le temps a manqué pour améliorer ce point. [reversi]:https://www.coolmathgames.com/blog/how-to-play-reversi-basics-and-best-strategies [etude]:https://courses.cs.washington.edu/courses/cse573/04au/Project/mini1/RUSSIA/Final_Paper.pdf From d66cd9975129a77ed210b330193b9db10a81dc9d Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Fri, 22 Dec 2023 01:56:44 +0100 Subject: [PATCH 7/7] Fix display for recuse informations --- src/game.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/game.py b/src/game.py index 58ecafd..ddc45de 100755 --- a/src/game.py +++ b/src/game.py @@ -245,7 +245,6 @@ if __name__ == '__main__': print("Null: {:>2} | ratio: {:>6}".format( null, - bplayer._get_class_name(), null * 100 / numbers )) print("---\nBlack player options: {}\nWhite player options: {}".format(