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 :
- assembler des classes existantes dans une classe qui orchestre une partie, sans y recopier leurs règles.
- lancer un programme PHP depuis le terminal et lire ce que l'utilisateur tape.
- centraliser la lecture des réponses du joueur dans deux méthodes, pour que le reste du jeu n'en dépende pas.
- concevoir deux classes à partir d'un diagramme,
RoometDungeon, 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.
Gamefabrique le héros, le donjon et le dé, donc les possède : losange plein {uml:composition}. Le0..1du 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 letake()du chapitre 5, avec plusieurs objets. Roomrend ce queGamedoit lui demander, rien de plus.monster()rendnulldans une salle vide.items()liste le sol pour que le joueur choisisse.take()retire l'objet choisi,drop()en repose un.Gamea 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 dansHero,Inventoryet 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
Herorefuse déjà un nom vide avec uneInvalidArgumentException. Ne réécrivez pas la règle : attrapez l'exception dansGameet 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'estGamequi la décide.echo $heroafficheArthur (30/30 PV)grâce à__toString(). Servez-vous-en partout.Gamegarde le héros dans une propriété privée, querun()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 = []). RelisezPolygon, au chapitre 8, pour le typearray: à vous de vérifier que chaque élément est unItem, et de lever uneInvalidArgumentExceptionsinon.describe()rend la phrase complète,Gamel'affiche. Une salle sans monstre dit « Personne. », une salle sans objet dit « Au sol : rien. » Les noms viennent de$item->name.isCleared()répondtruequand la salle n'a pas de monstre ou que celui-ci n'est plus en vie.Monstersait déjà répondre àisAlive().Dungeonconstruit ses trois salles dans son constructeur : l'Entrée avec un gobelin et la potion, le Couloir avec l'épée courte et uneWeapon('Masse du gardien', 18.0, 7), la Salle du trône avec le dragon.rooms()les rend dans l'ordre, etGameles parcourt avecforeach.
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 :
foreachsur les salles,whilesur 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).Gamemet 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
matchsur le numéro choisi, comme lematchdeRarity::multiplier(). Chaque cas appelle une méthode privée deGame. Lematchreste court, les méthodes portent le détail. fgets(STDIN)rend une chaîne.ctype_digit($answer)vauttruesi elle ne contient que des chiffres. Alors seulement,(int) $answerla transforme en nombre :(int) 'quatre'vaudrait0.
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é partakeDamage(). Les trois méthodes du contratFightersuffisent, dans un sens puis dans l'autre.Gameaffiche le montant et l'objet touché, avececho $monsteretecho $hero. - Pas d'
echodansHero::attack()ni danstakeDamage(). Ces méthodes sont testées, et un test n'affiche rien. C'estGamequi 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é
$overdu diagramme est faite pour cela. Une exception à vous,HeroDiedException, attrapée dansrun(), est l'autre façon. Choisissez, et dites pourquoi dans leREADME.
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,InventoryFullExceptionremonte : attrapez-la dansGame, affichez son message, et rendez l'objet à la salle avecdrop(). - Un
Weaponramassé peut être tenu tout de suite avecequip(). « Équiper une arme » fait la même chose avec une arme déjà dans le sac.Gameutiliseinstanceof Weaponpour décider ce qu'elle propose. Le calcul des dégâts reste dansHero. - Jeter, c'est le chemin inverse de ramasser :
remove()du sac,drop()dans la salle. L'arme en main ne se jette pas :Heron'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 : àGamede ne proposer que des potions du sac, trouvées par unforeachsur l'inventaire et uninstanceof Potion.- Boire à pleine santé ne rend rien et fait quand même disparaître la potion. Un message évite l'erreur :
isFullHealthest 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
0ou quandfgets()rendfalse. Trois fins, une seule méthode de bilan, avec la raison en paramètre. - Le sac s'affiche avec
foreachetecho '- ', $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 decount($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.
Gamerecalcule-t-elle des points de vie, un poids ou des dégâts d'arme ? Supprimez le calcul et appelez la méthode deHero,InventoryouWeapon.- 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.
- 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.
- Le programme se termine-t-il toujours par le bilan ? Pas d'
exit(), pas dedie(). Les boucles s'arrêtent,run()se termine, le script aussi. composer testest-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
Dicepour choisir le monstre de chaque salle,nullcompris, un autre pour le butin. - La rareté qui compte. Une
Weaponlégendaire frappe plus fort :$this->rarity->multiplier()existe déjà. Placez le calcul dansWeapon, appelé parHero::attack(). Le chapitre 3 dit pourquoi ce n'est pas le travail deGame. - Sauvegarder et reprendre.
JsonSerializablesur le héros et l'inventaire, un fichiersave.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
RoometDungeon. Ajouteztests/RoomTest.php: une salle sans monstre est franchie d'office,take()retire bien l'objet etdrop()le remet, un élément qui n'est pas unItemest refusé. Trois tests, dans le style de ceux du chapitre 2.
8. Ce que vous montrez
- Votre fork poussé, CI verte, avec
src/Game.php,src/Room.php,src/Dungeon.phpetbin/play.php. - Votre diagramme, sur papier ou en photo dans le dépôt, cohérent avec les trois classes rendues.
- Une section « Jouer » dans le
READMEdu fork : la commande, les règles, deux ou trois décisions de conception en une phrase chacune. - 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.
GameappelleHero,Inventoryet les monstres. Les règles restent chez elles, etGameraconte 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
Room::monster()rendnulldans une salle vide. CommentGames'en sort-elle sansif ($monster !== null)partout ?Gamepossè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.- Votre
matchsur le choix du joueur tient-il en moins de dix lignes ? Sinon, qu'est-ce qui devrait partir dans une méthode ? - Si demain le jeu se joue dans une page web au lieu du terminal, quelles méthodes de
Gamechangent ? Si la réponse est « toutes », relisez la section 4.