État d'une partie
Ajul – étape 6
1. Introduction
Le but principal de cette étape — la dernière de la première partie du projet — est d'écrire la classe représentant un état de partie non immuable, qu'il est possible de faire évoluer en fonction des coups joués par les joueurs.
Notez que cette étape devra être rendue deux fois :
- pour le rendu testé habituel (délai : le 27/3 à 18h00),
- pour le rendu intermédiaire (délai : le 3/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
Nous avons vu à l'étape précédente en quoi consistait la version immuable de l'état d'une partie. Cette étape étant consacrée à la version non immuable de ce même état, il importe de voir comment il peut changer à différents moments d'une partie.
2.1. Remplissage des fabriques
Au début d'une manche, les fabriques sont remplies de 4 tuiles chacune, avec des tuiles tirées aléatoirement du sac. Lors de cette opération, si le sac se retrouve vide, il doit être rempli à nouveau avec les tuiles précédemment sorties du jeu.
Il faut bien faire attention au fait que le sac ne peut pas être rempli avant d'avoir été totalement vidé. Par exemple, s'il faut 20 tuiles pour remplir les fabriques et que :
- le sac contient uniquement 4 tuiles, toutes de couleur A, et
- les tuiles sorties du jeu consistent en 20 tuiles de couleur B,
alors les 4 tuiles de couleur A provenant du sac doivent obligatoirement être utilisées pour remplir les fabriques. Il n'est pas possible de remplir les fabriques avec, par exemple, 2 tuiles de couleurs A et 18 tuiles de couleur B.
Sachant qu'il n'y a que 100 tuiles colorées dans le jeu, il est possible que, même après avoir rempli le sac avec les tuiles sorties du jeu, il n'y en ait pas assez pour remplir toutes les fabriques. Dans ce cas, les dernières fabriques doivent être laissées totalement ou partiellement vides.
2.1.1. Algorithme de remplissage
Dans les règles officielles du jeu physique (Azul), il est spécifié que les premières fabriques doivent être remplies avec les tuiles provenant du sac et que, s'il se retrouve vide, il doit être rempli avec les tuiles sorties du jeu et le remplissage des fabriques restantes doit se poursuivre.
Cette technique de remplissage a l'avantage d'être simple à mettre en œuvre en pratique, mais elle n'est pas tout à fait juste, dans le sens où certaines combinaisons de tuiles peuvent ne pas être possibles. Par exemple, s'il faut remplir 5 fabriques, que le sac ne contient que 4 tuiles de couleur A, et que les tuiles sorties du jeu sont constituées de 20 tuiles de couleur B, alors seul un remplissage est possible : la première fabrique est remplie de 4 tuiles de couleurs A, et celles restantes de 4 tuiles de couleur B chacune.
Dans notre version du jeu, nous utiliserons un algorithme de remplissage différent, qui ne présente pas ce problème et consiste à :
- déterminer le nombre n de tuiles nécessaires pour remplir les fabriques,
- si le sac contient (strictement) plus de n tuiles :
- en extraire n au hasard,
- sinon :
- sortir toutes les m tuiles que contient le sac (m ≤ n),
- le remplir à nouveau avec les tuiles sorties du jeu,
- en extraire n - m (ou moins, s'il n'y en a pas assez) tuiles au hasard,
- mélanger toutes les tuiles extraites,
- remplir les fabriques avec les tuiles mélangées.
Dans l'exemple donné plus haut, cet algorithme commencerait par sortir les 4 tuiles de couleur A du sac, puis remplirait le sac avec les 20 tuiles de couleur B sorties du jeu, avant d'en extraire au hasard 16 tuiles, qui seraient ici forcément de couleur B. Les 20 tuiles ainsi extraites — 4 de couleur A, 16 de couleur B — seraient mélangées puis utilisées pour remplir les fabriques. Les 4 tuiles de couleur A pourraient ainsi se retrouver sur n'importe laquelle des cinq fabriques, et pas forcément sur la première.
2.2. Prise en compte d'un coup
Une fois qu'un joueur a décidé du coup qu'il désire jouer, l'état de la partie doit être mis à jour en fonction de ce coup. Pour mémoire, un coup est composé de trois éléments :
- la source d'où proviennent les tuiles jouées,
- la couleur des tuiles jouées,
- la destination sur laquelle ces tuiles sont placées.
Lorsque la source est une fabrique, les éventuelles tuiles de cette même fabrique qui ne sont pas de la couleur de celles jouées sont déplacées sur la zone centrale.
Lorsque la source est la zone centrale, et que celle-ci contient le marqueur de premier joueur, celui-ci est déplacé sur la ligne plancher du joueur courant.
Lorsque la destination est une ligne de motif, et que celle-ci n'a pas la capacité nécessaire pour accueillir la totalité des tuiles jouées, les tuiles excédentaires sont placées sur la ligne plancher du joueur courant, ou sorties du jeu si cette dernière est pleine.
Finalement, lorsque toutes les tuiles qui doivent l'être ont été déplacées, le prochain joueur devient le joueur courant.
2.3. Fin de manche
À la fin d'une manche, les lignes de motif de tous les joueurs sont parcourues de la première à la dernière, et lorsque l'une d'elles est pleine, sa tuile la plus à droite est déplacée sur la case du mur correspondante, les éventuelles autres étant sorties du jeu. Chaque fois qu'une tuile est ainsi placée sur le mur, elle rapporte entre 1 et 10 points à son propriétaire, comme expliqué à l'étape précédente.
Lorsque toutes les lignes de motif d'un joueur ont été ainsi parcourues, l'éventuelle pénalité due aux tuiles se trouvant sur sa ligne plancher est déduite de ses points. Le score d'un joueur ne peut toutefois jamais devenir négatif, donc la pénalité qu'un joueur peut devoir encaisser est limitée par son score actuel.
Une fois la pénalité d'un joueur comptabilisée, sa ligne plancher est vidée, et toutes les tuiles qu'elle contient sont sorties du jeu, sauf le marqueur de premier joueur, qui est replacé dans la zone centrale. Bien entendu, si le marqueur de premier joueur se trouve sur la ligne plancher d'un joueur, alors il devient le joueur courant — donc le premier joueur de la prochaine manche, s'il y en a une.
Dans le cas — très peu probable en pratique — où le marqueur de premier joueur se trouverait encore dans la zone centrale à la fin de la manche, le joueur courant ne changerait pas. Ce serait donc le joueur suivant celui ayant terminé la manche qui commencerait la prochaine, s'il y en avait une.
2.4. Fin de partie
À la fin de la partie, les murs de tous les joueurs sont examinés afin d'y trouver les lignes, colonnes ou couleurs pleines qu'ils contiennent, et les points bonus correspondants sont ajoutés aux scores des différents joueurs.
3. Mise en œuvre Java
Cette étape consiste principalement à écrire la classe MutableGameState représentant l'état non immuable d'une partie. Avant de faire cela, il convient toutefois d'écrire une interface qui permettra d'observer l'évolution des scores des différents joueurs.
3.1. Interface PointsObserver
L'interface PointsObserver du paquetage principal, publique, représente un « observateur de points », c.-à-d. un objet qui est informé chaque fois que les points d'un joueur changent. Chacune des méthodes de cette interface correspond donc à une situation dans laquelle un joueur peut gagner ou perdre des points, et elle est destinée à être appelée lorsque cela se produit effectivement.
Toutes les méthodes de PointsObserver sont des méthodes par défaut, qui ne font rien du tout, c.-à-d. que leur corps est vide. De la sorte, lorsque cette interface est implémentée par un objet, celui-ci peut simplement ne pas redéfinir certaines méthodes s'il n'est pas intéressé par certains types de gain ou perte de points.
Les méthodes de PointsObserver sont :
void newWallTile(PlayerId playerId, TileDestination.Pattern line, TileKind.Colored color, int points)- qui est destinée à être appelée lorsque le joueur d'identité
playerIda ajouté à la lignelinede son mur une tuile de couleurcoloret remporté ainsipointspoints, void floor(PlayerId playerId, int penalty)- qui est destinée à être appelée à la fin d'une manche lorsque le joueur d'identité
playerIda perdupenalty(> 0) points en raison de la présence de tuiles sur sa ligne plancher, void fullRow(PlayerId playerId, TileDestination.Pattern line, int points)- qui est destinée à être appelée à la fin de la partie lorsque le joueur d'identité
playerIda gagnépoints(= 2) points de bonus car la lignelinede son mur est complète, void fullColumn(PlayerId playerId, int column, int points)- qui est similaire à
fullRowmais pour une colonne pleine, void fullColor(PlayerId playerId, TileKind.Colored color, int points)- qui est similaire à
fullRowmais pour une couleur pleine.
En plus de ces méthodes, PointsObserver offre l'attribut suivant :
PointsObserver EMPTY- qui est un observateur de points vide, c.-à-d. qu'aucune de ses méthodes ne fait quoi que ce soit.
Cet observateur vide est principalement destiné à être utilisé par l'intelligence artificielle, qui simulera un très grand nombre de parties sans être intéressée par la manière exacte dont les joueurs gagnent ou perdent des points. L'interface graphique du jeu utilisera quant à elle un observateur non vide qui affichera, directement sur le plateau des différents joueurs, les points qu'ils auront obtenus de différentes manières.
3.2. Classe MutableGameState
La classe MutableGameState du sous-paquetage gamestate, publique et finale, représente un état de partie non immuable. Elle implémente bien entendu l'interface ReadOnlyGameState.
MutableGameState offre deux constructeurs publics :
MutableGameState(ReadOnlyGameState initialState, PointsObserver pointsObserver)- le constructeur principal, qui retourne un nouvel état de partie ayant le même contenu que
initialState, et auquel l'observateurpointsObserverest attaché — dans le sens où ses méthodes seront appelées lorsque les points d'un joueur changeront, MutableGameState(ReadOnlyGameState initialState)- un constructeur secondaire qui appelle le premier en lui passant l'état initial
initialStateet un observateur de points vide.
De plus, MutableGameState offre des mises en œuvre concrètes des méthodes abstraites de ReadOnlyGameState, à savoir game, pkTileBag, pkTileSources, pkUniqueTileSources, pkPlayerStates et currentPlayerId. Notez que ces méthodes doivent être de simples méthodes d'accès, et ne doivent faire aucun calcul ! En particulier, pkUniqueTileSources ne doit rien faire d'autre que retourner la valeur d'un attribut privé, qui est gardé à jour par toutes les méthodes qui modifient le contenu des sources de tuiles.
À ces mises en œuvre concrètes des méthodes de ReadOnlyGameState, MutableGameState ajoute les méthodes publiques suivantes :
void fillFactories(RandomGenerator randomGenerator)- qui remplit les fabriques au moyen de la technique décrite à la §2.1.1, en utilisant
randomGeneratorpour obtenir les éventuelles valeurs aléatoires nécessaires, void registerMove(short pkMove)- qui modifie l'état de la partie pour tenir compte du fait que le joueur courant a joué le coup (empaqueté)
pkMove, void endRound()- qui termine la manche en mettant à jour les lignes de motif, les murs et les lignes plancher de tous les joueurs, et en comptabilisant les points de fin de manche de tous les joueurs,
void endGame()- qui termine la partie en comptabilisant les points bonus de tous les joueurs.
Notez bien que les méthodes endRound et endGame, qui comptabilisent les points, doivent obligatoirement appeler les méthodes de l'observateur de points passé au constructeur lorsqu'elles ajoutent ou suppriment des points à un joueur.
3.2.1. Conseils de programmation
Dans la méthode registerMove, vous aurez probablement besoin de distinguer le cas où la destination du coup est une ligne de motif de celui où il s'agit de la ligne plancher. Pour faire cela, vous pourriez bien entendu utiliser instanceof puis un transtypage (cast), en écrivant quelque chose comme :
TileDestination dst = …;
if (dst instanceof TileDestination.Pattern) {
TileDestination.Pattern line = (TileDestination.Pattern) dst;
// … utilisation de line
}
Toutefois, un nouveau concept a été ajouté à Java 17, qui se nomme le filtrage de motif pour instanceof (pattern matching for instanceof) et qui permet de récrire le code ci-dessus ainsi :
TileDestination dst = …;
if (dst instanceof TileDestination.Pattern line) {
// … utilisation de line
}
Comme on le voit, l'idée est de déclarer la variable line directement dans le contexte du instanceof, sans devoir faire de transtypage, ce qui simplifie le code.
3.3. Tests
Comme d'habitude, nous ne vous fournissons plus de tests mais un fichier de vérification de signatures à importer dans votre projet.
En dehors des tests JUnit, vous avez à présent la possibilité d'écrire un programme relativement simple vous permettant de jouer à Ajul entre plusieurs joueurs humains, au moyen d'une interface textuelle qui pourrait ressembler à ceci :
Fabriques : [1] A C DD [2] A C D E [3] A B C D
[4] BB C D [5] A BB C
Centre : [0] 1
Aline . abcde | Bertrand . abcde
0 pts .. eabcd | 0 pts .. eabcd
... deabc | ... deabc
.... cdeab | .... cdeab
..... bcdea | ..... bcdea
|
Pté 1 1 2 2 2 3 3 | Pté 1 1 2 2 2 3 3
|
Quel coup désirez-vous jouer, Aline ?
En plus d'afficher à l'écran l'état du jeu, votre programme doit demander au joueur courant le coup qu'il désire jouer, afin de faire évoluer l'état de la partie en fonction. Pour cela, il est utile d'avoir une représentation compacte d'un coup, et nous vous suggérons d'en utiliser une composée de trois caractères, qui sont :
- l'index de la source (0 pour la zone centrale, 1 pour la première fabrique, etc.),
- la lettre de la couleur des tuiles à jouer,
- l'index modifié de la destination, où 0 désigne la ligne plancher, 1 la première ligne de motif, 2 la seconde, et ainsi de suite — cet index modifié étant probablement plus intuitif que celui utilisé en interne par notre programme.
Dans l'exemple ci-dessus, le coup consistant à prendre les 2 tuiles de couleur B de la fabrique 4 pour les placer sur la seconde ligne de motif s'écrirait donc 4B2.
La classe AjulTUI ci-dessous — TUI étant l'acronyme de textual user interface — constitue une ébauche d'un programme permettant de jouer à Ajul via une interface textuelle. Même si cela n'est bien entendu pas obligatoire, nous vous conseillons de la compléter afin de pouvoir jouer à votre jeu dès à présent. Vous pourrez ainsi tester votre projet de manière agréable et ludique, et jouer contre l'intelligence artificielle dès que vous l'aurez développée.
public final class AjulTUI {
static void printState(ReadOnlyGameState gameState) {
// à faire : imprimer l'état du jeu à l'écran
}
static Move queryNextMove(String playerName,
ReadOnlyGameState gameState) {
// à faire : demander au joueur le coup qu'il désire jouer
}
// à faire : autres méthodes
static void main() {
RandomGenerator randomGenerator = /* à faire */;
List<Game.PlayerDescription> playerInfos = /* à faire */;
Map<PlayerId, String> playerNames = /* à faire */;
Game game = new Game(playerInfos);
MutableGameState gameState =
new MutableGameState(ImmutableGameState.initial(game));
gameState.fillFactories(randomGenerator);
while (!gameState.isGameOver()) {
printState(gameState);
String playerName = playerNames
.get(gameState.currentPlayerId());
Move move = queryNextMove(playerName, gameState);
gameState.registerMove(move.packed());
if (gameState.isRoundOver()) {
gameState.endRound();
if (!gameState.isGameOver())
gameState.fillFactories(randomGenerator);
}
}
gameState.endGame();
printState(gameState);
print("Partie terminée ! Scores finaux :\n");
// à faire : afficher les scores
}
}
4. Résumé
Pour cette étape, vous devez :
- écrire les classes et interfaces
PointsObserveretMutableGameStateselon 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 27 mars à 18h00, au moyen du programme
Submit.javafourni 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.