PyQuest Rat de cave
Cours de Python · Première année

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.

ÉtapeCe que tu fais
QuêteUne situation du jeu qui pose le problème du niveau.
NotionsLe cours, avec des exemples à copier dans ton éditeur et à modifier.
Prédis la sortieTu 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.
BossLa nouvelle pièce de ton jeu, à coder avec les notions du niveau.
QuizCinq 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.

Prologue

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 programmeEn CEn 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

  1. 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).
  2. VS Code : installe Visual Studio Code, puis l'extension Python de Microsoft (onglet Extensions, à gauche). PyCharm Community fonctionne aussi très bien.
  3. 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é » ?

  1. Il faut le traduire en anglais avant de l'exécuter
  2. Le code est exécuté directement, ligne par ligne, sans étape de compilation séparée
  3. 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 ?

  1. 3.4
  2. 2
  3. 3
  4. 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é ?

  1. Une faute de frappe dans le nom de la fonction
  2. Python n'est pas installé
  3. 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 # ?

  1. Elle affiche un titre
  2. Rien : c'est un commentaire, ignoré par Python
  3. 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 ?

  1. Sur la première ligne
  2. Au milieu, avec le ^
  3. 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.

Niveau 1

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_max oui, 2pv non.
  • Majuscules et minuscules comptent : Pv et pv sont 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_vie plutôt que x.
  • Interdit : les mots-clés du langage (if, for, class, or…). D'où or_ pour stocker de l'or !

Les quatre types de base

TypeNomExemples dans le jeu
intentierpv = 60, niveau = 3, or_ = -5
floatnombre à virgulechance_critique = 0.15, poids = 12.5
strchaîne de caractèresnom = "Aria", classe = 'mage'
boolbooléenest_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érateurRôleExempleRésultat
+ - *addition, soustraction, multiplication3 * 412
/division7 / 23.5
//division entière7 // 23
%reste (modulo)7 % 21
**puissance2 ** 38

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 :

  1. Demande le nom, l'âge, la force (1 à 10) et l'agilité (1 à 10) du héros.
  2. Calcule les PV (50 + force × 5) et la puissance (force × 2 + agilité). Le héros commence avec 10 pièces d'or.
  3. Stocke dans un booléen si le héros est majeur.
  4. 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 ?

  1. int
  2. float
  3. str

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 ?

  1. 13
  2. 16
  3. 11

5 + 3 = 8, puis 8 × 2 = 16.

Lequel de ces noms de variable est valide et respecte la convention Python ?

  1. pv_max
  2. 2_pv
  3. pv-max
  4. class

2_pv commence par un chiffre, pv-max serait lu comme une soustraction et class est un mot-clé.

Que vaut "Crypte"[-2] ?

  1. "C"
  2. "e"
  3. "y"
  4. "t"

Les indices négatifs partent de la fin : -1 est "e", -2 est "t".

Que vaut f"{10 / 4:.1f}" ?

  1. "2"
  2. "2.5"
  3. "2.50"

:.1f affiche un seul chiffre après la virgule.

Niveau 2

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érateurSignificationExemple
==égal àclasse == "mage"
!=différent depv != 0
< <= > >=inférieur, supérieur (ou égal)force >= 5
inest 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.")
ABA and BA or Bnot A
TrueTrueTrueTrueFalse
TrueFalseFalseTrueFalse
FalseTrueFalseTrueTrue
FalseFalseFalseFalseTrue
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.

  1. Demande la direction : gauche, droite ou tout droit. Accepte les majuscules et les espaces en trop.
  2. 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.
  3. Affiche le bilan avec une expression ternaire : « en forme » au-dessus de 50 PV, « blessé » sinon.
  4. 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")
  1. A
  2. B
  3. A puis B
  4. C

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 ?

  1. False
  2. Une erreur
  3. 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 ?

  1. Parce que "oui" seul est toujours considéré comme vrai
  2. Parce que or n'existe pas en Python
  3. 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 _: ?

  1. À ignorer la variable
  2. À traiter tous les cas non prévus par les motifs précédents
  3. À 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 ?

  1. 0 < note < 20
  2. note >= 0 or note <= 20
  3. 0 <= 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.

Niveau 3

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
AppelValeurs 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

  • break sort immédiatement de la boucle.
  • continue abandonne 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

  1. Le héros a 60 PV (max 60) et 2 potions. Une boucle for enchaîne 3 salles. Dans la salle n, un squelette a 15 + n × 10 PV.
  2. Une boucle while gè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.
  3. S'il n'y a plus de potion, on redemande sans que le monstre attaque (continue). La fuite quitte le combat (break).
  4. Si le monstre est encore debout, il riposte (3 à 9 dégâts).
  5. 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 ?

  1. 5
  2. 8
  3. 4
  4. 10

Les valeurs sont 2, 4, 6, 8. Le 10 est exclu.

Que fait continue ?

  1. Il passe directement au tour suivant de la boucle
  2. Il sort de la boucle
  3. 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
  1. while exige un break
  2. pv -= 1 n'est pas indenté, il est donc hors de la boucle
  3. pv devrait 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 ?

  1. 7
  2. 12
  3. 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 » ?

  1. for i in range(10)
  2. for lettre in mot_de_passe
  3. while

On ne sait pas combien d'essais il faudra : c'est le cas typique du while.

Niveau 4

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 :

  1. des.py : lancer(faces=6), lancer_plusieurs(nombre, faces=6), est_critique(chance=0.15), plus un bloc de test sous if __name__ == "__main__":.
  2. combat.py : calculer_degats(force, bonus=0) (2d6 + force // 2, doublés en cas de critique), barre_de_vie(pv, pv_max, largeur=20), et combattre(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.
  3. main.py : une fonction main() 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 ?

  1. 0
  2. None
  3. Une erreur
  4. 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) ?

  1. 10
  2. 50
  3. 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)))
  1. fini puis 3
  2. 2
  3. 3

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__": ?

  1. À rendre le fichier plus rapide
  2. À exécuter du code seulement quand on lance ce fichier directement, pas quand on l'importe
  3. À 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)
  1. Un cas de base qui arrête les appels
  2. Un return à la fin
  3. 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.

Niveau 5

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 ?

StructureSyntaxeOrdonnéeModifiableDoublonsDans le jeu
Liste list["épée", "potion"]ouiouiouil'inventaire
Tuple tuple(3, 4)ouinonouiune position (ligne, colonne)
Ensemble set{"rat", "gobelin"}nonouinonles salles déjà visitées
Dictionnaire dict{"nom": "Aria", "pv": 60}ordre d'insertionouiclés uniquesla 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

  1. Le héros devient un dictionnaire : nom, pv, pv_max, force, or, inventaire (une liste) et position (un tuple).
  2. La carte est une liste de chaînes convertie en liste de listes. # = mur, M = monstre, P = potion, C = coffre, S = sortie.
  3. Les déplacements z/q/s/d passent par un dictionnaire DIRECTIONS qui associe chaque touche à un tuple (dl, dc). On ne peut pas traverser les murs.
  4. Les cases visitées sont stockées dans un ensemble. Les cases non visitées s'affichent ? : c'est le brouillard de guerre.
  5. 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(...).
  6. La touche i affiche 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 ?

  1. Un ensemble
  2. Un dictionnaire
  3. 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] ?

  1. ["a", "b", "c"]
  2. ["b", "c"]
  3. ["b", "c", "d"]

De l'indice 1 inclus à l'indice 3 exclu.

heros = {"nom": "Aria"}. Que fait heros["pv"] ?

  1. Il lève une KeyError
  2. Il renvoie None
  3. 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}) ?

  1. 6
  2. 3
  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] ?

  1. [0, 2, 4, 6, 8]
  2. [2, 6]
  3. [0, 2, 4]
  4. [0, 4, 8]

Les x pairs sont 0, 2 et 4, multipliés par 2 : 0, 4, 8.

Niveau 6

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
ModeEffetSi le fichier n'existe pas
"r"lecture (mode par défaut)FileNotFoundError
"w"écriture : efface le contenu existantil est créé
"a"ajout à la fin (append)il est créé
"rb", "wb"lecture et écriture en binaireidem

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 :

  1. sauvegarder_partie(heros) et charger_partie() en JSON. Convertis la position (tuple vers liste à l'écriture, liste vers tuple à la lecture).
  2. enregistrer_score(nom, score) ajoute une ligne à scores.csv. meilleurs_scores(n=5) renvoie les n meilleurs, triés.
  3. charger_bestiaire() lit monstres.txt (format nom,pv,force,or, en ignorant les lignes vides et celles qui commencent par #) et renvoie une liste de dictionnaires.
  4. Au lancement du jeu : si partie.json existe (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 ?

  1. "w"
  2. "a"
  3. "r"

"a" (append) écrit à la fin. "w" commencerait par tout effacer.

Quel est l'avantage principal de with open(...) as f: ?

  1. Le fichier est fermé automatiquement, même en cas d'erreur
  2. La lecture est plus rapide
  3. 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) ?

  1. Le tuple (3, 4)
  2. La chaîne "(3, 4)"
  3. 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 ?

  1. int
  2. str
  3. float

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 ?

  1. Parce qu'il est trop gros
  2. Parce que charger un pickle peut exécuter du code malveillant
  3. 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.

Niveau 7

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

FamilleQuandExemple
Syntaxeavant même l'exécution : Python ne comprend pas le texteparenthèse oubliée, : manquant
Exécution (exceptions)pendant l'exécution, sur une opération impossibleint("douze"), division par 0
Logiquejamais 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

ExceptionCause typique
ValueErrorint("abc") : bon type, mauvaise valeur
TypeError"PV : " + 10 : types incompatibles
NameErrorvariable non définie ou mal orthographiée
IndexErrorsac[10] sur une liste de 3 éléments
KeyErrorheros["mana"] sur une clé absente
ZeroDivisionErrordivision par zéro
FileNotFoundErrorouverture 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
  1. La dernière ligne donne le type et le message : on a additionné une chaîne et un nombre.
  2. Juste au-dessus : où ça a planté (ligne 3, dans calculer_pv).
  3. En remontant : le chemin des appels. main a appelé calculer_pv avec force… qui venait d'un input() jamais converti en int.

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

  1. Module saisie.py : demander_entier(message, mini, maxi) et demander_choix(message, choix_possibles). Ces deux fonctions redemandent tant que la saisie est invalide.
  2. Module erreurs.py : ErreurJeu, et ses filles ActionImpossible et SauvegardeCorrompue.
  3. charger_partie() renvoie None si le fichier n'existe pas. Elle lève SauvegardeCorrompue si le JSON est illisible (json.JSONDecodeError) ou s'il manque des champs obligatoires.
  4. boire_potion(heros) et acheter(heros) lèvent ActionImpossible avec un message clair. La boucle principale l'attrape et affiche « Impossible : … ».
  5. Si le joueur fait Ctrl+C (KeyboardInterrupt), on fait une sauvegarde d'urgence. Un finally affiche « À 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 ?

  1. Toujours
  2. Quand une exception a été levée
  3. 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") ?

  1. TypeError
  2. ValueError
  3. 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 ?

  1. Il cache toutes les erreurs, même celles qu'on ne prévoyait pas
  2. Il ralentit le programme
  3. 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 ?

  1. En haut, après « Traceback »
  2. Juste au-dessus du message final
  3. 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") ?

  1. Il affiche le message et continue
  2. Il arrête définitivement Python
  3. 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.

Niveau 8

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).

TermeImageDans le jeu
Classele moule, le planMonstre
Instance (objet)un exemplaire fabriqué avec le moulece gobelin-là, avec ses 18 PV
Attributune caractéristiquegobelin.pv
Méthodeune fonction attachée à l'objetgobelin.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éthodeDé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 :

  1. Inventaire : liste privée et capacité, avec ajouter, retirer, __len__, __contains__, __iter__, __str__.
  2. Personnage : nom, pv_max, force, et des PV protégés par une @property qui les garde entre 0 et pv_max. Méthodes est_vivant(), degats() (1d6 + force), attaquer(cible), __str__, __repr__.
  3. Heros(Personnage) : un inventaire (composition), de l'or et boire_potion().
  4. Guerrier(Heros) : 80 PV, force 7, et degats() redéfinie avec +5 de rage sous la moitié des PV. Mage(Heros) : 60 PV, force 3, 50 de mana, et attaquer() redéfinie pour lancer une boule de feu (10 de mana, 10 à 18 dégâts).
  5. Monstre(Personnage) : un butin, une propriété danger et un __lt__, pour que sorted(monstres) range les monstres du moins au plus dangereux.
  6. main.py : choix de la classe, puis une fonction duel(heros, monstre) qui ne contient aucun if isinstance. Le polymorphisme fait tout le travail.

Quiz du niveau 8

Que représente self dans une méthode ?

  1. La classe elle-même
  2. L'instance sur laquelle la méthode est appelée
  3. La classe mère

Dans aria.attaquer(gob), self est aria.

Quelle méthode est appelée par print(monstre) ?

  1. __init__
  2. __print__
  3. __str__

print convertit l'objet en texte avec str(), qui appelle __str__.

À quoi sert super().__init__(nom, pv_max, force) dans la classe fille ?

  1. À exécuter le constructeur de la classe mère pour initialiser ses attributs
  2. À créer un deuxième objet
  3. À 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 ?

  1. L'héritage : class Heros(Inventaire)
  2. La composition : un attribut self.inventaire = Inventaire()
  3. 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 ?

  1. Une classe qui hérite de plusieurs mères
  2. Un attribut qui change de type
  3. 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.

Niveau 9

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.

MotifSignificationExemple qui correspond
\dun chiffre7
\wune lettre, un chiffre ou _a
\sun 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 :

  1. lancer_formule("2d6+3") analyse la formule avec re et lance les dés. Une formule invalide lève une ValueError.
  2. analyser_commande(texte) comprend des phrases comme attaquer gobelin, prendre 3 potions ou boire potion, grâce à une regex à groupes nommés (?P<verbe>...). Elle renvoie un dictionnaire, ou None si elle ne comprend pas.
  3. tirer_butin() tire une rareté pondérée (commun 70, rare 25, légendaire 5) avec random.choices, puis un objet de cette rareté.
  4. Une classe Journal note les événements avec datetime.now(). statistiques() les compte avec Counter, par_type() les regroupe avec defaultdict, et exporter() écrit journaux/partie_2026-10-03_14h05.txt avec pathlib.
> 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 ?

  1. math
  2. collections, avec Counter
  3. random

Counter(liste) renvoie directement un dictionnaire des effectifs.

Que trouve re.findall(r"\d+", "3 gobelins et 12 rats") ?

  1. [3, 12]
  2. ['3', '1', '2']
  3. ['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 ?

  1. datetime.now() + timedelta(days=7)
  2. datetime.now() + 7
  3. datetime.now().day + 7

On ajoute une durée (timedelta) à une date. .day + 7 donnerait le 38 du mois…

Que fait Path("saves") / "partie.json" ?

  1. Une division, donc une erreur
  2. Il construit le chemin saves/partie.json, avec le bon séparateur selon le système
  3. 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 ?

  1. À démarrer le thread
  2. À fusionner deux threads
  3. À attendre que le thread ait fini

start() lance le thread, join() bloque jusqu'à ce qu'il se termine.

Niveau 10

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

NotionImageExemple
Adresse IPl'adresse postale d'une machine192.168.1.12, 127.0.0.1 (toi-même)
Portle numéro d'appartement : quel programme reçoit80 (HTTP), 443 (HTTPS), 22 (SSH)
DNSl'annuaire qui traduit un nom en IPpython.org → 151.101.…
Client / serveurcelui qui demande / celui qui répondton navigateur / le site web
Protocolela langue et les règles de la conversationHTTP, TCP, UDP

TCP ou UDP ?

TCPUDP
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
Vitesseplus lent (contrôles, accusés de réception)plus rapide, en-tête plus léger
Usageweb, e-mail, transfert de fichiers, chatjeux 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éthodeRôle
GETlire une ressource
POSTcréer, envoyer des données
PUT / PATCHremplacer / modifier
DELETEsupprimer

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.txt du site.
  • Espace tes requêtes avec time.sleep(1) : un script trop rapide ressemble à une attaque.
  • Identifie-toi avec un en-tête User-Agent honnê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

  1. Crée un environnement virtuel et installe requests.
  2. invocation.py : lister_monstres(niveau) interroge l'API avec le paramètre challenge_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.
  3. 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.
  4. Les dégâts du monstre utilisent ta fonction lancer_formule du niveau 9 sur la formule renvoyée par l'API.
  5. 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 ?

  1. Pour que Python aille plus vite
  2. Pour isoler les paquets et leurs versions d'un projet à l'autre
  3. Pour pouvoir utiliser Internet

Chaque projet garde ses propres versions de bibliothèques, sans conflit avec les autres.

Que signifie un code HTTP 404 ?

  1. Erreur interne du serveur
  2. Accès interdit
  3. 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 ?

  1. UDP
  2. TCP
  3. HTTP

La vitesse compte plus que la fiabilité : une position perdue est vite remplacée par la suivante.

Que renvoie reponse.json() avec requests ?

  1. Une chaîne de caractères JSON
  2. Des objets Python (dictionnaires, listes…) construits à partir du JSON
  3. Un fichier .json

C'est l'équivalent de json.loads(reponse.text).

Avant d'envoyer la chaîne msg sur une socket, il faut…

  1. la mettre en majuscules
  2. l'écrire dans un fichier
  3. la convertir en octets avec msg.encode()

Les sockets ne transportent que des bytes.

Pygame · Étape 1

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 :

  1. Événements : qu'a fait le joueur ? (touches, souris, fermeture de la fenêtre)
  2. Mise à jour : on fait avancer le monde (positions, collisions, score).
  3. 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. x augmente vers la droite, et y augmente 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) ?

  1. Au centre de la fenêtre
  2. En haut à gauche
  3. En bas à gauche

Et y augmente en descendant.

Que fait pygame.display.flip() ?

  1. Il retourne l'image à l'envers
  2. Il efface l'écran
  3. 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 ?

  1. Événements, mise à jour, dessin
  2. Dessin, événements, mise à jour
  3. 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) ?

  1. Blanc
  2. Jaune
  3. Cyan

Rouge et vert au maximum, sans bleu : en synthèse additive, ça donne du jaune.

À quoi sert horloge.tick(60) ?

  1. À afficher l'heure
  2. À attendre 60 secondes
  3. À limiter la boucle à 60 tours par seconde

Le jeu tourne ainsi à la même vitesse sur toutes les machines.

Pygame · Étape 2

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éthodeQuandUsage
evenement.type == pygame.KEYDOWNune fois, au moment de l'appuisauter, tirer, ouvrir un menu
pygame.key.get_pressed()à chaque image, tant que la touche est enfoncéese 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 Surface est une image : la fenêtre en est une, chaque sprite aussi.
  • Un Rect est un rectangle : position et taille. Il a des attributs très pratiques comme x, y, center, top, bottom, left, right ou topleft.
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

  1. Une classe Heros(pygame.sprite.Sprite), dessinée avec des cercles (un corps et deux yeux).
  2. Déplacement continu aux flèches et en ZQSD, sans jamais sortir de la fenêtre.
  3. Un clic gauche pose une torche (un petit cercle orange) à l'endroit cliqué. Les torches sont stockées dans une liste.
  4. Échap quitte le jeu.

Quiz Pygame 2

Pour un déplacement fluide tant qu'une touche est maintenue, on utilise…

  1. l'événement KEYDOWN
  2. pygame.key.get_pressed()
  3. input()

KEYDOWN ne se déclenche qu'une fois par appui.

Quels sont les deux attributs indispensables d'un pygame.sprite.Sprite ?

  1. image et rect
  2. x et y
  3. surface et position

Les groupes de sprites s'en servent pour dessiner (image) au bon endroit (rect).

Que fait ecran.blit(image, rect) ?

  1. Il crée une nouvelle image
  2. Il efface l'image
  3. 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 ?

  1. À la rendre plus rapide
  2. À lui donner un fond transparent
  3. À 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 ?

  1. self.rect.x = 0
  2. self.vitesse = 0
  3. self.rect.clamp_ip(ecran.get_rect())

clamp_ip recale le rectangle à l'intérieur d'un autre (ip : sur place).

Pygame · Étape 3

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

FonctionRenvoie
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

  1. Dans la carte, G place une gemme et S un slime. Crée les classes Mur, Gemme, Slime et Heros, et répartis les sprites dans trois groupes.
  2. Le héros se déplace axe par axe et glisse le long des murs.
  3. Les gemmes flottent (sinusoïde, chacune avec un décalage aléatoire pour ne pas être synchronisées).
  4. Les slimes avancent en ligne droite et font demi-tour quand ils touchent un mur. Ils s'écrasent et s'étirent avec le temps.
  5. 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) ?

  1. Il supprime le héros
  2. Il renvoie True si une gemme est touchée
  3. 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 ?

  1. Pour savoir de quel côté il a touché le mur, et pouvoir glisser le long
  2. Pour aller plus vite
  3. 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 ?

  1. Toutes les 4 ms
  2. Toutes les 100 ms
  3. 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 ?

  1. bouton.colliderect(x, y)
  2. bouton.collidepoint(x, y)
  3. bouton.clamp(x, y)

collidepoint teste un point, colliderect un autre rectangle.

Que vaut True - False en Python ?

  1. Une erreur
  2. True
  3. 1

Les booléens se comportent comme les entiers 1 et 0. C'est pratique pour calculer une direction.

Pygame · Étape 4

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

  1. Lance generer_sons.py pour créer sons/gemme.wav, aie.wav, niveau.wav et game_over.wav. Charge-les de façon robuste.
  2. 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.
  3. 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.
  4. Un bandeau en haut affiche le score, les vies, le niveau et le record.
  5. 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 ?

  1. Aucune
  2. Sound charge un effet court en mémoire, music lit une piste longue en continu depuis le disque
  3. music ne 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 ?

  1. Sinon la collision dure plusieurs images et enlève plusieurs vies d'un coup
  2. Pour que le jeu soit plus facile
  3. Parce que Pygame l'exige

À 60 FPS, un contact d'une demi-seconde, c'est 30 collisions.

Que renvoie police.render("Score", True, couleur) ?

  1. Rien : le texte s'affiche directement
  2. Une chaîne
  3. 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 ?

  1. À stocker le score
  2. À savoir quel écran est actif (menu, jeu, fin) pour adapter la mise à jour et le dessin
  3. À 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é ?

  1. Le jeu plante au démarrage
  2. Pygame joue un son par défaut
  3. Rien

Le chargement lève une exception. D'où le try/except et l'objet Silence.

Projet final

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

JeuDifficultéLe cœur techniquePour aller plus loin
Pong★rebonds, deux joueurs sur un même clavier, scoreIA adverse, balle qui accélère
Snake★★déplacement sur grille, une liste pour le corps, collision avec soi-mêmemurs, niveaux, mode deux serpents
Casse-briques★★angle de rebond selon le point d'impact, niveaux en listes de chaînesbonus qui tombent, briques résistantes
Frogger★★★files de véhicules à vitesses différentes, plateformes mobileschronomètre, vies, niveaux
Space shooter★★★fond qui défile, tirs, vagues d'ennemis, explosions animéesboss 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èreDétailPoints
GitCommits réguliers, messages clairs, contributions des deux membres2
Branches, README complet2
Qualité du codeOrganisation en modules et en classes2
Lisibilité, nommage, commentaires utiles1
Respect de PEP 81
FonctionnementJeu fonctionnel, sans crash2
Respect des règles annoncées1
Fluidité des contrôles1
Esthétique et sonGraphismes lisibles et cohérents2
Sons et musique pertinents (sources citées)2
OralPrésentation claire et structurée2
Justification des choix techniques1
Réponses aux questions1
Avant les raids

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")
Raid 1 · 4 heures · binôme

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.

Raid 2 · 4 heures · binôme

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.

Raid 3 · 4 heures · binôme · difficile

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.

Projets libres · binôme

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êteObjectifNotions clés
Le décrypteurChiffrer 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-fortUn 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 journauxAnalyser des journaux système (connexions échouées, IP suspectes) et produire un rapport.re, collections, datetime, CSV
L'épreuve de forceUn 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 binairesExtraire les informations utiles d'un exécutable PE ou ELF : en-têtes, sections, architecture.binaire, struct, POO
Le cartographe de disquesRéimplémenter fdisk -l : lire le MBR d'une image disque et afficher la table des partitions.binaire, struct, hexadécimal
Le comparateurRé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