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. ## 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`. ## 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: ```shell # Lancement de base: les deux joueurs jouent avec le moteur aléatoire et les 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 ``` Voyons maintenant quelques paramètres. ### Moteur de jeu 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`. ### Profondeur d'exploration 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'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'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 à savoir *score*, avec *poids*, ou une combinaison *des deux*. Ils ne sont utilisés que pour les moteur de jeu avec exploration de l'arbre de jeu (minmax, alphabeta et leurs pendants avec *iterative deepening*): * `-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`) * `-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`) 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 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. ### Classes PlayerEngine 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): def __init__(self, logger, options): def get_move(self, board): # [...] class MinmaxPlayerEngine(PlayerEngine): def get_move(board): class AlphabetaPlayerEngine(PlayerEngine): def get_move(board): # [...] ``` Quatre moteur "joueurs" sont implémentés : * `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 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é 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 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 implementations sont 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 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 pion sera alors plus 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 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. Voici le tableau des poids 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 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 --- Games: 3 in 67.44769430160522s Black: 0 | ratio: 0.00 | time: 38.074s | score: .......94 | engine: AlphabetaPlayerEngine White: 3 | ratio: 100.00 | time: 29.374s | score: ......206 | engine: MinmaxPlayerEngine Null: 0 | ratio: 0.0 --- Black player options: {'depth': 4, 'time_limit': 10, 'randomize_moves': True} White player options: {'depth': 3, 'time_limit': 10, 'randomize_moves': True} ``` Je me suis servi ce des statistiques pour essayer d'ameliorer mes algorithmes, mais ma machine personnelles étant limitées en terme de performances, chaque analyse statistiques m'a pris beaucoup de temps. ## 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. J'ai aussi testé lutilisation de threads pour tenter d'améliorer les performances de mon implémentation. J'ai d'ailleurs un branche dans mon dépôt *git* mais elle n'est terminée et inutilisable pour le moment. [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