This post was published in french and cover the algorithm related to Connect 4 (Puissance 4).
Ce programme est une ébauche de Puissance 4 puisque l’algorithme d’élagage et les méthodes heuristiques n’ont pas été implementé. De plus, l’environnement graphique est inexistant puisque c’est une application de type console. Ceci permet de nous concentrer sur la méthode.
Notes:
- Le code a été testé sous Windows 98 avec Visual C++ 6.
- Les relations et la structure des classes sont représentées par la notation UML. Ceci étant à titre d’essai, il peut avoir des inexactitudes.
- Dans les explications, les noms possédant une majuscule designent des objets. L’Unité Logique est un objet de type CLogicUnit.
Une structure générale naturelle
Il semble au premier abord que le Puissance 4 soit assimilable en réalité à une console de jeu, style Game Boy. Il s’ensuit que le Puissance 4 a :
- un clavier (keyboard) qui gère les entrées. Il capture le numéro de colonne où le joueur veut jouer, les demandes d’informations, de remise à zéro du plateau, de la mise en route de l’IA,…
- une unité d’affichage (display). L’unité affiche le plateau, des informations, avertit quand un des joueurs a gagné ;
- une unité logique (logic unit). Le “ cœur du Puissance 4 “ qui gère les entrées et les sorties et calcule les meilleurs coup.
Le tout est inclus dans une “ boite “ que l’on modélisera par CJeu. Bref, ceci reste classique. On obtient ainsi :
Par l’intermédiaire de CKeyboard::GetKey(), l’objet Keyboard redirige les entrées claviers vers l’Unité Logique qui va traiter ces messages par des fonctions différentes :CLogicUnit:: GiveUp()si un des joueurs veut abandonner, CLogicUnit::SetIA() si un joueur veut jouer contre l’ordinateur… Les fonctions Keyboard devront être réécrites en cas de portage sous Windows, mais l’interface restera la même.
A propos de portage futur, la classe CDisplay a été dérivée pour donner des classes CDisplayTest et CDisplayWindow. CDisplay est une classe d’interface (toutes ses fonctions membres sont virtuelles). Seul dans cette version les fonctions membres de CDisplayTest ont été implantées, puisque le programme tourne uniquement en mode console.
On utilise CDisplayTest par :
m_pDisplay=new CDisplayTest(this);
dans le constructeur de CJeu.
On allume le jeu Puissance en appelant dans main()
, CJeu::Run()
.
Détection des alignements
Avant de poursuivre plus en amont, il faut s’intéresser à la détection des alignements. Cette détection est essentielle puisque :
- elle indique quel joueur va gagner ;
- elle va conditionner la méthode d’évaluation.
Qu’est ce que l’évaluation ?
C’est l’évaluation d’un plateau donné. Le plateau est constitué d’une grille qui contient des pions de couleur BLEU, ROUGE ou ne contient rien (RIEN). C’est pourquoi il faut définir une classe CPlateau
. L’objet LogicUnit
instancie une classe CPlateau
lors de sa construction. Ce Plateau contient une Grille m_Grille[LARGEUR][HAUTEUR]
. Au départ, m_Grille
est initialisé à RIEN. Donc pour évaluer un Plateau possédant une grille donnée du point de vu de joueur, on déclare une fonction Evaluation
:
CPlateau : : Evaluation(color_joueur joueur)
.
Le principe de l ‘évaluation est assez simple en théorie : il faut évaluer séparément chaque alignement qu’ils soient de longueur 2, 3 ou 4 et sommer leurs scores pour obtenir le score du plateau. D’où l’idée de définir une classe alignement que l’on nommera CVecteur
. Pourquoi CVecteur
? Et bien , en analogie à un … vecteur puisque un alignement peut être représenté par les données suivantes :
-
m_X
, l’abscisse du point base -
m_Y
, l’ordonnée du point base -
m_Longueur
, la longueur du vecteur (=2,3 ou 4) -
m_Dir
, la direction du vecteur -
couleur
, la couleur du vecteur (BLEU ou ROUGE)
Chaque vecteur pointe par rapport à une direction cardinale. En prenant pour origine le point base de coordonnée (m_X
, m_Y
), le vecteur peut pointer dans 8 directions possibles. Mais par un mécanisme de détection des vecteurs particulière, le vecteur ne peut pointer que vers le Nord-Est (m_Dir=1
), l’Est (m_Dir=2
), le Sud-Est (m_Dir=3
) et le Sud (m_Dir=4
).
Voici un plateau représentant une sitaution donnée.
On a :
m_X |
m_Y |
m_Longueur |
m_Dir |
Couleur |
---|---|---|---|---|
1 | 1 | 2 | 4 | BLEU |
1 | 0 | 3 | 1 | BLEU |
2 | 0 | 2 | 1 | ROUGE |
2 | 0 | 2 | 2 | ROUGE |
3 | 1 | 2 | 4 | ROUGE |
2 | 2 | 2 | 3 | ROUGE |
7 | 2 | 2 | 4 | ROUGE |
6 | 0 | 2 | 2 | BLEU |
Ainsi, Plateau
gère une liste de Vecteur par l’intermédiaire d’un conteneur séquentiel de la STL : vector
. Ainsi on a :
Ce n’est pur coïncidence que le conteneur et la classe representant un alignement portent (presque) le même nom.
valeur du Plateau=somme des valeurs des Vecteurs contenus dans le Plateau
Un objet ListVecteur
est construit dans le constructeur de LogicUnit
. Grâce à la fonction CPlayeau::Redetection()
, on remplit la ListVecteur
. Ainsi on peut détecter si un des joueurs a gagné : on cherche dans ListVecteur un vecteur dont la donnée membre m_Longueur
est égale à 4. On dipose aussi de la pierre angulaire pour l’évaluation d’une situation.
Evaluation des Vecteurs
La première idée est d’attribuer un score dépendant uniquement de la longueur du vecteur. Un vecteur 3 a un meilleur score qu’un vecteur 2. Mais c’est un peu plus complexe que cela. Que penser de ces divers cas ?
Le vecteur de longueur 3 vaut 0, il n’a aucun intéret puisque le joueur ROUGE ne pourra pas gagner avec ce vecteur.
Ce vecteur de longueur 2 vaut à priori autant qu’un vecteur de longueur 3.
Et celui-çi?
Environement des Vecteurs
On s’aperçoit que la valeur d’un vecteur dépend de sa longueur et de son environnement. L’environnement d’un vecteur est variable suivant la longueur du vecteur. Pour un vecteur 2, on a :
On considère qu’il ya deux environnements (droit et gauche) chacun constitués de 4 cases. Dans le schéma précédant, on oriente le schéma suivant la direction du vecteur (voir l’utilité de m_UX
et m_UY
données membres de CVecteur
) Cet environnement influe sur le score du vecteur.
L’environement d’un vecteur 3 est ainsi le suivant :
Chaque environement d’un Vecteur 3 est constitué de deux cases.
Pourquoi ne pas choisir des environenements plus grand ? En fait les deux environements des vecteurs définissent une zone d’extension maximum dans le cas où le vecteur se transformerait en coup gagnant (Vecteur de longueur 4). Le choix de définir une extension d’environement se situant sous les vecteurs (cases 3 et 4 pour un Vecteur 2 et case 2 pour un Vecteur 3) est motivé par le fait qu’il faut que ces cases soient remplies en partie pour que le Vecteur se transforme en coup gagnant. Je rappelle qu’un pion ne peut léviter…
Face à ces observations, on va définir une classe CEnvironementVecteur que l’on dérivera en CEnvironementVecteur2
et CEnvironementVecteur3
. Un Vecteur a deux Environement, celui de droite et celui de gauche. Les données membres des Environements sont des tableaux d’entiers prenant comme valeur :
- 0 pour dire que la case X est vide
- 1 pour dire que la case X est de la même couleur que le vecteur associé
- 2 pour dire que la case X est de la couleur opposée au vecteur associé
Evaluation des Environenements
Chaque Environement est lié à un autre objet, il s’agit de CParametre
. Un objet Parametre est une partie intégrante de l’Unité Logique. Il stocke les paramètres associés aux cases Environement.
Combien faut-il de paramètres pour carractériser un environement d’un vecteur 2 ? Il en faut 12 théoriquement. En effet, 3 paramètres (pour RIEN, COULEUR_VECTEUR, COULEUR_INVERSE_VECTEUR) pour chaque case d’environement(case 1, 2 ,3 et 4). 4*3=12! Il est clair que certaines paramètres sont inutiles, puisque attribuer un paramètre à la case 1 lorsqu’il y a COULEUR_VECTEUR ne sert à rien puisqu’on suppose que la méthode de détection marche.
Le but de la définition des classes CParametre
et CEnvironement
est de parvenir à évaluer les Environements des Vecteurs. Rappelez-vous que chaque vecteur a un Environement droit et un Environement gauche. Ainsi,
valeur du vecteur=valeur de son environement droit + valeur de son environement gauche
Cette dernière équation n’est pas valide dans le cas d’un Vecteur de direction 4 (il pointe alors vers le Sud). Dans ce cas, on évalura seulement un environnement constitué d’une seule case : la case qui est au dessus de l’extrémité haute du vecteur.
Je vais expliquer comment on évalue un CEnvironement2, la méthode sera la même pour un CEnvironement3 (voir le code source).
Un Environement ne vaut rien si un pion adverse est dans la Case 1, donc m_Parametre[0][2]=0
. Dans m_Parametre[x][y]
, x
represente la case x+1, y=0 quand la case=RIEN, y=1 (COULEUR_VECTEUR), y=2 (COULEUR_INVERSE_VECTEUR).
L’Environement 2 vaut autant qu’un Environement 3 si il y a un pion de COULEUR_VECTEUR dans la case 2 et RIEN dans la case 1.
Sa valeur augmente si l’on sait que un pion pourra être mis dans une case 1 et 2, c’est à dire si il y a un pion dans les cases 3 et 4. Ainsi m_Parametre[2][1] = m_Parametre[2][2]
. De même, m_Parametre[3][1] = m_Parametre[3][2]
. On peut affirmer que m_Parametre[2][1] > m_Parametre[3][1]
.
Ainsi par construction et en séparant des cas divers, on arrive à obtenir le score d’un Environement. Ce score pourra être modulé par l’objet Parametre, en faisant varier des coefficients. C’est dans la pratique que ces coefficients seront modifiés, mais le bon sens reste le principal facteur de choix des ces paramètres.
Synthèse
Après l’implémentation de CVecteur::GetScore()
, on a :
Il ne reste plus qu’à sommer les vecteurs pour trouver le score d’un Plateau. Ceci donne :
Qu’est-ce qu’il reste à faire ? On sait évaluer un Plateau, il reste à implémenter l’algorithme du Mini-Max.
La méthode de détection dans le code source n’a été implementé que partiellement. Seul la fonction CPlateau::Redetection()
est fiable, mais lente.
Ceci nous donne l’organisation générale suivante :
Implémentation de l’algorithme Mini-Max
Le reste est classique et ne devrait pas poser de problème. On définit une classe CArbre
à laquelle on associe un membre Plataeu qui est l’exacte réplique du Plateau qui sera la situation “racine” (voir l’algorithme du Puissance 4).
On calcule le meilleur coup par une fonction récurrente.