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.
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.
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.
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.
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, ounull
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èveIllegalArgumentException
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èveIllegalArgumentException
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 — ounull
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èveIllegalArgumentException
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èveIllegalArgumentException
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
- Méthode
cancelledAnimals
Souvenez-vous que
Board
est une classe immuable, ce qui implique que la méthodecancelledAnimal
doit garantir que l'ensemble qu'elle retourne n'est pas modifiable. - Méthode
equals
La méthode
equals
doit être redéfinie afin de garantir que les instances deBoard
sont comparées « par structure ». Cela signifie que deux instances deBoard
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
deArrays
doivent être utilisées. - 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éthodeequals
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
deArrays
, 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
deObjects
, et retourner l'entier qu'elle retourne comme résultat de votre méthodehashCode
.
L'utilité de ces différentes opérations deviendra claire plus tard.
- passer le tableau contenant les tuiles à la méthode statique
- 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éthodecopyOf
deArrays
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é !