Etape 3 – Joueurs et bombes

1 Introduction

Le but de cette troisième étape est d'écrire les classes permettant de représenter trois aspects important du jeu : les joueurs, les bombes et les explosions.

1.1 Joueur

Un joueur de XBlast est caractérisé par :

  • son identité (joueur 1, 2, 3 ou 4),
  • son nombre de vies,
  • son état (invulnérable, vulnérable, …),
  • sa position sur le plateau (une sous-case),
  • la direction dans laquelle il regarde,
  • le nombre maximum de bombes lui appartenant qui peuvent se trouver sur le plateau simultanément,
  • la portée des explosions produites par ses bombes.

Tout comme le contenu du plateau de jeu, plusieurs des caractéristiques d'un joueur évoluent au cours du temps. Par exemple, lorsqu'un joueur se déplace, sa position change à chaque coup d'horloge. De même, au début de l'une de ses vies, il ne reste invulnérable que pour un nombre de coups d'horloge déterminé.

Lors de l'étape précédente, nous avons utilisé des séquences pour gérer l'évolution temporelle du plateau, et nous ferons de même ici pour gérer l'évolution temporelle des caractéristiques des joueurs.

1.1.1 Evolution de la position

Etant donné qu'elle évolue au cours du temps, la position d'un joueur n'est pas représentée par une unique sous-case (celle qu'il occupe actuellement), mais bien comme une séquence de sous-cases : le premier élément de cette séquence représente sa position actuelle, le second sa position lors du coup d'horloge suivant, etc. A noter que ces positions futures constituent, en quelque sorte, le futur idéal du joueur. C'est-à-dire qu'elles ne tiennent pas compte des potentielles interactions du joueur avec son environnement (explosions, murs, bombes, etc.). Toutefois, comme pour les blocs du plateau de jeu, l'interaction avec l'environnement sera gérée séparément.

Le fait d'associer à un joueur ses positions futures a plusieurs avantages. Premièrement, il n'est pas nécessaire de lui associer une vitesse de déplacement, puisque celle-ci est implicitement exprimée au moyen des positions futures : un joueur arrêté a une liste de positions futures constante, ce qui n'est pas le cas d'un joueur en déplacement. Deuxièmement, étant donné que la séquence de positions futures peut être quelconque, elle permet d'exprimer des déplacements d'une complexité arbitraire. Comme nous le verrons dans une étape ultérieure, cela permet de résoudre élégamment un problème qui se pose lorsqu'un joueur désire changer de direction.

Comme cela a été mentionné plus haut, en plus d'occuper une sous-case donnée, un joueur regarde toujours dans l'une des quatre directions. L'évolution temporelle de sa position et de sa direction de regard sont liées, car lorsqu'un joueur se déplace dans une direction donnée, il regarde également dans cette direction. Dès lors, il est naturel de gérer l'évolution temporelle de ces deux attributs au moyen d'une unique séquence de paires (position, direction), et c'est ce que nous ferons. Cela ne change toutefois pas fondamentalement ce qui a été dit ci-dessus.

1.1.2 Evolution de l'état

Une autre caractéristique du joueur qui évolue au cours du temps est ce que nous appellerons son état.

Au début de chacune de ses vies, un joueur est dans un état particulier, dit invulnérable, dans lequel il n'est pas affecté par les explosions qui l'atteignent. Visuellement, cette invulnérabilité est indiquée par le clignotement rapide du joueur, comme on peut le voir dans la vidéo de présentation du jeu.

Une fois sa période d'invulnérabilité terminée, un joueur devient vulnérable, c-à-d qu'il perd une vie au contact d'une explosion. Cet état peut être considéré comme son état normal, puisqu'il y reste jusqu'à la fin de la partie s'il n'est pas atteint par une explosion.

Lorsqu'un joueur vulnérable est atteint par une explosion, il perd une vie. Cela ne se fait toutefois pas instantanément, puisqu'il passe d'abord dans un autre état, que nous appellerons mourant, dans lequel il est inerte.

Lorsqu'il a terminé d'être mourant, le joueur recommence un nouveau cycle de vie, à supposer qu'il lui reste encore une vie à vivre. Ce nouveau cycle commence bien entendu par une période d'invulnérabilité de la même durée que la précédente, suivie d'une période de vulnérabilité qui dure jusqu'à la fin de la partie ou jusqu'au prochain contact avec une explosion.

Ces cycles sont répétés jusqu'à ce qu'il ne reste plus de vie au joueur, moment auquel il passe de l'état mourant à l'état mort, dans lequel il reste jusqu'à la fin de la partie.

Cette évolution de l'état d'un joueur est résumée par la machine d'état présentée dans la figure ci-dessous. Pour faciliter sa compréhension, à chaque état est associée une image représentant l'un des joueurs dans l'état en question.

Sorry, your browser does not support SVG.

Figure 1 : Evolution possible de l'état d'un joueur

Au même titre que l'évolution temporelle de la position d'un joueur est liée à celle de la direction de son regard, l'évolution temporelle de son état est liée à celle de son nombre de vies. Dès lors, une séquence unique de paires (nombre de vies, état) est utilisée pour représenter l'évolution temporelle de ces deux attributs.

1.1.3 Evolutions dissociées

Il est légitime de se demander pourquoi deux séquences distinctes sont utilisées pour représenter l'évolution du couple (position, direction de regard) du joueur et celle du couple (état, nombre de vies). La raison est que cela permet de bloquer temporairement l'évolution du premier de ces couples, lorsqu'une bombe ou un mur empêche un joueur de se déplacer.

Par exemple, lorsqu'un joueur en déplacement est bloqué par un mur, il ne peut plus avancer. Toutefois, si le mur est ensuite détruit, le joueur est débloqué et se remet en marche. Un moyen simple de mettre en œuvre ce comportement est de bloquer l'évolution temporelle de sa position. Toutefois, il est clair que cela ne doit pas pour autant bloquer celle de son état, faute de quoi un joueur invulnérable bloqué par un mur resterait invulnérable en tout cas jusqu'à la disparition de celui-ci.

1.2 Bombe

Une bombe est caractérisée par :

  • son propriétaire, c-à-d l'identité du joueur qui l'a déposée,
  • sa position sur le plateau (une case),
  • la longueur de sa mèche, c-à-d le nombre de coups d'horloge restants avant qu'elle n'explose,
  • la portée de l'explosion qu'elle produit, exprimée en nombre de cases.

Connaître le propriétaire d'une bombe est important, car le nombre de bombes appartenant à un joueur et présentes simultanément sur le plateau est limité.

A l'image d'un joueur, une bombe évolue au cours du temps, puisque sa mèche se consume progressivement jusqu'à l'explosion. Comme pour les autres éléments du jeu, et dans un soucis d'uniformité, cette évolution temporelle est représentée au moyen d'une séquence. Dès lors, la longueur de sa mèche est en réalité une séquence de longueurs.

Représenter la longueur de la mèche comme une séquence peut sembler inutilement complexe, puisqu'un simple compteur suffirait. Néanmoins, utiliser une séquence permet de mettre facilement en œuvre certains comportements intéressants. Par exemple, une bombe déficiente qui n'explose jamais est simplement une bombe dont la mèche est une séquence infinie. Même si nous ne représenterons pas de telles bombes dans le cadre de ce projet, avoir la possibilité de le faire facilement est bienvenu, d'autant que les bombes déficientes faisaient partie de la version originale de XBlast.

Lorsque sa mèche s'est totalement consumée, ou lorsqu'elle est atteinte par une explosion, une bombe explose, c-à-d qu'elle se transforme en explosion. Une explosion est un objet relativement complexe, qui mérite donc d'être examiné en détail.

1.3 Explosion

Une explosion est active durant un certain nombre de coups d'horloge. Durant cette période d'activité, elle émet ce que nous appellerons des particules d'explosion dans chacune des quatre directions. Chaque particule émise se déplace à la vitesse d'une case par coup d'horloge et, après avoir occupé un nombre de cases égal à la portée de l'explosion, disparaît1.

Ce processus est illustré graphiquement dans les images ci-dessous. Ces images montrent les particules (ou d'un sous-ensemble d'entre-elles) d'une explosion se trouvant au centre d'un plateau de 7 cases de côté. Cette explosion débute au coup d'horloge t, dure 3 coups d'horloge, et a une portée de 3 cases. Chaque particule est représentée par un cercle noir dans lequel figure une lettre permettant de la suivre, ainsi qu'un quart de disque bleu, qui montre sa direction de déplacement.

La figure 2 ci-dessous montre une seule particule émise par l'explosion, à savoir celle émise au temps t et se déplaçant vers l'est. Sa position sur le plateau est illustrée entre les coups d'horloge t et t + 2. Au coup d'horloge t + 3, elle disparaît car elle a alors occupé un total de trois cases.

Sorry, your browser does not support SVG.

Figure 2 : Particule émise au temps t et se déplaçant à l'est

La figure 3 ci-dessous illustre quant à elle ce que nous appellerons un bras de l'explosion. Une explosion est composée de quatre bras, un par direction, et cette image montre le bras est.

En plus de la particule a se déplaçant vers l'est, cette image montre les deux autres particules émises dans cette direction, b et c. La particule b est émise au coup d'horloge t + 1, la particule c au coup d'horloge t + 2. Comme nous l'avons vu, la particule a disparaît au temps t + 3, la particule b au temps t + 4 et la particule c au temps t + 5.

Sorry, your browser does not support SVG.

Figure 3 : Bras est de l'explosion, composé de 3 particules

Finalement, la figure 4 montre la totalité des particules émises par l'explosion, pour toute leur durée de vie. En plus des trois particules du bras est, déjà présentes dans l'image ci-dessus, celles des autres bras apparaissent également. Toutes les particules émises au même moment sont étiquetées avec la même lettre, mais le quart de disque bleu permet de les distinguer.

Notez qu'au coup d'horloge t il y a un total de quatre particules visibles, toutes portant l'étiquette a et occupant la même case, mais chacune se dirigeant dans une direction différente, ce qui explique le disque bleu plein. Au temps t + 1, ces quatre particules se sont déplacées d'une case dans leur direction, et occupent alors chacune une case différente. Par contre, les quatre nouvelles particules, portant l'étiquette b, occupent maintenant la case centrale simultanément.

Sorry, your browser does not support SVG.

Figure 4 : Totalité de l'explosion

L'évolution temporelle des explosions est assez sophistiquée, puisque d'une part les particules se déplacent, et d'autre part de nouvelles particules sont émises à chaque coup d'horloge pendant la durée de vie de l'explosion. Comme nous allons le voir, l'utilisation de séquences permet de représenter élégamment ces deux axes d'évolution temporelle.

Pour comprendre cela, intéressons-nous d'abord à la seule particule présentée sur la figure 2. Il devrait être assez clair qu'elle peut être représentée par une séquence de particules de longueur 3, dont chaque particule occupe une position différente.

Qu'en est-il d'un bras complet de l'explosion, tel que celui présenté à la figure 3 ? Sachant qu'un bras est une séquence de particules en déplacement, et qu'une particule en déplacement est une séquence de particules (fixes), un bras est une séquence de séquence de particules !

Finalement, étant donné qu'une explosion comporte quatre bras, elle se représente naturellement comme un quadruplet de séquences de séquences de particules…

2 Mise en œuvre Java

En plus des classes modélisant les concepts présentés plus haut, la classe ArgumentChecker, permettant de vérifier la validité des arguments, est à réaliser dans le cadre de cette étape. Elle est décrite en premier.

2.1 Classe ArgumentChecker

Lors de l'écriture d'un programme, la validation des arguments des méthodes est importante car elle permet de détecter rapidement les erreurs de programmation.

La bibliothèque Java fournit déjà une méthode de validation des arguments, à savoir la méthode requireNonNull de la classe Objects (avec un 's', à ne pas confondre avec la classe Object se trouvant à la racine de la hiérarchie d'héritage). Cette méthode prend un objet quelconque en argument, et vérifie qu'il est différent de null. Si tel est le cas, l'objet est retourné, sinon l'exception NullPointerException est levée.

La méthode requireNonNull est très utile dans les constructeurs, où elle permet de s'assurer qu'un objet reçu en argument n'est pas nul avant de le stocker dans un attribut. Par exemple, dans une classe Person possédant entre autres un attribut name, la méthode requireNonNull pourrait être utilisée ainsi dans le constructeur, pour s'assurer que le nom reçu n'est pas nul :

public final class Person {
  private final String name;
  // … autres attributs

  public Person(String name) {
    this.name = Objects.requireNonNull(name);
  }

  // … méthodes
}

L'utilisation de requireNonNull ici permet de garantir que l'exception NullPointerException est levée aussi tôt que possible, c-à-d au moment de la construction de l'instance de Person, et pas beaucoup plus tard lorsque l'attribut name est utilisé. Cela facilite grandement le diagnostic.

La classe ArgumentChecker a pour but de fournir une méthode similaire à requireNonNull mais pour les valeurs entières qui doivent être positives ou nulles. Cette classe appartient au paquetage ch.epfl.xblast, est publique, finale et non instanciable — c-à-d que son constructeur par défaut est privé et vide. Elle offre la méthode publique et statique suivante :

  • int requireNonNegative(int value), qui retourne la valeur donnée si elle est positive ou nulle, et lève l'exception IllegalArgumentException sinon.

2.2 Enumération PlayerID

L'énumération PlayerID du paquetage ch.epfl.xblast, publique, définit les identités des quatre joueurs, à savoir (dans l'ordre) : PLAYER_1, PLAYER_2, PLAYER_3, PLAYER_4.

2.3 Classe Sq

La classe Sq a été introduite lors de l'étape précédente, mais de nouvelles méthodes sont nécessaires à la réalisation de cette étape. Elles sont présentées ci-dessous.

2.3.1 Répétition

Il est parfois utile de construire une séquence finie consistant en la répétition d'un seul et même élément. Pour cela, la classe Sq offre la méthode statique repeat, qui prend en argument la longueur de la séquence à créer, et la valeur.

Par exemple, une séquence constituée de trois répétitions de la chaîne hip peut être construite ainsi :

Sq<String> hipHipHip = Sq.repeat(3, "hip");

La méthode repeat est très similaire à la méthode constant introduite lors de l'étape précédente, la seule différence étant qu'elle construit une séquence finie plutôt qu'infinie.

2.3.2 Itération

En mathématiques, une suite définie par récurrence est une suite définie par son premier terme et par une fonction définissant les termes suivants en fonction du ou des précédent(s).

Par exemple, la suite des puissances de deux peut se définir ainsi :

\begin{align*} u_1 &= 1\\ u_{n+1} &= 2\times u_n \end{align*}

Le premier élément de cette suite est 1, le second 2, le troisième 4, le quatrième 8, et ainsi de suite.

La méthode iterate de la classe Sq permet de créer facilement une séquence dont les éléments sont ceux d'une suite définie par récurrence. On lui passe deux arguments : la première valeur de la suite, et une fonction (une lambda de Java) qui, étant donné l'élément d'indice n, retourne celui d'indice n + 1. Ainsi, la séquence des puissances de deux peut s'obtenir au moyen de l'expression suivante :

Sq.iterate(1, u -> 2 * u)

2.3.3 Concaténation

Deux séquences peuvent être concaténées au moyen de la méthode concat. On l'applique à une séquence (généralement finie) en lui passant une autre séquence, et elle retourne la concaténation d'elle-même et de la séquence passée en argument.

Par exemple, l'extrait de programme ci-dessous construit, par concaténation, une séquence infinie dont les trois premiers éléments sont la chaîne hip et tous les suivants sont la chaîne hourra :

Sq<String> hipHipHip = Sq.repeat(3, "hip");
Sq<String> hourra = Sq.constant("hourra");
Sq<String> hipHipHipHourra = hipHipHip.concat(hourra);

2.3.4 Limitation

Il peut être utile de limiter la longueur d'une séquence à une valeur donnée, ce que permet la méthode limit. Elle prend en argument une longueur maximum (un entier non négatif) et retourne une séquence identique à celle à laquelle on l'applique, mais dont la longueur au plus celle donnée.

L'extrait de programme ci-dessous construit une première séquence, s1, qui est infinie et dont le premier élément est la chaîne vide, le second la chaîne bla, le troisième la chaîne blabla et ainsi de suite. La séquence s2, quant à elle, est finie et de longueur 3, et ne contient que les trois premiers éléments de s1.

Sq<String> s1 = Sq.iterate("", s -> "bla" + s);
Sq<String> s2 = s1.limit(3);

Notez que la méthode repeat présentée plus haut n'est rien d'autre qu'une combinaison de la méthode constant et de la méthode limit, puisqu'on a l'équivalence suivante :

Sq.repeat(n, v)  <=>  Sq.constant(v).limit(n)

2.4 Classe Bomb

La classe Bomb du paquetage ch.epfl.xblast.server, publique, finale et immuable, représente une bombe. Elle est dotée de deux constructeurs publics :

  • Bomb(PlayerID ownerId, Cell position, Sq<Integer> fuseLengths, int range), qui construit une bombe avec le propriétaire, la position, la séquence de longueurs de mèche et la portée donnés ; lève l'exception NullPointerException si l'un des trois premiers arguments est nul ; lève l'exception IllegalArgumentException si la séquence des longueurs de mèche est vide, ou si la portée est (strictement) négative.
  • Bomb(PlayerID ownerId, Cell position, int fuseLength, int range), qui construit une bombe avec le propriétaire, la position et la portée donnés, et la séquence de longueur de mèche (fuseLength, fuseLength - 1, fuseLength - 2, …, 1). Lève les mêmes exceptions que le constructeur précédent, dans les mêmes situations.

Le premier de ces deux constructeurs est le constructeur principal, dans le sens où le second ne fait rien d'autre que l'appeler au moyen de la notation this(…), avec les arguments adéquats.

En plus de ces constructeurs, la classe Bomb offre les méthodes publiques suivantes :

  • PlayerID ownerId(), qui retourne l'identité du propriétaire de la bombe,
  • Cell position(), qui retourne la position de la bombe,
  • Sq<Integer> fuseLengths(), qui retourne la séquence des longueurs de mèche de la bombe (présente et futures),
  • int fuseLength(), qui retourne la longueur de mèche actuelle, c-à-d le premier élément de la séquence retournée par la méthode précédente,
  • int range(), qui retourne la portée de la bombe,
  • List<Sq<Sq<Cell>>> explosion(), qui retourne l'explosion correspondant à la bombe, sous la forme d'un tableau de 4 éléments, chacun représentant un bras ; la durée de cette explosion est donnée par la constante Ticks.EXPLOSION_TICKS définie lors de l'étape précédente.

Notez qu'une particule d'explosion n'a aucune autre caractéristique que la case qu'elle occupe. Pour cette raison, plutôt que de créer une classe représentant une telle particule, nous utilisons directement la classe Cell.

2.4.1 Création de l'explosion

Pour faciliter l'écriture de la méthode explosion, il est conseillé d'écrire tout d'abord la méthode privée suivante :

  • Sq<Sq<Cell>> explosionArmTowards(Direction dir), qui retourne le bras de l'explosion se dirigeant dans la direction dir.

Cette méthode est très courte à écrire (2 à 3 lignes) mais pas totalement triviale. Inspirez-vous de l'expression ci-dessous, qui construit une séquence infinie de cases placées sur une ligne verticale, partant de la case (2, 3) et se dirigeant vers le sud :

Sq.iterate(new Cell(2, 3), c -> c.neighbor(Direction.S));

Les premiers éléments de cette séquence sont donc les cases de coordonnées :

(2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8) …

2.5 Classe Player.LifeState

La classe LifeState, publique, finale, immuable et imbriquée statiquement dans la classe Player (décrite plus bas), représente un couple (nombre de vies, état) du joueur.

L'état est décrit par l'énumération State, publique, imbriquée dans la classe LifeState et possédant les éléments suivants (dans l'ordre) :

  • INVULNERABLE, lorsque le joueur est invulnérable aux explosions, et ne peut donc pas perdre de vie,
  • VULNERABLE, lorsque le joueur est vulnérable (son état normal) et peut donc perdre une vie s'il est atteint par une explosion,
  • DYING, lorsque le joueur est mourant,
  • DEAD, lorsque le joueur a perdu toutes ses vies et ne participe donc plus au jeu.

La classe LifeState offre un unique constructeur public :

  • LifeState(int lives, State state), qui construit le couple (nombre de vies, état) avec les valeurs données, ou lève l'exception IllegalArgumentException si le nombre de vies est (strictement) négatif, ou l'exception NullPointerException si l'état est nul.

En plus du constructeur, la classe LifeState possède les méthodes d'accès suivantes, permettant d'obtenir la valeur de ses attributs :

  • int lives(), qui retourne le nombre de vies du couple.
  • State state(), qui retourne l'état.

Enfin, la classe LifeState offre un prédicat permettant de savoir si le joueur peut se déplacer :

  • boolean canMove(), qui retourne vrai si et seulement si l'état permet au joueur de se déplacer, ce qui est le cas uniquement s'il est invulnérable ou vulnérable.

2.6 Classe Player.DirectedPosition

La classe DirectedPosition, publique, finale, immuable et imbriquée statiquement dans la classe Player, représente la « position dirigée » d'un joueur, c-à-d une paire (sous-case, direction). La sous-case donne la position du joueur, tandis que la direction est celle dans laquelle il regarde.

La classe DirectedPosition offre deux méthodes publiques et statiques facilitant la construction de séquences de positions dirigées pour les cas fréquents, à savoir :

  • Sq<DirectedPosition> stopped(DirectedPosition p), qui retourne une séquence infinie composée uniquement de la position dirigée donnée et représentant un joueur arrêté dans cette position,
  • Sq<DirectedPosition> moving(DirectedPosition p), qui retourne une séquence infinie de positions dirigées représentant un joueur se déplaçant dans la direction dans laquelle il regarde ; le premier élément de cette séquence est la position dirigée donnée, le second a pour position la sous-case voisine de celle du premier élément dans la direction de regard, et ainsi de suite.

En plus de ces méthodes statiques, la classe DirectedPosition offre un unique constructeur public :

  • DirectedPosition(SubCell position, Direction direction), qui construit une position dirigée avec la position et la direction donnés, ou lève l'exception NullPointerException si l'un ou l'autre de ces arguments est nul.

Elle possède finalement les méthodes de consultation et de dérivation suivantes :

  • SubCell position(), qui retourne la position,
  • DirectedPosition withPosition(SubCell newPosition), qui retourne une position dirigée dont la position est celle donnée, et la direction est identique à celle du récepteur,
  • Direction direction(), qui retourne la direction de la position dirigée,
  • DirectedPosition withDirection(Direction newDirection), qui retourne une position dirigée dont la direction est celle donnée, et la position est identique à celle du récepteur.

2.7 Classe Player

La classe Player du paquetage ch.epfl.xblast.server, publique, finale et immuable, représente un joueur. Elle offre deux constructeurs publics :

  • Player(PlayerID id, Sq<LifeState> lifeStates, Sq<DirectedPosition> directedPos, int maxBombs, int bombRange), qui construit un joueur avec les attributs donnés ; lève l'exception NullPointerException si l'un des trois premiers arguments est nul, et l'exception IllegalArgumentException si l'un des deux derniers arguments est (strictement) négatif,
  • Player(PlayerID id, int lives, Cell position, int maxBombs, int bombRange), qui construit un joueur avec les attributs donnés, ou lève l'exception NullPointerException si l'identité ou la position sont nulles, ou l'exception IllegalArgumentException si le nombre de vies, le nombre maximum de bombes ou leur portée sont (strictement) négatifs ; la séquence d'états est telle que le joueur soit invulnérable durant Ticks.PLAYER_INVULNERABLE_TICKS coups d'horloge, puis vulnérable ensuite, tandis que la séquence de positions dirigées représente le joueur arrêté sur la sous-case centrale de la case donné, et regardant vers le sud.

Le premier de ces constructeurs est le constructeur principal, le second est un constructeur secondaire, qui appelle simplement le constructeur principal en lui passant les bons arguments.

En plus de ces constructeurs, la classe Player possède les méthodes publiques suivantes :

  • PlayerID id(), qui retourne l'identité du joueur,
  • Sq<LifeState> lifeStates(), qui retourne la séquence des couples (nombre de vies, état) du joueur,
  • LifeState lifeState(), qui retourne le couple (nombre de vies, état) actuel du joueur, c-à-d la tête de la séquence retournée par la méthode précédente,
  • Sq<LifeState> statesForNextLife(), qui retourne la séquence d'états pour la prochaine vie du joueur, qui commence par une période de longueur Ticks.PLAYER_DYING_TICKS durant laquelle le joueur est mourant, suivie soit d'un état de mort permanent si le joueur n'a plus de vie, soit d'une période d'invulnérabilité de longueur Ticks.PLAYER_INVULNERABLE_TICKS suivie d'une période de vulnérabilité permanente, avec une vie en moins qu'actuellement (le nombre de vies change après la période durant laquelle le joueur est mourant, pas avant),
  • int lives(), qui retourne le nombre de vies actuel du joueur,
  • boolean isAlive(), qui retourne vrai si et seulement si le joueur est vivant, c-à-d si son nombre de vies actuel est supérieur à 0,
  • Sq<DirectedPosition> directedPositions(), qui retourne la séquence des positions dirigées du joueur,
  • SubCell position(), qui retourne la position actuelle du joueur, c-à-d la position du premier couple de la séquence retournée par directedPositions,
  • Direction direction(), qui retourne la direction vers laquelle le joueur regarde actuellement, c-à-d la direction du premier couple de la séquence retournée par directedPositions,
  • int maxBombs(), qui retourne le nombre maximum de bombes que le joueur peut déposer,
  • Player withMaxBombs(int newMaxBombs), qui retourne un joueur identique à celui auquel on l'applique, si ce n'est que son nombre maximum de bombes est celui donné,
  • int bombRange(), qui retourne la portée (en nombre de cases) des explosions produites par les bombes du joueur,
  • Player withBombRange(int newBombRange), qui retourne un joueur identique à celui auquel on l'applique, si ce n'est que la portée de ses bombes est celle donnée,
  • Bomb newBomb(), qui retourne une bombe positionnée sur la case sur laquelle le joueur se trouve actuellement, dont la mèche a la longueur donnée par la constante Ticks.BOMB_FUSE_TICKS et dont la portée est celle des bombes du joueur.

2.7.1 Création des séquences d'état

Pour faciliter la création des séquences de couples (nombre de vies, état), il est conseillé d'ajouter une méthode statique et privée à la classe Player. Etant donné un nombre de vies, cette méthode retourne une séquence de couples (nombre de vies, état) représentant :

  • l'état de mort permanente, représenté par le rectangle en bas à droite de la figure 1, si le nombre de vies reçu est égal à 0,
  • l'état invulnérable/vulnérable représenté par les deux rectangles du haut de la figure 1, constitué d'un séquence initiale dans laquelle le joueur est invulnérable (de longueur Ticks.PLAYER_INVULNERABLE_TICKS), suivie d'une séquence infinie dans laquelle le joueur est vulnérable.

2.8 Tests

Comme pour l'étape précédente, nous vous fournissons uniquement une archive Zip à importer dans votre projet, qui contient le fichier de vérification de noms correspondant à cette étape. A vous d'écrire les tests unitaires si vous désirez en avoir, ce qui est vivement conseillé.

3 Résumé

Pour cette étape, vous devez :

  • écrire l'énumération PlayerID et les classes ArgumentChecker, Bomb, Player et leurs classes/énumérations imbriquées 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 11 mars 2016 à 17h30, via le système de rendu, afin d'obtenir les 3 points associés à ce rendu mineur.

Attention : n'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus et aucun retard, aussi insignifiant soit-il, ne sera toléré !

Notez que votre code n'a pas besoin de fonctionner ou d'être documenté pour être accepté par le système de rendu. La seule chose qui compte est que les classes et méthodes publiques de cette étape existent afin qu'aucune erreur n'apparaissent dans les fichiers de vérification de noms. Dès ce moment, vous pouvez rendre votre code et collecter les points du rendu mineur, ce que nous vous conseillons fortement de faire dès que possible. Rien ne vous empêche d'effectuer ensuite d'autre rendus pour la même étape au fur et à mesure de votre progression.

Notes de bas de page

1

Les explosions décrites ici constituent ce qui est souvent appelé un système de particules. Les systèmes de particules sont fréquemment utilisés dans les jeux pour représenter différentes entités évoluant de manière complexe au cours du temps.