Plateau de jeu

Ajul – étape 9

1. Introduction

Le but de cette étape est d'écrire la classe qui construit et gère le plateau de jeu.

2. Concepts

2.1. Plateau de jeu

Le plateau de jeu (game board) est le nom que nous donnerons à l'arrière-plan de l'interface graphique, au-dessus duquel les tuiles se déplacent. Cette partie de l'interface est presque totalement statique, dans le sens où ce qu'elle affiche ne change que très peu, et les joueurs ne peuvent pas vraiment interagir avec elle.

La figure 1 ci-dessous montre le plateau de jeu d'une partie à quatre joueurs, tel qu'affiché par le programme de test décrit à la fin de cette étape.

board-ui-4;64.png
Figure 1 : Plateau de jeu d'une partie à 4 joueurs

Comme cette image l'illustre, le plateau de jeu est composé de deux parties :

  • à gauche, une partie commune à tous les joueurs, où se trouvent les sources de tuiles — les fabriques en haut, la zone centrale en bas,
  • à droite, les plateaux individuels des joueurs, où se trouvent leur nom, score actuel, lignes de motif, mur et ligne plancher.

Ces deux parties sont décrites individuellement dans les sections qui suivent.

2.1.1. Sources de tuiles

Les sources de tuiles qui occupent la partie gauche du plateau sont organisées ainsi :

  • dans la partie supérieure se trouvent les fabriques, groupées par paires,
  • dans la partie inférieure se trouve la zone centrale.

Les sources de tuiles ne sont pas aussi vides que leur apparence pourrait le laisser penser, puisque chaque fabrique est occupée par 4 ancres, et la zone centrale par un nombre d'ancres égal à sa taille maximale. Ces ancres sont normalement invisibles, mais elles ont été rendues visibles dans la figure 2 ci-dessous.

tile-sources-ui;16.png
Figure 2 : Sources de tuiles pour 2 joueurs, avec ancres visibles

Comme on le voit, les ancres de la zone centrale sont organisées par lignes de 8, afin que la largeur de cette zone soit approximativement égale à celle de deux fabriques.

2.1.2. Plateaux des joueurs

Chaque joueur possède un plateau individuel sur lequel figurent :

  1. son nom et score actuel, dans le coin haut-gauche,
  2. ses 5 lignes de motif, dans la partie supérieure gauche,
  3. son mur, dans la partie supérieure droite,
  4. ses points bonus obtenus en fin de partie, sur le pourtour du mur,
  5. sa ligne plancher, dans la partie inférieure, avec ses pénalités.

Comme nous l'avons vu à l'étape précédente, les cases des lignes de motif, du mur et de la ligne plancher ne sont rien d'autre que les ancres correspondantes qui, contrairement à celles des sources, sont visibles.

La figure 3 ci-dessous montre le plateau d'une joueuse nommée Anne, sur lequel tous les points bonus, invisibles la plupart du temps, ont été rendus visibles.

player-board;64.png
Figure 3 : Plateau d'une joueuse nommée Anne

Il faut noter que, même lorsqu'ils sont invisibles, les points bonus occupent de la place. De la sorte, au moment où certains d'entre eux apparaissent en fin de partie, la mise en page du plateau ne change pas. Bien entendu, à ce moment-là, seuls les points effectivement obtenus par un joueur apparaissent en bordure de son mur, ainsi :

  • sur le côté droit, un +2 apparaît à côté de chaque ligne complète,
  • sur le côté inférieur, un +7 apparaît sous chaque colonne complète,
  • sur le côté gauche, un +10 apparaît à côté de chaque tuile dont la couleur est complète.

Les cases de la ligne plancher sont également accompagnées de la pénalité qui leur est associée, et qui figure sous chacune d'entre elles. Contrairement aux points bonus, ces pénalités sont toujours visibles.

2.1.3. Destinations interactives

Comme nous l'avons dit, la représentation graphique du plateau de jeu est en grande partie figée et les joueurs ne peuvent généralement pas interagir avec elle. Une exception à cette règle est que, pour jouer un coup, un joueur humain clique sur un groupe de tuiles de même couleur se trouvant sur une source, le déplace sur la destination de son plateau sur laquelle il désire le déposer, puis relâche le bouton de la souris.

Dès lors, les destinations — lignes de motif et ligne plancher — des plateaux des joueurs doivent gérer cette interaction. Cela implique entre autres que, lorsque le joueur est en train de déplacer des tuiles et qu'il survole une destination de son plateau à même de les accueillir, cette destination est mise en évidence. Cela est visible dans la figure 4 ci-dessous, dans laquelle la troisième ligne de motif est mise en évidence.

player-board-accepting;64.png
Figure 4 : Ligne de motif mise en évidence

Afin qu'une destination puisse savoir quand se mettre en évidence, il faut qu'elle sache si les tuiles actuellement déplacées par le joueur peuvent être déposées sur elle.

Pour ce faire, dès que le joueur commence à déplacer les tuiles d'une source et d'une couleur données, l'ensemble des coups valides ayant cette source et cette couleur est calculé et mis à disposition des destinations. Cet ensemble — qui contient entre 1 et 6 éléments, un par destination au maximum — peut facilement être utilisé par une destination pour déterminer si elle doit se mettre en évidence lorsqu'elle est survolée par des tuiles ; il lui suffit en effet de regarder s'il inclut un coup avec elle comme destination.

Si les tuiles déplacées par le joueur sont lâchées sur une destination à même de les accueillir, alors elle peut déterminer quel coup a été joué en regardant lequel des coups de l'ensemble susmentionné l'a elle comme destination.

3. Mise en œuvre Java

Avant de commencer la rédaction de cette étape, vous devez télécharger un fichier nommé ajul.css que nous mettons à votre disposition. Ce fichier contient ce que l'on nomme une feuille de style en cascade (cascading style sheet, abrégé CSS en anglais), qui décrit l'aspect des éléments de l'interface graphique. Il n'est pas nécessaire de comprendre son contenu, il vous faudra simplement l'attacher à la racine du graphe de scène, comme illustré par le programme de test de la §3.2. Les personnes intéressées à en savoir plus sur les feuilles de style JavaFX pourront consulter le JavaFX CSS Reference Guide.

Pour importer correctement le fichier ajul.css dans votre projet, vous devez :

  • dans la partie Project d'IntelliJ, faire un clic droit sur la racine de votre projet, qui se nomme normalement Ajul,
  • dans le menu qui s'ouvre alors, sélectionner New puis Directory pour créer un nouveau dossier, que vous nommerez resources,
  • faire un clic droit sur ce dossier nouvellement créé puis, dans le menu qui s'ouvre, sélectionner Mark Directory As puis Resources Root,
  • copier — p. ex. par glisser-déposer — le fichier ajul.css dans ce dossier.

Une fois cela fait, vous pouvez lire la section qui suit et commencer à rédiger la classe BoardUI. Comme elle est d'une taille assez conséquente, nous vous conseillons toutefois de ne pas la rédiger d'une traite avant de la tester, mais de procéder par étapes :

  1. Écrivez une première version de la classe qui ne construit que la partie gauche du plateau de jeu, celle qui contient les sources de tuiles. Vérifiez qu'elles s'affichent correctement au moyen du programme de test de la §3.2.
  2. Augmentez votre classe pour y ajouter les plateaux des joueurs, en procédant là aussi par petites étapes :

    • commencez par ne construire que la ligne plancher et vérifiez qu'elle s'affiche correctement,
    • continuez en ajoutant les lignes de motif et le mur, y compris les points bonus que vous pouvez laisser visibles pour le test,
    • ajoutez finalement les informations concernant le joueur — nom et points.

    Ne faites que la partie « statique » des plateaux des joueurs, sans gérer les aspects dynamiques — ni la mise à jour des points ou du joueur courant, ni l'interaction avec les destinations.

  3. Terminez votre classe en lui ajoutant les aspects dynamiques :
    • utilisez les mécanismes d'observation de JavaFX pour que les scores des joueurs se mettent à jour lorsqu'ils changent,
    • faites de même pour le joueur courant, dont le plateau doit être entouré d'un cadre plus foncé que celui des autres joueurs,
    • ajoutez des gestionnaires d'événements aux destinations pour qu'elles gèrent correctement le glisser/déposer des tuiles.

3.1. Classe BoardUI

La classe BoardUI du sous-paquetage gui, publique et finale, gère la partie de l'interface graphique qui représente le plateau de jeu.

Son constructeur est privé, la création d'une instance devant se faire au moyen d'une méthode fabrique nommée p. ex. create. Cette méthode est celle qui contient la quasi-totalité du code de cette étape, car son but est de construire l'interface graphique du plateau de jeu. Pour ce faire, elle prend les arguments suivants :

  • la table associant les ancres à l'emplacement auquel elles correspondent, de type Map<TileLocation, Node>, et qui n'est rien d'autre que la table contenue dans l'enregistrement Tiles décrit à l'étape précédente,
  • la valeur observable contenant l'état immuable (!) de la partie, de type ObservableValue<ImmutableGameState>ObservableValue étant le type JavaFX des sujets d'observation, qui correspond au type Subject du patron Observer.

En plus de ces deux arguments, dont l'utilité devrait être claire, create en prend trois autres dont le seul but est de gérer le glisser/déposer des tuiles sur une destination. Ces arguments sont décrits ici dans un souci de cohérence, mais il est conseillé de les ignorer initialement, et de ne les ajouter que lorsque la partie « statique » de l'interface est terminée, et seule la partie dynamique manque encore. Il s'agit de :

  • l'ensemble qui, lorsqu'un joueur humain sélectionne un groupe de tuiles d'une couleur donnée se trouvant sur une source donnée, contient tous les coups valides correspondants, de type Set<Move>,
  • une valeur booléenne destinée à être mise à vrai si et seulement si les tuiles déplacées ont été lâchées sur une destination à même de les accueillir, de type boolean[] (voir ci-dessous pour une explication),
  • une file bloquante destinée à contenir le coup joué par le joueur, de type BlockingQueue<Move>.

La raison pour laquelle la valeur booléenne est passée sous la forme d'un tableau — à un seul élément — est que c'est la manière la plus simple, en Java, de passer une valeur booléenne à une méthode dans le but qu'elle la modifie.

Il est important de comprendre que différentes parties du programme ont la responsabilité de modifier le contenu des trois valeurs susmentionnées :

  • l'ensemble des coups est rempli par le composant gérant les tuiles — qui sera écrit à l'étape suivante — et les destinations ne font qu'examiner son contenu,
  • la valeur booléenne est mise à vrai par la destination sur laquelle les tuiles ont été déposées, ssi cette destination est à même de les accueillir ; elle est ensuite consultée puis réinitialisée — c.-à-d. remise à faux — par le composant gérant les tuiles,
  • le coup joué est placé dans la file bloquante par la destination sur laquelle les tuiles ont été déposées, ssi cette destination est à même de les accueillir ; une fois placé dans la file, le coup en sera par la suite extrait par la partie du programme gérant le jeu — qui sera le sujet de la dernière étape.

La méthode create crée la totalité du graphe de scène correspondant au plateau de jeu, et passe ensuite sa racine au constructeur (privé), qui la stocke dans un attribut de la classe. C'est cet attribut qui est retourné par la méthode root décrite ci-après.

En plus de cette méthode fabrique, BoardUI offre les méthodes suivantes :

  • une méthode nommée p. ex. root, qui retourne le nœud JavaFX, de type Node, à la racine du graphe de scène représentant le plateau de jeu,
  • une méthode nommée p. ex. showBonusPoints, qui prend en argument l'identité d'un joueur, de type PlayerId, et une « clef de bonus », de type Object, et qui rend visible les points bonus correspondants.

La « clef de bonus » passée à showBonusPoints peut être de trois types différents, en fonction de la catégorie des points bonus à afficher :

  1. TileDestination.Pattern, qui signifie que les points bonus (+2) associés à cette ligne doivent être affichés,
  2. Integer, qui signifie que les points bonus (+7) associés à cette colonne doivent être affichés,
  3. TileKind.Colored, qui signifie que les points bonus (+10) associés à cette couleur doivent être affichés.

Voir les conseils de programmation plus bas pour comprendre comment écrire showBonusPoints.

3.1.1. Graphe de scène

Le graphe de scène du plateau est présenté à la figure 5 ci-dessous. Sur cette figure, chaque rectangle blanc représente un nœud JavaFX du type écrit en son centre, et certains d'entre eux sont accompagnés d'annotations colorées qui donnent :

  • en rouge, l'identité du nœud, à 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.

Les parties du graphe de scène correspondant aux principaux éléments du plateau sont nommées et entourées d'un traitillé.

board-sg;16.png
Figure 5 : Graphe de scène du plateau de jeu

Le triangle nommé P1 représente le graphe de scène du plateau du joueur 1, et a la structure présentée à la figure 6 ci-dessous. Il en va de même pour le triangle nommé P2, qui représente le graphe de scène du plateau du joueur 2, et ainsi de suite pour les éventuels autres joueurs.

player-board-sg;16.png
Figure 6 : Graphe de scène d'un plateau de joueur

Dans la figure ci–dessus, la classe de style current-player est en bleu clair car elle ne doit être attachée qu'à l'instance de StackPane correspondant au plateau du joueur courant. C'est grâce à elle que le bord du plateau du joueur courant est plus foncé que le bord des autres plateaux.

Le contenu exact de la grille qui contient les lignes de motif et le mur du plateau d'un joueur n'est pas représenté dans la figure 6. Les cinq premières lignes de cette grille ont toutes la même structure et consistent en :

  • dans la première colonne, la ligne de motif, constituée d'une HBox contenant les ancres et ayant les classes de style tile-destination et tile-group,
  • dans la seconde colonne, le texte de bonus de couleur (+10),
  • dans les 5 colonnes suivantes, les cases de la ligne du mur correspondant à la ligne de motif, qui ont la classe de style wall-background ainsi que celle correspondant à la couleur de la case (A, B, C, D ou E),
  • dans la huitième colonne, le texte de bonus de ligne (+2).

La sixième et dernière ligne de cette grille contient, dans les colonnes correspondant à celles du mur, le texte de bonus de colonne (+7).

3.1.2. Conseils de programmation

  1. Panneau GridPane

    Le panneau de type GridPane de JavaFX joue un rôle important dans cette étape, car il apparaît à de nombreuses reprises. Il est donc impératif que vous lisiez sa documentation avant de commencer à l'utiliser.

    Notez en particulier que certains éléments des grilles doivent être alignés à droite ou centrés, ce qui peut se faire au moyen de la méthode (statique !) setHalignment. De plus, les composants représentant les lignes de motif ne doivent pas être redimensionnés pour remplir la totalité de la largeur de leur colonne, ce qui peut se faire au moyen de la méthode (statique également !) setFillWidth.

  2. Visibilité des points bonus

    Les textes correspondant aux points bonus doivent être initialement invisibles, et ne devenir visibles que lorsque la méthode showBonusPoints est appelée. La visibilité d'un nœud JavaFX peut être contrôlée au moyen de sa méthode setVisible.

    La méthode showBonusPoints doit rendre visible, sur le plateau d'un joueur donné, les points bonus associés à une ligne, une colonne ou une couleur. Une manière simple de faire cela est de créer, au moment de la construction du graphe de scène, une table associant les nœuds correspondants à ces bonus — des instances de Text — à des paires (identité de joueur, clef de bonus). La clef de bonus est, selon les cas, une valeur de type Pattern, Integer ou Colored. Grâce à cette table, showBonusPoints s'écrit en une seule ligne.

  3. Mise à jour des points des joueurs

    Le texte affichant, entre autres, les points d'un joueur doit être mis à jour lorsque ceux-ci changent. Cela peut se faire assez aisément au moyen de la méthode format de la classe Bindings, qui est une variante dynamique de la méthode format de String. C'est-à-dire que Bindings.format accepte comme arguments des valeurs observables, et produit une valeur (chaîne) observable.

    La valeur observable à lui passer est bien entendu celle des points du joueur auquel le plateau correspond. Pour l'obtenir, le plus simple est d'utiliser la méthode map sur la valeur observable représentant l'état de la partie. Cette méthode se comporte de manière assez similaire à map sur les flots, dans le sens où elle permet d'obtenir une valeur observable en appliquant une fonction à une autre valeur observable.

  4. Classe de style current-player

    Comme cela a été dit plus haut, la classe de style current-player ne doit être attachée qu'au plateau du joueur courant. Pour faire cela, le plus simple est d'attacher un observateur à la valeur observable représentant le joueur courant, et d'ajouter ou de supprimer la classe de style lorsqu'elle change.

    La valeur observable représentant le joueur courant s'obtient une fois encore en appliquant la méthode map à la valeur observable représentant l'état de la partie. Pour lui attacher un observateur, il est conseillé d'utiliser la méthode subscribe, que l'on peut voir comme l'équivalent JavaFX de la méthode addObserver du patron Observer.

  5. Gestion des événements

    Comme cela a été expliqué à la §2.1.3, les joueurs humains peuvent interagir avec les destinations en déplaçant des tuiles sur elles afin de jouer un coup.

    Pour que cette interaction soit possible, il faut que chacune des instances de HBox représentant une destination — ligne de motif ou plancher — gère trois types d'événements JavaFX, à savoir :

    • MOUSE_DRAG_ENTERED, qui se produit lorsque le pointeur de la souris entre dans une destination alors que le joueur déplace des tuiles,
    • MOUSE_DRAG_EXITED, qui se produit lorsque le pointeur de la souris sort d'une destination alors que le joueur déplace des tuiles,
    • MOUSE_DRAG_RELEASED, qui se produit lorsque l'utilisateur relâche le bouton de la souris alors qu'il déplace des tuiles et que son pointeur se trouve à l'intérieur d'une destination.

    Lorsque le premier événement se produit, la classe de style accepting doit être ajoutée au nœud HBox représentant la destination ssi elle peut accepter les tuiles. Lorsque le second événement se produit, cette même classe de style doit en être supprimée. Et finalement, lorsque le troisième événement se produit, le coup doit être accepté, ce qui implique que la valeur booléenne passée à create doit être mise à vrai, et le coup joué doit être placé dans la file bloquante.

    Notez toutefois qu'il est difficile d'écrire le code gérant ces événements — et aussi de le tester — sans avoir écrit celui gérant les tuiles, qui sera le sujet de l'étape suivante. Il peut donc être préférable de ne l'écrire qu'au même moment que le code de cette étape-là.

3.2. Tests

Pour tester votre classe BoardUI, il ne sert à rien d'écrire des tests unitaires, car de manière générale le test unitaire d'interfaces graphiques est très difficile. Nous vous conseillons donc d'écrire plutôt une petite application JavaFX affichant le plateau de jeu dans une fenêtre, qui pourrait ressembler à ceci :

public final class TestBoardUI extends Application {
  @Override
  public void start(Stage primaryStage) {
    int playersCount = 2;
    Game game = new Game(PlayerId.ALL.stream()
      .map(pId ->
             new Game.PlayerDescription(pId, pId.name(), HUMAN))
      .limit(playersCount)
      .toList());
    ImmutableGameState initialGameState =
      ImmutableGameState.initial(game);

    ObservableValue<ImmutableGameState> gameStateP =
      new SimpleObjectProperty<>(initialGameState);
    Set<Move> potentialMoves = new HashSet<>();
    boolean[] moveAccepted = new boolean[]{false};
    BlockingQueue<Move> moveQueue = new SynchronousQueue<>();

    Tiles tiles = Tiles.create(game);
    BoardUI boardUI = BoardUI.create(tiles.anchors(),
                                     gameStateP,
                                     potentialMoves,
                                     moveAccepted,
                                     moveQueue);
    Parent root = new StackPane(boardUI.root());
    root.getStylesheets().add("ajul.css");

    primaryStage.setScene(new Scene(root));
    primaryStage.setTitle(TestBoardUI.class.getSimpleName());
    primaryStage.show();
  }
}

En l'exécutant, vous devriez voir une fenêtre similaire à celle de la figure 1 plus haut. Si vous constatez des différences au niveau de l'affichage, vous pouvez essayer d'ajouter temporairement les lignes suivantes à la feuille de style fournie (ajul.css) :

GridPane {
    -fx-grid-lines-visible: true
}

Cela a pour effet d'afficher des lignes entourant et séparant toutes les cases des panneaux de type GridPane, ce qui permet de visualiser leur structure. Vous devriez voir quelque chose de similaire à la figure 7 ci-dessous.

board-ui-4_gridlines;64.png
Figure 7 : Plateau de jeu avec lignes visibles

4. Résumé

Pour cette étape, vous devez :

  • écrire la classe BoardUI selon les indications données plus haut,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies.

Vous n'avez pas l'obligation de rendre cette étape avant le rendu final, mais nous vous conseillons néanmoins de le faire afin de sauvegarder votre travail.