État de la partie

ChaCuN – étape 6

1. Introduction

Le but de cette étape, la dernière de la première partie du projet, est d'écrire la classe représentant l'état d'une partie de ChaCuN.

Notez que cette étape devra être rendue deux fois :

  1. pour le rendu testé habituel (délai : le 5/4 à 18h00),
  2. pour le rendu intermédiaire (délai : le 12/4 à 18h00).

Le deuxième de ces rendus sera corrigé par lecture du code de vos étapes 1 à 6, et il vous faudra donc soigner sa qualité et sa documentation. Il est fortement conseillé de lire notre guide à ce sujet.

2. Concepts

2.1. Déroulement d'un tour

Dans la plupart des cas, un tour de ChaCuN se déroule de manière extrêmement simple : le joueur courant tire la prochaine tuile du tas des tuiles normales et la place sur le plateau avant de poser éventuellement un occupant — pion ou hutte — sur l'une de ses zones. Ensuite, les points correspondants aux éventuelles forêts et rivières fermées par la pose de cette tuile sont attribués à leurs occupants majoritaires, et tous les occupants de ces aires sont retournés à leurs propriétaires. Le tour du joueur suivant peut alors commencer, sauf s'il ne reste plus de tuile à placer, auquel cas la partie se termine.

En pratique, un tour peut être plus compliqué que cela, en raison de l'existence des tuiles menhir et des pouvoirs spéciaux que certaines d'entre elles possèdent.

En effet, comme nous l'avons vu, lorsque le joueur courant ferme au moins une forêt contenant un menhir en posant sa tuile, alors il a le droit de jouer un second tour en plaçant cette fois une tuile tirée du tas des tuiles menhir — en supposant qu'il en reste. Parmi ces tuiles menhir, certaines ont un pouvoir spécial, et trois de ces pouvoirs impliquent d'effectuer immédiatement une action :

  • le chaman donne au joueur qui le pose le droit de reprendre en main, s'il le désire, l'un des pions qu'il a posé précédemment sur le plateau,
  • la pirogue permet au joueur qui la pose d'obtenir des points qui dépendent du nombre de lacs se trouvant dans le réseau hydrographique qui la contient,
  • la fosse à pieux permet au joueur qui la pose d'obtenir des points qui dépendent des animaux présents dans le pré adjacent à la fosse, selon le principe décrit à la §2.2 ci-dessous.

Il faut noter que même si le joueur qui pose une tuile menhir ferme une forêt contenant un menhir, il n'a pas le droit à un troisième tour. Dès lors, un joueur joue toujours un maximum de deux tours avant de passer la main au joueur suivant.

2.2. Fosse à pieux

Lorsqu'un joueur pose la tuile contenant la fosse à pieux, il obtient les points correspondants aux animaux présents dans le pré adjacent.

Pour déterminer ces points, on commence par compter le nombre d'animaux des différents types — mammouths, aurochs, cerfs et smilodons — présents dans le pré adjacent.

Ensuite, si le nombre de smilodons est supérieur ou égal au nombre de cerfs, alors on ignore tous les cerfs — car ils sont dévorés par les smilodons. Sinon, on ignore un nombre de cerfs égal au nombre de smilodons, pour la même raison.

Finalement, on calcule les points avec les animaux restants, un mammouth rapportant 3 points, un auroch 2 et un cerf 1.

Une fois les points décomptés, tous les animaux présents dans le pré adjacent à la fosse sont annulés (cancelled), c.-à-d. ignorés pour le reste de la partie. Dans le jeu physique — et dans l'interface graphique — ces animaux sont recouverts d'une croix indiquant leur annulation.

2.3. Calcul final des points

Une fois que la dernière tuile normale a été posée et que son placeur a terminé son tour — ou ses deux tours s'il a pu jouer une tuile menhir — le décompte final des points est effectué. Il consiste à attribuer aux joueurs possédant les occupants majoritaires des prés (chasseurs) et des réseaux hydrographiques (huttes de pêcheurs) les points correspondants aux aires qu'ils occupent, comme décrit ci-dessous.

2.3.1. Prés

Les joueurs possédant les chasseurs majoritaires d'un pré obtiennent un nombre de points qui dépend des animaux qui s'y trouvent, selon les mêmes règles que pour la fosse à pieux — les cerfs mangés par les smilodons sont ignorés, et les points sont attribués en fonction des animaux restants. Toutefois, si le pré contient le feu — un pouvoir spécial — alors aucun cerf n'est dévoré par les smilodons, qui fuient le pré1.

De plus, lorsqu'un pré contient la grande fosse à pieux, les chasseurs majoritaires obtiennent une seconde fois les points associés aux animaux présents dans le pré adjacent à la fosse. Afin de maximiser ces points additionnels, lorsque la grande fosse à pieux se trouve dans un pré, les cerfs sont annulés en commençant par ceux qui ne se trouvent pas dans le pré adjacent à la fosse.

2.3.2. Réseaux hydrographiques

Les joueurs possédant les huttes majoritaires d'un réseau hydrographique obtiennent un point par poisson qui y nage.

De plus, si le réseau hydrographique contient le radeau, alors les propriétaires des huttes majoritaires obtiennent en plus un point par lac qu'il contient.

2.4. Actions

Une partie de ChaCuN progresse au fur et à mesure que les joueurs effectuent les actions que l'on attend d'eux. Par exemple, lorsque la partie vient de commencer, le premier joueur doit tout d'abord placer la première tuile du tas des tuiles normales ; cela fait, il doit éventuellement l'occuper ; ensuite, le prochain joueur doit tirer et placer la prochaine tuile ; et ainsi de suite.

En d'autres termes, à chaque instant d'une partie, une action doit être effectuée pour que la partie puisse progresser. Nous en distinguerons cinq, auxquelles nous attribuerons des noms anglais concis qui apparaîtront aussi dans le code. Il s'agit de :

START_GAME
la tuile de départ doit être placée au centre du plateau et la tuile au sommet du tas des tuiles normales doit être retournée en vue d'être placée par le premier joueur,
PLACE_TILE
le joueur courant doit placer la tuile courante, qui est soit une tuile normale soit une tuile menhir,
RETAKE_PAWN
le joueur courant — qui vient de poser la tuile contenant le chaman — doit décider s'il désire ou non reprendre l'un des pions qu'il a posé précédemment sur le plateau, et si oui, lequel,
OCCUPY_TILE
le joueur courant — qui vient de placer une tuile — doit décider s'il désire occuper l'une de ses zones au moyen d'un des occupants qu'il a en main,
END_GAME
le décompte des points et l'annonce du ou des vainqueurs doit être faite, car le dernier joueur a terminé son (ou ses) tour(s) et le tas des tuiles normales est vide.

Une fois qu'une action a été effectuée, il est possible de déterminer la prochaine action qui doit l'être, et ce jusqu'à ce que la partie soit terminée. Par exemple, une fois que l'action PLACE_TILE a été effectuée, c.-à-d. que le joueur courant a placé la prochaine tuile, on peut déterminer que la prochaine action à effectuer est :

  • RETAKE_PAWN si la tuile que le joueur vient de poser est celle contenant le chaman et qu'il possède au moins un pion sur le plateau, sinon
  • OCCUPY_TILE si au moins une zone de la tuile que le joueur vient de poser peut être occupée, et qu'il a l'occupant nécessaire en main, sinon
  • PLACE_TILE (tuile menhir) si le joueur a fermé, avec une tuile normale, au moins une forêt contenant un menhir, et qu'il reste encore une tuile menhir qu'il est possible de placer sur le tas correspondant, sinon
  • PLACE_TILE (tuile normale, à placer par le joueur suivant) s'il reste encore une tuile normale qu'il est possible de placer sur le tas correspondant, sinon
  • END_GAME.

Ces transitions entre les différentes actions à effectuer peuvent être représentées de manière graphique, comme ci-dessous.

game-state-machine;16.png
Figure 1 : Transitions entre les actions à effectuer

Sur cette figure, chaque action à effectuer est représentée par un rectangle, et les flèches reliant ces actions représentent les transitions qui peuvent se produire entre elles. Pour ne pas alourdir le dessin, les transitions qui évitent OCCUPY_TILE car la dernière tuile posée ne peut pas être occupée ne sont pas représentées. Par exemple, il n'y a pas de flèche allant de PLACE_TILE (tuile normale) vers elle-même, alors que cette transition est possible, comme nous l'avons dit plus haut.

Pour faciliter la compréhension, les actions PLACE_TILE et OCCUPY_TILE apparaissent deux fois sur ce dessin : les occurrences de gauche correspondent au placement d'une tuile normale, celles de droite au placement d'une tuile menhir. En pratique, les actions consistant à placer ou occuper une tuile sont considérées comme les mêmes, indépendamment du type de la tuile en question.

Les actions START_GAME et END_GAME sont entourées deux fois car elles sont particulières : START_GAME est l'action de départ, celle qui doit être effectuée tout au début pour démarrer la partie, donc aucune flèche ne mène à elle ; END_GAME est quant à elle l'action de fin, après laquelle la partie est terminée et aucune autre action ne peut être effectuée, donc aucune flèche ne part d'elle.

2.5. Références

Les règles de la version physique du jeu sont décrites :

Il faut toutefois noter que les règles de notre version diffèrent parfois de celles du jeu original, principalement lorsque ces dernières sont ambiguës.

3. Mise en œuvre Java

3.1. Enregistrement GameState

L'enregistrement GameState du paquetage principal, public et immuable, représente l'état complet d'une partie de ChaCuN. C'est-à-dire qu'il contient la totalité des informations liées à une partie en cours. Ses attributs sont :

  • List<PlayerColor> players, la liste de tous les joueurs de la partie, dans l'ordre dans lequel ils doivent jouer — donc avec le joueur courant en tête de liste,
  • TileDecks tileDecks, les trois tas des tuiles restantes,
  • Tile tileToPlace, l'éventuelle tuile à placer, qui à été prise du sommet du tas des tuiles normales ou du tas des tuiles menhir, et qui peut être null si aucune tuile n'est à placer actuellement,
  • Board board, le plateau de jeu,
  • Action nextAction, la prochaine action à effectuer, le type Action étant décrit ci-dessous,
  • MessageBoard messageBoard, le tableau d'affichage contenant les messages générés jusqu'à présent dans la partie.

Le type énuméré Action, public et imbriqué dans GameState, représente la prochaine action à effectuer dans la partie et contient les éléments suivants, dans l'ordre : START_GAME, PLACE_TILE, RETAKE_PAWN, OCCUPY_TILE et END_GAME.

Le constructeur compact de GameState se charge de garantir l'immuabilité de la classe et de valider les arguments en vérifiant que :

  • le nombre de joueurs est au moins égal à 2,
  • soit la tuile à placer est null, soit la prochaine action est PLACE_TILE,
  • ni les tas de cartes, ni le plateau de jeu, ni la prochaine action, ni le tableau d'affichage ne sont nuls.

Pour faciliter la création de l'état initial d'une partie, GameState offre la méthode publique et statique suivante :

  • GameState initial(List<PlayerColor> players, TileDecks tileDecks, TextMaker textMaker), qui retourne l'état de jeu initial pour les joueurs, tas et « créateur de texte » donnés, dont la prochaine action est START_GAME (donc la tuile à placer est null), et dont le plateau et le tableau d'affichage sont vides.

En plus de la méthode statique ci-dessus, GameState offre les méthodes d'instance publiques suivantes, qui permettent d'obtenir différentes informations au sujet de l'état de la partie :

  • PlayerColor currentPlayer(), qui retourne le joueur courant, ou null s'il n'y en a pas, c.-à-d. si la prochaine action est START_GAME ou END_GAME,
  • int freeOccupantsCount(PlayerColor player, Occupant.Kind kind), qui retourne le nombre d'occupants libres — c.-à-d. qui ne sont pas actuellement placés sur le plateau de jeu — du type donné et appartenant au joueur donné,
  • Set<Occupant> lastTilePotentialOccupants(), qui retourne l'ensemble des occupants potentiels de la dernière tuile posée que le joueur courant pourrait effectivement placer — d'une part car il a au moins un occupant du bon type en main, et d'autre part car l'aire à laquelle appartient la zone que cet occupant occuperait n'est pas déjà occupée — ou lève IllegalArgumentException si le plateau est vide.

Finalement, GameState offre un certain nombre de méthodes dont le but est de gérer les transitions entre les différentes actions à effectuer. Ces méthodes sont destinées à être appelées uniquement lorsque la prochaine action à effectuer est une action spécifique, et leurs arguments sont les paramètres de cette action.

Par exemple, withPlacedTile, décrite plus bas, est destinée à être appelée lorsque la prochaine action à effectuer est PLACE_TILE, et elle prend donc en argument la tuile qui a été placée par le joueur courant. Elle effectue les éventuelles opérations nécessaires — p. ex. en attribuant les points correspondant à la pose de la tuile contenant la pirogue si c'est elle qui vient d'être posée — avant de déterminer la prochaine action à effectuer et de retourner l'état correspondant.

Ces méthodes — qui lèvent toutes une IllegalArgumentException (abrégée IAE dans leur description) si la prochaine action à effectuer n'est pas celle qu'elles attendent — sont :

  • GameState withStartingTilePlaced(), qui gère la transition de START_GAME à PLACE_TILE en plaçant la tuile de départ au centre du plateau et en tirant la première tuile du tas des tuiles normales, qui devient la tuile à jouer ; lève IAE si la prochaine action n'est pas START_GAME,
  • GameState withPlacedTile(PlacedTile tile), qui gère toutes les transitions à partir de PLACE_TILE en ajoutant la tuile donnée au plateau, attribuant les éventuels points obtenus suite à la pose de la pirogue ou de la fosse à pieux (voir la 3.1.1.1), et déterminant l'action suivante — qui peut être RETAKE_PAWN si la tuile posée contient le chaman ; lève IAE si la prochaine action n'est pas PLACE_TILE, ou si la tuile passée est déjà occupée,
  • GameState withOccupantRemoved(Occupant occupant), qui gère toutes les transitions à partir de RETAKE_PAWN, en supprimant l'occupant donné, sauf s'il vaut null, ce qui indique que le joueur ne désire pas reprendre de pion ; lève IAE si la prochaine action n'est pas RETAKE_PAWN, ou si l'occupant donné n'est ni null, ni un pion,
  • GameState withNewOccupant(Occupant occupant), qui gère toutes les transitions à partir de OCCUPY_TILE en ajoutant l'occupant donné à la dernière tuile posée, sauf s'il vaut null, ce qui indique que le joueur ne désire pas placer d'occupant ; lève IAE si la prochaine action n'est pas OCCUPY_TILE.

Notez que withStartingTilePlaced peut faire l'hypothèse que la première tuile du tas des tuiles normales peut toujours être posée, ce qui est effectivement le cas en pratique car la tuile de départ comporte au moins un bord de chacune des trois sortes — pré, forêt et rivière.

3.1.1. Conseils de programmation

  1. Gestion de la fosse à pieux

    Comme expliqué à la §2.2, afin de calculer correctement les points obtenus par la pose de la fosse à pieux, il est nécessaire d'annuler au préalable les éventuels cerfs mangés par des smilodons. Malheureusement, le calcul des points est fait par la méthode withScoredHuntingTrap de MessageBoard, qui ne prend pas d'ensemble d'animaux annulés en argument.

    Il s'agit d'une erreur de conception, mais pour faciliter l'organisation, nous n'allons pas la corriger avant le rendu intermédiaire. Dès lors, dans votre méthode withPlacedTile, vous pouvez déjà calculer l'ensemble des cerfs mangés par des smilodons, mais il vous faudra attendre une étape ultérieure pour le passer à la version corrigée de withScoredHuntingTrap.

  2. Possibilité d'occupation

    Comme la figure 1 l'illustre, l'action à effectuer après la pose d'une tuile (PLACE_TILE) ou la reprise d'un pion (RETAKE_PAWN) est normalement d'occuper la dernière tuile posée (OCCUPY_TILE).

    En réalité, il se peut que l'occupation de cette tuile soit impossible, soit car les aires auxquelles appartiennent ses zones sont déjà occupées, soit car le placeur n'a plus les occupants nécessaires en main.

    Dans ce cas-là, l'action OCCUPY_TILE doit être sautée, et l'action qui suit OCCUPY_TILE devient l'action suivante. Comme dit plus haut, les flèches correspondant à cette situation ne sont pas présentées sur la figure 1 afin de ne pas l'alourdir, mais il va de soi que votre code doit impérativement gérer ce cas-là.

    Une manière de le faire consiste à ajouter à GameState une méthode privée, nommée p. ex. withTurnFinishedIfOccupationImpossible, dont le but est de finir le tour si l'occupation de la dernière tuile posée est impossible. Ce que signifie exactement « finir le tour » est décrit à la §3.1.1.4 plus bas.

    Cette méthode peut être appelée à la fin de toutes les méthodes qui produisent un état dont la prochaine action est OCCUPY_TILE, c.-à-d. withPlacedTile et withOccupantRemoved, afin de sauter cette action si elle n'est pas possible.

  3. Possibilité de retrait de pion

    De la même manière que l'action OCCUPY_TILE ne doit être effectuée que si l'occupation de la dernière tuile posée est effectivement possible, l'action RETAKE_PAWN ne doit l'être que si le joueur courant a au moins un pion sur le plateau.

  4. Fin de tour

    Une fois que la tuile à placer l'a été, et qu'elle a éventuellement été occupée, le tour actuel du joueur courant se termine.

    La fin de tour n'est pas totalement triviale à gérer, d'une part car il faut compter les points obtenus par les occupants majoritaires de toutes les éventuelles forêts et rivières fermées par la dernière tuile posée, et d'autre part car il faut déterminer la prochaine action à effectuer et qui, comme nous l'avons dit plus haut, peut être PLACE_TILE (pour le même joueur, s'il peut placer une tuile menhir, pour le joueur suivant sinon) ou END_GAME.

    Nous vous conseillons donc là aussi d'ajouter à votre classe GameState une méthode privée, nommée p. ex. withTurnFinished, se chargeant d'effectuer les opérations nécessaires, à savoir :

    • déterminer les forêts et rivières fermées par la pose de la dernière tuile, et attribuer les points correspondants à leurs occupants majoritaires,
    • déterminer si le joueur courant devrait pouvoir jouer un second tour, car il a fermé au moins une forêt contenant un menhir au moyen d'une tuile normale,
    • éliminer du sommet du tas contenant la prochaine tuile à jouer la totalité de celles qu'il n'est pas possible de placer sur le plateau, s'il y en a,
    • passer la main au prochain joueur si le joueur courant n'a pas le droit ou la possibilité de jouer une tuile menhir,
    • terminer la partie si le joueur courant a terminé son ou ses tour(s) et qu'il ne reste plus de tuile normale jouable.

    Réfléchissez bien avant d'écrire le code qui détermine la prochaine tuile à jouer, en tenant compte de celles qu'il pourrait être impossible de placer, car il n'est pas trivial. Souvenez-vous en particulier que la classe TileDecks offre la méthode withTopTileDrawnUntil, qui peut faciliter la suppression du sommet d'un tas de toutes les tuiles impossibles à placer.

  5. Fin de partie

    Lorsque le joueur courant a terminé son (ou ses) tour(s) et qu'il est temps de passer au joueur suivant, si aucune tuile du tas des tuiles normales ne peut être jouée, alors la partie est terminée.

    Comme la fin d'un tour, la fin de la partie n'est pas totalement triviale à gérer puisqu'elle implique d'établir le décompte final des points. Nous vous recommandons donc une fois encore de placer le code chargé de faire cela dans une méthode privée, nommée p. ex. withFinalPointsCounted.

    Cette méthode doit commencer par ajouter aux animaux annulés la totalité des cerfs dévorés par des smilodons, en tenant compte de l'éventuelle présence du feu dans un pré. De plus, lorsque le pré contient la grande fosse à pieux, les cerfs qui ne se trouvent pas à sa portée doivent être annulés en priorité, afin de maximiser les points rapportés par la fosse.

    Une fois les cerfs annulés, le décompte des points rapportés par les prés à leurs chasseurs majoritaires peut être effectué.

    De même, le décompte des points rapportés par les réseaux hydrographiques à leurs huttes majoritaires peut être effectué, en pensant à tenir compte de l'éventuelle présence du radeau.

    Une fois les points comptés, le(s) vainqueur(s) peuvent être déterminés pour ajouter un dernier message au tableau d'affichage, qui annonce la fin de la partie.

  6. Attribution des points

    Comme nous l'avons vu à l'étape précédente, l'attribution des points aux joueurs se fait de manière indirecte, en ajoutant au tableau d'affichage les messages informant les joueurs des différents gains de points. Il ne faut donc pas oublier, dans vos différentes méthodes, d'ajouter les messages nécessaires au décompte des points, de même que ceux mentionnant la fermeture d'une forêt contenant un menhir et la fin de la partie.

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 GameState en fonction des 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 5 avril 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é !

Notes de bas de page

1

Il faut noter que le feu ne fait fuire les smilodons du pré que lors du décompte final des points, et il n'a aucune influence sur le décompte des points de la fosse à pieux.