diff --git a/README.md b/README.md index 72d2802..abf62f3 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,22 @@ IA: jeu de Reversi ------------------ -Le but de ce projet est d'implémenter plusieurs mécanismes de jeu (humain et -intelligence artificielle) pour le Reversi. +Le but de ce projet est d'implémenter plusieurs mécanisme de jeu (humain et +intelligence artificielle) pour le jeu de Reversi ## Installation -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`. +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` ## Utilisation -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: +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: ```shell # Lancement de base: les deux joueurs jouent avec le moteur aléatoire et les @@ -27,50 +24,38 @@ options de base: ./game.py # joueur noir humain et joueur blanc MinMax avec une profondeur de 5 -./game.py -be human -we minmax -wd 5 +./game.py -be human -we minmax --white-depth-exploration 5 ``` - -Voyons maintenant quelques paramètres. +Voici la liste des options : ### Moteur de jeu -Il est possible de définir le moteur de jeu indépendamment pour chaque joueur et -ainsi faire des matches: +Il est possible de définir le moteur de jeu indépedamment pour chaque joueur et +ainsi faire des match: * `-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 profondeur d'exploration de l'arbre de jeu + Il est aussi possible de définir la profindeur d'exploration de l'arbre de jeu pour chacun des joueurs: - * `-bd` | `--black-depth-exploration`: niveau d'exploration 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 `alphabeta`. Utilisé aussi pour définit la profondeur de départ pour l'*iterative deepening* - * `-wd` | `--white-depth-exploration`: niveau d'exploration de l'arbre de jeu - pour le joueur au pions blancs, valable pour les moteurs `minmax` et + * `-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* 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 à @@ -90,19 +75,14 @@ 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`. -### Debug et mode verbeux - -L'affichage verbeux est activé avec `-V` et les informations de débogages sont +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 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. +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. ### Classes PlayerEngine @@ -127,31 +107,30 @@ class AlphabetaPlayerEngine(PlayerEngine): Quatre moteur "joueurs" sont implémentés : - * `Human` pour gérer des joueurs humain, une saisie utilisateur est demandée + * `Human` pour gérer des joueurs humain, une saisir 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 de ceux + * `Ramdom` va choisir aléatoirement le coup à jouer en fonction des coups; 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é en - itérant sur la profondeur; - * `AlphaBetaDeepening` utilise AlphaBeta 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 les options évoquées plus haut. ### Classes HeuristicsEngine -Plusieurs classes implémentent plusieurs méthodes pour le calcul de +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. -Trois implementations sont disponibles: +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`; @@ -176,19 +155,17 @@ 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 pion sera alors plus + 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 -nous avons `[-5, 2, 10, 25]`. Ces quatre chiffres servent de base pour le -calcul de l'ensemble des poids +nous avons -5, 2, 10 et 25. 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 pistes -pour améliorer son calcul. [télécharger le pdf][etude]. Mon calcul des poinds -s'en inspire grandement. +et Muthukaruppan Annamalai de l'université de Washingtown propose d'autre piste +pour maéliorer l'heuristique. [télécharger le pdf][etude] -Voici le tableau des poids par défaut, il peut être affiché avec l'option +Voici le tableau des poinds par défaut, il peut être affiché avec l'option `--show-weights-table`: ```text @@ -209,39 +186,11 @@ Starting PyReverso... 9 |25 |-5 |10 | 8 | 8 | 8 | 8 |10 |-5 |25 | ``` + + ### À savoir: -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. +Les pois utilisé pour les heuristiques sont important. [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 diff --git a/src/classes/Engines.py b/src/classes/Engines.py index cd9bc90..ef92c94 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( - chr(move[1] + 65),move[2], + move[1],move[2], heuristic )) @@ -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,33 +114,23 @@ class HumanPlayerEngine(PlayerEngine): @param input: string @return: array """ - def validate_input(self, input, board): + @staticmethod + def validate_input(input, board): if input == 'print': - print("\n{}".format(board.show_board())) + print(board.show_board()) return None if input == 'help': - text = "Possible move:" - for m in board.legal_moves(): - text += " {}{}".format(chr(65+m[1]), m[2]) - print(text) - + print('{}'.format(board.legal_moves())) return None if len(input) != 2: - self.logger.error("Input coordinate (A1 for example), help or print") return None - x = ord(input[0]) - 65 + x = int(input[0]) y = int(input[1]) - 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 - + if not board.is_valid_move(board._nextPlayer, x, y): + return None return [board._nextPlayer, x, y] @@ -260,7 +250,7 @@ class AlphabetaPlayerEngine(PlayerEngine): if value >= alpha: alpha = value move = m - self._show_better_move(move, alpha) + self._show_stats_info(move, alpha) self._show_stats_info(depth, nodes, leafs, value) return move, alpha diff --git a/src/classes/Reversi.py b/src/classes/Reversi.py index 1a65e62..208dbcc 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.get_board_size()) + self.__init__() # Donne la taille du plateau def get_board_size(self): @@ -217,18 +217,16 @@ class Board: return '.' def show_board(self): - display = " |" - sep = "----" + display = " |" for x in range(self.get_board_size()): - display += " {} |".format(chr(65+x)) - sep += '----' - display += "\n" + sep + "\n" + display += "{}|".format(str(x)) + display += "\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"#+sep+"\n" - return display + sep + '\n' + display += "{}|".format(self._piece2str(self._board[x][y])) + display += "\n" + return display def __str__(self): toreturn="" diff --git a/src/game.py b/src/game.py index ddc45de..4285146 100755 --- a/src/game.py +++ b/src/game.py @@ -83,12 +83,6 @@ 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', @@ -183,71 +177,16 @@ if __name__ == '__main__': 'randomize_moves': args.black_randomize_moves } ) - 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" + while ( not game.is_game_over()): + if game._nextPlayer == 1: + move = bplayer.get_move(game) else: - 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, - null * 100 / numbers - )) - print("---\nBlack player options: {}\nWhite player options: {}".format( - bplayer.options, wplayer.options - )) - + 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())