TP et projets#
Programmation orientée objet et paradigmes
Projet La Bataille, en objets#
quatre classes qui jouent ensemble
Durée : 3 h environ (deux séances) · Sur machine, seul ou par deux
Fichiers à télécharger
But du projet
Programmer de bout en bout le jeu de cartes de la Bataille, puis le faire jouer mille fois pour répondre à de vraies questions : combien de temps dure une partie ? Avoir des As aide-t-il à gagner ? Une partie peut-elle ne jamais finir ? Le programme repose sur quatre classes qui interagissent, avec des attributs encapsulés, des méthodes spéciales et des tests ; les statistiques s’écrivent dans le style fonctionnel. ▶ signale ce qui est à programmer et tester.
Consignes
-
L’extension est facultative.
-
Le fichier
projet_poo_depart.pyest à télécharger (lien ci-dessus) : squelette des classes et, en fin de fichier, des tests. Relancer le fichier après chaque partie : chaque ligne[A FAIRE]doit devenir[OK]. On ne modifie pas les tests. -
Tous les attributs dont le nom commence par
_sont privés : en dehors de leur classe, on n’y accède que par des méthodes. -
Usage de l’IA, indiqué sur chaque exercice : aucune ; en appui (déboguer, reformuler, vérifier ; la réponse finale est écrite et comprise par vous) ; l’exercice s’appuie sur une réponse d’IA. Voir la charte d’usage de l’IA.
Les règles (version 32 cartes)
On utilise un jeu de 32 cartes : huit valeurs (7, 8, 9, 10, Valet, Dame, Roi, As, de la plus faible à la plus forte) dans quatre couleurs (pique, coeur, carreau, trefle). La couleur ne compte pas pour comparer deux cartes.
-
Le paquet est mélangé puis distribué entièrement, une carte à chacun à tour de rôle : 16 cartes par joueur, en tas, faces cachées.
-
À chaque pli, chaque joueur retourne la carte du dessus de son tas et la pose sur le tapis. La plus forte l’emporte : son propriétaire ramasse tout le tapis et le place sous son tas.
-
En cas d’égalité, c’est la bataille : chacun pose une carte face cachée, puis on rejoue (une carte face visible chacun), et ainsi de suite jusqu’à départager. Le gagnant ramasse tout le tapis.
-
Un joueur qui n’a plus de cartes a perdu, y compris au milieu d’une bataille (l’autre ramasse alors le tapis).
L’architecture du programme#
Avant d’écrire la moindre ligne, on fixe les classes, ce qu’elles savent (attributs) et ce qu’elles savent faire (méthodes).
★ Exercice 1 — Lire l’architecture #
-
Quelle classe crée des objets d’une autre classe dans son constructeur ? Lesquels ?
-
Pourquoi
nomn’est-il pas privé, alors que_mainl’est ? Donner une action interdite par les règles que l’encapsulation de_mainrend plus difficile. -
Ada a pour tas
Dame de coeur, 9 de pique, Roi de trefle, 7 de coeur(carte du dessus en premier) et Alan8 de trefle, 9 de coeur, As de pique, 10 de carreau. Jouer la partie à la main : écrire les deux tas après chaque pli. Qui gagne, et en combien de plis ?
La classe Carte#
★ Exercice 2 — Une carte qui se présente #
▶ Dans projet_poo_depart.py, compléter la classe Carte (les listes VALEURS, de la plus faible à la plus forte, et COULEURS sont déjà définies en haut du fichier). Il faut écrire le constructeur (il lève ValueError("carte inconnue") avec raise, voir le cours, « Signaler une erreur », si la valeur n’est pas dans VALEURS ou la couleur pas dans COULEURS), les accesseurs valeur() et couleur(), la méthode rang() (position de la valeur dans VALEURS : \(0\) pour un 7, \(7\) pour un As), et __repr__ (le texte affiché quand on tape le nom de l’objet). On doit obtenir :
>>> c = Carte("Roi", "coeur")
>>> c
Roi de coeur
>>> c.rang()
6
Pourquoi vaut-il mieux vérifier la valeur dès le constructeur, plutôt qu’au moment de comparer deux cartes ?
★★ Exercice 3 — Comparer et trier des cartes au-delà du programme #
Le cours a signalé qu’il existe d’autres méthodes spéciales, qui permettent d’utiliser == ou < sur nos propres objets. Écrire a == b déclenche l’appel a.__eq__(b) ; écrire a < b déclenche a.__lt__(b) (lt pour less than).
-
▶ Avant d’écrire ces méthodes, taper les deux instructions suivantes. Noter ce que Python répond à chacune (une valeur, ou une erreur). Que compare
==par défaut ?>>> Carte("Roi", "coeur") == Carte("Roi", "coeur") >>> sorted([Carte("Roi", "coeur"), Carte("7", "pique")]) -
▶ Écrire
__eq__(self, autre)et__lt__(self, autre), qui renvoient un booléen en comparant les rangs des deux cartes (la couleur ne compte pas). Vérifier que le tri donne bien les cartes de la plus faible à la plus forte :>>> main = [Carte("Roi", "coeur"), Carte("7", "pique"), ... Carte("As", "pique"), Carte("9", "coeur")] >>> sorted(main) [7 de pique, 9 de coeur, Roi de coeur, As de pique] -
Avec ces définitions,
Carte("Roi", "coeur") == Carte("Roi", "trefle")vautTrue. Est-ce gênant pour la Bataille ? Dans quel autre jeu (ou quelle autre situation) cette définition de l’égalité poserait-elle problème ?
Les classes Paquet et Joueur#
★★ Exercice 4 — Le paquet #
-
▶ Écrire le constructeur : il fabrique les 32 cartes (une par couple valeur/couleur) dans l’attribut privé
_cartes, une liste deCarte, avec deux bouclesforimbriquées (une sur les couleurs, une sur les valeurs). Pour les plus à l’aise : le réécrire ensuite en une ligne, avec une liste en compréhension à deux bouclesfor. -
▶ Écrire
taille(), puis la méthodemelanger(graine=None)(paramètre par défaut : voir le chapitre Récursivité). Si une graine est donnée, elle appelle d’abordrandom.seed(graine); dans tous les cas, elle mélange ensuite les cartes avec la fonctionrandom.shuffle. (Le modulerandomest déjà importé.) À quoi sert la graine quand on veut tester le programme ? -
▶ Écrire
distribuer(nb_joueurs): la carte d’indiceiva au joueuri % nb_joueurs; le paquet est vide ensuite, et la méthode renvoie la liste des mains (une liste de cartes par joueur). Pour deux joueurs, la carte \(0\) va au joueur \(0\), la carte \(1\) au joueur \(1\), la carte \(2\) au joueur \(0\), etc.
★★ Exercice 5 — Le joueur #
La main est une liste de cartes dont la carte du dessus est au début.
-
▶ Écrire le constructeur,
nb_cartes(),a_perdu(),jouer_carte()(retire et renvoie la carte du dessus) etramasser(cartes)(ajoute les cartes reçues, dans l’ordre, à la fin de la main, c’est-à-dire sous le tas). Le constructeur reçoit le nom et la liste des cartes de départ. -
Le test de la partie 3 ajoute une carte à la liste
cartesaprès avoir créé le joueur, et vérifie que la main d’Ada n’a pas changé. Parmi les deux écritures suivantes, laquelle faut-il donc mettre dans le constructeur :self._main = cartes, ou bienself._main = [c for c in cartes]? Expliquer ce qui différencie les deux. -
▶ Écrire
nb_cartes_de(valeur)en une ligne, avec une liste en compréhension : on garde les cartes deself._maindont la valeur est celle cherchée, et on compte le résultat aveclen(...). Pourquoi cette méthode est-elle nécessaire pour compter les As d’un joueur depuis l’extérieur de la classe ?Coup de pouce
[c for c in self._main if ...]garde les cartescde la main qui vérifient la condition ; la condition comparec.valeur()à la valeur cherchée.
La classe Partie : faire jouer les objets ensemble#
★★★ Exercice 6 — Un pli, avec ses batailles #
Le constructeur de Partie est fourni : il crée un paquet, le mélange, le distribue et crée les deux joueurs j1 et j2.
▶ Écrire jouer_pli() qui joue un pli complet et renvoie le joueur qui ramasse le tapis. Une liste locale tapis, vide au départ, accumule toutes les cartes posées pendant le pli (visibles ou cachées). On répète les étapes suivantes tant que le pli n’est pas terminé :
-
si l’un des joueurs n’a plus de cartes (
a_perdu()), l’autre est le gagnant du pli : c’est fini ; -
sinon, chaque joueur pose sa carte du dessus (
jouer_carte()) : on les ajoute au tapis. Si l’une est strictement plus forte, son propriétaire est le gagnant du pli : c’est fini ; -
sinon (égalité), on compte une bataille de plus (
nb_batailles) et, si les deux joueurs ont encore des cartes, chacun en pose une de plus, face cachée : on l’ajoute simplement au tapis sans la comparer. On revient alors à l’étape a) (la carte face visible suivante sera comparée à l’étape b)).
Coup de pouce
Une variable gagnant, qui vaut None tant que le pli n’est pas joué, et une boucle while gagnant is None ; après la boucle, gagnant ramasse le tapis (ramasser) et la méthode le renvoie. Les comparaisons s’écrivent c2 < c1 et c1 < c2 grâce à __lt__. Les cartes posées s’ajoutent au tapis dans l’ordre : carte de j1, puis carte de j2.
★★ Exercice 7 — Une partie entière, et un invariant #
-
▶ Écrire
jouer(): tant qu’aucun des deux joueurs n’a perdu et quenb_plisest inférieur àmax_plis, jouer un pli (jouer_pli()) et augmenternb_plisde 1. À la fin, renvoyerself.vainqueur()(méthode fournie : le nom du vainqueur, ouNonesi la limite a été atteinte).max_plisévite de boucler indéfiniment si la partie ne finit jamais. -
▶ On vérifie maintenant votre trace à la main (question 3 de l’exercice 1) avec le programme. Une
Partiedistribue au hasard, mais ici on veut des tas imposés. On procède donc ainsi :-
créer une partie quelconque :
p = Partie("Ada", "Alan"); -
remplacer ses deux joueurs par ceux de l’exercice 1, en affectant les attributs
p.j1etp.j2:p.j1 = Joueur("Ada", [...]) p.j2 = Joueur("Alan", [...])où chaque liste contient les cartes du tas, la carte du dessus en premier (par exemple
Carte("Dame", "coeur")) ; -
lancer
p.jouer()puis afficher le vainqueur,p.nb_plisetp.nb_batailles.
Le programme annonce-t-il le même vainqueur et le même nombre de plis que votre trace ? Sinon, qui a tort : la trace ou le programme ?
-
-
Le test de la partie 4 vérifie qu’en fin de partie les deux joueurs ont 32 cartes au total. Pourquoi cette propriété (un invariant, c’est-à-dire une propriété qui reste vraie tout au long du programme) doit-elle être vraie à la fin de chaque pli ? Donner une erreur de programmation dans
jouer_pliqui la ferait échouer. -
▶ Cette fois on joue une partie « pour de vrai » :
Partie("Ada", "Alan", 2026)mélange le paquet de façon reproductible (la graine2026fixe le mélange : tout le monde obtient la même distribution) et distribue 16 cartes à chacun. Créer cette partie, la jouer avecjouer()et indiquer le vainqueur et le nombre de plis (nb_plis). Le test de la partie 4 attend"Alan"et 106 plis.
Mille parties : des statistiques en style fonctionnel#
On veut maintenant observer le jeu. Dans cette partie, on filtre et on transforme la liste des résultats avec des listes en compréhension, comme en Première. Certaines questions ne se répondent pas avec une compréhension seule : pour une moyenne ou la recherche d’un minimum, on écrit un parcours avec une variable qui accumule ou retient le meilleur résultat ; pour classer les parties selon un critère (sorted), on introduira une fonction lambda, en la construisant pas à pas.
★★ Exercice 8 — Résumer une partie #
Pour observer le jeu, il faut d’abord pouvoir jouer une partie et en garder les faits marquants. On écrit pour cela la fonction resume(graine, tapis_melange=False) (le second paramètre ne sert que dans l’extension : on le transmet simplement à Partie).
-
▶ Dans
resume, créer la partiePartie("Ada", "Alan", graine, tapis_melange=tapis_melange), relever le nombre d’As du joueurj1(Ada) avant de jouer, avecnb_cartes_de("As"), puis jouer la partie avecjouer(). -
▶ La fonction renvoie un dictionnaire aux cinq clés
"graine"(la graine reçue),"vainqueur"(ce que renvoiejouer(): un nom, ouNonesimax_plisest atteint),"plis"(nb_plis),"batailles"(nb_batailles) et"as_ada". Pour la graine 2026, on doit obtenir :>>> resume(2026) {'graine': 2026, 'vainqueur': 'Alan', 'plis': 106, 'batailles': 8, 'as_ada': 1} -
Pourquoi faut-il relever le nombre d’As avant d’appeler
jouer()? Que vaudrait-il après ? -
resumeest-elle une fonction pure ? Que deviendrait la réponse si l’on supprimait le paramètregraine?
★★ Exercice 9 — Interroger les mille parties #
On joue mille parties (graines 0 à 999) et on garde leurs résumés : res = [resume(g) for g in range(1000)]. res est une liste de 1000 dictionnaires, comme celui de l’exercice précédent. Une partie est finie si elle a un vainqueur ; sinon elle a atteint max_plis.
-
▶ Avec une liste en compréhension, construire la liste
finiesdes parties qui ont un vainqueur (ne garder que les dictionnairesrtels quer["vainqueur"]n’est pasNone). Combien y en a-t-il ? Avec une seconde compréhension surfinies, construire la listegagnees_adades parties dont le vainqueur est"Ada": combien Ada en gagne-t-elle, et quel pourcentage des parties finies cela représente-t-il ? -
▶ Avec une liste en compréhension, extraire de
finiesla liste des nombres de plis, puis en calculer la durée moyenne (écrire une fonctionmoyenne(tab)qui additionne les valeurs dans un accumulateur, puis divise parlen(tab)). -
On veut maintenant les graines des trois parties finies les plus longues, et celle de la plus courte. Ici, une compréhension ne suffit plus : on ne filtre ni ne transforme
finies, on le classe selon le nombre de plis de chaque partie.sortedaccepte pour cela un paramètrekey: une fonction qui, à partir d’un résumér, renvoie la valeur à comparer. ▶ Écrire cette fonction avec unlambda(lambda r: r["plis"]se lit « la fonction qui, àr, associer["plis"]» ; c’est l’équivalent, pour un usage unique, dedef cle(r): return r["plis"]), puis afficher, sous la forme (graine, nombre de plis), les trois parties les plus longues avecsorted(finies, key=..., reverse=True)[:3]. Pour la plus courte, parcourirfiniesen retenant le résumé qui a le moins de plis (recherche du minimum, comme en Première). -
▶ Recopier et compléter le tableau suivant : pour chaque nombre \(k\) d’As d’Ada au départ (clé
"as_ada"), le nombre de parties finies avec ce \(k\) et le pourcentage d’entre elles gagnées par Ada (arrondi à l’unité à l’affichage, avec une f-string :f"{p:.0f}"). Pour chaque \(k\), on construit par compréhension le groupe des parties finies correspondantes, puis le sous-groupe gagné par Ada. Conclure : avoir des As aide-t-il ? Peut-on se fier aux pourcentages de tous les groupes ?As d’Ada au départ 0 1 2 3 4 Parties finies Victoires d’Ada (%)
Extension : des parties qui ne finissent jamais ?#
★★★ Exercice 10 — Casser les cycles #
-
Plus d’un tiers des parties atteignent
max_plis = 2000sans vainqueur. En augmentantmax_plisà \(20\,000\) pour quelques-unes, vérifier qu’elles ne finissent toujours pas. Proposer une explication : le programme est déterministe ; que se passe-t-il s’il retrouve exactement la même situation (mêmes tas) qu’à un pli précédent ? -
▶ Dans une vraie partie, les cartes ramassées ne sont pas reposées dans un ordre fixe. Ajouter à
Partieune méthode privée_donner(gagnant, tapis)qui mélange le tapis si l’attributtapis_melangevautTrue, puis le fait ramasser ; l’utiliser à chaque ramassage dansjouer_pli. -
▶ Refaire les statistiques avec
tapis_melange=True: nombre de parties finies, durée moyenne, partie la plus longue, victoires d’Ada. Les tests des parties 1 à 5 passent-ils toujours ? Pourquoi est-ce important ? -
(Libre.) Autres pistes : une partie à trois joueurs ; un affichage pli par pli ; une variante où le gagnant d’une bataille choisit l’ordre du ramassage.
Bilan à rédiger#
Exercice 11 — Ce que j’ai construit #
Rédiger, en une dizaine de lignes, un bilan qui répond aux questions suivantes.
-
Pour chacune des quatre classes, en une phrase : ce qu’elle représente et la méthode qui vous paraît la plus importante.
-
Citer un endroit du projet où l’encapsulation a imposé d’écrire une méthode plutôt que d’accéder directement à un attribut. Qu’y a-t-on gagné ?
-
Le projet mêle trois paradigmes. Citer pour chacun (impératif, objet, fonctionnel) un passage de votre code qui l’illustre, et dire pourquoi il convenait à cet endroit.