Jeu à distance

ChaCuN – étape 10

1. Introduction

Cette étape a pour but de permettre à plusieurs joueurs de prendre part à une partie de ChaCuN, sans qu'ils aient besoin de se rassembler devant un même ordinateur.

2. Concepts

2.1. Jeu à distance

Jusqu'à présent, nous avons fait l'hypothèse que lorsque plusieurs personnes désirent jouer à une partie de ChaCuN ensemble, elles le font en lançant le jeu sur un unique ordinateur, dont elles prennent le contrôle à tour de rôle.

Des joueurs motivés pourraient néanmoins jouer une partie à distance, sur plusieurs ordinateurs, à supposer qu'ils aient un moyen de communiquer entre eux — par exemple par visioconférence, ou au moyen d'un service de messagerie. Pour ce faire, les joueurs devraient tous lancer le jeu sur leur ordinateur, en le configurant exactement de la même manière, c.-à-d. avec les mêmes joueurs et des tas de tuiles rigoureusement identiques. Ensuite, chaque joueur pourrait jouer normalement lorsque c'est son tour, puis communiquer son action au reste du groupe, par exemple en expliquant où il a positionné la prochaine tuile, avec quelle rotation, etc. Les autres joueurs pourraient alors reproduire chaque action dans leur version du jeu, garantissant ainsi que toutes les parties progressent de la même manière.

Telle quelle, cette solution est assez fastidieuse à utiliser en pratique, car il n'est pas facile de communiquer, par exemple, la case sur laquelle une tuile a été posée. Toutefois, en supposant que l'on arrive à trouver une manière simple de représenter les différentes actions, elle pourrait devenir tout à fait réaliste. C'est ce que nous allons faire dans cette étape.

2.2. Paramètres des actions

Pour permettre à chaque joueur de facilement reproduire les actions des autres, il nous faut trouver un moyen de représenter les différentes actions possibles de manière concise. Heureusement, les règles du jeu rendent cela relativement facile, puisqu'à chaque instant, elles déterminent qui est le joueur courant, et quelle est l'action qu'il doit effectuer, qui ne peut être que l'une des trois suivantes :

  1. poser la prochaine tuile,
  2. occuper (ou pas) la dernière tuile posée,
  3. reprendre (ou pas) un pion en main — suite à la pose du chaman.

La seule chose qu'il nous faut donc pouvoir représenter de manière concise sont les paramètres de chacune de ces trois actions, puisque ceux-ci sont laissés au choix des joueurs. Par exemple, pour la pose de la prochaine tuile, ces paramètres sont la case de la frange sur laquelle la tuile est posée, et la rotation qui lui est appliquée.

Les sections suivantes examinent comment représenter les paramètres de ces différentes actions en utilisant aussi peu d'information que possible.

2.2.1. Pose de la prochaine tuile

Lorsque le joueur courant doit poser la prochaine tuile, il doit choisir :

  • sur laquelle des cases de la frange il désire la poser,
  • quelle rotation il désire lui appliquer.

Pour déterminer quelle quantité d'information est nécessaire pour représenter ces deux paramètres, il faut savoir combien de possibilités différentes existent pour chacun d'eux.

Pour la rotation, la réponse est simple puisqu'il y en a 4 possibles, soit 2 bits d'information.

Pour la case, on peut voir assez rapidement que la taille de la frange est maximale lorsque toutes les tuiles sauf une ont été posées, et que toutes ces tuiles forment une seule ligne. Dans ce cas, la frange contient les cases au nord et au sud de chaque tuile, ainsi que la case à l'ouest de la tuile la plus à l'ouest, et la case à l'est de la tuile la plus à l'est. Autrement dit, il y a alors 2×94 + 2 = 190 cases dans la frange, soit un peu moins de 8 bits d'information.

En résumé, la quantité d'information contenue dans les paramètres de la pose de la prochaine tuile est d'un peu moins de 10 bits.

Il serait possible de réduire encore cette quantité d'information en tenant compte de la taille limitée du plateau, ou en ne considérant que les paires case/rotation valides. Le nombre maximum de cas possibles est toutefois plus difficile à déterminer, et l'éventuel gain obtenu n'aurait probablement pas d'importance en pratique.

2.2.2. Occupation de la dernière tuile posée

Lorsque le joueur courant doit décider s'il veut occuper la tuile qu'il vient de poser, il doit choisir :

  • le type d'occupant qu'il désire placer,
  • la zone qu'il désire occuper.

Il y a deux types d'occupants, donc 1 bit d'information, et un maximum de 9 zones sur une tuile, soit un peu moins de 4 bits d'information, pour un total de 5 bits.

2.2.3. Reprise en main d'un pion

Lorsque le joueur courant doit reprendre un pion en main, il doit choisir lequel. Comme il peut avoir au maximum 5 pions sur le plateau, cela représente un peu moins de 3 bits d'information.

2.2.4. Base32

Connaissant le nombre de bits d'information présents dans les paramètres de chacune des actions, il reste à déterminer comment représenter ces bits au moyen de chaînes de caractères faciles à transmettre oralement ou via un service de messagerie.

Pour cela, nous utiliserons un encodage nommé base32, dont l'idée est grosso modo de représenter les données sous la forme de nombres en base 32, d'où le nom. Les 32 chiffres de ces nombres sont les 26 lettres de l'alphabet latin, puis les chiffres décimaux de 2 à 7, soit :

ABCDEFGHIJKLMNOPQRSTUVWXYZ234567

En d'autres termes, le « chiffre » A représente 0, le « chiffre » B représente 1, et ainsi de suite jusqu'au « chiffre » 7 qui représente 31.

Un nombre exprimé dans cette base avec la notation positionnelle usuelle est donc une simple séquence de chiffres. Par exemple, le nombre CH en base 32 est celui que l'on note 71 en base 10, car C vaut 2, H vaut 7, et 2×32+7 vaut 71.

Base32 permet de représenter 5 bits par chiffre, ce qui n'est pas énorme, mais son gros avantage est qu'il n'est pas sensible à la casse — les lettres peuvent être en minuscule ou majuscule — et qu'il évite les chiffres qui ressemblent aux lettres — p. ex. le chiffre 0 (zéro) n'est pas inclus car trop similaire à la lettre O, le chiffre 1 (un) non plus car trop similaire à la lettre L minuscule, etc. Ces caractéristiques sont intéressantes pour nous car elles réduisent les chances de commettre une erreur lorsque des nombres en base 32 sont transmis oralement, par exemple.

2.2.5. Encodage des actions

Comme nous l'avons dit, un chiffre en base 32 permet de représenter 5 bits, ce qui convient bien à la représentation des paramètres des actions, sachant qu'ils nécessitent 5 ou 10 bits, soit 1 ou 2 caractères.

La table ci-dessous résume la représentation que nous utiliserons pour représenter les paramètres des trois types d'actions possibles. Les colonnes c1 et c2 donnent les bits des 1 ou 2 caractères représentant les paramètres de l'action, chaque bit étant représenté par une lettre qui indique son contenu.

Action c1 c2
Pose d'une tuile ppppp ppprr
Occupation de la tuile   kzzzz
Reprise d'un pion   ooooo

Pour la pose d'une tuile, les 8 bits nommés p représentent l'index de la case de la frange sur laquelle la tuile est posée, compris entre 0 (inclus) et 190 (exclu) comme nous l'avons dit. Pour qu'elles puissent être indexées, les cases de la frange sont triées dans l'ordre croissant, d'abord par leur coordonnée x, puis par leur coordonnée y. Les deux bits nommés r représentent l'index de la rotation utilisée dans le type énuméré Rotation. Donc 00 correspond à NONE, 01 à RIGHT, etc.

Pour l'occupation d'une tuile, k est le bit représentant la sorte d'occupant, où 0 correspond à PAWN, 1 à HUT. Les 4 bits z sont le numéro de la zone occupée, compris entre 0 (inclus) et 10 (exclu). Le cas où aucun occupant n'est posé est représenté par les 5 bits 11111.

Pour la reprise d'un pion, les 5 bits o représentent l'index du pion à reprendre, parmi tous les pions présents sur le plateau, et pas seulement ceux du joueur courant, car cela simplifie la mise en œuvre. Sachant qu'il y a au maximum 5 joueurs disposant de 5 pions chacun, cet index est compris entre 0 (inclus) et 25 (exclu). Afin de pouvoir être indexés, les pions du plateau sont triés par ordre croissant de l'identifiant de la zone qu'ils occupent. Le cas où aucun pion ne doit être repris est représenté par les 5 bits 11111.

2.2.6. Exemple

Au début d'une partie, la frange contient les 4 cases voisines de celle à la position (0,0), qui contient la tuile de départ. Triées par coordonnées x d'abord puis par coordonnée y ensuite, ces cases ont les index suivants :

Index Position
0 (-1, 0)
1 (0, -1)
2 (0, 1)
3 (1, 0)

Admettons que le premier joueur place la première tuile au nord de la tuile de départ, donc à la position (0, -1), en la tournant d'un demi-tour. L'index de la position est 1, tandis que celui de la rotation est 2. Les 10 bits représentant les paramètres de cette action sont donc :

00000 00110

soit AG en base32.

Admettons maintenant que le premier joueur décide, après avoir posé la première tuile, d'occuper sa zone 3 au moyen d'un pion. Les 5 bits correspondants aux paramètres de cette action sont :

00011

soit D en base32.

2.3. Interface graphique

L'interface graphique permettant le jeu à distance est extrêmement simple et consiste en :

  • un affichage de la représentation en base32 des quatre dernières actions effectuées, avec leur numéro,
  • un champ textuel dans lequel une action en base32 peut être entrée.

Seules les 4 dernières actions sont visibles sur l'interface. Toutes les actions sont numérotées à partir du début de la partie, la première ayant le numéro 1, la seconde le numéro 2, etc.

Cette interface est présentée ci-dessous. Elle n'était pas présente dans les copies d'écran de l'interface graphique finale des étapes précédentes, mais nous la placerons dans la partie droite de l'interface, juste sous le tableau d'affichage.

actions-ui;32.png
Figure 1 : Interface graphique des actions

3. Mise en œuvre Java

Cette étape est un peu particulière en ce qu'elle combine du code appartenant au modèle avec du code appartenant à la vue et au contrôleur. Pour cette raison, les deux premières classes à réaliser appartiennent au paquetage principal, la dernière au sous-paquetage gui.

Avant de commencer à programmer ces classes, vous devez toutefois télécharger l'archive Zip que nous vous fournissons et qui contient un unique fichier nommé actions.css. Il s'agit bien entendu d'une feuille de style, à placer comme d'habitude dans le dossier resources de votre projet.

3.1. Classe Base32

La classe Base32 du paquetage principal, publique et non instanciable, contient des méthodes permettant d'encoder et de décoder des valeurs binaires en base32.

Cette classe possède un attribut public, statique et final, nommé p. ex. ALPHABET, qui est une chaîne contenant les caractères correspondant aux chiffres en base 32, ordonnés par poids croissant. En d'autres termes, cette chaîne est :

ABCDEFGHIJKLMNOPQRSTUVWXYZ234567

En plus de cet attribut, la classe Base32 contient des méthodes permettant de transformer des valeurs binaires en « nombres » base32, représentés par des séquences de caractères. Vu les besoins limités du projet, et dans un souci de simplicité, nous vous proposons de définir des méthodes qui ne permettent de transformer que des valeurs de 5 ou 10 bits en chaînes de 1 ou 2 caractères respectivement. Il s'agit de :

  • une méthode nommée p. ex. isValid, prenant en argument une chaîne de caractères et retournant vrai ssi elle n'est composée que de caractères de l'alphabet base32,
  • une méthode nommée p. ex. encodeBits5, prenant en argument une valeur de type int et retournant la chaîne de longueur 1 correspondant à l'encodage en base32 des 5 bits de poids faible de cette valeur,
  • une méthode nommée p. ex. encodeBits10, prenant en argument un valeur de type int et retournant la chaîne de longueur 2 correspondant à l'encodage en base32 des 10 bits de poids faible de cette valeur,
  • une méthode nommée p. ex. decode, prenant en argument une chaîne de longueur 1 ou 2 représentant un nombre en base32 et retournant l'entier de type int correspondant.

3.2. Classe ActionEncoder

La classe ActionEncoder du paquetage principal, publique et non instanciable, contient des méthodes permettant d'encoder et de décoder des (paramètres) d'actions, et d'appliquer ces actions à un état de jeu.

Toutes les méthodes de cette classe retournent une paire constituée de :

  • un état de jeu, de type GameState, qui est celui résultant de l'application d'une action à un état de jeu initial, passé en argument à chacune des méthodes,
  • une chaîne de caractère qui est l'encodage, en base32, de l'action appliquée à l'état du jeu initial pour obtenir celui se trouvant dans la paire.

Cette paire est représentée par un enregistrement StateAction, imbriqué dans ActionEncoder.

Les trois premières méthodes de ActionEncoder correspondent chacune aux méthodes withPlacedTile, withNewOccupant et withOccupantRemoved de GameState, et peuvent donc logiquement porter le même nom.

La première d'entre elles, withPlacedTile, prend en arguments un état de jeu et une tuile placée, et retourne une paire composée de :

  • le résultat de l'application de la méthode withPlacedTile de GameState à l'état reçu en argument, avec la tuile reçue en argument,
  • la chaîne base32, de longueur 2, représentant cette action.

Les méthodes withNewOccupant et withOccupantRemoved sont similaires.

La dernière méthode de ActionEncoder, nommée p. ex. decodeAndApply, prend en arguments un état de jeu et une chaîne de caractères, qui est l'encodage base32 d'une action. Elle retourne la paire composée de l'état du jeu résultant de l'application de l'action correspondante à l'état passé en argument, et de la chaîne de caractères représentant l'action.

La manière dont la chaîne est interprétée dépend de la prochaine action de l'état du jeu. Par exemple, s'il s'agit de PLACE_TILE, alors la chaîne est interprétée comme représentant les paramètres de cette action-là — un index d'une case de la frange, et une rotation.

Dans le cas où la chaîne qu'on lui a passé en argument ne représente pas une action valide qui peut être appliquée à l'état du jeu donné, decodeAndApply retourne null. Notez qu'il y a de nombreuses raisons pour lesquelles l'action peut être invalide : la chaîne peut contenir des caractères qui n'appartiennent pas à l'alphabet base32, elle peut ne pas avoir la bonne longueur, l'action décrite par la chaîne peut ne pas être valide pour l'état de jeu donné, etc.

3.2.1. Conseils de programmation

Vu le grande nombre de cas dans lesquels decodeAndApply doit retourner null, nous vous conseillons de l'écrire de manière à ce qu'elle ne fasse rien d'autre qu'appeler une méthode auxiliaire chargée du décodage et de l'application de l'action. Cette méthode auxiliaire lève une exception si l'action est invalide, que decodeAndApply intercepte pour retourner null.

3.3. Classe ActionUI

La classe ActionUI du sous-paquetage gui, publique et non instanciable, contient le code de création de l'interface graphique présentée à la §2.3.

Comme toutes les autres classes de création d'interface graphique, elle ne possède qu'une seule méthode publique, nommée p. ex. create, prenant en arguments :

  • une valeur de type ObservableValue<List<String>>, qui est la liste (observable) de la représentation en base32 de toutes les actions — de leurs paramètres, pour être précis — effectuées depuis le début de la partie,
  • une valeur de type Consumer<String>, qui est un gestionnaire d'événement destiné à être appelé avec la représentation en base32 d'une action, qui doit être effectuée si elle est valide.

3.3.1. Graphe de scène

Le graphe de scène créé par ActionUI est extrêmement simple et consiste en une instance de HBox à laquelle la feuille de style actions.css est attachée et dont l'identité est actions. Elle possède deux enfants, qui sont :

  • une instance de Text dont le texte est constitué de la représentation en base32 des 4 dernières actions, précédée chacune de son numéro et d'un deux-points, et séparées par des virgules et des espaces, par exemple :
    6:A5, 7:D, 8:AU, 9:C
  • une instance de TextField dont l'identité est action-field.

Le champ textuel — c.-à-d. l'instance de TextField — doit être configuré afin de n'accepter que les caractères de l'alphabet base32, et de transformer toutes les minuscules en majuscules. Cela peut se faire en attachant un « formateur de texte » au champ, comme expliqué dans les conseils de programmation ci-dessous.

De plus, dès que l'utilisateur appuie sur la touche d'entrée (⮐), le contenu du champ textuel doit être passé à la méthode accept du gestionnaire d'événement fourni à create, après quoi le champ doit être vidé.

3.3.2. Conseils de programmation

Afin que le champ textuel permettant l'entrée de l'action n'accepte que les caractères de l'alphabet base32 et les mette en majuscule, il faut lui attacher un « formateur de texte » au moyen de la méthode setTextFormatter.

Un formateur de texte est généralement spécifié sous la forme d'une lambda, qui prend en argument une valeur de type TextFormatter.Change, modifie éventuellement cette valeur, puis la retourne. Le formateur est appelé chaque fois que le texte du champ auquel il est attaché change, et il peut donc apporter des modifications au texte entré par l'utilisateur avant que celui-ci n'apparaisse dans le champ.

Par exemple, un formateur supprimant tous les espaces dans le texte entré dans un champ textuel peut s'écrire ainsi :

TextField textField = …;
textField.setTextFormatter(new TextFormatter<>(change -> {
      change.setText(change.getText().replace(" ", ""));
      return change;
}));

Notez que pour écrire le formateur de cette étape, vous pouvez utiliser la programmation par flots, car il est possible d'obtenir le flot des caractères d'une chaîne au moyen de la méthode chars.

3.4. Tests

Les classes Base32 et ActionEncoder peuvent être testées relativement facilement au moyen de tests unitaires, que nous vous conseillons d'écrire. Quant à ActionUI, il est possible d'adapter l'un des programmes de test des deux dernières étapes pour la tester.

4. Résumé

Pour cette étape, vous devez :

  • écrire les classes Base32, ActionEncoder et ActionUI selon les indications données ci-dessus,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies.

Aucun rendu n'est à faire pour cette étape avant le rendu final. N'oubliez pas de faire régulièrement des copies de sauvegarde de votre travail en suivant nos indications à ce sujet.