RouteJour 4 · Jeudi

Tout est objet

Une grenouille, ce n'est pas six variables éparpillées. C'est une chose qui a une position, qui sait sauter et se dessiner. Aujourd'hui, on range tout le jeu en classes.

  • Durée environ 3 h
  • Python classes, objets, __init__, self, attributs, méthodes, dataclasses, héritage, redéfinition de méthodes

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 724 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 : JEUDI, « TOUT EST OBJET »

## Où j'en suis

Lundi : bases, boucle de jeu, décor sur une grille (TILE = 48, COLS = 13, ROWS = 13 ; ligne 0 maisons, 1 à 5 eau, 6 berge, 7 à 11 route, 12 trottoir).
Mardi : fonctions ; la grenouille saute avec les flèches, animation par interpolation (start/target, jump_t, dt), effet de bond, pas de sortie d'écran.
Mercredi : listes et random ; obstacles représentés par des listes [x, y, largeur, vitesse, couleur] dans deux listes cars et rafts, générées voie par voie (ROAD_ROWS, RIVER_ROWS, 2 ou 3 obstacles par voie, découpage anti-chevauchement, même vitesse par voie, sens aléatoire), move_obstacles avec réapparition de l'autre côté, draw_obstacles. Le code est un script avec des fonctions et des variables globales dans la boucle.

## Objectif du jour

Le jeu fait exactement la même chose qu'hier (plus des phares à l'avant des voitures), mais il est rangé en classes : dataclass Obstacle, sous-classes Vehicle et Raft, classe Frog, classe App dans le style du professeur (__init__, process_events, update, draw, run ; App().run()). Notions : classes, objets, __init__, self, attributs, méthodes, dataclasses, héritage, redéfinition, polymorphisme. Pas de point direct du cahier des charges, mais la structure de tout le projet et une base solide pour la défense.

Questions de démarrage suggérées : est-ce que mes voitures et radeaux défilent bien ; ce que je sais des classes (self, __init__) ; si je me rappelle la classe Ball et la classe App du cours de mon professeur.

## Contenu de la fiche du jour

Section 1, Le Python du jour :
- 1.1 Le problème : variables de la grenouille éparpillées, obstacle[3] illisible, fonctions qui ne modifient pas les nombres.
- 1.2 Classe (plan) et objet (instance) ; Car(0, 50) construit un objet et appelle __init__ ; self = l'objet ; self.x crée un attribut ; objets indépendants.
- 1.3 Méthodes : fonctions de la classe, self en premier paramètre, passé automatiquement à l'appel (red_car.move(0.5)) ; une méthode modifie self.x sans return (résout le problème de portée de mardi).
- 1.4 Dataclasses (comme la classe Ball du cours 3 du prof) : @dataclass est un décorateur, chaque « nom: type » est un attribut, __init__ généré dans l'ordre, print lisible, valeurs par défaut placées après les attributs sans défaut. Dataclass pour un groupe de données (obstacle), classe classique quand l'initialisation calcule (grenouille).
- 1.5 Héritage : class Truck(Obstacle) ; sous-classe ; méthodes héritées ; redéfinition (describe) ; polymorphisme (même appel, réaction propre à chaque type) ; super() mentionné (pas utilisé).
- 1.6 Objets dans des objets : App contient self.frog, self.cars, self.rafts (imbrication de types du cours).

Section 2, Échauffements :
- E1 : classe classique Car (__init__(self, x, speed), move(self, dt), describe(self) qui renvoie une phrase) ; deux voitures indépendantes.
- E2 : la même en @dataclass ; print(car) ; attribut color = "rouge" par défaut ; le placer avant x donne TypeError: non-default argument follows default argument.
- E3 : dataclass Obstacle (x, speed, move, describe) ; sous-classes Vehicle (« Vroum ! ») et Raft (« Plouf ! ») ; une seule boucle sur une liste mixte.
- E4 : classe Player avec lives = 3 et lose_life(self) qui affiche « Game over » à 0 ; le problème de mardi résolu.

Section 3, Notions pygame : attributs de Rect (left, right, top, bottom, centerx, centery, center) ; pygame.draw.line(surface, couleur, départ, arrivée, épaisseur) ; structure App du cours : __init__ (init pygame, fenêtre, objets), process_events(event), update(dt), draw(), run() avec la boucle ; App().run().

Section 4, Fil rouge (solution disponible dans la fiche ; relancer le jeu après chaque étape) :
- F1 : dataclass Obstacle (x, y, width, speed) ; méthode update(dt) (avance + réapparition) ; rect() qui renvoie le Rect avec marge (servira aux collisions) ; draw(screen) ; create_cars/create_rafts créent des objets ; lane_positions(count, length) renvoie seulement les x tirés au hasard.
- F2 : Vehicle(Obstacle) avec color par défaut et draw redéfini (carrosserie + phares à l'avant : rect.right si speed > 0, sinon rect.left) ; Raft(Obstacle) avec draw redéfini (tronc brun + rainure avec draw.line).
- F3 : classe classique Frog : __init__(col, row), is_jumping(), jump(dx, dy), update(dt), rect() (avec l'effet de bond), draw(screen).
- F4 : classe App avec les 5 méthodes ; les fonctions outils restent des fonctions ; App().run().

Code de référence (une solution possible). Les constantes et les fonctions zone_color, draw_background, cell_to_pixel, direction_from_key, is_on_screen, lerp, jump_bump et random_speed sont identiques à mercredi ; on ajoute from dataclasses import dataclass, RADEAU_FONCE = (105, 78, 52), PHARE = (250, 240, 200), puis :

def lane_positions(count: int, length: int) -> list:
    # Chaque obstacle tire sa position au hasard dans sa propre part de la voie
    spacing = WIDTH / count
    positions = []
    for i in range(count):
        positions.append(i * spacing + random.uniform(0, spacing - length * TILE))
    return positions


@dataclass
class Obstacle:
    x: float
    y: int
    width: int
    speed: float

    def update(self, dt: float) -> None:
        self.x += self.speed * dt
        # Un obstacle sorti d'un côté réapparaît de l'autre
        if self.speed > 0 and self.x > WIDTH:
            self.x = -self.width
        elif self.speed < 0 and self.x + self.width < 0:
            self.x = WIDTH

    def rect(self) -> pygame.Rect:
        return pygame.Rect(int(self.x), self.y + 6, self.width, TILE - 12)

    def draw(self, screen: pygame.Surface) -> None:
        pygame.draw.rect(screen, (128, 128, 128), self.rect())


@dataclass
class Vehicle(Obstacle):
    color: tuple = (214, 96, 77)

    def draw(self, screen: pygame.Surface) -> None:
        body = self.rect()
        pygame.draw.rect(screen, self.color, body)
        # Les phares sont à l'avant, donc du côté où roule le véhicule
        if self.speed > 0:
            light_x = body.right - 6
        else:
            light_x = body.left + 2
        pygame.draw.rect(screen, PHARE, pygame.Rect(light_x, body.top + 4, 4, 6))
        pygame.draw.rect(screen, PHARE, pygame.Rect(light_x, body.bottom - 10, 4, 6))


@dataclass
class Raft(Obstacle):
    def draw(self, screen: pygame.Surface) -> None:
        log = self.rect()
        pygame.draw.rect(screen, RADEAU, log)
        pygame.draw.line(screen, RADEAU_FONCE, (log.left + 6, log.centery), (log.right - 6, log.centery), 2)


def create_cars() -> list:
    cars = []
    for row in ROAD_ROWS:
        length = random.randint(1, 2)
        speed = random_speed(40, 120)
        color = random.choice(CAR_COLORS)
        for x in lane_positions(random.randint(2, 3), length):
            cars.append(Vehicle(x, row * TILE, length * TILE, speed, color))
    return cars


def create_rafts() -> list:
    rafts = []
    for row in RIVER_ROWS:
        length = random.randint(2, 4)
        speed = random_speed(30, 80)
        for x in lane_positions(random.randint(2, 3), length):
            rafts.append(Raft(x, row * TILE, length * TILE, speed))
    return rafts


class Frog:
    def __init__(self, col: int, row: int):
        self.x, self.y = cell_to_pixel(col, row)
        self.start_x, self.start_y = self.x, self.y
        self.target_x, self.target_y = self.x, self.y
        self.jump_t = None

    def is_jumping(self) -> bool:
        return self.jump_t is not None

    def jump(self, dx: int, dy: int) -> None:
        if self.is_jumping():
            return
        new_x = self.target_x + dx * TILE
        new_y = self.target_y + dy * TILE
        if is_on_screen(new_x, new_y):
            self.start_x, self.start_y = self.target_x, self.target_y
            self.target_x, self.target_y = new_x, new_y
            self.jump_t = 0

    def update(self, dt: float) -> None:
        if not self.is_jumping():
            return
        self.jump_t += dt / JUMP_TIME
        if self.jump_t >= 1:
            # Fin du saut : on pose la grenouille pile sur la case d'arrivée
            self.jump_t = None
            self.x, self.y = self.target_x, self.target_y
        else:
            self.x = lerp(self.start_x, self.target_x, self.jump_t)
            self.y = lerp(self.start_y, self.target_y, self.jump_t)

    def rect(self) -> pygame.Rect:
        if self.is_jumping():
            margin = 8 - jump_bump(self.jump_t)
        else:
            margin = 8
        return pygame.Rect(int(self.x + margin), int(self.y + margin),
                           int(TILE - 2 * margin), int(TILE - 2 * margin))

    def draw(self, screen: pygame.Surface) -> None:
        pygame.draw.rect(screen, GRENOUILLE, self.rect())


class App:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("Frogger d'entraînement - Jeudi")
        self.frog = Frog(COLS // 2, ROWS - 1)
        self.cars = create_cars()
        self.rafts = create_rafts()

    def process_events(self, event: pygame.event.Event) -> None:
        if event.type == pygame.KEYDOWN:
            direction = direction_from_key(event.key)
            if direction is not None:
                dx, dy = direction
                self.frog.jump(dx, dy)

    def update(self, dt: float) -> None:
        self.frog.update(dt)
        for car in self.cars:
            car.update(dt)
        for raft in self.rafts:
            raft.update(dt)

    def draw(self) -> None:
        draw_background(self.screen)
        for raft in self.rafts:
            raft.draw(self.screen)
        for car in self.cars:
            car.draw(self.screen)
        self.frog.draw(self.screen)

    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()
                self.process_events(event)

            self.update(dt)
            self.draw()
            pygame.display.flip()


App().run()

Section 5, Le coin des commentaires : documenter classes et méthodes ; une docstring de classe dit ce que représente un objet (« La grenouille du joueur, repérée en pixels et animée pendant ses sauts. ») ; docstring de méthode seulement si le nom ne suffit pas, et elle dit quoi, pas comment (jump : « Lance un saut d'une case vers (dx, dy), sauf si la grenouille saute déjà ou sortirait de l'écran. ») ; draw ou is_jumping s'en passent ; les annotations de type documentent déjà les types. Exercice (sans solution) : docstrings pour Obstacle, Vehicle, Raft (en quoi elles diffèrent), Frog, App ; décider quelles méthodes en ont besoin et justifier.

Section 6, Défis perso (sans solution) :
- D1 : nouveau type d'obstacle (camion, moto, tortue, crocodile) en redéfinissant seulement draw ; savoir dire ce qui est hérité ou redéfini.
- D2 : une classe Lane (voie avec ses obstacles) ? essayer puis décider et justifier.
- D3 : méthode Frog.reset() pour revenir au départ (utile vendredi) ; orientation en attribut.
- D4 : __repr__ pour Frog, différence __repr__ / __str__.

Section 7, Pièges classiques : self oublié dans la définition (takes 2 positional arguments but 3 were given) ; x au lieu de self.x (NameError) ; x = … au lieu de self.x = … dans __init__ ; utiliser la classe au lieu d'un objet (AttributeError: type object) ; méthode sans parenthèses ; dataclass : non-default argument follows default argument ; dataclass : mutable default list interdit (field(default_factory=list)) ; méthode redéfinie mal nommée ou mal indentée.

Section 8, Checklist défense : classe contre objet ; rôle et moment de __init__ ; self ; attribut contre variable locale ; ce que génère @dataclass et quand l'utiliser ; ce que Vehicle et Raft héritent et redéfinissent ; polymorphisme dans mon jeu ; rôle des méthodes de App et enchaînement dans run ; chaque méthode de Frog ; attributs de Rect utilisés ; pourquoi certaines choses restent des fonctions.

Section 9, Mini-oral (réponses attendues) :
1. Frog est la classe ; Frog(6, 12) construit un objet en appelant __init__.
2. Dans self.frog.jump(0, -1), self vaut l'objet grenouille (dans App, self est l'application).
3. @dataclass génère __init__(self, x, y, width, speed), un affichage lisible et ==.
4. raft.update(dt) : méthode héritée d'Obstacle.
5. Polymorphisme : obstacle.draw(screen) dessine une voiture avec phares ou un tronc selon le type.
6. Frog en classe classique : choix, car l'initialisation calcule ; une dataclass aurait pu marcher.

# 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 : le grand rangementla brique Frogger du jour, à faire dans ton éditeur1 h
  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 jeuditout 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 (45 min)
  4. 2. Échauffements (30 min)
  5. 3. Les notions pygame du jour
  6. 4. Le fil rouge : le grand rangement (1 h)
  7. 5. Le coin des commentaires (10 min)
  8. 6. Défis perso (30 min, 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 jeudi
  13. Pour aller plus loin

Objectif du jour

À la fin de la journée, ton jeu fait exactement la même chose qu'hier, mais il est organisé en classes : Obstacle, Vehicle, Raft, Frog et App. C'est la structure qu'utilise ton prof dans son cours, et celle que ton correcteur s'attend à voir.

Ça peut sembler une journée « sans nouveauté visible ». C'est en fait la plus importante pour la défense : un code bien rangé en classes est un code que tu sais expliquer.

Lien avec le cahier des charges : aucun point directement, mais toute la suite (collisions, vies, états, menus) sera bien plus simple avec cette structure.


1. Le Python du jour (45 min)

1.1 Le problème

Regarde ce qu'il faut aujourd'hui pour décrire une grenouille :

frog_x, frog_y = ...
start_x, start_y = ...
target_x, target_y = ...
jump_t = None

Et pour une voiture : obstacle[0], obstacle[3]… Les données sont éparpillées, les noms sont obscurs, et les fonctions ne peuvent pas modifier les nombres (mardi, section 1.5). Les classes règlent ces trois problèmes d'un coup.

1.2 Classe et objet

Une classe est un plan de construction. Un objet (ou instance) est une chose construite à partir de ce plan.

class Car:
    def __init__(self, x, speed):
        self.x = x
        self.speed = speed

red_car = Car(0, 50)        # on construit un objet Car
blue_car = Car(300, -80)    # un deuxième, indépendant du premier

print(red_car.x)            # 0
print(blue_car.speed)       # -80
  • class Car: définit le plan.
  • Car(0, 50) construit un objet. Python appelle alors automatiquement la méthode spéciale __init__, qui initialise l'objet.
  • self désigne l'objet en train d'être construit (ou utilisé). self.x = x crée un attribut x qui appartient à cet objet.
  • Chaque objet a ses propres attributs : changer red_car.x ne change pas blue_car.x.

1.3 Les méthodes

Une méthode est une fonction qui appartient à une classe. Elle reçoit toujours l'objet en premier paramètre, self.

class Car:
    def __init__(self, x, speed):
        self.x = x
        self.speed = speed

    def move(self, dt):
        self.x += self.speed * dt

red_car = Car(0, 50)
red_car.move(0.5)
print(red_car.x)            # 25.0

À l'appel red_car.move(0.5), Python passe automatiquement red_car comme self : tu ne l'écris jamais entre les parenthèses.

1.4 Les dataclasses : le raccourci du cours

Écrire __init__ et tous les self.x = x est répétitif. Le module dataclasses le fait pour toi. C'est ce qu'utilise ton prof dans le Cours 3 avec sa classe Ball.

from dataclasses import dataclass

@dataclass
class Car:
    x: float
    speed: float

    def move(self, dt):
        self.x += self.speed * dt

car = Car(0, 50)
print(car)                  # Car(x=0, speed=50)
  • @dataclass est un décorateur : il modifie la classe juste en dessous.
  • Chaque ligne nom: type déclare un attribut. Python génère tout seul le __init__ qui les remplit, dans l'ordre.
  • Bonus : print(car) affiche un résumé lisible, pratique pour déboguer.
  • On peut donner une valeur par défaut : color: tuple = (255, 0, 0). Les attributs avec valeur par défaut doivent venir après ceux qui n'en ont pas.

1.5 L'héritage

Une voiture et un radeau ont beaucoup en commun : une position, une largeur, une vitesse, la même façon d'avancer et de revenir de l'autre côté. Mais ils ne se dessinent pas pareil.

L'héritage permet de créer une classe à partir d'une autre : elle récupère tout, et on ajoute ou on change ce qu'on veut.

@dataclass
class Obstacle:
    x: float
    speed: float

    def move(self, dt):
        self.x += self.speed * dt

    def describe(self):
        return "un obstacle"


@dataclass
class Truck(Obstacle):
    def describe(self):
        return "un camion"


truck = Truck(0, 40)
truck.move(1)               # méthode héritée d'Obstacle
print(truck.describe())     # « un camion » : la méthode a été redéfinie
  • class Truck(Obstacle): signifie « un Truck est un Obstacle ». On dit que Truck est une sous-classe (ou classe fille) d'Obstacle.
  • Truck hérite de move sans rien écrire.
  • Truck redéfinit describe : quand on l'appelle sur un camion, c'est sa version qui est utilisée.

La force de l'héritage : dans une liste qui mélange des obstacles de types différents, on peut appeler obstacle.describe() sur chacun sans se demander de quel type il est. Chacun répond à sa façon. C'est ce qu'on appelle le polymorphisme.

1.6 Un objet dans un objet

Un objet peut contenir d'autres objets comme attributs. Ton jeu sera une App qui contient une grenouille (self.frog) et des listes d'obstacles (self.cars, self.rafts). C'est exactement l'« imbrication de types » du cours de ton prof.

📚 Pour revoir ces notions : Python 2 · Cours 2 : Imbrication de types, classes et objets et le Labo 2.


2. Échauffements (30 min)

Du Python pur, dans un fichier echauffement_jeudi.py.

E1. Ma première classe

  1. Écris une classe classique Car avec un __init__(self, x, speed), une méthode move(self, dt) et une méthode describe(self) qui renvoie une phrase comme "Voiture en x=25.0, vitesse 50".
  2. Crée deux voitures, fais avancer seulement la première de 0.5 seconde, et affiche la description des deux. Vérifie qu'elles sont bien indépendantes.

E2. La même chose en dataclass

Réécris Car avec @dataclass. Supprime ton __init__ : il est généré automatiquement. Affiche une voiture avec print(car) et compare avec ta méthode describe.

Puis ajoute un attribut color avec la valeur par défaut "rouge". Essaie de le placer avant x : quelle erreur obtiens-tu, et pourquoi ?

E3. Héritage et polymorphisme

  1. Écris une dataclass Obstacle (attributs x et speed) avec les méthodes move(self, dt) et describe(self).
  2. Écris deux sous-classes : Vehicle, qui redéfinit describe pour renvoyer "Vroum !", et Raft, qui renvoie "Plouf !".
  3. Crée une liste qui contient deux véhicules et un radeau. Avec une seule boucle, fais avancer chaque obstacle et affiche sa description.

E4. Le compteur de vies, enfin

Mardi, on n'arrivait pas à écrire une fonction lose_life qui modifie les vies sans return. Écris une classe Player avec un attribut lives (3 au départ) et une méthode lose_life(self) qui enlève une vie et affiche "Game over" quand il n'en reste plus. Appelle-la trois fois.


3. Les notions pygame du jour

Les attributs d'un Rect

Un pygame.Rect ne contient pas seulement x, y, width et height. Il calcule aussi pour toi ses bords et son centre :

AttributValeur
rect.left, rect.rightAbscisse du bord gauche, du bord droit (left + width)
rect.top, rect.bottomOrdonnée du bord haut, du bord bas (top + height)
rect.centerx, rect.centeryCoordonnées du centre
rect.centerLe centre, sous forme de tuple (x, y)

On s'en sert aujourd'hui pour placer les phares à l'avant des voitures, et on s'en resservira beaucoup demain pour les collisions.

pygame.draw.line

pygame.draw.line(surface, couleur, depart, arrivee, epaisseur) trace un segment entre deux points (x, y). On l'utilise pour dessiner la rainure des troncs.

La structure App du cours

Ton prof range toujours son jeu dans une classe App avec cinq méthodes. On adopte exactement la même :

MéthodeRôle
__init__(self)Initialise pygame, crée la fenêtre et tous les objets du jeu
process_events(self, event)Réagit à un événement (touche, clic…)
update(self, dt)Met à jour l'état du jeu : fait avancer tout ce qui bouge
draw(self)Dessine toute l'image
run(self)Contient la boucle de jeu, qui appelle les trois méthodes précédentes

Le programme se lance avec une seule ligne : App().run(). Tu retrouves les 4 temps de lundi, chacun dans sa méthode.


4. Le fil rouge : le grand rangement (1 h)

On repart de ton code de mercredi. À chaque étape, relance le jeu : il doit toujours fonctionner exactement pareil. C'est le principe d'une refactorisation.

F1. La classe Obstacle

  1. Écris une dataclass Obstacle avec les attributs x, y, width et speed.
  2. Transforme move_obstacles en une méthode update(self, dt), qui fait avancer un seul obstacle (celui de self) et gère le retour de l'autre côté.
  3. Ajoute une méthode rect(self) qui renvoie le pygame.Rect de l'obstacle (avec la marge verticale). Demain, on s'en servira pour les collisions.
  4. Ajoute une méthode draw(self, screen) qui dessine ce rectangle.
  5. Modifie create_cars et create_rafts pour qu'elles créent des objets Obstacle(...) au lieu de petites listes.
  6. Dans ta boucle de jeu, remplace move_obstacles(cars, dt) par une boucle qui appelle car.update(dt) sur chaque voiture. Pareil pour le dessin.

Admire : obstacle[3] est devenu self.speed ✨

F2. Vehicle et Raft

  1. Crée une sous-classe Vehicle(Obstacle), avec un attribut supplémentaire color (avec une valeur par défaut). Redéfinis draw : la carrosserie dans sa couleur, et deux petits phares à l'avant, c'est-à-dire du côté où roule le véhicule.
  2. Crée une sous-classe Raft(Obstacle) qui redéfinit draw pour dessiner un tronc : un rectangle brun avec une rainure plus foncée au milieu (pygame.draw.line).
  3. create_cars crée des Vehicle, create_rafts crée des Raft.

F3. La classe Frog

Toutes les variables de la grenouille vont devenir les attributs d'une classe classique Frog :

  1. __init__(self, col, row) calcule la position de départ (avec cell_to_pixel) et initialise start_x, start_y, target_x, target_y et jump_t.
  2. is_jumping(self) renvoie True pendant un saut.
  3. jump(self, dx, dy) lance un saut si c'est possible (pas déjà en train de sauter, nouvelle case à l'écran).
  4. update(self, dt) fait avancer le saut en cours (ton code de mardi, avec des self. partout).
  5. rect(self) renvoie le rectangle de la grenouille, en tenant compte de l'effet de bond.
  6. draw(self, screen) dessine ce rectangle.

Remarque comme la boucle d'événements devient simple : self.frog.jump(dx, dy), et c'est la grenouille qui décide si elle peut sauter.

F4. La classe App

Dernière étape : range tout le reste dans une classe App, avec les cinq méthodes de la section 3. Tes fonctions « outils » (zone_color, draw_background, lerp, random_speed…) peuvent rester des fonctions : tout ne doit pas être une classe.

La fin de ton fichier doit être :

App().run()

Le résultat à l'écran est le même qu'hier… avec des phares en plus :

Le jeu rangé en classes, avec des phares à l'avant des voitures

Une solution possible (essaie vraiment avant de regarder !)

Les constantes, zone_color, draw_background, cell_to_pixel, direction_from_key, is_on_screen, lerp, jump_bump et random_speed ne changent pas par rapport à mercredi. En haut du fichier, on ajoute :

from dataclasses import dataclass

RADEAU_FONCE = (105, 78, 52)
PHARE = (250, 240, 200)

Puis :

def lane_positions(count: int, length: int) -> list:
    # Chaque obstacle tire sa position au hasard dans sa propre part de la voie
    spacing = WIDTH / count
    positions = []
    for i in range(count):
        positions.append(i * spacing + random.uniform(0, spacing - length * TILE))
    return positions


@dataclass
class Obstacle:
    x: float
    y: int
    width: int
    speed: float

    def update(self, dt: float) -> None:
        self.x += self.speed * dt
        # Un obstacle sorti d'un côté réapparaît de l'autre
        if self.speed > 0 and self.x > WIDTH:
            self.x = -self.width
        elif self.speed < 0 and self.x + self.width < 0:
            self.x = WIDTH

    def rect(self) -> pygame.Rect:
        return pygame.Rect(int(self.x), self.y + 6, self.width, TILE - 12)

    def draw(self, screen: pygame.Surface) -> None:
        pygame.draw.rect(screen, (128, 128, 128), self.rect())


@dataclass
class Vehicle(Obstacle):
    color: tuple = (214, 96, 77)

    def draw(self, screen: pygame.Surface) -> None:
        body = self.rect()
        pygame.draw.rect(screen, self.color, body)
        # Les phares sont à l'avant, donc du côté où roule le véhicule
        if self.speed > 0:
            light_x = body.right - 6
        else:
            light_x = body.left + 2
        pygame.draw.rect(screen, PHARE, pygame.Rect(light_x, body.top + 4, 4, 6))
        pygame.draw.rect(screen, PHARE, pygame.Rect(light_x, body.bottom - 10, 4, 6))


@dataclass
class Raft(Obstacle):
    def draw(self, screen: pygame.Surface) -> None:
        log = self.rect()
        pygame.draw.rect(screen, RADEAU, log)
        pygame.draw.line(screen, RADEAU_FONCE, (log.left + 6, log.centery), (log.right - 6, log.centery), 2)


def create_cars() -> list:
    cars = []
    for row in ROAD_ROWS:
        length = random.randint(1, 2)
        speed = random_speed(40, 120)
        color = random.choice(CAR_COLORS)
        for x in lane_positions(random.randint(2, 3), length):
            cars.append(Vehicle(x, row * TILE, length * TILE, speed, color))
    return cars


def create_rafts() -> list:
    rafts = []
    for row in RIVER_ROWS:
        length = random.randint(2, 4)
        speed = random_speed(30, 80)
        for x in lane_positions(random.randint(2, 3), length):
            rafts.append(Raft(x, row * TILE, length * TILE, speed))
    return rafts


class Frog:
    def __init__(self, col: int, row: int):
        self.x, self.y = cell_to_pixel(col, row)
        self.start_x, self.start_y = self.x, self.y
        self.target_x, self.target_y = self.x, self.y
        self.jump_t = None

    def is_jumping(self) -> bool:
        return self.jump_t is not None

    def jump(self, dx: int, dy: int) -> None:
        if self.is_jumping():
            return
        new_x = self.target_x + dx * TILE
        new_y = self.target_y + dy * TILE
        if is_on_screen(new_x, new_y):
            self.start_x, self.start_y = self.target_x, self.target_y
            self.target_x, self.target_y = new_x, new_y
            self.jump_t = 0

    def update(self, dt: float) -> None:
        if not self.is_jumping():
            return
        self.jump_t += dt / JUMP_TIME
        if self.jump_t >= 1:
            # Fin du saut : on pose la grenouille pile sur la case d'arrivée
            self.jump_t = None
            self.x, self.y = self.target_x, self.target_y
        else:
            self.x = lerp(self.start_x, self.target_x, self.jump_t)
            self.y = lerp(self.start_y, self.target_y, self.jump_t)

    def rect(self) -> pygame.Rect:
        if self.is_jumping():
            margin = 8 - jump_bump(self.jump_t)
        else:
            margin = 8
        return pygame.Rect(int(self.x + margin), int(self.y + margin),
                           int(TILE - 2 * margin), int(TILE - 2 * margin))

    def draw(self, screen: pygame.Surface) -> None:
        pygame.draw.rect(screen, GRENOUILLE, self.rect())


class App:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("Frogger d'entraînement - Jeudi")
        self.frog = Frog(COLS // 2, ROWS - 1)
        self.cars = create_cars()
        self.rafts = create_rafts()

    def process_events(self, event: pygame.event.Event) -> None:
        if event.type == pygame.KEYDOWN:
            direction = direction_from_key(event.key)
            if direction is not None:
                dx, dy = direction
                self.frog.jump(dx, dy)

    def update(self, dt: float) -> None:
        self.frog.update(dt)
        for car in self.cars:
            car.update(dt)
        for raft in self.rafts:
            raft.update(dt)

    def draw(self) -> None:
        draw_background(self.screen)
        for raft in self.rafts:
            raft.draw(self.screen)
        for car in self.cars:
            car.draw(self.screen)
        self.frog.draw(self.screen)

    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()
                self.process_events(event)

            self.update(dt)
            self.draw()
            pygame.display.flip()


App().run()

Regarde ton code

Lis ta méthode App.update à voix haute : « la grenouille se met à jour, chaque voiture se met à jour, chaque radeau se met à jour ». Le code se lit comme une phrase. Le jour de la défense, c'est exactement comme ça que tu le présenteras.


5. Le coin des commentaires (10 min)

Aujourd'hui : documenter ses classes et ses méthodes

Une classe mérite une phrase qui dit ce que représente un objet de cette classe. On l'écrit en docstring, juste sous la ligne class :

class Frog:
    """La grenouille du joueur, repérée en pixels et animée pendant ses sauts."""

Pour les méthodes, même règle que mardi pour les fonctions : une docstring seulement si le nom ne suffit pas, et elle dit ce que fait la méthode, pas comment.

    def jump(self, dx: int, dy: int) -> None:
        """Lance un saut d'une case vers (dx, dy), sauf si la grenouille saute déjà ou sortirait de l'écran."""

Ici, le nom jump ne dit pas que le saut peut être refusé : la docstring l'apprend au lecteur. En revanche, draw(self, screen) ou is_jumping(self) se passent très bien d'explication.

Exercice

  1. Écris une docstring d'une phrase pour chacune de tes classes : Obstacle, Vehicle, Raft, Frog, App. Pour Vehicle et Raft, dis en quoi elles diffèrent d'Obstacle.
  2. Passe en revue les méthodes de Frog et de App : lesquelles ont besoin d'une docstring, lesquelles non ? Justifie tes choix à ton prof IA.

6. Défis perso (30 min, sans solution)


7. Les pièges classiques

SymptômeCause probable
TypeError: Frog.jump() takes 2 positional arguments but 3 were givenTu as oublié self comme premier paramètre de la méthode.
NameError: name 'x' is not defined dans une méthodeTu as écrit x au lieu de self.x. Les attributs s'utilisent toujours avec self..
Un attribut « disparaît » après __init__Dans __init__, tu as écrit x = … (variable locale) au lieu de self.x = … (attribut).
AttributeError: type object 'Frog' has no attribute 'x'Tu utilises la classe Frog au lieu d'un objet : il manque Frog(...) pour construire la grenouille.
Rien ne se passe à l'appel d'une méthodeLes parenthèses manquent : self.frog.update au lieu de self.frog.update(dt).
TypeError: non-default argument 'b' follows default argument 'a'Dans une dataclass, un attribut sans valeur par défaut est placé après un attribut qui en a une.
ValueError: mutable default <class 'list'> for field … is not allowedUne dataclass ne peut pas avoir une liste comme valeur par défaut. Il faut field(default_factory=list).
La sous-classe se dessine comme la classe mèreLa méthode redéfinie n'a pas exactement le même nom, ou elle n'est pas indentée dans la sous-classe.

8. Checklist défense : je sais expliquer…


9. Mini-oral

Réponds à voix haute, puis vérifie. Mieux : tape /oral dans ton IA.

1. Quelle est la différence entre Frog et Frog(6, 12) ?

Frog est la classe, le plan de construction. Frog(6, 12) construit un objet grenouille à partir de ce plan, en appelant __init__ avec col = 6 et row = 12.

2. Dans self.frog.jump(0, -1), que vaut self à l'intérieur de jump ?

L'objet grenouille self.frog. Python le passe automatiquement comme premier argument de la méthode. (Attention : à l'extérieur, dans la méthode de App, self désigne l'application.)

3. Que génère @dataclass pour la classe Obstacle ?

Un __init__(self, x, y, width, speed) qui range chaque argument dans l'attribut du même nom, et une méthode d'affichage lisible (Obstacle(x=…, y=…, …)). Il génère aussi une comparaison == entre objets.

4. Que se passe-t-il quand on appelle raft.update(dt) sur un Raft ?

Raft ne définit pas update, donc Python cherche dans la classe mère Obstacle et utilise sa méthode update. C'est l'héritage.

5. Qu'est-ce que le polymorphisme, dans ton jeu ?

Appeler la même méthode sur des objets de types différents, et chacun réagit à sa façon. Par exemple, obstacle.draw(screen) dessine une voiture avec des phares pour un Vehicle, et un tronc pour un Raft.

6. Pourquoi la grenouille n'est-elle pas une dataclass ?

C'est un choix : son initialisation calcule la position à partir d'une case et prépare plusieurs attributs liés au saut. Une classe classique avec __init__ exprime ça plus naturellement. Une dataclass aurait aussi pu marcher.


10. Aide-mémoire du jeudi

# Classe classique
class Frog:
    def __init__(self, col, row):
        self.x = col * TILE          # attribut
    def jump(self, dx, dy):          # méthode
        self.x += dx * TILE

frog = Frog(6, 12)                   # construire un objet
frog.jump(1, 0)                      # appeler une méthode (self implicite)

# Dataclass
from dataclasses import dataclass

@dataclass
class Obstacle:
    x: float
    speed: float
    color: tuple = (255, 0, 0)       # valeurs par défaut à la fin

# Héritage et redéfinition
@dataclass
class Raft(Obstacle):
    def draw(self, screen):          # remplace la version d'Obstacle
        ...

# Structure du cours
class App:
    def __init__(self): ...
    def process_events(self, event): ...
    def update(self, dt): ...
    def draw(self): ...
    def run(self): ...

App().run()

# Rect
rect.left; rect.right; rect.top; rect.bottom; rect.centerx; rect.center

Pour aller plus loin

Demain, vendredi : les règles du jeu. Collisions, radeaux, vies, chrono, maisons et score : la grenouille peut enfin perdre 💥

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.