Aller au contenu

Jouer au Donjon : la classe Game

À la maison · ~4 périodes

Depuis le chapitre 1, vous construisez un donjon pièce par pièce : un héros, des monstres, des objets, un inventaire. Tout cela vit dans votre dépôt de katas, et personne n'y a encore joué. Ce chapitre assemble les briques en un vrai jeu, dans le terminal : un nom de héros, des salles, des combats coup par coup, du butin à choisir, une victoire ou une mort.

Vous recevez le plan, les étapes et ce que l'écran doit afficher. Le code, c'est vous.

À la fin de ce chapitre, vous serez capables de :

  1. assembler des classes existantes dans une classe qui orchestre une partie, sans y recopier leurs règles.
  2. lancer un programme PHP depuis le terminal et lire ce que l'utilisateur tape.
  3. centraliser la lecture des réponses du joueur dans deux méthodes, pour que le reste du jeu n'en dépende pas.
  4. concevoir deux classes à partir d'un diagramme, Room et Dungeon, puis les relier au reste.

Le travail se fait dans votre fork de https://github.com/opmvpc/poo-katas-26, niveau 4 vert. Vous ajoutez des fichiers, vous ne touchez ni aux tests ni aux classes existantes : composer test reste vert du début à la fin.

1. Ce que vous allez construire

Voici une partie complète, jouée avec le code de la correction. Vos textes peuvent varier, le menu non : il ne propose que ce qui est possible, et chaque action garde son numéro.

=== Le Donjon ===
Comment s'appelle votre héros ? Arthur
Bienvenue, Arthur (30/30 PV). 3 salles vous séparent de la sortie.

--- Entrée ---
Un Gobelin (5/5 PV) vous barre le passage. Au sol : Potion de soin.
Arthur (30/30 PV) · en main : mains nues · sac : 0/20 kg, 0 objet
  1. Attaquer
  2. Ramasser un objet
  0. Quitter
Votre choix : 1
Arthur frappe le Gobelin : 4 dégâts. Gobelin (1/5 PV)
Le Gobelin frappe Arthur : 6 dégâts. Arthur (24/30 PV)
Arthur (24/30 PV) · en main : mains nues · sac : 0/20 kg, 0 objet
  1. Attaquer
  2. Ramasser un objet
  0. Quitter
Votre choix : 1
Arthur frappe le Gobelin : 5 dégâts. Gobelin (0/5 PV)
Le Gobelin tombe.
Arthur (24/30 PV) · en main : mains nues · sac : 0/20 kg, 0 objet
  2. Ramasser un objet
  6. Avancer
  0. Quitter
Votre choix : 2
Vous ramassez : Potion de soin : potion (0.5 kg, +5 PV)
Arthur (24/30 PV) · en main : mains nues · sac : 0.5/20 kg, 1 objet
  3. Boire une potion
  5. Jeter un objet
  6. Avancer
  0. Quitter
Votre choix : 6

--- Couloir ---
Personne. Au sol : Épée courte, Masse du gardien.
Arthur (24/30 PV) · en main : mains nues · sac : 0.5/20 kg, 1 objet
  2. Ramasser un objet
  3. Boire une potion
  5. Jeter un objet
  6. Avancer
  0. Quitter
Votre choix : 2
Ramasser quoi ?
  1. Épée courte : arme (2 kg, 5 dégâts)
  2. Masse du gardien : arme (18 kg, 7 dégâts)
  0. Rien
Votre choix : 1
Vous ramassez : Épée courte : arme (2 kg, 5 dégâts)
L'équiper ? (o/n) o
Vous tenez maintenant : Épée courte
Arthur (24/30 PV) · en main : Épée courte · sac : 2.5/20 kg, 2 objets
  2. Ramasser un objet
  3. Boire une potion
  5. Jeter un objet
  6. Avancer
  0. Quitter
Votre choix : 2
Le sac refuse : "Masse du gardien" ne rentre pas : le sac ne porte que 20 kg.
Arthur (24/30 PV) · en main : Épée courte · sac : 2.5/20 kg, 2 objets
  2. Ramasser un objet
  3. Boire une potion
  5. Jeter un objet
  6. Avancer
  0. Quitter
Votre choix : 5
Vous posez au sol : Potion de soin
Arthur (24/30 PV) · en main : Épée courte · sac : 2/20 kg, 1 objet
  2. Ramasser un objet
  6. Avancer
  0. Quitter
Votre choix : 2
Ramasser quoi ?
  1. Masse du gardien : arme (18 kg, 7 dégâts)
  2. Potion de soin : potion (0.5 kg, +5 PV)
  0. Rien
Votre choix : 1
Vous ramassez : Masse du gardien : arme (18 kg, 7 dégâts)
L'équiper ? (o/n) o
Vous tenez maintenant : Masse du gardien
Arthur (24/30 PV) · en main : Masse du gardien · sac : 20/20 kg, 2 objets
  2. Ramasser un objet
  4. Équiper une arme
  5. Jeter un objet
  6. Avancer
  0. Quitter
Votre choix : 5
Vous posez au sol : Épée courte
Arthur (24/30 PV) · en main : Masse du gardien · sac : 18/20 kg, 1 objet
  2. Ramasser un objet
  6. Avancer
  0. Quitter
Votre choix : 6

--- Salle du trône ---
Un Dragon (30/30 PV) vous barre le passage. Au sol : rien.
Arthur (24/30 PV) · en main : Masse du gardien · sac : 18/20 kg, 1 objet
  1. Attaquer
  0. Quitter
Votre choix : 1
Arthur frappe le Dragon : 14 dégâts. Dragon (16/30 PV)
Le Dragon frappe Arthur : 11 dégâts. Arthur (13/30 PV)
Arthur (13/30 PV) · en main : Masse du gardien · sac : 18/20 kg, 1 objet
  1. Attaquer
  0. Quitter
Votre choix : 1
Arthur frappe le Dragon : 15 dégâts. Dragon (1/30 PV)
Le Dragon frappe Arthur : 10 dégâts. Arthur (3/30 PV)
Arthur (3/30 PV) · en main : Masse du gardien · sac : 18/20 kg, 1 objet
  1. Attaquer
  0. Quitter
Votre choix : 1
Arthur frappe le Dragon : 10 dégâts. Dragon (0/30 PV)
Le Dragon tombe.
Arthur (3/30 PV) · en main : Masse du gardien · sac : 18/20 kg, 1 objet
  6. Avancer
  0. Quitter
Votre choix : 6

=== Victoire ===
Arthur (3/30 PV) sort du Donjon après 3 salles, avec 1 objet dans son sac :
- Masse du gardien : arme (18 kg, 7 dégâts)

Sept actions, toujours les mêmes numéros : 1 attaquer, 2 ramasser, 3 boire, 4 équiper, 5 jeter, 6 avancer, 0 quitter. Une action absente du menu est une action impossible dans l'état courant.

2. Ce que vous avez déjà

Presque tout. Relisez les signatures dans src/ avant d'écrire une ligne : la moitié du travail consiste à appeler ce qui existe.

Brique Ce qu'elle vous donne Chapitre
Hero new Hero('Arthur', 30), ->inventory, ->weapon, ->equip(Weapon), ->drink(Potion), ->attack(), ->takeDamage(int), ->isAlive(), ->isFullHealth, echo $hero 1, 3, 5, 6, 7
Goblin, Dragon new Goblin() a 5 PV et frappe à 2, new Dragon() a 30 PV et frappe à 8. echo $monster donne Gobelin (5/5 PV) 6
Weapon, Potion new Weapon('Épée courte', 2.0, 5), new Potion('Potion de soin', 0.5, 5), ->name, ->weight, echo $item donne Épée courte : arme (2 kg, 5 dégâts) 6, 7
Inventory ->add(Item), qui lève InventoryFullException au-delà de ->maxWeight, ->remove(string), ->totalWeight(), count($inventory), foreach 5, 6, 7
Dice Dice::d6(), ->roll() 1, 3
Fighter, HasHealth, Rarity le contrat de combat, la santé, la rareté 7

Quatre pièces restent à écrire. Une salle avec ce qu'elle contient. Un donjon qui enchaîne les salles. Une classe Game qui mène la partie. Un script qui la lance. Room et Dungeon ont été dessinées au chapitre 5, jamais codées : c'est le moment.

3. Le plan

Un diagramme avant le code, comme au chapitre 4. Gardez les noms et les signatures publiques. Les méthodes privées de Game sont une proposition : une par action du menu, plus celles qui lisent le clavier.

@startuml
class Game {
  -hero: Hero [0..1]
  -dungeon: Dungeon [1]
  -dice: Dice [1]
  -over: bool
  -roomsCleared: int
  +run(): void
  -ask(question: string): string
  -askChoice(options: array): int
  -play(room: Room): void
  -fight(room: Room): void
  -pickUp(room: Room): void
  -drinkPotion(): void
  -equipWeapon(): void
  -dropItem(room: Room): void
  -report(reason: string): void
}
class Dungeon {
  -rooms: Room [1..*]
  +rooms(): Room [1..*]
}
class Room {
  +name: string
  -monster: Monster [0..1]
  -items: Item [0..*]
  +describe(): string
  +isCleared(): bool
  +monster(): Monster [0..1]
  +items(): Item [0..*]
  +take(name: string): Item
  +drop(item: Item): void
}
class Hero
class Dice
abstract class Monster <<abstract>>
abstract class Item <<abstract>>
Game "1 -game" *-- "0..1 -hero" Hero : dirige >
Game "1 -game" *-- "1 -dungeon" Dungeon : parcourt >
Game "1 -game" *-- "1 -dice" Dice : lance >
Dungeon "1 -dungeon" *-- "1..* -rooms" Room : enchaîne >
Room "0..1 -room" o-- "0..1 -monster" Monster : abrite >
Room "0..1 -room" o-- "0..* -items" Item : expose >
@enduml

Quatre décisions à lire dans ce diagramme.

  • Game fabrique le héros, le donjon et le dé, donc les possède : losange plein {uml:composition}. Le 0..1 du héros dit que la partie existe un instant sans lui, le temps de demander son nom.
  • Une salle garde un monstre et des objets sans contrôler leur durée de vie : losange vide {uml:agregation}. Le monstre existe avant d'être placé, l'objet quitte la salle pour le sac, et peut y revenir. C'est le drop() et le take() du chapitre 5, avec plusieurs objets.
  • Room rend ce que Game doit lui demander, rien de plus. monster() rend null dans une salle vide. items() liste le sol pour que le joueur choisisse. take() retire l'objet choisi, drop() en repose un.
  • Game a trois collaborateurs : le héros, le donjon, le dé. Elle demande, elle affiche, elle enchaîne. Elle ne calcule ni points de vie, ni poids, ni dégâts d'arme : ces règles vivent dans Hero, Inventory et les monstres.

Dessinez votre version sur papier, vingt minutes, avant d'ouvrir l'éditeur.

4. Lancer PHP depuis le terminal

Jusqu'ici, vos classes tournaient dans des tests. Un jeu se lance à la main. Créez bin/play.php à la racine du dépôt :

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

(new Dungeon\Game())->run();

La ligne require charge l'autoload de Composer, comme au chapitre 2 : Dungeon\Game se trouve tout seul dans src/Game.php. La dernière ligne construit la partie et la lance.

php bin/play.php
Fatal error: Uncaught Error: Class "Dungeon\Game" not found in .../bin/play.php:7

C'est normal, Game n'existe pas encore : l'étape 1 la crée. Lancez toujours le jeu avec cette commande. Un script composer play ne transmet pas le clavier au programme sous Windows.

Deux outils de PHP suffisent pour dialoguer. STDIN est le flux dans lequel PHP reçoit ce que l'utilisateur tape. fgets(STDIN) attend une ligne et Entrée, puis renvoie la ligne avec son saut de ligne à la fin, d'où un trim() systématique. Quand l'entrée est fermée, fgets() renvoie false : traitez ce cas comme un abandon. echo avec PHP_EOL affiche. La fonction readline() fait presque la même chose avec une invite, mais elle n'est pas installée partout : gardez fgets(STDIN).

Astuce

Écrivez deux méthodes privées dès le début. La première pose une question et rend la réponse nettoyée. La seconde affiche un menu numéroté et redemande tant que le numéro n'existe pas. Tout le jeu passe par elles : le jour où les réponses viendront d'ailleurs, il n'y aura que ces deux endroits à changer.

5. Les étapes

Étape 1 : le script et le nom

But : php bin/play.php demande un nom, construit un Hero et le salue.

=== Le Donjon ===
Comment s'appelle votre héros ? Arthur
Bienvenue, Arthur (30/30 PV).

Indices

  • Hero refuse déjà un nom vide avec une InvalidArgumentException. Ne réécrivez pas la règle : attrapez l'exception dans Game et reposez la question.
  • new Hero($name, 30) : un héros de 10 PV meurt au premier coup du dragon, qui frappe à 8 plus le dé. La difficulté, c'est Game qui la décide.
  • echo $hero affiche Arthur (30/30 PV) grâce à __toString(). Servez-vous-en partout.
  • Game garde le héros dans une propriété privée, que run() remplit après la lecture d'un nom valide.

Piège : écrire les six étapes avant de lancer le jeu une seule fois. Une partie complète prend une minute, lancez-la après chaque étape.

Étape 2 : les salles

But : src/Room.php et src/Dungeon.php existent, et le jeu décrit la première salle.

--- Entrée ---
Un Gobelin (5/5 PV) vous barre le passage. Au sol : Potion de soin.

Indices

  • __construct(string $name, ?Monster $monster = null, array $items = []). Relisez Polygon, au chapitre 8, pour le type array : à vous de vérifier que chaque élément est un Item, et de lever une InvalidArgumentException sinon.
  • describe() rend la phrase complète, Game l'affiche. Une salle sans monstre dit « Personne. », une salle sans objet dit « Au sol : rien. » Les noms viennent de $item->name.
  • isCleared() répond true quand la salle n'a pas de monstre ou que celui-ci n'est plus en vie. Monster sait déjà répondre à isAlive().
  • Dungeon construit ses trois salles dans son constructeur : l'Entrée avec un gobelin et la potion, le Couloir avec l'épée courte et une Weapon('Masse du gardien', 18.0, 7), la Salle du trône avec le dragon. rooms() les rend dans l'ordre, et Game les parcourt avec foreach.

Piège : donner à Room une méthode qui rend les points de vie du monstre pour que Game les regarde. Game n'a besoin du monstre que pour le combat. isCleared() répond au reste.

Étape 3 : la boucle et le menu

But : le jeu enchaîne les salles, affiche une ligne d'état et un menu à chaque tour, et exécute le choix.

Arthur (30/30 PV) · en main : mains nues · sac : 0/20 kg, 0 objet
  1. Attaquer
  2. Ramasser un objet
  0. Quitter
Votre choix : 9
Ce choix n'existe pas.
Votre choix :

Indices

  • Deux boucles emboîtées : foreach sur les salles, while sur les tours d'une salle. La boucle intérieure s'arrête quand le joueur avance ou quitte, ou quand le héros meurt.
  • La ligne d'état vient entièrement d'objets qui existent : echo $hero, $hero->weapon?->name, $hero->inventory->totalWeight(), count($hero->inventory). Game met les mots autour.
  • Le menu se construit à partir de l'état, avant l'affichage. « Attaquer » si un monstre est vivant, « Ramasser » s'il reste un objet au sol, « Boire » s'il y a une potion dans le sac, « Équiper » s'il y a dans le sac une arme qu'on ne tient pas, « Jeter » s'il y a autre chose que l'arme en main, « Avancer » quand la salle est franchie. Chaque action garde son numéro.
  • Un match sur le numéro choisi, comme le match de Rarity::multiplier(). Chaque cas appelle une méthode privée de Game. Le match reste court, les méthodes portent le détail.
  • fgets(STDIN) rend une chaîne. ctype_digit($answer) vaut true si elle ne contient que des chiffres. Alors seulement, (int) $answer la transforme en nombre : (int) 'quatre' vaudrait 0.

Piège : exit() ou die() pour quitter. Ces deux mots arrêtent le script sur-le-champ, sans rien exécuter ensuite : le bilan de l'étape 6 ne s'affichera jamais. La boucle doit se terminer proprement.

Étape 4 : le combat, un tour à la fois

But : « Attaquer » joue un tour. Le héros frappe, et si le monstre tient encore debout, il répond. Chaque coup dit ses dégâts et l'état de celui qui les reçoit.

Votre choix : 1
Arthur frappe le Gobelin : 4 dégâts. Gobelin (1/5 PV)
Le Gobelin frappe Arthur : 6 dégâts. Arthur (24/30 PV)
Votre choix : 1
Arthur frappe le Gobelin : 5 dégâts. Gobelin (0/5 PV)
Le Gobelin tombe.

Les dégâts dépendent du dé : vos chiffres seront différents. Le texte, lui, ne change pas.

Indices

  • La règle d'un coup est celle de Battle : attack() plus un lancer de dé, encaissé par takeDamage(). Les trois méthodes du contrat Fighter suffisent, dans un sens puis dans l'autre. Game affiche le montant et l'objet touché, avec echo $monster et echo $hero.
  • Pas d'echo dans Hero::attack() ni dans takeDamage(). Ces méthodes sont testées, et un test n'affiche rien. C'est Game qui raconte, avec les valeurs que les objets lui rendent.
  • Le dé se crée une fois, dans le constructeur de Game, et sert à tous les coups. Dice::d6() rend le jeu jouable. Dice::d20() rend chaque coup imprévisible.
  • Si le héros tombe, la partie est finie : sortez des deux boucles. La propriété $over du diagramme est faite pour cela. Une exception à vous, HeroDiedException, attrapée dans run(), est l'autre façon. Choisissez, et dites pourquoi dans le README.

Piège : afficher les points de vie du monstre en les recalculant dans Game. echo $monster le fait déjà, et il dit Gobelin (0/5 PV).

Étape 5 : le butin

But : ramasser, équiper, jeter, boire.

Votre choix : 2
Ramasser quoi ?
  1. Épée courte : arme (2 kg, 5 dégâts)
  2. Masse du gardien : arme (18 kg, 7 dégâts)
  0. Rien
Votre choix : 1
Vous ramassez : Épée courte : arme (2 kg, 5 dégâts)
L'équiper ? (o/n) o
Vous tenez maintenant : Épée courte
Votre choix : 2
Le sac refuse : "Masse du gardien" ne rentre pas : le sac ne porte que 20 kg.
Votre choix : 5
Vous posez au sol : Potion de soin

Indices

  • Quand plusieurs objets sont possibles, un sous-menu les liste avec echo $item, plus « 0. Rien ». Quand un seul est possible, pas de question. Écrivez ce sous-menu une fois, il sert aux quatre actions du butin.
  • Ramasser, c'est retirer l'objet de la salle puis l'ajouter au sac : Room::take() d'un côté, $this->hero->inventory->add() de l'autre. Si le sac refuse, InventoryFullException remonte : attrapez-la dans Game, affichez son message, et rendez l'objet à la salle avec drop().
  • Un Weapon ramassé peut être tenu tout de suite avec equip(). « Équiper une arme » fait la même chose avec une arme déjà dans le sac. Game utilise instanceof Weapon pour décider ce qu'elle propose. Le calcul des dégâts reste dans Hero.
  • Jeter, c'est le chemin inverse de ramasser : remove() du sac, drop() dans la salle. L'arme en main ne se jette pas : Hero n'a pas de méthode pour la lâcher, et c'est très bien ainsi.
  • Hero::drink() soigne, puis retire du sac la première potion de ce nom. Si elle n'y est pas, le héros se soigne quand même : à Game de ne proposer que des potions du sac, trouvées par un foreach sur l'inventaire et un instanceof Potion.
  • Boire à pleine santé ne rend rien et fait quand même disparaître la potion. Un message évite l'erreur : isFullHealth est déjà là.

Piège : un nom en dur, 'Potion de soin', dans Game. Le jour où le donjon contient une autre potion, le jeu ne la verra pas. Cherchez par type, pas par nom.

Étape 6 : la fin

But : la partie se termine toujours par un bilan.

=== Victoire ===
Arthur (3/30 PV) sort du Donjon après 3 salles, avec 1 objet dans son sac :
- Masse du gardien : arme (18 kg, 7 dégâts)

Indices

  • Victoire quand la boucle des salles se termine avec un héros en vie. Défaite quand le héros tombe. Abandon quand le joueur choisit 0 ou quand fgets() rend false. Trois fins, une seule méthode de bilan, avec la raison en paramètre.
  • Le sac s'affiche avec foreach et echo '- ', $item, comme l'inventaire du chapitre 7. count() donne le nombre d'objets.
  • Comptez les salles terminées dans Game, au moment d'avancer : c'est la seule information que personne d'autre ne connaît. Le nombre de salles de l'accueil, lui, vient de count($this->dungeon->rooms()).

6. Les règles du jeu, pour vous

Le résultat compte, la façon aussi. La faute la plus fréquente, et sa correction :

// Game recalcule ce que Hero sait déjà : à supprimer.
$hp = $this->hero->hp - $damage;
echo "Arthur ($hp/{$this->hero->maxHp} PV)", PHP_EOL;

// Game demande, Hero répond.
$this->hero->takeDamage($damage);
echo $this->hero, PHP_EOL;

Relisez votre Game avec ces cinq questions.

  1. Game recalcule-t-elle des points de vie, un poids ou des dégâts d'arme ? Supprimez le calcul et appelez la méthode de Hero, Inventory ou Weapon.
  2. Chaque exception est-elle attrapée là où vous pouvez y répondre ? Un nom vide se redemande, un sac plein s'annonce. Aucune trace PHP ne doit atteindre le joueur.
  3. Le menu ment-il ? Proposer « Attaquer » dans une salle vide ou « Boire » sans potion, c'est un bug d'affichage. Les options se construisent à partir de l'état.
  4. Le programme se termine-t-il toujours par le bilan ? Pas d'exit(), pas de die(). Les boucles s'arrêtent, run() se termine, le script aussi.
  5. composer test est-il toujours vert ? Vous avez ajouté trois classes et un script, rien cassé. Poussez, et regardez l'onglet Actions.

7. Pour aller plus loin

À faire seulement quand une partie complète se joue de bout en bout.

  • Le combat automatique. Si vous avez fait le niveau 5, Battle::fight() joue tout un combat d'un coup et rend le vainqueur. Ajoutez une action « Combattre jusqu'au bout » qui l'appelle, à côté du tour par tour.
  • Un donjon différent à chaque partie. Un Dice pour choisir le monstre de chaque salle, null compris, un autre pour le butin.
  • La rareté qui compte. Une Weapon légendaire frappe plus fort : $this->rarity->multiplier() existe déjà. Placez le calcul dans Weapon, appelé par Hero::attack(). Le chapitre 3 dit pourquoi ce n'est pas le travail de Game.
  • Sauvegarder et reprendre. JsonSerializable sur le héros et l'inventaire, un fichier save.json, un menu « Reprendre ou nouvelle partie ». L'exercice 2 du chapitre 7 est la moitié du chemin. Reconstruire les objets depuis le fichier est l'autre moitié, la plus longue.
  • Tester Room et Dungeon. Ajoutez tests/RoomTest.php : une salle sans monstre est franchie d'office, take() retire bien l'objet et drop() le remet, un élément qui n'est pas un Item est refusé. Trois tests, dans le style de ceux du chapitre 2.

8. Ce que vous montrez

  1. Votre fork poussé, CI verte, avec src/Game.php, src/Room.php, src/Dungeon.php et bin/play.php.
  2. Votre diagramme, sur papier ou en photo dans le dépôt, cohérent avec les trois classes rendues.
  3. Une section « Jouer » dans le README du fork : la commande, les règles, deux ou trois décisions de conception en une phrase chacune.
  4. Une partie jouée devant un binôme au début de la séance suivante. Il tape, vous regardez.
Ce qui est regardé Ce qui fait dire que c'est acquis
Le lancement php bin/play.php demande un nom et affiche la première salle
Le modèle Room et Dungeon existent, Game ne recalcule rien que le Donjon sait déjà faire
La partie du nom du héros jusqu'au bilan, coup par coup, sans trace d'erreur, avec un menu qui ne ment jamais
Les tests existants composer test vert, CI verte

À retenir

  • Une classe qui orchestre ne recalcule rien. Game appelle Hero, Inventory et les monstres. Les règles restent chez elles, et Game raconte ce qu'elles lui rendent.
  • Deux méthodes privées lisent le terminal, et le reste du jeu reçoit des réponses déjà nettoyées.
  • Un menu se construit à partir de l'état, il ne se tape pas en dur.
  • Une exception se traite là où vous pouvez y répondre, et jamais avec une trace PHP devant le joueur.
  • Dessinez le diagramme d'abord, vingt minutes sur papier, puis codez dans l'ordre des étapes.

Exercices

Sur papier

  1. Room::monster() rend null dans une salle vide. Comment Game s'en sort-elle sans if ($monster !== null) partout ?
  2. Game possède le héros, une salle n'en possède pas le monstre. Retrouvez les deux losanges dans votre diagramme et dites, en une phrase chacun, pourquoi.
  3. Votre match sur le choix du joueur tient-il en moins de dix lignes ? Sinon, qu'est-ce qui devrait partir dans une méthode ?
  4. Si demain le jeu se joue dans une page web au lieu du terminal, quelles méthodes de Game changent ? Si la réponse est « toutes », relisez la section 4.