RouteJour 2 · Mardi
Premier saut
Hier, la grenouille attendait sur le trottoir. Aujourd'hui, elle saute : on range le code en fonctions, on écoute le clavier et on anime le saut.
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 : MARDI, « PREMIER SAUT »
## Où j'en suis
Lundi, j'ai installé pygame-ce et pygame_gui, revu variables, types, opérateurs (// et % pour la grille), conditions et boucles, et écrit un script frogger.py : constantes TILE = 48, COLS = 13, ROWS = 13, WIDTH, HEIGHT ; boucle de jeu (clock.tick(60), boucle d'événements avec QUIT → sys.exit, dessin, flip) ; décor dessiné par une boucle sur les lignes (0 maisons/haie, 1 à 5 eau, 6 berge, 7 à 11 route, 12 trottoir), pointillés sur la route, 5 maisons aux colonnes 2, 4, 6, 8, 10 ; grenouille immobile dessinée à partir de frog_col = COLS // 2 et frog_row = ROWS - 1. Tout le code est au niveau du module, sans fonctions.
## Objectif du jour
La grenouille saute de case en case dans les 4 directions, avec une animation (elle glisse en 0.15 s et gonfle au milieu du saut). Notions : les fonctions. Point du cahier des charges visé : saut animé dans les 4 directions (3 pts).
Questions de démarrage suggérées : est-ce que mon décor de lundi s'affiche bien ; ce que je sais déjà des fonctions (def, return) ; la différence entre print et return.
## Contenu de la fiche du jour
Section 1, Le Python du jour : les fonctions.
- 1.1 def, appel ; définir n'exécute pas.
- 1.2 paramètres et arguments, arguments nommés (comme UIButton(relative_rect=..., text=..., manager=...) chez le prof), valeurs par défaut.
- 1.3 return : arrête la fonction, plusieurs valeurs renvoyées (tuple) et déballage x, y = f() ; sans return la fonction renvoie None. print n'est pas return.
- 1.4 None, testé avec is / is not.
- 1.5 Portée : variables locales ; exemple lose_life(lives) qui ne modifie pas lives (affiche 3) ; solution : return puis lives = lose_life(lives). global existe mais on l'évite ; jeudi, les classes régleront ça.
- 1.6 Annotations de type (col: int, -> tuple, tuple | None) : documentation, non vérifiées par Python. tuple | None demande Python 3.10+.
- 1.7 Pourquoi des fonctions : nommer, ne pas se répéter, tester à part, faciliter la défense.
Section 2, Échauffements :
- E1 : cell_to_pixel(col, row) et pixel_to_cell(x, y) ; pixel_to_cell(250, 400) → (5, 8) ; pixel_to_cell(*cell_to_pixel(5, 8)) → (5, 8) (déballage avec *).
- E2 : zone_name(row) qui renvoie le nom de la zone ; is_safe(row) qui renvoie True pour berge et trottoir, en utilisant zone_name.
- E3 : lerp(a, b, t) = a + (b - a) * t ; lerp(0, 48, 0.5) = 24 ; afficher t de 0 à 1 par 0.1 : 0.0, 4.8, 9.6 … 48.0.
- E4 : piège de portée avec add_points(score, points) qui ne modifie pas score (affiche 0) ; corriger avec return pour obtenir 60.
Section 3, Fonctions pygame du jour : pygame.KEYDOWN (une fois par appui), pygame.KEYUP, event.key, pygame.K_UP / K_DOWN / K_LEFT / K_RIGHT (et K_SPACE, K_z…) ; documentation https://pyga.me/docs/ref/key.html ; événement (KEYDOWN) contre état (pygame.key.get_pressed()) : pour Frogger, un saut par appui donc KEYDOWN. dt = clock.tick(60) / 1000 (secondes écoulées) pour des animations indépendantes de la vitesse de l'ordinateur. Module math : math.sin, math.pi ; sin(pi * t) vaut 0 en t = 0, 1 en t = 0.5, 0 en t = 1.
Section 4, Fil rouge (solution disponible dans la fiche) :
- F1 : refactoriser lundi en fonctions zone_color(row) (if/elif avec return), draw_background(screen), draw_frog(screen, x, y) ; le jeu doit afficher exactement la même chose (refactoriser = ranger sans changer le comportement).
- F2 : position en pixels frog_x, frog_y = cell_to_pixel(COLS // 2, ROWS - 1) ; direction_from_key(key) qui renvoie (0, -1), (0, 1), (-1, 0), (1, 0) ou None ; sur KEYDOWN, déballer dx, dy et déplacer d'une case ; is_on_screen(x, y) pour ne pas sortir (vendredi, sortir fera perdre une vie).
- F3 : animation avec start_x/start_y, target_x/target_y, jump_t (0 → 1, None au repos), JUMP_TIME = 0.15 ; à l'appui : départ = case actuelle, arrivée = voisine, jump_t = 0 ; à chaque image : jump_t += dt / JUMP_TIME et position = lerp ; quand jump_t >= 1 : recaler pile sur la case d'arrivée (sinon l'erreur s'accumule) et jump_t = None ; ignorer les touches pendant un saut (tester jump_t is None).
- F4 : jump_bump(t) = math.sin(math.pi * t) * 6 ; draw_frog reçoit bump, marge = 8 - bump ; bump = 0 au repos.
- Remarque : la boucle garde les 4 temps (attendre, écouter qui lance le saut, mettre à jour qui le fait avancer, dessiner).
Code de référence du fil rouge (une solution possible) :
import math
import sys
import pygame
TILE = 48
COLS = 13
ROWS = 13
WIDTH = COLS * TILE
HEIGHT = ROWS * TILE
JUMP_TIME = 0.15 # durée d'un saut, en secondes
(constantes de couleur identiques à lundi)
def zone_color(row: int) -> tuple:
if row == 0:
return HAIE
elif row <= 5:
return EAU
elif row == 6:
return BERGE
elif row <= 11:
return ROUTE
else:
return TROTTOIR
def draw_background(screen: pygame.Surface) -> None:
(bandes avec zone_color, pointillés et maisons comme lundi)
def cell_to_pixel(col: int, row: int) -> tuple:
return col * TILE, row * TILE
def direction_from_key(key: int) -> tuple | None:
if key == pygame.K_UP:
return (0, -1)
elif key == pygame.K_DOWN:
return (0, 1)
elif key == pygame.K_LEFT:
return (-1, 0)
elif key == pygame.K_RIGHT:
return (1, 0)
return None
def is_on_screen(x: float, y: float) -> bool:
return 0 <= x <= WIDTH - TILE and 0 <= y <= HEIGHT - TILE
def lerp(a: float, b: float, t: float) -> float:
return a + (b - a) * t
def jump_bump(t: float) -> float:
# Nul au départ et à l'arrivée, maximal au milieu du saut
return math.sin(math.pi * t) * 6
def draw_frog(screen: pygame.Surface, x: float, y: float, bump: float) -> None:
margin = 8 - bump
frog = pygame.Rect(int(x + margin), int(y + margin), int(TILE - 2 * margin), int(TILE - 2 * margin))
pygame.draw.rect(screen, GRENOUILLE, frog)
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Frogger d'entraînement - Mardi")
clock = pygame.time.Clock()
# Position affichée, et saut en cours (jump_t vaut None au repos)
frog_x, frog_y = cell_to_pixel(COLS // 2, ROWS - 1)
start_x, start_y = frog_x, frog_y
target_x, target_y = frog_x, frog_y
jump_t = None
while True:
dt = clock.tick(60) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
if event.type == pygame.KEYDOWN and jump_t is None:
direction = direction_from_key(event.key)
if direction is not None:
dx, dy = direction
new_x = target_x + dx * TILE
new_y = target_y + dy * TILE
if is_on_screen(new_x, new_y):
start_x, start_y = target_x, target_y
target_x, target_y = new_x, new_y
jump_t = 0
# Faire avancer le saut en cours, et le terminer pile sur la case d'arrivée
if jump_t is not None:
jump_t += dt / JUMP_TIME
if jump_t >= 1:
jump_t = None
frog_x, frog_y = target_x, target_y
else:
frog_x = lerp(start_x, target_x, jump_t)
frog_y = lerp(start_y, target_y, jump_t)
draw_background(screen)
if jump_t is None:
bump = 0
else:
bump = jump_bump(jump_t)
draw_frog(screen, frog_x, frog_y, bump)
pygame.display.flip()
Section 5, Le coin des commentaires : un bon nom vaut mieux qu'un commentaire (def f(a, b, t) commenté contre def lerp(start, end, t)) ; noms de verbe pour les fonctions, de question pour celles qui renvoient un booléen (is_on_screen) ; docstrings : texte entre triples guillemets sous le def, une phrase qui dit ce que fait la fonction (pas comment), gardée par Python et affichée par l'éditeur ; exemple jump_bump avec « Renvoie de combien de pixels la grenouille gonfle à l'instant t du saut (de 0 à 1). » ; pas de docstring pour une fonction évidente comme draw_frog. Exercice (sans solution) : renommer g(k) et h(x, y) commentées en direction_from_key et is_on_screen (ou équivalent) et supprimer les commentaires ; améliorer le nom ou ajouter une docstring à la fonction de mon code la moins explicite.
Section 6, Défis perso (sans solution) :
- D1 : touches Z, Q, S, D en plus des flèches (AZERTY) ; comprendre event.key contre event.scancode et justifier son choix.
- D2 : grenouille orientée selon la dernière direction (nouvelle variable, nouveau paramètre).
- D3 : son propre style de saut (easing / ease-out, ombre, écrasement), formule à expliquer.
- D4 : comportement quand on garde la touche enfoncée (pygame.key.set_repeat ou get_pressed), à décider et justifier.
Section 7, Pièges classiques : fonction définie mais pas appelée ou sans parenthèses ; TypeError missing argument ; variable non modifiée (locale, il faut return) ; cannot unpack NoneType (déballer sans tester None) ; fonction qui renvoie None (return oublié ou print à la place) ; grenouille décalée des cases (pas de recalage en fin de saut) ; grenouille qui part dans tous les sens (touches non ignorées pendant le saut) ; erreur sur tuple | None si Python < 3.10.
Section 8, Checklist défense : définir contre appeler ; paramètre contre argument ; print contre return, retour None ; portée et solution par return ; annotations non vérifiées ; KEYDOWN, KEYUP, get_pressed ; rôle de dt ; fonctionnement complet de l'animation ; pourquoi ignorer les touches pendant un saut ; rôle, paramètres et retour de chacune de mes fonctions.
Section 9, Mini-oral (réponses attendues) :
1. Fonction sans return : renvoie None.
2. lose_life(lives) ne change pas lives : variable locale ; il faut lives = lose_life(lives).
3. lerp(10, 20, 0.25) = 12.5 ; a + (b - a) * t.
4. KEYDOWN plutôt que get_pressed : un appui = un saut ; get_pressed ferait sauter en continu.
5. À 30 images/s le saut dure toujours 0.15 s car dt est deux fois plus grand.
6. jump_t = None indique « pas de saut » : sert à savoir s'il faut animer et à ignorer les touches.
# 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 : la grenouille sautela brique Frogger du jour, à faire dans ton éditeur1 h
- Checklist défense : je sais expliquer…ce que tu dois savoir expliquer le jour de la défense5 min
- Aide-mémoire du marditout 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 : les fonctions (45 min)
- 2. Échauffements (30 min)
- 3. Les fonctions pygame du jour
- 4. Le fil rouge : la grenouille saute (1 h)
- 5. Le coin des commentaires (10 min)
- 6. Défis perso (30 min, sans solution)
- 7. Les pièges classiques
- 8. Checklist défense : je sais expliquer…
- 9. Mini-oral
- 10. Aide-mémoire du mardi
- Pour aller plus loin
Objectif du jour
À la fin de la journée, la grenouille saute de case en case dans les 4 directions, avec une vraie animation : elle glisse en douceur et gonfle un peu au milieu du saut.
Pour y arriver, tu vas apprendre à écrire tes propres fonctions. C'est l'outil qui transforme un long script en un programme lisible, où chaque morceau a un nom et un rôle.
Lien avec le cahier des charges : « Permettre de faire sauter (animation !) la grenouille dans les 4 directions » : 3 points.
1. Le Python du jour : les fonctions (45 min)
1.1 Définir et appeler une fonction
Une fonction est un bloc de code qui porte un nom. On la définit une fois avec def, puis on l'appelle autant de fois qu'on veut.
def say_hello():
print("Bonjour !")
say_hello() # affiche Bonjour !
say_hello() # et encore une fois
Définir une fonction ne l'exécute pas : Python retient seulement « voici ce que fait say_hello ». Le code s'exécute à chaque appel, avec les parenthèses.
Tu connais déjà plein de fonctions écrites par d'autres : print(), range(), pygame.draw.rect()… Aujourd'hui, tu écris les tiennes.
1.2 Les paramètres
Une fonction peut recevoir des valeurs : ses paramètres.
def greet(name, times):
for i in range(times):
print(f"Salut {name} !")
greet("Kermit", 2)
Au moment de l'appel, name reçoit "Kermit" et times reçoit 2. Les valeurs qu'on donne à l'appel s'appellent les arguments.
On peut aussi nommer les arguments à l'appel, ce qui rend le code plus lisible : greet(name="Kermit", times=2). Tu l'as déjà vu chez ton prof avec UIButton(relative_rect=..., text=..., manager=...).
Un paramètre peut avoir une valeur par défaut, utilisée si on ne le donne pas :
def greet(name, times=1):
...
greet("Kermit") # times vaut 1
1.3 return : renvoyer un résultat
Une fonction peut calculer quelque chose et le renvoyer avec return. L'appel est alors remplacé par la valeur renvoyée.
def cell_to_pixel(col, row):
return col * 48, row * 48
x, y = cell_to_pixel(5, 8) # x vaut 240, y vaut 384
Trois choses à retenir :
returnarrête la fonction immédiatement. Ce qui suit n'est pas exécuté.- On peut renvoyer plusieurs valeurs séparées par des virgules : Python les groupe en un tuple, qu'on peut déballer dans plusieurs variables (
x, y = …). - Une fonction sans
returnrenvoie quand même quelque chose :None, la valeur qui veut dire « rien ».
1.4 None : la valeur « rien »
None sert à dire « il n'y a pas de valeur ». On le teste avec is :
direction = None
if direction is None:
print("Aucune touche de direction")
On va s'en servir aujourd'hui de deux façons : une fonction qui renvoie None quand une touche n'est pas une flèche, et une variable qui vaut None quand la grenouille ne saute pas.
1.5 La portée des variables
Les variables créées dans une fonction (et ses paramètres) sont locales : elles n'existent que pendant l'appel, et elles ne touchent pas les variables du même nom à l'extérieur.
lives = 3
def lose_life(lives):
lives = lives - 1 # modifie le lives LOCAL seulement
lose_life(lives)
print(lives) # affiche 3 !
La bonne façon de faire : renvoyer la nouvelle valeur, et l'affecter à l'extérieur.
def lose_life(lives):
return lives - 1
lives = lose_life(lives) # lives vaut maintenant 2
1.6 Les annotations de type
Ton prof écrit souvent ses fonctions comme ceci :
def cell_to_pixel(col: int, row: int) -> tuple:
return col * TILE, row * TILE
col: int indique que col devrait être un entier, et -> tuple que la fonction renvoie un tuple. Python ne vérifie rien à l'exécution : c'est de la documentation pour le lecteur (et pour ton éditeur, qui peut te prévenir d'une erreur). tuple | None veut dire « un tuple, ou None ».
1.7 Pourquoi écrire des fonctions ?
- Donner un nom à une idée :
draw_background(screen)se lit tout seul. - Ne pas se répéter : un calcul écrit une fois, utilisé partout.
- Tester un morceau à part : on peut vérifier
cell_to_pixel(5, 8)sans lancer le jeu. - Préparer la défense : une fonction courte avec un bon nom est bien plus facile à expliquer qu'un bloc de 80 lignes.
📚 Pour revoir ces notions : Python 1 · Chapitre 5 : Les fonctions du cours de ton prof.
2. Échauffements (30 min)
Du Python pur, dans un fichier echauffement_mardi.py.
E1. Aller-retour pixel / case
Écris deux fonctions qui reprennent ton exercice E2 de lundi :
cell_to_pixel(col, row)renvoie le coin en haut à gauche de la case, en pixels.pixel_to_cell(x, y)renvoie la case qui contient le pixel.
Vérifie : pixel_to_cell(250, 400) doit renvoyer (5, 8), et pixel_to_cell(*cell_to_pixel(5, 8)) aussi. (Le * déballe le tuple en deux arguments : demande à ton prof IA de te l'expliquer si c'est nouveau.)
E2. Une zone, une fonction
Transforme ton code de lundi (E3) en une fonction zone_name(row) qui renvoie le nom de la zone (au lieu de l'afficher). Ensuite, dans une boucle, affiche le nom de chaque ligne.
Puis écris is_safe(row) qui renvoie True si la ligne est la berge ou le trottoir, False sinon. Elle doit utiliser zone_name.
E3. L'interpolation, cœur de l'animation
Écris lerp(a, b, t) qui renvoie la valeur située à la fraction t du chemin entre a et b :
lerp(0, 48, 0)renvoie0(le départ)lerp(0, 48, 1)renvoie48(l'arrivée)lerp(0, 48, 0.5)renvoie24(la moitié)
Affiche lerp(0, 48, t) pour t allant de 0 à 1 par pas de 0.1. Tu dois voir les nombres avancer régulièrement : 0.0, 4.8, 9.6, … 48.0.
Ce que tu viens d'écrire, c'est exactement la formule qui va faire glisser ta grenouille d'une case à l'autre.
E4. Le piège de la portée
Sans l'exécuter, prédis ce qu'affiche ce code. Puis exécute-le.
score = 0
def add_points(score, points):
score = score + points
add_points(score, 10)
add_points(score, 50)
print(score)
Explique ce qui se passe à ton prof IA, puis corrige la fonction pour que score vaille bien 60 à la fin.
3. Les fonctions pygame du jour
Les événements clavier
Chaque fois qu'on appuie sur une touche, pygame ajoute un événement à la file. Tu les reçois dans ta boucle for event in pygame.event.get().
| Élément | Ce que c'est |
|---|---|
pygame.KEYDOWN | Le type d'événement « une touche vient d'être enfoncée ». Il arrive une seule fois par appui. |
pygame.KEYUP | Le type d'événement « une touche vient d'être relâchée ». |
event.key | Le code de la touche concernée (un entier). On le compare à des constantes. |
pygame.K_UP, K_DOWN, K_LEFT, K_RIGHT | Les codes des quatre flèches. Il y en a un par touche : K_SPACE, K_ESCAPE, K_z, K_q… |
if event.type == pygame.KEYDOWN and event.key == pygame.K_UP:
print("Flèche du haut !")
La liste complète des codes est dans la documentation de pygame.key. Ton prof en parle aussi dans le Cours 3 de Python 2, avec les touches de modification (Shift, Alt…).
dt : le temps écoulé
Lundi, on a vu que clock.tick(60) limite la boucle à 60 images par seconde. Il renvoie aussi le nombre de millisecondes écoulées depuis le tour précédent. On le convertit en secondes :
dt = clock.tick(60) / 1000 # environ 0.016 seconde
Pourquoi c'est utile ? Si l'ordinateur rame et ne fait que 30 images par seconde, dt vaudra 0.033. En calculant les animations avec dt, elles durent le même temps quelle que soit la vitesse de l'ordinateur.
Le module math
Ce n'est pas pygame, mais on en aura besoin pour l'effet de bond : import math, puis math.sin(x) (le sinus) et math.pi (π). La fonction math.sin(math.pi * t) vaut 0 quand t = 0, monte jusqu'à 1 quand t = 0.5, et redescend à 0 quand t = 1. Parfait pour une grenouille qui gonfle au milieu de son saut !
4. Le fil rouge : la grenouille saute (1 h)
F1. Ranger lundi en fonctions
Reprends ton frogger.py de lundi et range-le en fonctions, sans changer ce qu'il affiche :
zone_color(row)renvoie la couleur d'une ligne (c'est tonif / elif / elsede lundi, avec desreturn).draw_background(screen)dessine tout le décor : bandes, pointillés, maisons.draw_frog(screen, x, y)dessine la grenouille à la position en pixels(x, y).
Ta boucle de jeu doit maintenant être très courte : draw_background(screen), draw_frog(...), flip().
Vérifie : relance le jeu, il doit afficher exactement la même chose qu'hier. Ranger du code sans changer son comportement, ça s'appelle refactoriser.
F2. Un saut instantané
On va repérer la grenouille par sa position en pixels (frog_x, frog_y), le coin en haut à gauche de sa case. Au départ :
frog_x, frog_y = cell_to_pixel(COLS // 2, ROWS - 1)
- Écris
direction_from_key(key)qui renvoie(0, -1)pour la flèche du haut,(0, 1)pour le bas,(-1, 0)pour la gauche,(1, 0)pour la droite, etNonepour toute autre touche. - Dans ta boucle d'événements, sur un
KEYDOWN, appelle cette fonction. Si elle ne renvoie pasNone, déballe le résultat endx, dyet déplace la grenouille d'une case :frog_x += dx * TILE. - Écris
is_on_screen(x, y)qui renvoieTruesi une grenouille en(x, y)serait entièrement dans la fenêtre. Ne déplace la grenouille que si sa nouvelle position est à l'écran.
Teste : la grenouille doit se téléporter de case en case, et rester bloquée contre les bords.
F3. Animer le saut
Une téléportation, ce n'est pas un saut. On veut que la grenouille glisse de sa case de départ à sa case d'arrivée en JUMP_TIME = 0.15 seconde.
Il faut retenir plusieurs informations pendant le saut :
| Variable | Rôle |
|---|---|
start_x, start_y | La case de départ du saut (en pixels) |
target_x, target_y | La case d'arrivée |
jump_t | L'avancement du saut : de 0 (départ) à 1 (arrivé). Vaut None quand la grenouille ne saute pas. |
frog_x, frog_y | La position affichée, quelque part entre départ et arrivée |
Le principe, en 3 temps :
- Au moment de l'appui (si la grenouille ne saute pas déjà) : le départ devient la case actuelle, l'arrivée la case voisine, et
jump_t = 0. - À chaque image, dans la partie « mise à jour » de la boucle : on fait avancer
jump_tdedt / JUMP_TIME, et on calcule la position affichée avec tonlerpde E3. - Quand
jump_tatteint 1 : on pose la grenouille pile sur la case d'arrivée et on remetjump_tàNone.
F4. L'effet de bond
Pour que le saut ressemble à un saut, faisons gonfler la grenouille au milieu du trajet :
- Écris
jump_bump(t)qui renvoiemath.sin(math.pi * t) * 6(6 pixels de gonflement au maximum). - Ajoute un paramètre
bumpàdraw_frog: la marge autour de la grenouille devient8 - bump. Plusbumpest grand, plus la grenouille est grosse. - Dans la boucle, calcule
bump(0 si la grenouille ne saute pas) et passe-le àdraw_frog.
En plein saut, ta grenouille devrait ressembler à ceci :

Une solution possible (essaie vraiment avant de regarder !)
import math
import sys
import pygame
TILE = 48
COLS = 13
ROWS = 13
WIDTH = COLS * TILE
HEIGHT = ROWS * TILE
JUMP_TIME = 0.15 # durée d'un saut, en secondes
HAIE = (34, 70, 50)
NENUPHAR = (96, 150, 90)
EAU = (58, 104, 150)
BERGE = (118, 158, 98)
ROUTE = (52, 54, 60)
MARQUAGE = (225, 215, 150)
TROTTOIR = (150, 138, 118)
GRENOUILLE = (130, 205, 80)
def zone_color(row: int) -> tuple:
if row == 0:
return HAIE
elif row <= 5:
return EAU
elif row == 6:
return BERGE
elif row <= 11:
return ROUTE
else:
return TROTTOIR
def draw_background(screen: pygame.Surface) -> None:
for row in range(ROWS):
pygame.draw.rect(screen, zone_color(row), pygame.Rect(0, row * TILE, WIDTH, TILE))
# Pointillés entre les voies de la route
for row in range(8, 12):
for col in range(COLS):
if col % 2 == 0:
dash = pygame.Rect(col * TILE + 8, row * TILE - 2, TILE - 16, 4)
pygame.draw.rect(screen, MARQUAGE, dash)
# Les 5 maisons
for i in range(5):
col = 2 + i * 2
pygame.draw.rect(screen, NENUPHAR, pygame.Rect(col * TILE + 4, 4, TILE - 8, TILE - 8))
def cell_to_pixel(col: int, row: int) -> tuple:
return col * TILE, row * TILE
def direction_from_key(key: int) -> tuple | None:
if key == pygame.K_UP:
return (0, -1)
elif key == pygame.K_DOWN:
return (0, 1)
elif key == pygame.K_LEFT:
return (-1, 0)
elif key == pygame.K_RIGHT:
return (1, 0)
return None
def is_on_screen(x: float, y: float) -> bool:
return 0 <= x <= WIDTH - TILE and 0 <= y <= HEIGHT - TILE
def lerp(a: float, b: float, t: float) -> float:
return a + (b - a) * t
def jump_bump(t: float) -> float:
# Nul au départ et à l'arrivée, maximal au milieu du saut
return math.sin(math.pi * t) * 6
def draw_frog(screen: pygame.Surface, x: float, y: float, bump: float) -> None:
margin = 8 - bump
frog = pygame.Rect(int(x + margin), int(y + margin),
int(TILE - 2 * margin), int(TILE - 2 * margin))
pygame.draw.rect(screen, GRENOUILLE, frog)
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Frogger d'entraînement - Mardi")
clock = pygame.time.Clock()
# Position affichée, et saut en cours (jump_t vaut None au repos)
frog_x, frog_y = cell_to_pixel(COLS // 2, ROWS - 1)
start_x, start_y = frog_x, frog_y
target_x, target_y = frog_x, frog_y
jump_t = None
while True:
dt = clock.tick(60) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
if event.type == pygame.KEYDOWN and jump_t is None:
direction = direction_from_key(event.key)
if direction is not None:
dx, dy = direction
new_x = target_x + dx * TILE
new_y = target_y + dy * TILE
if is_on_screen(new_x, new_y):
start_x, start_y = target_x, target_y
target_x, target_y = new_x, new_y
jump_t = 0
# Faire avancer le saut en cours, et le terminer pile sur la case d'arrivée
if jump_t is not None:
jump_t += dt / JUMP_TIME
if jump_t >= 1:
jump_t = None
frog_x, frog_y = target_x, target_y
else:
frog_x = lerp(start_x, target_x, jump_t)
frog_y = lerp(start_y, target_y, jump_t)
draw_background(screen)
if jump_t is None:
bump = 0
else:
bump = jump_bump(jump_t)
draw_frog(screen, frog_x, frog_y, bump)
pygame.display.flip()
Remarque : on a besoin de int(...) dans draw_frog parce que lerp produit des nombres à virgule, alors qu'un rectangle se dessine au pixel près.
Regarde ta boucle de jeu
Compare avec lundi : la boucle suit toujours les 4 temps (attendre, écouter, mettre à jour, dessiner), mais chaque temps est maintenant plus clair. L'écoute lance un saut, la mise à jour le fait avancer, le dessin l'affiche. Garde cette séparation dans ton projet : c'est la question préférée des oraux.
5. Le coin des commentaires (10 min)
Aujourd'hui : un bon nom vaut mieux qu'un commentaire
Maintenant que tu écris des fonctions, tu choisis leurs noms. Un bon nom rend la plupart des commentaires inutiles.
❌ Le commentaire compense un nom qui ne veut rien dire :
def f(a, b, t):
# calcule la position entre a et b
return a + (b - a) * t
✅ Le nom suffit :
def lerp(start, end, t):
return start + (end - start) * t
Une fonction a un nom de verbe (draw_frog, create_cars) ; une fonction qui répond par vrai ou faux a un nom de question (is_on_screen, is_jumping).
Les docstrings
Quand le nom ne suffit pas, on ajoute une docstring : un texte entre triples guillemets, juste sous le def, qui dit ce que fait la fonction (pas comment elle le fait).
def jump_bump(t: float) -> float:
"""Renvoie de combien de pixels la grenouille gonfle à l'instant t du saut (de 0 à 1)."""
return math.sin(math.pi * t) * 6
Contrairement à un commentaire #, Python garde la docstring : ton éditeur l'affiche quand tu survoles la fonction. Une phrase suffit. Et une fonction évidente comme draw_frog n'en a pas besoin.
Exercice
- Renomme ces fonctions et leurs paramètres pour que les commentaires deviennent inutiles, puis supprime-les :
def g(k):
# renvoie la direction correspondant à la touche
...
def h(x, y):
# vrai si la grenouille serait dans l'écran
...
- Dans ton code du jour, trouve la fonction dont le nom explique le moins bien ce qu'elle fait. Améliore son nom, ou ajoute-lui une docstring d'une phrase.
6. Défis perso (30 min, sans solution)
7. Les pièges classiques
| Symptôme | Cause probable |
|---|---|
| La fonction ne fait rien | Tu l'as définie mais jamais appelée, ou tu as oublié les parenthèses : draw_background au lieu de draw_background(screen). |
TypeError: … missing 1 required positional argument | L'appel ne donne pas autant d'arguments que la fonction a de paramètres. |
| Une variable ne change pas après l'appel | La fonction a modifié une variable locale. Renvoie la valeur avec return et affecte-la. |
TypeError: cannot unpack non-iterable NoneType object | Tu fais dx, dy = direction_from_key(...) alors que la fonction a renvoyé None. Teste is not None avant de déballer. |
La fonction renvoie None alors qu'elle devrait renvoyer une valeur | Il manque un return, ou tu as écrit print à la place. |
| La grenouille finit à côté des cases | La position n'est pas recalée sur la case d'arrivée à la fin du saut. |
| La grenouille part dans tous les sens | Les touches ne sont pas ignorées pendant un saut. |
| `TypeError: unsupported operand type(s) for | ` |
8. Checklist défense : je sais expliquer…
9. Mini-oral
Réponds à voix haute, puis vérifie. Mieux : tape /oral dans ton IA.
1. Que renvoie une fonction qui n'a pas de return ?
None. Si on écrit x = draw_background(screen), x vaut None.
2. Pourquoi lose_life(lives) ne change-t-il pas la variable lives ?
Dans la fonction, lives est une variable locale, une copie du nom. La réaffecter ne touche pas la variable extérieure. Il faut renvoyer la nouvelle valeur et l'affecter : lives = lose_life(lives).
3. Que vaut lerp(10, 20, 0.25) ? Explique la formule.
12.5. La formule est a + (b - a) * t : on part de a et on parcourt la fraction t du chemin b - a. Ici 10 + 10 × 0.25.
4. Pourquoi utiliser KEYDOWN plutôt que get_pressed() pour le saut ?
KEYDOWN arrive une seule fois par appui : un appui donne un saut. get_pressed() dit si la touche est enfoncée à chaque image : la grenouille sauterait en continu tant qu'on garde la touche.
5. Si le jeu tourne à 30 images par seconde au lieu de 60, le saut dure-t-il plus longtemps ?
Non. jump_t avance de dt / JUMP_TIME à chaque image. Avec 30 images par seconde, dt est deux fois plus grand, donc jump_t avance deux fois plus vite par image. Le saut dure toujours 0.15 seconde.
6. À quoi sert jump_t = None ?
À indiquer que la grenouille ne saute pas. On le teste pour savoir s'il faut faire avancer une animation, et pour ignorer les touches pendant un saut.
10. Aide-mémoire du mardi
# Définir et appeler
def name(param1, param2=valeur_par_defaut):
...
return resultat
x = name(arg1)
# Renvoyer plusieurs valeurs, puis déballer
def f():
return 1, 2
a, b = f()
# None
if value is None: ...
if value is not None: ...
# Portée : renvoyer plutôt que modifier
lives = lose_life(lives)
# Annotations
def f(x: int, y: float) -> tuple | None: ...
# Clavier
if event.type == pygame.KEYDOWN and event.key == pygame.K_UP: ...
# Temps écoulé, en secondes
dt = clock.tick(60) / 1000
# Animation entre deux positions
def lerp(a, b, t):
return a + (b - a) * t
t += dt / DURATION # de 0 à 1
Pour aller plus loin
- 📚 Python 1 · Chapitre 5 : Les fonctions
- 📚 Python 2 · Cours 3 : Interface graphique (événements clavier, touches de modification)
- 📖 Documentation pygame-ce : pygame.key et pygame.event
Demain, mercredi : les listes et le hasard. La route se remplit de voitures, la rivière de radeaux 🚗🪵