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 :
- 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,
- 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.
- 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 :
- de nouvelles explosions apparaissent suite à l'explosion de certaines bombes (pour l'une des raisons évoquées plus bas),
- 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 :
- de nouvelles bombes apparaissent sur les cases occupées par les joueurs qui décident (et ont la possibilité) de déposer une bombe,
- leur mèche se consume, et les bombes dont elle l'est totalement explosent, et donc disparaissent (au profit d'une explosion),
- 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 :
- les particules d'explosion évoluent (de la manière décrite dans l'étape précédente),
- le plateau de jeu évolue (en fonction des nouvelles particules d'explosion calculée ci-dessus),
- les explosions évoluent,
- 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.
- 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
etbombDropEvents
), 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
etGameState
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.