Descends dans la Crypte du Serpent.
Dix niveaux pour apprendre Python en construisant ton propre jeu d'aventure, puis quatre étapes pour lui donner des graphismes avec Pygame. Chaque notion du cours devient une pièce du jeu.
Voici ce que ton programme affichera à la fin du niveau 8, quand ton héros sera un objet Python :
>>> Aria [60/60 PV] affronte Gobelin [18/18 PV] Aria lance une boule de feu sur Gobelin : -11 PV (7/18) Gobelin frappe Aria : -6 PV (54/60) Aria lance une boule de feu sur Gobelin : -14 PV (0/18) >>> Aria [54/60 PV] affronte Ogre [40/40 PV] Aria lance une boule de feu sur Ogre : -16 PV (24/40) ... Victoire ! Aria [31/60 PV] — 62 or — sac : potion, épée d'argent
Les règles du jeu
Chaque niveau suit le même déroulé. Tu peux le parcourir seul ou en séance avec ton enseignant.
| Étape | Ce que tu fais |
|---|---|
| Quête | Une situation du jeu qui pose le problème du niveau. |
| Notions | Le cours, avec des exemples à copier dans ton éditeur et à modifier. |
| Prédis la sortie | Tu lis un bout de code et tu devines ce qu'il affiche avant de révéler la réponse. C'est le meilleur entraînement pour l'examen. |
| Défis ★ à ★★★ | Des exercices de difficulté croissante, avec des indices dépliables. |
| Boss | La nouvelle pièce de ton jeu, à coder avec les notions du niveau. |
| Quiz | Cinq questions pour vérifier que tout est en place. |
Chaque défi relevé et chaque bonne réponse au quiz te rapportent de l'XP. Ton rang progresse de Rat de cave à Légende du Python. Ta progression est enregistrée dans ce navigateur uniquement.
Acte I · Les fondations
Écrire, calculer, décider, répéter, ranger son code. À la fin de cet acte, ton jeu tient debout.
Acte II · L'aventurier équipé
Organiser les données, les sauvegarder, et survivre aux erreurs.
Acte III · Au-delà du donjon
Programmation objet, bibliothèque standard, réseau : ton jeu devient un vrai projet.
Acte IV · Le donjon prend vie
Pygame : fenêtre, sprites, collisions, sons. Tu termines par un jeu d'arcade complet, puis par ton propre projet.
Raids
Des TD de synthèse de quatre heures, en équipe, qui mélangent toutes les notions sur des données réelles.
L'entrée du donjon
Installer ton équipement, prononcer tes premiers mots en Python et afficher l'écran titre de ton jeu.
- Installer Python et VS Code
- Utiliser l'interpréteur
- Écrire et lancer un script
- Lire un message d'erreur
Devant toi, une porte de pierre gravée d'un serpent. Une inscription : « Seul celui qui sait parler à la machine pourra entrer. »
Ta mission : faire afficher à l'ordinateur l'écran titre de La Crypte du Serpent, le jeu que tu vas construire tout au long du cours.
Python en 2 minutes
Python est un langage de programmation : une façon d'écrire des instructions qu'un ordinateur peut exécuter. Trois choses le rendent idéal pour débuter :
- Il se lit presque comme de l'anglais.
if age >= 18: print("Bienvenue")se comprend sans formation. - Il est interprété. Pas besoin de compiler : tu écris une ligne, tu l'exécutes, tu vois le résultat tout de suite.
- Il sert partout. Intelligence artificielle, analyse de données, sites web, cybersécurité, automatisation, jeux vidéo, scripts pour Blender ou QGIS…
| Le même programme | En C | En Python |
|---|---|---|
| Afficher « Bonjour » | #include <stdio.h>int main() { printf("Bonjour\n"); return 0; }puis compiler, puis exécuter |
print("Bonjour") |
Contrepartie : Python est plus lent que C ou Java à l'exécution. Pour un jeu en terminal ou un script, la différence ne se voit pas. Pour l'IA, les bibliothèques comme NumPy font le calcul lourd en C et gardent la simplicité de Python côté utilisateur.
Installer ton équipement
- Python : télécharge la dernière version sur python.org. Sous Windows, coche Add python.exe to PATH sur le premier écran de l'installateur : c'est l'oubli le plus fréquent. Sous macOS et Linux, Python 3 est souvent déjà là (
python3 --version). - VS Code : installe Visual Studio Code, puis l'extension Python de Microsoft (onglet Extensions, à gauche). PyCharm Community fonctionne aussi très bien.
- Vérifie en ouvrant un terminal :
python --version # Windows python3 --version # macOS / Linux
Parler à l'interpréteur
Tape python (ou python3) dans le terminal. Les trois chevrons >>> signifient que Python t'écoute. Chaque ligne est exécutée dès que tu appuies sur Entrée.
>>> 2 + 3 5 >>> 7 / 2 3.5 >>> 7 // 2 # division entière 3 >>> 2 ** 10 # puissance 1024 >>> "Py" + "thon" 'Python' >>> "Ha" * 3 'HaHaHa' >>> help(print) # l'aide intégrée ; q pour quitter
Pour sortir : exit() ou Ctrl+D (macOS/Linux), Ctrl+Z puis Entrée (Windows).
print("=" * 5 + "!" * 2)
print(10 - 2 * 3)
print((10 - 2) * 3)
=====!! 4 24
Ton premier script
L'interpréteur oublie tout quand tu le fermes. Pour garder ton travail, écris un script : un fichier texte qui finit par .py. Dans VS Code, crée un dossier crypte, puis un fichier bonjour.py :
# Ceci est un commentaire : Python l'ignore.
# Il sert aux humains qui liront ton code (toi dans trois semaines, par exemple).
print("Bonjour, aventurier !")
print("Tu as", 20, "ans.") # print accepte plusieurs valeurs, séparées par des virgules
print() # ligne vide
print("Fin du programme.")
Lance-le avec le bouton ▷ en haut à droite de VS Code, ou dans le terminal : python bonjour.py. Python exécute les lignes de haut en bas, une par une.
Lire un message d'erreur
Tu vas faire des erreurs. Tout le monde en fait, tout le temps. La compétence qui compte, c'est de lire le message au lieu de paniquer. Il se lit de bas en haut :
File "bonjour.py", line 4
print("Bonjour, aventurier !)
^
SyntaxError: unterminated string literal (detected at line 4)
- Dernière ligne : le type d'erreur et son explication. Ici, une chaîne de caractères non terminée : il manque le guillemet fermant.
- Au-dessus : le fichier, le numéro de ligne, et un
^qui pointe l'endroit du problème.
Défis
La calculatrice du nain
Dans l'interpréteur, calcule : le nombre de secondes dans une journée, le reste de la division de 2026 par 7, et 3 puissance 40. Quel opérateur te donne le reste ?
Indice
Le reste s'obtient avec % (modulo). 2026 % 7 vaut 3.
Casse le code
Écris un script qui fonctionne, puis provoque volontairement trois erreurs différentes : oublie une parenthèse, écris Print avec une majuscule, puis divise par zéro. Note le nom de chaque erreur (SyntaxError, NameError…).
Ce que tu dois obtenir
SyntaxError (parenthèse), NameError: name 'Print' is not defined (Python distingue majuscules et minuscules), ZeroDivisionError.
La pancarte de la taverne
Affiche un cadre de 30 caractères de large contenant le nom d'une taverne, en utilisant la multiplication de chaînes pour les bordures. Aucune ligne de bordure ne doit être tapée caractère par caractère.
Indice
print("+" + "-" * 28 + "+") dessine une bordure de 30 caractères.
L'écran titre
Crée le fichier crypte.py : c'est le début de ton jeu. Il doit afficher un écran titre avec le nom du jeu, une illustration en caractères (un serpent, une porte, un crâne…), une ligne de version et ton nom.
print("=" * 40)
print(" LA CRYPTE DU SERPENT")
print("=" * 40)
print(" /^\\/^\\")
print(" _|__| O|")
print(" \\/ /~ \\_/ \\")
print(" \\____|__________/ \\")
print("=" * 40)
print("Version 0.1 — par", "Aria")
Quiz du prologue
Que signifie « Python est un langage interprété » ?
- Il faut le traduire en anglais avant de l'exécuter
- Le code est exécuté directement, ligne par ligne, sans étape de compilation séparée
- Il ne fonctionne que dans un navigateur
Un interpréteur lit et exécute ton code au fur et à mesure. C'est pour ça que l'interpréteur interactif >>> existe.
Que vaut 17 // 5 ?
- 3.4
- 2
- 3
- 4
// est la division entière : 17 = 5 × 3 + 2, donc 3. Le reste (2) s'obtient avec 17 % 5.
Ton script affiche NameError: name 'pirnt' is not defined. Que s'est-il passé ?
- Une faute de frappe dans le nom de la fonction
- Python n'est pas installé
- Il manque un guillemet
Python ne connaît pas pirnt. Une faute de frappe dans un nom donne presque toujours une NameError.
Que fait une ligne qui commence par # ?
- Elle affiche un titre
- Rien : c'est un commentaire, ignoré par Python
- Elle importe un module
Les commentaires expliquent le code aux humains. Python les ignore complètement.
Dans un message d'erreur, où trouve-t-on le type d'erreur ?
- Sur la première ligne
- Au milieu, avec le
^ - Sur la dernière ligne
Un message d'erreur Python se lit de bas en haut : la dernière ligne donne le type et l'explication.
La fiche du héros
Un héros, c'est un nom, des points de vie, de la force, de l'or. En Python, chacune de ces informations est une variable.
- Créer et nommer des variables
- Connaître les 4 types de base
- Lire une saisie avec input
- Convertir entre types
- Mettre en forme avec les f-strings
Le gardien de la crypte te tend un parchemin vierge : « Inscris ton nom et tes aptitudes. Nul n'entre sans fiche. »
Ta mission : un programme qui demande au joueur son nom et ses statistiques, calcule ses points de vie et affiche une fiche de personnage bien alignée.
Les variables : des étiquettes sur des valeurs
Une variable est un nom qui désigne une valeur. Le signe = ne veut pas dire « égal » : il veut dire « range la valeur de droite sous le nom de gauche ».
nom = "Aria" pv = 60 pv = pv - 15 # on calcule pv - 15 (45), puis on range le résultat dans pv pv -= 5 # raccourci pour pv = pv - 5 ; existe aussi : += *= /= print(nom, "a", pv, "PV")
Bien nommer
- Lettres, chiffres et
_, sans commencer par un chiffre :pv_maxoui,2pvnon. - Majuscules et minuscules comptent :
Pvetpvsont deux variables différentes. - Convention Python (PEP 8) :
snake_case, en minuscules avec des tirets bas. - Un bon nom dit ce que contient la variable :
points_de_vieplutôt quex. - Interdit : les mots-clés du langage (
if,for,class,or…). D'oùor_pour stocker de l'or !
Les quatre types de base
| Type | Nom | Exemples dans le jeu |
|---|---|---|
int | entier | pv = 60, niveau = 3, or_ = -5 |
float | nombre à virgule | chance_critique = 0.15, poids = 12.5 |
str | chaîne de caractères | nom = "Aria", classe = 'mage' |
bool | booléen | est_vivant = True, a_la_cle = False |
La fonction type() te dit le type d'une valeur. Python devine le type tout seul : on dit qu'il est typé dynamiquement.
print(type(42))
print(type(4.2))
print(type("42"))
print(type(4 > 2))
print(7 / 7)
<class 'int'> <class 'float'> <class 'str'> <class 'bool'> 1.0
Dialoguer avec le joueur : input
input() affiche un message, attend que le joueur tape quelque chose puis appuie sur Entrée, et renvoie ce qu'il a tapé.
nom = input("Comment t'appelles-tu ? ")
print("Bienvenue,", nom)
Convertir entre types
age = int(input("Âge : ")) # "19" -> 19
taille = float(input("Taille : ")) # "1.75" -> 1.75
texte = str(42) # 42 -> "42"
int(3.99) # 3 : on coupe, on n'arrondit pas
round(3.99) # 4
bool(0), bool(""), bool("non") # False, False, True
a = "5" b = 3 print(a * b) print(int(a) * b) print(a + str(b))
555 15 53
Les opérateurs
| Opérateur | Rôle | Exemple | Résultat |
|---|---|---|---|
+ - * | addition, soustraction, multiplication | 3 * 4 | 12 |
/ | division | 7 / 2 | 3.5 |
// | division entière | 7 // 2 | 3 |
% | reste (modulo) | 7 % 2 | 1 |
** | puissance | 2 ** 3 | 8 |
Les priorités sont celles des maths : **, puis * / // %, puis + -. Dans le doute, mets des parenthèses.
Les chaînes de caractères
nom = " aria la Rapide "
nom = nom.strip() # "aria la Rapide" : enlève les espaces au bord
print(nom.upper()) # ARIA LA RAPIDE
print(nom.title()) # Aria La Rapide
print(len(nom)) # 14 caractères
print(nom[0]) # a : le premier caractère est à l'indice 0
print(nom[-1]) # e : le dernier
print(nom.replace("Rapide", "Lente"))
Les f-strings : la mise en forme facile
Un f devant les guillemets, et tout ce qui est entre { } est remplacé par sa valeur :
nom, pv, or_ = "Aria", 47, 1250.5
print(f"{nom} a {pv} PV et {or_} pièces.")
print(f"PV restants : {pv / 60 * 100:.1f} %") # :.1f -> 1 chiffre après la virgule
print(f"|{nom:<10}|") # aligné à gauche sur 10 caractères
print(f"|{nom:>10}|") # aligné à droite
print(f"|{nom:^10}|") # centré
print(f"{pv = }") # astuce de débogage : affiche "pv = 47"
force = 7
print(f"Dégâts : {force * 2 + 1}")
print(f"[{'#' * 3:<6}]")
Dégâts : 15 [### ]
Défis
Le change du marchand
Le marchand ne compte qu'en pièces de cuivre. 1 pièce d'argent vaut 100 cuivres, 1 pièce d'or vaut 100 argents. Demande un nombre de cuivres et affiche la conversion. Exemple : 123456 donne 12 or, 34 argent, 56 cuivre.
Indice
Utilise // pour le quotient et % pour le reste. 123456 // 10000 donne l'or.
Le voyage vers la crypte
Demande la distance en kilomètres et la vitesse du cheval en km/h. Affiche la durée du voyage en heures et minutes, par exemple 2 h 15 min.
Indice
Calcule d'abord la durée totale en minutes (un entier), puis réutilise la technique du défi précédent.
Le blason
Demande le prénom et le nom du héros. Affiche ses initiales en majuscules (A.L.), son nom complet en format titre, et le nombre total de lettres sans compter les espaces.
Indice
prenom[0].upper() donne l'initiale. Pour compter sans espaces : len(texte.replace(" ", "")).
La barre de vie
Demande les PV actuels et les PV max. Affiche une barre de 20 caractères, par exemple [#############.......] 65 %, sans utiliser de condition ni de boucle.
Indice
Nombre de # : round(20 * pv / pv_max). Le reste de la barre se remplit avec des points.
La fiche de personnage
Ajoute à crypte.py, sous l'écran titre :
- Demande le nom, l'âge, la force (1 à 10) et l'agilité (1 à 10) du héros.
- Calcule les PV (
50 + force × 5) et la puissance (force × 2 + agilité). Le héros commence avec 10 pièces d'or. - Stocke dans un booléen si le héros est majeur.
- Affiche une fiche encadrée et alignée grâce aux f-strings.
+------------------------------+ | FICHE DE PERSONNAGE | +------------------------------+ | Nom : Aria | | PV : 85 | | Force : 7 | | Agilité : 5 | | Puissance : 19 | | Or : 10 | +------------------------------+ Peut entrer dans la taverne : True
Indice pour l'alignement
f"| Nom : {nom:<16} |" : la valeur occupe toujours 16 caractères, donc les bordures restent alignées.
Quiz du niveau 1
Que vaut type(input("Nombre : ")) si le joueur tape 12 ?
intfloatstr
input renvoie toujours une chaîne. Il faut int(...) pour obtenir un entier.
Après x = 5 puis x += 3 puis x = x * 2, que vaut x ?
- 13
- 16
- 11
5 + 3 = 8, puis 8 × 2 = 16.
Lequel de ces noms de variable est valide et respecte la convention Python ?
pv_max2_pvpv-maxclass
2_pv commence par un chiffre, pv-max serait lu comme une soustraction et class est un mot-clé.
Que vaut "Crypte"[-2] ?
"C""e""y""t"
Les indices négatifs partent de la fin : -1 est "e", -2 est "t".
Que vaut f"{10 / 4:.1f}" ?
"2""2.5""2.50"
:.1f affiche un seul chiffre après la virgule.
Le carrefour
Un jeu sans choix n'est pas un jeu. Ton programme va enfin prendre des décisions.
- Comparer des valeurs
- if / elif / else
- L'indentation
- and, or, not, in
- L'expression ternaire
- match / case
Trois couloirs s'ouvrent devant toi. À gauche, un cliquetis métallique. À droite, une odeur de vieux bois. Tout droit, des grognements.
Ta mission : laisser le joueur choisir son chemin et faire réagir le jeu selon son choix, ses PV et son équipement.
Comparer : vrai ou faux
Une comparaison produit un booléen, True ou False.
| Opérateur | Signification | Exemple |
|---|---|---|
== | égal à | classe == "mage" |
!= | différent de | pv != 0 |
< <= > >= | inférieur, supérieur (ou égal) | force >= 5 |
in | est contenu dans | "a" in "chat" |
if, elif, else
pv = int(input("PV du héros : "))
if pv <= 0:
print("Tu es mort.")
elif pv < 20:
print("Tu es gravement blessé.")
print("Trouve vite une potion !")
elif pv < 50:
print("Tu es blessé.")
else:
print("Tu es en pleine forme.")
print("Fin du bilan de santé.") # hors du if : toujours exécuté
Python teste les conditions dans l'ordre et exécute uniquement le premier bloc dont la condition est vraie. elif et else sont facultatifs.
L'indentation n'est pas décorative
En Python, ce sont les 4 espaces en début de ligne qui disent ce qui appartient au if. Pas d'accolades comme en C ou en Java. Les deux-points : en fin de ligne annoncent un bloc indenté.
or_ = 30
if or_ >= 50:
print("Tu achètes l'épée.")
or_ -= 50
print("Il te reste", or_, "pièces.")
Il te reste 30 pièces.
Combiner : and, or, not
a_la_cle = True
force = 4
if a_la_cle or force >= 8:
print("La porte s'ouvre.") # avec la clé OU en la défonçant
if force >= 3 and not a_la_cle:
print("Tu pourrais forcer la serrure.")
if 1 <= force <= 10: # Python accepte les comparaisons enchaînées
print("Force valide.")
| A | B | A and B | A or B | not A |
|---|---|---|---|---|
| True | True | True | True | False |
| True | False | False | True | False |
| False | True | False | True | True |
| False | False | False | False | True |
classe = "Mage"
print(classe == "mage")
print(classe.lower() == "mage")
print("ag" in classe)
print(not (3 > 2 and 2 > 5))
False True True True
L'expression ternaire
Pour choisir entre deux valeurs en une ligne :
etat = "en forme" if pv > 50 else "blessé" titre = "Dame" if genre == "f" else "Sire"
match / case : l'aiguillage
Depuis Python 3.10, match compare une valeur à plusieurs motifs. C'est plus lisible qu'une longue suite de elif sur la même variable.
direction = input("Direction (n/s/e/o) : ").lower()
match direction:
case "n" | "nord": # | : l'un OU l'autre
print("Tu montes l'escalier.")
case "s":
print("Tu redescends vers l'entrée.")
case "e" | "o":
print("Un mur. Tu te cognes le nez.")
case _: # _ : tous les autres cas
print("Direction inconnue.")
Un peu de hasard
Un jeu a besoin d'imprévu. Le module random fournit des nombres aléatoires. On le charge avec import, en haut du fichier :
import random
de = random.randint(1, 20) # entier entre 1 et 20 inclus
if de == 20:
print("Coup critique !")
elif de == 1:
print("Échec critique : tu lâches ton arme.")
Défis
L'énigme du troll
Un troll garde un pont : « Plus j'ai de gardiens, moins je suis gardé. Qui suis-je ? ». Le joueur a une seule tentative. La réponse est « un secret », mais « secret » ou « Un Secret » doivent aussi être acceptés.
Indice
Normalise la réponse avec .strip().lower(), puis teste-la avec in sur un tuple de réponses acceptées.
Le calendrier des druides
Une année est bissextile si elle est divisible par 4 mais pas par 100, ou si elle est divisible par 400. Demande une année et dis si elle est bissextile. Teste avec 2024 (oui), 1900 (non) et 2000 (oui).
Indice
« divisible par 4 » s'écrit annee % 4 == 0. La condition complète tient en une ligne avec and, or et des parenthèses.
Pierre, parchemin, dague
Le joueur affronte le gobelin à pierre-feuille-ciseaux, version donjon : la pierre écrase la dague, la dague coupe le parchemin, le parchemin enveloppe la pierre. Le gobelin joue au hasard avec random.choice(["pierre", "parchemin", "dague"]). Affiche le choix des deux et le résultat.
Indice
Traite d'abord l'égalité. Ensuite, liste les 3 cas de victoire du joueur dans une seule condition. Tout le reste est une défaite.
Le tarif de la taverne
Une chambre coûte 20 pièces. Les moins de 12 ans paient moitié prix. Les membres de la guilde ont 25 % de réduction, mais pas cumulable avec le tarif enfant : on garde le plus avantageux. Entre 22 h et 6 h, une surtaxe de 5 pièces s'applique après réduction. Demande l'âge, l'appartenance à la guilde (o/n) et l'heure, puis affiche le prix.
Indice
Calcule les réductions possibles dans des variables, prends min(...), puis ajoute la surtaxe. Attention : « entre 22 h et 6 h » s'écrit heure >= 22 or heure < 6.
Le carrefour
Ajoute un carrefour à ton jeu. Le héros a 70 PV, 10 pièces d'or, et un booléen a_une_torche.
- Demande la direction : gauche, droite ou tout droit. Accepte les majuscules et les espaces en trop.
- Avec
match: à gauche, un piège enlève entre 5 et 25 PV au hasard. À droite, un coffre donne 50 pièces d'or, mais seulement si le héros a une torche. Tout droit, un gobelin attaque : lance un d20 (20 = il fuit, 10 ou plus = −10 PV, sinon −20 PV). Toute autre réponse coûte 5 PV. - Affiche le bilan avec une expression ternaire : « en forme » au-dessus de 50 PV, « blessé » sinon.
- Si les PV tombent à 0 ou moins, affiche GAME OVER.
Tu arrives à un carrefour. Trois couloirs s'ouvrent devant toi. Où vas-tu ? (gauche / droite / tout droit) : Tout Droit Un gobelin te barre la route ! Tu le repousses après un combat serré. PV restants : 60 (en forme) — Or : 10
Quiz du niveau 2
Avec x = 15, qu'affiche ce code ?
if x > 20:
print("A")
elif x > 10:
print("B")
elif x > 5:
print("C")
ABApuisBC
Seul le premier bloc vrai est exécuté. x > 10 est vrai, donc les conditions suivantes ne sont même pas testées.
Que vaut True and not False or False ?
False- Une erreur
True
not d'abord (True and True), puis and (True), puis or (True or False) : True.
Pourquoi if reponse == "o" or "oui": est-il un bug ?
- Parce que
"oui"seul est toujours considéré comme vrai - Parce que
orn'existe pas en Python - Parce qu'il faut des parenthèses autour de
"o"
Python évalue (reponse == "o") or "oui". Une chaîne non vide vaut vrai, donc la condition est toujours vraie.
Dans un match, à quoi sert case _: ?
- À ignorer la variable
- À traiter tous les cas non prévus par les motifs précédents
- À arrêter le programme
_ correspond à n'importe quelle valeur. Il joue le rôle du else.
Quelle écriture vérifie qu'une note est comprise entre 0 et 20 inclus ?
0 < note < 20note >= 0 or note <= 200 <= note <= 20
La première exclut 0 et 20. La deuxième est toujours vraie, car tout nombre vérifie note >= 0 ou note <= 20.
L'arène
Un combat, c'est la même action répétée jusqu'à ce que quelqu'un tombe. Bienvenue dans les boucles.
- while : répéter tant que
- for et range : répéter N fois
- Compteurs et accumulateurs
- break et continue
- Boucles imbriquées
La grille se referme derrière toi. Les gradins grondent. Un squelette s'avance, puis un deuxième, puis un troisième.
Ta mission : un combat au tour par tour qui continue tant que le héros et le monstre sont debout, enchaîné sur trois salles.
while : tant que…
while répète un bloc tant que sa condition est vraie. La condition est vérifiée avant chaque tour.
pv_monstre = 30
tour = 1
while pv_monstre > 0:
print(f"Tour {tour} : tu frappes ! ({pv_monstre} PV)")
pv_monstre -= 8
tour += 1
print("Le monstre est vaincu en", tour - 1, "tours.")
n = 1
while n < 50:
n = n * 3
print(n)
81
Le modèle « redemander jusqu'à ce que ce soit bon »
reponse = ""
while reponse not in ("o", "n"):
reponse = input("Ouvrir le coffre ? (o/n) ").lower()
for : pour chaque…
for parcourt une suite de valeurs, une par une. Avec range, on répète un nombre de fois connu à l'avance.
for i in range(3): # 0, 1, 2 (3 est exclu !)
print("Coup d'épée", i)
for salle in range(1, 6): # 1, 2, 3, 4, 5
print("Salle", salle)
for pv in range(100, 0, -25): # 100, 75, 50, 25 : de 100 à 0 exclu, pas de -25
print(pv)
for lettre in "PYTHON": # on peut parcourir une chaîne
print(lettre, end=" ") # end=" " : pas de retour à la ligne
| Appel | Valeurs produites |
|---|---|
range(5) | 0 1 2 3 4 |
range(2, 6) | 2 3 4 5 |
range(0, 10, 3) | 0 3 6 9 |
range(5, 0, -1) | 5 4 3 2 1 |
Compteurs et accumulateurs
Deux modèles reviennent sans cesse. On initialise une variable avant la boucle, on la met à jour dans la boucle.
import random
total_degats = 0 # accumulateur
critiques = 0 # compteur
for coup in range(10):
de = random.randint(1, 20)
total_degats += de
if de == 20:
critiques += 1
print(f"Total : {total_degats}, moyenne : {total_degats / 10}, critiques : {critiques}")
somme = 0
for i in range(1, 5):
somme += i
print(i, somme)
1 1 2 3 3 6 4 10
break et continue
breaksort immédiatement de la boucle.continueabandonne le tour en cours et passe directement au suivant.
while True: # boucle infinie volontaire…
commande = input("> ")
if commande == "quitter":
break # …dont on sort avec break
if commande == "":
continue # ligne vide : on redemande
print("Tu as tapé :", commande)
for n in range(10):
if n % 2 == 0:
continue
if n > 6:
break
print(n)
1 3 5
Boucles imbriquées
Une boucle dans une boucle : pour chaque tour de la boucle extérieure, la boucle intérieure fait tous ses tours.
for ligne in range(4):
for colonne in range(8):
print("#" if ligne in (0, 3) or colonne in (0, 7) else ".", end="")
print()
######## #......# #......# ########
Défis
Le nombre du sphinx
Le sphinx pense à un nombre entre 1 et 100 (random.randint). Le joueur propose des nombres, le sphinx répond « plus grand » ou « plus petit », jusqu'à ce que le joueur trouve. Affiche ensuite le nombre de tentatives.
Pour aller plus loin
Limite le joueur à 7 tentatives. Pourquoi 7 suffisent toujours avec la bonne stratégie ? Pense à la dichotomie : 27 = 128.
La table du sorcier
Demande un nombre et affiche sa table de multiplication de 1 à 10, au format 7 x 3 = 21, avec les résultats alignés à droite.
Le sapin du solstice
Demande une hauteur et un caractère, puis dessine un sapin centré avec son tronc.
* *** ***** ******* |
Indice
À la ligne i (en partant de 0) : hauteur - i - 1 espaces, puis 2 * i + 1 caractères.
Les runes premières
Une porte ne s'ouvre qu'avec un nombre premier (divisible seulement par 1 et par lui-même). Écris un programme qui dit si un nombre est premier, puis un autre qui affiche tous les nombres premiers inférieurs à 100.
Indice
Teste les diviseurs de 2 à n − 1 avec % et sors avec break dès que tu en trouves un. Le else de la boucle est parfait ici. Optimisation : il suffit de tester jusqu'à √n.
Les lapins de Fibonacci
Chaque mois, le nombre de couples de lapins de la crypte est la somme des deux mois précédents : 1, 1, 2, 3, 5, 8, 13… Au bout de combien de mois dépassent-ils 1 000 000 ? Affiche aussi la suite jusque-là.
Indice
Deux variables a, b = 1, 1, puis à chaque tour a, b = b, a + b. C'est une affectation multiple, très pratique en Python.
L'arène aux trois salles
- Le héros a 60 PV (max 60) et 2 potions. Une boucle
forenchaîne 3 salles. Dans la salle n, un squelette a15 + n × 10PV. - Une boucle
whilegère le combat tant que les deux combattants sont debout. À chaque tour, le joueur choisit : attaquer (4 à 12 dégâts, 15 % de chances de critique qui double), potion (+25 PV sans dépasser le max), ou fuir. - S'il n'y a plus de potion, on redemande sans que le monstre attaque (
continue). La fuite quitte le combat (break). - Si le monstre est encore debout, il riposte (3 à 9 dégâts).
- Si le héros tombe, GAME OVER, et on sort aussi de la boucle des salles. À la fin, affiche le nombre de monstres vaincus.
=== Salle 1 : un squelette (25 PV) surgit ! === [60/60 PV, 2 potion(s)] (a)ttaquer, (p)otion, (f)uir ? a COUP CRITIQUE ! Tu infliges 18 dégâts. Le squelette riposte : -7 PV. [53/60 PV, 2 potion(s)] (a)ttaquer, (p)otion, (f)uir ? a Tu infliges 9 dégâts. Le squelette s'effondre en tas d'os.
Indice : le critique à 15 %
random.random() renvoie un nombre entre 0 et 1. random.random() < 0.15 est vrai 15 fois sur 100 en moyenne.
Quiz du niveau 3
Combien de fois for i in range(2, 10, 2): fait-il tourner sa boucle ?
- 5
- 8
- 4
- 10
Les valeurs sont 2, 4, 6, 8. Le 10 est exclu.
Que fait continue ?
- Il passe directement au tour suivant de la boucle
- Il sort de la boucle
- Il relance le programme
continue saute la fin du tour en cours. C'est break qui sort de la boucle.
Ce code ne s'arrête jamais. Pourquoi ?
pv = 10
while pv > 0:
print("Aïe")
pv -= 1
whileexige unbreakpv -= 1n'est pas indenté, il est donc hors de la bouclepvdevrait valoir 0 au départ
Sans indentation, pv -= 1 ne s'exécute qu'après la boucle, ce qui n'arrive jamais. pv reste à 10 pour toujours.
Avec deux boucles imbriquées for i in range(3) et for j in range(4), combien de fois s'exécute le bloc intérieur ?
- 7
- 12
- 4
Pour chacune des 3 valeurs de i, la boucle intérieure fait 4 tours : 3 × 4 = 12.
Quelle boucle convient le mieux pour « redemander un mot de passe jusqu'à ce qu'il soit correct » ?
for i in range(10)for lettre in mot_de_passewhile
On ne sait pas combien d'essais il faudra : c'est le cas typique du while.
Le grimoire
Un sort s'écrit une fois dans le grimoire, puis s'invoque autant de fois que nécessaire. En Python, un sort s'appelle une fonction.
- Définir et appeler une fonction
- Paramètres et valeurs par défaut
- return ou print ?
- Portée des variables
- Créer et importer un module
Ton fichier crypte.py fait maintenant 150 lignes. Le code du combat est copié trois fois, avec trois bugs différents. Le vieux mage soupire : « Range ton grimoire, apprenti. »
Ta mission : découper le jeu en fonctions réutilisables, réparties dans plusieurs fichiers.
Définir et appeler
def saluer(nom): # définition : rien ne s'affiche encore
print(f"Salutations, {nom} !")
saluer("Aria") # appel : le code de la fonction s'exécute
saluer("Brom")
def, un nom, des parenthèses avec les paramètres, deux-points, puis un bloc indenté.- À l'appel, les valeurs passées (
"Aria") s'appellent les arguments. - Une fonction doit être définie avant d'être appelée.
return : rendre un résultat
return renvoie une valeur à l'endroit de l'appel et arrête la fonction. Sans return, la fonction renvoie None.
def calculer_degats(force, bonus):
return force * 2 + bonus
degats = calculer_degats(7, 3) # degats vaut 17
print(calculer_degats(5, 0) + 10) # 20 : on peut utiliser le résultat dans un calcul
def double_affiche(x):
print(x * 2)
def double_renvoie(x):
return x * 2
a = double_affiche(5)
b = double_renvoie(5)
print(a, b)
10 None 10
Paramètres : valeurs par défaut et arguments nommés
import random
def lancer_des(nombre=1, faces=6):
"""Lance `nombre` dés à `faces` faces et renvoie la somme."""
total = 0
for _ in range(nombre): # _ : variable dont on ne se sert pas
total += random.randint(1, faces)
return total
lancer_des() # 1d6
lancer_des(2) # 2d6
lancer_des(1, 20) # 1d20
lancer_des(faces=20) # 1d20, en nommant l'argument : plus lisible
Le texte entre triples guillemets sous le def est une docstring : help(lancer_des) l'affiche, et VS Code la montre au survol.
Renvoyer plusieurs valeurs
def convertir(cuivres):
return cuivres // 10000, cuivres // 100 % 100, cuivres % 100 # un tuple
or_, argent, cuivre = convertir(123456) # on « déballe » le tuple
La portée des variables
Une variable créée dans une fonction est locale : elle n'existe que pendant l'exécution de la fonction et disparaît ensuite.
pv = 100
def prendre_coup():
pv = 80
print("Dans la fonction :", pv)
prendre_coup()
print("Dehors :", pv)
Dans la fonction : 80 Dehors : 100
Le pv de la fonction est une autre variable que celui du dehors. La bonne pratique est de passer les données en paramètre et récupérer le résultat avec return, plutôt que de modifier des variables globales.
def prendre_coup(pv, degats):
return max(0, pv - degats)
pv = prendre_coup(pv, 20)
Une fonction qui s'appelle elle-même
Une fonction récursive s'appelle elle-même sur un problème plus petit, jusqu'à un cas de base qui arrête la récursion.
def factorielle(n):
if n <= 1: # cas de base : on s'arrête
return 1
return n * factorielle(n - 1) # 5! = 5 × 4!
print(factorielle(5)) # 120
Les modules : ranger le grimoire
Un module est simplement un fichier .py. On importe ses fonctions depuis un autre fichier du même dossier.
import random
def lancer(faces=6):
return random.randint(1, faces)
def est_critique(chance=0.15):
return random.random() < chance
if __name__ == "__main__":
# Exécuté seulement si on lance des.py directement (pour tester),
# pas quand un autre fichier fait « import des »
for _ in range(5):
print(lancer(20))
import des # on utilise ensuite des.lancer(...) from des import est_critique # on utilise directement est_critique() import math # module de la bibliothèque standard print(des.lancer(20)) print(est_critique()) print(math.sqrt(16), math.pi)
Défis
Les quatre sorts de base
Écris une fonction operations(a, b) qui renvoie la somme, la différence, le produit et le quotient de deux nombres. Si b vaut 0, le quotient vaut None. Récupère les 4 résultats en une ligne.
Le compteur de voyelles
Les incantations les plus puissantes contiennent beaucoup de voyelles. Écris compter_voyelles(texte), qui renvoie le nombre de voyelles d'une chaîne, majuscules et accents compris.
Indice
if lettre.lower() in "aeiouyàâéèêëîïôûù":
Le module de géométrie
Crée un module geometrie.py avec distance(x1, y1, x2, y2) (la distance euclidienne, avec math.sqrt) et est_a_portee(x1, y1, x2, y2, portee). Importe-le dans un autre fichier pour savoir si une flèche tirée de (0, 0) atteint un gobelin en (3, 4) avec une portée de 5.
Le miroir magique
Écris est_palindrome(mot), qui renvoie True si le mot se lit pareil dans les deux sens (« kayak », « Esope reste ici et se repose »), en ignorant espaces et majuscules. Écris une version avec une boucle, puis une version récursive.
Indice récursif
Un mot est un palindrome si sa première et sa dernière lettre sont égales et si le mot sans elles (mot[1:-1]) est un palindrome. Cas de base : longueur 0 ou 1.
L'interrogateur infaillible
Écris demander_entier(message, mini, maxi), qui redemande tant que la saisie n'est pas un entier compris entre mini et maxi. Utilise texte.isdigit() pour tester la saisie. Cette fonction te servira dans tout le reste du jeu.
Le grimoire du jeu
Réorganise ton jeu en trois fichiers dans le même dossier :
des.py:lancer(faces=6),lancer_plusieurs(nombre, faces=6),est_critique(chance=0.15), plus un bloc de test sousif __name__ == "__main__":.combat.py:calculer_degats(force, bonus=0)(2d6 + force // 2, doublés en cas de critique),barre_de_vie(pv, pv_max, largeur=20), etcombattre(nom_heros, pv_heros, force_heros, nom_monstre, pv_monstre, force_monstre), qui déroule un combat automatique et renvoie les PV restants du héros.main.py: une fonctionmain()qui enchaîne trois combats (Rat géant, Gobelin, Ogre) et s'arrête au premier GAME OVER.
==============================
Aria VS Ogre
==============================
-- Tour 1 --
Ogre [###########.........]
Aria [##########..........]
-- Tour 2 --
COUP CRITIQUE !
Ogre [....................]
Victoire ! Aria sort de la crypte avec 40 PV.
Quiz du niveau 4
Que renvoie une fonction qui n'a pas de return ?
- 0
None- Une erreur
- La dernière valeur calculée
Sans return, une fonction renvoie None.
Dans def soigner(pv, quantite=10):, que vaut quantite après l'appel soigner(50) ?
- 10
- 50
None
Le paramètre non fourni prend sa valeur par défaut.
Qu'affiche ce code ?
def f(x):
return x + 1
print("fini")
print(f(f(1)))
finipuis323
return arrête la fonction, donc le print("fini") n'est jamais atteint. f(1) = 2, puis f(2) = 3.
À quoi sert if __name__ == "__main__": ?
- À rendre le fichier plus rapide
- À exécuter du code seulement quand on lance ce fichier directement, pas quand on l'importe
- À déclarer la fonction principale
Quand un fichier est importé, __name__ vaut le nom du module. Il ne vaut "__main__" que pour le fichier lancé.
Que manque-t-il à cette fonction récursive ?
def compte_a_rebours(n):
print(n)
compte_a_rebours(n - 1)
- Un cas de base qui arrête les appels
- Un
returnà la fin - Une boucle
while
Sans condition d'arrêt (par exemple if n < 0: return), les appels ne s'arrêtent jamais et Python lève une RecursionError.
Le sac à dos
Une variable contient une valeur. Un héros transporte un inventaire, explore une carte, affronte un bestiaire. Il te faut des conteneurs.
- Listes
- Tuples
- Ensembles
- Dictionnaires
- Compréhensions
- Choisir la bonne structure
Le marchand te tend un sac de cuir : « Un aventurier sans sac, c'est un aventurier qui meurt les mains pleines. »
Ta mission : un héros avec un vrai inventaire, qui se déplace sur une carte en deux dimensions et rencontre des monstres tirés d'un bestiaire.
Quelle structure pour quel usage ?
| Structure | Syntaxe | Ordonnée | Modifiable | Doublons | Dans le jeu |
|---|---|---|---|---|---|
Liste list | ["épée", "potion"] | oui | oui | oui | l'inventaire |
Tuple tuple | (3, 4) | oui | non | oui | une position (ligne, colonne) |
Ensemble set | {"rat", "gobelin"} | non | oui | non | les salles déjà visitées |
Dictionnaire dict | {"nom": "Aria", "pv": 60} | ordre d'insertion | oui | clés uniques | la fiche du héros |
Les listes
sac = ["torche", "corde", "potion"]
print(sac[0]) # torche (le premier élément est à l'indice 0)
print(sac[-1]) # potion (le dernier)
print(len(sac)) # 3
sac.append("épée") # ajoute à la fin
sac.insert(0, "carte") # insère à l'indice 0
sac.remove("corde") # retire la première occurrence de "corde"
objet = sac.pop() # retire et renvoie le dernier élément ("épée")
sac[1] = "torche magique" # remplace l'élément d'indice 1
if "potion" in sac:
print("Tu as une potion.")
for objet in sac: # parcourir les éléments
print("-", objet)
for i, objet in enumerate(sac, start=1): # avec leur numéro
print(f"{i}. {objet}")
Le découpage (slicing)
salles = ["entrée", "couloir", "crypte", "trésor", "sortie"] salles[1:3] # ['couloir', 'crypte'] de 1 inclus à 3 exclu salles[:2] # ['entrée', 'couloir'] depuis le début salles[2:] # ['crypte', 'trésor', 'sortie'] jusqu'à la fin salles[::-1] # la liste à l'envers (fonctionne aussi sur les chaînes)
Trier
scores = [42, 7, 99, 15] scores.sort() # trie LA liste elle-même, renvoie None classement = sorted(scores, reverse=True) # renvoie une NOUVELLE liste triée print(max(scores), min(scores), sum(scores))
a = [1, 2, 3] b = a b.append(4) print(a) c = a.copy() c.append(5) print(a, c)
[1, 2, 3, 4] [1, 2, 3, 4] [1, 2, 3, 4, 5]
Les tuples
Un tuple est une liste figée : une fois créé, on ne peut plus le modifier. On l'utilise pour des valeurs qui vont ensemble et ne doivent pas changer, comme des coordonnées.
position = (2, 5)
ligne, colonne = position # déballage
position = (ligne + 1, colonne) # on ne modifie pas le tuple, on en crée un nouveau
# position[0] = 3 -> TypeError : 'tuple' object does not support item assignment
DIRECTIONS = {"z": (-1, 0), "s": (1, 0), "q": (0, -1), "d": (0, 1)}
Les ensembles
Un ensemble est un sac sans ordre et sans doublons. Tester l'appartenance avec in y est très rapide, même avec un million d'éléments.
vus = {"rat", "gobelin", "squelette", "rat"} # le doublon disparaît
vaincus = {"rat", "gobelin"}
vus.add("ogre")
print(vus - vaincus) # différence : vus mais pas vaincus -> {'squelette', 'ogre'}
print(vus & vaincus) # intersection : les deux -> {'rat', 'gobelin'}
print(vus | {"dragon"}) # union
print(vaincus <= vus) # inclusion : True
sans_doublons = list(set(["a", "b", "a", "c"])) # astuce classique
Les dictionnaires
Un dictionnaire associe des clés à des valeurs, comme un vrai dictionnaire associe un mot à sa définition. On n'accède pas aux éléments par position mais par clé.
heros = {"nom": "Aria", "pv": 60, "force": 7, "inventaire": ["torche"]}
print(heros["nom"]) # Aria
heros["pv"] -= 15 # modifier
heros["or"] = 120 # ajouter une nouvelle clé
del heros["force"] # supprimer
heros["inventaire"].append("potion") # une valeur peut être une liste
print(heros.get("mana", 0)) # 0 : valeur par défaut si la clé n'existe pas
# heros["mana"] -> KeyError : 'mana'
for cle, valeur in heros.items():
print(f"{cle:>10} : {valeur}")
Liste de dictionnaires : le bestiaire
bestiaire = [
{"nom": "Rat géant", "pv": 10, "force": 3},
{"nom": "Gobelin", "pv": 18, "force": 5},
{"nom": "Ogre", "pv": 40, "force": 8},
]
for monstre in bestiaire:
print(monstre["nom"], "-", monstre["pv"], "PV")
le_plus_fort = max(bestiaire, key=lambda m: m["force"]) # key : critère de comparaison
compte = {}
for mot in "le rat et le chat et le chien".split():
compte[mot] = compte.get(mot, 0) + 1
print(compte)
{'le': 3, 'rat': 1, 'et': 2, 'chat': 1, 'chien': 1}
Les compréhensions
Une compréhension construit une liste (ou un ensemble, ou un dictionnaire) en une ligne, à partir d'une autre collection. Elle se lit comme une phrase.
# « les carrés de n, pour n de 1 à 5 »
carres = [n ** 2 for n in range(1, 6)] # [1, 4, 9, 16, 25]
# « le nom de chaque monstre, pour chaque monstre du bestiaire, si ses pv dépassent 15 »
costauds = [m["nom"] for m in bestiaire if m["pv"] > 15] # ['Gobelin', 'Ogre']
# compréhension de dictionnaire
pv_par_nom = {m["nom"]: m["pv"] for m in bestiaire}
# compréhension d'ensemble
initiales = {m["nom"][0] for m in bestiaire}
Elle remplace ce modèle en quatre lignes :
costauds = []
for m in bestiaire:
if m["pv"] > 15:
costauds.append(m["nom"])
mots = ["épée", "arc", "hallebarde", "dague"]
print([len(m) for m in mots])
print([m.upper() for m in mots if len(m) < 5])
print({m: m[0] for m in mots[:2]})
[4, 3, 10, 5]
['ÉPÉE', 'ARC']
{'épée': 'é', 'arc': 'a'}
Listes de listes : une carte en 2D
CARTE = [
"#######",
"#..M..#",
"#.#.#T#",
"#######",
]
grille = [list(ligne) for ligne in CARTE] # chaque ligne devient une liste modifiable
print(grille[1][3]) # 'M' : ligne 1, colonne 3
grille[1][3] = "." # le monstre est vaincu
for ligne in grille:
print("".join(ligne)) # "".join : recolle une liste de caractères
Défis
L'inventaire du marchand
Représente le stock du marchand par un dictionnaire {article: quantité}. Écris ajouter(stock, article, quantite=1), qui crée l'article ou augmente sa quantité, et vendre(stock, article), qui diminue la quantité et supprime l'article à 0. Affiche le stock trié par ordre alphabétique.
Le parchemin bavard
Compte les occurrences de chaque mot dans une phrase saisie (en minuscules, sans ponctuation), puis affiche les 3 mots les plus fréquents.
Indice
sorted(compte.items(), key=lambda paire: paire[1], reverse=True)[:3]
Le tri du butin
Le butin est une liste de tuples (objet, valeur, poids). Avec des compréhensions et sorted : affiche les objets triés par valeur décroissante, la liste des objets de moins de 2 kg, et le meilleur rapport valeur/poids.
Les cartes des deux éclaireurs
Deux éclaireurs ont chacun noté les salles qu'ils ont visitées (listes avec doublons). Avec des ensembles, trouve les salles visitées par les deux, celles vues par un seul, et le nombre total de salles différentes.
Le traducteur runique
Construis un dictionnaire qui associe chaque lettre à la lettre décalée de 3 dans l'alphabet (a→d, …, x→a). C'est le chiffre de César. Utilise-le pour chiffrer un message, puis construis le dictionnaire inverse par compréhension pour le déchiffrer.
Indice
alphabet = "abcdefghijklmnopqrstuvwxyz" et alphabet[(i + 3) % 26]. Inverse : {v: k for k, v in code.items()}.
L'exploration de la crypte
- Le héros devient un dictionnaire : nom, pv, pv_max, force, or, inventaire (une liste) et position (un tuple).
- La carte est une liste de chaînes convertie en liste de listes.
#= mur,M= monstre,P= potion,C= coffre,S= sortie. - Les déplacements z/q/s/d passent par un dictionnaire
DIRECTIONSqui associe chaque touche à un tuple(dl, dc). On ne peut pas traverser les murs. - Les cases visitées sont stockées dans un ensemble. Les cases non visitées s'affichent
?: c'est le brouillard de guerre. - Sur une case M, on tire un monstre au hasard dans le bestiaire (une liste de dictionnaires) et on combat. Pense à en faire une copie avec
dict(...). - La touche
iaffiche l'inventaire et propose de boire une potion. La liste des potions se calcule par compréhension.
######### #...?...# #?##.##?# #??.@.??# #?##?##?# #???????# ######### PV 60/60 | Or 30 | Sac : torche, potion Déplacement (z/q/s/d), (i)nventaire, (x) quitter :
Quiz du niveau 5
Quelle structure choisir pour stocker les coordonnées (x, y) fixes d'un trésor ?
- Un ensemble
- Un dictionnaire
- Un tuple
Deux valeurs ordonnées qui ne doivent pas changer : c'est exactement le rôle d'un tuple.
Que vaut ["a", "b", "c", "d"][1:3] ?
["a", "b", "c"]["b", "c"]["b", "c", "d"]
De l'indice 1 inclus à l'indice 3 exclu.
heros = {"nom": "Aria"}. Que fait heros["pv"] ?
- Il lève une
KeyError - Il renvoie
None - Il crée la clé avec la valeur 0
Une clé absente lève une KeyError. heros.get("pv") renverrait None.
Que vaut len({1, 2, 2, 3, 3, 3}) ?
- 6
- 3
- 1
Un ensemble élimine les doublons : il reste {1, 2, 3}.
Que vaut [x * 2 for x in range(5) if x % 2 == 0] ?
[0, 2, 4, 6, 8][2, 6][0, 2, 4][0, 4, 8]
Les x pairs sont 0, 2 et 4, multipliés par 2 : 0, 4, 8.
La pierre de sauvegarde
Quand le programme s'arrête, toutes ses variables disparaissent. Pour qu'une partie survive, il faut l'écrire dans un fichier.
- Ouvrir avec with
- Lire et écrire du texte
- Les modes r, w, a
- CSV
- JSON
- Fichiers binaires
Au cœur de la crypte, une pierre runique pulse d'une lueur bleue. Celui qui la touche peut reprendre son aventure là où il l'a laissée.
Ta mission : sauvegarder et recharger une partie, tenir un tableau des meilleurs scores et charger le bestiaire depuis un fichier.
Texte ou binaire ?
- Fichier texte : des caractères lisibles dans n'importe quel éditeur (
.txt,.csv,.json,.py…). - Fichier binaire : des octets bruts, illisibles dans un éditeur (images, sons,
.exe,.zip…).
Ouvrir un fichier : with open
with open("journal.txt", "w", encoding="utf-8") as f:
f.write("Jour 1 : entrée dans la crypte.\n") # \n : retour à la ligne, à mettre soi-même
f.write("Jour 2 : rencontre avec un gobelin.\n")
# en sortant du bloc with, le fichier est fermé automatiquement, même en cas d'erreur
| Mode | Effet | Si le fichier n'existe pas |
|---|---|---|
"r" | lecture (mode par défaut) | FileNotFoundError |
"w" | écriture : efface le contenu existant | il est créé |
"a" | ajout à la fin (append) | il est créé |
"rb", "wb" | lecture et écriture en binaire | idem |
Lire
with open("journal.txt", encoding="utf-8") as f:
contenu = f.read() # tout le fichier dans une seule chaîne
with open("journal.txt", encoding="utf-8") as f:
for ligne in f: # ligne par ligne : idéal pour les gros fichiers
print(ligne.strip()) # strip() enlève le \n de fin de ligne
with open("journal.txt", encoding="utf-8") as f:
lignes = f.readlines() # une liste de chaînes, une par ligne
with open("test.txt", "w", encoding="utf-8") as f:
f.write("A\n")
with open("test.txt", "a", encoding="utf-8") as f:
f.write("B\n")
with open("test.txt", "w", encoding="utf-8") as f:
f.write("C\n")
with open("test.txt", encoding="utf-8") as f:
print(f.read())
C
CSV : des tableaux en texte
Un fichier CSV contient une ligne par enregistrement, avec des valeurs séparées par une virgule ou un point-virgule. Excel et LibreOffice l'ouvrent directement.
nom;score Aria;1250 Brom;980
import csv
with open("scores.csv", "a", newline="", encoding="utf-8") as f:
ecrivain = csv.writer(f, delimiter=";")
ecrivain.writerow(["Aria", 1250])
with open("scores.csv", newline="", encoding="utf-8") as f:
for ligne in csv.DictReader(f, delimiter=";"): # chaque ligne devient un dict
print(ligne["nom"], int(ligne["score"])) # tout est lu comme du texte !
JSON : sauvegarder des dictionnaires et des listes
JSON est le format d'échange de données du web. Il ressemble énormément aux dictionnaires et aux listes de Python.
import json
heros = {"nom": "Aria", "pv": 45, "inventaire": ["torche", "potion"], "position": (3, 4)}
with open("partie.json", "w", encoding="utf-8") as f:
json.dump(heros, f, ensure_ascii=False, indent=2) # indent : fichier lisible
with open("partie.json", encoding="utf-8") as f:
charge = json.load(f)
print(charge["position"]) # [3, 4] : une liste, plus un tuple !
{
"nom": "Aria",
"pv": 45,
"inventaire": [
"torche",
"potion"
],
"position": [
3,
4
]
}
json.dumps(objet) et json.loads(texte), avec un s pour string, font la même chose avec une chaîne au lieu d'un fichier. Tu les retrouveras au niveau 10 avec les API web.
Les fichiers binaires
with open("carte.png", "rb") as source, open("copie.png", "wb") as dest:
donnees = source.read() # un objet bytes : b'\x89PNG\r\n...'
print(donnees[:4]) # les 4 premiers octets identifient le format
dest.write(donnees)
Le module pickle sauvegarde n'importe quel objet Python en binaire. Il est pratique, mais ne charge jamais un fichier pickle reçu de quelqu'un d'autre : il peut exécuter du code arbitraire. Pour une sauvegarde de jeu, JSON est plus sûr et lisible.
Défis
Le scribe
Écris un programme qui lit un fichier texte et affiche son nombre de lignes, de mots et de caractères, comme la commande wc de Linux.
Le registre des recettes
Le fichier recettes.txt contient des nombres de pièces séparés par des virgules (12,45,3,78). Calcule et affiche la recette totale et la moyenne.
Le journal de bord
Chaque exécution du programme demande une phrase au joueur et l'ajoute à la fin de journal.txt, précédée d'un numéro d'entrée. Le numéro se calcule en comptant les lignes déjà présentes. Ensuite, le programme affiche les 3 dernières entrées.
Le grand livre de la guilde
Crée un fichier CSV d'aventuriers (nom, classe, niveau). Lis-le avec csv.DictReader, puis écris dans classes.json un dictionnaire {classe: [noms...]} et le niveau moyen de chaque classe.
Le détecteur de faux parchemins
Écris identifier(chemin), qui lit les 8 premiers octets d'un fichier et renvoie son vrai type (PNG, JPEG, PDF, ZIP, EXE) d'après sa signature, quelle que soit son extension. Teste-la sur un PNG renommé en .txt.
Signatures
PNG b"\x89PNG", JPEG b"\xff\xd8\xff", PDF b"%PDF", ZIP b"PK\x03\x04", EXE b"MZ". Utilise donnees.startswith(...).
La pierre de sauvegarde
Crée un module sauvegarde.py :
sauvegarder_partie(heros)etcharger_partie()en JSON. Convertis la position (tuple vers liste à l'écriture, liste vers tuple à la lecture).enregistrer_score(nom, score)ajoute une ligne àscores.csv.meilleurs_scores(n=5)renvoie les n meilleurs, triés.charger_bestiaire()litmonstres.txt(formatnom,pv,force,or, en ignorant les lignes vides et celles qui commencent par#) et renvoie une liste de dictionnaires.- Au lancement du jeu : si
partie.jsonexiste (os.path.exists), propose de reprendre la partie.
# nom,pv,force,or Rat géant,10,3,2 Gobelin,18,5,8 Squelette,22,6,12 Araignée de crypte,15,7,10
Quiz du niveau 6
Quel mode utiliser pour ajouter un score à la fin d'un fichier sans effacer les précédents ?
"w""a""r"
"a" (append) écrit à la fin. "w" commencerait par tout effacer.
Quel est l'avantage principal de with open(...) as f: ?
- Le fichier est fermé automatiquement, même en cas d'erreur
- La lecture est plus rapide
- Le fichier est créé s'il n'existe pas
Le bloc with garantit la fermeture du fichier, donc pas de données perdues ni de fichier verrouillé.
Après un json.dump puis un json.load, que devient le tuple (3, 4) ?
- Le tuple
(3, 4) - La chaîne
"(3, 4)" - La liste
[3, 4]
JSON n'a pas de tuple, seulement des tableaux, qui redeviennent des listes en Python.
Avec csv.reader, de quel type est la valeur "1250" lue dans la colonne score ?
intstrfloat
Le module csv lit tout comme du texte. Il faut convertir avec int().
Pourquoi ne faut-il pas ouvrir un fichier .pkl téléchargé sur Internet ?
- Parce qu'il est trop gros
- Parce que charger un pickle peut exécuter du code malveillant
- Parce qu'il ne fonctionne que sous Linux
pickle.load peut reconstruire n'importe quel objet, y compris un objet qui lance une commande système. Ne l'utilise que sur tes propres fichiers.
Les pièges du donjon
Le joueur tape « douze » au lieu de 12, la sauvegarde est corrompue, le fichier a disparu. Un bon programme ne plante pas : il réagit.
- Les trois familles d'erreurs
- Lire une trace d'erreur
- try / except / else / finally
- Lever ses propres exceptions
- Déboguer
Ton ami teste ton jeu. Au premier menu, il tape « bonjour ». Le jeu s'effondre dans un tas de lignes rouges. Il rit. Toi, moins.
Ta mission : rendre le jeu impossible à faire planter, quelle que soit la saisie du joueur ou l'état des fichiers.
Trois familles d'erreurs
| Famille | Quand | Exemple |
|---|---|---|
| Syntaxe | avant même l'exécution : Python ne comprend pas le texte | parenthèse oubliée, : manquant |
| Exécution (exceptions) | pendant l'exécution, sur une opération impossible | int("douze"), division par 0 |
| Logique | jamais signalée : le programme tourne mais donne un mauvais résultat | < au lieu de <= |
Les erreurs de logique sont les plus traîtresses. Seuls les tests, et ta lecture attentive, peuvent les détecter.
Les exceptions que tu croiseras
| Exception | Cause typique |
|---|---|
ValueError | int("abc") : bon type, mauvaise valeur |
TypeError | "PV : " + 10 : types incompatibles |
NameError | variable non définie ou mal orthographiée |
IndexError | sac[10] sur une liste de 3 éléments |
KeyError | heros["mana"] sur une clé absente |
ZeroDivisionError | division par zéro |
FileNotFoundError | ouverture en lecture d'un fichier absent |
AttributeError | (42).upper() : méthode inexistante pour ce type |
Lire une trace d'erreur (traceback)
Traceback (most recent call last):
File "main.py", line 12, in <module>
main()
File "main.py", line 8, in main
pv = calculer_pv(force)
File "main.py", line 3, in calculer_pv
return 50 + force * 5
~~~^~~~~~~~~~~
TypeError: can only concatenate str (not "int") to str
- La dernière ligne donne le type et le message : on a additionné une chaîne et un nombre.
- Juste au-dessus : où ça a planté (ligne 3, dans
calculer_pv). - En remontant : le chemin des appels.
maina appelécalculer_pvavecforce… qui venait d'uninput()jamais converti enint.
try / except : désamorcer le piège
try:
force = int(input("Force : ")) # code qui peut échouer
ratio = 100 / force
except ValueError:
print("Ce n'est pas un nombre.") # exécuté seulement si ValueError
except ZeroDivisionError:
print("La force ne peut pas être nulle.")
Si une ligne du try lève une exception, Python saute immédiatement au except correspondant. Les lignes suivantes du try ne sont pas exécutées.
Le bloc complet
try:
f = open("partie.json", encoding="utf-8")
donnees = f.read()
except FileNotFoundError as e: # « as e » : on récupère l'exception
print("Pas de sauvegarde :", e)
else:
print("Sauvegarde lue.") # seulement si AUCUNE exception
f.close()
finally:
print("Fin de la tentative.") # TOUJOURS exécuté, erreur ou pas
def test(x):
try:
print("A")
r = 10 / x
print("B")
except ZeroDivisionError:
print("C")
else:
print("D")
finally:
print("E")
test(0)
print("---")
test(5)
A C E --- A B D E
Le modèle « redemander jusqu'à ce que ce soit valide »
def demander_entier(message, mini, maxi):
while True:
try:
valeur = int(input(message))
except ValueError:
print("Entre un nombre entier.")
continue
if mini <= valeur <= maxi:
return valeur
print(f"Entre un nombre entre {mini} et {maxi}.")
raise : déclencher un piège
Ta propre fonction peut signaler une situation impossible en levant une exception. C'est le code qui l'appelle qui décide comment réagir.
def retirer_or(heros, montant):
if montant < 0:
raise ValueError("le montant doit être positif")
if montant > heros["or"]:
raise ValueError(f"pas assez d'or ({heros['or']} < {montant})")
heros["or"] -= montant
Tes propres exceptions
class ErreurJeu(Exception): # on hérite d'Exception (tu comprendras au niveau 8)
pass
class ActionImpossible(ErreurJeu):
pass
try:
raise ActionImpossible("le coffre est verrouillé")
except ActionImpossible as e:
print(f"Impossible : {e}.")
Déboguer comme un pro
- Lis le message. Vraiment. En entier.
- Affiche les valeurs :
print(f"{pv=} {type(pv)=}")juste avant la ligne qui plante. - Utilise le débogueur de VS Code : clique à gauche d'un numéro de ligne pour poser un point d'arrêt (un rond rouge), lance avec F5, puis avance ligne par ligne en regardant les variables.
- Explique ton code à un canard en plastique, ligne par ligne, à voix haute. Tu trouveras le bug avant la fin. C'est une vraie technique, le rubber duck debugging.
Défis
La division du trésor
Demande le montant du trésor et le nombre d'aventuriers, puis affiche la part de chacun. Gère la saisie non numérique et la division par zéro avec deux messages différents.
Le grimoire perdu
Demande un nom de fichier et affiche son contenu. Si le fichier n'existe pas, propose de le créer. Utilise un finally pour afficher « Fin de la tentative de lecture ».
Le traceback mystère
Ce code plante. Prédis l'exception et la ligne fautive, puis corrige-le de deux façons : avec .get(), puis avec try/except.
prix = {"potion": 15, "torche": 3}
panier = ["potion", "épée", "torche"]
total = 0
for article in panier:
total += prix[article]
print(total)
La moyenne têtue
Un fichier contient une note par ligne, mais certaines lignes sont abîmées (12, quinze, ligne vide, 14.5). Calcule la moyenne des lignes valides et affiche les numéros des lignes ignorées.
La banque des nains
Crée les exceptions SoldeInsuffisant et MontantInvalide. Écris deposer(compte, montant) et retirer(compte, montant), qui les lèvent. Écris ensuite un menu qui ne plante jamais et qui affiche le message de chaque exception.
Le jeu incassable
- Module
saisie.py:demander_entier(message, mini, maxi)etdemander_choix(message, choix_possibles). Ces deux fonctions redemandent tant que la saisie est invalide. - Module
erreurs.py:ErreurJeu, et ses fillesActionImpossibleetSauvegardeCorrompue. charger_partie()renvoieNonesi le fichier n'existe pas. Elle lèveSauvegardeCorrompuesi le JSON est illisible (json.JSONDecodeError) ou s'il manque des champs obligatoires.boire_potion(heros)etacheter(heros)lèventActionImpossibleavec un message clair. La boucle principale l'attrape et affiche « Impossible : … ».- Si le joueur fait Ctrl+C (
KeyboardInterrupt), on fait une sauvegarde d'urgence. Unfinallyaffiche « À bientôt dans la crypte. »
Sauvegarde abîmée (partie.json illisible (ligne 1)). On repart de zéro. Aria — 40/60 PV, 50 or, sac : ['torche'] Action (boire/acheter/sauver/quitter) : boire Impossible : tu n'as pas de potion. Action (boire/acheter/sauver/quitter) : acheter Combien de potions ? trois « trois » n'est pas un nombre entier. Réessaie.
Bonus : sauvegarde sans risque
Écris d'abord dans partie.json.tmp, puis remplace l'ancienne sauvegarde avec os.replace(...) seulement si l'écriture a réussi. Une coupure de courant pendant l'écriture n'abîmera plus la sauvegarde existante.
Quiz du niveau 7
Quand le bloc else d'un try s'exécute-t-il ?
- Toujours
- Quand une exception a été levée
- Quand aucune exception n'a été levée dans le
try
else s'exécute si tout s'est bien passé. C'est finally qui s'exécute toujours.
Quelle exception lève int("3.5") ?
TypeErrorValueError- Aucune, elle renvoie 3
int() accepte une chaîne, donc le type est bon, mais "3.5" n'est pas un entier valide. int(float("3.5")) fonctionnerait.
Pourquoi except: pass est-il dangereux ?
- Il cache toutes les erreurs, même celles qu'on ne prévoyait pas
- Il ralentit le programme
- Il provoque une SyntaxError
Les bugs deviennent invisibles et le programme continue dans un état incohérent.
Dans une trace d'erreur, où se trouve la ligne qui a levé l'exception ?
- En haut, après « Traceback »
- Juste au-dessus du message final
- Elle n'est pas indiquée
Les appels sont listés du plus ancien au plus récent. Le plus récent, celui qui a planté, est donc en bas.
Que fait raise ValueError("montant négatif") ?
- Il affiche le message et continue
- Il arrête définitivement Python
- Il lève une exception que le code appelant peut attraper avec
except ValueError
raise interrompt la fonction et remonte l'exception jusqu'au premier try qui la gère. S'il n'y en a aucun, le programme s'arrête avec une trace.
La guilde
Guerriers, mages, gobelins, ogres : chacun a des PV et attaque, mais pas de la même manière. La programmation orientée objet est faite pour ça.
- Classe et instance
- __init__ et self
- Encapsulation et @property
- Méthodes spéciales
- Héritage et super()
- Polymorphisme
- Composition
Ton héros est un dictionnaire, tes monstres aussi, et tes fonctions de combat vérifient partout que heros["pv"] ne devient pas négatif. Le jour où tu as ajouté le mage, tu as dû modifier huit fonctions.
Ta mission : rejoindre la guilde des développeurs et modéliser le jeu en objets, avec des personnages qui savent se battre tout seuls.
Pourquoi des objets ?
Jusqu'ici, les données (le dictionnaire du héros) et les actions (les fonctions qui le modifient) étaient séparées. Un objet regroupe les deux. Il sait des choses (ses attributs) et il sait faire des choses (ses méthodes).
| Terme | Image | Dans le jeu |
|---|---|---|
| Classe | le moule, le plan | Monstre |
| Instance (objet) | un exemplaire fabriqué avec le moule | ce gobelin-là, avec ses 18 PV |
| Attribut | une caractéristique | gobelin.pv |
| Méthode | une fonction attachée à l'objet | gobelin.attaquer(heros) |
Ta première classe
class Personnage:
def __init__(self, nom, pv_max, force): # le constructeur : appelé à la création
self.nom = nom # self = l'objet en cours de construction
self.pv_max = pv_max
self.pv = pv_max
self.force = force
def est_vivant(self):
return self.pv > 0
def attaquer(self, cible):
cible.pv -= self.force
print(f"{self.nom} frappe {cible.nom} : -{self.force} PV")
aria = Personnage("Aria", 60, 7) # création d'une instance : __init__ est appelé
gob = Personnage("Gobelin", 18, 4)
aria.attaquer(gob) # équivaut à Personnage.attaquer(aria, gob)
print(gob.pv, gob.est_vivant()) # 11 True
Encapsulation : protéger l'état interne
Rien n'empêche d'écrire gob.pv = -500. Pour garantir qu'un objet reste toujours cohérent, on cache l'attribut et on contrôle son accès.
_pv(un tiret bas) : une convention qui dit « usage interne, n'y touche pas ».__pv(deux tirets bas) : Python renomme l'attribut en_Personnage__pv, ce qui le rend difficile d'accès depuis l'extérieur.@property: un accès qui ressemble à un attribut mais passe par une méthode, dans laquelle on peut vérifier les valeurs.
class Personnage:
def __init__(self, nom, pv_max, force):
self.nom = nom
self.pv_max = pv_max
self.__pv = pv_max
self.force = force
@property
def pv(self): # lecture : print(perso.pv)
return self.__pv
@pv.setter
def pv(self, valeur): # écriture : perso.pv = 12
self.__pv = max(0, min(self.pv_max, valeur)) # toujours entre 0 et pv_max
gob = Personnage("Gobelin", 18, 4)
gob.pv -= 500
print(gob.pv) # 0 : impossible de descendre en dessous
gob.pv += 1000
print(gob.pv) # 18 : impossible de dépasser le max
Les méthodes spéciales
Les méthodes entourées de doubles tirets bas (dunder methods) permettent à tes objets de fonctionner avec les opérateurs et les fonctions natives de Python.
class Monstre(Personnage):
def __str__(self): # print(monstre), str(monstre), f"{monstre}"
return f"{self.nom} [{self.pv}/{self.pv_max} PV]"
def __repr__(self): # affichage technique, dans une liste par exemple
return f"Monstre({self.nom!r}, pv={self.pv})"
def __eq__(self, autre): # monstre1 == monstre2
return self.nom == autre.nom
def __lt__(self, autre): # monstre1 < monstre2, et donc sorted(monstres)
return self.pv_max < autre.pv_max
| Méthode | Déclenchée par |
|---|---|
__init__ | Classe(...) |
__str__ / __repr__ | print(obj) / affichage dans l'interpréteur |
__eq__, __lt__, __gt__ | ==, <, > |
__add__ | a + b |
__len__ | len(obj) |
__contains__ | x in obj |
__iter__ | for x in obj |
Héritage : la classe fille
Un Guerrier est un Heros, qui est un Personnage. La classe fille hérite de tout ce que sait la classe mère, puis ajoute ou modifie ce qui lui est propre.
class Heros(Personnage): # Heros hérite de Personnage
def __init__(self, nom, pv_max, force):
super().__init__(nom, pv_max, force) # on laisse la mère initialiser sa partie
self.or_ = 0 # puis on ajoute ce qui est spécifique
class Mage(Heros):
def __init__(self, nom):
super().__init__(nom, pv_max=60, force=3)
self.mana = 50
def attaquer(self, cible): # on REDÉFINIT la méthode de la mère
if self.mana >= 10:
self.mana -= 10
cible.pv -= 15
print(f"{self.nom} lance une boule de feu sur {cible.nom} !")
else:
super().attaquer(cible) # plus de mana : attaque normale de la mère
classDiagram
Personnage <|-- Heros
Personnage <|-- Monstre
Heros <|-- Guerrier
Heros <|-- Mage
Heros *-- Inventaire
class Personnage {
nom
pv
force
attaquer(cible)
est_vivant()
}
class Mage {
mana
attaquer(cible)
}
class Guerrier {
degats()
}
Polymorphisme : même ordre, réactions différentes
Le code du combat appelle combattant.attaquer(cible) sans savoir s'il a affaire à un guerrier, un mage ou un ogre. Chacun réagit à sa manière. Ajouter une nouvelle classe de héros ne demande donc aucune modification du code du combat.
equipe = [Guerrier("Brom"), Mage("Aria"), Monstre("Ogre", 40, 6)]
for combattant in equipe:
combattant.attaquer(cible) # chaque objet utilise SA version de attaquer
class Animal:
def cri(self):
return "..."
def presenter(self):
return f"Je fais {self.cri()}"
class Chat(Animal):
def cri(self):
return "miaou"
class Chaton(Chat):
pass
for a in [Animal(), Chat(), Chaton()]:
print(a.presenter())
print(isinstance(Chaton(), Animal))
Je fais ... Je fais miaou Je fais miaou True
Composition : « a un » plutôt que « est un »
Un héros n'est pas un inventaire, il a un inventaire. Dans ce cas, on ne passe pas par l'héritage : on met un objet dans un autre.
class Inventaire:
def __init__(self, capacite=10):
self.__objets = []
self.capacite = capacite
def ajouter(self, objet):
if len(self.__objets) >= self.capacite:
raise ValueError("sac plein")
self.__objets.append(objet)
def __contains__(self, objet):
return objet in self.__objets
def __len__(self):
return len(self.__objets)
class Heros(Personnage):
def __init__(self, nom, pv_max, force):
super().__init__(nom, pv_max, force)
self.inventaire = Inventaire() # composition
aria = Heros("Aria", 60, 7)
aria.inventaire.ajouter("potion")
print("potion" in aria.inventaire, len(aria.inventaire)) # True 1
Défis
La monture
Crée une classe Monture avec un nom, une espèce et une vitesse, une méthode __str__, et une méthode temps_trajet(distance) qui renvoie le temps en heures. Crée deux montures et compare leurs temps sur 120 km.
Le coffre de la guilde
Classe Coffre avec un solde privé (__solde), une propriété en lecture seule solde, et les méthodes deposer(montant) et retirer(montant). Ces méthodes lèvent une ValueError si le montant est négatif ou supérieur au solde. Vérifie que coffre.solde = 1000000 échoue.
Le tableau des quêtes
Une classe Quete (description, récompense, terminée ou non) et une classe TableauDesQuetes qui en contient une liste (composition). Méthodes : ajouter, terminer(description), __len__ (le nombre de quêtes restantes) et __iter__ (pour pouvoir faire for q in tableau). Les quêtes s'affichent triées par récompense grâce à __lt__.
Les sortilèges de zone
Une classe mère Zone avec une méthode aire() qui lève NotImplementedError, et ses filles Cercle, Rectangle et Triangle. Pour une liste de sorts, affiche lequel couvre la plus grande surface, avec max(zones, key=...). C'est du polymorphisme.
Quête annexe : le Robot explorateur
Classe Robot (nom, position, batterie, vitesse max) avec se_deplacer(direction, vitesse), qui lève une exception si la vitesse est négative ou trop grande. Ajoute une Enum Direction, puis des filles RobotVolant et RobotSousMarin qui ont chacune un coût énergétique différent. Une méthode se_deplacer_aleatoirement(n) utilise random.choice(list(Direction)).
La guilde des héros
Fichier modeles.py :
Inventaire: liste privée et capacité, avecajouter,retirer,__len__,__contains__,__iter__,__str__.Personnage: nom, pv_max, force, et des PV protégés par une@propertyqui les garde entre 0 et pv_max. Méthodesest_vivant(),degats()(1d6 + force),attaquer(cible),__str__,__repr__.Heros(Personnage): un inventaire (composition), de l'or etboire_potion().Guerrier(Heros): 80 PV, force 7, etdegats()redéfinie avec +5 de rage sous la moitié des PV.Mage(Heros): 60 PV, force 3, 50 de mana, etattaquer()redéfinie pour lancer une boule de feu (10 de mana, 10 à 18 dégâts).Monstre(Personnage): un butin, une propriétédangeret un__lt__, pour quesorted(monstres)range les monstres du moins au plus dangereux.main.py: choix de la classe, puis une fonctionduel(heros, monstre)qui ne contient aucunif isinstance. Le polymorphisme fait tout le travail.
Quiz du niveau 8
Que représente self dans une méthode ?
- La classe elle-même
- L'instance sur laquelle la méthode est appelée
- La classe mère
Dans aria.attaquer(gob), self est aria.
Quelle méthode est appelée par print(monstre) ?
__init____print____str__
print convertit l'objet en texte avec str(), qui appelle __str__.
À quoi sert super().__init__(nom, pv_max, force) dans la classe fille ?
- À exécuter le constructeur de la classe mère pour initialiser ses attributs
- À créer un deuxième objet
- À empêcher l'héritage
Sans cet appel, les attributs définis dans le __init__ de la mère n'existeraient pas dans l'objet.
Un Heros possède un Inventaire. Quelle relation utiliser ?
- L'héritage :
class Heros(Inventaire) - La composition : un attribut
self.inventaire = Inventaire() - Aucune, il faut tout mettre dans une seule classe
« Le héros a un inventaire » se traduit par une composition. L'héritage traduit « est un ».
Qu'est-ce que le polymorphisme ?
- Une classe qui hérite de plusieurs mères
- Un attribut qui change de type
- Des objets de classes différentes qui répondent au même appel de méthode, chacun à sa façon
combattant.attaquer(cible) fonctionne pour un guerrier, un mage ou un ogre, chacun avec son comportement.
L'armurerie
Python est livré avec plus de 200 modules prêts à l'emploi. Avant de forger une arme toi-même, regarde si elle n'est pas déjà accrochée au mur.
- random
- datetime et time
- collections
- os, pathlib, shutil
- Expressions régulières (re)
- Threads (bonus)
L'armurier de la guilde ouvre une porte dérobée. Des centaines d'outils brillent dans la pénombre : « Tout est fourni avec Python. La plupart des apprentis ne viennent jamais ici et passent des heures à tout refaire à la main. »
Ta mission : équiper ton jeu d'un butin à rareté variable, d'un journal de bord horodaté avec statistiques, et d'un analyseur de commandes tapées en langage naturel.
random : le hasard maîtrisé
import random random.randint(1, 20) # entier entre 1 et 20 inclus random.random() # flottant dans [0, 1[ random.choice(["rat", "gobelin", "ogre"]) # un élément au hasard random.sample(range(1, 50), 6) # 6 éléments distincts (un tirage de loto) random.shuffle(paquet) # mélange une liste sur place random.choices(["commun", "rare", "légendaire"], weights=[70, 25, 5])[0] # tirage pondéré random.seed(42) # même graine = même suite de « hasard »
datetime et time : maîtriser le temps
from datetime import datetime, timedelta
import time
maintenant = datetime.now()
print(maintenant.strftime("%d/%m/%Y %H:%M:%S")) # 03/10/2026 14:05:09
print(f"{maintenant:%Y-%m-%d}") # même chose dans une f-string
dans_100_jours = maintenant + timedelta(days=100, hours=10)
fin_quete = datetime.strptime("31/12/2026", "%d/%m/%Y") # texte -> date
reste = fin_quete - maintenant # une timedelta
print(reste.days, "jours restants")
debut = time.perf_counter()
time.sleep(0.5) # pause d'une demi-seconde
print(f"{time.perf_counter() - debut:.2f} s") # mesurer une durée
collections : des structures sur mesure
from collections import Counter, defaultdict
journal = ["attaque", "attaque", "potion", "attaque", "fuite", "potion"]
stats = Counter(journal)
print(stats) # Counter({'attaque': 3, 'potion': 2, 'fuite': 1})
print(stats.most_common(1)) # [('attaque', 3)]
print(Counter("abracadabra").most_common(3))
butin_par_salle = defaultdict(list) # une clé absente reçoit automatiquement []
butin_par_salle["crypte"].append("épée") # pas de KeyError
os, pathlib, shutil : fichiers et dossiers
from pathlib import Path
import shutil
dossier = Path("sauvegardes")
dossier.mkdir(exist_ok=True) # crée le dossier s'il n'existe pas
fichier = dossier / "partie_1.json" # l'opérateur / assemble les chemins
print(fichier.exists(), fichier.suffix, fichier.stem) # False .json partie_1
for chemin in Path(".").glob("*.py"): # tous les .py du dossier courant
print(chemin.name, chemin.stat().st_size, "octets")
shutil.copy("partie.json", dossier / "backup.json") # copier un fichier
Le module plus ancien os fait la même chose avec des fonctions : os.listdir, os.path.exists, os.path.splitext, os.path.join. Tu le croiseras dans beaucoup de code existant.
re : les expressions régulières
Une expression régulière décrit un motif de texte. Elle sert à vérifier, trouver ou extraire des morceaux d'une chaîne.
| Motif | Signification | Exemple qui correspond |
|---|---|---|
\d | un chiffre | 7 |
\w | une lettre, un chiffre ou _ | a |
\s | un espace | |
+ / * / ? | 1 ou plus / 0 ou plus / 0 ou 1 | \d+ → 2026 |
{4} | exactement 4 fois | \d{4} → 1234 |
[aeiou] | un caractère parmi ceux-là | e |
( ) | un groupe à extraire | (\d+)d(\d+) |
^ / $ | début / fin du texte |
import re
texte = "Rapport : ID-4521 suspect, ID-12 invalide, ID-9087 confirmé."
print(re.findall(r"ID-\d{4}\b", texte)) # ['ID-4521', 'ID-9087']
m = re.match(r"^(\d+)d(\d+)([+-]\d+)?$", "2d6+3") # une formule de dés de jeu de rôle
if m:
nombre, faces, bonus = m.groups() # ('2', '6', '+3')
print(re.sub(r"\s+", " ", "trop d' espaces")) # remplacer
import re from collections import Counter cmd = "prendre 3 potions" m = re.match(r"(\w+)\s+(\d+)\s+(\w+)", cmd) print(m.group(1), int(m.group(2)) * 2) print(Counter(cmd.split()[2]).most_common(1))
prendre 6
[('o', 2)]
Pour aller plus loin : faire plusieurs choses à la fois
Un thread est un fil d'exécution. Plusieurs threads avancent « en même temps » dans le même programme. C'est utile quand on attend quelque chose : une réponse réseau, un fichier, le joueur.
import threading
import time
def horloge(nom, intervalle, nombre):
for _ in range(nombre):
time.sleep(intervalle)
print(nom)
t1 = threading.Thread(target=horloge, args=("Tic", 2, 3))
t2 = threading.Thread(target=horloge, args=("Tac", 3, 2))
t1.start(); t2.start() # les deux démarrent
t1.join(); t2.join() # on attend qu'ils aient fini
print("Fin")
Défis
L'éclaireur de dossiers
Demande un chemin de dossier et affiche uniquement les fichiers .txt qu'il contient, avec leur taille. Fais une version avec os.listdir et os.path.splitext, puis une avec pathlib.
Le calendrier du siège
Affiche la date et l'heure actuelles, puis la date exacte dans 100 jours et 10 heures. Calcule aussi combien de jours il reste avant ton anniversaire.
Le sauvetage des parchemins temporaires
Crée un dossier Backup_Temp. Copie dedans, avec shutil.copy, tous les fichiers du dossier courant dont le nom contient « temp », puis liste son contenu.
Le décodeur de messages
Écris extraire_ids(texte), qui renvoie tous les codes de la forme ID-XXXX (4 chiffres exactement) d'un texte. Puis valider_email(adresse). Enfin, avec Counter, affiche les 3 caractères les plus fréquents d'une phrase saisie, espaces exclus.
Tic, Tac et le chronométreur
Lance deux threads : « Tic » toutes les 2 secondes (3 fois) et « Tac » toutes les 3 secondes (2 fois). Mesure la durée totale avec time.perf_counter. Pourquoi le programme dure-t-il environ 6 secondes et non 12 ?
L'armurerie du jeu
Module armurerie.py :
lancer_formule("2d6+3")analyse la formule avecreet lance les dés. Une formule invalide lève uneValueError.analyser_commande(texte)comprend des phrases commeattaquer gobelin,prendre 3 potionsouboire potion, grâce à une regex à groupes nommés(?P<verbe>...). Elle renvoie un dictionnaire, ouNonesi elle ne comprend pas.tirer_butin()tire une rareté pondérée (commun 70, rare 25, légendaire 5) avecrandom.choices, puis un objet de cette rareté.- Une classe
Journalnote les événements avecdatetime.now().statistiques()les compte avecCounter,par_type()les regroupe avecdefaultdict, etexporter()écritjournaux/partie_2026-10-03_14h05.txtavecpathlib.
> attaquer gobelin
Tu attaques gobelin : 9 dégâts.
L'ennemi lâche un objet rare : bouclier runique !
> prendre 3 potions
Tu fais l'action « prendre » x3 sur potions.
> fin
Statistiques : [('attaque', 1), ('butin', 1), ('prendre', 1)]
Journal enregistré dans journaux/partie_2026-10-03_14h05.txt
Quiz du niveau 9
Quel module pour compter les occurrences de chaque élément d'une liste en une ligne ?
mathcollections, avecCounterrandom
Counter(liste) renvoie directement un dictionnaire des effectifs.
Que trouve re.findall(r"\d+", "3 gobelins et 12 rats") ?
[3, 12]['3', '1', '2']['3', '12']
\d+ capture des suites d'au moins un chiffre, renvoyées sous forme de chaînes.
Comment obtenir la date dans une semaine ?
datetime.now() + timedelta(days=7)datetime.now() + 7datetime.now().day + 7
On ajoute une durée (timedelta) à une date. .day + 7 donnerait le 38 du mois…
Que fait Path("saves") / "partie.json" ?
- Une division, donc une erreur
- Il construit le chemin
saves/partie.json, avec le bon séparateur selon le système - Il crée le fichier
pathlib redéfinit l'opérateur / pour assembler les chemins. C'est la méthode spéciale __truediv__, encore elle !
À quoi sert t.join() sur un thread ?
- À démarrer le thread
- À fusionner deux threads
- À attendre que le thread ait fini
start() lance le thread, join() bloque jusqu'à ce qu'il se termine.
Le portail
Jusqu'ici, ton jeu vivait seul sur ta machine. Il est temps d'ouvrir un portail vers le reste du monde.
- pip et environnements virtuels
- Client, serveur, IP, port
- HTTP et les API JSON
- requests
- Scraping responsable
- Sockets TCP
Au fond de la crypte, un cercle de runes s'allume. À travers le portail, on aperçoit d'autres mondes, peuplés de centaines de monstres que tu n'as jamais codés. Et dans la taverne d'à côté, d'autres aventuriers discutent.
Ta mission : invoquer de vrais monstres depuis une API web, et ouvrir une taverne en réseau où les joueurs de la salle peuvent discuter.
Installer des modules : pip et venv
La bibliothèque standard ne fait pas tout. Plus de 500 000 paquets sont publiés sur PyPI, et on les installe avec pip. Pour ne pas mélanger les dépendances de tous tes projets, chaque projet a son environnement virtuel.
python -m venv .venv # crée l'environnement (une seule fois) .venv\Scripts\activate # Windows : active-le source .venv/bin/activate # macOS / Linux python -m pip install requests beautifulsoup4 python -m pip freeze > requirements.txt # note les versions installées python -m pip install -r requirements.txt # réinstalle tout sur une autre machine
Comment les machines se parlent
| Notion | Image | Exemple |
|---|---|---|
| Adresse IP | l'adresse postale d'une machine | 192.168.1.12, 127.0.0.1 (toi-même) |
| Port | le numéro d'appartement : quel programme reçoit | 80 (HTTP), 443 (HTTPS), 22 (SSH) |
| DNS | l'annuaire qui traduit un nom en IP | python.org → 151.101.… |
| Client / serveur | celui qui demande / celui qui répond | ton navigateur / le site web |
| Protocole | la langue et les règles de la conversation | HTTP, TCP, UDP |
TCP ou UDP ?
| TCP | UDP | |
|---|---|---|
| Connexion | établie avant d'échanger (une « poignée de main ») | aucune : on envoie directement |
| Fiabilité | tout arrive, dans l'ordre, sinon c'est renvoyé | des paquets peuvent se perdre ou arriver dans le désordre |
| Vitesse | plus lent (contrôles, accusés de réception) | plus rapide, en-tête plus léger |
| Usage | web, e-mail, transfert de fichiers, chat | jeux en ligne, visio, streaming, DNS |
HTTP : le langage du web
HTTP fonctionne en requête / réponse. Le client envoie une méthode et une adresse, le serveur répond avec un code de statut et un contenu. Chaque requête est indépendante : on dit que HTTP est sans état, d'où les cookies pour se souvenir de toi.
| Méthode | Rôle |
|---|---|
GET | lire une ressource |
POST | créer, envoyer des données |
PUT / PATCH | remplacer / modifier |
DELETE | supprimer |
Codes de statut : 2xx succès (200 OK, 201 Created), 3xx redirection, 4xx erreur du client (400, 401, 403, 404 Not Found, 429 trop de requêtes), 5xx erreur du serveur (500).
requests : interroger une API
Une API web est un serveur qui renvoie des données brutes, le plus souvent en JSON, au lieu de pages HTML. Exemple avec l'API gratuite de Donjons & Dragons :
import requests
reponse = requests.get("https://www.dnd5eapi.co/api/2014/monsters/goblin", timeout=5)
print(reponse.status_code) # 200
gobelin = reponse.json() # le JSON devient un dict Python
print(gobelin["name"], gobelin["hit_points"], "PV")
print(gobelin["actions"][0]["damage"][0]["damage_dice"]) # '1d6+2' : ta regex du niveau 9 !
# paramètres d'URL : ?challenge_rating=1
r = requests.get("https://www.dnd5eapi.co/api/2014/monsters",
params={"challenge_rating": 1}, timeout=5)
noms = [m["name"] for m in r.json()["results"]]
Télécharger un fichier binaire
r = requests.get("https://www.python.org/static/img/python-logo.png", timeout=10)
with open("logo.png", "wb") as f:
f.write(r.content) # .content : les octets bruts (.text : le texte décodé)
Le web scraping, avec respect
Quand un site n'a pas d'API, on peut lire son HTML et en extraire les données. C'est le scraping. Avant de commencer :
- Lis les conditions d'utilisation et le fichier
/robots.txtdu site. - Espace tes requêtes avec
time.sleep(1): un script trop rapide ressemble à une attaque. - Identifie-toi avec un en-tête
User-Agenthonnête. - Ne collecte pas de données personnelles : le RGPD s'applique. Les contenus restent aussi protégés par le droit d'auteur.
Le site books.toscrape.com a été créé exprès pour s'entraîner :
import requests
from bs4 import BeautifulSoup
page = requests.get("https://books.toscrape.com/", timeout=10)
soupe = BeautifulSoup(page.content, "html.parser") # .content : BeautifulSoup détecte l'encodage
for livre in soupe.select("article.product_pod")[:5]:
titre = livre.h3.a["title"]
prix = livre.select_one(".price_color").text
print(f"{prix:>8} {titre}")
Pour les sites qui construisent leur page en JavaScript, il faut piloter un vrai navigateur avec Selenium ou Playwright.
Les sockets : parler directement en TCP
Sous HTTP, il y a TCP. Avec le module socket, tu écris toi-même le client et le serveur.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as serveur: # SOCK_STREAM = TCP
serveur.bind(("127.0.0.1", 5000)) # IP et port d'écoute
serveur.listen()
print("En attente d'un client…")
client, adresse = serveur.accept() # bloque jusqu'à une connexion
with client:
while data := client.recv(1024): # jusqu'à 1024 octets
client.sendall(data.upper()) # on renvoie en majuscules
import socket
with socket.create_connection(("127.0.0.1", 5000)) as s:
s.sendall("bonjour la crypte".encode()) # str -> bytes
print(s.recv(1024).decode()) # bytes -> str : BONJOUR LA CRYPTE
Défis
La vigie
Pour une liste d'URL, affiche si chaque site est accessible, son code de statut et son temps de réponse (reponse.elapsed). Un site injoignable ne doit pas faire planter le programme.
Le bestiaire en ligne
Demande un nom de monstre en anglais (goblin, owlbear…) et affiche sa fiche (PV, classe d'armure, attaques) depuis l'API D&D. Gère le cas d'un monstre inconnu (code 404).
Le collectionneur de grimoires
Sur books.toscrape.com, récupère le titre, le prix et la note (en étoiles) des livres des 3 premières pages. Enregistre-les dans un CSV, puis affiche le livre le mieux noté au prix le plus bas. Attends 1 seconde entre chaque page.
L'écho en UDP
Réécris le serveur d'écho en UDP (SOCK_DGRAM, recvfrom, sendto). Qu'est-ce qui disparaît par rapport à TCP ?
Le scanner de la sentinelle
Sur ta propre machine (127.0.0.1), teste les ports 1 à 1024 avec socket.connect_ex et un timeout court, puis affiche ceux qui sont ouverts. Accélère le scan avec concurrent.futures.ThreadPoolExecutor et compare les durées.
Le portail des mondes
- Crée un environnement virtuel et installe
requests. invocation.py:lister_monstres(niveau)interroge l'API avec le paramètrechallenge_rating.invoquer(index)télécharge une fiche et la convertit en dictionnaire du jeu (nom, PV, attaque, formule de dégâts, XP). Garde la première action qui inflige réellement des dégâts.- Toute erreur réseau, ou un statut différent de 200, lève une exception
PortailFerme. Le jeu l'attrape et se rabat sur le bestiaire local du niveau 6. - Les dégâts du monstre utilisent ta fonction
lancer_formuledu niveau 9 sur la formule renvoyée par l'API. - La taverne : un serveur TCP qui accepte plusieurs joueurs (un thread par client) et diffuse chaque message à tous les autres. Un client envoie d'abord son pseudo, puis ses messages. Testez-le à toute la classe sur le réseau de la salle !
Un Giant Vulture (22 PV) traverse le portail ! Il utilise Beak (2d4+2) : 10 dégâts. * Aria pousse la porte de la taverne. * Brom pousse la porte de la taverne. [Brom] Une bière pour tout le monde !
Indice pour la taverne
Garde un dictionnaire {socket: pseudo} partagé, protégé par un threading.Lock(). Pour diffuser, parcours une copie des sockets (list(clients)) et ignore ceux qui ont fermé leur connexion (OSError).
Quiz du niveau 10
Pourquoi utiliser un environnement virtuel ?
- Pour que Python aille plus vite
- Pour isoler les paquets et leurs versions d'un projet à l'autre
- Pour pouvoir utiliser Internet
Chaque projet garde ses propres versions de bibliothèques, sans conflit avec les autres.
Que signifie un code HTTP 404 ?
- Erreur interne du serveur
- Accès interdit
- Ressource introuvable
4xx désigne une erreur côté client, et 404 une adresse qui ne correspond à rien.
Quel protocole pour un jeu en ligne qui envoie la position des joueurs 60 fois par seconde ?
- UDP
- TCP
- HTTP
La vitesse compte plus que la fiabilité : une position perdue est vite remplacée par la suivante.
Que renvoie reponse.json() avec requests ?
- Une chaîne de caractères JSON
- Des objets Python (dictionnaires, listes…) construits à partir du JSON
- Un fichier .json
C'est l'équivalent de json.loads(reponse.text).
Avant d'envoyer la chaîne msg sur une socket, il faut…
- la mettre en majuscules
- l'écrire dans un fichier
- la convertir en octets avec
msg.encode()
Les sockets ne transportent que des bytes.
La fenêtre magique
Fini le terminal. Ton donjon va s'afficher dans une vraie fenêtre, 60 fois par seconde.
- Installer Pygame
- La boucle de jeu
- Coordonnées et couleurs
- Dessiner des formes
- Contrôler les FPS
Le mage de la guilde trace un rectangle dans l'air. Une fenêtre s'ouvre, noire et silencieuse. « Tout ce qui apparaîtra ici, c'est toi qui le dessineras. Image par image. »
Ta mission : ouvrir une fenêtre et y dessiner la carte du donjon à partir d'une liste de chaînes, comme au niveau 5.
Qu'est-ce que Pygame ?
Pygame est une bibliothèque pour créer des jeux 2D : fenêtre, dessin, images, clavier, souris, sons. Elle est simple, multiplateforme et utilisée depuis plus de 20 ans par des milliers de débutants. On l'installe dans un environnement virtuel, comme au niveau 10 :
python -m pip install pygame python -m pygame.examples.aliens # un jeu de démonstration pour vérifier l'installation
La boucle de jeu
Tout jeu vidéo est une boucle infinie qui répète trois étapes, des dizaines de fois par seconde :
- Événements : qu'a fait le joueur ? (touches, souris, fermeture de la fenêtre)
- Mise à jour : on fait avancer le monde (positions, collisions, score).
- Dessin : on efface l'écran et on redessine tout.
import pygame
pygame.init()
ecran = pygame.display.set_mode((800, 600)) # largeur, hauteur en pixels
pygame.display.set_caption("Mon jeu")
horloge = pygame.time.Clock()
en_cours = True
while en_cours:
# 1. Événements
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT: # clic sur la croix
en_cours = False
# 2. Mise à jour
# ...
# 3. Dessin
ecran.fill((20, 22, 30)) # efface avec une couleur de fond
pygame.draw.circle(ecran, (240, 170, 70), (400, 300), 40)
pygame.display.flip() # affiche l'image qu'on vient de dessiner
horloge.tick(60) # au maximum 60 images par seconde
pygame.quit()
Coordonnées et couleurs
- L'origine
(0, 0)est en haut à gauche.xaugmente vers la droite, etyaugmente vers le bas, à l'inverse des maths. - Une couleur est un tuple
(rouge, vert, bleu), chaque composante allant de 0 à 255.(255, 0, 0)est rouge,(0, 0, 0)noir,(255, 255, 255)blanc.
pygame.draw.rect(ecran, (60, 64, 82), (x, y, largeur, hauteur)) # rectangle plein pygame.draw.rect(ecran, (0, 0, 0), (x, y, largeur, hauteur), 2) # contour de 2 px pygame.draw.circle(ecran, couleur, (cx, cy), rayon) pygame.draw.line(ecran, couleur, (x1, y1), (x2, y2), epaisseur) pygame.draw.polygon(ecran, couleur, [(10, 0), (20, 10), (10, 20), (0, 10)]) # un losange
Images par seconde
horloge.tick(60) fait patienter la boucle pour ne pas dépasser 60 tours par seconde. Sans ça, ton jeu tournerait à 2000 FPS sur un PC puissant et à 40 sur un vieux portable : un personnage qui avance de 3 pixels par tour n'irait pas à la même vitesse partout.
Défis
Le damier du roi
Dessine un damier de 8 × 8 cases de 60 pixels, en alternant deux couleurs.
Indice
Une case est claire si (ligne + colonne) % 2 == 0.
La boule rebondissante
Une boule part du centre avec une vitesse (vx, vy) et rebondit sur les 4 bords de la fenêtre. Change sa couleur à chaque rebond.
Indice
Si x - rayon < 0 ou x + rayon > LARGEUR, alors vx = -vx.
Le crépuscule
Remplis la fenêtre d'un dégradé vertical qui va de l'orange (en haut) au violet (en bas), en dessinant une ligne par pixel de hauteur. Ajoute ensuite un soleil qui descend lentement.
Le donjon à l'écran
Reprends la carte du niveau 5. Chaque caractère devient une tuile de 48 pixels : # en gris pierre, . en sol sombre, avec un contour noir d'un pixel. La taille de la fenêtre se calcule à partir de la taille de la carte.
TUILE = 48
CARTE = [
"############",
"#..........#",
"#.###..###.#",
"#..........#",
"#.#..##..#.#",
"#..........#",
"############",
]
LARGEUR, HAUTEUR = len(CARTE[0]) * TUILE, len(CARTE) * TUILE
COULEURS = {"#": (60, 64, 82), ".": (28, 30, 40)}
# dans la boucle, partie Dessin :
for l, ligne in enumerate(CARTE):
for c, case in enumerate(ligne):
rect = pygame.Rect(c * TUILE, l * TUILE, TUILE, TUILE)
pygame.draw.rect(ecran, COULEURS[case], rect)
pygame.draw.rect(ecran, (0, 0, 0), rect, 1)
Quiz Pygame 1
Dans Pygame, où se trouve le point (0, 0) ?
- Au centre de la fenêtre
- En haut à gauche
- En bas à gauche
Et y augmente en descendant.
Que fait pygame.display.flip() ?
- Il retourne l'image à l'envers
- Il efface l'écran
- Il affiche à l'écran tout ce qui a été dessiné pendant ce tour
On dessine sur une image cachée, puis flip() l'affiche d'un coup. Ça évite le scintillement.
Quelles sont, dans l'ordre, les trois étapes de la boucle de jeu ?
- Événements, mise à jour, dessin
- Dessin, événements, mise à jour
- Mise à jour, dessin, événements
On lit les actions du joueur, on fait évoluer le monde, puis on le dessine.
Quelle couleur donne (255, 255, 0) ?
- Blanc
- Jaune
- Cyan
Rouge et vert au maximum, sans bleu : en synthèse additive, ça donne du jaune.
À quoi sert horloge.tick(60) ?
- À afficher l'heure
- À attendre 60 secondes
- À limiter la boucle à 60 tours par seconde
Le jeu tourne ainsi à la même vitesse sur toutes les machines.
Le héros s'éveille
Un donjon vide, c'est joli mais ennuyeux. Fais apparaître ton héros et donne-lui des commandes.
- Événements clavier
- Touches maintenues
- La souris
- Surface et Rect
- La classe Sprite
- Charger des images
Une petite boule orange cligne des yeux au milieu de la carte. Elle attend tes ordres.
Ta mission : déplacer le héros avec les flèches ou ZQSD sans qu'il sorte de l'écran, et poser des torches d'un clic de souris.
Deux façons de lire le clavier
| Méthode | Quand | Usage |
|---|---|---|
evenement.type == pygame.KEYDOWN | une fois, au moment de l'appui | sauter, tirer, ouvrir un menu |
pygame.key.get_pressed() | à chaque image, tant que la touche est enfoncée | se déplacer en continu |
for evenement in pygame.event.get():
if evenement.type == pygame.KEYDOWN and evenement.key == pygame.K_SPACE:
print("Saut !") # une seule fois par appui
touches = pygame.key.get_pressed() # l'état de TOUT le clavier
if touches[pygame.K_LEFT]:
x -= vitesse # à chaque image
La souris
for evenement in pygame.event.get():
if evenement.type == pygame.MOUSEBUTTONDOWN and evenement.button == 1: # clic gauche
torches.append(evenement.pos) # (x, y) du clic
x, y = pygame.mouse.get_pos() # position actuelle
Surface et Rect : les deux briques de Pygame
- Une
Surfaceest une image : la fenêtre en est une, chaque sprite aussi. - Un
Rectest un rectangle : position et taille. Il a des attributs très pratiques commex,y,center,top,bottom,left,rightoutopleft.
image = pygame.Surface((32, 32), pygame.SRCALPHA) # SRCALPHA : fond transparent pygame.draw.circle(image, (240, 170, 70), (16, 16), 15) rect = image.get_rect(center=(400, 300)) # un Rect de la taille de l'image rect.x += 5 # on déplace le rectangle ecran.blit(image, rect) # on colle l'image à cet endroit
La classe Sprite
Ton niveau 8 sert enfin à quelque chose de visible ! Un sprite est un objet du jeu qui hérite de pygame.sprite.Sprite et possède deux attributs obligatoires : image et rect.
class Heros(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((32, 32), pygame.SRCALPHA)
pygame.draw.circle(self.image, (240, 170, 70), (16, 16), 15)
self.rect = self.image.get_rect(center=(x, y))
self.vitesse = 4
def update(self, touches):
if touches[pygame.K_LEFT] or touches[pygame.K_q]:
self.rect.x -= self.vitesse
if touches[pygame.K_RIGHT] or touches[pygame.K_d]:
self.rect.x += self.vitesse
if touches[pygame.K_UP] or touches[pygame.K_z]:
self.rect.y -= self.vitesse
if touches[pygame.K_DOWN] or touches[pygame.K_s]:
self.rect.y += self.vitesse
self.rect.clamp_ip(ecran.get_rect()) # reste à l'intérieur de l'écran
Charger une vraie image
image = pygame.image.load("images/heros.png").convert_alpha() # convert_alpha : rapide + transparence
image = pygame.transform.scale(image, (48, 48))
image_gauche = pygame.transform.flip(image, True, False) # miroir horizontal
Défis
Le familier
Un petit cercle suit le pointeur de la souris en permanence. Ajoute un effet de traîne : il ne se téléporte pas, il parcourt 10 % de la distance qui le sépare de la souris à chaque image.
Le peintre runique
Maintenir le clic gauche dessine, le clic droit efface tout, et les touches 1 à 5 changent la couleur du pinceau.
Le sprint
Le héros court deux fois plus vite tant que Shift est enfoncé, et se tourne vers la gauche ou la droite selon sa direction (image retournée avec pygame.transform.flip).
Le héros dans le donjon
- Une classe
Heros(pygame.sprite.Sprite), dessinée avec des cercles (un corps et deux yeux). - Déplacement continu aux flèches et en ZQSD, sans jamais sortir de la fenêtre.
- Un clic gauche pose une torche (un petit cercle orange) à l'endroit cliqué. Les torches sont stockées dans une liste.
- Échap quitte le jeu.
Quiz Pygame 2
Pour un déplacement fluide tant qu'une touche est maintenue, on utilise…
- l'événement
KEYDOWN pygame.key.get_pressed()input()
KEYDOWN ne se déclenche qu'une fois par appui.
Quels sont les deux attributs indispensables d'un pygame.sprite.Sprite ?
imageetrectxetysurfaceetposition
Les groupes de sprites s'en servent pour dessiner (image) au bon endroit (rect).
Que fait ecran.blit(image, rect) ?
- Il crée une nouvelle image
- Il efface l'image
- Il copie l'image sur l'écran, à la position du rect
« Blit » vient de block transfer : la copie d'un bloc de pixels.
À quoi sert pygame.SRCALPHA à la création d'une Surface ?
- À la rendre plus rapide
- À lui donner un fond transparent
- À la mettre en noir et blanc
Sans transparence, ton héros rond serait dessiné dans un carré noir.
Le héros sort de l'écran par la droite. Quelle ligne l'en empêche ?
self.rect.x = 0self.vitesse = 0self.rect.clamp_ip(ecran.get_rect())
clamp_ip recale le rectangle à l'intérieur d'un autre (ip : sur place).
Murs et slimes
Les murs doivent arrêter le héros, les gemmes doivent se ramasser, et les slimes doivent faire mal. Tout ça, ce sont des collisions.
- Groupes de sprites
- Collisions rectangle contre rectangle
- Glisser le long des murs
- Animer avec le temps
- Planches de sprites
Des gemmes bleues flottent dans les couloirs. Entre elles, des slimes verts rebondissent d'un mur à l'autre en gargouillant.
Ta mission : un héros qui ne traverse plus les murs, des gemmes à ramasser et des slimes qui patrouillent.
Les groupes de sprites
Un pygame.sprite.Group est un ensemble de sprites. On peut tous les mettre à jour, tous les dessiner, et tester les collisions avec eux, en une seule ligne.
murs = pygame.sprite.Group()
gemmes = pygame.sprite.Group()
for l, ligne in enumerate(CARTE):
for c, case in enumerate(ligne):
if case == "#":
murs.add(Mur(c * TUILE, l * TUILE))
elif case == "G":
gemmes.add(Gemme(c * TUILE + 24, l * TUILE + 24))
gemmes.update(temps) # appelle update(temps) sur chaque gemme
murs.draw(ecran) # blit de chaque image à son rect
Détecter les collisions
| Fonction | Renvoie |
|---|---|
rect1.colliderect(rect2) | True si les deux rectangles se chevauchent |
rect.collidepoint(x, y) | True si le point est dedans (un clic sur un bouton, par exemple) |
pygame.sprite.spritecollideany(sprite, groupe) | le premier sprite touché, ou None |
pygame.sprite.spritecollide(sprite, groupe, dokill) | la liste des sprites touchés. Avec dokill=True, ils sont retirés du groupe. |
pygame.sprite.groupcollide(g1, g2, k1, k2) | les collisions entre deux groupes (balles contre ennemis…) |
ramassees = pygame.sprite.spritecollide(heros, gemmes, dokill=True)
score += 10 * len(ramassees)
if pygame.sprite.spritecollideany(heros, slimes):
vies -= 1
Glisser le long des murs
Si on déplace le héros en diagonale puis qu'on annule tout le mouvement dès qu'il touche un mur, il reste collé. L'astuce : bouger un axe à la fois, et après chaque axe, recaler le héros contre le mur touché.
def deplacer(self, dx, dy, murs):
self.rect.x += dx * self.vitesse
for mur in pygame.sprite.spritecollide(self, murs, False):
if dx > 0: # on allait à droite : on se cale à gauche du mur
self.rect.right = mur.rect.left
elif dx < 0:
self.rect.left = mur.rect.right
self.rect.y += dy * self.vitesse
for mur in pygame.sprite.spritecollide(self, murs, False):
if dy > 0:
self.rect.bottom = mur.rect.top
elif dy < 0:
self.rect.top = mur.rect.bottom
Avec dx = touches[K_RIGHT] - touches[K_LEFT], on obtient -1, 0 ou 1 : un booléen vaut 1 quand il est vrai et 0 quand il est faux.
Animer
Avec le temps et les maths
pygame.time.get_ticks() donne le nombre de millisecondes écoulées depuis le lancement. Avec un sinus, on obtient une oscillation douce, parfaite pour une gemme qui flotte ou un slime qui respire.
temps = pygame.time.get_ticks() / 1000 # en secondes self.rect.centery = self.base_y + math.sin(temps * 4) * 4 # oscille de ±4 pixels
Avec une planche de sprites
Une animation dessinée, c'est une suite d'images posées côte à côte sur une seule « planche ». On en découpe une à la fois avec subsurface et on change d'image toutes les N millisecondes.
planche = pygame.image.load("heros_marche.png").convert_alpha() # 4 images de 32x32 côte à côte
images = [planche.subsurface((i * 32, 0, 32, 32)) for i in range(4)]
indice = (pygame.time.get_ticks() // 120) % len(images) # une nouvelle image toutes les 120 ms
self.image = images[indice]
Défis
Le bouton du menu
Dessine un rectangle « Jouer » qui change de couleur quand la souris le survole (collidepoint) et affiche « Lancement ! » dans la console quand on clique dessus.
La pluie de météores
Des carrés tombent du haut de l'écran à des positions aléatoires. Le joueur, en bas, les esquive à gauche et à droite. Chaque météore touché fait perdre une vie. Utilise un groupe et spritecollide.
Le slime chasseur
Ajoute un slime rouge qui se dirige vers le héros au lieu de rebondir au hasard. Il calcule le vecteur héros − slime, le normalise et avance de 1,5 pixel par image. Utilise pygame.math.Vector2.
Le donjon vivant
- Dans la carte,
Gplace une gemme etSun slime. Crée les classesMur,Gemme,SlimeetHeros, et répartis les sprites dans trois groupes. - Le héros se déplace axe par axe et glisse le long des murs.
- Les gemmes flottent (sinusoïde, chacune avec un décalage aléatoire pour ne pas être synchronisées).
- Les slimes avancent en ligne droite et font demi-tour quand ils touchent un mur. Ils s'écrasent et s'étirent avec le temps.
- Une gemme ramassée disparaît (
dokill=True). Un slime touché renvoie le héros à son point de départ.
Quiz Pygame 3
Que fait spritecollide(heros, gemmes, True) ?
- Il supprime le héros
- Il renvoie
Truesi une gemme est touchée - Il renvoie la liste des gemmes touchées et les retire du groupe
Le troisième argument, dokill, retire les sprites touchés de tous leurs groupes.
Pourquoi déplacer le héros un axe à la fois ?
- Pour savoir de quel côté il a touché le mur, et pouvoir glisser le long
- Pour aller plus vite
- Parce que Pygame ne gère pas les diagonales
Si on bouge les deux axes ensemble, on ne sait pas s'il faut corriger x ou y.
Avec indice = (get_ticks() // 100) % 4, à quelle fréquence change l'image ?
- Toutes les 4 ms
- Toutes les 100 ms
- Toutes les 400 ms
La division entière par 100 change de valeur toutes les 100 ms. Le modulo 4 fait boucler sur les 4 images.
Quelle méthode teste si un clic en (x, y) tombe sur un bouton ?
bouton.colliderect(x, y)bouton.collidepoint(x, y)bouton.clamp(x, y)
collidepoint teste un point, colliderect un autre rectangle.
Que vaut True - False en Python ?
- Une erreur
True1
Les booléens se comportent comme les entiers 1 et 0. C'est pratique pour calculer une direction.
Dungeon Dash
Un jeu, c'est aussi un écran titre, un score, des sons, un game over et l'envie de rejouer pour battre son record.
- Sons et musique
- Fabriquer ses propres sons
- Afficher du texte
- Vies et invincibilité
- Machine à états
- Sauvegarder un record
Le roi de la crypte lance un défi : ramasser toutes les gemmes de trois salles, avec seulement trois vies. Les meilleurs scores seront gravés dans la pierre.
Ta mission : transformer ta démo en jeu d'arcade complet et rejouable.
Les sons
pygame.mixer.init()
son_gemme = pygame.mixer.Sound("sons/gemme.wav") # effet court, en mémoire
son_gemme.play()
pygame.mixer.music.load("musique.ogg") # musique, lue en continu depuis le disque
pygame.mixer.music.set_volume(0.4)
pygame.mixer.music.play(-1) # -1 : en boucle
Fabriquer ses propres sons, sans rien installer
Un son est une suite de nombres qui décrivent une onde. Avec le module standard wave, tu peux écrire un fichier .wav toi-même :
import math, struct, wave
FREQ = 22050 # échantillons par seconde
def bip(f_debut, f_fin, duree):
n = int(FREQ * duree)
for i in range(n):
f = f_debut + (f_fin - f_debut) * i / n # la note glisse
onde = 1 if math.sin(2 * math.pi * f * i / FREQ) > 0 else -1 # onde carrée : son « 8 bits »
yield onde * (1 - i / n) * 0.5 # le volume décroît
with wave.open("sons/gemme.wav", "wb") as f:
f.setnchannels(1); f.setsampwidth(2); f.setframerate(FREQ)
f.writeframes(b"".join(struct.pack("<h", int(e * 20000)) for e in bip(880, 1760, 0.12)))
Afficher du texte
police = pygame.font.Font(None, 32) # None : police par défaut
texte = police.render(f"Score {score}", True, (235, 235, 245)) # True : lissage
ecran.blit(texte, texte.get_rect(center=(LARGEUR // 2, 20)))
Vies et invincibilité temporaire
Sans protection, un slime touché enlève une vie à chaque image : 60 vies par seconde ! Après un coup, le héros devient invincible pendant 1,5 s et clignote.
maintenant = pygame.time.get_ticks()
if maintenant >= heros.invincible_jusqua and pygame.sprite.spritecollideany(heros, slimes):
vies -= 1
heros.invincible_jusqua = maintenant + 1500
# clignotement : une image sur deux toutes les 100 ms
visible = maintenant >= heros.invincible_jusqua or (maintenant // 100) % 2 == 0
La machine à états
Ton jeu passe par plusieurs écrans : menu, jeu, fin. Une variable etat indique l'écran actif. La boucle de jeu ne met à jour et ne dessine que ce qui le concerne.
stateDiagram-v2
[*] --> menu
menu --> jeu : Entrée
jeu --> jeu : toutes les gemmes ramassées / niveau suivant
jeu --> fin : plus de vies
fin --> jeu : Entrée
fin --> [*] : Échap
class Jeu:
def __init__(self):
# init de Pygame, fenêtre, polices, sons, record
self.etat = "menu"
def nouvelle_partie(self): ... # score 0, 3 vies, niveau 0, etat = "jeu"
def charger_niveau(self): ... # construit les groupes depuis NIVEAUX[self.niveau]
def mettre_a_jour(self): ... # déplacements, collisions, score, changement d'état
def dessiner(self): ... # selon self.etat : menu, jeu ou fin
def lancer(self):
while True:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
pygame.quit()
return
if evenement.type == pygame.KEYDOWN and evenement.key == pygame.K_RETURN and self.etat != "jeu":
self.nouvelle_partie()
if self.etat == "jeu":
self.mettre_a_jour()
self.dessiner()
self.horloge.tick(60)
if __name__ == "__main__":
Jeu().lancer()
Le record, gravé dans un fichier
def lire_record():
try:
with open("record.json", encoding="utf-8") as f:
return json.load(f)["record"]
except (FileNotFoundError, KeyError, json.JSONDecodeError):
return 0
Fichiers (niveau 6), exceptions (niveau 7), classes (niveau 8), structures de données (niveau 5) : ton jeu d'arcade utilise tout ce que tu as appris.
Défis
Le chronomètre
Affiche le temps écoulé dans le niveau, en haut à droite, au format 01:23. Donne un bonus de score si le niveau est fini en moins de 30 secondes.
La pause
Ajoute un état "pause" déclenché par P : le jeu se fige, un voile semi-transparent couvre l'écran (une Surface avec set_alpha) et « PAUSE » s'affiche au centre.
Le top 5 gravé
Remplace le record unique par un top 5 sauvegardé en JSON. En fin de partie, si le score y entre, le joueur saisit ses initiales (3 lettres, avec des événements KEYDOWN et evenement.unicode) avant d'afficher le tableau.
Dungeon Dash
- Lance
generer_sons.pypour créersons/gemme.wav,aie.wav,niveau.wavetgame_over.wav. Charge-les de façon robuste. - Trois niveaux (des listes de chaînes) dans une liste
NIVEAUX. Quand toutes les gemmes sont ramassées : +50 points et niveau suivant. Après le dernier niveau, on reprend au premier avec des slimes plus rapides. - Une gemme vaut +10 et joue un son. Toucher un slime coûte une vie, joue un son et donne 1,5 s d'invincibilité avec clignotement.
- Un bandeau en haut affiche le score, les vies, le niveau et le record.
- Trois états : menu (titre et record), jeu, fin (score et « Entrée pour rejouer »). Le record est sauvegardé dans
record.json.
Quiz Pygame 4
Quelle est la différence entre mixer.Sound et mixer.music ?
- Aucune
Soundcharge un effet court en mémoire,musiclit une piste longue en continu depuis le disquemusicne lit que les MP3
On peut jouer plusieurs Sound en même temps, mais une seule musique à la fois.
Pourquoi rendre le héros invincible quelques instants après un coup ?
- Sinon la collision dure plusieurs images et enlève plusieurs vies d'un coup
- Pour que le jeu soit plus facile
- Parce que Pygame l'exige
À 60 FPS, un contact d'une demi-seconde, c'est 30 collisions.
Que renvoie police.render("Score", True, couleur) ?
- Rien : le texte s'affiche directement
- Une chaîne
- Une Surface qu'il faut ensuite coller avec
blit
Le texte devient une image, comme n'importe quel sprite.
À quoi sert la variable etat dans la classe Jeu ?
- À stocker le score
- À savoir quel écran est actif (menu, jeu, fin) pour adapter la mise à jour et le dessin
- À compter les images
C'est une machine à états, le modèle de base de tous les jeux et de beaucoup d'interfaces.
Que se passe-t-il si sons/gemme.wav n'existe pas et que le chargement n'est pas protégé ?
- Le jeu plante au démarrage
- Pygame joue un son par défaut
- Rien
Le chargement lève une exception. D'où le try/except et l'objet Silence.
Ton propre jeu
Tu as toutes les armes. À toi de créer ton jeu, en binôme, versionné avec Git et présenté à l'oral.
- Concevoir avant de coder
- Travailler à deux avec Git
- Livrer un jeu jouable
- Défendre ses choix techniques
Choisis ta quête
| Jeu | Difficulté | Le cœur technique | Pour aller plus loin |
|---|---|---|---|
| Pong | ★ | rebonds, deux joueurs sur un même clavier, score | IA adverse, balle qui accélère |
| Snake | ★★ | déplacement sur grille, une liste pour le corps, collision avec soi-même | murs, niveaux, mode deux serpents |
| Casse-briques | ★★ | angle de rebond selon le point d'impact, niveaux en listes de chaînes | bonus qui tombent, briques résistantes |
| Frogger | ★★★ | files de véhicules à vitesses différentes, plateformes mobiles | chronomètre, vies, niveaux |
| Space shooter | ★★★ | fond qui défile, tirs, vagues d'ennemis, explosions animées | boss avec motifs de tir |
| Ton idée | ? | à valider avec l'enseignant avant la 2e séance |
Les étapes du projet
Le plan de bataille
Une page qui décrit les règles, les contrôles, un croquis de l'écran et la liste des classes prévues. Découpe le travail en tâches et répartis-les dans le binôme.
Le dépôt Git
Un dépôt partagé avec un README.md (comment installer et jouer), un requirements.txt et un .gitignore (.venv/, __pycache__/). Chacun travaille sur sa branche et fusionne régulièrement.
Le prototype jouable
À mi-parcours : le joueur bouge, l'élément principal fonctionne, et le jeu ne plante pas. Le reste viendra ensuite. Un jeu simple qui marche vaut mieux qu'un jeu ambitieux qui plante.
Le polissage
Sons, écran titre, game over, score ou record, réglage de la difficulté. Fais tester ton jeu à un autre binôme et corrige ce qui l'a gêné.
La soutenance
10 minutes : une démo en direct, l'architecture du code (montre une classe dont tu es fier), une difficulté rencontrée et la façon dont tu l'as résolue, puis les questions.
Le barème (20 points)
| Critère | Détail | Points |
|---|---|---|
| Git | Commits réguliers, messages clairs, contributions des deux membres | 2 |
| Branches, README complet | 2 | |
| Qualité du code | Organisation en modules et en classes | 2 |
| Lisibilité, nommage, commentaires utiles | 1 | |
| Respect de PEP 8 | 1 | |
| Fonctionnement | Jeu fonctionnel, sans crash | 2 |
| Respect des règles annoncées | 1 | |
| Fluidité des contrôles | 1 | |
| Esthétique et son | Graphismes lisibles et cohérents | 2 |
| Sons et musique pertinents (sources citées) | 2 | |
| Oral | Présentation claire et structurée | 2 |
| Justification des choix techniques | 1 | |
| Réponses aux questions | 1 |
Préparer un raid
Un raid est un TD de synthèse de 4 heures, en binôme, sur des données réelles. Quatre techniques avancées y reviennent souvent. Les voici en version courte.
- Décorateurs
- @classmethod
- Gestionnaire de contexte maison
- ThreadPoolExecutor
- Variables d'environnement
Les décorateurs : enchanter une fonction
En Python, une fonction est une valeur comme une autre : on peut la passer en paramètre et en renvoyer une. Un décorateur est une fonction qui prend une fonction et renvoie une version améliorée de celle-ci.
from datetime import datetime
from functools import wraps
def journaliser(fonction):
@wraps(fonction) # conserve le nom et la docstring d'origine
def enveloppe(*args, **kwargs): # accepte n'importe quels arguments
print(f"[LOG] {fonction.__name__} appelée à {datetime.now():%H:%M:%S}")
resultat = fonction(*args, **kwargs)
return resultat
return enveloppe
@journaliser # équivaut à : soigner = journaliser(soigner)
def soigner(pv, quantite):
return pv + quantite
soigner(40, 25) # [LOG] soigner appelée à 14:05:09 -> 65
Un décorateur avec un argument
Il faut un étage de plus : une fonction qui reçoit l'argument et fabrique le décorateur.
import time
def chronometrer(seuil=0.5):
def decorateur(fonction):
@wraps(fonction)
def enveloppe(*args, **kwargs):
debut = time.perf_counter()
resultat = fonction(*args, **kwargs)
duree = time.perf_counter() - debut
if duree > seuil:
print(f"[WARNING] {fonction.__name__} : {duree:.2f} s")
return resultat
return enveloppe
return decorateur
@chronometrer(seuil=0.1)
def requete_lente():
time.sleep(0.3)
@classmethod : un constructeur alternatif
class Monstre:
def __init__(self, nom, pv):
self.nom, self.pv = nom, pv
@classmethod
def depuis_json(cls, data): # cls = la classe elle-même
return cls(data["name"], data["hit_points"])
ogre = Monstre.depuis_json({"name": "Ogre", "hit_points": 59})
Ton propre with
Une classe qui définit __enter__ et __exit__ s'utilise avec with. __exit__ est toujours appelée, même en cas d'exception.
import csv
class Rapport:
def __init__(self, chemin):
self.chemin = chemin
def __enter__(self):
self.fichier = open(self.chemin, "a", newline="", encoding="utf-8")
self.ecrivain = csv.writer(self.fichier, delimiter=";")
return self # c'est ce que reçoit « as r »
def ecrire(self, *valeurs):
self.ecrivain.writerow(valeurs)
def __exit__(self, type_exc, exc, trace):
self.fichier.close()
return False # False : on laisse remonter l'éventuelle exception
with Rapport("alertes.csv") as r:
r.ecrire("BTC", 84642.01, "CRITICAL")
Lancer plusieurs requêtes en parallèle
from concurrent.futures import ThreadPoolExecutor
symboles = ["BTCUSDT", "ETHUSDT", "SOLUSDT"]
with ThreadPoolExecutor(max_workers=5) as pool:
resultats = list(pool.map(recuperer_prix, symboles)) # même ordre que symboles
Trois requêtes de 300 ms chacune prennent environ 900 ms à la suite, et environ 300 ms en parallèle.
Les secrets dans un fichier .env
API_KEY=ma-cle-secrete-de-test
import os
from dotenv import load_dotenv # python -m pip install python-dotenv
load_dotenv()
API_KEY = os.getenv("API_KEY")
if API_KEY is None:
raise RuntimeError("API_KEY manquante : crée un fichier .env")
Le comptoir du marchand
Une application qui récupère des prix en ligne, les modélise en objets, les filtre et les archive.
Le marchand de la guilde a entendu parler de pièces d'or « numériques » dont le cours change chaque seconde. Il veut un comptoir qui surveille les prix et garde un historique.
Étape 1 · Le comptoir s'installe (30 min)
Crée le dossier du projet, un environnement virtuel, et installe requests. Dans utils.py, écris un décorateur @log_execution qui affiche le nom de la fonction appelée et l'heure de l'appel.
Étape 2 · La pièce (45 min)
Dans models.py, une classe Asset avec les attributs privés __symbol, __price et __timestamp. Un getter (ou une @property) pour le prix, sans setter. print(asset) affiche Asset BTC : 84642.01 $ (MàJ à 14:05).
Étape 3 · Le coursier (1 h)
fetch_price(symbol), décorée par @log_execution, interroge https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT et renvoie un Asset. Absence de réseau ou symbole invalide (statut 400) : le programme ne plante pas.
Si Binance est inaccessible
CoinGecko fonctionne sans clé : https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur.
Étape 4 · Le grand livre (1 h)
Un portefeuille, c'est une liste d'Asset. Avec une compréhension de liste, construis expensive_assets à partir d'un seuil saisi par l'utilisateur. Sauvegarde l'historique en CSV ou en JSON, avec with.
Étape 5 · La vigie (45 min)
Le programme se rafraîchit toutes les 10 secondes (time.sleep). Bonus : un thread par symbole, pour que les appels réseau ne se bloquent pas entre eux.
La ménagerie
Un outil qui récupère des données d'une API, modélise des images en objets et les télécharge en parallèle.
Les chats de la crypte sont les seuls à ne pas craindre les rats géants. La guilde veut un catalogue illustré de chaque recrue potentielle.
Étape 1 · Le registre (30 min)
Environnement virtuel et requests. Dans utils.py, un décorateur @trace_action qui affiche [LOG] Exécution de : {nom} à {heure}. Niveau + : il compte combien de fois chaque fonction a été appelée, grâce à un dictionnaire.
Étape 2 · La fiche du chat (45 min)
Dans models.py, une classe CatImage avec __id, __url, __width et __local_path (initialement None). Des getters pour l'URL et l'ID. __str__ renvoie [Chat ID: eb2] - URL: … (1200px).
Étape 3 · La capture (1 h 15)
fetch_cat_data() interroge https://api.thecatapi.com/v1/images/search?limit=5 et renvoie une liste de CatImage. download_cat(cat), décorée par @trace_action, télécharge l'image (.content) dans downloads/{id}.jpg en mode "wb". Les erreurs de connexion sont gérées.
Étape 4 · Le catalogue (45 min)
Avec une compréhension, construis large_cats, la liste des images de plus de 1000 px de large. Enregistre les téléchargements réussis dans downloads_history.csv (colonnes ID, URL, Local_Path).
Étape 5 · La meute (45 min)
Chaque téléchargement se fait dans un thread séparé. Toutes les 20 secondes, le programme redemande 5 nouvelles images. Mesure le gain de temps par rapport à la version séquentielle.
La sentinelle
Un système de surveillance de marché qui analyse les variations de prix en temps réel, déclenche des alertes et archive les données de façon structurée.
Le trésor de la guilde est investi. La sentinelle ne dort jamais : à la moindre variation suspecte, elle sonne l'alarme et consigne tout dans ses registres.
Étape 1 · Sceaux et chronomètres (45 min)
Environnement virtuel, requests et python-dotenv. Une clé d'API fictive dans .env, chargée par config.py. Dans utils.py, un décorateur @monitor_performance(threshold=0.5) qui écrit dans logs.txt la date, le nom de la fonction, sa durée et ses arguments, et qui affiche [WARNING] Lenteur détectée ! au-delà du seuil.
Étape 2 · Les entités (1 h)
Une classe de base TradeEntity (attributs privés __uuid et __timestamp), dont hérite CryptoAsset (__symbol, __price). Le prix est une @property dont le setter refuse les valeurs <= 0. Ajoute @classmethod from_json(cls, data) et __gt__ pour comparer deux actifs.
Étape 3 · L'acquisition (1 h)
fetch_market_data(symbols) interroge l'API pour plusieurs symboles. L'exception personnalisée APIConnectionError est levée si le statut n'est pas 200. Avec une seule compréhension de dictionnaire, construis market_summary (symbole → CryptoAsset) pour les actifs dont le volume dépasse un seuil.
Étape 4 · Les registres (45 min)
Une classe ReportManager, gestionnaire de contexte (__enter__ / __exit__), écrit alerts_history.csv avec csv.DictWriter. Une variation de plus de 5 % depuis la dernière lecture ajoute Status = CRITICAL.
Étape 5 · La veille (30 min)
Les mises à jour passent par un ThreadPoolExecutor. Le script tourne indéfiniment, se rafraîchit toutes les 15 secondes et affiche un tableau récapitulatif aligné dans la console. Ctrl+C l'arrête proprement.
Les quêtes libres
Des projets plus longs, à choisir en binôme, qui utilisent Python comme le font les professionnels de la sécurité et de l'outillage.
| Quête | Objectif | Notions clés |
|---|---|---|
| Le décrypteur | Chiffrer et déchiffrer avec César, Vigenère et la substitution, puis casser César par analyse de fréquences. | chaînes, dictionnaires, Counter |
| Le coffre-fort | Un gestionnaire de mots de passe local, chiffré par un mot de passe maître. | fichiers, hashlib, cryptography, getpass |
| Le gardien d'intégrité | Calculer l'empreinte SHA-256 de chaque fichier d'un dossier et détecter toute modification ultérieure. | pathlib, binaire, JSON |
| L'analyste de journaux | Analyser des journaux système (connexions échouées, IP suspectes) et produire un rapport. | re, collections, datetime, CSV |
| L'épreuve de force | Un serveur génère un mot de passe aléatoire, un client le casse par force brute. Mesure l'effet de la longueur et de l'alphabet sur le temps. | sockets, itertools, threads |
| Le lecteur de grimoires binaires | Extraire les informations utiles d'un exécutable PE ou ELF : en-têtes, sections, architecture. | binaire, struct, POO |
| Le cartographe de disques | Réimplémenter fdisk -l : lire le MBR d'une image disque et afficher la table des partitions. | binaire, struct, hexadécimal |
| Le comparateur | Récupérer régulièrement le prix d'un produit sur plusieurs sites, l'historiser et alerter en cas de baisse. | scraping, CSV ou SQLite, planification |