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.

  • Durée environ 3 h 30
  • Python dictionnaires, listes de dictionnaires, tri avec une clé, tranches, fichiers, JSON, exceptions, états du jeu

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.

3 594 mots · à coller dans une nouvelle conversation
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.

  1. Objectif du jource que tu dois avoir à la fin de la journée1 min
  2. Le fil rouge : se souvenirla brique Frogger du jour, à faire dans ton éditeur1 h 30
  3. Checklist défense : je sais expliquer…ce que tu dois savoir expliquer le jour de la défense5 min
  4. 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
  1. Ton prof du jour
  2. Objectif du jour
  3. 1. Le Python du jour (50 min)
  4. 2. Échauffements (35 min)
  5. 3. Les fonctions pygame_gui du jour
  6. 4. Le fil rouge : se souvenir (1 h 30)
  7. 5. Le coin des commentaires (10 min)
  8. 6. Défis perso (sans solution)
  9. 7. Les pièges classiques
  10. 8. Checklist défense : je sais expliquer…
  11. 9. Mini-oral
  12. 10. Aide-mémoire du samedi
  13. 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 erreur KeyError. Pour éviter ça : entry.get("inconnu", 0) renvoie 0 si la clé n'existe pas.
  • "name" in entry teste 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 appelle score_value sur 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

  1. Crée un dictionnaire player avec les clés "name" (ton prénom) et "score" (0).
  2. Ajoute 50 au score, puis ajoute une clé "lives" qui vaut 3.
  3. Affiche toutes les clés et valeurs avec items().
  4. Affiche player.get("level", 1) puis player["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},
]
  1. Trie scores du meilleur au moins bon score, avec une fonction score_value.
  2. Garde seulement les 3 premiers avec une tranche.
  3. Affiche le podium sous la forme 1. Froggy : 900, avec enumerate.

E3. Le tableau des scores, en console

Écris un petit programme highscores.py qui, à chaque lancement :

  1. charge scores.json (une liste vide si le fichier n'existe pas ou est abîmé) ;
  2. demande un pseudo avec input() et tire un score au hasard ;
  3. ajoute la nouvelle ligne, trie, garde les 5 meilleurs ;
  4. 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()
FonctionCe 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.

WidgetCe 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éthodeEffet
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

  1. Un état plus riche. Remplace le booléen self.game_over par un attribut self.state qui vaut "playing" pendant la partie et "game_over" à la fin. Adapte update, process_events, draw et lose_life.
  2. 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 appelle self.new_game().
  3. 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

  1. Crée self.manager dans __init__, et modifie run pour brancher le manager en 4 points, comme en section 3.
  2. Écris end_game(self), appelée par lose_life à la dernière vie. Elle passe l'état à "game_over" et crée un UILabel qui affiche le score, et un UIButton « Rejouer ». Range tous les widgets créés dans une liste self.game_over_ui.
  3. Dans process_events, pendant le Game Over, un clic sur Rejouer appelle new_game().
  4. Au début de new_game, supprime tous les widgets de self.game_over_ui avec kill(), et vide la liste.
  5. 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) :

L'écran de Game Over avec le pseudo, le tableau des scores et le bouton Rejouer


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 :

  1. Pour chaque commentaire, vérifie qu'il est encore vrai. Corrige ou supprime.
  2. Supprime tout le code commenté et les print de débogage.
  3. 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ômeCause 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 lancementLe fichier des scores n'existe pas encore : intercepte l'erreur avec try / except.
json.decoder.JSONDecodeErrorLe fichier est vide ou abîmé. Intercepte aussi cette erreur.
Le fichier des scores est introuvableIl 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éssorted(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 pasmanager.draw_ui n'est pas appelé, ou il est appelé avant de dessiner le jeu (qui les recouvre).
Cliquer sur le bouton ne fait rienmanager.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ésIls 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 pseudoLe 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

Demain, dimanche : la maison. On organise le code en fichiers, on soigne le visuel et on prépare la défense 🏠

Du nouveau dans le cours

Petite rectification

Il manquait une étape au cours : comprendre ce qu'est un jeu vidéo avant de le coder. Une nouvelle section t'explique la boucle de jeu et le plan __init__ / process_events / update / draw que suivront tous les éléments de ton Frogger.