Exercices UML et Java
Durée2h30Contexte
Le travail de cette séance sera en deux parties distinctes : une partie conception UML puis une partie programmation Java.
Diagrammes UML
Conception d’un jeu de Puissance 4. Voici les spécifications du logiciel :
L’objectif est de modéliser un système permettant de gérer une partie de Puissance 4 entre deux joueurs : le
Joueur Jauneet leJoueur Rouge. Le jeu est orchestré par unMeneur de Jeuqui interagit avec uneGrille.
Fonctionnement de la partie :
- Initialisation : Au début de la partie, le
Meneur de Jeudoit vider laGrille.- Boucle de jeu : Tant qu’il n’y a pas de gagnant et que la
Grillen’est pas pleine :
- Le
Meneur de Jeudemande auJoueur Jaunede choisir une colonne pour y déposer son pion.- Si la colonne choisie est pleine, le
Meneur de Jeuredemande une colonne jusqu’à ce que le choix soit valide.- Le pion jaune est ensuite lâché dans la colonne de la
Grille.- Le
Meneur de Jeuvérifie s’il y a un gagnant ou si laGrilleest remplie.- Si la partie n’est pas finie, la même procédure est répétée pour le
Joueur Rouge.- Fin de partie : Une fois la boucle terminée, le
Meneur de Jeuaffiche le résultat : soit le nom du gagnant, soit un message de “Match nul” si laGrilleest pleine sans vainqueur.
Exercice 1 (Diagramme de classe)
Créer un diagramme de classes (avec attributs et méthodes) représentant une modélisation possible du jeu Puissance 4. On s’interrogera sur la pertinence d’utiliser l’héritage, de modéliser les colonnes et les pions comme des classes.
Correction
Exercice 2 (Diagramme de séquence)
Réaliser un diagramme de séquence modélisant le fonctionnement de la partie.
Correction
Petite architecture logicielle en Java
Contexte
Cet exercice vous permettra de consolider vos connaissances en programmation objet Java en réalisant une variante du jeu Démineur.
Une partie de démineur commence par l’affichage d’une grille. Le contenu des cases est initialement masqué. L’enjeu est de déterminer les cases qui contiennent une mine (sans cliquer dessus sans quoi on perd la partie). Pour dévoiler le contenu d’une case il faut cliquer dessus. Le premier coup s’effectue au hasard puisque rien ne permet de savoir initialement si une case contient une mine ou non. Mais si la case ne contient pas de mine alors :
- Si au moins une des 8 cases adjacentes contient une mine, alors le contenu de la case est dévoilé et ce contenu est le nombre de cases adjacentes minées.
- Sinon (aucune mine à proximité), la case est dévoilée (elle est vide) et on simule un clic sur les 8 cases adjacentes. Il y a alors propagation : toutes les cases vides se dévoilent de proche en proche, mais aussi les cases numérotées directement voisines des cases vides dévoilées. Si on a pu déterminer la position d’une mine on peut la marquer via un clic-droit. Un petit drapeau rouge symbolise alors l’emplacement de la mine et on ne peut plus cliquer dessus par mégarde. La partie s’achève lorsqu’on clique sur une case minée (on perd) ou lorsqu’on a dévoilé toutes les cases ne contenant pas de mines (on gagne).
Diagramme de classes
La classe Demineur modélise l’interface graphique du jeu. Vous
n’avez pas à la modifier. Le programme a également besoin d’une
modélisation du terrain miné. Les méthodes utiles à l’interface
graphique pour interagir avec le terrain et ses cases sont listées
dans les interfaces ITerrainMine et ICase. Votre travail consiste
à réaliser des classes implémentant convenablement ces interfaces.

Implémenter l’interface ICase
Décompressez l’archive fournie ici sur votre disque
local puis ouvrez le dossier obtenu dans VS Code à partir du menu
File>>Add Folder to Workspace....
CaseAbstraite
Le jeu comporte trois types de cases :
- des cases vides (classe
CaseVide), qui n’ont pas de mine dans les cases voisines. - des cases numérotées (classe
CaseNumerotee), qui sont des cases vides qui portent un numéro correspondant au nombre (strictement positif) de mines dans les cases voisines. - des cases minées (classe
CaseMinee).
Toute case :
- a des coordonnées (un index de colonne et un index de ligne).
- mémorise si elle est marquée à l’aide d’un drapeau (ou non).
- mémorise si son contenu est dévoilé ou encore caché.
- connaît le terrain dans lequel elle figure (car elle doit accéder aux cases voisines pour propager le dévoilement des cases).
Les trois types de cases ont donc des attributs communs. Le code de la
déclaration de ces variables d’instances et de leurs accesseurs est
factorisé au sein de la classe abstraite CaseAbstraite. La notion de
classe abstraite est présentée
ici. La classe
CaseAbstraite implémente l’interface ICase. Mais étant abstraite
la classe CaseAbstraite n’a pas l’obligation de fournir une
implémentation de toutes les méthodes de ICase (car il est
impossible de créer une instance de CaseAbstraite). Cependant,
toute classe concrète qui étend CaseAbstraite doit fournir une
implémentation des méthodes de ICase absentes de CaseAbtraite
(i.e. getVal(), estMinee(), clic() et propager()). Les
classes CaseVide, CaseMinee et CaseNumerotee vont étendre
CaseAbstraite en fournissant une implémentation des méthodes
manquantes.
CaseVide
- Déclarez que la classe
CaseVideétendCaseAbstraite. - Ajoutez un constructeur
CaseVide(ITerrainMine terrain, int colonne, int ligne)qui fait appel au constructeur de sa super-classe afin d’initialiser les variables d’instances héritées. En consultant les spécifications indiquées dansICase, vous verrez que :- la méthode
getValeur()doit retourner la constanteICase.VIDEdans le cas d’une case vide. - la méthode
estMinee()devra toujours retournerfalse(la case est vide, donc non minée). - la méthode
clic()est appelée lorsque l’utilisateur clique sur la case. Cette méthode est déjà implémentée dansCaseVide. Le code fourni (1) dévoile le contenu de la case et (2) appelle la méthodepropager()sur chacune des cases voisines. - la méthode
propager()est appelée lorsqu’une case voisine vient d’être dévoilée. Le comportement à implémenter pour une case vide est d’appelerclic()mais uniquement si le contenu de la case est encore caché (vérifier si la case est encore cachée permet d’éviter à la propagation de boucler indéfiniment).
- la méthode
- Implémentez les méthodes
getValeur()etestMinee(). Testez en exécutant la méthodemaindeTesteur. - Implémentez la méthode
propager()et dé-commentez le code de la méthodeclic(). Pour tester, dé-commentez la ligne correspondante deTerrainMinepuis exécutez la méthodemaindeDemineur. À ce stade, la grille est remplie de cases vides et un clic sur n’importe quelle case doit dévoiler toute la grille.
CaseMinee
-
Dans le cas d’une case minée :
- l’état n’a pas à être complété : le fait d’être de type
CaseMineeimplique implicitement que la case est minée. - la méthode
getValeur()doit retourner la constanteICase.MINEE. - la méthode
estMinee()devra toujours retournertrue(puisque la case est minée). - la méthode
clic()ne fait qu’appeler la méthodedevoiler(). - la méthode
propager()ne fait rien.
- l’état n’a pas à être complété : le fait d’être de type
-
Rajoutez au projet une classe
CaseMineehéritant deCaseAbstraite. -
Rajoutez un constructeur prenant les mêmes paramètres que le constructeur de
CaseAbstraiteet qui fait appel au constructeur de sa super-classe afin d’initialiser les variables héritées. -
Implémentez les méthodes
getValeur()etestMinee(). Testez en exécutant la méthodemaindeTesteur. -
Implémentez les méthodes
clic()etpropager(). Pour tester, dé-commentez la ligne correspondante deTerrainMinepuis exécutez la méthodemaindeDemineur. À ce stade, un clic sur une case vide dévoile toutes les cases qui ne sont pas minées. Les cases qui demeurent cachées sont minées.
CaseNumerotee
Une case numérotée est avant tout une case vide (non minée), mais une case vide qui a des cases voisines minées. Lorsque son contenu est dévoilé, l’interface graphique indique le nombre de cases voisines minées. Nous pourrions vouloir recalculer chaque fois que nécessaire le nombre de cases voisines minées. Cependant, ce nombre ne change pas tout au long de la partie et pour éviter ces re-calculs inutiles nous allons mémoriser l’information au sein de la case numérotée.
- Rajoutez au projet une classe
CaseNumeroteehéritant deCaseVide. - Complétez l’état en rajoutant une variable d’instance
valeur(de typeint) qui mémorisera le nombre de cases voisines minées. - Rajoutez un constructeur ayant un paramètre de plus (comparé au constructeur de
CaseAbstraite) et qui servira à initialiservaleur.
En cas de clic sur une case numérotée seul le contenu de la case est dévoilé (appel à devoiler()) : contrairement à CaseVide il n’y a pas propagation.
- Redéfinissez les méthodes qui doivent l’être.
Pour tester, dé-commentez la ligne correspondante de
TerrainMinepuis exécutez la méthodemaindeDemineur.
Correction
Pour aller plus loin
Interface Comparable
Vous avez probablement déjà exploré diverses méthodes pour trier un ensemble (ou une collection). En connaissant un algorithme de tri, comme le tri par fusion (https://fr.wikipedia.org/wiki/Tri_fusion), vous pourriez souhaiter l’implémenter dans une méthode public void sort(List<Integer> list) pour trier une liste d’entiers. Plus tard, vous pourriez avoir besoin d’implémenter une méthode public void sort(List<String> list) pour trier des listes de chaînes de caractères. Les implémentations seraient très similaires, car l’algorithme reste le même. Il serait donc plus efficace de créer une version générique unique qui fonctionnerait indépendamment du type des éléments.
Pour tous types d’éléments ? Pas tout à fait. Pour trier nous avons besoin de pouvoir comparer les éléments entre eux. Nous serions capables d’écrire une méthode de tri générique, capable de trier une List<T> pour peu que les éléments de type T soient comparables entre eux, et donc que le type T dispose d’une méthode capable de déterminer si un élément de type T est inférieur ou supérieur à un autre.
Les interfaces sont utilisées pour imposer l’existence de la méthode requise. Dans notre exemple, la méthode de classe Collections.sort(List<T> list) ne peut être utilisée que si T implémente l’interface Comparable<T>. C’est ce qu’indique la condition <T extends Comparable<? super T>> dans la javadoc de la classe Collections.
static <T extends Comparable<? super T>> void sort(List<T> list)
"Sorts the specified list into ascending order, according to the natural ordering of its elements."L’interface Comparable<T> ne comporte qu’une méthode, compareTo dont voici un extrait des spécifications indiquées dans la javadoc :
int compareTo(T o)
Parameters: o - the object to be compared.
Returns: a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.Les classes « enveloppes » (Integer, Double, Character, …)
et String implémentent cette interface. Il est donc possible
d’utiliser Collections.sort(List<T> list) sur des listes d’éléments
de ces types.
Exercice
- Dans l’archive de l’exercice précédent, quelques classes vous sont fournies dans le package
s05.comparable. - Sous VS Code, consultez la méthode
maindeTestSortdu packages05.comparablepuis exécutez-là.
Nous aimerions à présent trier des List<Heure>.
- Modifiez la classe
Heureafin qu’elle implémenteComparable<Heure>. Ne réinventez pas la roue et sachez exploiter les méthodes qui figurent déjà dans la classeHeure. - Complétez la méthode
maindeTestSortafin de trier la listelisteHeuresavant de l’afficher.
La classe Collections (avec un s à la fin !) contient des méthodes de classe utiles à la gestion de collections.
- Complétez la méthode
maindeTestSorten exploitant les méthodes deCollections(javadoc disponible sous https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Collections.html) afin de :- Mélanger (shuffle) la liste
listeHeurespuis l’afficher. - Afficher la valeur minimale de
listeHeures. - Afficher la valeur maximale de
listeHeures. - Trier la liste
listeHeuresdans l’ordre décroissant puis l’afficher.
- Mélanger (shuffle) la liste