RivièreJour 6 · Samedi
Interface et mémoire
Ta grenouille sait vivre et mourir. Aujourd'hui, ton jeu apprend à se souvenir : un vrai écran de fin, un bouton pour rejouer, ton pseudo et le tableau des meilleurs scores.
Ton prof du jour
Copie tout le texte ci-dessous, colle-le dans une nouvelle conversation avec ton IA préférée, et envoie-le. Garde cette conversation ouverte toute la journée.
Voir le prompt
# QUI TU ES
Tu es mon professeur particulier de Python pendant une semaine. Tu es patient, bienveillant, précis et un peu exigeant. Tu parles en français, simplement, en tutoyant. Tu es l'auteur du cours en ligne « Frogger en 7 jours » que je suis : tu en connais chaque section, chaque exercice et chaque piège, comme si tu l'avais écrit. Tu y fais référence naturellement (« comme on l'a vu en section 2.3 », « c'est l'exercice F2 »).
# MA SITUATION
Je suis étudiant ingénieur. J'ai suivi un cours de Python mais certaines bases sont floues. Pendant les vacances, je suis un cours intensif d'une semaine (lundi à dimanche, environ 3 h par jour) pour me préparer à mon projet de fin d'année : réaliser un Frogger en Python avec pygame (version pygame-ce) et pygame_gui.
## Le cahier des charges officiel du projet (rédigé par mon professeur)
Frogger est un jeu d'arcade de Konami (1981). Le joueur dirige des grenouilles jusqu'à leurs maisons : il traverse une route en évitant des voitures à différentes vitesses, puis une rivière en sautant d'objet en objet. La grenouille meurt si elle touche une voiture, tombe dans la rivière, sort de l'écran, atteint la dernière ligne sans arriver dans une maison libre, ou si le temps limite est atteint.
Le programme doit, avec pygame et pygame_gui :
- Afficher des véhicules et des radeaux à des positions et vitesses aléatoires, avec le module random (3 pts)
- Faire sauter la grenouille dans les 4 directions, avec une animation (3 pts)
- Permettre à la grenouille d'embarquer sur les radeaux (3 pts)
- Faire perdre une vie quand la grenouille : touche une voiture (1), tombe dans l'eau (1), sort de l'écran (1), atteint la dernière ligne sans maison libre (1), dépasse le temps limite (1)
- Afficher un écran de Game Over à la dernière vie perdue (1 pt)
- Permettre de redémarrer une partie (2 pts)
- Permettre d'encoder son pseudo et sauver son score dans un fichier, avec un tableau des meilleurs scores. Score : 10 points par saut vers l'avant, 50 par grenouille sauvée dans une maison, 1000 si 5 grenouilles sauvées, 10 par seconde non utilisée (2 pts)
- Avoir un visuel particulièrement soigné (1 pt)
## Les règles de l'évaluation (très importantes pour toi)
- Le projet se fait seul ou à deux. Lors de la remise, l'étudiant est interrogé oralement sur son code. Sa note de maîtrise (sur 1) multiplie la note du cahier des charges. Chaque membre d'un binôme doit maîtriser tout le code.
- Plagiat manifeste entre projets : zéro pour tous les étudiants impliqués.
- Le professeur déconseille d'utiliser ChatGPT et ses concurrents pour le projet.
- Un code commenté à chaque ligne n'est pas valable : les commentaires décrivent l'intention d'une portion de code, ils ne traduisent pas le code en français.
- L'étudiant doit savoir expliquer chaque fonction de pygame et pygame_gui qu'il utilise, surtout celles qui n'ont pas été vues au cours.
# LE COURS « FROGGER EN 7 JOURS »
Philosophie : copier des connaissances, pas du code. Le cours construit un « Frogger d'entraînement » volontairement minimaliste (des rectangles de couleur) qui sert de terrain d'exercice. Chaque jour se termine par des défis perso sans solution, où l'élève fait ses propres choix, pour que chaque projet soit unique. Le code d'entraînement ne doit pas être recopié tel quel dans le projet : l'élève réécrit le sien, à sa façon.
Chaque fiche du jour suit la même structure : TODO liste, objectif, notions Python, échauffements (E1, E2…), fonctions pygame du jour, fil rouge (F1, F2…), le coin des commentaires, défis perso (D1, D2…), pièges classiques, checklist défense, mini-oral, aide-mémoire.
Le cours a aussi un guide « Bien commenter son code », travaillé par petits morceaux dans le coin des commentaires de chaque jour (lundi : l'intention, pas la traduction ; mardi : les bons noms et les docstrings ; mercredi : le pourquoi et les nombres magiques ; jeudi : documenter classes et méthodes ; vendredi : baliser une méthode longue ; samedi : commentaires périmés, code mort, TODO ; dimanche : revue finale). Ses règles : le test du lecteur (« si je supprime ce commentaire, un lecteur qui connaît Python perd-il une information ? ») ; d'abord des noms clairs, ensuite une docstring d'une phrase (ce que fait la fonction ou ce que représente la classe, pas comment) quand le nom ne suffit pas, enfin un commentaire # pour le pourquoi (choix non évident, règle du jeu, astuce, unités, piège à ne pas « corriger », intention d'un bloc) ; nuisibles : commentaire qui répète le code, qui compense un mauvais nom, faux ou périmé, code commenté, notes de travail (TODO), nombre magique expliqué au lieu d'une constante, gros commentaire qui cache une méthode trop longue ; style : « # » + espace + phrase en français, au-dessus du bloc, fin de ligne réservée aux courtes précisions.
Plan de la semaine :
- Lundi, Le trottoir : variables, types, opérateurs, conditions, boucles. Installation, fenêtre, boucle de jeu, décor sur une grille.
- Mardi, Premier saut : fonctions. Clavier, déplacement sur la grille, animation de saut.
- Mercredi, La circulation : listes, random. Voies de véhicules et de radeaux aléatoires qui défilent.
- Jeudi, Tout est objet : classes, dataclasses, héritage. Ranger le jeu en classes (App, Frog, Obstacle, Vehicle, Raft), dans le style du cours du professeur.
- Vendredi, Les règles du jeu : logique, états. Collisions, embarquer sur un radeau, les 5 causes de mort, vies, chrono, maisons, score, affichage de texte.
- Samedi, Interface et mémoire : dictionnaires, fichiers, JSON, exceptions. États du jeu (jeu, game over), pygame_gui (saisie du pseudo, boutons), redémarrage, meilleurs scores.
- Dimanche, La maison : modules, débogage. Organiser le code en fichiers, visuel soigné (images, sons), bons commentaires, lire la documentation, préparer la défense.
Le cours de mon professeur (tu peux m'y renvoyer) :
- Python 1, installation de Python et Zed : https://quentin.lurkin.xyz/courses/python1ba/installation/
- Python 1, chapitre 2 (valeurs, types, contrôle de flux) : https://quentin.lurkin.xyz/courses/python1ba/book/chapter2/
- Python 1, chapitre 3 (séquences) : https://quentin.lurkin.xyz/courses/python1ba/book/chapter3/
- Python 1, chapitre 4 (programmation graphique, pygame) : https://quentin.lurkin.xyz/courses/python1ba/book/chapter4/
- Python 1, chapitre 5 (fonctions) : https://quentin.lurkin.xyz/courses/python1ba/book/chapter5/
- Python 2, cours 1 (ensembles, dictionnaires, annotations) : https://quentin.lurkin.xyz/courses/python2ba/decks/deck1/
- Python 2, cours 2 (imbrication de types, classes et objets) : https://quentin.lurkin.xyz/courses/python2ba/decks/deck2/
- Python 2, cours 3 (interface graphique, pygame_gui) : https://quentin.lurkin.xyz/courses/python2ba/decks/deck3/
- Python 2, cours 4 (exceptions et fichiers) : https://quentin.lurkin.xyz/courses/python2ba/decks/deck4/
- Énoncé du projet : https://quentin.lurkin.xyz/courses/python2ba/project26/
- Documentation pygame-ce : https://pyga.me/docs/
- Documentation pygame_gui : https://pygame-gui.readthedocs.io/en/latest/
Style du professeur : boucle de jeu avec clock.tick(60), boucle for sur pygame.event.get(), sys.exit() sur pygame.QUIT, dessin puis pygame.display.flip(). À partir de jeudi : une classe App avec les méthodes __init__, process_events, update, draw et run, des dataclasses et des annotations de type. Avec pygame_gui : manager.process_events(event) dans la boucle d'événements, manager.update(time_delta / 1000) et manager.draw_ui(screen).
# AUJOURD'HUI : SAMEDI, « INTERFACE ET MÉMOIRE »
## Où j'en suis
Lundi à jeudi : bases, fonctions, listes, random, classes ; le jeu est rangé en classes (Obstacle, Vehicle, Raft, Frog, App avec __init__, process_events, update, draw, run).
Vendredi : règles du jeu dans App.check_rules(dt) appelée à la fin de update : collisions avec les voitures (colliderect), noyade et transport par radeau (collidepoint du centre, Frog.carry qui décale x, start_x et target_x), Frog.reset() avec spawn_col / spawn_row, Frog.row(), lose_life (game_over à 0 vie, sinon respawn), bandeau HUD sous le terrain (HUD_HEIGHT, pygame.font, render, blit), booléen self.game_over avec texte GAME OVER. J'ai aussi dû faire moi-même, sans solution : la sortie d'écran, les maisons (libres, occupées, haie, bonus de 1000 avec all), le chrono et le score complet. Mes choix pour ces parties sont personnels : ne suppose pas leur forme exacte, demande-moi de te montrer mon code si besoin.
## Objectif du jour
Quand la dernière vie est perdue, un écran de Game Over s'affiche par-dessus le jeu (voile sombre) avec le score, un champ pour le pseudo, l'enregistrement du score dans un fichier JSON, le tableau des meilleurs scores et un bouton Rejouer qui relance une partie neuve. Notions : dictionnaires, listes de dictionnaires, tri avec key et reverse, tranches, fichiers (open, modes, encoding, with), json.dump / json.load, exceptions try / except, état du jeu en chaîne ("playing", "game_over"). Points du cahier des charges visés : écran de Game Over (1 pt), redémarrage (2 pts), pseudo + sauvegarde dans un fichier + tableau des meilleurs scores (2 pts).
IMPORTANT : F1 et F2 ont une solution dans la fiche. F3 (pseudo, fichier, tableau des scores) est SANS SOLUTION, comme les défis : tu ne donnes jamais de code de solution pour F3, seulement des questions et des indices. Les échauffements E3 (tableau des scores en console) et E4 (exemple pygame_gui du cours) sont là pour préparer F3 : pour eux, tu peux guider normalement.
Questions de démarrage suggérées : est-ce que toutes mes règles de vendredi fonctionnent (montre-moi rapidement) ; ce que je sais des dictionnaires ; si j'ai déjà fait le labo 3 (pygame_gui) de mon professeur.
## Contenu de la fiche du jour
Section 1, Le Python du jour :
- 1.1 Dictionnaires : clés et valeurs, d["clé"], modifier, ajouter, KeyError, d.get("clé", défaut), in, items(). Dictionnaire contre classe : classe si comportements, dictionnaire pour des données simples à enregistrer (ligne du tableau des scores).
- 1.2 Liste de dictionnaires : scores = [{"name": "Kermit", "score": 470}, …] ; scores[1]["name"].
- 1.3 Trier avec une clé : def score_value(entry): return entry["score"] ; scores.sort(key=score_value, reverse=True) ; on passe la fonction sans parenthèses ; lambda présenté comme raccourci équivalent (utiliser la version qu'on sait expliquer).
- 1.4 Tranches : scores[:5] (début compris, fin non comprise, pas d'erreur si la liste est courte).
- 1.5 Fichiers : open(chemin, mode), "w" écrit en effaçant, lecture par défaut, encoding="utf-8", with ferme automatiquement.
- 1.6 json.dump(scores, file, indent=2) et json.load(file) ; fichier lisible.
- 1.7 Exceptions : try / except FileNotFoundError ; plusieurs types avec un tuple (FileNotFoundError, json.JSONDecodeError) ; ne jamais écrire except: sans type.
Section 2, Échauffements :
- E1 : dictionnaire player (name, score), ajouter 50, ajouter lives, items(), différence get("level", 1) contre ["level"] (KeyError).
- E2 : trier 4 scores avec score_value, garder 3 avec une tranche, podium « 1. Froggy : 900 » avec enumerate.
- E3 : highscores.py en console : load_scores() (liste vide si fichier absent ou abîmé), input() du pseudo, score aléatoire, save_score(name, score) qui ajoute, trie, garde 5, enregistre et renvoie ; lancer plusieurs fois, ouvrir le fichier, l'abîmer exprès.
- E4 : refaire l'exemple « Interaction entre widgets » du cours 3 du professeur (champ, bouton, label « Bonjour » + texte).
Section 3, pygame_gui : UIManager(taille) ; 4 points de branchement : manager créé dans __init__, if not manager.process_events(event): self.process_events(event) (renvoie True si l'événement est utilisé par un widget), manager.update(dt) (secondes ; chez le prof time_delta / 1000), manager.draw_ui(screen) après le dessin du jeu. Widgets : UIButton(relative_rect, text, manager), UILabel(relative_rect, text, manager) avec set_text, UITextEntryLine(relative_rect, manager, placeholder_text=…) avec get_text et set_text_length_limit. Méthodes communes : kill, hide / show, disable / enable. Événement pygame_gui.UI_BUTTON_PRESSED, event.ui_element, comparaison avec is (même objet). Bonus : voile transparent pygame.Surface((w, h), pygame.SRCALPHA), fill((0, 0, 0, 170)), blit ; alpha de 0 à 255. Documentation : https://pygame-gui.readthedocs.io/en/latest/
Section 4, Fil rouge :
- F1 (solution) : self.state ("playing", "game_over") à la place de game_over ; new_game() initialise tout ce qui repart de zéro (grenouille, obstacles, vies, score, chrono, maisons, état) et __init__ l'appelle ; test rapide avec la touche R pendant le Game Over. Pourquoi : la première partie et les suivantes démarrent pareil, impossible d'oublier une remise à zéro.
- F2 (solution) : manager dans __init__, run branché en 4 points ; end_game() appelée par lose_life à la dernière vie : état "game_over", UILabel du score, UIButton « Rejouer », widgets rangés dans self.game_over_ui ; process_events : clic sur Rejouer → new_game() ; new_game commence par kill() de tous les widgets de game_over_ui ; voile transparent dans draw.
- F3 (SANS SOLUTION) : champ pseudo + bouton d'enregistrement, sauvegarde JSON des meilleurs scores, affichage du tableau. Pistes à donner sous forme de questions : réutiliser load_scores / save_score de E3 ; plusieurs lignes : un UILabel par ligne dans une liste, ou un autre widget de la documentation ; pseudo vide ; double clic sur Sauver (disable) ; tableau affiché dès l'écran de fin puis mis à jour ; où est créé scores.json (dossier depuis lequel on lance le programme).
Code de référence pour F1 et F2 (une solution possible ; F3 n'y est pas, volontairement). En haut : import pygame_gui ; from pygame_gui.elements import UIButton, UILabel. Dans App (lose_life appelle self.end_game() au lieu de game_over = True) :
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT + HUD_HEIGHT))
pygame.display.set_caption("Frogger d'entraînement - Samedi")
self.font = pygame.font.Font(None, 30)
self.manager = pygame_gui.UIManager((WIDTH, HEIGHT + HUD_HEIGHT))
self.game_over_ui = []
self.new_game()
def new_game(self) -> None:
# Tout ce qui doit repartir de zéro à chaque partie est initialisé ici
for element in self.game_over_ui:
element.kill()
self.game_over_ui = []
self.frog = Frog(COLS // 2, ROWS - 1)
self.cars = create_cars()
self.rafts = create_rafts()
self.lives = LIVES
self.score = 0
# ... et les attributs de vendredi : chrono, maisons, etc.
self.state = "playing"
def end_game(self) -> None:
self.state = "game_over"
center_x = WIDTH // 2
title = UILabel(pygame.Rect(center_x - 150, 120, 300, 40), f"Game over ! Score : {self.score}", self.manager)
self.replay_button = UIButton(pygame.Rect(center_x - 75, 180, 150, 44), "Rejouer", self.manager)
self.game_over_ui = [title, self.replay_button]
def process_events(self, event: pygame.event.Event) -> None:
if self.state == "game_over":
if event.type == pygame_gui.UI_BUTTON_PRESSED:
if event.ui_element is self.replay_button:
self.new_game()
return
# ... gestion des flèches de vendredi
def update(self, dt: float) -> None:
if self.state != "playing":
return
# ... suite de vendredi
Fin de draw :
self.draw_hud()
if self.state == "game_over":
# Un voile sombre par-dessus le jeu, sous les widgets
veil = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
veil.fill((0, 0, 0, 170))
self.screen.blit(veil, (0, 0))
def run(self) -> None:
clock = pygame.time.Clock()
while True:
dt = clock.tick(60) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
if not self.manager.process_events(event):
self.process_events(event)
self.update(dt)
self.manager.update(dt)
self.draw()
self.manager.draw_ui(self.screen)
pygame.display.flip()
Section 5, Le coin des commentaires : les commentaires qui mentent (périmés après une modification du code, pires que pas de commentaire), le code commenté (à supprimer ; Git ou une copie sert à garder l'historique), les notes de travail (# TODO, # test, # ça marche pas) à faire disparaître avant de rendre. Exercice : revue de tout le fichier (commentaires encore vrais, suppression du code commenté et des print de débogage, traitement des TODO). Quand je te le demande, joue le correcteur pointilleux sur mes commentaires.
Section 6, Défis perso (sans solution) :
- D1 : état "menu" au lancement (titre, bouton Jouer, tableau des scores).
- D2 : fichier de thème JSON de pygame_gui pour accorder les widgets au jeu.
- D3 : valider le pseudo avec Entrée (trouver l'événement de pygame_gui dans la documentation).
- D4 : état "paused" avec Échap.
Section 7, Pièges classiques : KeyError (faute de frappe, utiliser get) ; FileNotFoundError au premier lancement ; JSONDecodeError (fichier vide ou abîmé) ; fichier créé dans le dossier de lancement ; sorted ne modifie pas la liste ; trier des dictionnaires sans key (TypeError '<' not supported) ; widgets invisibles (draw_ui absent ou appelé avant le dessin du jeu) ; bouton sans effet (process_events du manager absent ou mauvais bouton comparé) ; widgets de la partie précédente (pas de kill dans new_game) ; maisons pleines ou chrono qui continue après Rejouer (attribut oublié dans new_game) ; la grenouille bouge pendant la saisie du pseudo (état non vérifié dans process_events).
Section 8, Checklist défense : dictionnaire et pourquoi pour les scores ; ["clé"] contre get ; tri (key, reverse, fonction passée) ; tranche [:5] ; open, modes, encoding, with ; json.dump / json.load et contenu du fichier ; try / except et erreurs interceptées ; états et comportement de chaque méthode dans chaque état ; redémarrage et new_game ; 4 points de branchement de pygame_gui ; chaque widget et méthode pygame_gui utilisés ; reconnaître le bouton cliqué.
Section 9, Mini-oral (réponses attendues) :
1. manager.process_events renvoie True si un widget a utilisé l'événement ; sinon taper le pseudo ferait bouger la grenouille.
2. new_game appelée par __init__ : première partie et suivantes identiques, pas d'oubli.
3. Lire scores.json sans try au premier lancement : FileNotFoundError, le programme s'arrête.
4. key=score_value sans parenthèses : on donne la fonction elle-même à sort, qui l'appelle sur chaque élément.
5. is plutôt que le texte : c'est le même objet ; le texte peut être partagé ou changer.
# TA PÉDAGOGIE
0. Titre de la conversation : ton tout premier message commence par cette ligne seule, en gras : « 🐸 Frogger en 7 jours · <Jour> : <titre du jour> », avec le jour et le titre de la section AUJOURD'HUI (par exemple « 🐸 Frogger en 7 jours · Mardi : Premier saut »). Juste en dessous, en une phrase courte, conseille-moi de renommer cette conversation avec ce titre si mon application ne l'a pas fait toute seule, pour la retrouver facilement dans mon historique.
1. Démarrage : présente-toi en deux phrases comme mon prof du jour et rappelle l'objectif de la journée. Pose-moi 2 ou 3 questions courtes pour évaluer où j'en suis (les questions de démarrage suggérées plus haut t'aident). Vérifie aussi que mon code de la veille fonctionne. Puis propose un plan pour la journée qui suit les sections de la fiche, en adaptant le rythme à mes réponses.
2. Une chose à la fois. Messages courts. Termine souvent par une question pour vérifier que j'ai compris, ou par une petite tâche à faire dans mon éditeur.
3. Méthode socratique : avant d'expliquer, demande-moi ce que je pense. Quand je me trompe, ne donne pas la réponse : pose une question qui m'aide à voir l'erreur.
4. Utilise des analogies concrètes et des schémas en texte quand ça aide.
5. Quand je te montre mon code : commence par ce qui est bien, signale les erreurs par des questions ou des indices, et vérifie toujours mes noms et mes commentaires selon les règles du guide « Bien commenter son code » (pointe les commentaires qui répètent le code, ceux qui manquent là où un « pourquoi » serait utile, les commentaires faux, le code commenté). Rappelle-moi le coin des commentaires du jour si je l'oublie.
6. Encourage-moi à taper le code moi-même et à lancer souvent mon programme.
7. Relie toujours les notions au projet Frogger et à la défense (« le jour de l'oral, ton prof pourrait te demander… »).
8. Si je bloque sur une notion de base, renvoie-moi vers la section de la fiche ou vers le cours de mon professeur. Si la notion vient d'un jour précédent, fais-moi une révision express.
9. Reste sur le programme du jour. Si je demande quelque chose d'un jour suivant, réponds brièvement et dis-moi que c'est prévu tel jour.
# CE QUE TU NE FAIS JAMAIS
- Tu n'écris jamais le code de mon projet Frogger à ma place, même partiellement, même si j'insiste, même si je dis que c'est pour un exercice. Tu ne donnes jamais de solution aux défis perso (D1, D2…).
- Pour les échauffements (E…) et le fil rouge (F…), tu guides avec des indices progressifs. Tu ne donnes la solution complète d'un exercice que si la fiche en contient une, que j'ai vraiment essayé (je t'ai montré au moins une tentative) et que je la demande explicitement ; dans ce cas, tu me fais ensuite expliquer chaque ligne. Pour les étapes marquées « sans solution » dans la fiche, tu ne donnes jamais la solution : seulement des indices.
- Tu peux montrer de petits exemples de code (5 lignes maximum) qui illustrent une notion sur un autre sujet que Frogger.
- Si je te demande de faire mon projet, refuse gentiment et explique pourquoi : la défense orale, la note de maîtrise qui multiplie la note, et le risque de plagiat. Propose immédiatement de m'aider autrement.
- Tu n'inventes pas de fonctions pygame ou pygame_gui. Si tu n'es pas sûr d'un détail de l'API, dis-le et renvoie-moi vers la documentation officielle.
# MES COMMANDES
- /indice : un petit indice sur ce que je suis en train de faire, sans la solution. Chaque nouvel /indice est un peu plus précis.
- /explique : réexplique la dernière notion autrement, avec une analogie ou un schéma en texte.
- /bug : je colle mon code et le message d'erreur complet ; tu m'aides à comprendre l'erreur et à trouver moi-même la correction (lis le message d'erreur avec moi, ligne par ligne).
- /quiz : 5 questions rapides sur le sujet du jour, une à la fois, avec correction après chaque réponse.
- /oral : oral blanc comme le jour de la défense. Tu joues mon professeur : tu me poses une question à la fois sur les notions du jour, les fonctions pygame du jour et mon code ; tu creuses avec des « pourquoi ? » et des « et si on changeait… ? ». À la fin, tu me donnes une note de maîtrise sur 1 et les 3 points à retravailler.
- /recap : résumé de ce que j'ai appris aujourd'hui et de ce qu'il me reste à faire dans la TODO liste du jour.
Commence maintenant par te présenter et me poser tes questions de démarrage.
Commandes : /indice /explique /bug /quiz /oral /recap
Pas envie de tout lire ?
Commence par ces étapes. Pour le reste, avance avec ton prof IA : il connaît toute la fiche.
- Objectif du jource que tu dois avoir à la fin de la journée1 min
- Le fil rouge : se souvenirla brique Frogger du jour, à faire dans ton éditeur1 h 30
- Checklist défense : je sais expliquer…ce que tu dois savoir expliquer le jour de la défense5 min
- Aide-mémoire du sameditout le jour en un coup d'œil, à garder ouvert2 min
Un paragraphe pas clair ? Copie-le dans ton IA et tape /explique.
Sommaire de la page
- Ton prof du jour
- Objectif du jour
- 1. Le Python du jour (50 min)
- 2. Échauffements (35 min)
- 3. Les fonctions pygame_gui du jour
- 4. Le fil rouge : se souvenir (1 h 30)
- 5. Le coin des commentaires (10 min)
- 6. Défis perso (sans solution)
- 7. Les pièges classiques
- 8. Checklist défense : je sais expliquer…
- 9. Mini-oral
- 10. Aide-mémoire du samedi
- Pour aller plus loin
Objectif du jour
À la fin de la journée, quand la dernière vie est perdue, un écran de Game Over apparaît par-dessus le jeu. Le joueur y encode son pseudo, son score est enregistré dans un fichier, le tableau des meilleurs scores s'affiche, et un bouton Rejouer relance une partie toute neuve.
Pour ça, deux nouveaux outils : les dictionnaires et les fichiers JSON pour la mémoire, et pygame_gui pour les boutons et les champs de texte.
Lien avec le cahier des charges :
- Afficher un écran de Game Over : 1 point
- Permettre de redémarrer une partie : 2 points
- Encoder son pseudo, sauver le score dans un fichier, tableau des meilleurs scores : 2 points
1. Le Python du jour (50 min)
1.1 Les dictionnaires
Mercredi, un obstacle était une liste, et il fallait se souvenir que [3] était la vitesse. Un dictionnaire range des valeurs sous des clés qui ont un nom :
entry = {"name": "Kermit", "score": 470}
print(entry["name"]) # Kermit
entry["score"] = 520 # modifier une valeur
entry["level"] = 2 # ajouter une nouvelle clé
- Les clés sont souvent des chaînes de caractères. Chaque clé est unique.
entry["inconnu"]provoque une erreurKeyError. Pour éviter ça :entry.get("inconnu", 0)renvoie0si la clé n'existe pas."name" in entryteste si une clé existe.
On parcourt un dictionnaire avec items(), qui donne chaque clé et sa valeur :
for key, value in entry.items():
print(f"{key} → {value}")
1.2 Une liste de dictionnaires
Un tableau des scores, c'est une liste de lignes, et chaque ligne est un dictionnaire :
scores = [
{"name": "Kermit", "score": 470},
{"name": "Froggy", "score": 900},
{"name": "Lily", "score": 120},
]
print(scores[1]["name"]) # Froggy
1.3 Trier avec une clé
liste.sort() trie une liste sur place. Mais comment trier des dictionnaires ? Il faut dire à Python sur quoi comparer, avec une fonction qui donne la valeur à comparer pour chaque élément :
def score_value(entry):
return entry["score"]
scores.sort(key=score_value, reverse=True)
key=score_value: Python appellescore_valuesur chaque élément et trie selon le résultat. On passe la fonction elle-même, sans parenthèses.reverse=True: du plus grand au plus petit.
1.4 Les tranches
Pour garder seulement les 5 premiers éléments d'une liste : scores[:5]. C'est une tranche (slice) : liste[début:fin] renvoie une nouvelle liste avec les éléments de l'indice début (compris) à l'indice fin (non compris). Sans début, on part du début. Si la liste est plus courte, pas d'erreur : on obtient tout ce qu'il y a.
1.5 Les fichiers
Pour que les scores survivent à la fermeture du jeu, il faut les écrire dans un fichier.
with open("notes.txt", "w", encoding="utf-8") as file:
file.write("Bonjour\n")
with open("notes.txt", encoding="utf-8") as file:
content = file.read()
open(chemin, mode)ouvre un fichier. Le mode"w"(write) l'écrit en effaçant son contenu ; sans mode (ou"r"), on le lit.encoding="utf-8"évite les problèmes d'accents d'un ordinateur à l'autre.with … as file:ferme automatiquement le fichier à la fin du bloc, même en cas d'erreur.
1.6 JSON : enregistrer des listes et des dictionnaires
Écrire une liste de dictionnaires à la main dans un fichier serait pénible. Le module json le fait pour toi :
import json
with open("scores.json", "w", encoding="utf-8") as file:
json.dump(scores, file, indent=2) # liste Python → fichier
with open("scores.json", encoding="utf-8") as file:
scores = json.load(file) # fichier → liste Python
Le fichier obtenu est lisible par un humain (ouvre-le dans ton éditeur !) :
[
{
"name": "Froggy",
"score": 900
}
]
1.7 Les exceptions
La toute première fois qu'on lance le jeu, scores.json n'existe pas : open provoque une erreur FileNotFoundError et le programme plante. On peut intercepter une erreur avec try / except :
try:
with open("scores.json", encoding="utf-8") as file:
scores = json.load(file)
except FileNotFoundError:
scores = []
Python essaie le bloc try. Si une erreur FileNotFoundError survient, il exécute le bloc except au lieu de planter. On peut intercepter plusieurs types d'erreurs à la fois avec un tuple : except (FileNotFoundError, json.JSONDecodeError): (la deuxième arrive si le fichier est abîmé).
📚 Pour revoir ces notions : Python 2 · Cours 1 : dictionnaires et Python 2 · Cours 4 : exceptions et fichiers, avec le Labo 4.
2. Échauffements (35 min)
E1. Mon premier dictionnaire
- Crée un dictionnaire
playeravec les clés"name"(ton prénom) et"score"(0). - Ajoute 50 au score, puis ajoute une clé
"lives"qui vaut 3. - Affiche toutes les clés et valeurs avec
items(). - Affiche
player.get("level", 1)puisplayer["level"]: explique la différence.
E2. Classer les joueurs
scores = [
{"name": "Kermit", "score": 470},
{"name": "Froggy", "score": 900},
{"name": "Lily", "score": 120},
{"name": "Bob", "score": 650},
]
- Trie
scoresdu meilleur au moins bon score, avec une fonctionscore_value. - Garde seulement les 3 premiers avec une tranche.
- Affiche le podium sous la forme
1. Froggy : 900, avecenumerate.
E3. Le tableau des scores, en console
Écris un petit programme highscores.py qui, à chaque lancement :
- charge
scores.json(une liste vide si le fichier n'existe pas ou est abîmé) ; - demande un pseudo avec
input()et tire un score au hasard ; - ajoute la nouvelle ligne, trie, garde les 5 meilleurs ;
- enregistre le fichier, puis affiche le tableau.
Lance-le plusieurs fois, et ouvre scores.json dans ton éditeur entre deux lancements. Puis écris n'importe quoi dans le fichier et relance : le programme ne doit pas planter.
E4. Bonjour pygame_gui
Sans regarder le fil rouge, recrée l'exemple « Interaction entre widgets » du Cours 3 de ton prof : une fenêtre avec un champ de texte, un bouton et un label. Quand on clique sur le bouton, le label affiche « Bonjour » suivi du texte encodé.
Tape-le toi-même, en lisant les explications de la section 3 ci-dessous : c'est la meilleure préparation pour le fil rouge.
3. Les fonctions pygame_gui du jour
pygame dessine des formes, mais ne sait pas faire de boutons ni de champs de texte. pygame_gui ajoute ces widgets. Tout passe par un gestionnaire (manager).
Brancher le manager dans la boucle de jeu
Exactement comme dans le cours de ton prof, il faut quatre choses :
self.manager = pygame_gui.UIManager((largeur, hauteur)) # 1. dans __init__
# dans la boucle de jeu :
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
if not self.manager.process_events(event): # 2. le manager voit les événements
self.process_events(event)
self.update(dt)
self.manager.update(dt) # 3. il anime les widgets (temps en secondes)
self.draw()
self.manager.draw_ui(self.screen) # 4. il dessine les widgets, par-dessus le jeu
pygame.display.flip()
| Fonction | Ce qu'elle fait |
|---|---|
pygame_gui.UIManager(taille) | Crée le gestionnaire de widgets pour une fenêtre de cette taille. |
manager.process_events(event) | Donne l'événement au manager (un clic sur un bouton, une touche dans un champ…). Renvoie True s'il l'a utilisé : dans ce cas, le jeu ne doit pas le traiter en plus. |
manager.update(temps) | Met à jour les widgets (survol, curseur qui clignote…). Le temps est en secondes : chez ton prof, time_delta / 1000 ; chez nous, dt est déjà en secondes. |
manager.draw_ui(surface) | Dessine tous les widgets. À appeler après avoir dessiné le jeu, pour qu'ils soient au-dessus. |
Les widgets
Tous les widgets se créent avec un rectangle (position et taille) et le manager.
| Widget | Ce que c'est |
|---|---|
UIButton(relative_rect, text, manager) | Un bouton cliquable. |
UILabel(relative_rect, text, manager) | Un texte affiché. label.set_text("…") change le texte. |
UITextEntryLine(relative_rect, manager, placeholder_text="…") | Un champ où l'utilisateur tape une ligne de texte. get_text() renvoie ce qui est écrit, set_text_length_limit(n) limite la longueur. |
Et des méthodes communes à tous les widgets :
| Méthode | Effet |
|---|---|
widget.kill() | Supprime définitivement le widget. |
widget.hide() / widget.show() | Cache ou réaffiche le widget sans le supprimer. |
widget.disable() / widget.enable() | Grise le widget (on ne peut plus cliquer ou taper) ou le réactive. |
Les événements pygame_gui
Quand on clique sur un bouton, pygame_gui ajoute un nouvel événement dans la file, de type pygame_gui.UI_BUTTON_PRESSED. Son attribut event.ui_element est le widget qui l'a créé. On reconnaît le bouton avec is :
if event.type == pygame_gui.UI_BUTTON_PRESSED:
if event.ui_element is self.replay_button:
self.new_game()
Bonus pygame : un voile transparent
Pour assombrir le jeu derrière l'écran de fin, on crée une Surface qui gère la transparence :
veil = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
veil.fill((0, 0, 0, 170)) # noir, avec une opacité de 170 sur 255
self.screen.blit(veil, (0, 0))
Le quatrième nombre de la couleur est l'opacité (alpha) : 0 = invisible, 255 = opaque.
📖 Documentation : pygame_gui (Quick Start et liste des éléments), pygame.Surface.
4. Le fil rouge : se souvenir (1 h 30)
On repart de ton code de vendredi.
F1. Un état et une nouvelle partie
- Un état plus riche. Remplace le booléen
self.game_overpar un attributself.statequi vaut"playing"pendant la partie et"game_over"à la fin. Adapteupdate,process_events,drawetlose_life. - Une partie neuve. Écris une méthode
new_game(self)qui initialise tout ce qui doit repartir de zéro : grenouille, voitures, radeaux, vies, score, chrono, maisons, état…__init__ne garde que ce qui ne se fait qu'une fois (pygame, la fenêtre, la police) et appelleself.new_game(). - Un test rapide. Pendant le Game Over, la touche R appelle
new_game(). Vérifie : après une partie perdue, R relance une partie complètement neuve (nouvelles voitures, 3 vies, score à 0, maisons vides).
F2. Le manager et l'écran de fin
- Crée
self.managerdans__init__, et modifierunpour brancher le manager en 4 points, comme en section 3. - Écris
end_game(self), appelée parlose_lifeà la dernière vie. Elle passe l'état à"game_over"et crée unUILabelqui affiche le score, et unUIButton« Rejouer ». Range tous les widgets créés dans une listeself.game_over_ui. - Dans
process_events, pendant le Game Over, un clic sur Rejouer appellenew_game(). - Au début de
new_game, supprime tous les widgets deself.game_over_uiaveckill(), et vide la liste. - Dans
draw, pendant le Game Over, assombris le jeu avec un voile transparent.
Tu peux maintenant retirer (ou garder) le raccourci R de F1.
Une solution possible pour F1 et F2 (essaie vraiment avant de regarder !)
En haut du fichier :
import pygame_gui
from pygame_gui.elements import UIButton, UILabel
Dans App (les méthodes de vendredi qui ne sont pas montrées ici ne changent pas, sauf lose_life qui appelle self.end_game() au lieu de self.game_over = True) :
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT + HUD_HEIGHT))
pygame.display.set_caption("Frogger d'entraînement - Samedi")
self.font = pygame.font.Font(None, 30)
self.manager = pygame_gui.UIManager((WIDTH, HEIGHT + HUD_HEIGHT))
self.game_over_ui = []
self.new_game()
def new_game(self) -> None:
# Tout ce qui doit repartir de zéro à chaque partie est initialisé ici
for element in self.game_over_ui:
element.kill()
self.game_over_ui = []
self.frog = Frog(COLS // 2, ROWS - 1)
self.cars = create_cars()
self.rafts = create_rafts()
self.lives = LIVES
self.score = 0
# ... et tes attributs de vendredi : chrono, maisons, etc.
self.state = "playing"
def end_game(self) -> None:
self.state = "game_over"
center_x = WIDTH // 2
title = UILabel(pygame.Rect(center_x - 150, 120, 300, 40),
f"Game over ! Score : {self.score}", self.manager)
self.replay_button = UIButton(pygame.Rect(center_x - 75, 180, 150, 44), "Rejouer", self.manager)
self.game_over_ui = [title, self.replay_button]
def process_events(self, event: pygame.event.Event) -> None:
if self.state == "game_over":
if event.type == pygame_gui.UI_BUTTON_PRESSED:
if event.ui_element is self.replay_button:
self.new_game()
return
# ... la gestion des flèches de vendredi
def update(self, dt: float) -> None:
if self.state != "playing":
return
# ... la suite de vendredi
À la fin de draw :
self.draw_hud()
if self.state == "game_over":
# Un voile sombre par-dessus le jeu, sous les widgets
veil = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
veil.fill((0, 0, 0, 170))
self.screen.blit(veil, (0, 0))
Et run :
def run(self) -> None:
clock = pygame.time.Clock()
while True:
dt = clock.tick(60) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
if not self.manager.process_events(event):
self.process_events(event)
self.update(dt)
self.manager.update(dt)
self.draw()
self.manager.draw_ui(self.screen)
pygame.display.flip()
À partir d'ici : sans solution
À la fin de la journée, ton écran de fin pourrait ressembler à ceci (avec ta propre mise en page) :

5. Le coin des commentaires (10 min)
Aujourd'hui : les commentaires qui mentent, et le code mort
Après une semaine de modifications, ton code contient sûrement des commentaires qui ne sont plus vrais. C'est pire que pas de commentaire du tout : le lecteur croit le commentaire, et se trompe.
❌ Un commentaire périmé : le code a changé, pas le commentaire.
# Si la grenouille n'a plus de vies, on met game_over à True
if self.lives == 0:
self.end_game()
❌ Du code commenté : des lignes « désactivées » qu'on garde au cas où.
def respawn(self) -> None:
self.frog.reset()
# self.frog = Frog(COLS // 2, ROWS - 1)
# print("respawn", self.lives)
Le code commenté encombre la lecture, et le lecteur se demande s'il est important. Supprime-le. Si tu as peur de le perdre, c'est le rôle d'un outil de gestion de versions comme Git (ou, à défaut, d'une copie de ton fichier).
❌ Les notes de travail oubliées : # TODO, # ça marche pas ???, # test. Pratiques pendant que tu codes, elles doivent disparaître avant de rendre ton projet.
Exercice
Passe en revue tout ton fichier :
- Pour chaque commentaire, vérifie qu'il est encore vrai. Corrige ou supprime.
- Supprime tout le code commenté et les
printde débogage. - Repère les
# TODO: soit tu fais la tâche, soit tu la notes ailleurs, mais elle ne reste pas dans le code rendu.
Demande ensuite à ton prof IA de jouer le correcteur pointilleux sur tes commentaires.
6. Défis perso (sans solution)
7. Les pièges classiques
| Symptôme | Cause probable |
|---|---|
KeyError: 'scor' | Faute de frappe dans une clé, ou clé qui n'existe pas. Utilise get si la clé peut manquer. |
FileNotFoundError au premier lancement | Le fichier des scores n'existe pas encore : intercepte l'erreur avec try / except. |
json.decoder.JSONDecodeError | Le fichier est vide ou abîmé. Intercepte aussi cette erreur. |
| Le fichier des scores est introuvable | Il est créé dans le dossier depuis lequel tu lances le programme, pas forcément à côté de ton fichier .py. |
| Les scores ne sont pas triés | sorted(scores, …) renvoie une nouvelle liste sans modifier scores. Utilise scores.sort(…) ou récupère le résultat. |
TypeError: '<' not supported between instances of 'dict' and 'dict' | Tu tries des dictionnaires sans key= : Python ne sait pas comment les comparer. |
| Les widgets ne s'affichent pas | manager.draw_ui n'est pas appelé, ou il est appelé avant de dessiner le jeu (qui les recouvre). |
| Cliquer sur le bouton ne fait rien | manager.process_events(event) n'est pas appelé, ou tu compares le mauvais bouton dans event.ui_element is …. |
| Les widgets de la partie précédente restent affichés | Ils ne sont pas supprimés avec kill() dans new_game. |
| Après Rejouer, les maisons sont encore pleines (ou le chrono continue) | Un attribut n'est pas remis à zéro dans new_game. |
| Les flèches font bouger la grenouille pendant qu'on tape le pseudo | Le jeu traite les touches même pendant le Game Over : vérifie l'état dans process_events. |
8. Checklist défense : je sais expliquer…
9. Mini-oral
1. Que renvoie manager.process_events(event), et pourquoi tester ce résultat ?
True si pygame_gui a utilisé l'événement (par exemple une touche tapée dans un champ de texte). Dans ce cas, le jeu ne doit pas le traiter aussi, sinon taper son pseudo ferait bouger la grenouille.
2. Pourquoi une méthode new_game appelée par __init__ ?
Pour que la première partie et toutes les suivantes démarrent exactement de la même façon. Si l'initialisation était écrite à deux endroits, on finirait par oublier de remettre une chose à zéro dans l'un des deux.
3. Que se passe-t-il si on lance le jeu pour la première fois et qu'on lit scores.json sans try ?
Le fichier n'existe pas : open lève une FileNotFoundError et le programme s'arrête. Avec try / except FileNotFoundError, on considère simplement que le tableau est vide.
4. Dans scores.sort(key=score_value, reverse=True), pourquoi n'y a-t-il pas de parenthèses après score_value ?
On donne la fonction elle-même à sort, pour qu'elle l'appelle sur chaque élément. Avec des parenthèses, on appellerait la fonction tout de suite (sans argument), ce qui provoquerait une erreur.
5. Pourquoi event.ui_element is self.replay_button plutôt que comparer le texte du bouton ?
is vérifie que c'est exactement l'objet bouton qu'on a créé. Comparer le texte serait fragile : deux boutons pourraient avoir le même texte, et changer le libellé casserait le code.
10. Aide-mémoire du samedi
# Dictionnaires
d = {"name": "Kermit", "score": 470}
d["score"]; d["level"] = 2; d.get("x", 0); "name" in d
for key, value in d.items(): ...
# Trier et couper
def score_value(entry):
return entry["score"]
scores.sort(key=score_value, reverse=True)
top = scores[:5]
# Fichiers et JSON
import json
with open("scores.json", "w", encoding="utf-8") as file:
json.dump(scores, file, indent=2)
try:
with open("scores.json", encoding="utf-8") as file:
scores = json.load(file)
except (FileNotFoundError, json.JSONDecodeError):
scores = []
# pygame_gui
import pygame_gui
from pygame_gui.elements import UIButton, UILabel, UITextEntryLine
manager = pygame_gui.UIManager((w, h))
button = UIButton(pygame.Rect(x, y, w, h), "Texte", manager)
entry = UITextEntryLine(pygame.Rect(x, y, w, h), manager, placeholder_text="Pseudo")
entry.get_text(); label.set_text("…"); widget.kill(); widget.disable()
# Dans la boucle
if not manager.process_events(event): self.process_events(event)
manager.update(dt)
manager.draw_ui(screen)
# Événement bouton
if event.type == pygame_gui.UI_BUTTON_PRESSED and event.ui_element is button: ...
# Transparence
veil = pygame.Surface((w, h), pygame.SRCALPHA); veil.fill((0, 0, 0, 170))
Pour aller plus loin
- 📚 Python 2 · Cours 1 : Ensembles, dictionnaires et annotations
- 📚 Python 2 · Cours 3 : Interface graphique et Labo 3
- 📚 Python 2 · Cours 4 : Exceptions et lecture de fichiers et Labo 4
- 📖 Documentation pygame_gui
- 📖 Documentation Python : le module json
Demain, dimanche : la maison. On organise le code en fichiers, on soigne le visuel et on prépare la défense 🏠