Voir le flux RSS

rawsrc

[Actualité] [POO] : Gestion d'une partie de tennis en PHP et en objet

Note : 3 votes pour une moyenne de 5,00.
par , 10/01/2020 à 00h48 (914 Affichages)
Salut les développeurs,

comme nous sommes en début d'année, je vous souhaite tout plein de bonnes choses pour 2020 avec une tonne de chouettes imprévus.
Et si dans la sempiternelle liste des bonnes résolutions, il y avait l'envie de se mettre à la programmation objet, ça tombe bien que vous lisiez ce billet car ça va être principalement l'objet de la présente publication.

1 - INTRODUCTION

L'idée de ce billet m'est venue simplement suite à une discussion sur le forum PHP où un apprenant essayait avec son code de suivre le déroulé d'un match de tennis.
A priori, et au pifomètre on se dit que c'est du gâteau, et puis les règles du tennis sont tellement enfantines !
Avant de lire plus en avant ce billet, je vous invite donc à vous pencher un peu dessus... Et revenir quand vous aurez pondu une soluce de génie
Vous verrez, on se rend très vite compte que c'est loin d'être trivial et que suivre correctement le déroulé d'une partie de tennis et des scores est loin de se faire en un claquement de doigts. D'ailleurs, je trouve que c'est un super exercice d'école.

Dans les échanges sur le forum, très rapidement les codes postés commençaient à avoir des variables nommées : $jeux11, $jeux21, $jeux13, $jeux23, $newpoints13 avec des chaînages de if à n'en plus finir. Quand on arrive à ce genre de nommage et de code spaghetti, c'est que la fin du développeur est proche. L’expérience vous apprendra que le code est foutu même en le documentant à mort. Et quand bien même, cela resterait accessible, cela dénote un énorme problème de modélisation.

2 - POO - PROGRAMMATION ORIENTÉE OBJET

A un moment, le code classique à base de fonctions avec une tonne de variables montre ses limites tant au niveau du codage que de la lisibilité et même de la compréhension. Il est alors nécessaire d'avoir à disposition la possibilité de conceptualiser à un niveau supérieur. C'est là que rentre en jeu la Programmation Orientée Objet ou POO. La POO apporte ce niveau d'abstraction qui de par ses fondations ouvre une infinité de voies. La POO poursuit plusieurs buts :

  • découpage,
  • organisation,
  • généricité,
  • réutilisation

Citation Envoyé par rawsrc
L’utilité principale de la programmation objet réside dans la possibilité de représenter des éléments tangibles sous forme de concepts abstraits (équivalent à une représentation purement informatique). Source.
Donc, les principes élémentaires de la POO vont nous permettre de diviser notre problématique en concepts distincts autonomes. C'est la mise en œuvre directe du principe phare en informatique : diviser pour mieux régner.

Il va de soi que ve billet n'a pas pour vocation de mettre à plat toute la théorie de la programmation orientée objet mais juste de vous la faire découvrir par l'entremise d'une résolution garantie 100 % objet, d'une problématique réelle. Je vous invite quand même à aller vous documenter sur les aspects de base pour ne pas trop être largué.

3 - OBJECTIF

L'objectif est de créer un programme en PHP nous permettant de suivre une partie de tennis tout en respectant les règles du jeu en vigueur à aujourd'hui.

Cet outil devra
  • suivre le déroulé de la partie
  • suivre les scores

Par déroulé de la partie, j'entends le fait que l'on n'ait pas à se soucier de l'ouverture des sets, des jeux, des jeux décisifs (tie-break) et des scores, etc.
Comme les règles de ce sport sont parfaitement connues : le programme devra les suivre scrupuleusement et être totalement autonome sur tous ces aspects.

L'utilisateur ne doit rentrer que les points gagnants. Pour tout le reste cela doit être automatiquement géré.

Là, je sens que vous vous dites : "la vache, ça pique !" C'est ambitieux mais largement faisable. Et puis si c'est juste pour coder une calculette 4 opérations, autant rester couché, non ?

4 - ANALYSE DE LA PROBLÉMATIQUE

Tout l'analyse primaire démarre à partir des règles du tennis extraites à partir du site www.artengo.fr :

Citation Envoyé par www.artengo.fr
Le décompte des points, au tennis, un jeu se compose de 4 points.

On compte les points du serveur en premier.

Pas de point : « zéro »
Premier point : « 15 »
Deuxième point : « 30 »
Troisième point : « 40 »
Quatrième point : « jeu »

Si les 2 joueurs ont marqué 3 points, alors on compte « 40A ».

Après « 40A », le point suivant se note « Avantage » pour le joueur qui le gagne. Si le même joueur gagne un autre point alors il gagne le « jeu ». Sinon les deux joueurs repartent à « égalité » (soit « 40A »).

Pour gagner un match il faut gagner 2 sets (ou manche).

Un set, correspond à 6 jeux. Lorsqu'il y a « 5-5 », il faut aller jusqu’à 7 jeux. S’il y a « 6-6 » on réalise alors un « jeu décisif » (tie break).
Le « jeu décisif » se compte différemment des autres jeux. En effet, la marque des points est compté « zéro », « 1 », « 2 », « 3 », … jusqu'à 7.
Le premier joueur allant à 7 points remporte le « jeu décisif » et le set, à condition d’avoir 2 points d’écart sur son adversaire.
S’il y a « 6-6 », alors le jeu décisif continuera jusqu'à ce qu'il y ait 2 points d’écart.
A ces règles, on va ajouter :
- qu'une partie se joue qu'en 3 ou 5 sets

5 - ABSTRACTION DES RÈGLES DU JEU ET CONCEPTUALISATION OBJET

Là, on se jette à l'eau et on commence à décortiquer ce qu'il ressort des règles.

- Une partie se joue avec 2 joueurs
- Une partie se décompose en sets
- Un set se décompose en jeux

A ce stade, on peut déjà isoler sous forme de concepts (class) les éléments essentiels au jeu du tennis :
  • Un joueur ⇒ class Joueur,
  • Une partie ⇒ class Partie,
  • Un set ⇒ class Set,
  • Un jeu ⇒ class Jeu

A ce stade, on ne connait pas encore précisément ce que fera chaque concept, mais on a commencé à découper notre problématique en éléments abstraits délimités. D'ores-et-déjà se dessinent les liens qui vont exister entre tous ces concepts... Je vais enfoncer une porte ouverte (même avec de l'élan) mais pour résoudre un problème, quel qu'il soit, il faut commencer par le dégrossir. Cette étape est essentielle, ça s'appelle tout simplement : la conception.

Faites bien attention dans le découpage de votre problématique à ne pas trop vous limiter ou à l'inverse trop diluer les concepts. Il est important que chaque concept reste cohérent et ne soit ni trop limité, ni trop généraliste. S'il est trop limité, vous allez avoir un empilement de concepts qui ne feront pas grand chose et à l'inverse si c'est trop généraliste, vous allez vous retrouver avec des concepts qui feront même le café... Chaque classe ne doit pas avoir trop de responsabilités ou à l'inverse trop peu.
Pour arriver à ne pas tomber dans ces travers, il n'y a qu'une seule solution : la pratique, encore et toujours en y mettant en plus les mains dans le cambouis.



On ne va pas paraphraser les règles mais juste préciser plus logiquement certains aspects qui ont été présentés de manière plus littéraire :
Un set est remporté par le premier joueur à cumuler 6 ou 7 jeux gagnants à condition qu'il y ait au moins 2 jeux d'écart,
Si les deux joueurs cumulent chacun 6 jeux gagnés : le jeu se transforme en jeu-décisif (tie-break).

Une partie est gagnée par le joueur ayant cumulé :
- 2 sets gagnants dans une partie en 3 sets
- 3 sets gagnants dans une partie en 5 sets
5.1 - CONCEPTUALISATION AVANCÉE

A ce stade, on va pouvoir définir quelques propriétés élémentaires et règles pour nos concepts.

À NOTER :
Bien que généralement, je code toujours dans la langue de Shakespeare, je vais faire une dérogation à des fin de compréhension et coder dans la langue de Molière.
Le nommage des variables dans tout le code sera long, verbeux et le plus auto-descriptif possible.
Je ne ferais pas non plus référence aux théories plus pointues de la POO : abstraction, traits, interfaces, héritage, polymorphisme. Cela relève d'une étude beaucoup plus poussée de la POO. Etude qui va bien au-delà de l'objectif de ce billet.

Pas d'utilisation d'espace de nom, d'autloader et autres joyeusetés qui rajouterait de la complexité à la complexité déjà présente. Je vais faire simple.
La seule entorse c'est pour le code de test, je vais utiliser mon petit moteur de rendu en une seule classe PhpEcho
5.1.1 - JOUEUR

Maintenant il faut faire preuve d'imagination. Il faut s'imaginer le concept Joueur. Plus prosaïquement, cela correspondrait à la fiche d'un joueur. Qu'est ce qu'il est possible de suivre et qu'est-ce qu'on va décider de suivre.
Pour ce qui est possible de suivre, la liste peut être très très longue (nom, prénom, date de naissance, classement ATP, taille, régime alimentaire, manies, droitier, gaucher...)
Pour ce billet, on va aller à l'essentiel et juste se contenter de suivre le minimum :

  • identifiant
  • nom
  • prénom

Ce qui se traduit en classe ainsi Joueur.php :
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
 
/**
 * Classe représentant le concept de Joueur
 *
 * @see https://www.developpez.net/forums/blogs/32058-rawsrc/b8721/poo-gestion-partie-tennis-php-objet/
 * @date 2020-01-08
 * @author rawsrc http://www.developpez.net/forums/u32058/rawsrc
 */
class Joueur
{
    private $id;
    private $nom;
    private $prenom;
 
    public function __construct($id, string $nom, string $prenom)
    {
        $this->id     = $id;
        $this->nom    = $nom;
        $this->prenom = $prenom;
    }
 
    public function id()
    {
        return $this->id;
    }
 
    public function nom(): string
    {
        return $this->nom;
    }
 
    public function prenom(): string
    {
        return $this->prenom;
    }
 
    public function nomComplet(): string
    {
        return $this->prenom.' '.$this->nom;
    }
}
Le code de la classe reste simple à appréhender, pour peu que vous ne soyez pas complètement néophyte en programmation.

Il faut bien comprendre un point important avec l'objet c'est l'abstraction que cela apporte et surtout les bienfaits.
Ici, notre classe Joueur est un concept. Rien de plus. Un concept totalement abstrait. À ce stade, aucun joueur n'existe. Tout ce que l'on sait c'est qu'on a une possibilité de disposer et de manipuler un pur concept abstrait dénommé : Joueur. Comme cela est corrélé à la notion d'un joueur (de tennis dans notre étude), on en déduit assez facilement les contours. Mais n'empêche que cela doit bien rester abstrait dans votre esprit, c'est important pour la suite, notre classe Joueur est un modèle théorique d'un joueur (une représentation purement informatique).

J'ai décidé unilatéralement, qu'un joueur ne pouvait exister que parce qu'il avait un nom, prénom et un identifiant. C'est la raison pour laquelle ces 3 informations sont à fournir au moment où l'on crée un joueur.
Pour créer un joueur, on instancie simplement la classe : $joueur = new Joueur('rf', 'FEDERER', 'Roger');. Cette étape vous fait quitter l'environnement abstrait, le mot-clé new fait qu'un joueur avec des caractéristiques définies existe dans votre environnement informatique. On est passé du concept abstrait à une réalité immatérielle mais réalité quand même !

La force de l'objet c'est qu'il est possible de créer autant d'instances d'une classe que l'on a besoin.
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
$roger_federer = new Joueur('rf', 'FEDERER', 'Roger');
$rafael_nadal = new Joueur('rn', 'NADAL', 'Rafael');
$andy_murray = new Joueur('am', 'MURRAY', 'Andy');
Il faut bien comprendre que chacun de ces joueurs est unique mais ça reste quoi qu'il arrive un Joueur au sens conceptuel.
Ainsi si par exemple vous paramétrez une fonction : function classementATP(Joueur $j), n'importe quel de ces joueurs sera accepté, vous pouvez faire :
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
$atp_rf = classementATP($roger_federer); // ici $roger_federer est un Joueur car il est une instance de la classe Joueur
$atp_rn = classementATP($rafael_nadal); // ici $rafael_nadal est un Joueur car il est une instance de la classe Joueur

5.1.2 - RÉFLEXION SUR LE DÉROULÉ D'UNE PARTIE DE TENNIS

Avant d'aller plus loin, il faut s'arrêter et réfléchir sur l'organisation et les rôles de chaque concept.

On a traité notre concept Joueur, parfait.

Maintenant observons comment se déroule dans la réalité une partie de tennis et essayons d'en tirer une modélisation conceptuelle.
- On sait que le points sont comptés dans chaque jeu : 0 15 30 40 40A AD
- Chaque jeu a un gagnant
- Pour chaque joueur le total des jeux gagnés doit être comptés
- Chaque set à un gagnant
- Pour chaque joueur le total des sets gagnés doit être comptés
- Une partie est composée de sets qui sont composés de jeux
- Une partie a un gagnant
- On doit être capable de suivre toute la rencontre, ce qui veut dire : conserver l'historique des échanges, des points et des gagnants.

On peut d'ores-et-déjà conclure que le décompte des points sera fait dans la classe Jeu.

Maintenant, posons nous la question suivante : comment savoir quand devons-nous démarrer un nouveau jeu ou un nouveau set ?
Revoyons les bases :
  • un jeu ne sert qu'à stocker des points pour les deux joueurs et en l'état actuel du règlement il ne fait rien d'autre.
  • un set ne sert qu'a stocker des jeux pour les deux joueurs. Le décompte des jeux influe directement sur le comportement du set. Pour preuve : si le nombre de jeux atteint un seuil particulier, on peut considérer par exemple que le set courant est fini, ou encore si le décompte des jeux arrive pour chaque joueur à 6, le set se termine en tie break. Donc, on peut affirmer qu'à priori la décision de démarrer ou pas un nouveau Jeu appartient au concept Set.
  • une partie ne sert qu'à stocker des sets pour les deux joueurs. Le décompte des sets influe directement sur le déroulement de la partie. Pour preuve : si le nombre de sets remportés atteint 2 alors que la partie se joue en 3 sets, la partie est déclarée finie. Ou encore, si le nombre de sets remportés atteint 3 alors que la partie se joue en 5 sets, la partie est déclarée finie. Donc, on peut affirmer qu'à priori la décision de démarrer ou pas un nouveau Set appartient au concept Partie.

Ce qui schématiquement nous donne :
Nom : vue_ensemble-75%.jpg
Affichages : 3594
Taille : 85,0 Ko

5.1.3 - NOTION DE RÉFÉRENCE

Quand vous manipulez des objets, vous ne faites que manipuler des références. Les instances manipulées sont originales et JAMAIS copiées.
En reprenant notre exemple de tout à l'heure : function classementATP(Joueur $j), le joueur que vous passez à cette fonction est la version originale de l'instance. C'est très important de bien comprendre ce qui se passe : à l'inverse du passage de variables scalaires par copie, les instances de classes sont passées par référence. Si votre fonction modifie l'instance du joueur qui lui a été passée, c'est la version originale du joueur qui sera modifiée et non une copie. Donc, en sortie de fonction, votre joueur sera définitivement modifié pour le reste de votre application.
Il y a un énorme avantage à coder en POO, c'est que le passage des paramètres ne coûte absolument rien en terme de ressources.

Comme l'indique le schéma, et grâce au mécanisme des références, il sera possible de matérialiser l'appartenance de chaque concept enfant à un concept englobant.
Une partie englobe des sets qui englobent des jeux.
Donc dans chaque jeu, on aura un pointeur qui référencera le set courant parent qui lui même référencera la partie en cours.

5.1.4 - CONCEPT : PARTIE

Partie.php
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
<?php
 
/**
 * Classe représentant le concept de Partie au tennis
 *
 * @see https://www.developpez.net/forums/blogs/32058-rawsrc/b8721/poo-gestion-partie-tennis-php-objet/
 * @date 2020-01-08
 * @author rawsrc http://www.developpez.net/forums/u32058/rawsrc
 */
class Partie
{
    /**
     * Nombre total de sets pour la partie : 3 ou 5
     * @var int
     */
    private $nb_sets;
    /**
     * @var array   [num_set => [Set]]
     */
    private $sets = []; // suivi des sets
    /**
     * @var Joueur
     */
    private $joueur1;
    /**
     * @var Joueur
     */
    private $joueur2;
    /**
     * @var Set
     */
    private $set_courant;
    /**
     * @var Joueur
     */
    private $gagnant;
    /**
     * @var bool
     */
    private $est_finie = false;
 
    /**
     * @param Joueur $j1      Joueur numéro 1
     * @param Joueur $j2      Joueur numéro 2
     * @param int    $nb_sets nombre de sets à jouer : 3 ou 5
     */
    public function __construct(Joueur $j1, Joueur $j2, int $nb_sets)
    {
        $this->joueur1 = $j1;
        $this->joueur2 = $j2;
        if (($nb_sets === 3) || ($nb_sets === 5)) {
            $this->nb_sets = $nb_sets;
        } else {
            $this->nb_sets = 3;
        }
        $this->startNouveauSet();
    }
 
    /**
     * @return Joueur
     */
    public function joueur1(): Joueur
    {
        return $this->joueur1;
    }
 
    /**
     * @return Joueur
     */
    public function joueur2(): Joueur
    {
        return $this->joueur2;
    }
 
    /**
     * @return bool
     */
    public function estFinie(): bool
    {
        return $this->est_finie;
    }
 
    /**
     * @return bool
     */
    public function estLeDernierSetDeLaPartie(): bool
    {
        return $this->set_courant->numero() === $this->nb_sets;
    }
 
    /**
     * Pointeur vers le set en cours de jeu
     * @return Set
     */
    public function setCourant(): Set
    {
        return $this->set_courant;
    }
 
    /**
     * @return int
     */
    public function nbSetsPrevu(): int
    {
        return $this->nb_sets;
    }
 
    /**
     * Fonction à appeler quand le Joueur 1 marque le point
     */
    public function pointGagnantJoueur1()
    {
        $this->point($this->joueur1, $this->joueur2);
    }
 
    /**
     * Fonction à appeler quand le Joueur 2 marque le point
     */
    public function pointGagnantJoueur2()
    {
        $this->point($this->joueur2, $this->joueur1);
    }
 
    /**
     * @return Joueur|null
     */
    public function gagnant(): ?Joueur
    {
        return $this->gagnant;
    }
 
    private function startNouveauSet(): void
    {
        if (empty($this->sets)) {
            $num = 1;
        } else {
            // on incrémente de 1 le numéro du set courant
            $num = $this->set_courant->numero() + 1;
        }
        // la classe Set attend dans son constructeur une référence à la partie en cours :
        //         public function __construct(Partie $partie, int $numero)
        // l'instance en cours est désignée par $this
        // comme nous somme dans le code source de la classe Partie, $this référence bien une Partie
        $set = new Set($this, $num);
        // on enregistre le nouveau set dans le tableau de suivi des sets
        $this->sets[$num] = $set;
        // pour une question de commodité, on garde un accès direct au set en cours de jeu
        // comme $set est une instance de classe donc une référence à un objet, ici on a un accès direct
        // à l'objet original
        $this->set_courant = $set;
    }
 
    /**
     * Fonction qui gère toute la logique d'une partie de tennis
     * @param Joueur $gagnant
     * @param Joueur $perdant
     */
    private function point(Joueur $gagnant, Joueur $perdant)
    {
        // calcul des points pour le set courant
        $this->set_courant->point($gagnant, $perdant);
 
        // si le set n'est pas fini on continue la suite de la partie
        if ($this->set_courant->estFini() === false) {
            return;
        }
 
        // set fini : on vérifie si la partie n'est pas finie
        $nb_actuel_sets_du_gagnant = $this->nbSetsGagnes($gagnant);
 
        if ($nb_actuel_sets_du_gagnant / $this->nb_sets >= 0.5) {
            $this->est_finie = true;
            $this->gagnant   = $gagnant;
        } else {
            // le set est fini mais la partie n'est pas finie => on démarre un nouveau set
            $this->startNouveauSet();
        }
    }
 
    /**
     * Pour la partie en cours renvoie le nombre total
     * de sets gagnés par le joueur en paramètre
     * @param Joueur $joueur
     * @return int
     */
    public function nbSetsGagnes(Joueur $joueur): int
    {
        $nb = 0;
        // ici $this->sets est un tableau d'instances de la classe Set
        // et comme chaque $set est un objet, on a accès à ses propriétés publiques
        // en particulier on peut savoir si le set est fini et s'il a un gagnant
        /** @var Set $set */
        foreach ($this->sets as $num => $set) {
            if ($set->estFini() && ($set->gagnant() === $joueur)) {
                ++$nb;
            }
        }
        return $nb;
    }
 
    /**
     * Pour la partie en cours, renvoie le nombre total
     * de jeux gagnés dans un set pour le joueur en paramètre
     * @param  Joueur $joueur
     * @param  int    $numero_set
     * @return int
     */
    public function nbJeuxGagnesDansUnSet(Joueur $joueur, int $numero_set) : int
    {
        $nb = 0;
        if (isset($this->sets[$numero_set])) {
            /** @var Set $set */
            $set = $this->sets[$numero_set];
            $nb = $set->nbJeuxGagnes($joueur);
        }
        return $nb;
    }
}
À la lecture de ce code et en particulier du constructeur, on voit immédiatement comment il faudra faire pour démarrer une nouvelle partie en 5 sets :
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
 
// dépendances
include 'Joueur.php';
include 'Partie.php';
include 'Set.php';
include 'Jeu.php';
 
// création de 2 joueurs distincts
$joueur1 = new Joueur('rf', 'FEDERER', 'Roger');
$joueur2 = new Joueur('rn', 'NADAL', 'Rafael');
 
// démarrage d'une nouvelle partie de tennis en 5 sets
$partie  = new Partie($joueur1, $joueur2, 5);
Comme il a été indiqué au début, l'utilisateur ne devra suivre que les points gagnants, tout le reste doit lui rester inconnu. Cela ne doit pas l'empêcher de faire fonctionner ce système même s'il est ignare en matière de règles du tennis. Le contrat est rempli : la classe Partie, fournit deux fonctions qui se chargent de comptabiliser les points gagnants. Donc après avoir démarré la partie, tout ce que l'utilisateur a à faire se résume à l'appel d'une des fonctions à sa disposition $partie->pointGagnantJoueur1(); ou $partie->pointGagnantJoueur2().

5.1.5 - CONCEPT : SET (OU MANCHE)

Set.php
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
<?php
 
/**
 * Classe représentant le concept de Set au tennis
 *
 * @see https://www.developpez.net/forums/blogs/32058-rawsrc/b8721/poo-gestion-partie-tennis-php-objet/
 * @date 2020-01-08
 * @author rawsrc http://www.developpez.net/forums/u32058/rawsrc
 */
class Set
{
    /**
     * @var Partie
     */
    private $partie;
    /**
     * @var int
     */
    private $numero;
    /**
     * @var array   suivi des jeux
     */
    private $jeux = [];
    /**
     * @var Jeu
     */
    private $jeu_courant;
    /**
     * Joueur ayant remporté le set
     * @var Joueur
     */
    private $gagnant;
    /**
     * @var bool
     */
    private $est_fini = false;
 
    /**
     * @param Partie $partie
     * @param int    $numero
     */
    public function __construct(Partie $partie, int $numero)
    {
        $this->partie = $partie;    // chaque set appartient à une partie
        $this->numero = $numero;    // chaque set est numéroté de 1 à 7
        $this->startNouveauJeu();   // on prépare directement un nouveau jeu
    }
 
    /**
     * @return Partie
     */
    public function partie(): Partie
    {
        return $this->partie;
    }
 
    /**
     * @return int
     */
    public function numero(): int
    {
        return $this->numero;
    }
 
    /**
     * @return bool
     */
    public function estFini(): bool
    {
        return $this->est_fini;
    }
 
    /**
     * Quand le set est fini, on enregistre le joueur gagnant
     * @return Joueur|null
     */
    public function gagnant(): ?Joueur
    {
        return $this->gagnant;
    }
 
    /**
     * @param bool $tie_break
     */
    private function startNouveauJeu(bool $tie_break = false): void
    {
        if (empty($this->jeux)) {
            $num = 1;
        } else {
            // on incrémente de 1 le numéro du jeu courant
            $num = $this->jeu_courant->numero() + 1;
        }
        $jeu = new Jeu($this, $num, $tie_break);
        $this->jeu_courant = $jeu;
        $this->jeux[$num]  = $jeu;
    }
 
    /**
     * @return Jeu
     */
    public function jeuCourant(): Jeu
    {
        return $this->jeu_courant;
    }
 
    /**
     * @param Joueur $gagnant
     * @param Joueur $perdant
     */
    public function point(Joueur $gagnant, Joueur $perdant)
    {
        // on comptabilise le point dans le jeu courant
        $this->jeu_courant->point($gagnant, $perdant);
 
        // si le jeu courant est fini on regarde la suite du jeu
        // sinon le set continue
        if ($this->jeu_courant->estFini() === false) {
            return;
        }
 
        if ($this->jeu_courant->estTieBreak()) {
            // tie break terminé
            $this->est_fini = true;
            $this->gagnant  = $gagnant;
            return;
        }
 
        // récupération du décompte des jeux gagnés pour chaque joueur pour le set en cours
        $nb_actuel_jeux_du_gagnant = $this->nbJeuxGagnes($gagnant);
        $nb_actuel_jeux_du_perdant = $this->nbJeuxGagnes($perdant);
        $ecart = $nb_actuel_jeux_du_gagnant - $nb_actuel_jeux_du_perdant;
 
        // si strictement inférieur à 6 => le jeu continue
        if ($nb_actuel_jeux_du_gagnant < 6) {
            $this->startNouveauJeu();
        } elseif ($ecart >= 2) {
            // si >= 6 => si écart de sets >= 2 alors le set est fini
            $this->est_fini = true;
            $this->gagnant = $gagnant;
        } elseif ($nb_actuel_jeux_du_perdant === 6) {
            // jeu décisif => tie break
            $this->startNouveauJeu(true);
        } else {
            $this->startNouveauJeu();
        }
    }
 
    /**
     * Pour le set en cours renvoie le nombre
     * de jeux gagnés par le joueur en paramètre
     * @param Joueur $joueur
     * @return int
     */
    public function nbJeuxGagnes(Joueur $joueur): int
    {
        $nb = 0;
        /** @var Jeu $jeu */
        foreach ($this->jeux as $num => $jeu) {
            if ($jeu->estFini() && ($jeu->gagnant() === $joueur)) {
                ++$nb;
            }
        }
        return $nb;
    }
}

5.1.6 - CONCEPT : JEU

Jeu.php
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
<?php
 
/**
 * Classe représentant le concept de Jeu au tennis
 *
 * @see https://www.developpez.net/forums/blogs/32058-rawsrc/b8721/poo-gestion-partie-tennis-php-objet/
 * @date 2020-01-08
 * @author rawsrc http://www.developpez.net/forums/u32058/rawsrc
 */
class Jeu
{
    /**
     * @var Set
     */
    private $set;
    /**
     * @var int
     */
    private $numero;
    /**
     * A chaque coup joué, on suit les scores
     * [num coup joué => [joueur1 => score, joueur2 => score]]
     * @var array
     */
    private $score = [];
    /**
     * Joueur ayant remporté le set
     * @var Joueur
     */
    private $gagnant;
    /**
     * Indique si le set est fini
     * @var bool
     */
    private $est_fini = false;
    /**
     * @var array   suivi du score du tie_break
     */
    private $tie_break = [];
 
    /**
     * @param Set  $set
     * @param int  $numero
     * @param bool $tie_break
     */
    public function __construct(Set $set, int $numero, bool $tie_break)
    {
        $this->set    = $set;    // chaque jeu appartient à un set
        $this->numero = $numero; // chaque jeu est numéroté
 
        // initialisation du tableau des scores
        // ici on remonte les références parentes pour atteindre la liste des joueurs
        // qui est disponible que dans le concept Partie
        $scores = [
            $set->partie()->joueur1()->id() => 0,
            $set->partie()->joueur2()->id() => 0
        ];
        if ($tie_break) {
            $this->tie_break[] = $scores;   // initialisation du jeu décisif
        } else {
            $this->score[] = $scores;       // initialisation d'un jeu standard
        }
    }
 
    /**
     * @return int
     */
    public function numero(): int
    {
        return $this->numero;
    }
 
    /**
     * @return bool
     */
    public function estFini(): bool
    {
        return $this->est_fini;
    }
 
    /**
     * Quand le jeu est fini, on enregistre le joueur gagnant
     * @return Joueur|null
     */
    public function gagnant(): ?Joueur
    {
        return $this->gagnant;
    }
 
    /**
     * @return array    [num coup joué => [joueur1 => score, joueur2 => score]]
     */
    public function tableauDesScore(): array
    {
        return $this->score;
    }
 
    /**
     * Renvoie le dernier score connu pour le joueur
     * @param  Joueur $joueur
     * @return string
     */
    public function score(Joueur $joueur): string
    {
        return (string)end($this->score)[$joueur->id()];
    }
 
    /**
     * @return bool
     */
    public function estTieBreak(): bool
    {
        return ( ! empty($this->tie_break));
    }
 
    /**
     * @param Joueur $gagnant
     * @param Joueur $perdant
     */
    public function point(Joueur $gagnant, Joueur $perdant)
    {
        // traitement particulier si on est dans un tie-break
        if ($this->estTieBreak()) {
            $this->pointTieBreak($gagnant, $perdant);
            return;
        }
 
        $id_gagnant = $gagnant->id();
        $id_perdant = $perdant->id();
 
        // score actuel du joueur ayant marqué le point
        $score_actuel_gagnant = end($this->score)[$id_gagnant];
        // score actuel de l'adversaire
        $score_actuel_perdant = end($this->score)[$id_perdant];
 
        // Par défaut le score du perdant ne bouge pas
        // sauf dans le cas de l'égalité notée 40A et de l'avantage noté AD ou de sa perte
        $nouveau_score_perdant = $score_actuel_perdant;
 
        // détermination du prochain score des joueurs
        // ajout d'un coup joué et sauvegarde des scores
        if ($score_actuel_gagnant === 0) {
            $nouveau_score_gagnant = 15;
        } elseif ($score_actuel_gagnant === 15) {
            $nouveau_score_gagnant = 30;
        } elseif ($score_actuel_gagnant === 30) {
            if ($score_actuel_perdant === 40) {
                // score égalité pour les deux joueurs
                $nouveau_score_gagnant = '40A';
                $nouveau_score_perdant = '40A';
            } else {
                $nouveau_score_gagnant = 40;
            }
        } elseif ($score_actuel_gagnant === '40A') {
            if ($score_actuel_perdant === 'AD') {
                // perte de l'avantage : retour à l'égalité pour les deux joueurs
                $nouveau_score_gagnant = '40A';
                $nouveau_score_perdant = '40A';
            } else {
                $nouveau_score_gagnant = 'AD';
            }
        } elseif (($score_actuel_gagnant === 40) || ($score_actuel_gagnant === 'AD')) {
            // l'augmentation du score provoque la fin du jeu
            // on conserve le gagnant du jeu
            $this->gagnant = $gagnant;
            $this->est_fini = true;
            // on va sauvegarder le coup joué avec des valeur particulières
            // pour bien repérer la fin du jeu
            $nouveau_score_gagnant = true;
            $nouveau_score_perdant = false;
        }
 
        $this->score[] = [
            $id_gagnant => $nouveau_score_gagnant,
            $id_perdant => $nouveau_score_perdant
        ];
    }
 
    /**
     * @param Joueur $gagnant
     * @param Joueur $perdant
     */
    private function pointTieBreak(Joueur $gagnant, Joueur $perdant)
    {
        $id_gagnant = $gagnant->id();
        $id_perdant = $perdant->id();
 
        // récupération du décompte des points pour chaque joueur
        $score_actuel_tie_break_du_gagnant = end($this->tie_break)[$id_gagnant];
        $score_actuel_tie_break_du_perdant = end($this->tie_break)[$id_perdant];
 
        // le tie-break se joue en 7 points de base et il continue jusqu'à obtenir un écart de 2 points
 
        $nouveau_score_tie_break_du_gagnant = $score_actuel_tie_break_du_gagnant + 1;
        $ecart = $nouveau_score_tie_break_du_gagnant - $score_actuel_tie_break_du_perdant;
 
        if ($nouveau_score_tie_break_du_gagnant >= 7) {
            if ($ecart >= 2) {
                $this->est_fini = true;    // clôture du jeu
                $this->gagnant = $gagnant; // enregistrement du gagnant
            }
        }
        // enregistrement du score du tie-break
        $this->tie_break[] = [
            $id_gagnant => $nouveau_score_tie_break_du_gagnant,
            $id_perdant => $score_actuel_tie_break_du_perdant
        ];
    }
 
    /**
     * @param  Joueur $joueur
     * @return int
     */
    public function scoreTieBreak(Joueur $joueur): int
    {
        $nb = 0;
        if ($this->estTieBreak()) {
            $nb = end($this->tie_break)[$joueur->id()];
        }
        return $nb;
    }
}

5.1.7 - INDEX

index.php
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
 
define('DIR_ROOT', __DIR__.DIRECTORY_SEPARATOR);
 
// dépendances
include 'PhpEcho.php';  // moteur de rendu pour l'affichage
// modélisation : concepts d'une partie de tennis
include 'src/Joueur.php';
include 'src/Partie.php';
include 'src/Set.php';
include 'src/Jeu.php';
 
// routage très basique
$routes = [
    'start' => 'actions/start.php', // soumission du formulaire de création d'une nouvelle partie de tennis
    'point' => 'actions/point.php'  // soumission d'un point marqué par un joueur
];
 
parse_str($_SERVER['QUERY_STRING'], $query);
$action = $query['action'] ?? '';
 
session_start();
 
// page d'accueil
if ($action === '') {
    if (isset($_SESSION['partie'])) {
        // demande de la page d'accueil : alors qu'une partie est en cours => réinitialisation de la partie
        unset($_SESSION['partie']);
    }
    echo new PhpEcho([DIR_ROOT, 'vue Nouvelle_Partie.php']);
} elseif (isset($routes[$query['action']])) {
    include DIR_ROOT.$routes[$query['action']];
} else {
    echo 'Action non gérée';
}
Il faut noter que pour enregistrer les scores de la partie en cours, j'utilise une simple session dans laquelle je sauvegarde directement l'instance complète de ma classe Partie. Ce mécanisme particulier permet de conserver des objets entre les appels : le fonctionnement sous-jacent est basé sur les mécanismes de sérialisation et de déserialisation des objets. Je vous invite à aller consulter la doc de PHP sur ces aspects (serialize() et unserialize()). À noter qu'au sein d'une session, ces deux mécanismes sont appelés automatiquement sans que l'on ait à s'en préoccuper.

6 - PREUVE DU CONCEPT

Afin que vous puissiez faire vos propres tests et autre décortiquage, j'ai également codé un petite application qui vous permet de vérifier le fonctionnement de l'application.
Vous pouvez faire un test grandeur nature par ici
Dans le zip du programme, vous trouverez les 4 fichiers supplémentaires qui pilotent cette mini-app.

Comme je vous l'avais déjà annoncé, j'ai utilisé mon moteur de rendu en une seule classe PhpEcho pour bâtir les vues.

La vue la plus intéressante est celle qui affiche le déroulé de la partie de tennis :
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<?php
 
/** @var Partie $partie */
$partie = $this['partie'];
 
$en_5sets = ($partie->nbSetsPrevu() === 5);
 
// pointeur vers les instances de classe des joueurs
$joueur1 = $partie->joueur1();
$joueur2 = $partie->joueur2();
 
// score en cours
if ($partie->setCourant()->jeuCourant()->estTieBreak()) {
    $titre = ' - TIE BREAK';
    $score_j1 = $partie->setCourant()->jeuCourant()->scoreTieBreak($joueur1);
    $score_j2 = $partie->setCourant()->jeuCourant()->scoreTieBreak($joueur2);
} else {
    $titre = '';
    $score_j1 = $partie->setCourant()->jeuCourant()->score($joueur1);
    $score_j2 = $partie->setCourant()->jeuCourant()->score($joueur2);
}
?>
<style>
  * {
    font-family: Arial;
    font-size: 14px;
  }
  table {
    border: solid 1px black;
    border-collapse: collapse;
  }
  th, td {
    text-align: center;
    padding: 5px;
    border: solid 1px black;
    border-collapse: collapse;
  }
</style>
<p>DÉROULEMENT DE LA PARTIE<br>SUIVI DES POINTS</p><br>
<?php if ($partie->estFinie()) { ?>
<br><p>LA PARTIE EST FINIE : VICTOIRE DE : <?= $this('hsc', $partie->gagnant()->nomComplet()) ?></p><br>
<?php } ?>
<table>
    <thead>
        <tr>
            <th>JOUEUR</th>
            <th>Set 1</th>
            <th>Set 2</th>
            <th>Set 3</th>
            <?php if ($en_5sets) { ?>
            <th>Set 4</th>
            <th>Set 5</th>
            <?php } ?>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><?= $this('hsc', $joueur1->nomComplet()) ?></td>
            <td><?= $partie->nbJeuxGagnesDansUnSet($joueur1, 1) ?></td>
            <td><?= $partie->nbJeuxGagnesDansUnSet($joueur1, 2) ?></td>
            <td><?= $partie->nbJeuxGagnesDansUnSet($joueur1, 3) ?></td>
            <?php if ($en_5sets) { ?>
            <td><?= $partie->nbJeuxGagnesDansUnSet($joueur1, 4) ?></td>
            <td><?= $partie->nbJeuxGagnesDansUnSet($joueur1, 5) ?></td>
            <?php } ?>
        </tr>
        <tr>
          <td><?= $this('hsc', $joueur2->nomComplet()) ?></td>
          <td><?= $partie->nbJeuxGagnesDansUnSet($joueur2, 1) ?></td>
          <td><?= $partie->nbJeuxGagnesDansUnSet($joueur2, 2) ?></td>
          <td><?= $partie->nbJeuxGagnesDansUnSet($joueur2, 3) ?></td>
            <?php if ($en_5sets) { ?>
              <td><?= $partie->nbJeuxGagnesDansUnSet($joueur2, 4) ?></td>
              <td><?= $partie->nbJeuxGagnesDansUnSet($joueur2, 5) ?></td>
            <?php } ?>
        </tr>
    </tbody>
</table>
<br><br>
<?php if ($partie->estFinie() === false) { ?>
<form method="post" action="index.php?action=point">
    <p>POINT GAGNÉ PAR</p>
    <input type="submit" name="pointJoueur1" value="<?= $this('hsc', $joueur1->nomComplet()) ?>">&nbsp;
    <input type="submit" name="pointJoueur2" value="<?= $this('hsc', $joueur2->nomComplet()) ?>">
</form>
<br><br>
<p>JEU EN COURS<?= $titre ?></p>
<table>
    <tbody>
        <tr>
            <td><?= $this('hsc', $joueur1->nomComplet()) ?></td>
            <td><?= $score_j1 ?></td>
        </tr>
        <tr>
          <td><?= $this('hsc', $joueur2->nomComplet()) ?></td>
          <td><?= $score_j2 ?></td>
        </tr>
    </tbody>
</table>
<?php } ?>
Il faut bien voir l'économie de variables que procure l'objet. Au lieu d'avoir 15 variables à passer à la vue, on se contente juste de passer directement l'instance de notre classe Partie qui contient absolument toutes les données relative à la partie en cours.

7 - CONCLUSION

Nous voilà rendus à la fin de ce billet. J'espère que cet exercice va vous permettre de prendre le temps nécessaire pour vous frotter à la pensée objet. J'ai tenté de réaliser ce tuto de la manière la plus simple et didactique possible en appuyant surtout sur les avantages que peut procurer une approche et une modélisation objet en général.
N'oubliez pas que le code cette application est volontairement verbeux et pas optimisé afin de faciliter la compréhension des concepts et mécanismes mis en jeu dans une résolution 100% objet d'une problématique réelle.

Le plus important est le découpage de notre problématique en briques simples et assez élémentaires qui collent assez finement à la réalité d'une partie de tennis. Ces concepts parlent à tout le monde et n'importe quel développeur saura immédiatement ce qu'il manipule rien qu'en s'appuyant sur le nommage : Joueur, Partie, Set, Jeu. De même les fonctions sont explicites sans que l'on soit obligé d'aller tartiner le code d'explications inutiles : public function estFini(), public function gagnant(), etc.

Comme toujours si vous avez des questions ou si vous relevez des erreurs, n'hésitez pas à m'en faire part.

Bon code à tous

Fichier ZIP : blog_rawsrc_tennis_www.zip

Envoyer le billet « [POO] : Gestion d'une partie de tennis en PHP et en objet » dans le blog Viadeo Envoyer le billet « [POO] : Gestion d'une partie de tennis en PHP et en objet » dans le blog Twitter Envoyer le billet « [POO] : Gestion d'une partie de tennis en PHP et en objet » dans le blog Google Envoyer le billet « [POO] : Gestion d'une partie de tennis en PHP et en objet » dans le blog Facebook Envoyer le billet « [POO] : Gestion d'une partie de tennis en PHP et en objet » dans le blog Digg Envoyer le billet « [POO] : Gestion d'une partie de tennis en PHP et en objet » dans le blog Delicious Envoyer le billet « [POO] : Gestion d'une partie de tennis en PHP et en objet » dans le blog MySpace Envoyer le billet « [POO] : Gestion d'une partie de tennis en PHP et en objet » dans le blog Yahoo

Mis à jour 18/01/2020 à 22h58 par rawsrc

Catégories
PHP , Développement Web

Commentaires

  1. Avatar de transgohan
    • |
    • permalink
    Beau billet, clair et concis.

    Il faut bien voir l'économie de variables que procure l'objet. Au lieu d'avoir 15 variables à passer à la vue, on se contente juste de passer directement l'instance de notre classe Partie qui contient absolument toutes les données relative à la partie en cours.
    J'aurai tendance à crisser un peu des dents, même si je comprends le premier intérêt de l'argument.
    Attention à ne pas avoir l'effet inverse de ce qui est attendu.
    Quand on passe à une vue une liste de variable on contrôle la quantité juste d'informations nécessaires.
    Quand on commence à passer des objets qui contiennent des tonnes de données... On se retrouve à donner accès à de nombreuses informations inutiles, qui noient l'utile, voire qui peuvent aider à accéder aux données qu'on ne devrait pas voir.