\documentclass{article} \usepackage[T1]{fontenc} % encodage \renewcommand{\familydefault}{\sfdefault} % police en sans-serif \usepackage[french]{babel} % langue \frenchsetup{SmallCapsFigTabCaptions=false} \usepackage[hidelinks]{hyperref} % liens cliquable dans la table des matières \usepackage{graphicx} % images \usepackage{caption} \usepackage[a4paper, left=20mm, top=20mm]{geometry} % dimensions de la page \usepackage{minted} % intégration code \usemintedstyle{emacs} \title{Projet - IA pour le jeu d'Othello \thanks{\href{https://jj.up8.site/AA/ProjetsAA.pdf}{Sujet 35}}} \author{\href{mailto:anri.kennel@etud.univ-paris8.fr}{Anri Kennel} \thanks{Numéro d'étudiant : 20010664}\, (L3-A) \\Algorithmique avancée $\cdot$ Université Paris 8} \date{Année universitaire 2022-2023} \begin{document} \maketitle \tableofcontents \clearpage \section{Projet} Ce projet présente une implémentation d'un jeu d’Othello ainsi que deux intelligences artificielles jouant; l'une selon un algorithme minimax et l'autre via élagage alpha-bêta. Il y a aussi une comparaison d'efficacité des IA à différentes profondeurs de jeu. \section{Implémentation} \subsection{Othello} \subsubsection{Règles du jeu} L'Othello est un jeu qui se joue sur un plateau de 8x8 où deux couleurs, les noirs et les blancs s'affrontent. Les noirs commencent la partie. Quand aucun joueur ne peut jouer, la partie s'arrête. Au début d'une partie le plateau ressemble à la \autoref{fig:init}. \begin{figure}[h] \centering \includegraphics[width=0.35\textwidth]{imgs/othello_init.png} \caption{Début d'une partie} \label{fig:init} \end{figure} \subsubsection{Exemple d'une partie} Chaque joueur doit poser un pion de sa couleur sur une case vide de l’othellier, il faut prendre en sandwich les pions ennemis, peu importe la direction. Une fois posé, les pions prient en sandwich sont récupérés par le joueur qui vient de jouer. Dans la configuration du début, les cases pouvant être joué par les noirs sont indiqués en rouge dans la \autoref{fig:prem}. \begin{figure}[h] \centering \includegraphics[width=0.35\textwidth]{imgs/othello_premiercoup.png} \caption{Possibilités au premier coup} \label{fig:prem} \end{figure} Mon implémentation du jeu indique au joueur les coups possibles (c'est-à-dire les cases rouges). Le joueur doit indiquer les coordonnées où il veut poser son jeton et les changements se font automatiquement. Chaque joueur joue chacun son tour et passe son tour si aucun coup lui est possible. La partie s'arrête si le plateau est plein ou si aucun joueur ne peut jouer, cf. \autoref{fig:human}. Les jetons blancs sont notés \texttt{B} et les jetons noirs \texttt{N}. \begin{figure}[h] \centering \includegraphics[width=0.35\textwidth]{imgs/othello_impl_player.jpg} \caption{Demande au joueur de jouer} \label{fig:human} \end{figure} Si un coup illégale est joué, le jeu refuse le coup et demande au joueur de choisir un autre coup. \subsubsection{Problèmes rencontrés} Mon enjeu numéro 1 était d'éviter tout problème de mémoire. Pour cela dans le \texttt{Makefile} il y a un label \texttt{dev} qui permet d'ajouter plein de flags pour \texttt{gcc}, notamment \texttt{fanalyzer} et \texttt{fsanitize=undefined} qui permettent de trouver plein de problèmes relatifs à la mémoire. Aussi j'ai utilisé \texttt{Valgrind} (avec les flags \texttt{g} et \texttt{Og} pour \texttt{gcc} et les flags \texttt{leak-check=full}, \texttt{show-leak-kinds=all}, \texttt{track-origins=yes} et \texttt{s} pour \texttt{Valgrind}) me permettant d'avoir un maximum d'avertissements et d'informations me permettant de débugger tous les problèmes. \\\\ Aussi, je voulais rendre mon code modulable, pour tester plus facilement. Ainsi la taille du plateau est complètement modulable, aussi les couleurs des joueurs (cf. \autoref{cod:plateau}). \begin{figure}[h] \centering \begin{minipage}{0.8\textwidth} \begin{minted}[autogobble,linenos]{c} /* Une case est soit vide, soit occupé par un des joueurs, noir ou blanc */ enum CASE { VIDE = ' ', BLANC = 'B', NOIR = 'N' }; \end{minted} \begin{minted}[autogobble,linenos,firstnumber=last]{c} /* Propriété globale du jeu */ enum PLATEAU { LONGEUR = 8, LARGEUR = 8 }; \end{minted} \end{minipage} \caption{Début de \texttt{jeu.h}} \label{cod:plateau} \end{figure} Enfin, sachant qu'il faut aussi implémenter des IAs, j'ai fait en sorte que le jeu du joueur humain ne soit pas complètement lié au fonctionnement du jeu. Le projet est donc séparé en plusieurs fichiers/fonctions (cf. \autoref{tree:project}). \\\\ Grâce à ça, les fonctions relatives au jeu sont indifférentes par rapport à quelle fonction "joueur" est appelée. Autrement dit, tout ce qui est relatif à un joueur humain est dans \texttt{humain.h}. Le fichier \texttt{joueur.h} ne contient rien spécifique à un joueur humain. \begin{figure}[ht] \centering \begin{minipage}{0.15\textwidth} \begin{minted}[autogobble,frame=lines,rulecolor=gray]{mcf} - includes |-- humain.h |-- jeu.h |-- joueur.h |-- liste.h |-- plateau.h \-- utils.h - src |-- humain.c |-- jeu.c |-- joueur.c |-- liste.c |-- main.c |-- plateau.c \-- utils.c \end{minted} \end{minipage} \caption{Arborescence du projet sans l'implémentation des IA et tests} \label{tree:project} \end{figure} \newpage \subsection[Minimax]{Algorithme minimax} \subsubsection{Algorithme} L'idée de l'algorithme minimax est de calculer tous les coups possibles pour chaque coup possible et d'aller le plus loin possible dans les "et si" pour faire le coup qui nous rapportera le plus de points dans le futur. Le problème de cet algorithme c'est qu'il est difficilement réalisable. En mémoire, on doit copier le plateau plusieurs fois (pour chaque "et si"). La complexité est de $\Theta(c^{n})$ avec $c$ les coups légaux et $n$ la profondeur de jeu, donc lorsque l'on utilise cet algorithme on doit garder notre profondeur de test relativement basse. Aussi, l'algorithme est dépendant d'une fonction d'évaluation, c'est elle qui décide si le coup est bien ou non, plus elle est précise plus le coup décidé sera meilleur. \subsubsection{Exemple d'utilisation} L'utilisation de l'algorithme est simple. Il suffit d'appeler une seule fonction et elle jouera un coup (cf. \autoref{cod:minimax_def}). \begin{figure}[h] \centering \begin{minipage}{0.8\textwidth} \begin{minted}[autogobble,linenos]{c} /* Joue le tour d'après l'algorithme minimax */ void action_joueur_minimax(Jeu *jeu, const int couleur, const int profondeur); \end{minted} \begin{minted}[autogobble,linenos,firstnumber=last]{c} /* Auxiliaire : Décide d'une case à jouer via l'algorithme minimax */ void _action_joueur_minimax(Jeu *jeu, int profondeur, const int couleur, const int gagnant, int *ligne, int *colonne, int *score); \end{minted} \end{minipage} \caption{Déclaration de minimax, dans \texttt{minimax.h}} \label{cod:minimax_def} \end{figure} Pour l'utiliser, il faut l'appeler dans la fonction qui s'occupe du déroulement du jeu (cf. \autoref{cod:minimax_play}). \begin{figure}[ht] \centering \begin{minipage}{0.8\textwidth} \begin{minted}[autogobble,linenos]{c} while (!partie_finie(jeu)) { Coups *possibilites = action_possible_joueur(jeu->plateau, tour->couleur); if (possibilites->taille_liste > 0) { // Si le joueur peut jouer if (tour->couleur == NOIR) { // Tour du joueur minimax action_joueur_minimax(jeu, tour->couleur, profondeur); } else { // Tour du joueur humain action_joueur_humain(jeu, tour->couleur, profondeur); } } // On passe la main à l'autre joueur tour = jeu->j1->couleur == tour->couleur ? jeu->j2 : jeu->j1; } \end{minted} \end{minipage} \caption{Extrait simplifié de \texttt{jeu.c}, avec minimax jouant les noirs et un humain les blancs} \label{cod:minimax_play} \end{figure} \newpage \subsubsection{Problèmes rencontrés} Le plus dur était l'implémentation en elle-même et de pas s'emmêler entre les copies de plateau. La fonction \texttt{action\_joueur\_minimax} appelle une fonction auxiliaire \texttt{\_action\_joueur\_minimax} (cf. \autoref{cod:minimax_def}) qui fait implémente directement minimax. Elle se passe en paramètre récursivement : \begin{itemize} \item la ligne choisie \item la colonne choisie \item le meilleur score \item la profondeur actuellement évaluée \item la couleur du joueur (soit MIN, soit MAX) \end{itemize} \subsection[Alpha-Bêta]{Élagage alpha-bêta} \subsubsection{Algorithme} Les coupures $\alpha - \beta$ permettent de réduire la taille en mémoire de minimax en ne calculant pas certains scénarios en retirant ceux inutiles basé sur les calculs précédents. \subsubsection{Exemple d'utilisation} L'utilisation est identique à celle de Minimax (cf. \autoref{cod:alphabeta_def}). \begin{figure}[h] \centering \begin{minipage}{0.8\textwidth} \begin{minted}[autogobble,linenos]{c} /* Joue le tour d'après l'algorithme alpha-beta */ void action_joueur_alphabeta(Jeu *jeu, const int couleur); \end{minted} \begin{minted}[autogobble,linenos,firstnumber=last]{c} /* Auxiliaire : Décide d'une case à jouer via l'algorithme alpha-beta */ void _action_joueur_alphabeta(Jeu *jeu, int profondeur, int couleur, const int gagnant, int *ligne, int *colonne, int *score, const int note, const int qui); // qui => couleur associée \end{minted} \end{minipage} \caption{Déclaration d'alphabeta, dans \texttt{alphabeta.h}} \label{cod:alphabeta_def} \end{figure} \subsubsection{Problèmes rencontrés} Pour l'implémentation je me passe en argument, en plus de ceux de minimax, le meilleur score et sa couleur associée. L'avantage avec cet algorithme est qu'il ressemble beaucoup à minimax vu que c'est une amélioration de ce dernier, alors j'ai pu réutiliser le même code. En revanche l'élagage était compliqué à placer dans le code. Je fais l'élagage en passant des possibilités pour alpha (cf. \autoref{cod:elagage_alpha}), même technique pour bêta (cf. \autoref{cod:elagage_beta}). \begin{figure}[h] \centering \begin{minipage}{0.8\textwidth} \begin{minted}[autogobble,linenos]{c} while (i->suivant) { if (i->suivant->suivant) { i = i->suivant; } else { break; } } *score = INT_MAX; \end{minted} \dots\, et \dots \begin{minted}[autogobble,linenos,firstnumber=last]{c} if (*score == INT_MAX) { while (i->suivant) { if (i->suivant->suivant) { i = i->suivant; } else { break; } } } \end{minted} \end{minipage} \caption{Élagage $\alpha$, dans \texttt{alphabeta.h}} \label{cod:elagage_alpha} \end{figure} \begin{figure}[h] \centering \begin{minipage}{0.8\textwidth} \begin{minted}[autogobble,linenos]{c} while (i->suivant) { if (i->suivant->suivant) { i = i->suivant; } else { break; } } *score = INT_MIN; \end{minted} \dots\, et \dots \begin{minted}[autogobble,linenos,firstnumber=last]{c} if (*score == INT_MIN) { while (i->suivant) { if (i->suivant->suivant) { i = i->suivant; } else { break; } } } \end{minted} \end{minipage} \caption{Élagage $\beta$, dans \texttt{alphabeta.h}} \label{cod:elagage_beta} \end{figure} \section{Comparaison d'efficacité} Le fichier \texttt{text.h} déclare une série de fonctions qui permet de tester minimax et alpha-bêta. Je teste 2 facette des IA, la rapidité d'exécution et le taux de victoire des IA (cf. \autoref{txt:tests_res}). \label{sec:comp_eff} La partie test de rapidité permet de montrer que l'implémentation d'alpha-bêta fonctionne car a profondeur égale, alpha-bêta est beaucoup plus rapide que minimax. Je n'ai d'ailleurs pas testé minimax sur d'aussi grandes profondeurs qu'alpha-bêta car cela prenait trop de temps. Le temps total des tests dure environ 4 minutes sur ma machine. Cette partie montre aussi qu'il n'y a pas grande différence en temps entre minimax et alpha-bêta pour une profondeur inférieur ou égal à 4. La partie sur le taux de victoire permet de montrer qu'à profondeur égale, alpha-bêta et minimax ne se démarquent pas, et que c'est bien la différence de profondeur qui donne l'avantage. En effet, là où minimax prend beaucoup de temps à partir d'une profondeur de 5, alpha-bêta peut aller jusqu'à une profondeur de 8. À noter que lors des tests, je fais jouer l'algorithme contre lui-même donc en réalité, l'algorithme prend 2x moins de temps que le temps indiqué, vu qu'il joue pour les 2 joueurs. En revanche ça ne veut pas dire que l'on peut doubler la profondeur car l'augmentation n'est pas linéaire. \section{Conclusion} Le problème de mon implémentation est la fonction d'évaluation qui regarde juste le nombre de points gagnés/perdus. En ne prenant pas en compte par exemple si le pion placé est sur un coin ou non (le coin étant une position avantageuse). Enfin, je ne teste pas la quantité de mémoire utilisés par les algorithmes. \newpage \appendix \section*{Appendix} \listoffigures \vspace{1cm} \begin{figure}[h] \centering \begin{minipage}{1\textwidth} \begin{minted}[autogobble,linenos,fontsize=\footnotesize]{text} $ ./othello -t \end{minted} \begin{minted}[autogobble,linenos,fontsize=\footnotesize,firstnumber=last]{text} Lancement des tests, on va jusqu'à une profondeur de 8 avec une moyenne de 5 répétitions. On fait jouer alpha-bêta contre lui-même puis minimax contre lui-même. Profondeur de 1 (moyenne de 5 tests) -> alpha-bêta = 0.001742s | minimax = 0.001829s | différence (m - a) = 0.000087s Profondeur de 2 (moyenne de 5 tests) -> alpha-bêta = 0.007442s | minimax = 0.011332s | différence (m - a) = 0.003890s avec le précédent = 0.003803s Profondeur de 3 (moyenne de 5 tests) -> alpha-bêta = 0.029347s | minimax = 0.073701s | différence (m - a) = 0.044354s avec le précédent = 0.040464s Profondeur de 4 (moyenne de 5 tests) -> alpha-bêta = 0.117613s | minimax = 0.344193s | différence (m - a) = 0.226580s avec le précédent = 0.182226s Profondeur de 5 (moyenne de 5 tests) -> alpha-bêta = 0.524146s | minimax = 4.843884s | différence (m - a) = 4.319738s avec le précédent = 4.093158s Profondeur de 6 (moyenne de 5 tests) -> alpha-bêta = 1.507593s Profondeur de 7 (moyenne de 5 tests) -> alpha-bêta = 23.077325s Profondeur de 8 (moyenne de 5 tests) -> alpha-bêta = 13.677932s \end{minted} \begin{minted}[autogobble,linenos,fontsize=\footnotesize,firstnumber=last]{text} Alpha-bêta a gagné (profondeur minimax = 1 vs 1 = profondeur alphabêta) en tant que blanc Minimax a gagné (profondeur minimax = 2 vs 1 = profondeur alphabêta) en tant que noir Minimax a gagné (profondeur minimax = 3 vs 1 = profondeur alphabêta) en tant que noir Minimax a gagné (profondeur minimax = 4 vs 1 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 1 vs 2 = profondeur alphabêta) en tant que noir Minimax a gagné (profondeur minimax = 2 vs 2 = profondeur alphabêta) en tant que blanc Alpha-bêta a gagné (profondeur minimax = 3 vs 2 = profondeur alphabêta) en tant que noir Minimax a gagné (profondeur minimax = 4 vs 2 = profondeur alphabêta) en tant que blanc Alpha-bêta a gagné (profondeur minimax = 1 vs 3 = profondeur alphabêta) en tant que blanc Minimax a gagné (profondeur minimax = 2 vs 3 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 3 vs 3 = profondeur alphabêta) en tant que blanc Minimax a gagné (profondeur minimax = 4 vs 3 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 1 vs 4 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 2 vs 4 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 3 vs 4 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 4 vs 4 = profondeur alphabêta) en tant que noir Minimax a gagné (profondeur minimax = 1 vs 5 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 2 vs 5 = profondeur alphabêta) en tant que blanc Alpha-bêta a gagné (profondeur minimax = 3 vs 5 = profondeur alphabêta) en tant que blanc Minimax a gagné (profondeur minimax = 4 vs 5 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 1 vs 6 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 2 vs 6 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 3 vs 6 = profondeur alphabêta) en tant que noir Alpha-bêta a gagné (profondeur minimax = 4 vs 6 = profondeur alphabêta) en tant que noir Nombre totale de parties : 24. Alpha-bêta a gagné 15 fois. Minimax a gagné 9 fois. Il y a eu 0 égalités. \end{minted} \begin{minted}[autogobble,linenos,fontsize=\footnotesize,firstnumber=last]{text} ________________________________________________________ Executed in 234.85 secs fish external usr time 234.83 secs 62.00 micros 234.83 secs sys time 0.02 secs 105.00 micros 0.02 secs \end{minted} \end{minipage} \caption{Résultat des tests, sous commande \texttt{time} (cf. \autoref{sec:comp_eff})} \label{txt:tests_res} \end{figure} \end{document}