Lignes de motif et plancher
Ajul – étape 3
1. Introduction
Le but principal de cette troisième étape est de continuer la rédaction des classes permettant de manipuler des représentations empaquetées des différents éléments de l'état du jeu, en écrivant celles gérant les lignes de motif et la ligne plancher.
2. Concepts
2.1. Lignes de motif
Comme cela a été expliqué dans l'introduction au projet, les plateaux personnels des différents joueurs contiennent cinq lignes de motif (pattern lines) ayant chacune la capacité d'accueillir un nombre variable de tuiles colorées : la première (depuis le haut) peut en accueillir une, la seconde deux, et ainsi de suite jusqu'à la cinquième, qui peut en accueillir cinq. Lorsqu'une ligne de motif contient des tuiles, elles doivent toutes être de la même couleur.
Sachant que les lignes de motif font partie de l'état de jeu, nous les représenterons de manière empaquetée.
2.1.1. Représentation empaquetée
Une ligne de motif peut contenir au maximum 5 tuiles d'une seule couleur, et il est donc possible de représenter chacune des cinq lignes de motif au moyen d'une paire composée du nombre de tuiles qu'elle contient, et de la couleur de ces tuiles.
Le nombre de tuiles peut être compris entre 0 et 5 (inclus), et peut donc se représenter au moyen de 3 bits. La couleur, ou plutôt son index, est compris entre 0 et 4 (inclus) et nécessite donc également 3 bits. Au total, chaque ligne nécessite donc 6 bits, et comme il y a 5 lignes, un total de 30 bits suffit à représenter le contenu de toutes les lignes de motif. Nous représenterons donc les lignes de motif au moyen d'une valeur de type int dont :
- les 3 bits de poids faible contiennent le nombre de tuiles présentes sur la première ligne de motif, compris entre 0 et 1,
- les 3 bits suivants contiennent l'index de la couleur de ces tuiles, ou 0 si la ligne est vide,
- les 3 bits suivants contiennent le nombre de tuiles présentes sur la seconde ligne de motif, compris entre 0 et 2,
- etc.
Les deux bits de poids fort valent toujours 0.
Par exemple, le contenu des lignes de motif visibles dans la figure 1 plus bas est représenté par les 32 bits suivants (des espaces ont été insérées entre les groupes de bits pour faciliter la lecture) :
00 000 000 100 011 000 011 000 010 010 001
Pour mémoire, les tuiles de couleur A () ont l'index 0, celles de couleur C () l'index 2, et celles de couleur E () l'index 4.
2.2. Ligne plancher
La ligne plancher (floor line) d'un joueur collecte les tuiles qu'il n'a pas pu placer sur ses lignes de motif, ainsi que le marqueur de premier joueur. À la fin d'une manche, les tuiles présentes sur la ligne plancher d'un joueur le pénalisent, en lui faisant perdre un nombre de points qui dépend du nombre de ces tuiles.
La ligne plancher a une capacité de 7 tuiles. Lorsqu'elle est pleine, les éventuelles tuiles qu'on voudrait encore y ajouter sont simplement sorties du jeu1, sauf le marqueur de premier joueur qui doit toujours être soit sur la zone centrale, soit sur la ligne plancher d'un joueur. Dès lors, dans le cas — très peu probable en pratique — où le marqueur de premier joueur est pris par un joueur dont la ligne plancher est déjà pleine, ce marqueur prend la place de la dernière tuile, qui est elle sortie du jeu.
La ligne plancher faisant également partie de l'état du jeu, nous la représenterons aussi de manière empaquetée.
2.2.1. Représentation empaquetée
Le contenu de la ligne plancher peut être représenté au moyen d'une séquence contenant entre 0 et 7 (inclus) sortes de tuiles.
Il y a six sortes de tuiles, et 3 bits suffisent donc à représenter l'index de l'une d'entre elles. La taille de la ligne plancher, c.-à-d. le nombre de tuiles qu'elle contient, est quant à lui compris entre 0 et 7 (inclus), donc 3 bits suffisent également à la représenter.
Dès lors, le contenu de la ligne plancher peut être représenté au moyen d'un compteur de 3 bits et de 0 à 7 valeurs de 3 bits, pour un maximum de 3 × 8 = 24 bits. Nous placerons ces 24 bits dans les bits de poids faible d'une valeur de type int dont :
- les 3 bits de poids faible contiennent la taille de la ligne plancher, comprise entre 0 et 7 (inclus),
- les 3 bits suivants contiennent l'index de la sorte de tuile se trouvant dans la première position de la ligne plancher, compris entre 0 et 5 (inclus), ou 0 si la ligne plancher est vide,
- les 3 bits suivants contiennent l'index de la sorte de tuile se trouvant dans la seconde position de la ligne plancher, compris entre 0 et 5 (inclus), ou 0 si la ligne plancher ne contient qu'une seule tuile,
- etc.
Les 8 bits de poids fort valent toujours 0.
Par exemple, le contenu de la ligne plancher visible dans la figure 2 ci-dessous est représenté par les 32 bits suivants :
00000000 000 000 000 000 001 001 101 011
Pour mémoire, les tuiles de couleur B () ont l'index 1, tandis que le marqueur de premier joueur (1) a l'index 5.
3. Mise en œuvre Java
En plus des classes PkPatterns et PkFloor permettant de manipuler des lignes de motif ou plancher empaquetées, une classe nommée Game et représentant une configuration de partie doit également être réalisée dans le cadre de cette étape, de même qu'une classe simplifiant la validation des arguments des méthodes.
3.1. Classe PkPatterns
La classe PkPatterns du sous-paquetage gamestate.packed, publique et finale, contient des méthodes statiques permettant de manipuler le contenu des lignes de motif d'un joueur, empaqueté de la manière décrite à la §2.1.1, à savoir :
- les 3 bits de poids faible contiennent le nombre de tuiles présentes sur la ligne de motif 1,
- les 3 bits suivants contiennent l'index de la couleur des tuiles présentes sur la ligne de motif 1, ou 0 si la ligne de motif est vide,
- et ainsi de suite pour les lignes de motif 2 à 5,
et les 2 bits de poids fort valent toujours 0.
PkPatterns possède un unique attribut public, statique et final :
int EMPTY- qui représente les lignes de motif vides.
Elle offre de plus les méthodes publiques et statiques suivantes pour manipuler des lignes de motif empaquetées :
int size(int pkPatterns, TileDestination.Pattern line)- qui retourne le nombre de tuiles présentes sur la ligne de motif
linedes lignes de motif empaquetéespkPatterns, TileKind.Colored color(int pkPatterns, TileDestination.Pattern line)- qui retourne la couleur des tuiles présentes sur la ligne de motif
linedes lignes de motif empaquetéespkPatterns— voir les conseils de programmation plus bas pour savoir que faire si cette ligne est vide, boolean isFull(int pkPatterns, TileDestination.Pattern line)- qui retourne vrai ssi la ligne de motif
linedes lignes de motif empaquetéespkPatternsest pleine, c.-à-d. contient le nombre maximum de tuiles qu'elle peut contenir, boolean canContain(int pkPatterns, TileDestination.Pattern line, TileKind.Colored color)- qui retourne vrai ssi la ligne de motif
linedes lignes de motif empaquetéespkPatternspeut contenir des tuiles de couleurcolor, c.-à-d. si elle est vide ou si elle contient déjà des tuiles de couleurcolor— indépendemment du fait qu'elle soit pleine ou non, int withAddedTiles(int pkPatterns, TileDestination.Pattern line, int tileCount, TileKind.Colored color)- qui retourne des lignes de motif empaquetées identiques à
pkPatternsmais avectileCounttuiles de couleurcolorajoutées à la ligneline— voir les conseils de programmation plus bas pour savoir que faire si l'ajout est invalide, int withEmptyLine(int pkPatterns, TileDestination.Pattern line)- qui retourne des lignes de motif empaquetées identiques à
pkPatternsmais avec la lignelinevide, int asPkTileSet(int pkPatterns)- qui retourne l'ensemble de tuiles empaqueté constitué de toutes les tuiles se trouvant sur les lignes de motif empaquetées
pkPatterns, String toString(int pkPatterns)- qui retourne la représentation textuelle des lignes de motif empaquetées
pkPatterns, décrite ci-après.
La représentation textuelle des lignes de motif est constituée de cinq éléments — un par ligne — séparés par des virgules suivies d'espaces, et entourés de crochets. L'élément représentant une ligne commence par la lettre correspondant à la couleur des tuiles qu'elle contient, répétée autant de fois qu'il y a de tuiles sur la ligne, et se termine par une suite de points (.) assez longue pour que l'élément ait une longueur égale à la capacité de la ligne. Ainsi, la représentation textuelle des lignes de motif de la figure 1 est :
[C, AA, AAA, EEE., .....]
3.1.1. Conseils de programmation
La spécification de la méthode color ci-dessus ne dit pas que faire si la ligne de motif donnée est vide. De même, la spécification de withAddedTiles ne dit pas que faire si la ligne de motif donnée ne peut pas accueillir les tuiles données. Nous vous recommandons une fois encore d'utiliser des assertions Java pour vérifier que ces méthodes sont appelées avec des arguments valides. Nous avons choisi de ne pas les valider explicitement — c.-à-d. avec un test levant une exception en cas d'erreur — pour des raisons de performance, mais cela ne vous interdit pas de les valider au moyen d'assertions, qui peuvent aisément être désactivées.
La méthode repeat de String peut être utile dans la définition de la méthode toString.
3.2. Classe PkFloor
La classe PkFloor du sous-paquetage gamestate.packed, publique et finale, contient des méthodes statiques permettant de manipuler le contenu d'une ligne plancher empaqueté de la manière décrite à la §2.2.1, à savoir :
- les 3 bits de poids faible contiennent le nombre de tuiles que contient la ligne plancher,
- les 3 bits suivants contiennent l'index de la première tuile se trouvant sur la ligne plancher, ou 0 s'il n'y en a aucune,
- et ainsi de suite pour les 6 autres tuiles qui peuvent se trouver sur la ligne plancher,
et les 8 bits de poids fort valent toujours 0.
PkFloor possède un attribut public, statique et final :
int EMPTY- qui représente la ligne plancher vide.
Elle offre de plus les méthodes publiques et statiques suivantes pour manipuler une ligne plancher empaquetée :
int size(int pkFloor)- qui retourne la taille de la ligne plancher empaquetée
pkFloor, c.-à-d. le nombre de tuiles qu'elle contient, TileKind tileAt(int pkFloor, int i)- qui retourne la sorte de tuile se trouvant à l'index
ide la ligne plancher empaquetéepkFloor, int withAddedTiles(int pkFloor, int pkTileSet)- qui retourne une ligne plancher empaquetée identique à
pkFloormais avec les tuiles de l'ensemble empaquetépkTileSetajoutées par ordre de sorte (A, B, C, D, E, marqueur de premier joueur) ; si la ligne n'a pas la capacité d'accueillir toutes ces tuiles, les excédentaires sont ignorées sauf le marqueur de premier joueur qui doit toujours être ajouté, en remplaçant au besoin la dernière tuile, boolean containsFirstPlayerMarker(int pkFloor)- qui retourne vrai ssi la ligne plancher empaquetée
pkFloorcontient le marqueur de premier joueur, int asPkTileSet(int pkFloor)- qui retourne l'ensemble de tuiles empaqueté constitué de toutes les tuiles se trouvant sur la ligne plancher empaquetée
pkFloor, String toString(int pkFloor)- qui retourne la représentation textuelle de la ligne plancher empaquetée
pkFloor, décrite ci-après.
La représentation textuelle d'une ligne plancher est constituée des noms des sortes des tuiles qu'elle contient, séparés par des virgules suivies d'espaces, et entourés de crochets. Ainsi, la représentation textuelle de la ligne plancher de la figure 2 est :
[FIRST_PLAYER_MARKER, B, B]
3.2.1. Conseils de programmation
Une fois encore, nous vous conseillons d'utiliser des assertions pour valider les arguments passés à tileAt.
3.3. Classe Preconditions
Fréquemment, les méthodes d'un programme exigent 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 classe Preconditions 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
}
Preconditions appartient au paquetage principal. Elle est publique et finale et n'offre rien d'autre que la méthode publique et statique suivante :
void checkArgument(boolean shouldBeTrue)- qui lève l'exception
IllegalArgumentExceptionsi son argument est faux, et ne fait rien sinon.
3.4. Classe Game
La classe Game du paquetage principal, publique et immuable (donc finale), représente une configuration de partie d'Ajul.
Game possède un enregistrement imbriqué nommé PlayerDescription qui permet de décrire un joueur et qui possède les trois attributs suivants (dans l'ordre) :
PlayerId id- l'identité du joueur,
String name- le nom du joueur,
PlayerKind kind- la sorte du joueur,
PlayerKindétant un type énuméré public, imbriqué dansPlayerDescriptionet contenant uniquement deux valeurs :HUMANetAI.
En dehors de ces trois attributs, PlayerDescription possède un constructeur compact qui vérifie, au moyen de requireNonNull, qu'aucune des valeurs passées au constructeur ne vaut null.
Game offre un unique constructeur public :
Game(List<PlayerDescription> playerDescriptions)- qui retourne une nouvelle configuration de partie pour les joueurs décrits par
playerDescriptions, ou lève uneIllegalArgumentExceptionsi le nombre de joueurs n'est pas compris entre 2 et 4, ou si la position de l'un d'eux dans la liste ne correspond pas à son identité — en d'autres termes, si le premier joueur de la liste n'a pas l'identitéP1, le secondP2, etc.
De plus, Game offre les méthodes publiques suivantes :
List<PlayerDescription> playerDescriptions()- qui retourne la liste (immuable) des descriptions des joueurs de la partie, dont le contenu doit être identique à celui de la liste passée au constructeur,
List<PlayerId> playerIds()- qui retourne la liste (immuable) des identités des joueurs de la partie, qui doit être un préfixe de
PlayerId.ALL, int playersCount()- qui retourne le nombre de joueurs dans la partie,
List<TileSource.Factory> factories()- qui retourne la liste (immuable) des identités des fabriques utilisées dans la partie, qui doit être un préfixe de
TileSource.Factory.ALL, int factoriesCount()- qui retourne le nombre de fabriques utilisées dans la partie,
List<TileSource> tileSources()- qui retourne la liste (immuable) des sources de tuiles utilisées dans la partie, qui doit être un préfixe de
TileSource.ALL, int tileSourcesCount()- qui retourne le nombre de sources de tuiles dans la partie,
int centralAreaMaxSize()- qui retourne le nombre maximum de tuiles pouvant se trouver dans la zone centrale durant la partie, marqueur de premier joueur inclus.
Pour écrire ces méthodes, souvenez-vous que le nombre de fabriques dans une partie à n joueurs vaut 2n + 1. D'autre part, le nombre maximum de tuiles pouvant se trouver dans la zone centrale durant une partie à m fabriques vaut 3m + 1. En effet, chaque fabrique peut contribuer au maximum 3 tuiles à la zone centrale, et elle peut aussi contenir le marqueur de premier joueur.
3.4.1. Conseils de programmation
Pour alléger la validation des arguments du constructeur, utilisez bien entendu la méthode checkArgument de Preconditions.
La méthode subList de List peut être utile pour obtenir les différentes listes retournées par les méthodes de Game.
N'oubliez pas que la classe Game doit être immuable, ce qui implique que la liste passée au constructeur doit être copiée au moyen de List.copyOf.
3.5. Tests
Comme pour l'étape précédente, nous ne vous fournissons plus de tests mais un fichier de vérification de signatures à importer dans votre projet.
4. Résumé
Pour cette étape, vous devez :
- écrire les classes
PkPatterns,PkFloor,PreconditionsetGameselon les indications plus haut, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies,
- rendre votre code au plus tard le 6 mars à 18h00, au moyen du programme
Submit.javafourni et des jetons disponibles sur votre page privée.
Ce rendu est un rendu testé, auquel 20 points sont attribués, au prorata des tests unitaires passés avec succès.
N'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus.
Si vous manquez la date limite de rendu, vous avez encore la possibilité de faire un rendu tardif au moyen des jetons prévus à cet effet, et ce durant les 2 heures qui suivent, mais il vous en coûtera une pénalité inconditionnelle de 2 points.
Notes de bas de page
Attention au fait que les tuiles sorties du jeu ne sont pas replacées dans le sac d'où les tuiles sont tirées aléatoirement en début de partie. Elles sont stockées ailleurs — dans le cas du jeu physique, on les place dans le couvercle de la boîte — et ne retournent dans le sac que lorsque ce dernier est totalement vide. Nous reviendrons sur ce point ultérieurement, mais il peut être bon de le garder à l'esprit.