VI. Chapitre 7▲
VI-A. Exercice 1 : nombre de victoires/défaites▲
VI-A-1. Analyses et stratégies▲
On nous demande d'ajouter au panneau supérieur deux champs affichant le nombre de victoires et de défaites (code ajouté en marron).
Nous allons essayer d'aboutir au résultat suivant :
On peut ajouter les champs en tant que composants Label, dont les textes seront mis à jour à chaque fin de partie. On aura aussi besoin de garder une référence sur les deux composants, afin que les différentes méthodes de la classe puissent en modifier le texte. J'en fais donc des variables de classe et j'y déclare ces deux composants, mais en dehors de toute méthode.
Button cases[];
Button boutonNouvellePartie;
Label score;
Label labelVictoires;
Label labelDefaites;
Mais il convient également de réfléchir quant à l'agencement des composants dans le panneau supérieur. En effet, on n'aura plus affaire à un seul Button qui en occupera tout l'espace, mais à trois composants qu'il faudra faire coopérer le plus harmonieusement possible.
Pour ma part, j'ai opté pour un GridLayout à trois lignes et une colonne. Donc, avant de créer et ajouter les composants, je définis le LayoutManager :
// Cree le bouton Nouvelle partie et enregistre
// le recepteur d'actions aupres de lui
boutonNouvellePartie =
new
Button
(
"Nouvelle partie"
);
boutonNouvellePartie.addActionListener
(
this
);
// Cree deux panneaux et un label et les agence en
// utilisant le border layout
Panel panneauSuperieur =
new
Panel
(
);
panneauSuperieur.setLayout
(
new
GridLayout
(
3
,1
));
// Cree les deux labels victoires/defaites
labelVictoires =
new
Label
(
"Nombre de victoires : 0"
);
labelDefaites =
new
Label
(
"Nombre de defaites : 0"
);
// Ajoute les composants au panneau superieur
panneauSuperieur.add
(
labelVictoires);
panneauSuperieur.add
(
labelDefaites);
panneauSuperieur.add
(
boutonNouvellePartie);
this
.add
(
panneauSuperieur, "North"
);
On nous recommande également d'utiliser deux variables de classes et de les utiliser pour comptabiliser le nombre de victoires et de défaites :
int
casesLibresRestantes =
9
;
int
nombreVictoires =
0
int
nombreDefaites =
0
;
Enfin, il ne nous reste plus qu'à incrémenter le nombre de victoires/défaites juste en fin de partie et de mettre à jour les Labels correspondants :
}
// Fin de la boucle for
if
(
gagnant.equals
(
"X"
)) {
score.setText
(
"Vous avez gagne !"
);
nombreVictoires++
;
labelVictoires.setText
(
"Nombre de victoires : "
+
nombreVictoires);
}
else
if
(
gagnant.equals
(
"O"
)) {
score.setText
(
"Vous avez perdu !"
);
nombreDefaites++
;
labelDefaites.setText
(
"Nombre de defaites : "
+
nombreDefaites);
}
else
if
(
gagnant.equals
(
"T"
)) {
score.setText
(
"Partie nulle !"
);
}
}
// Fin de actionPerformed
Vous connaissez déjà l'opérateur d'incrémentation ++, car vous l'avez déjà utilisé pour la boucle for.
NombreVictoires++
;
équivaut à
nombreVictoires =
nombreVictoires +
1
;
VI-A-2. Code complet▲
VI-B. Exercice 2 : résolution d'un bogue▲
VI-B-1. Quelques astuces▲
Un bogue grave subsiste : on peut modifier la valeur d'une case déjà jouée !
Pour remédier à ceci, il faut modifier le gestionnaire de clics. Qui dit gestionnaire de clics, dit méthode actionPerformed de l'interface ActionListener.
Il suffira donc, dans la méthode actionPerformed, de n'exécuter la modification du bouton cliqué uniquement si son texte est vide.
Mais d'abord, il ne faut pas perdre de vue que les chaines de caractères ne sont pas de simples types primitifs (à l'instar des int, boolean,…), mais des données de type Object. Et la meilleure manière de comparer deux objets (on suppose ici que objet1 et objet2 sont comparables, ce qui est le cas si objet1 et objet2 sont du même type ou si objet1 et objet2 ont au moins une classe mère commune), c'est de faire appel à la méthode equals() de l'un des objets :
objet1.equals
(
objet2) ;
objet2.equals
(
objet1) ;
Donc, si j'ai une chaine de caractère chaine1 et que je veuille tester si elle est vide, je pourrais écrire :
chaine1.equals
(
""
) ;
Enfin le texte d'un composant java.awt.Button s'obtient en appelant sa méthode getLabel().
VI-B-2. Méthode actionPerformed corrigée▲
VI-B-3. Code complet▲
VI-C. Exercice 3 : ajout d'une méthode main()▲
Cet exercice-là peut s'avérer beaucoup plus compliqué qu'il n'y paraît. On nous demande de nous baser sur l'applet existante afin de coder une application à part entière : on doit pouvoir l'exécuter sans l'intégrer dans une page HTML.
Dans la méthode init() de l'applet, on a construit les différents composants. On les a également ajoutés à l'applet : c'est-à-dire à une instance de java.awt.Container. En effet, la javadoc nous apprend que la classe java.applet.Applet est une classe fille de java.awt.Container, même si c'est de manière indirecte.
Or si l'on veut que le jeu puisse être lancé en mode application, il faudra créer nous-même la fenêtre, y ajouter une instance de Panel et ajouter les composants à ce Panel (ainsi qu'ajouter le Panel à la fenêtre). Et la classe java.awt.Panel hérite notamment de la classe java.awt.Container. Il faudra donc adapter tout ce qui a été fait dans la méthode init() pour n'importe quel objet Container.
VI-C-1. Code de la méthode constuireContainer()▲
J'ai donc créé une méthode construireContainer() dans la classe Morpion : elle prend en paramètre un Container et en remplit le contenu avec les composants de notre jeu Morpion.
Souvenez-vous que dans le cas d'une Applet, le conteneur parent de tout composant est l'Applet elle-même. Évidemment, dans le cas de l'application, cela sera juste un Panel : lequel sera ajouté au LayoutManager de la fenêtre.
N'oubliez pas que dans le code original, this désigne l'instance de Morpion courante : il suffit donc, si cela est approprié, de le remplacer par container.
VI-C-2. Nouvelle version de la méthode init()▲
Le code de la méthode init() devient donc :
public
void
init
(
) {
construireContainer
(
this
);
}
On peut déjà, à ce stade, vérifier que l'applet fonctionne de la même manière qu'auparavant.
Maintenant on dispose d'une manière simple de remplir un Panel avec le contenu de notre jeu Morpion. On peut se mettre à coder la création de la fenêtre, dans la méthode main().
L'utilisation d'une fenêtre au lieu du codage d'une applet a tout de même quelques répercussions :
- il convient de définir, pour la fenêtre, certains aspects dont on ne se préoccupait pas en développant une applet : le titre de la fenêtre (jusque-là définit par la page HTML), la position de la fenêtre, la taille de la fenêtre… ;
- il faut prévoir un moyen de fermer la fenêtre si l'utilisateur clique sur son bouton de fermeture ;
- c'est à nous-même d'afficher la fenêtre.
Afin de prévoir une fermeture de la fenêtre, il nous faut ajouter un WindowListener à la fenêtre créée et coder dans la méthode windowClosing() la fermeture proprement dite. Pour mettre fin à l'application, on appellera :
System.exit
(
0
) ;
Passer 0 signifie que le programme s'est finalement déroulé normalement. Passer une valeur supérieure à 0 signifie que le programme a du finir précipitamment. C'est une convention.
La classe WindowListener présentant de nombreuses méthodes dont nous n'aurons pas besoin, on peut se tourner vers la classe WindowAdapter. Il nous suffira alors de redéfinir la méthode windowClosing(). J'attire votre attention sur le fait que la méthode windowClosed() est appelée une fois que la fenêtre a été fermée : alors que pour windowClosing(), on se contente de détecter si la fermeture de la fenêtre a été demandée.
VI-C-3. Code de la méthode main()▲
public
static
void
main
(
String[] args) {
Frame fenetre =
new
Frame
(
);
/*
* Cette fois-ci, c'est le code qui
* fixe les dimensions de la zone de jeu,
* et non une page html.
*/
fenetre.setSize
(
400
, 300
);
/*
* Cette fois-ci, le titre se definit dans la fenetre.
*/
fenetre.setTitle
(
"Jeu du morpion"
);
/*
* Centre la fenetre sur l'ecran
*/
fenetre.setLocationRelativeTo
(
null
);
Panel panneauFenetre =
new
Panel
(
);
fenetre.add
(
panneauFenetre);
Morpion logiqueMorpion =
new
Morpion
(
);
logiqueMorpion.construireContainer
(
panneauFenetre);
fenetre.addWindowListener
(
new
WindowAdapter
(
) {
@Override
public
void
windowClosing
(
WindowEvent e) {
System.exit
(
0
);
}
}
);
fenetre.setVisible
(
true
);
}
VI-C-4. Code complet▲
Voici ce que le programme peut donner en version fenêtre :
VI-D. Exercice pour les petits malins▲
VI-D-1. Les tableaux multidimensionnels▲
VI-D-1-a. Théorie▲
Jusqu'ici vous n'avez utilisé que les tableaux à une seule dimension, par exemple :
int
monTableau[] =
new
int
[6
] ;
int
monTableau[] =
{
2
,4
,6
,8
,10
,12
}
;
Les deux tableaux précédents ont la même dimension.
Mais en fait, un tableau peut avoir, en théorie, une infinité de dimensions :
int
monTableau3D[][][] =
new
int
[2
][6
][3
] ;
int
monTableau3D[][][] =
{
{
{
5
,1
,12
}
,{
2
,-
6
,18
}
,{
8
,0
,-
54
}
,{
1
,-
5
,-
67
}
,{
3
,10
,20
}
,{
0
,78
,27
}
}
,
{
{
23
,1
,134
}
,{
25
,6
,12
}
,{
99
,6
,54
}
,{
19
,100
,-
67
}
,{
37
,-
2
,18
}
,{
5
,72
,82
}
}
}
;
Ce sont deux tableaux à trois dimensions et presque équivalents : c'est juste que le premier tableau n'est rempli qu'avec des zéros.
Comment alors parcourir un tel tableau ? Il suffit d'utiliser plusieurs boucles for imbriquées : la première boucle for pour parcourir la première dimension, la deuxième pour parcourir la deuxième dimension et la dernière pour parcourir la dernière dimension :
for
(
int
dim1 =
0
; dim1 <
monTableau3d.length ; dim1++
){
for
(
int
dim2 =
0
; dim2 <
monTableau3D[dim1].length ; dim2++
){
for
(
int
dim3 =
0
; dim3 <
monTableau3D[dim1][dim2].length ; dim3++
){
System.out.println
(
monTableau3d[dim1][dim2][dim3]) ;
}
}
}
Certains auront sans doute remarqué comment j'ai consulté la longueur de chaque dimension dans les différentes boucles :
monTableau3d.length // pour la première dimension
monTableau3D[dim1].length // pour la deuxième dimension
monTableau3D[dim1][dim2].length // pour la troisième dimension
En effet, il faut faire attention à deux choses :
- si l'on écrit juste montableau3d.length, on n'obtient que la longueur de la première dimension ;
- on réutilise les variables dim1, dim2 car il se peut que le tableau ne soit pas de dimension uniforme (régulière).
Pour vous en convaincre, essayez de déclarer le tableau comme ceci :
int
[][][] monTableau3D =
{
{
{
1
,2
,3
}
, {
4
}
, {
5
,6
,7
,8
}
, {
9
,10
}
}
,
{
{
11
}
, {
12
,13
}
, {
14
,15
,16
}
, {
17
,18
, 19
,20
}
}
}
;
Testez-le aussi avec l'extrait de code avec les trois boucles imbriquées, que j'ai donné peu avant. Vous constaterez alors que :
- il n'y a aucune erreur à la compilation (depuis Eclipse, rien ne nous est signalé avant le test) ;
- il n'y a aucune erreur à l'exécution et on a bien l'ensemble des valeurs affichées, dans l'ordre.
Le tableau que vous avez testé est un tableau multidimensionnel légals et la triple boucle imbriquée que je vous ai introduit est tout à fait adaptée pour le tester. Si on ne se basait pas sur la valeur courante de chaque dimension lors des différents tests de longueur des dimensions, on pourrait alors facilement aboutir à une erreur d'exécution : ArrayOutOfBoundsException.
Donc ceci :
for
(
int
dim1 =
0
; dim1 <
monTableau3d.length ; dim1++
){
for
(
int
dim2 =
0
; dim2 <
monTableau3D[0
].length ; dim2++
){
for
(
int
dim3 =
0
; dim3 <
monTableau3D[0
][0
].length ; dim3++
){
System.out.println
(
monTableau3d[dim1][dim2][dim3]) ;
}
}
}
est à proscrire pour le tableau irrégulier que je viens de vous donner.
On nous demande ici d'utiliser un tableau à deux dimensions, ce qui ne devrait pas poser plus de problèmes que pour le tableau à trois dimensions que l'on vient de voir.
Enfin, une dernière difficulté technique que l'on se doit de résoudre. La section du code original suivante :
// Regarde si la ligne 2 a 2 cases identiques et une vide
if
(
poids[3
] +
poids[4
] +
poids[5
] ==
deuxPoids) {
if
(
poids[3
] ==
0
)
return
3
;
else
if
(
poids[4
] ==
0
)
return
4
;
else
return
5
;
}
peut nous poser problème. On passe en effet d'un tableau où les neuf cases sont alignées à un tableau à deux dimensions. Il faut donc trouver un moyen de convertir ("à la main") tous les indices qui étaient prévus pour un tableau à une dimension, en une paire d'indices pour notre nouveau tableau.
VI-D-1-b. Mise en pratique▲
La première question à se poser : la première dimension représentera-t-elle les lignes ou les colonnes ? Pour le savoir, il faut se baser sur la manière dont les boutons sont ajoutés au conteneur. Le panneauCentral, sur lequel sont ajoutés les boutons, a un agencement défini par un GridLayout de trois lignes par trois colonnes.
Les boutons seront donc ajoutés colonne par colonne, en passant à la ligne suivante au besoin. La boucle for la plus interne devrait donc être consacrée au parcours de colonnes. On en déduit donc qu'il sera plus commode d'attribuer les lignes à la première dimension (revoyez au besoin le code de parcours du tableau à trois dimensions : la boucle for la plus externe examine la première dimension du tableau).
La deuxième question à se poser : comment obtenir la ligne et la colonne à partir de l'index initial ?
On peut remarquer que :
colonne =
index %
3
; // reste de la division entière de index par 3
ligne =
index /
3
; // quotient de la division index / 3 => c'est un entier
en assumant qu'index est une variable de type int.
Par exemple, pour l'index 5 :
colonne =
5
%
3
=
2
ligne =
5
/
3
=
1
Au lieu d'écrire cases[5], on écrira donc cases[1][2]
VI-D-2. Code résultat▲
VI-E. Synthèse▲
Ces exercices ont pu vous apprendre :
- à ajouter des composants dans une interface AWT et les mettre régulièrement à jour grâce à l'utilisation de variables de classe ;
- à résoudre simplement un bogue ;
- à rendre une applet utilisable en mode application, en y ajoutant une méthode main(). Mais aussi à adapter au besoin le code ;
- à utiliser un tableau à deux dimensions, sans obtenir d'erreur à l'exécution.