État immuable d'une partie

Ajul – étape 5

1. Introduction

Le but principal de cette cinquième étape est d'écrire les classes permettant de regrouper les morceaux d'état empaquetés définis aux étapes précédentes dans une classe unique représentant l'état complet — et pour l'instant immuable — d'une partie d'Ajul.

2. Concepts

L'état complet d'une partie est composé d'une part de l'état partagé — contenu des sources de tuiles, du sac, etc. — et d'autre part de l'état individuel de chacun des joueurs — contenu de ses lignes de motif, mur, etc.

2.1. État d'un joueur

L'état d'un joueur est constitué des éléments suivants :

  1. le contenu des lignes de motif de son plateau,
  2. le contenu de la ligne plancher de son plateau,
  3. le contenu du mur de son plateau,
  4. son nombre de points actuel (score).

Nous avons vu aux étapes précédentes comment empaqueter, chaque fois dans un entier de type int, tous les éléments du plateau d'un joueur :

  1. le contenu des lignes de motif à l'étape 3,
  2. le contenu de la ligne plancher à l'étape 3,
  3. le contenu du mur à l'étape 4.

Le seul élément manquant de l'état complet d'un joueur est son nombre de points. Celui-ci peut aussi être représenté au moyen d'un entier de type int, le score maximal qu'il est possible d'atteindre à Ajul étant de 240 points1.

Dès lors, l'état complet d'un joueur peut être représenté au moyen de 4 entiers de type int, et l'état complet des n joueurs d'une partie peut l'être au moyen de 4​n entiers de type int. C'est la représentation que nous utiliserons, en stockant l'état de tous les joueurs dans un unique tableau d'entiers de type int.

2.2. État d'une partie

L'état d'une partie est constitué des éléments suivants :

  • la configuration de la partie (nom et type des joueurs, principalement),
  • le contenu du sac de tuiles utilisé pour remplir les fabriques,
  • le contenu des sources de tuiles (fabriques et zone centrale),
  • l'ensemble des tuiles sorties du jeu,
  • l'état individuel des différents joueurs,
  • l'identité du joueur courant.

L'ensemble des tuiles sorties du jeu est constitué de toutes les tuiles qui ont été sorties du plateau à différents moments du jeu — généralement à la fin des manches. Sachant que cet ensemble de tuiles est constitué de celles qui ne sont ni dans le sac, ni sur le plateau, il n'est pas nécessaire de le stocker explicitement. On peut simplement le calculer en prenant l'ensemble des tuiles existant dans le jeu — 20 de chaque couleur, et un marqueur de premier joueur — puis en en retirant les tuiles se trouvant :

  • dans le sac, ou
  • sur une des sources (zone centrale ou fabrique), ou
  • sur une ligne de motif, la ligne plancher ou le mur d'un des joueurs.

Pour cette raison, nous ne stockerons pas l'ensemble des tuiles sorties du jeu dans l'état, mais nous le calculerons au besoin.

2.2.1. Sources de tuiles uniques

Il peut arriver que plusieurs fabriques contiennent le même ensemble de tuiles. Lorsque c'est le cas, il est clair qu'un joueur n'a besoin de prendre en compte qu'une seule fabrique parmi celles qui ont le même contenu. En effet, jouer les tuiles de l'une ou de l'autre d'entre elles est totalement équivalent. Cela est particulièrement important pour l'IA, car elle peut ainsi éviter de perdre du temps à évaluer différents coups équivalents.

Pour cette raison, nous mémoriserons dans l'état de la partie l'ensemble de ce que nous appellerons les sources uniques de tuiles (unique tile sources). Nous considérerons qu'une source de tuiles est unique ssi elle :

  • contient au moins une tuile colorée, et
  • ne contient pas le même ensemble de tuiles qu'une fabrique se trouvant avant elle dans l'ordre des sources.

Par exemple, imaginons qu'au début d'une partie à 2 joueurs, les 6 sources aient le contenu suivant — où la source 0 est la zone centrale, les sources 1 à 5 sont les fabriques, et M représente le marqueur de premier joueur :

0: {1*M}            1: {2*A,2*B}        2: {2*A,2*B}
3: {2*A,2*B}        4: {2*A,1*C,1*E}    5: {4*A}

Dans ce cas, l'ensemble des sources uniques est {1,4,5}. En effet, même si la zone centrale contient une tuile, elle ne contient aucune tuile colorée. D'autre part, les sources 2 et 3 contiennent le même ensemble que la fabrique 1 qui les précède dans l'ordre des sources.

Si plus tard dans la même partie les sources ont le contenu suivant :

0: {2*A,2*B}        1: {}               2: {2*A,2*B}
3: {2*A,2*B}        4: {}               5: {4*A}

alors l'ensemble des sources uniques est maintenant {0,2,5}. En effet, même si la source 2 a le même contenu que la zone centrale, elle est unique car aucune fabrique se trouvant avant elle n'a le même contenu. Par contre, les sources 1 et 4 ne sont pas uniques, car elles sont vides, et la 3 ne l'est pas non plus car son contenu est identique à celui de la fabrique 2, qui la précède.

Nous représenterons l'ensemble des sources uniques par l'ensemble de leurs index, compris entre 0 et 9 (inclus), que nous empaquetterons bien entendu dans un entier de type int.

2.3. Coups jouables

Lorsque c'est à son tour de jouer, un joueur doit choisir un coup parmi tous ceux qu'il a le droit de jouer selon les règles du jeu. Comme nous l'avons vu à l'étape 2, le plus grand nombre de coups parmi lesquels un joueur peut devoir choisir ne peut pas être supérieur à 216.

L'ensemble des coups jouables peut être déterminé assez facilement en énumérant :

  • toutes les sources,
  • toutes les couleurs de tuiles que ces sources contiennent,
  • toutes les destinations capables d'accueillir ces tuiles.

Sachant que la ligne plancher peut toujours accueillir des tuiles — même lorsqu'elle est pleine, car les tuiles excédentaires sont alors sorties du jeu — il existe toujours au moins une destination pour chaque couleur de chaque source.

Il faut noter que lorsqu'une ligne de motif est pleine, la choisir comme destination est équivalent à choisir la ligne plancher. Dès lors, nous considérerons qu'un coup ayant une ligne de motif pleine comme destination n'est pas un coup jouable, même si les tuiles de la ligne ont la bonne couleur.

3. Mise en œuvre Java

3.1. Classe PkPlayerStates

La classe PkPlayerStates du sous-paquetage gamestate.packed, publique et finale, contient des méthodes permettant de manipuler les états empaquetés de tous les joueurs d'une partie d'Ajul. Ces états empaquetés sont stockés dans un tableau de 4​n entiers, où n est le nombre de joueurs. Les 4 premiers éléments contiennent :

  • à l'index 0, le contenu des lignes de motif du premier joueur,
  • à l'index 1, le contenu de la ligne plancher du premier joueur,
  • à l'index 2, le contenu du mur du premier joueur,
  • à l'index 3, le nombre de points du premier joueur.

Les 4 prochains éléments du tableau contiennent les données correspondantes du second joueur, et ainsi de suite.

Il faut noter que ce tableau sera représenté de deux manières, en fonction de s'il doit être en lecture seule ou modifiable :

  • les méthodes ayant uniquement besoin de lire une partie de l'état d'un joueur recevront le tableau sous la forme d'une instance de ReadOnlyIntArray (classe définie à l'étape 1),
  • les méthodes ayant besoin de modifier (et éventuellement lire) une partie de l'état d'un joueur recevront le tableau sous la forme d'un tableau primitif de type int[].

PkPlayerStates offre les méthodes publiques et statiques suivantes :

ImmutableIntArray initial(Game game)
qui retourne un tableau immuable contenant l'état empaqueté initial des joueurs de la partie game, dans lequel toutes les lignes de motif, lignes plancher et murs sont vides, et tous les points valent 0,
int pkPatterns(ReadOnlyIntArray pkPlayerStates, PlayerId playerId)
qui retourne le contenu empaqueté des lignes de motif du joueur donné, extrait du tableau pkPlayerStates,
int pkFloor(ReadOnlyIntArray pkPlayerStates, PlayerId playerId)
qui est similaire à pkPatterns mais retourne le contenu de la ligne plancher,
int pkWall(ReadOnlyIntArray pkPlayerStates, PlayerId playerId)
qui est similaire à pkPatterns mais retourne le contenu du mur,
int points(ReadOnlyIntArray pkPlayerStates, PlayerId playerId)
qui est similaire à pkPatterns mais retourne le nombre de points,
void setPkPatterns(int[] pkPlayerStates, PlayerId playerId, int pkPatterns)
qui change, dans le tableau pkPlayerStates, le contenu (empaqueté) des lignes de motif du joueur d'identité playerId afin qu'il vaille pkPatterns,
void setPkFloor(int[] pkPlayerStates, PlayerId playerId, int pkFloor)
qui est similaire à setPkPatterns mais change le contenu de la ligne plancher,
void setPkWall(int[] pkPlayerStates, PlayerId playerId, int pkWall)
qui est similaire à setPkPatterns mais change le contenu du mur,
void addPoints(int[] pkPlayerStates, PlayerId playerId, int pointsToAdd)
qui change, dans le tableau pkPlayerStates, le nombre de points du joueur d'identité playerId, en lui ajoutant pointsToAdd (qui peut être négatif !) points.

3.2. Interface ReadOnlyGameState

L'interface ReadOnlyGameState du sous-paquetage gamestate, publique, représente un état de partie en lecture seule. Elle possède les méthodes abstraites suivantes :

Game game()
qui retourne la configuration de la partie,
int pkTileBag()
qui retourne le contenu du sac duquel les tuiles sont extraites pour remplir les fabriques, sous la forme d'un ensemble de tuiles empaqueté (voir PkTileSet),
ReadOnlyIntArray pkTileSources()
qui retourne un tableau décrivant le contenu des sources de tuiles, l'élément à l'index i de ce tableau étant un ensemble de tuiles empaqueté (voir PkTileSet) correspondant au contenu de la source d'index i,
int pkUniqueTileSources()
qui retourne l'ensemble empaqueté (voir PkIntSet32) des index des sources uniques, selon la définition de la §2.2.1,
ReadOnlyIntArray pkPlayerStates()
qui retourne un tableau contenant les états empaquetés des joueurs,
PlayerId currentPlayerId()
qui retourne l'identité du joueur courant.

De plus, ReadOnlyGameState offre les méthodes par défaut (!) suivantes :

ImmutableGameState immutable()
qui retourne une version immuable de l'état de la partie auquel on l'applique,
List<PlayerId> playerIds()
qui retourne la liste des identités des joueurs de la partie, qui est identique à celle stockée dans l'instance de Game retournée par la méthode game,
boolean isRoundOver()
qui retourne vrai ssi la manche est terminée, c.-à-d. qu'aucune source de tuile ne contient de tuile colorée,
boolean isGameOver()
qui retourne vrai ssi la partie est terminée, c.-à-d. que la manche est terminée et qu'au moins un joueur possède une ligne horizontale complète dans son mur,
int pkDiscardedTiles()
qui retourne l'ensemble empaqueté (voir PkTileSet) des tuiles sorties du jeu,
int validMoves(short[] destination)
qui place dans le tableau destination, dont la taille doit être au moins égale à Move.MAX_MOVES, tous les coups empaquetés (voir PkMove) que le joueur courant pourrait jouer, et retourne leur nombre,
int uniqueValidMoves(short[] destination)
qui est similaire à validMoves mais ne prend en compte que les sources uniques.

Notez bien que les méthodes ci-dessus sont des méthodes par défaut, et qu'il faut donc écrire leur code dans l'interface ReadOnlyGameState !

3.2.1. Conseils de programmation

Les méthodes validMoves et uniqueValidMoves étant très similaires, il est fortement conseillé d'écrire une méthode privée contenant le code commun.

3.3. Enregistrement ImmutableGameState

L'enregistrement ImmutableGameState, du sous-paquetage gamestate, public, représente un état de jeu immuable. Il implémente l'interface ReadOnlyGameState et possède les attributs suivants, dans l'ordre :

  • Game game,
  • int pkTileBag,
  • ImmutableIntArray pkTileSources,
  • int pkUniqueTileSources,
  • ImmutableIntArray pkPlayerStates,
  • PlayerId currentPlayerId.

Ces attributs contiennent les valeurs retournées par les méthodes de même nom de ReadOnlyGameState et ne sont donc pas décrits en détail ici.

ImmutableGameState possède un constructeur compact qui vérifie, au moyen de requireNonNull, qu'aucun des arguments qui pourrait être null ne l'est. De plus, il offre une méthode fabrique permettant de construire l'état initial d'une partie :

ImmutableGameState initial(Game game)
qui retourne l'état initial d'une partie, dans lequel toutes les sources sont vides sauf la zone centrale qui contient le marqueur de premier joueur, le sac contient la totalité des tuiles colorées (20 de chaque couleur), et le joueur courant est le premier joueur de la partie.

En dehors de cette méthode fabrique et des méthodes ajoutées automatiquement aux enregistrements par Java, ImmutableGameState ne possède qu'une redéfinition de la méthode immutable() de l'interface ReadOnlyGameState. Bien entendu, cette redéfinition ne fait que retourner le récepteur (this), afin d'éviter la création d'une copie d'un état déjà immuable.

3.4. Tests

Comme pour l'étape précédente, 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 les classes et interfaces PkPlayerStates, ReadOnlyGameState et ImmutableGameState selon 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 20 mars à 18h00, au moyen du programme Submit.java fourni 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.

Notes de bas de page

1

Voir Achieving the maximal score in Azul de Sara Kooistra.