Aller au contenu principal

3) Enrichir le jeu

Ajouter le mage et le plateau

Plateau de jeu

Objectifs de la séance

  • Créer une deuxième classe fille, Mage, avec ses propres mécaniques (mana, sorts offensifs).
  • Modifier une classe existante (Personnage) pour y ajouter des propriétés sans casser le code existant.
  • Utiliser les valeurs par défaut de paramètres pour rendre des arguments facultatifs.
  • Ajouter une méthode de déplacement avec la structure switch.
  • Afficher un plateau de jeu sous forme de grille HTML avec des boucles for imbriquées.
  • Utiliser instanceof pour adapter le comportement selon le type d'un objet.
  • Parcourir un tableau d'objets avec foreach.

Notions théoriques

Faire évoluer une classe existante sans tout casser

Jusqu'ici, la classe Personnage ne connaît que trois informations : nom, vie et DPS.

Pour ce jeu, on a besoin que les personnages occupent une position sur un plateau.

On va donc ajouter deux propriétés $x et $y à la classe Personnage.

attention

Le défi quand on modifie une classe existante est de ne pas casser le code qui l'utilise déjà. Si l'on ajoute $x et $y comme paramètres obligatoires du constructeur, toutes les lignes existantes du type new Guerrier("Lara", 100, 10, 5) cesseront de fonctionner.

astuce

La solution : les valeurs par défaut de paramètres. Un paramètre peut avoir une valeur par défaut, ce qui le rend facultatif à l'appel. Si on ne le précise pas, la valeur par défaut est utilisée.

public function __construct($nom, $vie, $dps, $x = 0, $y = 0) {
// ...
}

Avec cette signature, les deux appels suivants sont valides :

new Guerrier("Lara", 100, 10, 5); // x=0, y=0 par défaut
new Guerrier("Lara", 100, 10, 5, 2, 3); // x=2, y=3 explicitement
info

Les paramètres avec une valeur par défaut doivent toujours être placés à la fin de la liste. PHP interdirait d'écrire function __construct($x = 0, $nom, $vie) car un paramètre obligatoire ne peut pas suivre un paramètre facultatif.

La structure switch

Quand on doit comparer une même variable à plusieurs valeurs possibles, la structure switch est plus lisible qu'une série de if / else if.

$direction = "haut";

switch ($direction) {
case "haut":
$this->y = $this->y - 1;
break;
case "bas":
$this->y = $this->y + 1;
break;
case "gauche":
$this->x = $this->x - 1;
break;
case "droite":
$this->x = $this->x + 1;
break;
}

Chaque case se termine par break. Sans ce mot-clé, PHP continuerait à exécuter les case suivants même si la condition ne correspond pas. Ce comportement involontaire est l'une des erreurs les plus courantes avec switch.

Les boucles for imbriquées pour parcourir une grille

Un plateau de jeu est une grille à deux dimensions : des lignes et des colonnes. Pour parcourir toutes les cases, on utilise deux boucles for imbriquées : la boucle externe parcourt les lignes, la boucle interne parcourt les colonnes.

$taille = 5;

for ($ligne = 0; $ligne < $taille; $ligne++) {
for ($colonne = 0; $colonne < $taille; $colonne++) {
// On est sur la case ($colonne, $ligne)
}
}

La convention adoptée dans ce projet : $x représente la colonne (axe horizontal), et $y représente la ligne (axe vertical). La case en haut à gauche a pour coordonnées (0, 0). Aller vers la droite augmente $x, aller vers le bas augmente $y.

L'opérateur instanceof

instanceof permet de vérifier de quelle classe est un objet. C'est utile quand on manipule une collection d'objets de types différents (des guerriers et des mages mélangés) et qu'on veut adapter le comportement selon le type.

if ($personnage instanceof Guerrier) {
// ce personnage est un guerrier
}

if ($personnage instanceof Mage) {
// ce personnage est un mage
}

instanceof fonctionne aussi avec l'héritage : si Guerrier hérite de Personnage, alors un guerrier est à la fois une instance de Guerrier et une instance de Personnage.

Parcourir un tableau d'objets avec foreach

Quand plusieurs personnages sont stockés dans un tableau, foreach permet de les parcourir un par un sans avoir à gérer un compteur manuellement.

$personnages = [$p1, $p2, $p3];

foreach ($personnages as $p) {
print "<p>" . $p . "</p>";
}

On peut aussi récupérer l'indice de chaque élément avec la syntaxe as $index => $p :

foreach ($personnages as $index => $p) {
print "<p>Personnage numéro " . $index . " : " . $p->getNom() . "</p>";
}

Test de mémorisation/compréhension


Quel est le principal risque si on ajoute de nouveaux paramètres obligatoires au constructeur d'une classe existante ?


Quelle est la syntaxe correcte pour définir un paramètre facultatif avec une valeur par défaut en PHP ?


Pour quelle raison PHP interdit de commencer par un paramètre facultatif, comme par exemple `function __construct($x = 0, $nom)` ?


Dans quel cas précis la structure `switch` est-elle plus appropriée qu'une suite de `if` ?


Que se passe-t-il concrètement si on oublie le mot-clé `break` à la fin d'un `case` dans une structure `switch` ?


Pourquoi utilise-t-on 2 boucles `for` imbriquées pour représenter le plateau de jeu ?


Dans notre projet, que représente exactement la variable `$x` ?


Dans notre projet, quelles sont les coordonnées de la case située en haut à gauche du plateau ?


Dans notre projet, quelle action a pour conséquence d'augmenter la valeur de `$y` ?


Dans quel contexte l'utilisation de l'opérateur `instanceof` est-elle justifiée ?


Quel est l'avantage principal de la boucle `foreach` pour parcourir un tableau d'objets ?


Quelle est la syntaxe exacte décrite pour récupérer à la fois l'indice et la valeur d'un élément dans une boucle `foreach` ?


Dans notre code du `switch`, quelle opération mathématique est effectuée si la direction est "gauche" ?



TP pour réfléchir et résoudre des problèmes

Le mage, les coordonnées et le plateau

Ce TP reprend le projet issu de la séance 2.

Le dossier contient 3 fichiers :

  • Personnage.php
  • Guerrier.php
  • index.php

À la fin de cette séance, le projet contiendra les fichiers suivants :

FichierStatutDescription
Personnage.phpmodifiéajout de $x, $y, deplacer(), accesseurs
Guerrier.phpmodifiéconstructeur mis à jour pour transmettre $x, $y
Mage.phpnouveauclasse fille avec mana et attaque par sort
index.phpmodifiéplateau de jeu, grille HTML, affichage des personnages
Rappel : $dps et $mana dans Personnage

Les deux propriétés communes à tous les personnages ont été nommées $dps et $mana, en référence au vocabulaire des jeux de rôle numériques :

  • $dps (damage per second) : la puissance d'attaque, qu'il s'agisse d'un coup d'épée ou d'un sort.
  • $mana : la ressource défensive — l'armure pour un guerrier, la réserve magique pour un mage.

Ces noms auraient pu s'appeler $puissance et $protection, ce qui aurait été sémantiquement correct. Le choix de $dps et $mana est délibéré : ils sont courts, reconnaissables par tout développeur familier des jeux vidéo, et évitent toute confusion entre "protection" au sens défensif et "encapsulation" (protéger les données avec private).

Étape 1 - Créer la classe Mage (structure de base)

Créer un nouveau fichier Mage.php dans le dossier du projet.

La classe Mage hérite de Personnage. Elle n'ajoute aucune propriété supplémentaire : la mana maximale est toujours 100, inutile de la stocker.

La mana courante est stockée dans la propriété $mana héritée de Personnage. Les méthodes getMana() et setMana() sont héritées directement — pas besoin de les réécrire.

La puissance d'attaque est portée par $dps, également hérité. Le mage lance ses sorts avec la même propriété que le guerrier frappe : getDps() renvoie la puissance dans les deux cas.

Écrire le constructeur __construct($nom, $vie, $dps, $mana) :

  1. Appeler parent::__construct($nom, $vie, $dps, $mana) pour initialiser toutes les propriétés héritées.

Pour tester, modifier index.php pour créer un mage et l'afficher :

require_once "Mage.php";
$mage = new Mage("Mario", 80, 25, 100);
print $mage;
// attendu : "Le personnage Mario a 80 points de vie"

Étape 2 - Ajouter la méthode attaquer

Ajouter dans la classe Mage une méthode attaquer($cible) qui surcharge celle héritée de Personnage.

Un mage n'attaque pas en frappant, il lance un sort. Sa méthode attaquer inflige des dégâts directement égaux à ses $dps (la puissance de son sort), sans réduction par le mana de la cible.

Elle doit :

  1. Infliger des dégâts à la cible égaux à $this->getDps(), en utilisant setVie et getVie.

Tester dans index.php :

  1. Créer un guerrier et un mage.
  2. Afficher les points de vie du guerrier avant l'attaque.
  3. Faire attaquer le guerrier par le mage.
  4. Afficher les points de vie du guerrier après l'attaque.
  5. Faire attaquer le guerrier par le guerrier aussi, et constater que les dégâts infligés sont différents (le guerrier soustrait le mana de la cible, le mage inflige ses $dps directement).

Polymorphisme

$mage->attaquer($guerrier) et $guerrier->attaquer($guerrier) appellent la même méthode attaquer(), mais le résultat est différent : le mage utilise getDps() directement (sort sans résistance), le guerrier utilise getDps() réduit par le mana de la cible. C'est le polymorphisme : un même nom de méthode, des comportements différents selon la classe de l'objet qui l'exécute.

Étape 3 - Ajouter les coordonnées à la classe Personnage

On va maintenant modifier la classe Personnage pour que chaque personnage ait une position sur le plateau, représentée par deux coordonnées entières $x (colonne) et $y (ligne).

Ouvrir Personnage.php et effectuer les modifications suivantes.

1. Ajouter deux propriétés privées dans le bloc des déclarations de propriétés :

private $x;
private $y;

2. Modifier le constructeur pour qu'il accepte $x et $y avec des valeurs par défaut à 0. Les ajouter à la fin de la liste des paramètres, après $mana :

public function __construct($nom, $vie, $dps, $mana, $x = 0, $y = 0) {

Ajouter dans le corps du constructeur les deux initialisations :

$this->setX($x);
$this->setY($y);

3. Ajouter les quatre accesseurs setX, getX, setY, getY. Les setters vérifient que la valeur est numérique avec is_numeric().

Vérifier que index.php fonctionne toujours sans modification : les appels existants new Guerrier("Lara", 100, 10, 5) doivent continuer à fonctionner, car $x et $y ont des valeurs par défaut.


Bonne pratique - Compatibilité ascendante

Quand on ajoute des paramètres à un constructeur déjà utilisé, toujours les rendre facultatifs (avec une valeur par défaut) pour ne pas casser le code existant. C'est une règle fondamentale de la compatibilité ascendante : le code qui fonctionnait avant doit continuer à fonctionner après la modification.

Étape 4 - Mettre à jour les constructeurs de Guerrier et Mage

Maintenant que Personnage accepte $x et $y, il faut que Guerrier et Mage puissent transmettre ces coordonnées au constructeur parent.

Ouvrir Guerrier.php et modifier le constructeur :

  • Ajouter $x = 0 et $y = 0 à la fin de la liste des paramètres (après $mana).
  • Transmettre $mana, $x et $y à parent::__construct.

Faire de même dans Mage.php :

  • Ajouter $x = 0 et $y = 0 à la fin (après $mana).
  • Transmettre $x et $y à parent::__construct.

Tester en créant un guerrier et un mage avec des coordonnées explicites, puis en affichant leurs positions :

$g = new Guerrier("Lara", 100, 10, 5, 1, 2);
$m = new Mage("Mario", 80, 25, 100, 3, 0);

print "<p>" . $g->getNom() . " est en (" . $g->getX() . ", " . $g->getY() . ")</p>";
print "<p>" . $m->getNom() . " est en (" . $m->getX() . ", " . $m->getY() . ")</p>";

Bonne pratique - Cohérence des signatures de constructeur

L'ordre des paramètres dans le constructeur d'une classe fille doit être cohérent avec celui du parent. Ici, $x et $y arrivent en dernier dans Personnage, Guerrier et Mage. Cette uniformité évite les confusions quand on lit le code ou qu'on passe d'une classe à l'autre.

Étape 5 - Ajouter la méthode deplacer dans Personnage

Ajouter dans la classe Personnage une méthode deplacer($direction) qui modifie les coordonnées du personnage en fonction d'une chaîne de caractères représentant la direction.

Les quatre directions possibles sont "haut", "bas", "gauche" et "droite".

Rappel de la convention de coordonnées :

  • "droite" : $x augmente de 1 ;
  • "gauche" : $x diminue de 1 ;
  • "bas" : $y augmente de 1 (sur un écran, l'axe Y pointe vers le bas) ;
  • "haut" : $y diminue de 1.

Utiliser une structure switch pour gérer les quatre cas. Si la direction est inconnue, ne rien faire (pas de default nécessaire).

Tester dans index.php : créer un guerrier en (2, 2), le déplacer plusieurs fois dans différentes directions, et afficher sa position après chaque déplacement.


Bonne pratique - Toujours terminer les case d'un switch par break

Dans un switch, chaque case doit se terminer par break. Sans ce mot-clé, PHP continue d'exécuter le bloc du case suivant, même si sa condition ne correspond pas. Ce comportement (appelé "fall-through") est rarement voulu et provoque des bugs discrets et difficiles à identifier.

Étape 6 - Afficher le plateau sous forme de grille HTML

C'est l'étape centrale de cette séance. On va construire un plateau de jeu de 5×5 cases et y afficher les personnages à leur position respective.

La structure HTML d'une grille est un tableau <table> composé de lignes <tr> et de cellules <td>.

L'algorithme est le suivant :

  1. Pour chaque ligne $ligne de 0 à 4 :
    • Ouvrir une balise <tr>.
    • Pour chaque colonne $colonne de 0 à 4 :
      • Initialiser deux variables $contenu = "" et $classe = "".
      • Parcourir le tableau $personnages avec foreach : si un personnage a getX() == $colonne et getY() == $ligne, remplir $contenu avec son nom et ses PV, et $classe avec son type ("guerrier" ou "mage") en utilisant instanceof.
      • Afficher la cellule <td class="...">...</td> avec le contenu et la classe.
    • Fermer la balise </tr>.

Ajouter dans le <head> un bloc <style> pour que les cellules aient une taille fixe, une bordure visible, et des couleurs différentes selon la classe CSS (rouge clair pour le guerrier, bleu clair pour le mage).

Placer un guerrier en (0, 0) et un mage en (4, 4), puis vérifier qu'ils apparaissent aux bons emplacements dans la grille.


Bonne pratique - Séparation des préoccupations

Il est conseillé de séparer la logique (PHP) de la présentation (CSS). Les couleurs et dimensions des cellules sont définies dans le bloc <style>, pas dans les instructions print PHP. On se contente d'ajouter une classe CSS (guerrier ou mage) sur la cellule depuis PHP, et c'est le CSS qui décide de la couleur. Si l'on veut changer la couleur du guerrier demain, on modifie une seule ligne dans le <style>, pas une dizaine de print dispersés dans le code.

Étape 7 - Afficher les coordonnées dans l'en-tête du plateau

Pour rendre la grille plus lisible, ajouter une ligne d'en-tête (numéros de colonnes) et une colonne d'en-tête (numéros de lignes) au tableau HTML.

En HTML, les cellules d'en-tête utilisent la balise <th> au lieu de <td>.

Modifier la boucle d'affichage du plateau pour :

  1. Ajouter une première ligne spéciale avant la boucle des lignes, qui contient une cellule vide (coin supérieur gauche) suivie des numéros de colonnes (0, 1, 2, 3, 4) dans des <th>.
  2. Au début de chaque ligne, ajouter une cellule <th> avec le numéro de la ligne.

L'aspect attendu :

0 1 2 3 4
0 [ ] [ ] [ ] [ ] [ ]
1 [ ] [ ] [ ] [ ] [ ]
2 [ ] [ ] [ ] [ ] [ ]
3 [ ] [ ] [ ] [ ] [ ]
4 [ ] [ ] [ ] [ ] [ M ]

Bonne pratique - Séparation des préoccupations

Les numéros de ligne et de colonne n'ont de sens qu'en développement pour déboguer les positions. Dans un jeu final, on pourrait les supprimer ou les masquer avec du CSS. Concevoir le code de manière à pouvoir facilement enlever ou modifier ces éléments de débogage est une bonne habitude.

Étape 8 - Déplacer les personnages et observer le plateau

On va simuler quelques déplacements et vérifier visuellement que les personnages se déplacent correctement sur le plateau.

Dans index.php, après la création des personnages et avant l'affichage du plateau :

  1. Déplacer le guerrier deux fois vers la droite et une fois vers le bas.
  2. Déplacer le mage deux fois vers la gauche et une fois vers le haut.
  3. Afficher le plateau.
  4. Vérifier que les positions correspondent aux déplacements effectués.

Ajouter ensuite un test de cas limite : tenter de déplacer le guerrier vers la gauche depuis la colonne 0. Observer que les coordonnées deviennent négatives ($x = -1). Pour le moment, aucune vérification n'empêche un personnage de sortir du plateau. Ce comportement sera géré proprement lors de la séance 5, quand la classe Jeu sera créée.


Bonne pratique - Documenter les comportements non gérés

Identifier et documenter les comportements non gérés est aussi important qu'écrire du code qui fonctionne. Ici, on sait que les coordonnées négatives sont possibles et qu'elles seront gérées plus tard. Ne pas le noter, c'est risquer de l'oublier.

Étape 9 - Enrichir l'affichage de la grille selon le type de personnage

Pour rendre le plateau plus informatif, adapter le contenu affiché dans chaque cellule selon le type du personnage qui s'y trouve.

  • Pour un guerrier : afficher son nom, ses PV, et son mana.
  • Pour un mage : afficher son nom, ses PV, et sa mana courante.

Modifier la partie foreach à l'intérieur de la double boucle for pour utiliser instanceof et construire un $contenu différent selon le type.

Le rendu attendu dans une cellule de guerrier :

Lara
100 PV
mana : 5

Le rendu attendu dans une cellule de mage :

Mario
80 PV
mana : 80/100

Bonne pratique - Utiliser instanceof pour adapter le comportement selon le type

L'opérateur instanceof est l'outil approprié pour adapter un comportement à la volée selon le type d'un objet, sans avoir à ajouter une propriété $type dans les classes. Cependant, si cette logique conditionnelle se multiplie dans le code, c'est souvent le signe qu'il vaudrait mieux déplacer le comportement dans une méthode de la classe elle-même (par exemple une méthode afficherInfos() surchargée dans chaque classe fille).


Solution complète

Solution complète du TP

Exercices complémentaires

Exercice A - Ajouter un troisième personnage sur le plateau

Créer un deuxième guerrier nommé "Aragorn" avec les caractéristiques de votre choix, et le placer en (2, 2) (au centre du plateau).

Vérifier qu'il s'affiche correctement sur la grille, avec sa propre couleur.

Pour s'entraîner avec instanceof, tester ce que renvoie $aragorn instanceof Personnage : un guerrier est-il également une instance de Personnage ? Afficher le résultat avec var_dump() et expliquer pourquoi.

Exercice B - Faire se rapprocher deux personnages

Écrire une fonction rapprocher($p1, $p2) dans un fichier fonctions.php.

Cette fonction compare les coordonnées de $p1 et de $p2, et déplace $p1 d'une case dans la direction qui le rapproche le plus de $p2 (en priorité sur l'axe horizontal, puis vertical si les colonnes sont identiques).

Afficher le plateau après chaque appel à rapprocher pour visualiser le déplacement.

Exercice C - Surcharger __toString dans Mage

La méthode __toString héritée de Personnage affiche le nom et les points de vie. Pour le mage, on voudrait aussi voir sa mana.

Ajouter dans la classe Mage une méthode __toString() qui renvoie :

Le mage Mario a 80 points de vie et 100/100 de mana

Vérifier que print $mage dans index.php utilise bien la nouvelle méthode, et que print $guerrier utilise toujours celle de Personnage.


Ce qu'il faut retenir

NotionRésumé
Valeurs par défautUn paramètre avec = valeur est facultatif à l'appel. Toujours en dernier.
switchStructure de contrôle pour comparer une variable à plusieurs valeurs. Chaque case se termine par break.
Boucles for imbriquéesDeux boucles for l'une dans l'autre permettent de parcourir une grille 2D.
foreachParcourt un tableau sans gestion manuelle d'indice. Syntaxe as $valeur ou as $indice => $valeur.
instanceofVérifie si un objet est une instance d'une classe donnée. Remonte la chaîne d'héritage.
.=Opérateur de concaténation et affectation : $a .= "texte" équivaut à $a = $a . "texte".
Tableau HTML<table>, <tr>, <td>, <th> permettent d'afficher une grille dans le navigateur.

Aperçu de la prochaine séance

Pour l'instant, les données du jeu (les personnages, leurs positions, leurs points de vie) disparaissent à chaque rechargement de la page. PHP reconstruit tout à partir de zéro à chaque requête.

La séance 4 introduira les bases de données SQLite pour stocker ces informations de manière persistante. On apprendra à créer une table, à y insérer des données, et à les relire lors du prochain chargement. C'est la condition nécessaire pour que le jeu puisse se dérouler sur plusieurs requêtes consécutives.