Date de première publication : 2014/02/27
Notions : Swing, patron MVC, patron observateur
Nous voulons réaliser un démineur en Java.
Le principe du jeu est très simple : on propose à une personne de découvrir petit à petit une zone "minée" en signalant à chaque case, le nombre de mines adjacentes. Il faut découvrir toute la zone sans tomber sur une mine. Pour une plus grande sécurité, on peut en général marquer la zone où l'on est persuadé qu'il y a une mine pour ne pas la découvrir.
Nous voulons programmer ce petit jeu en utilisant le patron de conception Modèle-Vue-Contrôleur, même si en Swing on fusionne assez facilement les parties
Vue et Contrôleur. Nous montrerons comment des objets peuvent communiquer entre eux : par appel de méthodes bien entendu mais surtout
en utilisant le patron de conception Observateur/Observable
qui permet à un objet d'être prévenu d'un changement. Le patron Observateur/Observable
permet de mettre en place un couplage lâche entre deux classes dans le sens où l'objet observé n'a pas d'information sur l'observateur.
Voici l'arborescence que nous vous proposons ainsi que le résultat que l'on cherche à obtenir :
Le modèle
Une pratique usuelle de développement consiste à mettre toutes les classes d'un modèle dans un même package
Nous allons tout d'abord nous intéresser à la zone de jeu, le Terrain
: celui-ci comporte une grille de cases ou cellules (voir plus bas)
d'une certaine dimension (nous l'avons choisie carrée :-)) et un nombre de mines à découvrir. Nous le dotons également du nombre de cases
qui n'ont pas encore été ou découvertes ou marquées ainsi que d'un état : partie en cours, partie gagnée, partie perdue.
Pour l'état de la partie, vous pouvez utiliser une énumération : dans son expression la plus simple, elle s'écrit comme en C++ même si en java la notion d'énumération est beaucoup plus puissante (c'est une classe avec constructeur et méthodes)
enum Enumeration { VALEUR1, VALEUR2, VALEUR3 }
Si vous devez utiliser une énumération dans un switch, le type de l'énumération est déduit au niveau du case : seule la valeur suffit.
Cellule ou Case
Le terrain est un tableau de cases ou cellules pour lequelles on a les informations suivantes :
- la position
x
ety
dans la grille - un entier
valeur
: -1 si c'est une mine ou le nombre de mines adjacentes - un booléen
visible
si la case affiche sa valeur (on dira qu'elle est découverte) - un booléen
selected
si la case est marquée (une case marquée ne peut être découverte)
Ecrire un constructeur qui permet d'initialiser les attributs x
, y
et valeur
(visible
et selected
sont "faut par défaux" ;-))
Doter tous les attributs de méthodes get (cela peut se faire automatiquement avec un EDI)
Dois-je vous rappeler qu'avec les conventions d'écriture l'accesseur pour selected
se nomme isSelected()
?
Doter la classe d'autres méthodes : decouvrir()
qui permet de rendre visible l'objet s'il n'est pas marqué, et toggleSelected()
qui permet de changer la valeur du marquage.
Il faut encore les méthodes set pour les attributs selected
et visible
mais elles nécessitent un peu de travail.
Elles ne devront pas être accessibles de l'extérieur alors je propose de les mettre privées et ces méthodes auront la "lourde" tâche de prévenir d'éventuels
observateurs que l'état de la classe a changé ! Pour réaliser cela, la classe devra être une instance de java.util.Observable
et toute classe qui veut être notifiée
des changements devra implémenter java.util.Observer
.
setChanged();
notifyObservers();
L'observateur devra implémenter une méthode update()
que je vous laisse découvrir dans la documentation.
Les classes Observer
et Observable
sont antérieures à Swing. On peut utiliser PropertyChangeListener
pour implémenter le patron Observateur.
Si vous ne voulez pas utiliser le patron observateur, il reste quand meme des solutions :
- Passer une référence du terrain à chaque cellule
- Faire un singleton avec le terrain
Terrain
Écrire le constructeur de la classe qui initialisera la grille (taille et nombre de mines donnés en paramètres).
Proposer une méthode créer()
qui s'assure que la grille est vierge de toute information, qui génère le nombre fixé de mines
et qui calcule la matrice d'adjacence. Il est alors possible de commencer à jouer ... Cette méthode est appelable plusieurs fois lors d'une même exécution du programme
Le terrain sera un observateur pour chacune des cellules de la grille et cela permettra de prendre en compte les situations suivantes :
- Si une cellule est changée, l'état de la partie de jeu peut être changé.
- On peut aussi programmer la découverte automatique des cellules.
La vue
La vue est concentrée sur une fenêtre de type JFrame
où le contentPane
possède un layout particulier : une grille contenant des JCase
ou
JCellule
. Nous enrichirons l'interface graphique petit à petit.
La case graphique
Nous devons afficher une cellule qui aura différentes représentations en fonction des choix du joueur.
- une zone uniforme si la case n'est pas découverte
- un symbole représentant si la case est marquée
- un nombre si la case est découverte et qu'elle n'est pas minée.
Je vous propose d'utiliser un JPanel
(ou un eJLabel
) dont vous aurez fixé la taille et redéfini la méthode paintComponent()
.
Voici un morceau de code qui permet d'afficher un message (une String
) sur toute la case :
Graphics2D g2 = (Graphics2D) g;
FontMetrics fm = g.getFontMetrics();
int w = fm.stringWidth(message);
g2.scale(getWidth()/(double)w,getHeight()/(double)fm.getHeight());
g2.drawString(message, 0 , fm.getHeight()-fm.getDescent());
Il faudra aussi implémenter l'interface MouseListener
pour gérer les clics souris. La classe SwingUtilities
fournit
une méthode qui permet de savoir quel bouton de la souris - gauche ou droit - a été appuyé. Le clic droit permet de changer le marquage de la case,
le clic gauche permet de découvrir la case si cela est possible.
La case graphique doit observer la case du modèle pour être repeinte lorsque la case du modèle est changée.
Mesurer le temps qui s'écoule...
Je vous propose d'afficher le temps qui s'écoule dans une barre des tâches constituée de deux JLabel
s (un fixe et l'autre variable).
Ce n'est pas la peine de jouer avec la classe Thread
et la méthode sleep()
, il suffit de définir un Timer
qui doit exécuter une tâche TimerTask
à intervalle donné :
Timer timer = new Timer();
timer.schedule(tt, 1000, 1000);
Un Timer
est annulable (méthode cancel()
). TimerTask
est une simple interface qui définit une méthode run()
.
Si vous créez une classe StatusBar
avec un Layout spécifique, un attribut temps
et une méthode
qui incrémente ce temps, cela devrait être plutôt facile.
Fin du jeu
Il reste à signaler la fin de la partie au joueur : gagné ou perdu ! Je vais vous proposer deux solutions, les deux considèrent que la vue principale observe le terrain afin de connaître le moindre changement de statut dans la partie du jeu.
La première solution consiste à afficher un simple un message avec un JOptionPane
La deuxième est plus "sympathique" à mettre en oeuvre : nous allons utiliser le GlassPane
pour afficher le message "Gagné" ou "Perdu". Là encore je vous propose de créer un composant JPanel
avec le bon layout et les bons composants pour obtenir le résultat ci-dessous :
Un BoxLayout orienté verticalement fait l'affaire. Pour que les boutons soient alignés, il faut préciser leur "alignement en X".
Le glasspane doit être créé et associé au JFrame
avant le pack()
et sera rendu visible le moment opportun grâce à setVisible()
.
Aller plus loin ...
Affichage de la case
La case, quand elle est marquée, n'est pas très jolie. Si vous trouvez une belle image, vous pouvez l'intégrer comme suit.
ImageIcon imageIcon = new ImageIcon("mine.png");
bombImage = imageIcon.getImage();
La lecture de l'image est synchrone : le programme est bloqué tant que la lecture n'est pas terminée. Dans mon programme,
bombImage
(de type Image
)est un attribut de classe initialisé dans un bloc statique.
Ce qui est important dans le cas présent, c'est de savoir où placer le fichier de ressource pour qu'il soit lu par le programme Java. Si vous développez avec un éditeur simple, le répertoire par défaut où cherche la machine virtuelle est le répertoire à partir duquel la machine virtuelle est lancée. Maintenant, si vous êtes sur Eclipse, vous pouvez placer l'image dans le répertoire principal du projet. À l'exécution tout se passe comme si l'image était au bon endroit. Il faut rafraîchir (touche F5) le projet pour voir le fichier dans l'explorateur d'Eclipse
Vous auriez pu placer l'image dans le répertoire src, mais pas dans le répertoire bin qui est "reconstruit" régulièrement à partir des sources (il est d'abord effacé)
J'ai aussi affiné la couleur de la case en fonction du nombre de mines adjacentes :
- vert si la case n'est proche d'aucune bombe
- orange si la case est adjacente à une ou deux bombes
- rouge si la situation est critique.
Différer la création de la grille
La génération de la grille peut être frustrante parfois : une découverte de bombe dès le premier clic. Vous pouvez différer la dispersion des bombes au premier clic si vous en avez envie.
Modèle objet explosif
Cela peut vous frustrer que l'on exploite pas complètement le modèle objet pour les cases / cellules. On pourrait tout à fait créer
une classe Bombe
qui spécialise Case
/Cellule
. Si vous voulez savoir de quel type est la case/cellule,
on peut utiliser un opérateur comme instanceof
if (case instanceof Bombe)
System.out.println("je suis une bombe");