Plateau de jeu

ChaCuN – étape 5

1. Introduction

Le but de cette étape est d'écrire la classe représentant le plateau de jeu.

2. Concepts

Comme cela a été dit à l'étape 1, ChaCuN se joue sur un plateau de jeu carré de 25×25 cases, la case centrale étant occupée par la tuile de départ. Ces cases sont indexées par une paire d'index (x, y) tels que la tuile de départ occupe la position (0, 0), et les deux index augmentent dans le sens de lecture — de gauche à droite, et de haut en bas.

2.1. Représentation du plateau de jeu

Le plateau de jeu peut être représenté de manière assez naturelle au moyen d'un tableau de tuiles. Ce tableau pourrait être bidimensionnel, mais un tableau unidimensionnel convient également très bien, et présente même certains avantages par rapport à un bidimensionnel — copie simplifiée, indexation au moyen d'un unique entier, etc. — donc c'est ce que nous utiliserons.

Ce tableau aura une taille de 625 (25×25) éléments, et sera organisé dans l'ordre de lecture en partant de la cellule en haut à gauche du plateau, et en parcourant les lignes avant les colonnes — ce qu'on appelle l'ordre row-major en anglais. En d'autres termes, le premier élément du tableau contiendra la tuile à la position (-12, -12), le second la tuile à la position (-11, -12), et ainsi de suite jusqu'au dernier élément du tableau qui contiendra la tuile à la position (12, 12). La tuile de départ, dont la position est (0, 0), sera donc toujours à l'index 312.

Cette représentation, très simple, a toutefois deux inconvénients.

Le premier est que, sachant qu'il y a un total de 95 tuiles dans le jeu, ce tableau de 625 éléments sera toujours en grande partie vide. Dès lors, certaines opérations qu'on pourrait vouloir effectuer sur lui — p. ex. parcourir la totalité des tuiles posées — sont assez coûteuses. En pratique, ce problème n'en est toutefois pas vraiment un, car un tableau de 625 éléments reste petit pour les ordinateurs modernes, et les performances ne sont de toute manière pas un soucis majeur dans ce projet.

Le second inconvénient de cette représentation est qu'il n'est pas possible de connaître l'ordre dans lequel les tuiles ont été posées sur le plateau. Cet inconvénient est plus sérieux pour nous, car nous aurons parfois besoin de savoir quelle tuile a été posée en dernier.

Une manière simple d'éviter ces deux inconvénients consiste à ajouter au tableau contenant les tuiles un second tableau contenant les index, dans le premier tableau, des tuiles posées, dans l'ordre dans lequel elles l'ont été. Par exemple, imaginons le plateau suivant, composé — de gauche à droite — des tuiles 17, 56 et 27.

board_01-three-tiles.png
Figure 1 : Le plateau composé des tuiles 17, 56 et 27

La tuile 56 étant la tuile de départ, c'est bien entendu elle qui a été déposée en premier. Admettons que la tuile 17 ait été déposée ensuite, puis la 27. Dans ce cas, les deux tableaux susmentionnés contiendraient les éléments suivants :

  • celui contenant les tuiles, d'une taille de 625 éléments, serait totalement vide sauf aux index suivants :
    • 311, qui contiendrait la tuile 17,
    • 312, qui contiendrait la tuile 56,
    • 313, qui contiendrait la tuile 27,
  • celui contenant les index des tuiles posées, d'une taille de 3 éléments, contiendrait, dans l'ordre, 312, 311 et 313.

Parcourir la totalité des tuiles posées est alors très rapide, puisqu'il suffit de parcourir les trois éléments du second tableau et les utiliser pour indexer le premier. Déterminer quelle tuile a été déposée en dernier est aussi très simple, puisqu'il suffit d'extraire le dernier élément du second tableau (313 ici) et de regarder quelle tuile se trouve à cet index dans le premier tableau (la tuile 27).

2.2. Positions d'insertion

Les cases du plateau qui ne contiennent aucune tuile mais dont au moins une des quatre cases voisines en contient une sont celles sur lesquelles la prochaine tuile pourrait potentiellement être déposée. Nous nommerons les positions de ces cases les positions d'insertion (insertion positions).

Par exemple, au tout début de la partie, lorsque la première tuile doit être déposée, les positions d'insertion sont celles des quatre cases voisines de la tuile de départ, c.-à-d. (-1,0), (0,-1), (1,0) et (0,1).

Dans l'interface graphique, les cases correspondant aux positions d'insertion seront coloriées avec la couleur du joueur courant, comme la copie d'écran présentée dans l'introduction au projet l'illustre.

2.3. Possibilité de jouer une tuile

Lorsque le joueur courant doit poser une tuile, il ne peut le faire que sur l'une des positions d'insertion, et à condition que les bords de la tuile qu'il pose — après l'avoir éventuellement tournée d'un certain nombre de quarts de tour — soient de la même sorte que ceux des tuiles voisines.

On peut dès lors déterminer relativement facilement si une tuile peut être placée ou non, puisqu'il suffit de faire une recherche exhaustive parmi toutes les positions d'insertion et toutes les rotations, pour voir si une combinaison convient.

Le plateau ci-dessous illustre une situation dans laquelle il peut être impossible de placer une tuile.

board_05-unplaceable-tile.png
Figure 2 : Un plateau sans bord rivière libre

Il est facile de voir que la tuile ci-dessous ne peut pas y être placée, puisqu'aucune combinaison d'une des 6 positions d'insertions et des 4 rotations ne lui convient.

board_05-all-river-tile.png
Figure 3 : Une tuile dont tous les bords sont des rivières

Lors d'une partie de ChaCuN, lorsque la prochaine tuile ne peut pas être jouée, elle est simplement éliminée du jeu.

2.4. Pré adjacent

Les deux fosses à pieux du jeu ont un comportement particulier, puisqu'elles ont un effet qui ne s'applique qu'aux zones du pré qu'elles occupent et qui se trouvent soit sur la tuile contenant la fosse, soit sur l'une de ses 8 voisines.

Nous nommerons ce sous-pré le pré adjacent (adjacent meadow) à la fosse. Une particularité de ce pré adjacent est que nous considérerons que ses occupants sont les mêmes que ceux du pré complet, et pas seulement ceux qui se trouvent sur les tuiles voisines de la fosse.

L'image ci-dessous montre un pré contenant la fosse à pieux et deux occupants, un rouge et un vert. Le pré complet est entouré de pointillés rouges, tandis que le pré adjacent à la fosse l'est d'un trait plein. Il est important de noter que les occupants du pré adjacent sont les mêmes que ceux du pré complet, à savoir un chasseur rouge et un chasseur vert, malgré le fait que le chasseur rouge occupe une zone qui n'appartient pas au pré adjacent — mais appartient au pré complet.

trap-adjacent-meadow;128.png
Figure 4 : Le pré adjacent à la petite fosse à pieux

Les chasseurs jaune et bleu ne font quant à eux pas partie du pré contenant la fosse à pieux, donc ils ne font pas non plus partie du pré adjacent — malgré le fait que le chasseur bleu se trouve sur une tuile qui est à la portée de la fosse.

3. Mise en œuvre Java

3.1. Classe Board

La classe Board du paquetage principale, publique et immuable, représente le plateau de jeu.

Contrairement à la plupart des classes écrites jusqu'à présent, il ne s'agit pas d'un enregistrement, car ses attributs sont des tableaux Java primitifs, qui sont toujours modifiables. Ces tableaux ne doivent donc pas être exposés à l'extérieur, faute de quoi la classe ne peut être immuable. Nous sommes donc forcés d'en faire une classe « normale », car les enregistrements ne peuvent pas avoir d'attributs inaccessibles depuis l'extérieur.

Board possède les attributs (privés) suivants :

  • un tableau de tuiles placées de type PlacedTile[], contenant 625 éléments pour la plupart égaux à null, comme décrit plus haut,
  • un tableau d'entiers de type int[], contenant les index, dans le premier tableau, des tuiles posées sur le plateau, dans l'ordre dans lequel elles ont été posées,
  • une instance de ZonePartitions, dont le contenu correspond à celui du plateau — c.-à-d. que les partitions sont celles qui correspondent à celles des zones des tuiles posées,
  • l'ensemble des animaux annulés, de type Set<Animal>.

Ces différents attributs sont tous finaux et initialisés par un constructeur privé (!) qui, pour des questions de performances, ne fait aucune copie défensive de ses arguments ! Sachant que la classe doit être immuable, cela implique que ce sont aux utilisateurs du constructeur de faire attention à ce que les objets qu'ils lui passent en argument ne changent plus dans le futur. Comme ce constructeur est privé, tous ses utilisateurs se trouvent forcément dans la classe Board elle-même, et il n'est donc pas déraisonnable de placer une telle exigence sur eux.

En plus de ces attributs privés, Board possède les deux attributs publics, statiques et finaux suivants :

  • int REACH, la « portée » du plateau, qui est le nombre de cases qui séparent la case centrale de l'un des bords du plateau, soit 12,
  • Board EMPTY, le plateau vide, qui ne contient absolument aucune tuile, même pas celle de départ.

Finalement, Board offre les méthodes publiques suivantes :

  • PlacedTile tileAt(Pos pos), qui retourne la tuile à la position donnée, ou null s'il n'y en a aucune ou si la position se trouve hors du plateau,
  • PlacedTile tileWithId(int tileId), qui retourne la tuile dont l'identité est celle donnée, ou lève IllegalArgumentException si cette tuile ne se trouve pas sur le plateau,
  • Set<Animal> cancelledAnimals(), qui retourne l'ensemble des animaux annulés,
  • Set<Occupant> occupants(), qui retourne la totalité des occupants se trouvant sur les tuiles du plateau,
  • Area<Zone.Forest> forestArea(Zone.Forest forest), qui retourne l'aire forêt contenant la zone donnée, ou lève IllegalArgumentException si la zone en question n'appartient pas au plateau,
  • Area<Zone.Meadow> meadowArea(Zone.Meadow meadow), identique à forestArea mais pour une aire pré,
  • Area<Zone.River> riverArea(Zone.River riverZone), identique à forestArea mais pour une aire rivière,
  • Area<Zone.Water> riverSystemArea(Zone.Water water), identique à forestArea mais pour un réseau hydrographique,
  • Set<Area<Zone.Meadow>> meadowAreas(), qui retourne l'ensemble de toutes les aires pré du plateau,
  • Set<Area<Zone.Water>> riverSystemAreas(), identique à meadowAreas mais pour les réseaux hydrographiques,
  • Area<Zone.Meadow> adjacentMeadow(Pos pos, Zone.Meadow meadowZone), qui retourne le pré adjacent à la zone donnée, sous la forme d'une aire qui ne contient que les zones de ce pré mais tous les occupants du pré complet, et qui, pour simplifier, ne possède aucune connexion ouverte,
  • int occupantCount(PlayerColor player, Occupant.Kind occupantKind), qui retourne le nombre d'occupants de la sorte donnée appartenant au joueur donné et se trouvant sur le plateau,
  • Set<Pos> insertionPositions(), qui retourne l'ensemble des positions d'insertions du plateau,
  • PlacedTile lastPlacedTile(), qui retourne la dernière tuile posée — qui peut être la tuile de départ si la première tuile normale n'a pas encore été placée — ou null si le plateau est vide,
  • Set<Area<Zone.Forest>> forestsClosedByLastTile(), qui retourne l'ensemble de toutes les aires forêts qui ont été fermées suite à la pose de la dernière tuile, ou un ensemble vide si le plateau est vide,
  • Set<Area<Zone.River>> riversClosedByLastTile(), identique à forestsClosedByLastTile mais pour les aires rivières,
  • boolean canAddTile(PlacedTile tile), qui retourne vrai ssi la tuile placée donnée pourrait être ajoutée au plateau, c.-à-d. si sa position est une position d'insertion et que chaque bord de la tuile qui touche un bord d'une tuile déjà posée est de la même sorte que lui,
  • boolean couldPlaceTile(Tile tile), qui retourne vrai ssi la tuile donnée pourrait être posée sur l'une des positions d'insertion du plateau, éventuellement après rotation,
  • Board withNewTile(PlacedTile tile), qui retourne un plateau identique au récepteur, mais avec la tuile donnée en plus, ou lève IllegalArgumentException si le plateau n'est pas vide et la tuile donnée ne peut pas être ajoutée au plateau,
  • Board withOccupant(Occupant occupant), qui retourne un plateau identique au récepteur, mais avec l'occupant donné en plus, ou lève IllegalArgumentException si la tuile sur laquelle se trouverait l'occupant est déjà occupée,
  • Board withoutOccupant(Occupant occupant), qui retourne un plateau identique au récepteur, mais avec l'occupant donné en moins,
  • Board withoutGatherersOrFishersIn(Set<Area<Forest>> forests, Set<Area<River>> rivers), qui retourne un plateau identique au récepteur mais sans aucun occupant dans les forêts et les rivières données,
  • Board withMoreCancelledAnimals(Set<Animal> newlyCancelledAnimals), qui retourne un plateau identique au récepteur mais avec l'ensemble des animaux donnés ajouté à l'ensemble des animaux annulés.

En plus de ces méthodes, Board redéfinit les méthodes equals et hashCode de Object pour faire en sorte que les instances de Board soient « comparées par structure » (voir les conseils de programmation).

3.1.1. Conseils de programmation

  1. Méthode cancelledAnimals

    Souvenez-vous que Board est une classe immuable, ce qui implique que la méthode cancelledAnimal doit garantir que l'ensemble qu'elle retourne n'est pas modifiable.

  2. Méthode equals

    La méthode equals doit être redéfinie afin de garantir que les instances de Board sont comparées « par structure ». Cela signifie que deux instances de Board doivent être considérées comme égales si et seulement si le contenu de tous leurs attributs sont égaux.

    Souvenez-vous que pour tester si deux tableaux ont le même contenu, il n'est pas valide d'utiliser leur méthode equals, car celle-ci fait une comparaison par référence ! Pour effectuer une comparaison par contenu, les différentes versions de la méthode (statique) equals de Arrays doivent être utilisées.

  3. Méthode hashCode

    Comme nous le verrons plus tard au cours lorsque nous examinerons le hachage, la méthode hashCode doit toujours être redéfinie lorsque la méthode equals l'est.

    Pour les enregistrements, Java se charge automatiquement de redéfinir les deux méthodes, comme nous l'avons vu. Pour la classe Board, c'est à nous de le faire, étant donné qu'il s'agit d'une classe « normale ».

    Pour cela, votre méthode hashCode doit effectuer les opérations suivantes :

    • passer le tableau contenant les tuiles à la méthode statique hashCode de Arrays, pour obtenir un premier entier,
    • faire de même avec le tableau contenant les index des tuiles,
    • passer ces deux entiers et les deux autres attributs de la classe — le groupe de partitions et l'ensemble des animaux annulés — à la méthode hash de Objects, et retourner l'entier qu'elle retourne comme résultat de votre méthode hashCode.

    L'utilité de ces différentes opérations deviendra claire plus tard.

  4. Méthodes de dérivation

    Toutes les méthodes « de dérivation » — celles dont le nom commence par with et qui permettent d'obtenir un nouveau plateau dérivé du récepteur — doivent prendre garde à copier les tableaux avant de les modifier puis de les passer au constructeur, faute de quoi l'immuabilité de la classe ne serait pas garantie.

    Lorsque les tableaux ne changent pas de taille, cette copie peut se faire au moyen de la méthode clone. Lorsqu'ils changent de taille, la méthode copyOf de Arrays est préférable, car elle permet de copier un tableau dans un nouveau tableau de taille différente de l'original.

    Les méthodes de dérivation doivent également s'assurer que les partitions de zones correspondent toujours au contenu du plateau. Pour ce faire, elles doivent toujours calculer non seulement les nouveaux tableaux contenant les tuiles et leurs indices, mais aussi les nouvelles partitions correspondantes.

3.2. Tests

Comme d'habitude, 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 la classe Board selon les indications 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 22 mars 2024 à 18h00, au moyen du programme Submit.java fourni et des jetons disponibles sur votre page privée.

Ce rendu est un rendu testé, auquel 18 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. Souvenez-vous qu'aucun retard, aussi insignifiant soit-il, ne sera toléré !