Etape 5 – Evolution de l'état du jeu

1 Introduction

Le but de cette étape est de compléter la gestion de l'évolution de l'état du jeu en faisant correctement évoluer :

  • le plateau de jeu,
  • les explosions, et
  • les bombes.

L'évolution des particules d'explosion ayant déjà été mise en œuvre lors de l'étape précédente, il ne manquera à la fin de cette étape que l'évolution des joueurs, gardée pour la prochaine étape.

1.1 Evolution du plateau

Le plateau de jeu (c-à-d les blocs qui le composent) évolue de trois manières :

  1. les murs destructibles atteints par une particule d'explosion commencent à s'écrouler, et au bout d'un certain nombre de coups d'horloge, sont remplacés par un bloc libre ou un bonus choisi aléatoirement,
  2. les blocs bonus occupant une case dont la sous-case centrale est occupée par au moins un joueur sont consommés et disparaissent, c-à-d qu'ils font place à un bloc libre.
  3. les blocs bonus atteints par une particule d'explosion commencent à disparaître, c-à-d qu'au bout d'un certain nombre de coups d'horloge ils sont remplacés par un bloc libre, sauf s'ils sont consommés par un joueur entre temps,

La probabilité qu'un mur destructible soit remplacé par un bonus bombe, un bonus portée ou un bloc libre est la même, et vaut donc 1/3 pour chacun des cas.

1.2 Evolution des explosions

Les explosions évoluent de manière simple :

  1. de nouvelles explosions apparaissent suite à l'explosion de certaines bombes (pour l'une des raisons évoquées plus bas),
  2. les explosions qui ont atteint la fin de leur durée de vie disparaissent.

Il va de soi que, de plus, les explosions actives émettent à chaque coup d'horloge une particule, mais cela a déjà été décrit lors de l'étape précédente.

1.3 Evolution des bombes

Les bombes du jeu évoluent de trois manières :

  1. de nouvelles bombes apparaissent sur les cases occupées par les joueurs qui décident (et ont la possibilité) de déposer une bombe,
  2. leur mèche se consume, et les bombes dont elle l'est totalement explosent, et donc disparaissent (au profit d'une explosion),
  3. les bombes qui sont atteintes par une particule d'explosion disparaissent immédiatement (au profit d'une explosion), indépendamment de la longueur de leur mèche.

Un joueur n'a la possibilité de déposer une bombe que si :

  • il est vivant,
  • la case contenant la sous-case sur laquelle il se trouve ne contient pas déjà une bombe,
  • il n'a pas déjà atteint le nombre maximum de bombes lui appartenant et pouvant se trouver sur le plateau simultanément.

1.4 Ordre d'évolution

On l'a vu lors de l'étape précédente, l'ordre dans lequel les éléments du nouvel état sont calculés, c-à-d en fonction de quelle version des autres éléments, a une importance fondamentale. La liste ci-dessous spécifie donc cet ordre :

  1. les particules d'explosion évoluent (de la manière décrite dans l'étape précédente),
  2. le plateau de jeu évolue (en fonction des nouvelles particules d'explosion calculée ci-dessus),
  3. les explosions évoluent,
  4. les bombes existantes et celles nouvellement déposées par les joueurs évoluent, en fonction des nouvelles particules d'explosion ; ce faisant, elles génèrent éventuellement de nouvelles explosions qui sont ajoutées à celles résultant de l'évolution des explosions existantes, mais qui n'évolueront donc que durant la prochaine évolution de l'état.
  5. les joueurs évoluent, de la manière qui sera décrite dans l'étape suivante (en fonction des nouvelles particules d'explosion, des nouvelles bombes et du nouveau plateau).

2 Mise en œuvre Java

A l'exception de deux ajouts à faire aux classes des (sous-)cases, la totalité du code à écrire pour cette étape fait partie de la classe GameState.

2.1 Classes Cell et SubCell

Ajoutez aux classes Cell et SubCell une redéfinition de la méthode hashCode, qui soit compatible avec la redéfinition de la méthode equals qu'elles contiennent déjà.

Dans les deux cas, l'index dans l'ordre de lecture (row-major index) de la (sous-)case constitue une excellente valeur de hachage. Pour la classe Cell il suffit donc d'appeler la méthode permettant de l'obtenir, tandis que pour la classe SubCell il faut faire le calcul, en pensant bien au fait que la longueur des lignes de sous-cases est plus grande que celle des lignes de cases.

2.2 Classe GameState

La classe GameState commencée lors de l'étape précédente doit être complétée par l'ajout des trois méthodes publiques suivantes :

  • Map<Cell, Bomb> bombedCells(), qui retourne une table associant les bombes aux cases qu'elles occupent,
  • Set<Cell> blastedCells(), qui retourne l'ensemble des cases sur lesquelles se trouve au moins une particule d'explosion,
  • GameState next(Map<PlayerID, Optional<Direction>> speedChangeEvents, Set<PlayerID> bombDropEvents), qui retourne l'état du jeu pour le coup d'horloge suivant, en fonction de l'actuel et des événements donnés (speedChangeEvents et bombDropEvents), décrits plus bas.

2.2.1 Résolution des conflits

Comme on l'a vu lors de l'étape précédente, différents conflits peuvent apparaître entre les joueurs lors du calcul du nouvel état. Par exemple, deux joueurs peuvent vouloir déposer au même coup d'horloge une bombe sur la même case, alors qu'une case peut contenir au plus une bombe.

Pour résoudre ces conflits, nous avons choisi de calculer l'ensemble des permutations des joueurs, et d'utiliser à chaque coup d'horloge un autre élément de cette ensemble pour ordonner les joueurs. Tous les conflits apparaissant lors d'un coup d'horloge sont résolus au moyen de la permutation courante. (Notez que cela signifie que si deux conflits apparaissent entre certains joueurs au même coup d'horloge, ils sont tous résolus en faveur du même joueur !)

Dans ce but, la classe GameState contient une liste des permutations des identités des joueurs dans un champ privé statique, et utilise à chaque coup d'horloge le prochain élément (de manière circulaire) de cette liste pour ordonner les joueurs.

2.2.2 Apparition des bonus

Comme cela a été expliqué dans l'introduction, la probabilité qu'un mur qui s'écroule soit finalement remplacé par un bonus bombe, un bonus portée ou un bloc libre est identique, et vaut donc 1/3 pour chaque cas.

Pour mettre ce comportement en œuvre, il faut avoir un moyen de choisir aléatoirement l'une de ces trois possibilités. Pour cela, nous utiliserons la classe Random de la bibliothèque standard Java, dont une instance représente un générateur de valeurs (pseudo-)aléatoires.

Si une instance de cette classe est créée au moyen du constructeur par défaut, alors la séquence de valeurs qu'elle produit est différente à chaque exécution. Cela pourrait sembler souhaitable pour ce projet, car ainsi les bonus apparaissent dans un ordre différent à chaque partie.

Toutefois, ce comportement a le défaut de compliquer le test et la correction des projets, précisément car certains comportements deviennent (réellement) aléatoires. Dès lors, il vous est demandé de créer une instance de la classe Random en lui passant une graine (seed en anglais) constante, valant 2016, en utilisant le constructeur approprié. Cette instance doit être stockée dans un attribut privé et statique de la classe GameState, nommé p.ex. RANDOM :

private static final Random RANDOM = new Random(2016);

La graine passée au constructeur détermine totalement les valeurs retournées par le générateur, dans le sens où une graine donnée produira toujours la même séquence de valeurs. Néanmoins, cette séquence aura l'aspect (et les caractéristiques statistiques) d'une séquence effectivement aléatoire.

Une fois le générateur de valeurs pseudo-aléatoires créé, il est utilisé pour obtenir un entier distribué uniformément et valant 0, 1 ou 2, via la méthode nextInt. Cet entier est utilisé pour choisir le bloc remplaçant un mur ayant terminé de s'écrouler, selon la règle suivante :

  • 0 représente un bloc bonus bombe,
  • 1 représente un bloc bonus portée,
  • 2 représente un bloc libre.

2.2.3 Evénements

Les actions entreprises par les joueurs humains sont « perçues » par le serveur comme des événements arrivant du monde extérieur. Deux types d'événements peuvent se produire : les joueurs peuvent demander à déposer une bombe, ou à changer de direction.

Pour cette étape, seuls les événements de dépôt de bombe nous intéressent, et sont donc les seuls décrits. Ils sont très simple : à chaque coup d'horloge, un joueur peut (tenter de) déposer au maximum une bombe. Dès lors, les événements de dépôt de bombe sont simplement représentés par l'ensemble des (identités) de joueurs désirant déposer une bombe. Cet ensemble a le type Set<PlayerID>, et est passé en argument à la méthode next, comme illustré plus haut.

2.2.4 Evolution du plateau

Le plateau évolue selon les règles données plus haut. Nous vous conseillons d'isoler la mise en œuvre de cette évolution dans une méthode séparée, privée et statique, appelée par la méthode next et ayant la signature suivante :

  • Board nextBoard(Board board0, Set<Cell> consumedBonuses, Set<Cell> blastedCells1), qui calcule le prochain état du plateau en fonction du plateau actuel, des bonus consommés par les joueurs et les nouvelles particules d'explosion donnés.

Le paramètre consumedBonuses donne la position des bonus consommés par les joueurs, et il est calculé par la méthode next. Tout bloc bonus occupant une case de cette ensemble doit donc disparaître au prochain coup d'horloge, et faire place à un bloc libre.

Les murs destructibles atteints par une particule d'explosion (c-à-d dont la position appartient à l'ensemble blastedCells1) se transforment en murs en train de s'écrouler (Block.CRUMBLING_WALL), restent dans cet état durant Ticks.WALL_CRUMBLING_TICKS coups d'horloge, puis se transforment, aléatoirement et selon les règles données plus haut, en un bloc libre ou un bonus.

Les bonus atteints par une particule d'explosion commencent à disparaître, dans le sens où ils restent encore dans leur état actuel pour Ticks.BONUS_DISAPPEARING_TICKS coups d'horloge avant de se transformer en bloc libre. Attention à bien gérer le cas où un bloc bonus est atteint par une particule d'explosion alors qu'il est déjà en cours de disparition (ce qui demande très peu de code, mais une bonne dose de réflexion) !

2.2.5 Evolution des joueurs

Même si l'évolution des joueurs ne sera faite qu'à l'étape suivante, nous vous conseillons d'ajouter une méthode d'évolution des joueurs, et de l'appeler depuis la méthode next, afin que celle-ci soit terminée. Bien entendu, cette méthode d'évolution des joueurs ne fait rien pour l'instant, et se contente de retourner les joueurs reçus. Elle a la signature suivante, qui sera expliquée en détail lors de la prochaine étape :

  • List<Player> nextPlayers(List<Player> players0, Map<PlayerID, Bonus> playerBonuses, Set<Cell> bombedCells1, Board board1, Set<Cell> blastedCells1, Map<PlayerID, Optional<Direction>> speedChangeEvents)

La table associative playerBonuses associe les bonus collectés par les joueurs à leur identité. Par exemple, si le joueur 1 collecte un bonus portée, la table associe la valeur Bonus.INC_RANGE à la clef PlayerID.PLAYER_1. Cette table est calculée par la méthode next en même temps que l'ensemble des (positions des blocs) bonus consommés par les joueurs et passé à la méthode nextBoard décrite ci-dessus.

2.2.6 Evolution des explosions

Les bombes évoluent de manière très simple, dans le sens où elles ne font que « vieillir » d'un coup d'horloge. Dès lors, le calcul des nouvelles explosions en fonction des anciennes peut se faire dans une méthode privée et statique ayant la signature suivante :

  • List<Sq<Sq<Cell>>> nextExplosions(List<Sq<Sq<Cell>>> explosions0), qui calcule les explosions pour le prochain état en fonction des actuelles.

Notez que cette méthode s'occupe uniquement du « vieillissement » des explosions existantes. Bien entendu, de nouvelles explosions peuvent apparaître suite à l'explosion de bombes, mais elles sont déterminées lors de l'évolution des bombes, décrite ci-après.

2.2.7 Evolution des bombes

Les bombes évoluent selon les règles données plus haut, et ont la particularité de produire des explosions dans certaines circonstances. Dès lors, nous vous conseillons de les faire évoluer directement dans la méthode next. En effet, si on désirait placer le code d'évolution des bombes dans une méthode séparée, celle-ci devrait retourner deux valeurs : les bombes de l'état suivant, et une partie des explosions de l'état suivant (celles dues aux explosions). Comme il n'est pas facile de retourner plus d'une valeur d'une méthode Java, et que le code d'évolution des bombes est court, il est plus simple de le placer dans la méthode next.

Cela dit, un élément de l'évolution des bombes peut facilement être placé dans une méthode séparée : le calcul des bombes déposées par les joueurs. Pour ce faire, vous pouvez définir une méthode privée et statique ayant la signature suivante :

  • List<Bomb> newlyDroppedBombs(List<Player> players0, Set<PlayerID> bombDropEvents, List<Bomb> bombs0), qui retourne la liste des bombes nouvellement posées par les joueurs, étant donnés les joueurs actuels, les événements de dépôt de bombes et les bombes actuelles donnés.

Si au moins deux joueurs désirent poser une bombe sur la même cellule, le conflit est résolu en faveur de celui apparaissant en premier dans la liste des joueurs donnée. Dès lors, la méthode next doit passer la liste triée selon l'ordre de priorité du coup d'horloge actuel à cette méthode !

Bien entendu, la méthode newlyDroppedBombs n'autorise que les dépôts de bombe conformes aux règles du jeu, stipulées plus haut. Prenez bien garde au fait qu'il est possible que l'identité d'un joueur mort figure dans l'ensemble bombDropEvents, mais cela ne doit pas entraîner le dépôt d'une bombe pour ce joueur fantôme.

2.3 Tests

Comme d'habitude, nous vous fournissons uniquement un fichier de vérification de noms, contenu dans une archive Zip à importer dans votre projet. A vous d'écrire les tests unitaires si vous désirez en avoir, ce qui est vivement conseillé.

En plus des tests, nous vous conseillons d'augmenter la classe GameStatePrinter dont le squelette vous a été fourni lors de l'étape précédente, afin d'afficher les éléments de l'état auquel vous avez maintenant accès, en particulier les bombes (via la méthode bombedCells) et les particules d'explosion (via la méthode blastedCells).

3 Résumé

Pour cette étape, vous devez :

  • compléter les classes Cell, SubCell et GameState en fonction des spécifications 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 jeudi 24 mars 2016 à 17h30, via le système de rendu, afin d'obtenir les 3 points associés à ce rendu mineur.

Note : en raison du Vendredi Saint, la date du rendu a été avancée d'un jour ! Ne l'oubliez pas, car comme d'habitude aucun retard ne sera toléré pour ce rendu.