Mur et points

Ajul – étape 4

1. Introduction

Le but principal de cette quatrième étape est de continuer la rédaction des classes permettant de représenter l'état du jeu de manière empaquetée, en écrivant celle représentant le contenu du mur. Son but secondaire est d'écrire une classe permettant de calculer les points obtenus par les joueurs au cours d'une partie.

2. Concepts

2.1. Mur

Comme cela a été expliqué dans l'introduction au projet, à la fin d'une manche, les lignes de motif de chacun des joueurs sont parcourues de haut en bas, et chaque fois que l'une d'entre elles est pleine, sa tuile la plus à droite est déplacée dans la case correspondante du mur, les autres étant sorties du plateau.

Le mur est donc composé d'autant de lignes qu'il y a de lignes de motif (cinq), et d'autant de colonnes qu'il y a de couleurs de tuiles (cinq également). Chaque case du mur ne peut accueillir qu'une seule tuile, d'une couleur fixe.

Ainsi, la première case de la première ligne ne peut accueillir qu'une tuile de couleur A ; la seconde de la même ligne, une tuile de couleur B ; la troisième, une tuile de couleur C ; la quatrième, une de couleur D ; et la cinquième, une de couleur E. La seconde ligne est similaire, si ce n'est que les couleurs ont subi une rotation d'une case vers la droite. Et ainsi de suite pour les lignes 3 à 5.

La figure 1 ci-dessous montre la couleur des tuiles que peuvent accueillir les 25 cases du mur. Pour faciliter sa compréhension, les noms des couleurs que peuvent accueillir les cases de la première ligne leur ont été superposés.

wall;32.png
Figure 1 : Couleurs des tuiles accueillies par les cases du mur

2.1.1. Identification des cases

Les 25 cases du mur peuvent être identifiées de différentes manières. En fonction des besoins, nous identifierons l'une d'entre elles par :

  1. la ligne de motif à laquelle elle correspond et la couleur des tuiles qu'elle peut accueillir, ou
  2. la ligne de motif à laquelle elle correspond et la colonne (comprise entre 0 et 4 inclus) à laquelle elle appartient, ou
  3. son index, les cases étant numérotées de 0 à 24 (inclus), de gauche à droite et de haut en bas.

Ces trois manières d'identifier les cases du mur sont résumées dans la figure 2 ci-dessous, les deux basées sur les lignes de motif à gauche, et celle basée sur un index à droite.

wall-indexing;32.png
Figure 2 : Identification des cases du mur

Par exemple, la case tout en bas à droite du mur peut être identifiée de ces trois manières ainsi :

  1. ligne de motif PATTERN_5, couleur A, ou
  2. ligne de motif PATTERN_5, colonne 4, ou
  3. index 24.

2.1.2. Contenu

Lors d'une partie d'Ajul, chacune des 25 cases du mur d'un joueur peut soit être vide, soit contenir une tuile colorée dont la couleur est déterminée par la case.

Pour représenter graphiquement le contenu d'un mur, nous utiliserons dorénavant la même convention que dans l'interface graphique finale, c.-à-d. que les cases du mur seront colorées de la couleur des tuiles qu'elles acceptent, mais très pâlement, tandis que les tuiles seront colorées vivement.

Par exemple, la figure 3 ci-dessous montre un mur dont seules les cases 0, 2, 3, 4, 7, 8 et 12 sont occupées par une tuile, les autres étant vides. Les index des cases leur ont été superposés pour faciliter leur identification.

wall-contents;64.png
Figure 3 : Mur contenant 7 tuiles

Étant donné que les index des cases sont compris entre 0 et 24 (inclus), le contenu d'un mur peut être représenté par un ensemble d'entiers compris entre 0 et 24.

Un tel ensemble peut quant à lui facilement être empaqueté dans un entier de 32 bits en associant l'entier i au bit d'index i et en adoptant la convention que ce bit vaut 1 ssi l'entier correspondant appartient à l'ensemble. Par exemple, les 32 bits suivants représentent le contenu du mur visible à la figure 3 :

00000000000000000001000110011101

2.2. Points

Les joueurs d'une partie d'Ajul peuvent recevoir des points à la fin de chacune des manches, ainsi qu'à la fin de la partie.

2.2.1. Fin de manche

À la fin d'une manche, les lignes de motif des joueurs sont parcourues de haut en bas, et chaque fois que l'une d'entre elles est pleine, sa tuile la plus à droite est déplacée dans la case du mur correspondante. Cette tuile nouvellement ajoutée au mur rapporte alors au joueur un nombre de points déterminé par la taille du groupe horizontal et du groupe vertical auquel la tuile appartient.

Un groupe horizontal de tuiles est un ensemble maximal de 1 à 5 tuiles qui se trouvent sur la même ligne du mur et occupent des cases adjacentes. Un groupe vertical de tuiles est similaire, mais ses tuiles occupent la même colonne du mur. Cette notion de groupe peut être illustrée au moyen de la figure 4 ci-dessous, qui montre un mur contenant :

  • sept groupes horizontaux : {0}, {2,3}, {6}, {11}, {13}, {18}, {20,21,22,23,24}, et
  • neuf groupes verticaux : {0}, {20}, {6,11}, {21}, {2}, {22}, {3}, {13,18,23}, {24}.
wall-groups;64.png
Figure 4 : Différents groupes de tuiles

Une fois qu'une tuile a été ajoutée au mur, elle appartient à exactement un groupe horizontal et un groupe vertical. Si l'on note \(h\) la taille de son groupe horizontal et \(v\) la taille de son groupe vertical, le nombre de points \(p\) qu'elle rapporte est donné par :

\[ p = \begin{cases} h & \text{si $v = 1$}\\ v & \text{si $h = 1$}\\ h+v & \text{sinon} \end{cases} \]

Par exemple, si la tuile 0 de la figure 4 était ajoutée alors que toutes les autres tuiles de la figure étaient déjà présentes, elle rapporterait 1 point ; si la tuile 2 était ajoutée dans les mêmes conditions, elle rapporterait 2 points ; finalement, si la tuile 23 était ajoutée dans les mêmes conditions, elle rapporterait 8 points.

Une fois les points dûs aux nouvelles tuiles ajoutés au score d'un joueur, l'éventuelle pénalité due aux tuiles se trouvant dans sa ligne plancher en est soustraite. La pénalité associée aux tuiles de la ligne plancher est de :

  • un point pour chacune des deux premières tuiles,
  • deux points pour chacune des trois tuiles du milieu,
  • trois points pour chacune des deux dernières tuiles.

Une ligne plancher entièrement pleine vaut donc une pénalité totale de 14 points à son propriétaire. Comme nous le verrons plus tard, la pénalité est toutefois limitée par le score d'un joueur, qui ne peut jamais devenir négatif.

2.2.2. Fin de partie

À la fin de la partie, une fois que les points de la dernière manche ont été comptabilisés, des points bonus sont attribués pour chaque ligne, colonne ou couleur complète du mur, ainsi :

  • chaque ligne complète du mur d'un joueur lui rapporte 2 points,
  • chaque colonne complète du mur d'un joueur lui rapporte 7 points,
  • chaque couleur complète du mur d'un joueur lui rapporte 10 points.

Une ligne, colonne ou couleur est complète si elle contient cinq tuiles.

3. Mise en œuvre Java

La première classe à écrire dans le contexte de cette étape permet de manipuler des ensembles d'entiers empaquetés dans un entier de type int. Cette classe doit bien entendu être utilisée par celle manipulant le contenu empaqueté d'un mur, et elle sera aussi utile dans une étape ultérieure, raison pour laquelle nous avons choisi de la définir.

3.1. Classe PkIntSet32

La classe PkIntSet32, du sous-paquetage gamestate.packed, publique et finale, contient des méthodes permettant de manipuler un ensemble d'entiers compris entre 0 et 31 (inclus), empaqueté dans un entier de type int. Dans cette représentation, le bit d'index i correspond à l'entier i, et s'il vaut 1, cela signifie que l'entier i appartient à l'ensemble.

PkIntSet32 offre un attribut public, statique et final :

int EMPTY
qui représente l'ensemble vide,

ainsi que des méthodes (publiques et statiques) permettant de manipuler des ensembles :

boolean contains(int pkIntSet32, int i)
qui retourne vrai ssi l'ensemble d'entiers empaqueté pkIntSet32 contient l'entier i,
boolean containsAll(int pkIntSet32a, int pkIntSet32b)
qui retourne vrai ssi l'ensemble d'entiers empaqueté pkIntSet32a contient la totalité des éléments de l'ensemble pkIntSet32b,
int add(int pkIntSet32, int i)
qui retourne un ensemble d'entiers empaqueté identique à pkIntSet32, mais contenant l'entier i,
int remove(int pkIntSet32, int i)
qui retourne un ensemble d'entiers empaqueté identique à pkIntSet32, mais ne contenant pas l'entier i.

3.1.1. Conseils de programmation

Comme avec toutes les classes manipulant des données empaquetées, il est conseillé de valider les arguments qui peuvent l'être au moyen d'assertions.

3.2. Classe PkWall

La classe PkWall du sous-paquetage gamestate.packed, publique et finale, contient des méthodes statiques permettant de manipuler le contenu du mur d'un joueur, représenté par un ensemble d'entiers empaqueté comme décrit à la section précédente.

PkWall offre trois attributs publics, statiques et finaux :

int EMPTY
qui représente un mur vide,
int WALL_WIDTH
qui contient la largeur du mur, en cases (5),
int WALL_HEIGHT
qui contient la hauteur du mur, en cases (5),

ainsi que les méthodes (publiques et statiques) suivantes :

int indexOf(TileDestination.Pattern line, TileKind.Colored color)
qui retourne l'index, compris entre 0 et 24 (inclus), de la case du mur correspondant à la ligne de motif line et pouvant accueillir une tuile de couleur color,
int column(TileDestination.Pattern line, TileKind.Colored color)
qui retourne le numéro de la colonne, compris entre 0 et 4 (inclus), de la case du mur correspondant à la ligne de motif line et pouvant accueillir une tuile de couleur color,
TileKind.Colored colorAt(TileDestination.Pattern line, int column)
qui retourne la couleur de la tuile que la case du mur correspondant à la ligne de motif line et se trouvant à la colonne column (comprise entre 0 et 4 inclus) peut accueillir,
int withTileAt(int pkWall, TileDestination.Pattern line, TileKind.Colored color)
qui retourne le contenu du mur empaqueté identique à pkWall mais avec la tuile de couleur color ajoutée à la ligne line,
boolean hasTileAt(int pkWall, TileDestination.Pattern line, TileKind.Colored color)
qui retourne vrai ssi la case correspondant à la ligne de motif line et pouvant accueillir une tuile de couleur color du mur empaqueté pkWall contient une tuile,
int hGroupSize(int pkWall, TileDestination.Pattern line, TileKind.Colored color)
qui retourne la taille du groupe horizontal auquel la tuile de couleur color de la ligne line du mur empaqueté pkWall appartient,
int vGroupSize(int pkWall, TileDestination.Pattern line, TileKind.Colored color)
qui retourne la taille du groupe vertical auquel la tuile de couleur color de la ligne line du mur empaqueté pkWall appartient,
boolean hasFullRow(int pkWall)
qui retourne vrai ssi au moins une des lignes du mur empaqueté pkWall est pleine,
boolean isRowFull(int pkWall, TileDestination.Pattern line)
qui retourne vrai ssi la ligne correspondant à la ligne de motif line du mur empaqueté pkWall est pleine,
boolean isColumnFull(int pkWall, int column)
qui retourne vrai ssi la colonne column (comprise entre 0 et 4 inclus) du mur empaqueté pkWall est pleine,
boolean isColorFull(int pkWall, TileKind.Colored color)
qui retourne vrai ssi la couleur color du mur empaqueté pkWall est pleine,
int asPkTileSet(int pkWall)
qui retourne l'ensemble de tuiles empaqueté constitué de toutes les tuiles se trouvant sur le mur empaqueté pkWall,
String toString(int pkWall)
qui retourne la représentation textuelle du contenu du mur empaqueté pkWall, décrite ci-après.

La représentation textuelle du contenu d'un mur est constituée de cinq éléments, un par ligne (de haut en bas), séparés par des virgules suivies d'espaces et entourés de crochets. Chaque élément est constitué de cinq lettres représentant chacune une case du mur. La lettre représentant une case est celle correspondant à la couleur qu'elle peut accueillir, et est minuscule si la case ne contient pas de tuile, majuscule sinon.

Ainsi, la représentation textuelle du mur vide est :

[abcde, eabcd, deabc, cdeab, bcdea]

tandis que celle du mur de la figure 4 est :

[AbCDe, eAbcd, dEaBc, cdeAb, BCDEA]

3.2.1. Conseils de programmation

Plusieurs des méthodes de PkWall doivent extraire toutes les tuiles d'une ligne, colonne ou couleur présente dans le mur. Cela peut se faire aisément au moyen d'un « et » bit à bit entre l'entier représentant le mur empaqueté et le masque correspondant. Par exemple, le masque permettant d'extraire toutes les tuiles présentes sur la première ligne peut se définir ainsi :

int ROW0_MASK = 0b00000_00000_00000_00000_11111;

De simples décalages permettent d'obtenir les masques correspondant aux autres lignes à partir de celui-là. Il en va de même avec les masques correspondant aux différentes colonnes, qui peuvent être obtenus facilement de celui correspondant à la colonne 0. Les 5 masques correspondant aux différentes couleurs sont par contre plus difficiles à calculer, et il est donc conseillé de les définir individuellement.

Notez que les masques correspondant aux différentes couleurs permettent de définir facilement la méthode asPkTileSet puisque, une fois qu'on a extrait les tuiles d'une couleur donnée présente dans le mur, la méthode bitCount de la classe Integer permet de connaître leur nombre.

Pour écrire la méthode toString, la classe StringBuilder, qui représente un bâtisseur de chaîne, peut s'avérer utile.

Finalement, nous vous conseillons une fois encore de valider les arguments qui peuvent l'être au moyen d'assertions.

3.3. Classe Points

La classe Points du paquetage principal, publique et finale, contient des constantes et méthodes permettant de calculer les points obtenus par les joueurs lors d'une partie d'Ajul.

Points offre les attributs suivants, publics, statiques et finaux, qui donnent le nombre de points bonus obtenus dans différentes situations :

int FULL_ROW_BONUS_POINTS
qui donne le nombre de points obtenus pour chaque ligne complète (2),
int FULL_COLUMN_BONUS_POINTS
qui donne le nombre de points obtenus pour chaque colonne complète (7),
int FULL_COLOR_BONUS_POINTS
qui donne le nombre de points obtenus pour chaque couleur complète (10).

De plus, Points offre les méthodes publiques et statiques suivantes :

int newWallTilePoints(int hGroupSize, int vGroupSize)
qui retourne le nombre de points dus à l'ajout d'une tuile au mur qui appartient à un groupe horizontal de taille hGroupSize et à un groupe vertical de taille vGroupSize,
int floorPenalty(int tileIndex)
qui retourne la pénalité (positive !) associée à la tuile d'index tileIndex (compris entre 0 et 6 inclus) de la ligne plancher — et qui vaut 1, 2 ou 3 en fonction des cas,
int totalFloorPenalty(int tilesCount)
qui retourne la pénalité totale (positive ou nulle !) associée à une ligne plancher contenant tilesCount tuiles (compris entre 0 et 7 inclus) — et qui vaut entre 0 et 14.

3.3.1. Conseils de programmation

Points faisant partie des classes utilisées par l'IA, ses méthodes doivent être rapides, raison pour laquelle nous ne demandons pas à ce qu'elles valident leurs arguments. Nous vous conseillons toutefois de le faire, mais comme d'habitude au moyen d'assertions exclusivement.

Les pénalités associées aux différentes tuiles de la ligne plancher peuvent bien entendu être stockées dans un tableau, mais il est encore plus simple de les stocker de manière empaquetée. Par exemple, en attribuant 4 bits par tuile, une constante contenant les 7 pénalités s'écrit très facilement en hexadécimal, car à chaque pénalité correspond un chiffre hexadécimal :

private static final int FLOOR_PENALTY = 0x3322211;

Une fois cela fait, l'extraction d'une pénalité se fait par simple décalage et masquage.

Il est également possible de définir une constante contenant les pénalités cumulées, dans laquelle chaque groupe de 4 bits correspond à la pénalité totale d'une ligne plancher contenant un nombre de tuiles donné, compris entre 0 et 7 (inclus). Cette constante a la forme suivante :

private static final int TOTAL_FLOOR_PENALTY = 0x????4210;

Les points d'interrogation représentant des chiffres que vous devez trouver par vous-même.

Étonnamment, cette constante peut être déterminée par décalage à gauche de 4 bits du résultat d'une multiplication de la constante précédente (FLOOR_PENALTY) avec une valeur hexadécimale composée de 7 chiffres identiques. En d'autres termes, elle peut aussi se définir ainsi :

private static final int TOTAL_FLOOR_PENALTY =
  (FLOOR_PENALTY * 0x???????) << 4;

??????? représente une constante hexadécimale composée de 7 chiffres identiques. À vous de trouver de quel chiffre il s'agit, et de comprendre pourquoi cela fonctionne !

3.4. 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 PkIntSet32, PkWall et Points selon 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 13 mars à 18h00, au moyen du programme Submit.java fourni 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.