Ensembles de cartes
Javass – étape 3

1 Introduction

Le but de cette troisième étape est d'écrire les classes permettant de manipuler des ensembles de cartes, aussi bien sous forme empaquetée que sous forme d'objets.

1.1 Ensembles de cartes

Dans le cadre de ce projet, un ensemble de cartes n'est rien d'autre qu'un ensemble — au sens mathématique — de cartes de Jass. Ces ensembles sont utiles dans plusieurs situations, par exemple pour représenter les cartes qu'un joueur a en main. Les ensembles de cartes définis ici auront pour particularité d'être triés, dans le sens où les cartes y seront stockées dans un ordre bien défini.

1.2 Ensembles de cartes empaquetés

Sachant qu'il existe en tout 36 cartes différentes au Jass, tout ensemble de cartes peut être représenté au moyen de 36 bits. Il suffit pour cela d'attribuer un bit à chaque carte, et de dire que ce bit vaut 1 si et seulement si la carte correspondante appartient à l'ensemble.

En Java, cela signifie qu'un ensemble de cartes quelconque peut être représenté au moyen d'une unique valeur de type long (64 bits).

Pour associer les bits aux différentes cartes, nous utiliserons une technique très simple consistant à faire correspondre à une carte donnée le bit dont l'index est égal à sa version empaquetée. Par exemple, comme nous l'avons vu à l'étape 1, l'as de cœur est représenté par l'entier valant 0…011000, c-à-d 24. Dès lors, le bit 24 d'une valeur de type long représentant un ensemble de cartes empaqueté sera utilisé pour déterminer si l'as de cœur fait partie de l'ensemble (bit 24 = 1) ou non (bit 24 = 0).

Étant donné la manière dont les cartes sont empaquetées, il est facile de voir qu'à chaque couleur correspond un paquet de 16 bits dans un ensemble empaqueté de la sorte :

Bits 63 à 48 47 à 32 31 à 16 15 à 0
Contenu

et que chacun de ces paquets de 16 bits est organisé ainsi :

Bits 15 à 9 8 7 1 0
Contenu inutilisés (0) as roi 7 6

Une caractéristique importante de cette représentation est que les cartes y sont organisées par couleur, et triées dans l'ordre. Par exemple, le bit 0 correspond au 6 de pique, le bit 1 au 7 de pique, et ainsi de suite. Dès lors, en parcourant les bits par poids croissant, on parcourt les cartes de l'ensemble dans un ordre bien défini et naturel pour un être humain : d'abord les cartes de pique, du 6 à l'as, ensuite les cartes de cœur, du 6 à l'as, et ainsi de suite.

C'est la raison pour laquelle nous avons choisi d'empaqueter les cartes de la manière décrite à l'étape 1, en plaçant la couleur dans les bits de poids fort et le rang dans ceux de poids faible.

2 Mise en œuvre Java

2.1 Classe PackedCardSet

La classe PackedCardSet du paquetage ch.epfl.javass.jass, publique, finale et non instanciable, possède des méthodes permettant de manipuler des ensembles de cartes empaquetés dans des valeurs de type long (64 bits).

Elle possède deux attributs publics, statiques et finaux contenant des ensembles de cartes souvent utiles, qui sont :

  • long EMPTY, l'ensemble de cartes vide,
  • long ALL_CARDS, l'ensemble des 36 cartes du jeu de Jass.

De plus, la classe PackedCardSet offre les méthodes publiques suivantes :

  • boolean isValid(long pkCardSet), qui retourne vrai ssi la valeur donnée représente un ensemble de cartes empaqueté valide, c-à-d si aucun des 28 bits inutilisés ne vaut 1,
  • long trumpAbove(int pkCard), qui retourne l'ensemble des cartes strictement plus fortes que la carte empaquetée donnée, sachant qu'il s'agit d'une carte d'atout ; par exemple, appliquée à l'as de cœur, elle doit retourner un ensemble contenant deux éléments, le neuf et le valet de cœur, car ces deux cartes sont les seules à être strictement plus fortes que l'as de cœur lorsque cœur est atout ; attention, cette méthode doit s'exécuter rapidement (voir les conseils de programmation plus bas),
  • long singleton(int pkCard), qui retourne l'ensemble de cartes empaqueté contenant uniquement la carte empaquetée donnée,
  • boolean isEmpty(long pkCardSet), qui retourne vrai ssi l'ensemble de cartes empaqueté donné est vide,
  • int size(long pkCardSet), qui retourne la taille de l'ensemble de cartes empaqueté donné, c-à-d le nombre de cartes qu'il contient,
  • int get(long pkCardSet, int index), qui retourne la version empaquetée de la carte d'index donné de l'ensemble de cartes empaqueté donné, la carte d'index 0 étant celle correspondant au bit de poids le plus faible valant 1,
  • long add(long pkCardSet, int pkCard), qui retourne l'ensemble de cartes empaqueté donné auquel la carte empaquetée donnée a été ajoutée,
  • long remove(long pkCardSet, int pkCard), qui retourne l'ensemble de cartes empaqueté donné duquel la carte empaquetée donnée a été supprimée,
  • boolean contains(long pkCardSet, int pkCard), qui retourne vrai ssi l'ensemble de cartes empaqueté donné contient la carte empaquetée donnée,
  • long complement(long pkCardSet), qui retourne le complément de l'ensemble de cartes empaqueté donné,
  • long union(long pkCardSet1, long pkCardSet2), qui retourne l'union des deux ensembles de cartes empaquetés donnés,
  • long intersection(long pkCardSet1, long pkCardSet2), qui retourne l'intersection des deux ensembles de cartes empaquetés donnés,
  • long difference(long pkCardSet1, long pkCardSet2), qui retourne la différence entre le premier ensemble de cartes empaqueté donné et le second, c-à-d l'ensemble des cartes qui se trouvent dans le premier ensemble mais pas dans le second,
  • long subsetOfColor(long pkCardSet, Card.Color color), qui retourne le sous-ensemble de l'ensemble de cartes empaqueté donné constitué uniquement des cartes de la couleur donnée ; attention, tout comme trumpAbove, cette méthode doit s'exécuter rapidement (voir les conseils de programmation ci-dessous),
  • String toString(long pkCardSet), qui retourne la représentation textuelle de l'ensemble de cartes empaqueté donné, spécifiée plus bas.

2.1.1 Conseils de programmation

  1. Validation des arguments

    Tout comme pour la classe PackedScore, nous vous conseillons fortement de valider les arguments des différentes méthodes au moyen d'assertions Java.

    Il faut noter à ce sujet que la méthode add (resp. remove) ne produit aucune erreur si la carte qu'on lui passe appartient déjà (resp. n'appartient pas) à l'ensemble. Dans ce cas, l'ensemble retourné est simplement identique à celui donné en argument.

  2. Méthodes trumpAbove et subsetOfColor

    Les méthodes trumpAbove et subsetOfColor doivent s'exécuter rapidement car elles sont utilisées par le joueur simulé pour décider quelle carte jouer.

    Dès lors, il vous est demander de les programmer sans utiliser de boucle. Pour ce faire, vous pouvez précalculer et stocker dans des attributs privés finaux de la classe différentes informations, qui pourraient p.ex. être :

    • pour trumpAbove, un tableau (éventuellement bidimensionnel) contenant la valeur que trumpAbove doit retourner pour chacune des 36 cartes du jeu,
    • pour subsetOfColor, un tableau contenant, pour chaque couleur, un ensemble de toutes les cartes de cette couleur.

    L'écriture de chacune des deux méthodes devient alors très simple, et elles s'exécutent rapidement.

  3. Méthodes size et get

    La classe Long de la bibliothèque Java comporte plusieurs méthodes qui facilitent énormément l'écriture des méthodes size et get. Avant de se lancer dans leur programmation, il est donc fortement conseillé de lire et comprendre la documentation de :

    Les deux dernières en particulier peuvent être combinées judicieusement pour écrire la méthode get en trois lignes seulement — deux d'entre elles constituant une boucle. À vous de trouver comment !

  4. Méthodes union, intersection, etc.

    Il va sans dire que les opérateurs bit à bit de Java (&, |, ^, etc.), ainsi que les opérateurs de décalage, permettent d'écrire la plupart des méthodes de la classe PackedCardSet de manière concise et efficace. Pensez donc à les utiliser !

  5. Méthode toString

    La méthode toString doit retourner une chaîne constituée de la représentation textuelle de chacune des cartes de l'ensemble, ordonnées par index croissant, séparées par une virgule et entourées d'accolades.

    Par exemple, l'extrait de programme suivant :

    long s = PackedCardSet.EMPTY;
    int c1 = PackedCard.pack(Color.HEART, Rank.SIX);
    int c2 = PackedCard.pack(Color.SPADE, Rank.ACE);
    int c3 = PackedCard.pack(Color.SPADE, Rank.SIX);
    s = PackedCardSet.add(s, c1);
    s = PackedCardSet.add(s, c2);
    s = PackedCardSet.add(s, c3);
    System.out.println(PackedCardSet.toString(s));
    

    doit imprimer ceci à l'écran (le symbole cœur pouvant aussi être plein) :

    {♠6,♠A,♡6}
    

    La manière la plus simple de construire cette chaîne consiste à utiliser la classe StringJoiner de la bibliothèque Java. L'extrait de code ci-dessous, dont vous pouvez vous inspirer, montre comment l'utiliser pour construire la chaîne {a,an,ane} :

    StringJoiner j = new StringJoiner(",", "{", "}");
    for (String s: new String[]{ "a", "an", "ane" })
      j.add(s);
    System.out.println(j.toString()); // affiche {a,an,ane}
    

    Pour plus de détails, reportez-vous à la documentation de la classe StringJoiner.

2.2 Classe CardSet

La classe CardSet, du paquetage ch.epfl.javass.jass, finale et immuable, représente un ensemble de cartes.

A l'image de PackedCardSet, elle possède deux attributs publics, statiques et finaux, contenant des ensembles constants importants, à savoir :

  • CardSet EMPTY, l'ensemble de cartes vide,
  • CardSet ALL_CARDS, l'ensemble des 36 cartes du jeu de Jass.

Comme les classes des étapes précédente, CardSet ne possède pas de constructeur public, mais deux méthodes statiques jouant le rôle de constructeurs :

  • static CardSet of(List<Card> cards), qui retourne l'ensemble des cartes contenues dans la liste donnée,
  • static CardSet ofPacked(long packed), qui retourne l'ensemble de cartes dont packed est la version empaquetée, ou lève l'exception IllegalArgumentException si cet argument ne représente pas un ensemble empaqueté valide.

De plus, la classe CardSet offre les méthodes publiques suivantes. Seule la première est décrite, les autres étant équivalentes à celles de PackedCardSet portant le même nom :

  • long packed(), qui retourne la version empaquetée de l'ensemble de cartes.
  • boolean isEmpty(),
  • int size(),
  • Card get(int index),
  • CardSet add(Card card),
  • CardSet remove(Card card),
  • boolean contains(Card card),
  • CardSet complement(),
  • CardSet union(CardSet that),
  • CardSet intersection(CardSet that),
  • CardSet difference(CardSet that),
  • CardSet subsetOfColor(Card.Color color).

Finalement, la classe CardSet redéfinit les méthodes equals, hashCode et toString de Object :

  • equals retourne vrai ssi l'objet qu'on lui passe est également un ensemble de cartes de type CardSet et qu'il contient exactement les mêmes cartes que le récepteur,
  • hashCode retourne simplement le résultat de la méthode hashCode de la classe Long appliquée à l'ensemble empaqueté,
  • toString se comporte comme la méthode toString de PackedCardSet.

2.2.1 Conseils de programmation

  1. Immuabilité de CardSet

    Lors de la programmation de CardSet, souvenez-vous qu'il s'agit d'une classe immuable ! Cela implique que ses instances ne peuvent absolument pas être modifiées après création.

  2. Liste de cartes (List<Card>)

    Comme cela a été dit dans l'énoncé de l'étape 1, vous pouvez considérer pour l'instant que le type List<Card> est identique au type ArrayList<Card> que vous connaissez déjà.

    Cela implique, entre autres, qu'il est possible de parcourir les éléments d'une telle liste au moyen de la boucle for each, comme illustré dans l'extrait de programme suivant :

    List<Card> cards = …;
    for (Card c: cards)
      System.out.println(c);
    

2.3 Tests

Comme pour l'étape précédente, nous ne vous fournissons plus de tests mais un fichier de vérification de signatures contenu dans une archive Zip à importer dans votre projet.

3 Résumé

Pour cette étape, vous devez :

  • écrire les classes PackedCardSet et CardSet selon les instructions données plus haut,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies,
  • rendre votre code au plus tard le 8 mars 2019 à 16h30, via le système de rendu.

Ce rendu est un rendu testé, auquel 18 points sont attribués, au prorata des tests unitaires passés avec succès. Notez que la documentation de votre code ne sera pas évaluée avant le rendu intermédiaire. Dès lors, si vous êtes en retard, ne vous en préoccupez pas pour l'instant.

N'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus. Souvenez-vous qu'aucun retard, aussi insignifiant soit-il, ne sera toléré !