L'objectif de cet exercice est la programmation d'une version JavaFx du jeu Lights Out. Lights Out est un jeu électronique publié par Tiger Electronics en 1995.
Le jeu consiste en une grille de lumières de 5 sur 5. Lorsque le jeu commence, un nombre aléatoire ou un ensemble mémorisé de ces lumières est activé. En appuyant sur l'une des lumières, vous basculerez sur celle-ci et sur les lumières adjacentes. Le but du puzzle est d’éteindre toutes les lumières, de préférence en appuyant le moins possible sur les boutons.
L'IHM que vous allez en partie réaliser ressemblera aux fenêtres suivantes :
L'objectif de ce test est d'évaluer votre capacité à écrire une IHM à l'aide du langage Java, les méthodes complexes car trop algorithmiques n'auront pas à être implémentées. Vous pourrez retrouver une proposition de correction à l'adresse suivante : https://github.com/IUTInfoAix-m2105/TestIHM2019/
L'application définit plusieurs types d'objets :
- Un objet
LightsOutMain
est une application JavaFX permettant de jouer. - Un objet
LightOutView
est la racine de la scène de jeu (l'intérieur de la fenêtre de l'image). - Un objet
LightOutControleur
est la classe contrôleur de l'IHM décrite parLightOutView
. - Un objet
Plateau
est le plateau de jeu composé des 25 cases, que l'on voit au centre duLightOutView
- Un objet
Case
représente une case. - Un objet
StatusBar
est la barre en bas duLightOutIHM
qui affiche le score et l'état de la partie. - Un objet
Position
contient la position d'une case dans le plateau.
Le diagramme UML suivant donne un aperçu synthétique de la structure des classes de l'application. Il n'est pas nécessaire de l'étudier pour l'instant, mais il vous sera très utile pour retrouver les données membres et méthodes des différentes classes.
Votre travail dans la suite de ce sujet sera d'écrire pas à pas plusieurs des classes ci-dessus. Le code des classes Position
et StatusBar
vous est donné à titre d'information ci-dessous, pour que vous puissiez vous y référer si besoin au cours des exercices.
Cette classe permet d'enregistrer la position d'une Case
sur le plateau de jeu. Son implémentation, très simple, vous est donnée ci-dessous à titre d'information :
public class Position {
private final int x;
private final int y;
public Position(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
La classe StatusBar
est un composant graphique permettant d'afficher l'état de la partie en cours. La description FXML du composant graphique StatusBar
est donnée dans le fichier StatusBarView.fxml
:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<fx:root xmlns:fx="http://javafx.com/fxml/" type="javafx.scene.layout.BorderPane"
xmlns="http://javafx.com/javafx/">
<left>
<Label fx:id="labelNombreDeCoupsJoués"/>
</left>
<center>
<Label fx:id="labelTemps"/>
</center>
<right>
<Label fx:id="labelpartieTerminee"/>
</right>
</fx:root>
L'implémentation de cette classe vous est donnée ci-dessous (où la gestion et l'affichage de la durée ont été omis pour ne pas surcharger le texte):
public class StatusBar extends BorderPane implements Initializable {
@FXML
private Label labelNombreDeCoupsJoués;
@FXML
private Label labelTemps;
@FXML
private Label labelpartieTerminee;
private IntegerProperty nombreDeCoupsJoués;
private BooleanProperty estPartieTerminee;
public StatusBar() {
nombreDeCoupsJoués = new SimpleIntegerProperty();
estPartieTerminee = new SimpleBooleanProperty();
FXMLLoader fxmlLoader = new FXMLLoader(getClass()
.getResource("/fr/univ_amu/iut/lightsout/StatusBarView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
creerBindings();
}
private void creerBindings() {
labelTemps.textProperty().bind(Bindings.concat("Durée : ", clock));
labelNombreDeCoupsJoués.textProperty()
.bind(concat("Nombres de coups : ", nombreDeCoupsJoués));
labelpartieTerminee.textProperty().bind(
when(estPartieTerminee)
.then("Partie terminée !")
.otherwise(""));
}
public IntegerProperty nombreDeCoupsJouésProperty() {
return nombreDeCoupsJoués;
}
public BooleanProperty estPartieTermineeProperty() {
return estPartieTerminee;
}
}
Le plateau de jeu disposera de 25 cases. Par commodité, chaque case conserve la position qu'elle occupe sur le plateau.
-
Écrire la déclaration d'une classe publique
Case
, sous-classe de (étendant)Button
, réduite pour le moment à la déclaration des variables d'instance suivantes, toutes privées (cf. diagramme UML) :position
de typePosition
, la position dans le plateau.estAllumé
une propriété booléenne qui permet de savoir si la case courante est allumée.
-
Écrire les accesseurs publics
getPosition()
,estAllumé()
etestAlluméProperty()
qui renvoient la donnée correspondante. -
Écrire la méthode
void allumer()
qui modifie la propriétéestAllumé
comme son nom l'indique et change la couleur de fond du bouton en vert. -
Écrire la méthode
void eteindre()
qui modifie la propriétéestAllumé
comme son nom l'indique et change la couleur de fond du bouton en noir. -
Écrire la méthode
public void permuter()
qui allume la case si elle est éteinte et inversement. -
Écrire le constructeur public
Case(int x, int y)
qui :- Assigne les données membres aux paramètres donnés correspondants, sachant que la
position
devra être créée avec les deux paramètres. - Fixe la largeur et la hauteur du
Case
àCELL_SIZE
, soit la taille d'une cellule. Aide : utilisez les méthodessetMinSize()
,setMaxSize()
etsetPrefSize()
qu'uneCase
hérite deButton
. - Allume la case.
- Fixe un espace vertical et horizontal de 3 pixels.
- Assigne les données membres aux paramètres donnés correspondants, sachant que la
Cette classe est celle qui permet d'implémenter toute la logique du jeu. Elle est celle qui demanderait le plus de travail dans une implémentation complète. Dans votre cas, vous n'aurez pas à implémenter les méthodes les plus complexes. Vous supposerez disposer de la méthode private void permuterVoisin(Case caseChoisi)
qui permute les voisins d'une case donnée en paramètre.
-
Écrire la classe
Plateau
qui dérive deGridPane
. Cette classe aura les données membres privées suivantes :taille
de typeint
qui mémorise la taille du plateau de jeu.cases
est une matrice detaille x taille
objets de typeCase
qui représente le plateau de jeu.nombreDeCoupsJoués
de typeIntegerProperty
qui mémorise le nombre de coups joués depuis le début de la partie.nombreDeCasesEteintes
de typeIntegerProperty
qui mémorise le nombre de cases actuellement éteintes sur le plateau.aGagné
est une propriété booléenne qui permet de savoir si le dernier coup était gagnant.caseListener
est un écouteur de case du typeEventHandler<ActionEvent>
.
-
Écrire la déclaration de
caseListener
sous forme d'une expression lambda. Cet écouteur doit gérer le clic sur uneCase
, ce qui consiste à incrémenter le nombre de coups joués puis récupérer la case qui a déclenché l’événement (pensez àevent.getSource()
) pour la permuter ainsi que toutes ses voisines. -
Écrire le constructeur
Plateau()
qui initialise toutes les données membres et appelle les méthodescreerBindings()
,remplir()
etnouvellePartie()
. -
Écrire la méthode
private void toutAllumer()
qui allume toutes les cases du plateau. -
Écrire la méthode
public int getNombreDeCoupsJoués()
qui retourne le nombre de coups joués depuis le début de la partie. -
Écrire la méthode
private void remplir()
qui remplit le plateau en y créant toutes les cases. Chaque case du plateau doit être créée avec une nouvelle instance deCase
, doit avoircaseListener
comme écouteur d'action et doit être placée au bon endroit du plateau avec la méthodeadd()
qu'il hérite deGridPane
. -
Écrire la méthode
public creerBindings()
qui s'occupe de correctement lier les propriétésaGagné
etnombreDeCaseEteintes
. La première sera vraie si le nombre de cases éteintes est égal au nombre total de cases. Quant à la seconde, sa valeur évoluera en fonction du changement d'état des cases. Sur chacune des cases decases
, ajouter un écouteur de changement sur la propriétéestAllumé
pour incrémenter ou décrémenternombreDeCasesEteintes
comme il se doit. -
Écrire la méthode
public void nouvellePartie()
qui réinitialise le plateau de jeu en allumant toutes ses cases et en remettant à zéro le nombre de coups joués.
Même s'ils n'ont pas été écrits, vous supposerez dans la suite que vous disposez des accesseurs suivants pour différentes propriétés de cette classe :
- pour
nombreDeCoupsJoués
:nombreDeCoupsJouésProperty()
etgetNombreDeCoupsJoués()
- pour
aGagné
:aGagnéProperty()
Outre le composant graphique StatusBar
et son fichier FXML associé StatusBArView.fxml
présentés en début de sujet, l'IHM se compose d'un fichier FXML et d'une classe contrôleur pour ce fichier.
Le fichier LightsOutView.fxml
est la description de la fenêtre principale du Jeu.
En plus du plateau situé au centre, cette fenêtre contient une barre de menu située en haut, et en bas la barre de statut.
La barre de menu contient un menu "Jeu" constitué d'une entrée "Nouvelle Partie" et d'une entrée "Quitter".
- Écrire le contenu de
LightsOutView.fxml
en n'oubliant pas d’associer les actions adéquates aux items du menu (actionMenuJeuNouveau()
etactionMenuJeuQuitter()
). Penser à valoriser l'attributfx:id
pour être en mesure de récupérer laStatusBar
et lePlateau
dans le contrôleur.
La classe LightsOutControleur
est chargée de contrôler la vue décrite par le fichier FXML :
- Écrire la déclaration de la classe
LightsOutControleur
. Cette classe disposera d'une donnée membre pour la barre de statut et pour le plateau. Ne pas oublier les annotations pour que la mise en correspondance vue/contrôleur puisse avoir lieu. - Écrire la méthode
public void initialize(URL location, ResourceBundle resources)
appelée juste après l'initialisation de la vue. Cette méthode doit lancer une nouvelle partie et appeler la méthodecreerBindings()
ci-dessous. - Écrire la méthode
private void creerBindings()
qui devra ajouter un écouteur de changement sur la propriétéaGagné
du plateau pour afficher le dialogue de fin de partie quand cette dernière devient vraie. Soumettre la propriété du nombre de coups joués de la barre de statut à celle correspondante pour le plateau. - Écrire la méthode
void actionMenuJeuNouveau()
qui relance une nouvelle partie sur le plateau et la barre de statut. - Écrire la méthode
void actionMenuJeuQuitter()
qui crée une alerte de typeCONFIRMATION
pour demander la confirmation avant de sortir correctement de l'application. - Écrire la méthode
void afficherDialogFinDePartie()
qui affiche une alerte de typeINFORMATION
pour dire au joueur en combien de coups il a terminé la partie. Pour cela, vous utiliserez la classeAlert
avec un titre et un contenu adapté. Le dialogue sera affiché et attendra que l'utilisateur le ferme.
La classe LightsOutMain
est le programme principal de notre application. C'est elle qui a la responsabilité de charger la vue principale et de l'ajouter à la scène.
-
Écrivez une méthode
main
aussi réduite que possible pour lancer l’exécution de tout cela. -
Écrire la méthode
public void start(Stage primaryStage)
. Elle devra :-
Modifier le titre de la fenêtre en "Lights Out".
-
Créer un objet
loader
du typeFXMLLoader
et charger leBorderPane
principal à partir du fichierLightsOutView.fxml
. -
Récupérer le contrôleur du type
LightsOutController
avec la méthodegetController()
duloader
. -
Appeler la méthode
setStageAndSetupListeners()
de la classeLightsOutController
qui rajoutera l'écouteur d’évènement de fermeture de la fenêtre principale. -
Ajouter le
BorderPane
comme racine du graphe de scène. -
Rendre visible le stage.
-