tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
Institut d'informatique ISIMA
  • twitter
  • linkedin
  • viadeo

[JavaSE] Démineur

 Cette page commence à dater. Son contenu n'est peut-être plus à jour. Contactez-moi si c'est le cas!

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 : diagramme

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 :

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 :

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 :

  1. Si une cellule est changée, l'état de la partie de jeu peut être changé.
  2. 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.

fenetre principale du demineur

La case graphique

Nous devons afficher une cellule qui aura différentes représentations en fonction des choix du joueur.

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 JLabels (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

glasspane de fin de jeu

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 :

amelioration de la case

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");