Mise en place
Ajul – étape 1
1. Introduction
Le but de cette première étape est d'écrire des classes et interfaces représentant quelques concepts importants d'Ajul — les types de tuiles, les fabriques et la zone centrale, etc. — ainsi que différents types de tableaux d'entiers.
Comme toutes les descriptions d'étapes, 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 — ce qui est fortement recommandé au vu de la difficulté du projet — vous êtes néanmoins priés de :
- lire le (court) guide Travailler en groupe, qui vous aidera à bien vous organiser,
- faire cette première étape en travaillant ensemble devant un seul ordinateur, comme décrit à la section Travailler en binôme de ce guide, ce qui vous permettra de bien démarrer votre collaboration.
De plus, nous vous demandons de ne pas créer un entrepôt git avant d'avoir au moins terminé les étapes 1 et 2, même si vous comptez utiliser git par la suite. N'oubliez pas que les étapes 1 à 6 devront être rendues à la fin de chaque semaine, et qu'il est donc important de vous mettre au travail rapidement, sans perdre du temps à configurer un outil dont vous pouvez très bien vous passer au début.
2. Concepts
2.1. Sortes de tuiles
Comme cela a été expliqué dans l'introduction au projet, une partie d'Ajul se joue au moyen de tuiles colorées de 5 couleurs différentes. Il existe 20 tuiles de chaque couleur, pour un total de 100 tuiles. Les couleurs exactes utilisées importent peu, tant et aussi longtemps qu'elles sont distinctes. Dans le programme, nous les représenterons donc généralement par les lettres A à E.
En plus des tuiles colorées, il existe un unique marqueur de premier joueur, dont le rôle est différent de celui des tuiles colorées, mais qu'il est parfois utile de considérer comme une tuile.
Pour une raison qui deviendra claire plus tard, nous devrons souvent représenter les différentes sortes de tuiles — et de nombreux autres éléments du jeu — par un index numérique. Dans ce cas, nous attribuerons les index 0 à 4 (inclus) aux tuiles colorées — A ayant l'index 0, B l'index 1, etc. — et l'index 5 au marqueur de premier joueur.
2.2. Sources de tuiles
Lorsque c'est à son tour, un joueur s'empare, comme nous l'avons dit, de toutes les tuiles d'une couleur donnée se trouvant soit :
- sur l'une des fabriques, ou
- sur la zone centrale.
Nous nommerons donc sources de tuiles (tile sources en anglais) les fabriques et la zone centrale. Nous attribuerons de plus un index à chacune de ces sources, qui vaudra 0 pour la zone centrale et 1 à 9 pour les fabriques. Attribuer l'index 0 à la zone centrale permet de garantir que les index de source valides pour une partie sont toujours compris dans un intervalle allant de 0 à 5 (pour une partie à 2 joueurs), 7 (3 joueurs) ou 9 (4 joueurs).
2.3. Destinations de tuiles
Une fois qu'il s'est emparé de toute les tuiles d'une couleur et d'une source donnée, le joueur les place soit :
- sur l'une des lignes de motif de son plateau personnel, ou
- sur la ligne plancher de son plateau personnel.
Nous nommerons donc destinations de tuiles (tile destinations) les lignes de motif et la ligne plancher. Une fois encore, nous attacherons à chaque destination un index qui vaudra :
- de 0 à 4 pour les lignes de motif (de haut en bas),
- 5 pour la ligne plancher.
Ces différentes destinations ont toutes une capacité (capacity), qui est le nombre de tuiles qu'elles peuvent accueillir. La première ligne de motif a une capacité de 1, la seconde de 2, et ainsi de suite. De son côté, la ligne plancher a une capacité de 7 tuiles.
2.4. Mélange des tuiles
Au début d'une manche, chacune des fabriques doit être remplie avec 4 tuiles choisies au hasard. Dans la version physique du jeu (Azul), cela se fait en tirant ces tuiles d'un sac, mais bien entendu cette technique n'est pas directement utilisable dans notre version informatique.
Au lieu de littéralement tirer les tuiles d'un sac, notre programme commencera par créer — d'une manière qui sera décrite ultérieurement — un tableau contenant juste assez de tuiles pour remplir les fabriques, puis mélangera ce tableau. Ensuite, les 4 premières tuiles du tableau mélangé seront placées dans la première fabrique, les 4 suivantes dans la seconde, et ainsi de suite.
La question se pose donc de savoir comment mélanger les éléments d'un tableau de tuiles, de manière efficace et juste, c.-à-d. en garantissant que chaque permutation des éléments ait la même probabilité d'être choisie. Il existe pour cela un algorithme très simple et élégant, souvent appelé l'algorithme de Fisher-Yates.
Cet algorithme s'inspire justement de la technique consistant à placer les éléments à mélanger dans un sac, et à les en extraire dans un ordre aléatoire. L'idée est de parcourir les éléments du tableau à mélanger au moyen d'un index i allant de 0 à la taille du tableau moins 1. Cet index partitionne les éléments du tableau en deux :
- les éléments d'index strictement inférieur à
isont ceux qui se trouvent à leur position finale dans le tableau — c.-à-d. ceux qui ont déjà été sortis du sac, - les éléments d'index supérieur ou égal à
isont ceux dont la position finale est encore inconnue — c.-à-d. ceux qui se trouvent encore dans le sac.
À chaque itération, un élément d'index supérieur ou égal à i — donc se trouvant encore dans le sac — est choisi aléatoirement et échangé avec celui d'index i. Lorsque i est ensuite incrémenté, cet élément est « sorti du sac » et placé à sa position finale dans le tableau.
Lorsque l'index i désigne le dernier élément du tableau, il ne reste plus qu'un élément dans le sac, et l'algorithme peut se terminer car cet élément est forcément déjà à sa position finale.
Sous forme de pseudo-code, cet algorithme ressemble à ceci, tableau étant le tableau à mélanger :
pour i de 0 à taille(tableau) - 2: j := nombre aléatoire entre i et taille(tableau) - 1 échanger tableau[i] et tableau[j]
Notez que les bornes des intervalles, pour i et pour j, sont toutes inclusives.
Pour bien comprendre le fonctionnement de cet algorithme, utilisez-le pour mélanger « à la main » les éléments du tableau de 5 lettres suivant :
| index | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| élément | a | n | c | r | e |
en admettant que le nombre choisi aléatoirement (j dans le pseudo-code) vaut 3 à chaque itération. Quel autre mot français forment les lettres du tableau mélangé ? (Si vous n'êtes pas sûr que le mot que vous obtenez soit un mot français, cherchez-le dans un dictionnaire comme Le Robert en ligne.)
2.5. Tableaux d'entiers
Comme vous l'avez vu au semestre précédent, Java offre des tableaux d'entiers primitifs de type int[]. La bibliothèque standard offre de plus ce que vous avez nommé des « tableaux dynamiques » d'entiers, de type ArrayList<Integer>. Les différences entre ces deux types de tableaux sont nombreuses, mais deux d'entre elles sont particulièrement importantes pour ce projet :
- Les tableaux primitifs sont toujours modifiables, dans le sens où n'importe quelle partie d'un programme ayant accès à une valeur de type
int[]a la possibilité de modifier les éléments qu'elle contient ; par contre, il est possible de construire des tableaux dynamiques non modifiables (au moyen de la méthodeCollections.unmodifiableList) et même immuables (au moyen de la méthodeList.ofentre autres). - Les tableaux primitifs sont plus performants et utilisent moins de mémoire que les tableaux dynamiques, car ils stockent directement des valeurs du type primitif
intalors que les tableaux dynamiques stockent des objets de typeInteger, chacun d'entre eux contenant une valeur de type primitifint.
Dans ce projet, nous utiliserons des tableaux d'entiers pour représenter certaines parties de l'état du jeu. Idéalement, nous aimerions pour cela avoir à disposition des tableaux d'entiers ayant les mêmes bonnes performances que les tableaux primitifs, mais aussi la possibilité d'être non modifiables ou immuables. Plus précisément, nous aurions besoin de pouvoir représenter trois types différents de tableaux d'entiers int :
- des tableaux modifiables, en tous points similaires aux tableaux primitifs de type
int[], - des tableaux non modifiables, que nous appellerons en lecture seule (read-only), dont le contenu peut être lu mais pas modifié, mais qui peut néanmoins changer au cours du temps (!),
- des tableaux immuables, dont le contenu ne peut pas changer au cours du temps.
Il est important de bien comprendre la distinction entre un tableau (ou, plus généralement, un objet) en lecture seule et un tableau (ou objet) immuable. L'analogie suivante pourrait vous aider à la comprendre.
Imaginez que tous les jours vous consultiez le site de MétéoSuisse pour connaître les dernières prévisions météo. Il est bien clair que les informations qui s'affichent dans votre navigateur lorsque vous faites cela peuvent changer au cours du temps — et c'est justement ce qui motive vos visites quotidiennes. Pour autant, vous n'avez pas la possibilité vous-même de changer le contenu de ce site, car vous ne pouvez que le lire, et pas le modifier — votre accès est en lecture seule. La raison pour laquelle son contenu change néanmoins au cours du temps est que quelqu'un d'autre — employé par MétéoSuisse — a un accès plus permissif à ce site, et peut en changer le contenu.
Imaginez maintenant que le 17 février 2026 à 8h52 vous décidiez d'imprimer les prévisions actuellement affichées sur le site de MétéoSuisse. Il est évident que la version imprimée du site que vous obtenez alors est figée à tout jamais, et ne changera plus, même si le site de MétéoSuisse est mis à jour. Cette version imprimée du site est immuable.
Avec les tableaux d'entiers Java dont il est question plus haut, la différence est similaire.
Si vous avez une référence vers un tableau d'entiers non modifiable, vous pouvez consulter le contenu de ce tableau en demandant la valeur d'un de ses éléments, mais vous n'avez pas pour autant la possibilité de changer ce contenu. Il est toutefois possible que ce contenu change, et donc qu'en demandant à différents instants la valeur d'un même élément de ce tableau, vous obteniez des valeurs différentes. Cela pourrait se produire si une autre partie du programme avait un accès plus permissif à ce même tableau, qui lui donne la possibilité d'en changer le contenu.
Par contre, si vous avez une référence vers un tableau d'entier immuable, alors vous avez la garantie que son contenu ne changera jamais. Donc vous êtes sûr qu'en demandant à différents instants la valeur d'un même élément de ce tableau, vous obtiendrez toujours la même valeur.
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.
Toutes les classes et interfaces de ce projet appartiendront au paquetage ch.epfl.ajul — qui sera désigné par le terme paquetage principal dans les descriptions d'étapes — ou à l'un de ses sous-paquetages.
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 et d'IntelliJ
Avant de commencer à programmer, il faut vous assurer que la version 25 de Java est bien installée sur votre ordinateur, car c'est celle qui sera utilisée pour ce projet.
Si vous ne l'avez pas encore installée, rendez-vous sur le site Web du projet Adoptium, téléchargez le programme d'installation correspondant à votre système d'exploitation (macOS, Linux ou Windows), et exécutez-le.
Cela fait, si vous n'avez pas encore installé IntelliJ, téléchargez-le depuis le site de JetBrains et installez-le.
3.2. Importation du squelette
Une fois Java et IntelliJ installés, vous pouvez télécharger le squelette de projet que nous mettons à votre disposition. Il s'agit d'une archive Zip dont vous devrez tout d'abord extraire le contenu à un emplacement de votre choix sur votre ordinateur. (Notez que certains navigateurs comme Safari extraient automatiquement le contenu de telles archives.)
Une fois le contenu de l'archive extrait, vous constaterez qu'il se trouve en totalité dans un dossier nommé Ajul. Lancez IntelliJ, choisissez Open, sélectionnez ce dossier, et finalement cliquez Ok.
Le dossier Ajul contient les sous-dossiers suivants :
src, destiné à contenir le code source de votre projet, ainsi que divers fichiers que nous mettrons à votre disposition, et qui contient pour l'instant :SignatureChecks_1.java, un fichier de vérification de signatures pour l'étape 1, qui ne devrait plus contenir d'erreur lorsque vous aurez terminé la rédaction de cette étape,Submit.java, un programme qui vous permettra de rendre votre projet à la fin de chaque semaine,
test, destiné à contenir le code des tests unitaires de votre projet que nous vous fournirons ou que vous écrirez vous-même, et qui contient pour l'instant les tests de l'étape 1 — fournis exceptionnellement pour faciliter votre démarrage.
Pour vous assurer d'avoir bien importé le projet, vérifiez que le contenu du panneau Project d'IntelliJ ressemble à l'image ci-dessous.
3.3. Interface TileKind
L'interface TileKind du paquetage principal (ch.epfl.ajul), publique, représente les six sortes de tuiles qui existent dans le jeu, à savoir les cinq sortes de tuiles colorées et le marqueur de premier joueur.
TileKind est implémentée par les types énumérés Colored, décrit à la §3.4 plus bas, et FirstPlayerMarker, décrit à la §3.5. Il est donc fortement conseillé de lire également ces deux sections avant de commencer la rédaction de TileKind. Ces deux types énumérés sont imbriqués à l'intérieur de l'interface TileKind, qui a donc la structure suivante :
public interface TileKind {
// … attributs et méthodes abstraites
enum Colored implements TileKind {
// … valeurs du type énuméré, attributs, méthodes concrètes
}
enum FirstPlayerMarker implements TileKind {
// … valeurs du type énuméré, attributs, méthodes concrètes
}
}
En raison de cette organisation, trois types Java différents existent pour représenter une sorte de tuile Ajul, à savoir :
TileKind- qui représente n'importe laquelle des six sortes de tuiles existantes dans le jeu — les cinq sortes de tuiles colorées et le marqueur de premier joueur,
TileKind.Colored- qui représente l'une des cinq sortes de tuiles colorées (A, B, C, D ou E),
TileKind.FirstPlayerMarker- qui représente le marqueur de premier joueur.
Comme vous le verrez ultérieurement, avoir ces trois types à disposition nous permet de mieux exprimer, au moyen de types Java, les valeurs valides dans une situation donnée. Par exemple, lorsqu'un joueur joue un coup, il choisit toutes les tuiles d'une même couleur d'une source donnée. Lorsque nous représenterons un coup, nous utiliserons donc le type TileKind.Colored plutôt que TileKind pour décrire la couleur choisie, garantissant ainsi que seules les tuiles colorées peuvent être jouées, et pas le marqueur de premier joueur.
L'interface TileKind offre les attributs publics, statiques et finaux suivants :
TileKind A- qui représente les tuiles colorées de couleur A,
TileKind B- qui représente les tuiles colorées de couleur B,
TileKind C- qui représente les tuiles colorées de couleur C,
TileKind D- qui représente les tuiles colorées de couleur D,
TileKind E- qui représente les tuiles colorées de couleur E,
TileKind FIRST_PLAYER_MARKER- qui représente le marqueur de premier joueur.
Ces attributs sont simplement définis comme étant égaux aux valeurs correspondantes des types énumérés décrits aux §3.4 et §3.5. Ainsi, TileKind.A référence exactement le même objet que TileKind.Colored.A mais avec un type différent ! En effet, TileKind.A a le type TileKind tandis que TileKind.Colored.A a le type TileKind.Colored. En fonction des besoins, nous utiliserons l'un ou l'autre de ces attributs.
En plus des attributs ci-dessus représentant les différentes sortes de tuiles, TileKind offre les deux attributs suivants :
List<TileKind> ALL- une liste immuable qui contient les six attributs définis plus haut (
A,B,C,D,EetFIRST_PLAYER_MARKER), dans le même ordre, int COUNT- qui contient le nombre de sortes de tuiles qui existent (6).
Les conseils de programmation ci-dessous vous donneront les indications nécessaires à la définition de ces deux attributs.
Finalement, TileKind offre deux méthodes publiques et abstraites, qui sont :
int index()- qui retourne l'index de la sorte de tuile à laquelle on l'applique, à savoir 0 pour
A, 1 pourB, …, 4 pourEet 5 pourFIRST_PLAYER_MARKER, int tilesCount()- qui retourne le nombre de tuiles de la sorte à laquelle on l'applique qui existent dans le jeu, à savoir 20 pour les tuiles colorées et 1 pour le marqueur de premier joueur.
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 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 de la méthode of de l'interface List, qui retourne une liste immuable ayant les éléments qu'on lui passe en argument. Par exemple, l'extrait de programme ci-dessous construit une liste contenant les noms des saisons :
List<String> seasons =
List.of("printemps", "été", "automne", "hiver");
Une fois ALL défini, COUNT se défini très facilement au moyen de la méthode size de List.
3.4. Type énuméré TileKind.Colored
Le type énuméré Colored, imbriqué dans l'interface TileKind, représente les différentes sortes de tuiles colorées qui existent dans le jeu. Il possède donc cinq valeurs, nommées A, B, C, D et E, qui représentent les cinq couleurs de tuiles. Notez que ces valeurs doivent impérativement être définies dans cet ordre.
Colored implémente l'interface TileKind et possède donc des définitions concrètes des méthodes index et tilesCount. De plus, Colored offre des attributs similaires à ceux offerts par TileKind, à savoir :
List<Colored> ALL- une liste immuable qui contient la totalité des valeurs du type énuméré, dans leur ordre de définition (voir les conseils de programmation plus bas),
int COUNT- qui contient le nombre de valeurs du type énuméré, à savoir 5.
Finalement, Colored offre une méthode publique et statique permettant de mélanger les éléments d'un tableau de valeurs de type Colored au moyen de l'algorithme décrit à la §2.4 :
void shuffle(Colored[] tiles, RandomGenerator randomGenerator)- qui mélange les éléments du tableau
tilesen utilisant le générateur aléatoirerandomGeneratorpour obtenir des nombres aléatoires (voir les conseils de programmation ci-dessous).
3.4.1. Conseils de programmation
Étant donné que Colored est un type énuméré, la définition de l'attribut ALL est encore plus simple que dans le cas de TileKind. En effet, tous les types énumérés Java possèdent une méthode statique nommée values qui ne prend aucun argument et retourne un tableau contenant tous les éléments du type énuméré, dans leur ordre de définition. Ce tableau peut directement être passé à List.of pour obtenir la liste immuable désirée.
De plus, toutes les valeurs des types énumérés en Java possèdent une méthode ordinal qui retourne l'index de la valeur en question dans le type énuméré auquel elle appartient. Ainsi, appliquée à A, ordinal retourne 0, tandis qu'appliquée à E elle retourne 4. Il va de soi que cette méthode est très utile pour définir la méthode index.
L'interface RandomGenerator de la bibliothèque Java représente un générateur aléatoire, c.-à-d. un objet capable de fournir des valeurs choisies aléatoirement satisfaisant certains critères. Pour écrire la méthode shuffle, la méthode nextInt de cette interface est particulièrement utile. Avant de l'utiliser, lisez bien sa documentation pour comprendre la signification de ses deux arguments !
3.5. Type énuméré TileKind.FirstPlayerMarker
Le type énuméré FirstPlayerMarker est similaire à Colored mais ne possède qu'une seule valeur, nommée FIRST_PLAYER_MARKER, qui représente le marqueur de premier joueur.
Les seules méthodes offertes par FirstPlayerMarker sont des définitions concrètes des méthodes de TileKind (index et tilesCount).
3.6. Interface scellée
En Java, une interface peut normalement être implémentée par n'importe quelle classe. Par exemple, l'interface TileKind pourrait très bien être implémentée par d'autres classes que Colored et FirstPlayerMarker.
Dans ce cas particulier, cela n'est toutefois pas souhaitable, car nous savons que les seules sortes de tuiles qui existent dans le projet sont celles-là. Il serait donc bien de pouvoir communiquer cela à Java, afin d'interdire la définition d'autres classes implémentant l'interface TileKind.
Cela peut se faire en scellant (seal) l'interface TileKind, simplement en ajoutant le mot-clef sealed à l'interface, ainsi :
public sealed interface TileKind { … }
Lorsqu'une interface est ainsi scellée, les seules classes qui ont le droit de l'implémenter sont celles se trouvant dans la même unité de compilation (compilation unit), c.-à-d. le même fichier Java.
3.7. Interface TileSource
L'interface TileSource du paquetage principal, publique et scellée, identifie une source de tuiles, c.-à-d. l'une des fabriques ou la zone centrale.
Pour la même raison qu'il est utile d'avoir trois types représentant les sortes de tuiles, il est utile d'avoir trois types identifiant les sources de tuiles, qui sont :
TileSource, une interface identifiant une source de tuiles quelconque (fabrique ou zone centrale),TileSource.CenterArea, un type énuméré ne possédant qu'une seule valeur, qui identifie la zone centrale,TileSource.Factory, un type énuméré possédant neuf valeurs, qui identifient chacune l'une des fabriques.
L'organisation de l'interface TileSource est donc très similaire à celle de TileKind et n'est donc décrite que très rapidement ci-après.
TileSource possède dix attributs publics, statiques et finaux qui correspondent aux dix sources de tuiles existant. Tous ces attributs ont le type TileSource et celui correspondant à la zone centrale se nomme CENTER_AREA tandis que ceux correspondant aux fabriques se nomment FACTORY_1 à FACTORY_9.
TileSource offre une unique méthode abstraite et publique :
int index()- qui retourne l'index de la source, à savoir 0 pour la zone centrale, et 1 à 9 pour les fabriques 1 à 9.
Finalement, TileSource offre deux attributs similaires à ceux qui existent dans TileKind :
List<TileSource> ALL- une liste immuable qui contient toutes les sources, chacune d'entre elles étant à l'index retourné par la méthode
index— 0 pour la zone centrale, 1 pour la fabrique 1, etc. int COUNT- qui contient le nombre total de sources de tuiles, à savoir 10.
3.8. Type énuméré TileSource.CenterArea
Le type énuméré CenterArea, imbriqué dans l'interface TileSource qu'il implémente, identifie la zone centrale. Il ne contient donc qu'une seule valeur, nommée CENTER_AREA, qui représente cette zone.
CenterArea n'offre rien d'autre qu'une mise en œuvre concrète de la méthode index de TileSource, qui retourne 0.
3.9. Type énuméré TileSource.Factory
Le type énuméré Factory, imbriqué dans l'interface TileSource qu'il implémente, identifie les fabriques. Il contient donc neuf valeurs, nommées FACTORY_1 à FACTORY_9, définies dans cet ordre.
Factory offre un attribut public, statique et final de type int, nommé TILES_PER_FACTORY et contenant le nombre de tuiles que contient une fabrique, à savoir 4. De plus, elle offre les attributs ALL et COUNT déjà décrits plus haut pour d'autres types, ainsi qu'une mise en œuvre concrète de la méthode index de TileSource, qui retourne 1 pour la fabrique 1, 2 pour la fabrique 2, etc.
3.10. Interface TileDestination
L'interface TileDestination du paquetage principal, publique et scellée, identifie une destination de tuile, c.-à-d. l'une des 5 lignes de motif ou la ligne plancher.
TileDestination est très similaire à TileSource et est également implémentée par deux types énumérés représentant chacun un type de destination. Il s'agit de :
TileDestination possède six attributs publics, statiques et finaux qui correspondent aux six destinations de tuiles existant. Ils ont le type TileDestination et ceux correspondant aux lignes de motif se nomment PATTERN_1 à PATTERN_5 tandis que celui correspondant à la ligne plancher se nomme FLOOR.
TileDestination offre deux méthodes abstraite et publiques :
int index()- qui retourne l'index de la destination, à savoir 0 pour la ligne de motif 1, 1 pour la ligne de motif 2, …, 4 pour la ligne de motif 5, et 5 pour la ligne plancher,
int capacity()- qui retourne la capacité de la destination, c.-à-d. le nombre de tuiles qu'elle peut contenir, à savoir 1 pour la première ligne, 2 pour la seconde, etc. et 7 pour la ligne plancher.
Finalement, TileDestination offre des attributs ALL et COUNT définis de manière similaire à ceux des classes ci-dessus.
3.11. Type énuméré TileDestination.Pattern
Le type énuméré Pattern, imbriqué dans l'interface TileDestination qu'il implémente, identifie les lignes de motif. Il contient donc cinq valeurs, nommées PATTERN_1 à PATTERN_5, définies dans cet ordre.
Pattern offre les attributs ALL et COUNT déjà décrits plus haut pour d'autres types, ainsi qu'une mise en œuvre concrète des méthodes index et capacity de TileDestination.
3.12. Type énuméré TileDestination.Floor
Le type énuméré Floor, imbriqué dans l'interface TileDestination qu'il implémente, représente la ligne plancher. Il ne contient donc qu'une seule valeur, nommée FLOOR, qui représente cette ligne.
En plus de cette valeur, Floor offre bien entendu une mise en œuvre concrète des méthodes index et capacity de TileDestination.
3.13. Interface ReadOnlyIntArray
L'interface ReadOnlyIntArray du sous-paquetage intarray (donc du paquetage ch.epfl.ajul.intarray), publique, représente un tableau d'entier en lecture seule. C'est-à-dire que les éléments d'un tableau de ce type peuvent être lus, mais pas modifiés.
ReadOnlyIntArray offre les méthodes publiques et abstraites suivantes :
int size()- qui retourne la taille du tableau,
int get(int i)- qui retourne l'élément d'index
idu tableau, ou lève une exception de typeIndexOutOfBoundsExceptionsi cet index est invalide, ImmutableIntArray immutable()- qui retourne un tableau d'entier immuable (voir §3.15) ayant les mêmes éléments et la même taille que le tableau auquel on applique la méthode,
int[] toArray()- qui retourne un nouveau tableau Java primitif ayant les mêmes éléments et la même taille que le tableau auquel on applique la méthode.
3.14. Classe AbstractIntArray
La classe AbstractIntArray du sous-paquetage intarray, publique et abstraite, sert de classe-mère aux différentes classes mettant en œuvre les tableaux d'entiers. Elle implémente donc l'interface ReadOnlyIntArray.
AbstractIntArray possède un attribut privé de type int[] qui est le tableau primitif contenant les éléments du tableau d'entier, ainsi qu'un constructeur protégé (protected) prenant en argument ce tableau et le stockant dans l'attribut. Ce constructeur est bien entendu destiné à être utilisé par les deux sous-classes de AbstractIntArray, à savoir ImmutableIntArray (§3.15) et MutableIntArray (§3.16).
En plus de cet attribut privé et du constructeur protégé, AbstractIntArray offre des mises en œuvre concrètes de toutes les méthodes de l'interface ReadOnlyIntArray, à savoir :
sizeetget, qui sont triviales à écrire car elles correspondent respectivement à l'attributlengthet l'opération d'indexation ([]) du tableau primitif stocké en attribut,immutable, qui est aussi triviale à écrire mais seulement une fois que la méthodecopyOfdeImmutableIntArraya été écrite (voir §3.15),toArray, qui est aussi triviale à écrire car elle doit simplement retourner une copie du tableau primitif stocké en attribut, qu'elle peut obtenir p. ex. au moyen de la méthodeclone.
En plus de ces mises en œuvre concrètes des méthodes de ReadOnlyIntArray, AbstractIntArray contient également une redéfinition de la méthode toString de Object. Cette redéfinition de toString doit retourner la même chaîne que celle obtenue en appliquant la méthode toString de Arrays au tableau primitif stocké en attribut.
3.15. Classe ImmutableIntArray
La classe ImmutableIntArray du sous-paquetage intarray, publique et finale, représente un tableau d'entier immuable, c.-à-d. dont le contenu ne peut pas changer après sa création. Elle hérite de AbstractIntArray et implémente donc, indirectement, l'interface ReadOnlyIntArray.
Le constructeur de ImmutableIntArray est privé et ne fait rien d'autre qu'appeler celui de sa super-classe en lui passant en argument le tableau reçu. Comme ce constructeur est privé, il n'est pas possible de l'utiliser depuis l'extérieur de la classe pour créer des instances de ImmutableIntArray. Pour cette raison, ImmutableIntArray offre ce que l'on nomme une méthode fabrique (factory method), publique et statique, qui permet de créer des instances de la classe :
ImmutableIntArray copyOf(int[] array)- qui retourne un nouveau tableau d'entiers immuable dont la taille et les éléments sont les mêmes que ceux du tableau primitif
array.
La raison pour laquelle nous avons choisi de rendre le constructeur privé et d'offrir à la place la méthode fabrique copyOf est que le nom de cette méthode rappelle à ses utilisateurs que le tableau passé en argument est copié. Notez que cette technique consistant à offrir des méthodes fabriques plutôt que des constructeurs est fréquemment utilisée en Java, et il est donc bon de la connaître.
En plus de la méthode fabrique copyOf, ImmutableIntArray redéfinit la méthode immutable de ReadOnlyIntArray pour qu'elle retourne simplement le récepteur (this). En effet, lorsque cette méthode est appliquée à un tableau qui est déjà immuable, il n'est pas nécessaire de copier ce dernier.
3.16. Classe MutableIntArray
La classe MutableIntArray du sous-paquetage intarray, publique et finale, représente un tableau d'entier non immuable. Elle hérite de AbstractIntArray et implémente donc, indirectement, l'interface ReadOnlyIntArray.
Une instance de MutableIntArray est très similaires à un tableau d'entiers primitifs de type int[], si ce n'est qu'elle a un type différent. En particulier, son type est un sous-type de ReadOnlyIntArray. Cela nous permet d'utiliser une instance de MutableIntArray dans les situations dans lesquelles une valeur de type ReadOnlyIntArray est attendue, ce qui ne serait pas possible avec un tableau d'entiers primitif de type int[].
Tout comme ImmutableIntArray, MutableIntArray « cache » son constructeur en le rendant privé, et offre à la place la méthode fabrique suivante, publique et statique :
MutableIntArray wrapping(int[] array)- qui retourne un nouveau tableau d'entiers non immuable qui « emballe » simplement le tableau
arraydonné, sans le copier (!).
Là aussi, le nom de la méthode fabrique a pour but de rappeler à ses utilisateurs que la méthode se contente « d'emballer » le tableau primitif qu'on lui passe en argument, en le stockant dans un de ses attributs, mais sans le copier. En conséquence, si le contenu de ce tableau primitif change par la suite, le tableau MutableIntArray changera également.
Comme MutableIntArray représente un tableau d'entiers non immuable, il serait logique de lui ajouter au moins une méthode permettant de modifier un élément (nommée p. ex. set). Mais comme cette modification peut également se faire en modifiant directement le tableau passé à la méthode wrapping, et que c'est ce que nous ferons dans ce projet, nous avons renoncé à ajouter une telle méthode.
3.17. Tests
Pour vous aider à démarrer ce projet, des tests unitaires JUnit vous sont exceptionnellement fournis pour cette étape, et se trouvent dans le dossier test du squelette. Une fois que vous aurez terminé la rédaction des classes de cette étape, vous pourrez les exécuter en :
- ajoutant JUnit à votre projet, comme expliqué dans notre guide à ce sujet,
- effectuant un clic droit sur le dossier
testdu projet et sélectionnant Run 'All Tests'.
3.18. 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 25 (et ni une plus récente, ni une plus ancienne !) de Java sur votre ordinateur, ainsi que la dernière version d'IntelliJ IDEA,
- écrire les classes, interfaces et types énumérés
ReadOnlyIntArray,AbstractIntArray,ImmutableIntArray,MutableIntArray,TileKind,TileKind.Colored,TileKind.FirstPlayerMarker,TileSource,TileSource.CenterArea,TileSource.Factory,TileDestination,TileDestination.PatternetTileDestination.Floorselon les indications ci-dessus, - 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 20 février à 18h00, en exécutant le programme
Submitfourni dans le squelette, après avoir modifié les définitions des variablesTOKEN_1etTOKEN_2pour qu'elles contiennent les jetons individuels des deux membres du groupe, disponibles sur la page privée de chacun d'eux. (Les personnes travaillant seules doivent mettre leur jeton individuel dans les deux variables.)
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.