Aller au contenu principal

6) Finalisation du jeu

Objectifs de la séance

  • Implémenter la détection de fin de partie et l'affichage du vainqueur.
  • Afficher les personnages éliminés différemment sur le plateau.
  • Stocker un historique des actions de la partie dans une nouvelle table SQLite.
  • Introduire les sessions PHP pour conserver de petites informations entre les requêtes sans passer par la base de données.
  • Finaliser la mise en forme de la page pour obtenir un jeu complet et jouable.

Notions théoriques

Ce qu'il reste à construire

À l'issue de la séance 5, le jeu permet de créer des personnages, de les déplacer et de les faire attaquer. Il lui manque trois choses pour être complet :

  1. La fin de partie n'est pas détectée : quand un personnage atteint 0 PV, le jeu continue.
  2. L'affichage général est fonctionnel mais brut. Quelques améliorations CSS et structurelles rendront le jeu réellement agréable à jouer.

Les sessions PHP

Une session PHP est un mécanisme qui permet de stocker de petites informations côté serveur, associées à un visiteur précis, entre plusieurs requêtes consécutives.

astuce

Il est possible de conserver l'historique des dernières actions entre les rechargements de page, sans avoir besoin de créer une table SQLite dédiée en utilisant les sessions PHP.

Contrairement à la base de données qui stocke les données durablement (elles survivent à un redémarrage du serveur), une session PHP est temporaire : elle disparaît quand le navigateur est fermé ou quand elle expire.

Pour utiliser les sessions, il faut appeler session_start() au tout début du script PHP, avant tout affichage.

Comment fonctionne l'échange de cookies ?

Schéma de fonctionnement des sessions PHP

Lorsqu'un visiteur accède à une page PHP qui appelle session_start(), PHP génère un identifiant de session unique (une chaîne aléatoire) et l'associe à un fichier temporaire sur le serveur où les données de session seront stockées.

PHP envoie ensuite un cookie HTTP au navigateur du visiteur, contenant cet identifiant de session. Le navigateur stocke ce cookie et le renvoie automatiquement à chaque requête suivante vers le même domaine.

Ainsi, à chaque nouvelle requête, PHP peut lire l'identifiant de session dans le cookie envoyé par le navigateur, retrouver le fichier de session correspondant sur le serveur, et charger les données associées à cette session dans le tableau $_SESSION.

PHP envoie alors un cookie au navigateur qui identifie la session.

<?php
session_start();

// Stocker une valeur
$_SESSION['message_precedent'] = "Mario a attaqué Lara.";

// Lire une valeur
if (isset($_SESSION['message_precedent'])) {
print $_SESSION['message_precedent'];
}

// Supprimer une valeur
unset($_SESSION['message_precedent']);

Stocker un historique dans SQLite

Pour un historique persistant (visible d'une session à l'autre), la base de données reste la solution appropriée.

Pour stocker les actions des joueurs, on va créer une table action qui enregistre chaque événement du jeu : qui a fait quoi, quand, avec quel résultat.

CREATE TABLE IF NOT EXISTS action (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tour INTEGER NOT NULL,
description TEXT NOT NULL,
date_action TEXT NOT NULL
)

La colonne date_action contiendra la date et l'heure au format texte, obtenues avec la fonction PHP date('Y-m-d H:i:s').

Gérer les tours de jeu

Un tour de jeu est simplement un compteur : chaque action du joueur incrémente ce compteur d'une unité.

On peut stocker ce compteur dans :

  • la session PHP (léger, éphémère)
  • ou dans la base de données (persistant entre fermetures du navigateur).

Pour ce projet, on utilisera la session : le numéro de tour est stocké dans $_SESSION['tour'] et incrémenté à chaque action.

La balise <s> pour les personnages éliminés

En HTML, la balise <s> affiche du texte barré. C'est le moyen standard d'indiquer qu'un contenu est annulé ou obsolète, sans le supprimer visuellement. On s'en servira pour indiquer qu'un personnage est éliminé dans sa cellule du plateau.

<s>Lara</s> <!-- affiche "Lara" barré -->

Test de mémorisation/compréhension


Par quel mécanisme technique PHP identifie-t-il concrètement la session d'un visiteur précis ?


A quel moment la fonction `session_start()` doit être appelée ?


Quelle instruction PHP est utilisée pour supprimer définitivement une information stockée en session ?


Quel est l'objectif principal de la création de la table `action` en SQLite, par rapport au stockage en session ?


Que signifie `INTEGER PRIMARY KEY AUTOINCREMENT` pour la colonne `id` ?


Comment la date et l'heure sont-elles formatées pour être insérées dans la colonne `date_action` ?


Quelle structure de contrôle est recommandée avant de lire une valeur dans `$_SESSION` (comme pour `$_POST`) ?



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

Finaliser le jeu de combat : détection de fin de partie, affichage amélioré et historique des actions.

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

Le dossier contient :

  • Personnage.php,
  • Guerrier.php,
  • Mage.php,
  • Database.php,
  • Jeu.php,
  • index.php,
  • styles.css,
  • plateau.php
  • et jeu.db.

À la fin de cette séance, le projet contiendra les modifications et ajouts suivants :

FichierStatutModifications
Personnage.phpinchangé
Guerrier.phpinchangé
Mage.phpinchangé
Database.phpmodifiétables action et partie, colonne joueur, méthodes de gestion du tour
Jeu.phpmodifiéverifierFinDePartie(), getJoueurCourant(), passerLeTour()
index.phpmodifiéidentification du joueur en session, vérification du tour, <meta refresh>, formulaires filtrés
styles.cssmodifiéajout de la classe .attente
plateau.phpinchangé
jeu.dbmodifiétables action et partie créées automatiquement

Étape 1 - Détecter la fin de partie

Ajouter dans la classe Jeu une méthode verifierFinDePartie().

Elle parcourt $this->personnages et collecte les personnages dont les PV sont supérieurs à 0. La fin de partie ne peut être déclarée que si au moins 2 personnages ont rejoint l'arène : sans adversaire, il n'y a pas de combat possible et donc pas de vainqueur.

Si la partie comptabilise 2 personnages ou plus et que le nombre de vivants tombe à 1 ou 0, la partie est terminée :

  • 1 vivant : ce personnage est le vainqueur.
  • 0 vivant : égalité totale.

La méthode retourne un tableau des vivants si la partie est terminée, ou null si elle est encore en cours (partie non commencée ou encore des combattants vivants).

Dans index.php, appeler cette méthode après chaque traitement d'action (juste avant la construction des listes $vivants, $magesVivants, etc.). Si elle retourne un résultat non nul, stocker le message de victoire dans $message et placer un indicateur $partieTerminee = true pour masquer les formulaires d'action dans la partie HTML.


Bonne pratique - Clauses de garde

Avant de tester une condition de fin, il faut s'assurer que les prérequis de cette condition sont réunis. Ici, la fin de partie n'a de sens qu'une fois qu'au moins 2 personnages s'affrontent. Vérifier count($this->personnages) < 2 en entrée de méthode est une clause de garde qui évite un faux positif dès la création du premier personnage.

Étape 2 - Enrichir l'affichage du plateau pour les personnages éliminés

Sur le plateau actuel, un personnage éliminé (PV ≤ 0) s'affiche de la même manière qu'un personnage vivant. On veut le distinguer visuellement.

Modifier la double boucle for dans la grille HTML pour traiter différemment un personnage éliminé :

  • Sa cellule reçoit la classe CSS "mort".
  • Son contenu affiche son nom barré (avec <s>) et la mention "Éliminé".
  • Les règles CSS pour .mort : fond gris, texte gris clair, opacité réduite.

Pour un personnage vivant, améliorer l'affichage en affichant clairement :

  • Son nom.
  • Ses PV courants et les PV maximum (par exemple "80 / 100 PV") : pour cela, stocker la vie maximum est nécessaire. Plutôt que de modifier les classes maintenant, afficher simplement les PV courants suivis d'une barre de progression HTML5 <progress>.

La balise <progress> est native en HTML5 :

<progress value="80" max="100"></progress>

Elle affiche une barre colorée proportionnelle au rapport valeur/maximum. Le problème est qu'on ne stocke pas encore $vieMax. Pour contourner cela proprement, utiliser les PV au moment de la création comme référence, qu'on peut lire depuis la base avec un SELECT dédié. Pour simplifier cette séance, on peut prendre une valeur fixe de 100 comme maximum (à améliorer en exercice complémentaire).


Bonne pratique - Différencier visuellement les états des objets

Différencier visuellement les états d'un objet (vivant/mort, disponible/bloqué) est une règle fondamentale d'ergonomie. Un joueur ne devrait jamais avoir à lire les PV pour savoir si un personnage est encore en jeu. La couleur et la mise en forme le disent immédiatement.

Étape 3 - Introduire les sessions pour le compteur de tour

Chaque action du joueur correspond à un tour. On veut afficher le numéro du tour courant et l'incrémenter à chaque action.

Ajouter session_start() au tout début de index.php (avant toute sortie HTML, idéalement en toute première ligne).

Initialiser le compteur de tour s'il n'existe pas encore dans la session :

if (!isset($_SESSION['tour'])) {
$_SESSION['tour'] = 1;
}

Dans chaque bloc de traitement d'action (création, déplacement, attaque), incrémenter le compteur avant de construire le message :

$_SESSION['tour'] = $_SESSION['tour'] + 1;

Afficher le numéro du tour courant dans la page, par exemple juste sous le titre principal.

Vérifier que le compteur est remis à zéro lors de la réinitialisation de la partie : dans le bloc if ($action == "reinitialiser"), ajouter $_SESSION['tour'] = 1;.


Bonne pratique - Initialiser les sessions

session_start() doit être la toute première instruction du fichier PHP, avant toute sortie vers le navigateur (y compris les espaces ou sauts de ligne avant <?php). Toute sortie avant l'envoi des en-têtes HTTP provoque l'erreur "Cannot modify header information - headers already sent".

Étape 4 - Ajouter une table action dans la base et enregistrer l'historique

On veut conserver un journal des actions de la partie, accessible même après fermeture du navigateur. Ce journal sera stocké dans une nouvelle table SQLite action.

Modifier la méthode creerTables() dans Database.php pour créer cette nouvelle table en plus de celle existante :

CREATE TABLE IF NOT EXISTS action (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tour INTEGER NOT NULL,
description TEXT NOT NULL,
date_action TEXT NOT NULL
)

Ajouter dans Database.php 2 méthodes :

  • ajouterAction($tour, $description) : insère une ligne dans la table action avec la date courante (date('Y-m-d H:i:s')).
  • chargerHistorique($limite) : retourne les $limite dernières actions, triées de la plus récente à la plus ancienne (ORDER BY id DESC LIMIT :limite).

Ajouter dans Jeu.php une méthode enregistrerAction($tour, $description) qui délègue à $this->database->ajouterAction(...). Ce passage par Jeu évite d'appeler Database directement depuis index.php.

Dans index.php, appeler $jeu->enregistrerAction(...) dans chaque bloc d'action, avec un message descriptif, après avoir construit la variable $message.


Bonne pratique - Utiliser un format de date standard pour les bases de données

Il est conseillé d'utiliser date('Y-m-d H:i:s') pour stocker les dates en base de données dans un format standard (ISO 8601). Ce format est trié correctement par ordre alphabétique, ce qui le rend compatible avec un tri SQL ORDER BY date_action. Éviter de stocker des dates dans des formats locaux (comme "14/04/2026") qui ne se trient pas correctement.

Étape 5 - Afficher l'historique des actions

Ajouter dans la partie HTML de index.php une section qui affiche les dix dernières actions de la partie, sous forme de liste ordonnée ou de tableau.

Charger l'historique depuis Jeu (qui délègue à Database) :

$historique = $jeu->getDatabase()->chargerHistorique(10);

Afficher chaque ligne avec le numéro de tour, l'heure et la description.

Cette section doit être masquée si l'historique est vide (par exemple au tout début d'une nouvelle partie).


Bonne pratique - Appliquer htmlspecialchars() à l'affichage de données textuelles

Il est conseillé d'appliquer htmlspecialchars() lors de l'affichage de données textuelles lues depuis la base, même si elles ont déjà été filtrées à l'insertion. Cette "défense en profondeur" garantit que même une donnée qui aurait échappé à la vérification initiale ne pourra pas provoquer d'injection HTML lors de l'affichage.

Étape 6 - Afficher un récapitulatif des personnages sous le plateau

Ajouter sous le plateau de jeu une section listant tous les personnages avec leurs statistiques complètes, y compris ceux qui ont été éliminés.

Pour chaque personnage, afficher :

  • son nom et son type ;
  • ses PV courants ;
  • pour un guerrier : son armure et sa frappe (dps) ;
  • pour un mage : sa mana et sa puissance de sort (dps) ;
  • son état : "Vivant" ou "Éliminé" selon ses PV.

Utiliser instanceof pour adapter l'affichage selon le type. Styler différemment les lignes des personnages éliminés (par exemple en gris, avec une opacité réduite).

Étape 7 - Afficher la bannière de fin de partie

Quand $partieTerminee est true, afficher une bannière visuelle de fin de partie en haut de la page, avec le nom du vainqueur et un texte d'invitation à recommencer.

Cette bannière doit être visuellement distincte du reste de la page : couleur de fond marquée, texte centré, taille de police plus grande.

Étape 8 - Vérification finale et tests complets

À ce stade, le jeu est complet. Cette étape consiste à le tester méthodiquement, scénario par scénario, pour s'assurer que tout fonctionne comme attendu.

Voici les scénarios à vérifier :

Scénario 1 - Partie de base

  1. Lancer le serveur et charger la page (http://localhost:8000).
  2. Si la base est vide, créer un guerrier et un mage avec des caractéristiques différentes.
  3. Vérifier que les 2 personnages s'affichent sur le plateau.
  4. Déplacer le guerrier vers le mage (plusieurs fois).
  5. Faire attaquer le guerrier sur le mage. Vérifier que les PV du mage diminuent.
  6. Faire attaquer le mage sur le guerrier (avec attaquer()). Vérifier que les dégâts correspondent à la puissance du sort.

Scénario 2 - Fin de partie

  1. Répéter les attaques jusqu'à ce qu'un personnage soit éliminé.
  2. Vérifier que la bannière de fin s'affiche avec le bon vainqueur.
  3. Vérifier que les formulaires d'action ont disparu.
  4. Cliquer sur "Nouvelle partie" et confirmer. Vérifier que la page revient à l'état initial.

Scénario 3 - Limites du plateau

  1. Essayer de déplacer un personnage au-delà du bord du plateau.
  2. Vérifier que le personnage reste sur sa case (le déplacement est annulé sans message d'erreur).

Scénario 4 - Persistance

  1. Créer des personnages et effectuer quelques actions.
  2. Fermer l'onglet du navigateur.
  3. Rouvrir http://localhost:8000. Vérifier que les personnages et l'historique sont toujours là.
  4. Vérifier que le compteur de tour repart de 1 (il est dans la session, qui a été effacée).

Bonne pratique - Tester les cas limites

Tester systématiquement les cas limites (bords du plateau, mana nulle, personnage déjà mort) est aussi important que de tester les cas classiques. Un jeu qui plante ou se comporte de manière incohérente dans des situations rares décourage les joueurs. Prévoir et gérer ces cas, même avec un simple "rien ne se passe", est une marque de qualité.


Solution complète

Solution complète du TP

Exercices complémentaires

Exercice A - Stocker la vie maximale pour une barre de progression exacte

La barre <progress> utilise actuellement max="100" comme valeur fixe. Si un personnage est créé avec 120 PV, la barre débordera (valeur supérieure au max).

Ajouter une colonne vie_max dans la table personnage de SQLite, initialisée au moment de la création et jamais modifiée ensuite. Mettre à jour les méthodes sauvegarderPersonnage et chargerPersonnages dans Database.php, ainsi que la classe Personnage (nouvelle propriété $vieMax sans setter public).

Utiliser $p->getVieMax() comme valeur max de la balise <progress>.

Exercice B - Limiter le nombre de personnages par joueur

Pour l'instant, chaque joueur peut créer autant de personnages qu'il veut. Ajouter une règle dans Jeu::creerPersonnage() : si le joueur possède déjà 2 personnages, refuser la création et retourner un message d'erreur.

Modifier la méthode pour qu'elle retourne null en cas de succès, ou une chaîne d'erreur si la limite est atteinte. Traiter ce retour dans index.php.

Exercice C - Surcharger __toString dans Mage pour l'affichage en barre de récapitulatif

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 dans le récapitulatif texte.

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é
session_start()Démarre ou reprend une session PHP. Doit être appelé avant tout affichage.
$_SESSIONTableau superglobal associé à la session du visiteur. Persiste entre les requêtes tant que la session est active.
unset($_SESSION['cle'])Supprime une valeur de la session.
date('Y-m-d H:i:s')Retourne la date et l'heure courantes au format SQL standard.
substr($s, $debut, $longueur)Extrait une sous-chaîne à partir d'une position donnée.
<progress value="" max="">Balise HTML5 pour afficher une barre de progression.
<s>texte</s>Affiche du texte barré en HTML.
Fin de partieVérifier après chaque action si le nombre de personnages vivants est ≤ 1.
Historique en baseStocker les événements dans une table action avec horodatage pour les rendre persistants.
Session vs baseSessions : légères, éphémères, liées au navigateur. Base de données : persistante, partagée entre tous les navigateurs.
Tour partagé en baseStocker le numéro de tour dans SQLite (table partie) le rend commun à tous les joueurs.
<meta http-equiv="refresh" content="3">Recharge la page automatiquement toutes les 3 secondes. Permet au joueur en attente de voir les actions de l'adversaire sans JavaScript.
Identification par sessionChaque joueur stocke son numéro (1 ou 2) dans $_SESSION['joueur']. La session est propre à chaque navigateur.

Bilan des 6 séances

À l'issue de ces six séances, le projet "Jeu de combat PHP" est une application web complète, jouable à 2 en réseau local :

FichierRôle
Personnage.phpClasse abstraite : modèle commun à tous les personnages
Guerrier.phpClasse fille : guerrier avec armure
Mage.phpClasse fille : mage avec mana et attaque par sort
Database.phpAccès à SQLite : CRUD personnages, historique, tour partagé
Jeu.phpLogique du jeu : actions, règles, validation, gestion du tour
index.phpInterface : identification du joueur, formulaires POST, <meta refresh>
styles.cssFeuille de styles CSS
plateau.phpAffichage de la grille HTML
jeu.dbBase de données SQLite (générée automatiquement)

Les notions abordées au fil des séances :

SéanceNotions clés
1Variables, fonctions, conditions, require_once, print
2Classes, private/public, accesseurs, héritage, abstract, __toString
3Classes filles, valeurs par défaut, instanceof, switch, boucles for, grille HTML
4SQLite, PDO, requêtes préparées, FETCH_ASSOC, persistance
5Formulaires POST, $_POST, isset(), intval(), htmlspecialchars(), classe Jeu
6Sessions $_SESSION, historique en base, fin de partie, tour partagé, <meta refresh>, date(), substr()