Interface graphique

tCHu – étape 10

1. Introduction

Le but de cette étape est de terminer l'interface graphique de tCHu commencée à l'étape précédente.

2. Concepts

2.1. Interface graphique

La figure 1 ci-dessous, identique à celle de l'étape 9, montre l'interface graphique de tCHu. Pour mémoire, trois des quatre parties principales de cette interface ont déjà été réalisées, la seule manquant étant celle portant le numéro 4 sur cette image.

tchu-gui-parts;128.png
Figure 1 : Interface graphique de tCHu (cliquer pour agrandir)

2.1.1. Vue des informations

Nous appellerons vue des informations (info view) la partie de l'interface portant le numéro 4 sur la figure 1 et montrant :

  • en haut : pour chaque joueur, un disque de la couleur de ses wagons, suivi des statistiques le concernant — nombre de billets et de cartes en main, wagons restants et points de construction obtenus,
  • en bas : les cinq dernières informations sur le déroulement de la partie.

Un exemple de cette vue est présenté ci-dessous.

info-view;32.png
Figure 2 : Vue des informations

Le premier joueur présenté dans la vue des informations est toujours le joueur auquel l'interface graphique correspond. Ainsi, la vue des informations présentée ci-dessus est celle visible dans l'interface de la joueuse Ada. Celle du joueur Charles aurait ses statistiques à lui en premier.

2.2. Interaction

Pour mémoire, au début de la partie, chaque joueur doit choisir au moins trois billets parmi les cinq qui lui ont été distribués. Ensuite, lorsque c'est à son tour de jouer, un joueur peut effectuer une des trois actions suivantes :

  1. tirer trois billets et en garder au moins un,
  2. tirer deux cartes,
  3. (tenter de) s'emparer d'une route.

La manière dont le joueur interagit avec l'interface pour effectuer ces différentes actions est présenté dans les sections suivantes.

2.2.1. Choix initial des billets

Au début de la partie, une fenêtre similaire à celle de la figure 3 ci-dessous s'ouvre au-dessus de la fenêtre principale de l'interface.

Les cinq billets distribués au joueur y sont présentés, et dès que ce dernier en a sélectionné trois ou plus, le bouton Choisir devient actif. Le joueur peut alors confirmer son choix en cliquant dessus, ce qui provoque la fermeture de la fenêtre. Notez qu'il n'est pas possible de fermer la fenêtre en cliquant sur le cercle rouge en haut à gauche, malgré ce que l'on pourrait penser.

initial-tickets-choice;32.png
Figure 3 : Choix des billets initiaux

La fenêtre de choix de billets est ce que l'on nomme parfois une boîte de dialogue modale (modal dialog box), terme qui indique que l'utilisateur ne peut pas interagir avec le reste de l'interface graphique tant que cette fenêtre est à l'écran.

2.2.2. Tirage de billets

Pour tirer des billets supplémentaires en cours de partie, le joueur clique sur le bouton Billets qui représente la pioche des billets. Cela provoque l'ouverture d'une fenêtre similaire à celle de la figure 4 ci-dessous. Le choix du ou des billets à garder se fait exactement comme le choix initial.

tickets-choice;32.png
Figure 4 : Fenêtre de choix de billets

2.2.3. Tirage de cartes

Pour tirer des cartes, le joueur clique sur l'une des cinq cartes dont la face est visible, ou sur le bouton Cartes qui représente la pioche de cartes. Cela a pour effet de tirer la carte en question, et de l'ajouter à la main du joueur.

Dès qu'il a tiré une première carte, un joueur n'a plus d'autre choix que d'en tirer une seconde pour terminer son tour, en procédant exactement de la même manière. Les deux autres actions — tirage de billets ou prise de possession de routes — sont impossibles. Dès lors, le billet représentant la pioche de billets devient inactif, de même que toutes les routes.

2.2.4. Prise de possession d'une route

Lorsque le curseur de la souris du joueur survole une route et que celle-ci s'agrandit, cela signifie que le joueur a les cartes (initiales) et les wagons nécessaires pour s'en emparer. Il lui suffit alors de cliquer pour (tenter de) s'en emparer effectivement.

À ce moment, s'il dispose de plus d'une combinaison de cartes lui permettant de s'emparer de la route, une fenêtre similaire à celle de la figure 5 ci-dessous s'ouvre. Les combinaisons utilisables y sont présentées triées par ordre croissant de nombre de locomotives puis par couleur, la première combinaison étant sélectionnée par défaut. Là aussi, le joueur peut confirmer son choix en cliquant sur le bouton Choisir, ce qui provoque la fermeture de la fenêtre.

initial-cards-choice;32.png
Figure 5 : Fenêtre de choix de cartes initiales

Lorsqu'un joueur décide de s'emparer d'un tunnel, que des cartes additionnelles sont nécessaires et qu'il dispose d'au moins une combinaison de telles cartes en main, une fenêtre similaire à celle de la figure 6 ci-dessous s'ouvre. Les choix qui s'offrent à lui sont triés par ordre croissant de nombre de locomotives. Là encore, le joueur peut confirmer son choix en cliquant sur le bouton Choisir, la différence étant qu'il lui est possible de ne choisir aucune des combinaisons proposées, afin de renoncer à s'emparer du tunnel.

additional-cards-choice;32.png
Figure 6 : Fenêtre de choix de cartes additionnelles

3. Mise en œuvre Java

3.1. Classe InfoViewCreator

La classe InfoViewCreator du paquetage ch.epfl.tchu.gui, non instanciable et package private (!), contient une unique méthode publique, nommée p.ex. createInfoView, permettant de créer la vue des informations. Elle prend quatre arguments, qui sont :

  1. l'identité du joueur auquel l'interface correspond,
  2. la table associative des noms des joueurs,
  3. l'état de jeu observable,
  4. une liste (observable) contenant les informations sur le déroulement de la partie, sous la forme d'instances de Text.

Lorsque la méthode createInfoView est appelée, la partie n'a pas encore commencé, et la liste des informations est donc vide à ce moment. Cela n'a toutefois pas d'importance, cette liste observable — son contenu, pour être précis — étant destinée à être liée au graphe de scène, comme expliqué plus bas.

3.1.1. Graphe de scène

Le graphe de scène de la vue des informations est présenté dans la figure 7 ci-dessous. Il faut noter qu'à sa création, l'instance de TextFlow portant l'identité game-info ne possède aucun fils. Ceux-ci sont liés à la liste observable passée en argument à la méthode createInfoView comme décrit à la section suivante.

info-pane-hierarchy;64.png
Figure 7 : Graphe de scène (partiel) de la vue des informations

Le cercle montrant la couleur du joueur dans les statistiques a un rayon de 5 unités. Le séparateur (de type Separator) séparant les statistiques des joueurs de la liste de messages est orienté horizontalement.

3.1.2. Liens et auditeurs

L'instance de Text contenant les statistiques de chacun des joueurs doit contenir la chaîne PLAYER_STATS de StringsFr, formatée de manière à ce que les différentes occurrences de %s soit remplacées par les bonnes valeurs — nombre de billets, cartes, etc.

Cette chaîne doit bien entendu être formatée à nouveau chaque fois que l'une de ces valeurs change. Cela peut se faire très facilement au moyen de la méthode format de Bindings (!), qui fonctionne de manière similaire à celle de String si ce n'est qu'elle ne retourne pas une chaîne, mais une valeur de type StringExpression. Il s'agit en quelque sorte d'une « chaîne dynamique », dont la valeur change chaque fois qu'un des arguments passé à format change de valeur. Cette « chaîne dynamique » retournée par format peut être directement liée — au moyen de la méthode bind — à la propriété textProperty de l'instance de Text.

Le contenu de la liste d'enfants — retournée par getChildren — de l'instance de TextFlow contenant les messages d'information doit être lié à celui de la liste d'informations passée à la méthode createInfoView. Cela peut se faire au moyen de la méthode bindContent de Bindings.

3.1.3. Gestionnaires d'événements

Aucune gestion d'événements n'est nécessaire pour la vue des informations, aucune interaction n'étant possible avec elle.

3.2. Classe GraphicalPlayer

La classe instanciable GraphicalPlayer du paquetage ch.epfl.tchu.gui représente l'interface graphique d'un joueur de tCHu.

Malgré son nom, cette classe n'implémente pas l'interface Player, même si elle offre des méthodes similaires, avec toutefois des différences importantes. La principale d'entre elles est que, contrairement à celles de Player, les méthodes de GraphicalPlayer ne bloquent jamais pour une durée indéterminée. Cette différence est expliquée plus en détail à la §3.2.2.

Le constructeur de GraphicalPlayer prend en arguments l'identité du joueur auquel l'instance correspond, et la table associative des noms des joueurs. Il construit l'interface graphique, constituée d'une fenêtre ayant l'aspect présenté à la figure 1 et dont le graphe de scène est décrit à la §3.2.3. Cela implique entre autres de créer une instance d'un état de jeu observable, qui est d'une part stocké comme attribut de la classe et d'autre part passé en argument aux méthodes construisant les différentes vues, écrites lors de l'étape précédente et de celle-ci.

En plus de son constructeur, la classe GraphicalPlayer offre un certain nombre de méthodes publiques. Aucune d'entre elles ne retourne quoi que ce soit, et elles ont donc toutes le type void comme type de retour. Il s'agit de :

  • une méthode nommée p.ex. setState, prenant les mêmes arguments que la méthode setState de ObservableGameState et ne faisant rien d'autre que d'appeler cette méthode sur l'état observable du joueur,
  • une méthode nommée p.ex. receiveInfo, prenant un message — de type String — et l'ajoutant au bas des informations sur le déroulement de la partie, qui sont présentées dans la partie inférieure de la vue des informations ; pour mémoire, cette vue ne doit contenir que les cinq derniers messages reçus,
  • une méthode nommée p.ex. startTurn, qui prend en arguments trois gestionnaires d'action, un par types d'actions que le joueur peut effectuer lors d'un tour, et qui permet au joueur d'en effectuer une — voir la section suivante pour plus de détails,
  • une méthode nommée p.ex. chooseTickets, qui prend en arguments un multiensemble contenant cinq ou trois billets que le joueur peut choisir et un gestionnaire de choix de billets — de type ChooseTicketsHandler —, et qui ouvre une fenêtre similaire à celle des figures 3 et 4, permettant au joueur de faire son choix ; une fois celui-ci confirmé, le gestionnaire de choix est appelé avec ce choix en argument1,
  • une méthode nommée p.ex. drawCard, qui prend en argument un gestionnaire de tirage de carte — de type DrawCardHandler — et qui autorise le joueur a choisir une carte wagon/locomotive, soit l'une des cinq dont la face est visible, soit celle du sommet de la pioche ; une fois que le joueur a cliqué sur l'une de ces cartes, le gestionnaire est appelé avec le choix du joueur ; cette méthode est destinée à être appelée lorsque le joueur a déjà tiré une première carte et doit maintenant tirer la seconde,
  • une méthode nommée p.ex. chooseClaimCards, qui prend en arguments une liste de multiensembles de cartes, qui sont les cartes initiales qu'il peut utiliser pour s'emparer d'une route, et un gestionnaire de choix de cartes — de type ChooseCardsHandler —, et qui ouvre une fenêtre similaire à celle de la figure 5 permettant au joueur de faire son choix ; une fois que celui-ci a été fait et confirmé, le gestionnaire de choix est appelé avec le choix du joueur en argument ; cette méthode n'est destinée qu'à être passée en argument à createMapView en tant que valeur de type CardChooser,
  • une méthode, nommée p.ex. chooseAdditionalCards, qui prend en arguments une liste de multiensembles de cartes, qui sont les cartes additionnelles qu'il peut utiliser pour s'emparer d'un tunnel et un gestionnaire de choix de cartes — de type ChooseCardsHandler —, et qui ouvre une fenêtre similaire à celle de la figure 6 permettant au joueur de faire son choix ; une fois que celui-ci a été fait et confirmé, le gestionnaire de choix est appelé avec le choix du joueur en argument.

3.2.1. Gestionnaires d'action

Lors de l'étape précédente, nous avons défini l'interface ActionHandlers contenant cinq interfaces fonctionnelles que nous avons appelées les gestionnaires d'action. Parmi ces cinq interfaces, trois correspondent directement aux actions que le joueur peut effectuer lors d'un tour :

  1. DrawTicketsHandler correspond au tirage de billets,
  2. DrawCardHandler correspond au tirage de cartes,
  3. ClaimRouteHandler correspond à la prise de possession d'une route.

Un gestionnaire de chacun de ces trois types est passé à la méthode startTurn, et cette méthode doit faire en sorte que, lorsque le joueur (humain) décide d'effectuer une action, le gestionnaire correspondant soit appelé. Par exemple, s'il clique sur le bouton Cartes pour tirer une carte du sommet de la pioche, alors le second gestionnaire doit être appelé avec -1 en argument, pour signaler que la carte est tirée de la pioche.

À première vue, les choses semblent donc simples : on peut imaginer que startTurn stocke simplement les trois gestionnaires reçus dans des attributs, puis que les différents gestionnaires d'événements (!) de l'interface graphique — p.ex. celui associé au bouton Cartes — appellent simplement les gestionnaires d'action contenus dans ces attributs.

Les choses ne sont toutefois pas aussi simples, car il est possible que certaines actions ne soient pas autorisées à certains moment du jeu. Par exemple, il est interdit de tirer des billets si la pioche est vide. De même, lorsqu'un joueur a déjà tiré une première carte, la seule action qu'il peut effectuer avant de terminer son tour est d'en tirer une seconde.

Pour gérer cela, il est utile de stocker les gestionnaires d'action non pas dans de simples attributs, mais dans des propriétés qui elles-mêmes sont des attributs de la classe. Cela fait, on peut adopter la convention que si la propriété correspondant à une action donnée contient null, alors cela signifie que l'action en question est actuellement interdite.

Disposer ainsi de propriétés contenant les gestionnaires d'action est fort utile, car cela permet de très facilement activer ou désactiver les différentes parties de l'interface graphique en fonction des actions qu'il est possible d'effectuer. Par exemple, le bouton Cartes ne doit être activé que lorsque la propriété contenant le gestionnaire d'action correspondant au tirage de cartes ne contient pas null, ce qui se fait aisément au moyen de liens (bindings) JavaFX. Le code correspondant a d'ailleurs déjà été écrit à l'étape précédente.

Au début d'un tour, c-à-d lorsque startTurn est appelée, les propriétés contenant les gestionnaires doivent être modifiées ainsi :

  • celle contenant le gestionnaire de tirage de billets doit être laissée vide (contenir null) si ce tirage est actuellement impossible, c-à-d si canDrawTickets retourne faux, sinon elle doit être remplie,
  • celle contenant le gestionnaire de tirage de cartes doit être laissée vide si ce tirage est actuellement impossible, c-à-d si canDrawCards retourne faux, sinon elle doit être remplie,
  • celle contenant le gestionnaire correspondant à la prise de possession d'une route doit toujours être remplie.

Lorsqu'on remplit ces propriétés, on y stocke un gestionnaire qui est presque identique au gestionnaire correspondant passé à startTurn. Pourquoi « presque » ? Simplement car les gestionnaires stockés dans les propriétés doivent aussi vider chacune des trois propriétés, afin de garantir qu'une fois que le joueur a effectué une action, il ne peut plus en effectuer une autre !

La seule autre méthode modifiant les propriétés contenant les gestionnaires est drawCard, qui remplit uniquement celle contenant le gestionnaire de tirage de cartes. En effet, au moment où drawCard est appelée, il s'agit de la seule action autorisée. Là aussi, la méthode drawCard doit s'arranger pour que le gestionnaire qu'elle stocke vide toutes les propriétés contenant des gestionnaires dès que le joueur aura choisi la carte à tirer.

3.2.2. Méthodes bloquantes et non bloquantes

En comparant les méthodes de Player et celles de GraphicalPlayer, on se rend compte qu'il y a beaucoup de similarités entre elles, mais aussi un certain nombre de différences. La principale différence, comme nous l'avons dit, est que les méthodes de Player peuvent bloquer pour une durée indéterminée, alors que celles de GraphicalPlayer ne peuvent pas bloquer ainsi.

Cette différence peut être illustrée au moyen de la méthode permettant de commencer un tour. Dans l'interface Player, cette méthode se nomme nextTurn et retourne une valeur de type TurnKind. Cette méthode est bloquante, dans le sens où elle ne peut retourner son résultat que lorsque le joueur a décidé quel type d'action il désirait effectuer durant son tour. Si le joueur en question est un humain, la prise de décision peut durer plusieurs secondes, voir plusieurs minutes. Pendant ce temps, l'appelant de la méthode nextTurn est bloqué en attente de la réponse.

Dans la classe GraphicalPlayer, la méthode qui joue un rôle similaire à nextTurn s'appelle startTurn. Contrairement à nextTurn, elle ne retourne aucune valeur ! Au lieu de cela, on lui passe en argument trois gestionnaires, et elle se charge d'appeler l'un d'entre eux dès que le joueur humain a pris sa décision. Dès lors, et contrairement à nextTurn, elle n'a pas besoin de bloquer en attendant que le joueur prenne sa décision, et peut retourner (presque) immédiatement, ce qu'elle fait.

Toutes les méthodes de GraphicalPlayer fonctionnent de la sorte, et c'est la raison pour laquelle elles reçoivent (presque) toutes un gestionnaire d'action à appeler plus tard, et qu'elles ne retournent aucune valeur.

La raison pour laquelle il est important que les méthodes de GraphicalPlayer retournent immédiatement est qu'elles sont exécutées sur le fil d'exécution (thread) de JavaFX, qui gère la totalité de l'interface graphique. Donc lorsque ce fil est bloqué par l'exécution d'une méthode, l'interface graphique est inutilisable, ce qui doit être évité autant que possible.

3.2.3. Graphe de scène

Le graphe de scène de la fenêtre principale de l'interface du joueur est extrêmement simple est n'est donc pas présentée sous forme graphique. La fenêtre principale est une instance de Stage contenant une instance de Scene contenant une instance de BorderPane. Cette instance de BorderPane contient les éléments auquel on s'attend, à savoir :

  • dans la zone centrale, la vue de la carte,
  • dans la zone du haut, rien du tout (laissée vide),
  • dans la zone de droite, la vue des pioches,
  • dans la zone du bas, la vue de la main,
  • dans la zone de gauche, la vue des informations.

Le titre de la fenêtre principale est tCHu suivi d'un tiret long (le caractère , qui peut aussi s'écrire \u2014 en Java) suivi du nom du joueur auquel l'interface correspond, par exemple :

tCHu — Ada

pour la joueuse Ada.

Les fenêtres de sélection de billets ou de cartes ont toutes un graphe de scène identique, présenté à la figure 8 ci-dessous. En fonction du type de fenêtre de sélection dont il s'agit, leur titre est donné soit par la constante TICKETS_CHOICE, soit par la constante CARDS_CHOICE de StringsFr.

chooser-hierarchy;64.png
Figure 8 : Graphe de scène d'une fenêtre de sélection

Pour que ces fenêtres de sélection soient effectivement des boîtes de dialogue modales, il faut que :

L'instance de Text du graphe de scène de la figure 8 contient le texte d'introduction. Là aussi, en fonction des cas, il est donné par une constante de StringsFr : CHOOSE_TICKETS, CHOOSE_CARDS ou CHOOSE_ADDITIONAL_CARDS. La première de ces chaînes est une chaîne de formatage, et les occurrences de %s qu'elle contient doivent être remplacées par les valeurs correspondantes.

L'instance de ListView du graphe de scène de la figure 8 présente la liste des choix qui s'offrent au joueur. Deux aspects de cette vue doivent, dans certains cas, être configurés :

  1. par défaut, il n'est possible de sélectionner qu'un seul élément de la liste ; pour autoriser la sélection multiple lorsque cela est nécessaire, il faut appeler la méthode setSelectionMode sur le modèle de sélection attaché à la vue et qui s'obtient au moyen de la méthode getSelectionModel, en lui passant SelectionMode.MULTIPLE,
  2. par défaut, la vue représente chaque élément par la chaîne obtenue en leur appliquant toString ; pour changer cela, il faut changer la « fabrique de cellule » de la vue.

La « fabrique de cellules » (cell factory) d'une vue de type ListView est un objet que cette dernière utilise pour créer les cellules qu'elle contient, chaque cellule présentant un élément de la liste. Le fonctionnement exact importe peu, il faut juste savoir qu'une autre fabrique que celle par défaut est nécessaire pour le cas où la liste contient des multiensembles de cartes.

En effet, la fabrique de cellules par défaut en crée qui utilisent simplement toString pour transformer l'élément auquel elles correspondent en chaîne de caractères. Or si cela convient très bien aux listes de billets, cela ne convient pas aux listes de multiensembles de cartes, la chaîne produite par la méthode toString de SortedBag étant, disons, peu conviviale.

Par exemple, pour un multiensemble contenant une carte violette et trois cartes rouges, la méthode toString de SortedBag produit la chaîne {VIOLET, 3×RED}. Il serait préférable que ce multiensemble soit représenté plutôt par la chaîne :

1 violette et 3 rouges

Pour ce faire, il faut changer la fabrique de cellule de la vue en appelant sa méthode setCellFactory de la manière suivante :

ListView<SortedBag<Card>> list = …;
list.setCellFactory(v ->
  new TextFieldListCell<>(new CardBagStringConverter()));

CardBagStringConverter est une classe qu'il vous faut définir, qui hérite de StringConverter<SortedBag<Card>> et définit des versions concrètes de ses deux méthodes abstraites ainsi :

  • la méthode toString transforme le multiensemble de cartes en chaînes de la manière illustrée plus haut,
  • la méthode fromString lève simplement une exception de type UnsupportedOperationException car elle n'est jamais utilisée dans ce contexte.

Le test JUnit ci-dessous illustre le fonctionnement de la méthode toString de cette classe :

CardBagStringConverter c = new CardBagStringConverter();
SortedBag<Card> b = SortedBag.of(1, Card.VIOLET, 3, Card.RED);
assertEquals("1 violette et 3 rouges", c.toString(b));

3.2.4. Liens et auditeurs

Le bouton de confirmation des fenêtres de sélection doit être désactivé tant et aussi longtemps que le choix n'est pas valide, à savoir :

  • pour le choix de billets, tant et aussi longtemps que le nombre de billets sélectionné n'est pas au moins égal au nombre de billets présentés moins deux,
  • pour le choix des cartes initiales, tant et aussi longtemps qu'aucun des choix proposés n'est sélectionné.

Pour le choix des cartes additionnelles, le bouton est toujours actif, une sélection vide permettant au joueur de déclarer qu'il désire abandonner sa tentative de prise de possession du tunnel.

La désactivation du bouton de confirmation se fait comme d'habitude en liant sa propriété disableProperty à une propriété qui est vraie si le nombre d'éléments sélectionnés est trop petit. Pour obtenir cette dernière, on peut s'aider de la méthode size de Bindings appliquée à la propriété getSelectedItems, qui donne le nombre d'éléments sélectionnés.

3.2.5. Gestionnaires d'événements

Pour éviter que les fenêtres de sélection ne puissent être fermées en cliquant sur leur bouton de fermeture, il faut leur ajouter un gestionnaire d'événement au moyen de setOnCloseRequest. Ce gestionnaire ne doit rien faire d'autre que consommer l'événement qu'il reçoit au moyen de sa méthode consume.

Le bouton de confirmation des fenêtres de sélection doit avoir un gestionnaire d'événement associé au clic, que l'on peut attacher au moyen de la méthode setOnAction. Ce gestionnaire doit fermer la fenêtre de sélection (au moyen de la méthode hide) puis appeler le gestionnaire de choix correspondant, à savoir :

  • pour le choix des billets, le gestionnaire de choix des billets, de type ChooseTicketsHandler, passé à la méthode chooseTickets, en lui passant le multiensemble de billets choisi,
  • pour le choix des cartes, le gestionnaire de choix de cartes, de type ChooseCardsHandler, passé à la méthode chooseClaimCards ou chooseAdditionalCards, en lui passant le multiensemble de cartes choisi — qui est vide si le joueur décide d'abandonner sa tentative de prise de possession d'un tunnel.

Les éléments des listes sélectionnés par le joueur s'obtiennent au moyen de la méthode getSelectedItem (si un seul choix est possible) ou getSelectedItems (si plusieurs choix sont possibles) sur le modèle de sélection. Ce dernier s'obtient, pour mémoire, avec la méthode getSelectionModel.

3.2.6. Conseils de programmation

Les méthodes de GraphicalPlayer, y compris son constructeur, manipulent directement ou indirectement l'interface graphique du joueur. Pour cette raison, il est fondamental que ces méthodes soient toujours appelées sur le fil d'exécution (thread) de JavaFX. Nous vous conseillons donc d'ajouter, au début de chacune de vos méthodes publiques, une assertion vérifiant que tel est bien le cas :

assert isFxApplicationThread();

isFxApplicationThread est une méthode statique de la classe Platform qui ne retourne vrai que lorsqu'on l'appelle depuis le fil d'exécution JavaFX.

En Java, une assertion telle que celle ci-dessus est presque équivalente au code suivant :

if (! isFxApplicationThread()) throw new AssertionError();

la seule différence étant que la vérification des assertions doit être explicitement activée. Pour ce faire, il vous faut ajouter l'argument -ea (ou -enableassertions) au VM arguments lorsque vous lancez votre programme. En d'autres termes, il faut ajouter -ea aux arguments que vous passez déjà pour configurer JavaFX (--module-path etc.).

3.3. Tests

Pour tester la classe InfoViewCreator, vous pouvez augmenter le programme de test de l'étape précédente en effectuant les ajouts illustrés dans l'extrait ci-dessous à la méthode start — sans oublier de passer infoView en dernier argument au constructeur de BorderPane :

public void start(Stage primaryStage) {
  // … comme avant

  Map<PlayerId, String> playerNames =
    Map.of(PLAYER_1, "Ada", PLAYER_2, "Charles");
  ObservableList<Text> infos = FXCollections.observableArrayList(
    new Text("Première information.\n"),
    new Text("\nSeconde information.\n"));
  Node infoView = InfoViewCreator
    .createInfoView(PLAYER_1, playerNames, gameState, infos);

  BorderPane mainPane =
    new BorderPane(mapView, null, cardsView, handView, infoView);

  // … comme avant
}

Une fois cela fait, en lançant votre programme, vous devriez constater l'apparition de la vue des informations dans la partie gauche de l'interface.

stage10-gui;128.png
Figure 9 : Interface après ajout de la vue des informations

Pour tester GraphicalPlayer, vous pouvez écrire un autre petit programme de test, en vous inspirant par exemple de celui présenté ci-dessous. Notez qu'il appelle la méthode startTurn dans le but de simuler le début d'un tour, en passant des gestionnaires d'actions qui appellent receiveInfo pour signaler qu'ils ont été appelés. Bien entendu, receiveInfo ne sera pas du tout utilisée de la sorte dans le programme final, mais offre une alternative à l'affichage de messages sur la console pour un programme de test comme celui-ci.

public final class GraphicalPlayerTest extends Application {
  private void setState(GraphicalPlayer player) {
    // … construit exactement les mêmes états que la méthode setState
    // du test de l'étape 9
    player.setState(publicGameState, p1State);
  }

  @Override
  public void start(Stage primaryStage) {
    Map<PlayerId, String> playerNames =
      Map.of(PLAYER_1, "Ada", PLAYER_2, "Charles");
    GraphicalPlayer p = new GraphicalPlayer(PLAYER_1, playerNames);
    setState(p);

    DrawTicketsHandler drawTicketsH =
      () -> p.receiveInfo("Je tire des billets !");
    DrawCardHandler drawCardH =
      s -> p.receiveInfo(String.format("Je tire une carte de %s !", s));
    ClaimRouteHandler claimRouteH =
      (r, cs) -> {
      String rn = r.station1() + " - " + r.station2();
      p.receiveInfo(String.format("Je m'empare de %s avec %s", rn, cs));
    };

    p.startTurn(drawTicketsH, drawCardH, claimRouteH);
  }
}

La vidéo ci-dessous illustre le comportement de cette interface. Notez bien comment les boutons des deux pioches se désactivent au moment où le joueur clique sur une route pour s'en emparer. Cette désactivation, décrite à la §3.2.1, est nécessaire pour garantir que le joueur ne peut effectuer qu'une action par tour.

Notez de plus que comme le programme de test ne contient rien d'autre qu'un appel à startTurn, il n'est plus possible d'interagir avec l'interface dès qu'une action a été effectuée. La seule chose qu'il est alors possible de faire est de quitter le programme.

4. Résumé

Pour cette étape, vous devez :

  • écrire les classes InfoViewCreator et GraphicalPlayer 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.

Notes de bas de page

1

Lorsqu'on dit que le gestionnaire est appelé avec des arguments, on veut bien entendu dire que c'est son unique méthode qui est appelée avec ces arguments.