I. Introduction▲
Bienvenue dans la troisième partie du tutoriel sur le jeu du morpion. Nous allons voir comment ajouter un peu de logique de jeu et comment intégrer une remise à zéro d'un jeu (sans devoir relancer le programme !).
À la fin de la deuxième partie de ce tutoriel, nous avions un programme qui affichait un quadrillage de morpion (image de fond). Lorsque nous cliquons dans la fenêtre, le programme est capable d'afficher les ronds et les croix des deux joueurs.
Voyons maintenant comment ajouter un peu de logique de jeu et rendre ce morpion un peu plus correct.
I-A. Présentation de cette partie▲
Dans cette troisième partie, nous allons ajouter les règles associées au morpion. Il faudra donc pouvoir vérifier si le jeu est fini et pouvoir déterminer qui a gagné (ou s'il y a un match nul).
Nous allons compléter le code du deuxième tutoriel. Voici le lien vers ce code: zip (105 Ko).
I-B. Qu'est-ce que la logique de jeu?▲
J'appelle « logique de jeu » toutes les règles du jeu. Par exemple, pour un morpion, si trois ronds forment une ligne alors les ronds gagnent. Ceci fait partie de la logique du jeu. Donc dans la logique du jeu, nous devons pouvoir savoir si le jeu est fini, qui a gagné (ou déterminer qu'il y a un match nul).
Graphiquement, nous traduirons cela par l'affichage d'une autre surface pour montrer à l'utilisateur que quelqu'un a gagné.
II. Le programme▲
Aucun fichier ne sera ajouté par rapport à la deuxième version du programme. De plus, le fait d'avoir séparé correctement le lancement du programme et la boucle générale de la gestion du morpion, aucun changement ne sera apporté aux fichiers Main.cpp, Main.h et Define.h. Regardons donc les changements des fichiers Jeu.h et Jeu.cpp.
II-A. Modification du fichier Jeu.h▲
Nous allons modifier l'énumération Case pour ajouter un type Gagne et ajouter quelques fonctions dans la classe Jeu pour gérer les règles du morpion. Ce qui suit est une présentation plus détaillée.
II-A-1. Changement de l'énumération Case▲
Dans le fichier Jeu.h, nous avons ajouté un élément dans l'énumération des types de cases pour avoir le type Gagne. Ce nouveau type servira pour pouvoir afficher une surface différente pour les cases qui forment une ligne.
enum
Case {
Vide=
0
,
Rond,
Croix,
Gagne
}
;
II-A-2. Modification des membres de la classe Jeu▲
Ensuite, nous ajoutons deux surfaces pour pouvoir dessiner les cases Gagne pour les ronds et les croix. Nous ajoutons aussi une variable typegagne de type Case pour nous donner la personne qui a gagné (s'il y en a une). Finalement, nous avons un booléen fini qui nous dit si le jeu est fini.
//Surfaces d'un rond, d'une croix, d'un fond et les surfaces gagnées
SDL_Surface *
o, *
x, *
bg, *
gagneo, *
gagnex;
//Variable pour un tour et pour savoir qui a gagné
Case tour, typegagne;
//Booléen pour savoir si la partie est terminée
bool
fini;
N'est-ce pas redondant d'avoir un booléen fini et une variable typegagne ?
On pourrait par exemple dire: Si typegagne != Vide alors quelqu'un a gagné donc le jeu est fini. Mais alors, comment définir le match nul ?
On pourrait répondre que si typegagne == Gagne, alors c'est une égalité…
Je n'aime pas ce genre de chose. Il est bon de séparer (tant qu'on a le luxe d'avoir assez de mémoire) les choses correctement. Nous avons donc un booléen qui nous informe si le jeu est fini et une autre variable pour savoir qui a gagné (ou match nul).
Alors la convention prise pour ce jeu est de dire :
- Rond a gagné si (fini==true) && (typegagne==Rond) ;
- Croix a gagné si (fini==true) && (typegagne==Croix) ;
- Égalité si (fini==true) && (typegagne==Vide) ;
- Pas encore fini si (fini==false).
II-A-3. Ajout de fonctions membres dans la classe Jeu▲
Ensuite nous ajoutons deux fonctions à la classe :
//Fonction pour vider le plateau de jeu
void
videJeu();
//Fonction pour vérifier si la partie est terminée
void
verifFini();
II-A-3-a. La fonction videJeu▲
La fonction videJeu remet toutes les valeurs à zéro. Elle sera appelée dans le constructeur pour éviter une redondance de code. En fait, elle n'a rien de nouveau, ce n'est qu'une migration de code qui se trouvait dans le constructeur (on y ajoute tout de même la mise à false de la variable fini). Cette fonction est plus importante que ce qu'on pourrait croire. Elle permettra de recommencer le jeu sans quitter le programme. Plus le jeu est compliqué, plus cette fonction sera difficile à écrire, mais sera toujours aussi importante.
void
Jeu::
videJeu()
{
int
i,j;
//On vide le plateau
for
(i=
0
;i<
3
;i++
)
for
(j=
0
;j<
3
;j++
)
plateau[i][j] =
Vide;
//On commence par rond
tour =
Rond;
//On met fini à false
fini =
false
;
}
II-A-3-b. La fonction verifFini▲
La deuxième fonction la plus importante pour un jeu est une fonction de type verifFini qui permet de savoir si le jeu est terminé. De plus, si la fonction est capable de savoir qui a gagné, alors c'est la cerise sur le gâteau. Nous n'allons pas montrer tout le code de cette fonction, seulement le début puisque c'est toujours la même chose.
Nous vérifions les lignes, les colonnes puis les diagonales. Si la première case est non vide, alors nous regardons le reste de la ligne/colonne/diagonale. S'ils sont tous du même type, alors nous savons que quelqu'un a gagné. Nous mettons donc les cases en question à Gagne et nous mettons typegagne et fini à jour.
for
(i=
0
;i<
3
;i++
)
{
if
( plateau[i][0
]!=
Vide )
{
//Tester la ligne
if
((plateau[i][0
]==
plateau[i][1
]) &&
(plateau[i][0
]==
plateau[i][2
]))
{
fini =
true
;
typegagne =
plateau[i][0
];
plateau[i][0
] =
Gagne;
plateau[i][1
] =
Gagne;
plateau[i][2
] =
Gagne;
}
}
else
{
casevide =
true
;
}
}
Rappelons que le jeu se termine aussi s'il n'y a plus de cases vides. Dans cette fonction, nous avons donc un booléen casevide qui est initialisé à faux et qu'on met à vrai dès qu'on voit une case vide. Vu qu'on teste que la première case des lignes, des colonnes et des diagonales pour les cases vides, nous devons aussi regarder les cases qui restent. Ceci est fait à la fin de la fonction, avant de mettre à jour la variable fini.
//Dernières cases
if
( (plateau[1
][1
] ==
Vide)||
(plateau[1
][2
] ==
Vide)||
(plateau[2
][1
] ==
Vide)||
(plateau[2
][2
] ==
Vide))
casevide =
true
;
fini =
!
casevide ||
fini;
Le test booléen dit bien: le jeu est terminé si aucune case n'est vide ou si fini a déjà été mis à vrai (cela veut dire qu'on a eu un alignement).
II-A-4. Modification des fonctions membres de Jeu▲
Finalement, le fait d'ajouter ces informations dans le programme a affecté toutes les fonctions de la classe Jeu. Nous avons de la chance puisque cette classe est encore relativement petite. Regardons ensemble les différences apportées.
II-A-4-a. Le constructeur et destructeur▲
Les seules deux modifications du constructeur de la class Jeu est la mise à NULL des variables gagnex et gagneo et la migration du code vers la fonction videJeu.
Jeu::
Jeu()
{
//Vider le Jeu
videJeu();
//Valeur par défaut pour les surfaces
o=
NULL
;
x=
NULL
;
gagneo=
NULL
;
gagnex=
NULL
;
bg=
NULL
;
}
Du côté destructeur, nous ajoutons simplement la désallocation des nouvelles surfaces.
Jeu::
~
Jeu()
{
//On libére les surfaces
SDL_FreeSurface(o);
SDL_FreeSurface(bg);
SDL_FreeSurface(x);
SDL_FreeSurface(gagneo);
SDL_FreeSurface(gagnex);
}
II-A-4-b. La fonction clic▲
Très peu de changements sont effectués à la fonction clic. On ajoute un test dans le cas où fini est mis à vrai. La décision pour le moment est de simplement remettre le jeu à zéro.
if
(fini)
{
videJeu();
}
Sinon, on gère le clic de la même façon que dans le dernier tutoriel, mais on ajoute un appel à la fonction verifFini. Donc, à chaque clic, on vérifie si la partie est terminée.
II-A-4-c. La fonction aff▲
On ajoute dans cette fonction la gestion des cases de type « Gagne ». Ceci se fait en ajoutant deux if-else ou alors un switch :
//On dessine en fonction du type de la case
switch
(plateau[k][l])
{
case
Croix:
SDL_BlitSurface(x, NULL
, screen,&
r);
break
;
case
Rond:
SDL_BlitSurface(o, NULL
, screen,&
r);
break
;
case
Gagne:
if
(typegagne==
Rond)
SDL_BlitSurface(gagneo, NULL
, screen,&
r);
else
SDL_BlitSurface(gagnex, NULL
, screen,&
r);
break
;
default
:
break
;
}
On aurait pu ajouter deux éléments dans l'énumération de la Case: GagneX et GagneO à la place d'un type Gagne. C'est une solution acceptable, et vous pouvez, si vous voulez, tenter de le faire comme exercice !
III. Conclusion▲
En conclusion, on remarque que finalement les changements ne sont pas extraordinaires pour ajouter les règles du jeu du morpion et les intégrer dans le programme. En effet, avec quelques ajustements, on peut les gérer de façon intéressante et claire. Dans la prochaine partie, nous montrerons comment abstraire le moteur du jeu par rapport à tout ce qui se trouve en dessous, afin de bien séparer les différentes parties du programme.
Jc
Liens
- Sommaire du tutoriel
- Ouvrir une fenêtre SDL
- Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué
- Ajouter les règles du jeu et le finaliser
- Ajouter les classes Objet et Moteur pour rendre le jeu plus souple
- Ajouter un menu dans un jeu
- Ajouter une Intelligence Artificielle (Min-Max)
- Ajouter une Intelligence Artificielle (Alpha-Beta)
IV. Téléchargements▲
Voici le code source de ce tutoriel : zip (127 Ko)