Interface graphique

ChaCuN – étape 8

1. Introduction

Le but de cette étape est de commencer à réaliser l'interface graphique du projet en écrivant les classes gérant l'affichage des différentes informations visibles à droite du plateau de jeu.

2. Concepts

2.1. Interface graphique

La figure 1 ci-dessous, déjà présentée dans l'introduction au projet, montre l'interface graphique de ChaCuN.

chacun-gui.jpg
Figure 1 : L'interface graphique de ChaCuN (cliquer pour agrandir)

La majeure partie de cette interface est occupée par le plateau de jeu (numéro 1 sur l'image), qui se trouve à gauche, et qui sera le sujet d'une étape ultérieure. Cette étape ci a pour but de mettre en œuvre la partie droite de l'interface, qui présente :

  • des informations sur les différents joueurs (numéro 2) : leur couleur, leur nom, leur nombre de points actuel, et les occupants qu'il leur reste en main,
  • le tableau d'affichage (numéro 3), sur lequel les messages informant les joueurs du déroulement de la partie sont affichés,
  • les deux tas de tuiles (numéro 4),
  • la tuile à poser (numéro 5), qui est remplacée par un texte lorsque le joueur courant a la possibilité de placer ou de reprendre un occupant.

Ces différents éléments de l'interface graphique sont décrits plus en détail dans les sections suivantes.

2.1.1. Informations sur les joueurs

La partie de l'interface présentant les informations sur les joueurs montre, pour chacun d'entre eux :

  • sa couleur, au moyen d'un petit disque coloré,
  • son nom,
  • le nombre de points qu'il a obtenu jusqu'à présent dans la partie,
  • ses occupants (hutte et pions).

La figure ci-dessous montre un exemple de cette partie de l'interface.

players-info;64.png
Figure 2 : Informations à propos des joueurs

Les joueurs sont présentés dans l'ordre de jeu, qui est toujours l'ordre des couleurs utilisé jusqu'à présent — à savoir rouge, bleu, vert, jaune et violet — et le joueur courant est entouré.

Pour distinguer les occupants qu'un joueur possède encore en mains de ceux qu'il a posé sur le plateau, l'opacité des icônes correspondant à ces derniers est de 0.1 uniquement. En d'autres termes, ces icônes sont transparentes à 90%, et donc à peine visibles.

On admet que les icônes correspondant aux éventuels occupants placés sur le plateau sont toujours celles qui se trouvent le plus à droite. Par exemple, si un joueur a actuellement deux pions placés sur le plateau, ce sont toujours les deux icônes des pions les plus à droite de sa ligne qui sont (quasi) transparentes.

2.1.2. Tableau d'affichage

Le tableau d'affichage montre la totalité des messages produits depuis le début de la partie.

Pour mémoire, à chaque message du tableau d'affichage peut être associé un ensemble de tuiles. Par exemple, lorsqu'un message annonce que certains joueurs ont remporté des points suite à la fermeture d'une forêt, les tuiles composant la forêt — leurs identifiants, pour être précis — sont associées au message.

Lorsque le pointeur de la souris survole l'un des messages du tableau d'affichage auquel des tuiles sont associées, ces tuiles sont mises en évidence sur le plateau. Cela permet aux joueurs de mieux comprendre la raison pour laquelle ils ont obtenu des points.

2.1.3. Tas de tuiles, et tuile à poser

Les deux tas de tuiles — celui des tuiles normales à gauche, celui des tuiles menhir à droite — sont représentés par une image du dos d'une tuile du type correspondant, au-dessus de laquelle est affiché le nombre de tuiles restant dans le tas. En dessous, la face de la tuile à placer est visible, comme on peut le voir dans la figure ci-dessous.

decks;64.png
Figure 3 : Tas de tuiles et tuile à placer

Lorsque le joueur courant peut placer ou reprendre un occupant, l'image de la tuile à placer est recouverte d'un texte d'instruction. La figure ci-dessous montre par exemple celui qui apparaît lorsqu'un occupant peut être placé.

decks-text-ui;64.png
Figure 4 : Tas de tuiles et message d'occupation

Comme le texte l'indique, en cliquant sur lui le joueur courant a la possibilité de déclarer qu'il ne désire pas poser (ou reprendre) un occupant.

2.2. Images

Les images utiles au projet — celles des tuiles principalement — vous ont été fournies avec le squelette du projet, et elles se trouvent dans le dossier resources de votre projet, et dans les sous-dossiers qu'il contient.

Au niveau supérieur, le dossier resources contient une image au format PNG, nommée marker.png, visible ci-dessous. Elle est destinée à être utilisée pour couvrir les animaux annulés.

marker.png
Figure 5 : L'image du jeton utilisé pour recouvrir les animaux

Les sous-dossiers nommés 256 et 512 contiennent les images des deux côtés des différentes tuiles, au format JPEG. Chacun de ces sous-dossiers contient une image de la face de chaque tuile, dans un fichier dont le nom est l'identifiant de la tuile, sur deux chiffres, suivi de l'extension .jpg. Par exemple, l'image de la face de la tuile 56 se trouve dans le fichier 56.jpg. De plus, chaque dossier contient une image du dos d'une tuile normale dans un fichier nommé NORMAL.jpg, et une image du dos d'une tuile menhir dans un fichier nommé MENHIR.jpg.

Les images des faces et des dos des tuiles sont toutes carrées, et celles du sous-dossier 256 font 256 pixels de côté, tandis que celles du sous-dossier 512 en font 512. Les grandes images (du dossier 512) sont destinées à être utilisées pour l'affichage de la prochaine tuile à poser, tandis que les images « normales » (du dossier 256) sont destinées à être affichées sur le plateau de jeu.

À l'écran, ces images seront toujours affichées à la moitié de leur taille réelle, afin qu'elles apparaissent nettes sur les écrans à haute résolution — dits « HiDPI » ou « Retina ». Ainsi, l'image utilisée pour afficher la tuile à poser proviendra toujours du sous-dossier 512, mais elle sera affichée à l'écran à une taille de 256 pixels.

L'effet de cette stratégie d'affichage se constate en comparant les deux images ci-dessous, qui montrent toutes les deux la face de la tuile 56. La première image provient du sous-dossier 256 et est affichée à sa taille réelle de 256 pixels de côté, tandis que la second provient du sous-dossier 512 et est affichée à la moitié de sa taille réelle, donc à 256 pixels. Sur un écran à haute résolution, la seconde apparaît plus nette que la première.

56_256.jpg
Figure 6 : La version 256 pixels de la tuile 56, affichée à sa taille réelle
56_512.jpg
Figure 7 : La version 512 pixels de la tuile 56, affichée à la moitié de sa taille

3. Mise en œuvre Java

Avant de commencer à programmer cette étape, il vous faut télécharger une archive Zip que nous mettons à votre disposition et qui contient trois fichiers nommés decks.css, message-board.css et players.css. Vous devez les placer dans votre projet, dans le dossier resources, donc au même niveau que l'image marker.png.

Ces fichiers sont des feuille de style (cascading style sheets ou CSS en anglais), qui décrivent l'aspect des éléments de l'interface graphique. Il n'est pas nécessaire de comprendre leur contenu, il vous faudra simplement les attacher à différents nœuds du graphe de scène, comme décrit plus bas. Les personnes intéressées à en savoir plus à leur sujet pourront néanmoins consulter le document JavaFX CSS Reference Guide.

3.1. Classe ImageLoader

La classe ImageLoader du sous-paquetage gui, publique et non instanciable, a pour but de charger les images des tuiles. Elle offre de plus un certain nombre de constantes publiques, statiques et finales donnant les dimensions des différentes images, et qui sont regroupées dans la table suivante :

Nom Valeur Signification
LARGE_TILE_PIXEL_SIZE 512 Taille des grandes tuiles
LARGE_TILE_FIT_SIZE 256 Taille d'affichage des grandes tuiles
NORMAL_TILE_PIXEL_SIZE 256 Taille des tuiles normales
NORMAL_TILE_FIT_SIZE 128 Taille d'affichage des tuiles normales
MARKER_PIXEL_SIZE 96 Taille du marqueur
MARKER_FIT_SIZE 48 Taille d'affichage du marqueur

ImageLoader offre deux méthodes publiques (et statiques), qui retournent chacune une valeur de type Image, et qui sont :

  • une méthode, nommée p. ex. normalImageForTile, qui prend en argument un identifiant de tuile et retourne l'image de 256 pixels de côté de la face de cette tuile,
  • une méthode, nommée p. ex. largeImageForTile, qui prend en argument un identifiant de tuile et retourne l'image de 512 pixels de côté de la face de cette tuile.

3.1.1. Conseils de programmation

Pour charger une image avec JavaFX, il suffit de passer au constructeur de Image une chaîne de caractère qui est le « nom de ressource » de l'image en question. Par exemple, le nom de ressource de la grande image de la tuile 56 est /512/56.jpg, et on peut donc l'obtenir au moyen de l'appel suivant :

Image tile56 = new Image("/512/56.jpg");

N'oubliez surtout pas la barre oblique (/) au début du nom !

3.2. Classe PlayersUI

La classe PlayersUI du sous-paquetage gui, publique et non instanciable, contient le code de création de la partie de l'interface graphique qui affiche les informations sur les joueurs, qui porte le numéro 2 sur la figure 1.

Comme toutes les classes chargées de créer une partie de l'interface graphique que nous définirons par la suite, elle n'offre qu'une seule méthode publique (et statique), nommée p. ex. create. Cette méthode prend en arguments :

  • la version observable de l'état actuel de la partie, c.-à-d. une valeur de type ObservableValue<GameState>,
  • un générateur de texte, c.-à-d. une valeur de type TextMaker,

et elle retourne le nœud JavaFX, de type Node, à la racine du graphe de scène dont elle est responsable, décrit à la section suivante. L'utilisation de la version observable de l'état actuel de la partie est quant à elle décrite dans les conseils de programmation plus bas.

3.2.1. Graphe de scène

Le graphe de scène créé par la méthode create est présenté à la figure 8 ci-dessous.

players-sg;32.png
Figure 8 : Graphe de scène des informations sur les joueurs

Sur cette image, chaque rectangle blanc représente un nœud JavaFX, et certains d'entre eux sont accompagnés d'annotations colorées qui donnent :

  • en vert, les feuilles de style à attacher au nœud en plaçant leur nom dans la liste retournée par getStylesheets,
  • en rouge, l'identité des nœuds, à définir au moyen de la méthode setId,
  • en bleu, les classes de style à associer au nœud en plaçant leur nom dans la liste retournée par getStyleClass.

À la racine du graphe de scène — au sommet de l'image — se trouve une instance de VBox dont le but est de contenir, et d'aligner verticalement, les éléments graphiques correspondant aux différents joueurs. À chaque joueur correspond une instance de TextFlow qui contient tous les éléments graphiques et textuels donnant les informations le concernant, à savoir :

  • une instance de Circle d'un rayon de 5 unités, colorée avec la couleur du joueur,
  • une instance de Text dont le texte est constitué d'une espace (pour séparer le disque coloré du nom), du nom du joueur, d'une espace, d'un deux-points, de son nombre de points actuel et d'un retour à la ligne (\n), par exemple :
    Dalia : 5 points,
  • trois instances de SVGPath montrant les huttes du joueur,
  • une instance de Text dont le texte est une chaîne composée de 3 espaces, qui sert à séparer visuellement les huttes des pions,
  • cinq instances de SVGPath montrant les pions du joueur.

Toutes les instances de TextFlow représentant un joueur doivent avoir la classe de style player attachée à elles. Une seule d'entre elles, celle qui correspond au joueur courant, doit en plus avoir la classe de style current attachée à elle — c'est la raison pour laquelle cette classe de style apparaît en bleu clair sur la figure 8.

3.2.2. Conseils de programmation

  1. Joueurs participant à une partie

    Pour construire le graphe de scène, la méthode create doit savoir quels joueurs participent à la partie. Pour cela, elle peut simplement parcourir la totalité des couleurs de joueurs (PlayerColor.ALL) et ignorer celles pour lesquelles la méthode playerName de l'instance de TextMaker qu'elle reçoit retourne null.

    Le fait que la méthode playerName retourne null lorsqu'elle ne connaît pas le nom correspondant à une couleur de joueur n'était malheureusement pas mentionné dans l'interface TextMaker fournie, donc assurez-vous que c'est bien ce que fait votre mise en œuvre.

  2. Version observable de l'état de la partie

    L'état de la partie est fourni à la méthode create de PlayersUI sous la forme d'une valeur de type ObservableValue<GameState>. Pour mémoire, l'interface ObservableValue de JavaFX correspond à ce que nous avons appelé un sujet (Subject) dans le cours sur le patron Observer.

    La valeur passée à create peut donc être vue comme une espèce de cellule de tableur observable qui contient une valeur de type GameState, qui est l'état actuel de la partie. Le contenu de cette cellule change au cours du jeu, en fonction des règles et des actions effectuées par les joueurs. Tous les éléments de l'interface graphique qui dépendent de l'état de la partie observent donc cette valeur, et se mettent à jour lorsqu'elle change d'une manière qui les affecte.

    La manière évidente de réagir aux changements de cette valeur observable est de lui attacher un auditeur (listener), qui pour mémoire est le terme que JavaFX utilise pour désigner ce que le patron Observer appelle un observateur. Par exemple, pour afficher le joueur courant sur la console chaque fois que l'état de la partie change, on peut ajouter un auditeur à la valeur observable contenant l'état ainsi :

    ObservableValue<GameState> gameStateO = …;
    
    gameStateO.addListener((o, oldState, newState) ->
      System.out.println(newState.currentPlayer()));
    

    Comme cet exemple l'illustre, l'auditeur passé à addListener est généralement spécifié sous la forme d'une lambda qui prend trois arguments : la valeur observable qui a changé (o ci-dessus, qui dans cet exemple sera toujours identique à gameStateO), son ancien contenu (oldState ci-dessus, de type GameState) et son nouveau contenu (newState ci-dessus, de type GameState également).

    L'utilisation d'auditeurs fonctionne, mais JavaFX offre depuis peu une méthode map sur les valeurs observables qui permet souvent d'écrire du code plus simple que celui qu'on obtient avec des auditeurs. Cette méthode map fonctionne de manière similaire à la méthode map des flots (Stream), ce qui n'est pas étonnant sachant qu'une valeur observable, qui change au cours du temps, peut être vue comme le flot de ses valeurs successives ! Par exemple, une valeur observable qui contient successivement les entiers 1, 2 et 3 est très similaire à un flot contenant ces mêmes trois valeurs, dans le même ordre.

    L'exemple plus haut peut être récrit pour utiliser cette méthode map afin d'obtenir, à partir de la valeur observable contenant l'état complet du jeu, une nouvelle valeur observable qui ne contient que le joueur courant. Un auditeur peut ensuite être ajouté sur cette valeur observable « dérivée » :

    ObservableValue<GameState> gameStateO = …;
    
    ObservableValue<PlayerColor> currentPlayerO =
      gameStateO.map(GameState::currentPlayer);
    
    currentPlayerO.addListener((o, oldPlayer, newPlayer) ->
      System.out.println(newPlayer));
    

    Encore une fois, il est important de voir que ce code est très similaire à celui que l'on pourrait écrire pour faire quelque chose de semblable avec un flot de valeurs de type GameState :

    Stream<GameState> gameStateS = …;
    
    Stream<PlayerColor> currentPlayerS =
      gameStateS.map(GameState::currentPlayer);
    
    currentPlayerS.forEach(player ->
      System.out.println(player));
    

    Il est fortement recommandé d'utiliser cette méthode map autant que possible dans ce projet, car elle permet de simplifier énormément le code de l'interface graphique. Comme PlayersUI est la première classe dans laquelle vous devez l'utiliser, quelques indications supplémentaires sont données dans les sections qui suivent.

  3. Points des joueurs

    Dans le but d'afficher, et de garder à jour, les points des joueurs, la méthode create peut utiliser la méthode map en deux temps : tout d'abord pour obtenir une valeur observable contenant une table associant ses points à chaque joueur (obtenue du tableau d'affichage) ; puis pour obtenir de cette valeur observable-là une valeur observable qui contient le texte à afficher dans l'interface. Cette seconde valeur observable peut directement être liée à la propriété textProperty du nœud JavaFX de type Text correspondant au joueur, grâce à la méthode bind, garantissant ainsi la mise à jour automatique du texte !

    Cette idée est résumée dans le squelette de code ci-dessous, dont vous pouvez vous inspirer pour écrire votre méthode create :

    // Valeur observable contenant l'état du jeu
    ObservableValue<GameState> gameStateO = …;
    
    // Valeur observable contenant les points des joueurs
    ObservableValue<Map<PlayerColor, Integer>> pointsO =
      gameStateO.map(…);
    
    for (PlayerColor p: PlayerColor.ALL) {
      // Valeur observable contenant le texte des points
      // du joueur de couleur `p` (p.ex. "Dalia : 5 points")
      ObservableValue<String> pointsTextO =
        pointsO.map(…);
    
      // Nœud JavaFX affichant le nom et les points du joueur
      // de couleur `p` (mis à jour automatiquement !)
      Text pointsText = new Text();
      pointsText.textProperty().bind(pointsTextO);
    
      // …
    }
    
  4. Joueur courant

    Comme expliqué plus haut, le joueur courant est toujours entouré d'un cadre gris. L'aspect du cadre est défini dans la feuille de style fournie, et il est automatiquement attaché à toute instance de TextFlow qui possède la classe de style current.

    Cela implique que la méthode create doit faire en sorte que, chaque fois que le joueur courant change, la classe de style current soit ajoutée à celles du nœud TextFlow qui représente le nouveau joueur courant, et supprimée de celles des autres nœuds TextFlow.

    Cela peut se faire assez facilement en utilisant tout d'abord la méthode map pour obtenir une valeur observable contenant le joueur courant, puis en lui attachant un auditeur modifiant les classes de style du nœud TextFlow.

  5. Occupants placés

    Pour faire en sorte que les occupants que chaque joueur a placé sur le plateau soient quasi transparents, il est possible une fois encore d'utiliser la méthode map pour obtenir, pour chaque occupant, une valeur observable valant 1 si cet occupant est dans les mains de l'utilisateur, et 0.1 sinon. Cette valeur observable peut ensuite être directement liée, avec la méthode bind, à la propriété opacity du nœud correspondant à l'icône de l'occupant.

3.3. Classe MessageBoardUI

La classe MessageBoardUI, du sous-paquetage gui, publique et non instanciable, contient le code de création de l'interface graphique du tableau d'affichage, qui porte le numéro 3 sur la figure 1.

Cette classe ne possède qu'une seule méthode publique (et statique), nommée p. ex. create, et qui prend en arguments :

  • la version observable des messages affichés sur le tableau d'affichage, c.-à-d. une valeur de type ObservableValue<List<MessageBoard.Message>>,
  • une propriété JavaFX contenant l'ensemble des identités des tuiles à mettre en évidence sur le plateau, de type ObjectProperty<Set<Integer>>.

Le premier argument est destiné à être observé au moyen d'un auditeur, dont le but est d'ajouter au graphe de scène la représentation graphique des nouveaux messages, chaque fois qu'il y en a.

Le second argument est destiné à être modifié de manière à ce que, lorsque le pointeur de la souris survole un message, les identités des tuiles qui lui sont associées — s'il y en a — soient stockées dans la propriété.

3.3.1. Graphe de scène

Le graphe de scène du tableau d'affichage est assez simple pour ne pas nécessiter de dessin. Il consiste en :

  • une instance de ScrollPane à la racine, avec l'identité message-board et auquel la feuille de style nommée message-board.css est attachée ; son but est de permettre aux joueur de faire défiler la totalité des messages affichés sur le tableau, même s'il y en a trop pour qu'ils puissent tous être affichés simultanément,
  • une instance de VBox comme unique enfant de ce « panneau de défilement », qui aligne verticalement les messages,
  • comme enfants de cette VBox, une instance de Text par message du panneau d'affichage.

Initialement, aucune instance de Text n'existe, car on fait l'hypothèse que le tableau d'affichage est vide au moment de l'appel à create. La manière dont les instances de Text sont créées pour les nouveaux messages est décrite ci-dessous.

3.3.2. Conseils de programmation

  1. Affichage des messages

    La méthode create doit faire en sorte que, chaque fois que la liste des messages affichés sur le tableau d'affichage change, le graphe de scène change également afin d'incorporer les nouveaux messages — ou, pour être précis, leur représentation graphique.

    Pour ce faire, create peut installer un auditeur sur la valeur observable qu'on lui passe, qui se charge de créer les instances de Text pour les nouveaux messages. Cet auditeur peut faire l'hypothèse que le nouveau nombre de messages est toujours supérieur ou égal à l'ancien, et que l'ancienne liste de message constitue un préfixe de la nouvelle.

    Les instances de Text contenant le texte des messages doivent être configurées de manière à ce que leur largeur ne dépasse pas la largeur d'une grande image de tuile. Cela peut se faire au moyen de la méthode setWrappingWidth à laquelle on passe la constante LARGE_TILE_FIT_SIZE.

    Une fois toutes les nouvelles instances de Text créées, l'auditeur doit faire défiler le panneau de défilement afin que le dernier message soit visible. Il faut pour cela utiliser la méthode setVvalue, mais l'appel doit être retardé au moyen de runLater, faute de quoi il n'a aucun effet. Cela peut se faire ainsi :

    ScrollPane messageScrollPane = …;
    runLater(() -> messageScrollPane.setVvalue(1))
    

    Malheureusement, vous constaterez que cet appel ne suffit pas toujours à faire en sorte que le dernier message soit visible, pour des raisons actuellement inconnues.

  2. Mise en évidence des tuiles

    Comme expliqué plus haut, lorsque la souris survole un message du tableau d'affichage et qu'un ensemble non vide d'identités de tuiles lui est associé, ces tuiles doivent être mises en évidence sur le plateau.

    Dans ce but, la méthode create reçoit une propriété JavaFX contenant un ensemble d'entiers, et elle doit faire en sorte que cette propriété contienne l'ensemble des identités des tuiles associé au message actuellement survolé par la souris, s'il y en a un, et un ensemble vide sinon. Le contenu de cette propriété sera utilisé ultérieurement pour mettre les tuiles correspondantes en évidence.

    Pour mémoire, une propriété JavaFX est similaire à une cellule d'un tableur contenant une valeur — par opposition à une cellule contenant une formule. La valeur stockée dans la propriété peut être modifiée au moyen de sa méthode setValue.

    Afin de déterminer quand la souris survole un message, il suffit d'utiliser les méthodes setOnMouseEntered et setOnMouseExited de l'instance de Text représentant le message afin d'installer des gestionnaires d'événements modifiant le contenu de la propriété.

3.4. Classe DecksUI

La classe DecksUI, du sous-paquetage gui, publique et non instanciable, contient le code de création de la partie de l'interface graphique qui affiche les tas de tuiles ainsi que la tuile à poser, qui portent les numéros 4 et 5 sur la figure 1.

L'unique méthode publique (et statique) de DecksUI, nommée p. ex. create, prend en arguments :

  • la version observable de la tuile à placer, de type ObservableValue<Tile>,
  • la version observable du nombre de tuiles restantes dans le tas des tuiles normales, de type ObservableValue<Integer>,
  • la version observable du nombre de tuiles restantes dans le tas des tuiles menhir,
  • la version observable du texte à afficher à la place de la tuile à placer, de type ObservableValue<String>,
  • un gestionnaire d'événement destiné à être appelé lorsque le joueur courant signale qu'il ne désire pas poser ou reprendre un occupant, en cliquant sur le texte affiché à la place de la prochaine tuile, de type Consumer<Occupant>.

Le texte à afficher à la place de la tuile à placer peut soit être une chaîne vide, ce qui signifie que la tuile doit être visible (comme à la figure 3), soit une chaîne non vide, ce qui signifie que son texte doit être placé au-dessus de la tuile (comme à la figure 4).

Lorsqu'un texte est affiché à la place de la tuile à placer, et que l'utilisateur clique dessus, alors la méthode accept du gestionnaire d'événement passé en dernier argument doit être appelée avec null en argument. C'est de cette manière que le reste du programme pourra être informé du fait que le joueur ne désire pas placer (ou reprendre) un occupant.

3.4.1. Graphe de scène

Le graphe de scène construit par la méthode create est visible dans la figure ci-dessous.

decks-sg;32.png
Figure 9 : Graphe de scène des tas de tuiles et de la prochaine tuile

Afin de garantir que le texte affiché par l'instance de Text qui peut recouvrir la tuile à placer ne soit pas plus large que la tuile qu'il recouvre, sa méthode setWrappingWidth doit être appelée avec une taille égale à 80% de la largeur de la tuile recouverte.

3.4.2. Conseils de programmation

Pour forcer les images des tuiles à être affichées à la taille désirée — c.-à-d. à la moitié de leur taille réelle — les méthodes setFitWidth et setFitHeight de ImageView sont utiles.

Pour faire en sorte que le texte qui peut recouvrir la tuile à placer ne soit visible que lorsqu'il n'est pas vide, il faut lier, au moyen de bind, sa propriété visibleProperty à une expression qui n'est vraie que dans ce cas-là. Bien entendu, cette expression s'obtient au moyen de la méthode map.

3.5. Tests

Les classes de cette étape sont difficiles à tester avec des tests unitaires. Nous vous conseillons donc plutôt d'écrire une petite application JavaFX de test et de vérifier « manuellement » qu'elle se comporte correctement.

Par exemple, l'application ci-dessous créée un état de jeu puis utilise PlayersUI pour construire l'interface graphique montrant les informations sur les joueurs :

public final class PlayersUITest extends Application {
  public static void main(String[] args) { launch(args); }

  @Override
  public void start(Stage primaryStage) {
    var playerNames = Map.of(PlayerColor.RED, "Rose",
                             PlayerColor.BLUE, "Bernard");
    var playerColors = playerNames.keySet().stream()
      .sorted()
      .toList();

    var tilesByKind = Tiles.TILES.stream()
      .collect(Collectors.groupingBy(Tile::kind));
    var tileDecks =
      new TileDecks(tilesByKind.get(Tile.Kind.START),
                    tilesByKind.get(Tile.Kind.NORMAL),
                    tilesByKind.get(Tile.Kind.MENHIR));

    var textMaker = new TextMakerFr(playerNames);

    var gameState =
      GameState.initial(playerColors,
                        tileDecks,
                        textMaker);

    var gameStateO = new SimpleObjectProperty<>(gameState);

    var playersNode = PlayersUI.create(gameStateO, textMaker);
    var rootNode = new BorderPane(playersNode);
    primaryStage.setScene(new Scene(rootNode));

    primaryStage.setTitle("ChaCuN test");
    primaryStage.show();
  }
}

En l'exécutant, vous devriez voir la fenêtre ci-dessous s'afficher à l'écran.

players-ui-test;32.png
Figure 10 : Fenêtre du programme PlayersUITest

4. Résumé

Pour cette étape, vous devez :

  • écrire les classes ImageLoader, PlayersUI, MessageBoardUI et DecksUI selon les indications données ci-dessus,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies.

Aucun rendu n'est à faire pour cette étape avant le rendu final. N'oubliez pas de faire régulièrement des copies de sauvegarde de votre travail en suivant nos indications à ce sujet.