Éléments de jeu

tCHu – étape 1

1 Introduction

Le but principal de cette première étape est d'écrire les classes représentant trois éléments importants du jeu : les gares, les cartes et les billets.

Comme toutes les descriptions d'étapes de ce projet, celle-ci commence par une introduction aux concepts nécessaires à sa réalisation (§2), suivie d'une présentation de leur mise en œuvre en Java (§3).

Si vous travaillez en groupe, vous êtes toutefois priés de lire, avant de continuer, les guides Travailler en groupe et Synchroniser son travail, qui vous aideront à bien vous organiser.

2 Concepts

2.1 Réseau

Comme cela a été expliqué dans l'introduction au projet, une partie de tCHu se joue sur une carte de la Suisse et des pays voisins, visible ci-dessous1.

network-map;128.png
Figure 1 : Carte de tCHu (cliquer pour agrandir)

Un certain nombre de villes, toutes suisses, sont nommées sur cette carte. Elles sont reliées entre elles par un réseau ferroviaire représenté par des cases rectangulaires colorées placées les unes derrière les autres. Ce réseau s'étend également aux pays voisins — Allemagne, Autriche, Italie et France — mais les villes des pays voisins ne sont pas nommées et sont simplement représentées par un drapeau du pays.

2.2 Gares

Une gare (station en anglais) est un point du réseau ferroviaire de tCHu. Il en existe de deux types : celles qui correspondent à des villes suisses, et celles qui correspondent à des villes non spécifiées dans un pays voisin.

La principale caractéristique d'une gare est son nom. Pour celles situées en Suisse, ce nom est simplement celui de leur ville. Pour celles situées dans un pays voisin, ce nom est celui du pays en question. Ainsi, les quatre gares situées en France portent toutes le nom de « France », mais il s'agit néanmoins de gares différentes.

La carte de tCHu comporte 34 gares en Suisse, et 17 dans les pays voisins : 5 en Allemagne, 3 en Autriche, 5 en Italie et 4 en France. Il y a donc 51 gares au total.

2.3 Cartes wagon/locomotive

Les joueurs de tCHu ont en main en certain nombre de cartes wagon (car cards) et de cartes locomotive (locomotive cards), qu'ils utilisent pour s'emparer des routes du réseau ferroviaire, comme nous le verrons à l'étape suivante.

Les cartes wagon sont ornées d'un dessin de wagon et colorées d'une des huit couleurs utilisées dans le jeu, à savoir : noir, violet, bleu, vert, jaune, orange, rouge, ou blanc. Chaque carte wagon d'une couleur donnée existe en 12 exemplaires, et le jeu comporte donc un total de 96 (8×12) cartes wagon.

cards-cars;64.png
Figure 2 : Cartes wagon

Les cartes locomotive sont ornées d'un dessin de locomotive. Bien que ces cartes soient aussi colorées (en turquoise), leur couleur est purement décorative et ne joue aucun rôle dans le jeu, contrairement à celle des cartes wagon. Le jeu comporte 14 cartes locomotive.

cards-locomotive;8.png
Figure 3 : Carte locomotive

Au total, le jeu comporte donc 110 (96+14) cartes wagon et locomotive, que nous appellerons désormais simplement « les cartes ».

2.4 Billets

En plus des cartes, les joueurs ont également en main un certain nombre de billets (tickets) représentant chacun un objectif à réaliser. Tout comme un billet de train réel, un billet de tCHu est constitué d'un point de départ (p.ex. Lausanne) et d'un point d'arrivée (p.ex. Saint-Gall). De plus, un billet de tCHu vaut un certain nombre de points, qui sont ceux que le joueur qui le possède gagnera (resp. perdra) à la fin de la partie s'il a réussi (resp. échoué) à relier le point de départ au point d'arrivée avec ses propres wagons.

Il existe de trois types de billets tCHu, décrits en détail plus bas :

  1. ville à ville,
  2. ville à pays,
  3. pays à pays.

Le jeu de tCHu comporte un total de 46 billets : 34 billets ville à ville, 4 billets ville à pays, et 8 billets pays à pays. Les billets pays à pays existent en deux exemplaires chacun, tandis que tous les autres billets sont uniques.

2.4.1 Billet ville à ville

Un billet « ville à ville » relie simplement deux villes (suisses) entre elles, par exemple Lausanne et Saint-Gall. Un nombre de points fixe est attribué à ce type de billet, qui correspond souvent à la longueur du plus court chemin reliant les deux villes, 13 dans le cas de Lausanne et de Saint-Gall.

Un joueur possédant un tel billet et réussissant effectivement à relier les deux villes avant la fin de la partie gagne les points correspondants, 13 dans notre exemple. S'il ne parvient pas à relier les villes, il perd ces 13 points, qui sont soustraits de son total.

2.4.2 Billet ville à pays

Un billet « ville à pays » relie une ville suisse à chacun des pays voisins (France, Allemagne, Autriche et Italie). Les points attribués à ce type de billet dépendent du pays destination. Par exemple, dans le cas du billet allant de Berne à un pays voisin, la liaison avec l'Allemagne vaut 6 points, celle avec l'Autriche 11, celle avec l'Italie 8 et celle avec la France 5. Là aussi, ces points correspondent généralement à la longueur du plus court chemin reliant la ville à la gare la plus proche du pays en question.

Un joueur possédant un billet ville à pays et réussissant effectivement à relier la ville à au moins une gare d'un des pays voisins gagne le nombre de points maximum correspondant. S'il ne réussit pas à relier la ville avec une gare d'un des pays voisins, il perd le nombre minimum de points.

Dans l'exemple ci-dessus, un joueur ayant réussi à relier Berne à la France — c-à-d à au moins une gare française, peu importe laquelle — et à l'Italie gagne 8 points, pour la connexion avec l'Italie. Par contre, s'il ne réussit à relier Berne à aucun des pays voisins, il ne perd que 5 points, le minimum possible — pour la connexion avec la France.

2.4.3 Billet pays à pays

Un billet « pays à pays » relie un pays voisin de départ donné à chacun des autres pays voisins, par exemple la France à chacun des pays restants (Allemagne, Autriche et Italie). Là aussi, le nombre de points dépend du pays. Dans l'exemple du billet partant de la France, la connexion avec l'Allemagne vaut 5 points, celle avec l'Autriche 14 et celle avec l'Italie 11.

Les points gagnés ou perdus à la fin de la partie par un joueur possédant un tel billet sont déterminés de la même manière que pour les billets ville à pays.

2.4.4 Trajets

La description des billets donnée ci-dessus peut faire penser qu'il existe trois types de billets totalement différents. Or ce n'est pas vraiment le cas, et il est tout à fait possible d'unifier ces trois types de billets en un seul.

Pour ce faire, il suffit de considérer qu'un billet est constitué d'une liste de ce que nous appellerons des trajets (trips). Un trajet est constitué de deux gares à relier et d'un nombre de points.

Ainsi, un billet ville à ville est toujours constitué d'un unique trajet, dont les deux gares et les points sont bien entendu ceux du billet lui-même. Par exemple, le billet Lausanne - Saint-Gall décrit plus haut comporte un seul trajet, qui va de Lausanne à Saint-Gall et vaut 13 points.

Un billet ville à pays est constitué quant à lui d'un assez grand nombre de trajets dont :

  • la première gare est toujours la gare de la ville,
  • la seconde gare est une gare d'un pays voisin,
  • le nombre de points est celui attribué au pays voisin en question.

Par exemple, le billet de Berne à un pays voisin est constitué de 17 trajets, un pour chaque gare se trouvant dans un pays voisin, parmi lesquels on trouve :

  • un trajet de Berne à la gare française voisine de La Chaux-de-Fonds, valant 5 points,
  • un trajet de Berne à la gare française voisine de Delémont, valant aussi 5 points,
  • un trajet de Berne à la gare allemande voisine de Bâle, valant 6 points,
  • etc.

Un billet pays à pays est constitué également de plusieurs trajets, qui vont de chacune des gares du pays de départ à chacune des gares d'un autre pays voisin. Par exemple, le billet France - pays voisin est constitué d'un total de 52 (4×13) trajets, chacun d'entre eux reliant l'une des 4 gares françaises à l'une des 13 gares des autres pays voisins (Allemagne, Autriche et Italie).

2.4.5 Représentation

Dans la version originale des Aventuriers du Rail, les billets sont représentés par des petites cartes de Suisse sur lesquelles figurent une représentation graphique des trajets et de leurs points. Par exemple, le billet allant de Berne à un pays voisin est représenté ainsi :

ticket-bern-neighbors;128.png
Figure 4 : Version originale du billet Berne - pays voisin (© Days of Wonders)

Pour tCHu, dans un soucis de simplicité, nous utiliserons une représentation textuelle des billets, construite de la manière suivante.

La représentation textuelle d'un billet ville à ville est constituée du nom de la ville de départ, d'un tiret entouré d'espaces, du nom de la ville d'arrivée, et du nombre de points du billet entre parenthèses et précédé d'un espace. Par exemple, la représentation textuelle du billet allant de Lausanne à Saint-Gall et valant 13 points est :

Lausanne - Saint-Gall (13)

La représentation textuelle d'un billet ville à pays est similaire, mais le nom de la gare d'arrivée est remplacé par une liste des différents pays d'arrivée entre accolades, chaque pays étant suivi du nombre de points correspondant. Les pays sont triés par ordre alphabétique (Allemagne, Autriche, France, Italie). Par exemple, le billet allant de Berne aux pays voisins a la représentation textuelle suivante :

Berne - {Allemagne (6), Autriche (11), France (5), Italie (8)}

La représentation textuelle d'un billet pays à pays est similaire. Par exemple, le billet allant de la France à l'un des autres pays voisins a pour représentation textuelle :

France - {Allemagne (5), Autriche (14), Italie (11)}

3 Mise en œuvre Java

Les concepts importants pour cette étape ayant été introduits, il est temps de décrire leur mise en œuvre en Java.

En plus des classes permettant de représenter les éléments de jeu expliqués ci-dessus, une classe « utilitaire » est à réaliser : Preconditions, qui offre une méthode de validation d'argument.

Notez que toutes les classes et interfaces de ce projet appartiendront au paquetage ch.epfl.tchu ou à l'un de ses sous-paquetages. Par exemple, à l'exception de la classe Preconditions, toutes les classes de cette étape appartiendront au sous-paquetage ch.epfl.tchu.game, destiné à contenir les classes représentant la logique du jeu.

Attention : jusqu'à l'étape 6 du projet (incluse), vous devez suivre à la lettre les instructions qui vous sont données dans l'énoncé, et vous n'avez pas le droit d'apporter la moindre modification à l'interface publique des classes, interfaces et types énumérés décrits.

En d'autres termes, vous ne pouvez pas ajouter des classes, interfaces ou types énumérés publics à votre projet, ni ajouter des attributs ou méthodes publics aux classes, interfaces et types énumérés décrits dans l'énoncé. Vous pouvez par contre définir des méthodes et attributs privés si cela vous semble judicieux.

Cette restriction sera levée dès l'étape 7.

3.1 Installation de Java

Avant de commencer à programmer, il faut vous assurer que la version 11 de Java est bien installée sur votre ordinateur, car c'est celle qui sera utilisée pour ce projet.

Si vous n'avez pas encore installé cette version, rendez-vous sur le site Web du projet AdoptOpenJDK, téléchargez le programme d'installation correspondant à votre système d'exploitation (macOS, Linux ou Windows), et exécutez-le. Ensuite, si vous utilisez Eclipse plutôt qu'IntelliJ, lisez le guide Configurer Eclipse et suivez les indications qui s'y trouvent.

3.2 Classe Preconditions

Fréquemment, les méthodes d'un programme demandent que leurs arguments satisfassent certaines conditions. Par exemple, une méthode déterminant la valeur maximale d'un tableau d'entiers exige que ce tableau contienne au moins un élément.

De telles conditions sont souvent appelées préconditions (preconditions) car elles doivent être satisfaites avant l'appel d'une méthode : c'est à l'appelant de s'assurer qu'il n'appelle la méthode qu'avec des arguments valides.

En Java, la convention veut que chaque méthode vérifie, autant que possible, ses préconditions et lève une exception — souvent IllegalArgumentException — si l'une d'entre elles n'est pas satisfaite. Par exemple, une méthode max calculant la valeur maximale d'un tableau d'entiers, et exigeant logiquement que celui-ci contienne au moins un élément, pourrait commencer ainsi :

int max(int[] array) {
  if (! (array.length > 0))
    throw new IllegalArgumentException();
  // … reste du code
}

(Notez au passage que la méthode max ne déclare pas qu'elle lève potentiellement IllegalArgumentException au moyen d'une clause throws, car cette exception est de type unchecked.)

La première classe à réaliser dans le cadre de ce projet a pour but de faciliter l'écriture de telles préconditions. En l'utilisant, la méthode ci-dessus pourrait être simplifiée ainsi :

int max(int[] array) {
  Preconditions.checkArgument(array.length > 0);
  // … reste du code
}

Cette classe est nommée Preconditions et appartient au paquetage ch.epfl.tchu. Elle est publique et finale et n'offre rien d'autre que la méthode checkArgument décrite plus bas. Elle a toutefois la particularité d'avoir un constructeur par défaut privé :

public final class Preconditions {
  private Preconditions() {}
  // … méthodes
}

Le but de ce constructeur privé est de rendre impossible la création d'instances de la classe, puisque cela n'a clairement aucun sens — elle ne sert que de conteneur à une méthode statique. Dans la suite du projet, nous définirons plusieurs autres classes du même type, que nous appellerons dès maintenant classes non instanciables.

La méthode publique (et statique) offerte par la classe Preconditions est :

  • void checkArgument(boolean shouldBeTrue), qui lève l'exception IllegalArgumentException si son argument est faux, et ne fait rien sinon.

3.3 Type énuméré Color

Le type énuméré Color du paquetage ch.epfl.tchu.game, public, représente les huit couleurs utilisées dans le jeu pour colorer les cartes wagon et les routes. Les valeurs de ce type énuméré sont, dans l'ordre :

  1. BLACK (noir),
  2. VIOLET (violet)
  3. BLUE (bleu),
  4. GREEN (vert),
  5. YELLOW (jaune),
  6. ORANGE (orange),
  7. RED (rouge),
  8. WHITE (blanc).

En plus de ces valeurs, le type énuméré Color offre deux attributs publics, statiques et finaux, qui sont :

  • List<Color> ALL, une liste immuable contenant la totalité des valeurs du type énuméré, dans leur ordre de définition (voir les conseils de programmation ci-dessous),
  • int COUNT, qui contient le nombre de valeurs du type énuméré.

3.3.1 Conseils de programmation

La définition de l'attribut ALL peut vous poser quelques problèmes, car le concept de liste, représenté en Java par l'interface java.util.List, n'a pas encore été vu au cours. Toutefois, vous avez vu au premier semestre le concept de tableau dynamique (ArrayList), qui est une sorte de liste. Pour l'instant, vous pouvez donc admettre que le type List est synonyme du type ArrayList, et que les méthodes offertes par une valeur de type ArrayList le sont aussi par une valeur de type List.

Sachant cela, il n'est pas très difficile de définir l'attribut ALL en s'aidant des deux méthodes statiques suivantes :

  1. la méthode values définie pour tous les types énumérés et retournant un tableau contenant les éléments du type énuméré dans leur ordre de définition — vue à la leçon 10 du cours du semestre précédent,
  2. la méthode of de l'interface List, qui retourne une liste immuable ayant les mêmes éléments que le tableau qu'on lui passe en argument.

L'extrait de programme ci-dessous, dont vous pouvez vous inspirer, montre comment la méthode of de List peut être utilisée pour obtenir une liste (seasons) des noms de saisons stockés dans un tableau (seasonsArray) :

String[] seasonsArray = new String[] {
  "printemps", "été", "automne", "hiver"
};
List<String> seasons = List.of(seasonsArray);

Une fois l'attribut ALL défini, l'attribut COUNT est trivial à définir puisque sa valeur n'est rien d'autre que la longueur de la liste ALL, qui s'obtient au moyen de la méthode size de List, que vous avez certainement déjà utilisée avec ArrayList.

3.4 Type énuméré Card

Le type énuméré Card du paquetage ch.epfl.tchu.game, public, représente les différents types de cartes du jeu, à savoir les huit types de cartes wagon (un par couleur), et le type de carte locomotive.

Les éléments de ce type énuméré sont, dans l'ordre :

  1. BLACK (wagon noir),
  2. VIOLET (wagon violet),
  3. BLUE (wagon bleu),
  4. GREEN (wagon vert),
  5. YELLOW (wagon jaune),
  6. ORANGE (wagon orange),
  7. RED (wagon rouge),
  8. WHITE (wagon blanc),
  9. LOCOMOTIVE (locomotive).

En plus de ces valeurs, Card offre les attributs ALL et COUNT, définis de la même manière que dans Color, ainsi que l'attribut public, final et statique suivant :

  • List<Card> CARS, qui contient uniquement les cartes wagon, dans l'ordre de définition, c-à-d de BLACK à WHITE.

En dehors des attributs, le type énuméré Card offre la méthode publique et statique suivante :

  • Card of(Color color), qui retourne le type de carte wagon correspondant à la couleur donnée.

Finalement, le type énuméré Card offre la méthode publique suivante :

  • Color color(), qui retourne la couleur du type de carte auquel on l'applique s'il s'agit d'un type wagon, ou null s'il s'agit du type locomotive.

3.4.1 Conseils de programmation

Il peut être judicieux de stocker, dans les instances du type énuméré Card, la couleur qui leur correspond, ou null pour le type locomotive. Cela implique d'ajouter un attribut au type énuméré, ainsi qu'un constructeur l'initialisant. Si vous ne vous souvenez plus comment faire cela, consultez la leçon 10 du cours du semestre passé.

3.5 Classe Station

La classe Station du paquetage ch.epfl.tchu.game, publique, finale et immuable, représente une gare. Elle offre un unique constructeur public :

  • Station(int id, String name), qui construit une gare ayant le numéro d'identification et le nom donnés, ou lève IllegalArgumentException si le numéro d'identification est strictement négatif (< 0).

Le numéro d'identification d'une gare est un numéro unique compris entre 0 et le nombre de gares du réseau moins un. Par exemple, étant donné que le réseau de tCHu comporte 51 gares, elles seront identifiées par un numéro allant de 0 à 50. La valeur exacte du numéro d'identification importe peu, la seule chose qui compte est que chaque gare ait son propre numéro. L'utilité de ce numéro deviendra claire à l'étape 4.

Il faut noter que, comme nous l'avons vu à la §2.2, les gares qui se trouvent dans un pays voisin de la Suisse portent le nom de leur pays. Il est donc tout à fait possible que plusieurs gares différentes portent le même nom, mais elles doivent néanmoins avoir chacune un numéro d'identification différent.

En plus du constructeur, la classe Station offre deux méthodes publiques qui donnent accès aux deux attributs de la gare, à savoir :

  • int id(), qui retourne le numéro d'identification de la gare,
  • String name(), qui retourne le nom de la gare.

Finalement, la classe Station redéfinit la méthode toString pour qu'elle retourne le nom de la gare.

3.6 Interface StationConnectivity

L'interface StationConnectivity du paquetage ch.epfl.tchu.game, publique, représente ce que nous appellerons la « connectivité » du réseau d'un joueur, c-à-d le fait que deux gares du réseau de tCHu soient reliées ou non par le réseau du joueur en question.

Cette interface n'offre qu'une seule méthode, publique et abstraite, qui est :

  • boolean connected(Station s1, Station s2), qui retourne vrai si et seulement si les gares données sont reliées par le réseau du joueur.

Un exemple simple mais peu réaliste d'une classe implémentant cette interface est :

public class FullConnectivity implements StationConnectivity {
  @Override
  public boolean connected(Station s1, Station s2) {
    return true;
  }
}

Cette classe considère que n'importe quelle gare est connectée à n'importe quelle autre, puisque sa méthode connected retourne toujours vrai ! Elle correspond donc au réseau d'un joueur qui aurait réussi à connecter toutes les gares du réseau de tCHu entre elles, ce qui n'est probablement pas possible.

Bien entendu, la classe implémentant StationConnectivity que nous utiliserons dans le projet sera plus sophistiquée — et plus utile — que cela. Elle sera en effet capable de déterminer s'il est possible d'aller d'une gare quelconque du réseau à une autre en empruntant uniquement des routes appartenant à un joueur donné. Grâce à cette classe, il sera possible de déterminer les objectifs qui auront été atteints par les différents joueurs, et de calculer ainsi les points qu'ils méritent.

Cette classe plus sophistiquée est toutefois trop complexe pour être écrite dans le cadre de cette étape, et nous ne l'écrirons qu'à l'étape 4. Grâce à l'existence de l'interface StationConnectivity ci-dessus, il nous est toutefois déjà possible d'écrire du code ayant besoin de cette classe sans qu'elle existe pour autant. Ce code est bien entendu celui déterminant le nombre de points que rapportent les billets, qui est réparti entre les classes Trip et Ticket décrites ci-après.

3.7 Classe Trip

La classe Trip du paquetage ch.epfl.tchu.game, publique, finale et immuable, représente ce que nous avons appelé un trajet. Elle offre un unique constructeur public :

  • Trip(Station from, Station to, int points), qui construit un nouveau trajet entre les deux gares données et valant le nombre de points donné, ou lève NullPointerException si l'une des deux gares est nulle et IllegalArgumentException si le nombre de points n'est pas strictement positif (> 0).

En plus de ce constructeur, la classe Trip offre une méthode publique et statique :

  • List<Trip> all(List<Station> from, List<Station> to, int points), qui retourne la liste de tous les trajets possibles allant d'une des gares de la première liste (from) à l'une des gares de la seconde liste (to), chacun valant le nombre de points donné ; lève IllegalArgumentException si l'une des listes est vide, ou si le nombre de points n'est pas strictement positif.

Finalement, la classe Trip offre les méthodes publiques suivantes :

  • Station from(), qui retourne la gare de départ du trajet,
  • Station to(), qui retourne la gare d'arrivée du trajet,
  • int points(), qui retourne le nombre de points du trajet,
  • int points(StationConnectivity connectivity), qui retourne le nombre de points du trajet pour la connectivité donnée.

La seconde variante de la méthode points retourne le nombre de points du trajet si la méthode connected de la connectivité qu'on lui passe retourne vrai lorsqu'on l'applique aux deux gares du trajet — ce qui signifie qu'elles sont bien connectées —, et la négation de ce nombre de points sinon.

3.7.1 Conseils de programmation

  1. Constructeur

    Prenez garde au fait que le constructeur doit obligatoirement lever l'exception NullPointerException si l'une des gares qu'on lui passe vaut null. Il est donc faux de simplement stocker ces valeurs dans des attributs ainsi :

    public final class Trip {
      private final Station from;
      // … autres attributs
    
      public Trip(Station from, Station to, int points) {
        this.from = from;  // Faux, car accepte null
        // … reste du code
      }
    }
    

    Au lieu de cela, il faut explicitement vérifier que from (et to) n'est pas null avant de le stocker, ce qui peut se faire très facilement au moyen de la méthode requireNonNull de la classe Objects (avec un s à la fin !), ainsi :

    public final class Trip {
      private final Station from;
      // … autres attributs
    
      public Trip(Station from, Station to, int points) {
        this.from = Objects.requireNonNull(from);
        // … reste du code
      }
    }
    
  2. Méthode all

    La méthode all doit retourner une valeur de type List. Comme List est une interface, il n'est pas possible d'en créer directement une instance. Au lieu de cela, il faut créer (et retourner) une instance d'une classe qui implémente cette interface, comme ArrayList que vous connaissez déjà.

    Notez que pour parcourir les éléments des listes from et to, vous pouvez utiliser la boucle for-each que vous avez vue au premier semestre, et probablement déjà utilisée pour parcourir, entre autres, des tableaux dynamiques.

3.8 Classe Ticket

La classe Ticket du paquetage ch.epfl.tchu.game, publique, finale et immuable, représente un billet. Pour des raisons qui deviendront claires plus tard, cette classe implémente l'interface Comparable<Ticket> et sa définition ressemble donc à ceci :

public final class Ticket implements Comparable<Ticket> {
  // … corps de la classe
}

Pour l'instant, il n'est pas nécessaire de comprendre pourquoi la classe Ticket implémente cette interface. Il suffit de savoir que cela implique qu'elle doit offrir une méthode de comparaison nommée compareTo, décrite plus bas.

La classe Ticket offre les deux constructeurs suivants :

  • Ticket(List<Trip> trips), qui construit un billet constitué de la liste de trajets donnée, ou lève IllegalArgumentException si celle-ci est vide, ou si toutes les gares de départ des trajets n'ont pas le même nom,
  • Ticket(Station from, Station to, int points), qui construit un billet constitué d'un unique trajet, dont les attributs sont from, to et points.

Notez que le premier de ces constructeurs est ce que nous nommerons le constructeur principal, et c'est lui qui se charge d'initialiser les attributs de la classe. Le second constructeur est quant à lui ce que nous nommerons un constructeur secondaire, ce qui signifie qu'il consiste en un simple appel au constructeur principal, qui se fait au moyen de la notation this(…).

En plus de ces constructeurs, la classe Ticket offre les méthodes publiques suivantes, au sujet desquels des conseils de programmation vous sont donnés plus bas :

  • String text(), qui retourne la représentation textuelle du billet, telle que décrite à la §2.4.5,
  • int points(StationConnectivity connectivity), qui retourne le nombre de points que vaut le billet, sachant que la connectivité donnée est celle du joueur possédant le billet,
  • int compareTo(Ticket that), qui compare le billet auquel on l'applique (this) à celui passé en argument (that) par ordre alphabétique de leur représentation textuelle, et retourne un entier strictement négatif si this est strictement plus petit que that, un entier strictement positif si this est strictement plus grand que that, et zéro si les deux sont égaux.

De plus, la classe Ticket redéfinit la méthode toString de Object afin qu'elle retourne la même valeur que la méthode text.

3.8.1 Conseils de programmation

  1. Méthode text

    La méthode text doit retourner la représentation textuelle du billet selon la spécification donnée à la §2.4.5. Le calcul de cette représentation étant non trivial, il est conseillé de le placer dans une méthode privée (et idéalement statique) nommée p.ex. computeText. Cette méthode peut ensuite être appelée depuis le constructeur, et son résultat stocké dans un attribut privé (et final) de la classe. La méthode text n'a ensuite plus qu'à retourner la valeur de cet attribut.

    La méthode computeText n'en reste pas moins difficile à écrire avec vos connaissances actuelles. Nous vous donnons donc ci-après quelques indications au sujet de classes et de méthodes qui pourront vous être utiles, à vous de voir comment les combiner.

    Pour commencer, une méthode utile pour écrire computeText est la méthode statique join de la classe String. On lui passe en premier argument une chaîne qui sert de séparateur et en second argument une collection de chaînes, p.ex. un tableau dynamique de type ArrayList<String> ou encore un ensemble de type TreeSet<String> décrit ci-dessous. Elle retourne une chaîne constituée des chaînes de la collection, séparées les unes des autres par le séparateur.

    L'extrait de programme ci-dessous illustre son utilisation en réutilisant la liste seasons introduite à la §3.3.1.

    List<String> seasons = …; // voir plus haut
    
    // s est égale à "printemps, été, automne, hiver"
    String s = String.join(", ", seasons);
    

    En plus de la méthode join de String, la classe TreeSet est aussi très utile à la rédaction de computeText. Cette classe possède la plupart des méthodes de ArrayList et ses instances se comportent presque de la même manière que celles de ArrayList mais avec deux différences importantes :

    1. TreeSet n'admet pas les doublons, ce qui signifie que si un même élément est ajouté plusieurs fois à une instance de TreeSet au moyen de la méthode add, cet élément n'apparaît qu'une fois lorsqu'on parcourt ensuite les éléments de cette instance, p.ex. au moyen de la boucle for-each,
    2. lors du parcours des éléments d'une instance de TreeSet, les éléments apparaissent triés par ordre croissant, et pas dans l'ordre dans lequel on les a insérés au moyen de la méthode add.

    L'extrait de programme suivant illustre ces différences ajoutant les mêmes éléments à une instance de ArrayList et de TreeSet puis en utilisant la méthode join pour obtenir une chaîne de leur contenu. Observez bien la différence entre les deux chaînes !

    ArrayList<String> l = new ArrayList<>();
    TreeSet<String> s = new TreeSet<>();
    
    for (String w: List.of("to","be","or","not","to","be")) {
      l.add(w);
      s.add(w);
    }
    
    // ls est égale à "to be or not to be"
    String ls = String.join(" ", l);
    // ss est égale à "be not or to"
    String ss = String.join(" ", s);
    

    Finalement, une dernière méthode utile à la rédaction de computeText est la méthode statique format de String. Elle prend en premier argument une chaîne de caractères, que l'on nomme la chaîne de formatage (format string), puis un nombre variable d'arguments. Elle retourne une chaîne qui est identique à la chaîne de formatage qu'on lui a passé, si ce n'est que chaque occurrence de la chaîne %s a été remplacée par la représentation textuelle de l'argument correspondant.

    L'extrait de programme ci-dessous illustre l'utilisation de la méthode format pour construire une chaîne :

    String n = "Lausanne";
    int p = 139720;
    
    // s est égale à "Lausanne compte 139720 habitants"
    String s = String.format("%s compte %s habitants", n, p);
    

    Comme vous l'aurez constaté, telle que présentée ci-dessus la méthode format ne semble pas offrir de gros avantage par rapport à l'opérateur de concaténation de chaîne (+). En effet, l'exemple ci-dessus peut aussi s'écrire ainsi :

    String s = n + " compte " + p + " habitants";
    

    En réalité, la méthode format permet de formater les arguments qu'on lui passe de manière sophistiquée, ce que ne permet pas l'opérateur de concaténation. Nous n'utiliserons pas ces possibilités de formatage dans le cadre de ce projet, raison pour laquelle nous ne les présentons pas ici, mais les personnes intéressées par les détails pourront se reporter à la documentation de la classe Formatter.

    Cela dit, nous aurons plus tard de bonnes raisons de préférer l'utilisation de la méthode format à l'opérateur de concaténation, raison pour laquelle il semble judicieux que vous preniez l'habitude de l'utiliser dès maintenant.

  2. Méthode compareTo

    Il est spécifié ci-dessus que la méthode compareTo doit comparer les billets this et that par ordre alphabétique de leur représentation textuelle. Pour ce faire, il faut savoir que la classe String offre également une méthode compareTo, qui compare la chaîne à laquelle on l'applique (this) à celle qu'on lui passe en argument, et retourne un entier négatif quelconque si la première vient avant la seconde dans l'ordre alphabétique, zéro si les deux sont égales, et un entier positif quelconque sinon.

    L'extrait de programme suivant montre trois utilisations de cette méthode, qui correspondent à ses trois types de valeurs de retour possible :

    "EPFL".compareTo("ETHZ");  // retourne un entier négatif
    "EPFL".compareTo("EPFL");  // retourne l'entier 0
    "ETHZ".compareTo("EPFL");  // retourne un entier positif
    

3.9 Tests

Pour vous aider à démarrer ce projet, nous vous fournissons exceptionnellement une archive Zip contenant des tests unitaires JUnit pour cette étape.

Pour pouvoir utiliser ces tests, il vous faut :

  1. les importer dans votre projet (instructions pour IntelliJ ou Eclipse),
  2. ajouter la bibliothèque JUnit à votre projet (instructions pour IntelliJ ou Eclipse).

3.10 Documentation

Une fois les tests exécutés avec succès, il vous reste à documenter la totalité des entités publiques (classes, attributs et méthodes) définies dans cette étape, au moyen de commentaires Javadoc, comme décrit dans le guide consacré à ce sujet. Vous pouvez écrire ces commentaires en français ou en anglais, en fonction de votre préférence, mais vous ne devez utiliser qu'une seule langue pour tout le projet.

4 Résumé

Pour cette étape, vous devez :

  • installer la version 11 (et ni une plus récente, ni une plus ancienne !) de Java sur votre ordinateur, ainsi que la dernière version d'IntelliJ ou d'Eclipse,
  • si vous utilisez Eclipse, le configurer selon les indications données dans le document consacré à ce sujet,
  • écrire les classes, interfaces et types énumérés Preconditions, Color, Card, Station, StationConnectivity, Trip et Ticket selon les indications données plus haut,
  • vérifier que les tests que nous vous fournissons s'exécutent sans erreur, et dans le cas contraire, corriger votre code,
  • documenter la totalité des entités publiques que vous avez définies,
  • (optionnel mais fortement recommandé) rendre votre code au plus tard le 26 février 2021 à 17h00, via le système de rendu.

Ce premier rendu n'est pas noté, mais celui de la prochaine étape le sera. Dès lors, il est vivement conseillé de faire un rendu de test cette semaine afin de se familiariser avec la procédure à suivre.

Notes de bas de page

1

Bien qu'assez réaliste, cette carte comporte deux inexactitudes : premièrement, le Liechtenstein est intégré à la Suisse ; et deuxièmement, par manque de place, La Chaux-de-Fonds est située en France, bien qu'il s'agisse d'une ville suisse.