Snake en Python – 3 versions pour comprendre ce que fait vraiment Pygame

Snake en Python – 3 versions pour comprendre ce que fait vraiment Pygame 🐍

Dans cet article, on va programmer un même petit jeu ultra classique : Snake. Le but n’est pas juste d’avoir un jeu qui marche, mais de comprendre ce que Pygame fait pour nous.

On réalise 3 versions du même jeu :

  1. Version A : Snake en mode console (texte) avec le module curses.
  2. Version B : Snake en mode graphique mais sans Pygame (avec tkinter).
  3. Version C : Snake en Pygame, comme dans les vrais jeux 2D.

L’idée : voir ce qui change, ce qui reste identique, et ce que Pygame simplifie (gestion de la fenêtre, du clavier, des images, du temps…).


1. Rappel des règles du jeu Snake

  • Le serpent est une chaîne de cases (une liste de coordonnées).
  • À chaque “tour”, la tête avance d’une case dans une direction (haut / bas / gauche / droite).
  • Si la tête mange une pomme, le serpent grandit.
  • Si la tête touche un mur ou son propre corpsGame Over.

Dans les 3 versions, on garde la même logique de base :

  1. Lire le clavier (pour la direction).
  2. Mettre à jour la position du serpent et de la pomme.
  3. Tester les collisions (mur / soi-même / pomme).
  4. Afficher la nouvelle image (console ou graphique).

2. Version A – Snake en mode console (module curses)

Ici, on utilise uniquement du texte dans le terminal. On va dessiner une grille avec des caractères :

  • # pour les murs
  • O pour la tête du serpent, o pour le corps
  • * pour la pomme
  • (espace) pour le vide

Le module curses permet :

  • de contrôler le curseur ;
  • d’écrire du texte à des positions précises ;
  • de lire les touches sans bloquer le programme ;
  • de rafraîchir l’écran très vite.

Code complet – Version console

💡 Sous Windows, il faut parfois installer windows-curses avec pip install windows-curses.

import curses
import random

GRID_WIDTH = 25   # largeur de la grille (colonnes)
GRID_HEIGHT = 15  # hauteur de la grille (lignes)

def main(stdscr):
    # --- Initialisation du terminal ---
    curses.curs_set(0)        # cacher le curseur
    stdscr.nodelay(True)      # lecture non bloquante du clavier
    stdscr.timeout(120)       # 120 ms par "frame" ≈ 8 images / seconde

    # Serpent : liste de (x, y), tête en premier
    snake = [(GRID_WIDTH // 2, GRID_HEIGHT // 2)]
    direction = (1, 0)  # (dx, dy) -> vers la droite

    # Pomme de départ
    apple = (
        random.randint(1, GRID_WIDTH - 2),
        random.randint(1, GRID_HEIGHT - 2),
    )

    score = 0
    alive = True

    while alive:
        # --- 1) LIRE LE CLAVIER ---
        key = stdscr.getch()
        if key == curses.KEY_UP and direction != (0, 1):
            direction = (0, -1)
        elif key == curses.KEY_DOWN and direction != (0, -1):
            direction = (0, 1)
        elif key == curses.KEY_LEFT and direction != (1, 0):
            direction = (-1, 0)
        elif key == curses.KEY_RIGHT and direction != (-1, 0):
            direction = (1, 0)

        # --- 2) METTRE À JOUR LE SERPENT ---
        head_x, head_y = snake[0]
        dx, dy = direction
        new_head = (head_x + dx, head_y + dy)

        # a) Collision avec les murs ?
        if (new_head[0] <= 0 or new_head[0] >= GRID_WIDTH - 1 or
            new_head[1] <= 0 or new_head[1] >= GRID_HEIGHT - 1):
            alive = False
            break

        # b) Collision avec soi-même ?
        if new_head in snake:
            alive = False
            break

        # c) Avancer : on insère la nouvelle tête
        snake.insert(0, new_head)

        # d) Pomme mangée ?
        if new_head == apple:
            score += 1
            apple = (
                random.randint(1, GRID_WIDTH - 2),
                random.randint(1, GRID_HEIGHT - 2),
            )
        else:
            # Sinon, on enlève la dernière case (la queue)
            snake.pop()

        # --- 3) DESSINER LA GRILLE ---
        stdscr.clear()
        for y in range(GRID_HEIGHT):
            line = ""
            for x in range(GRID_WIDTH):
                # Murs
                if x == 0 or x == GRID_WIDTH - 1 or y == 0 or y == GRID_HEIGHT - 1:
                    ch = "#"
                # Pomme
                elif (x, y) == apple:
                    ch = "*"
                # Serpent
                elif (x, y) in snake:
                    if (x, y) == snake[0]:
                        ch = "O"    # tête
                    else:
                        ch = "o"    # corps
                else:
                    ch = " "
                line += ch
            stdscr.addstr(y, 0, line)

        stdscr.addstr(GRID_HEIGHT, 0, f"Score : {score}")
        stdscr.refresh()

    # --- 4) FIN DE PARTIE ---
    stdscr.clear()
    stdscr.addstr(0, 0, "GAME OVER")
    stdscr.addstr(1, 0, f"Score final : {score}")
    stdscr.addstr(3, 0, "Appuie sur une touche pour quitter.")
    stdscr.refresh()
    stdscr.nodelay(False)
    stdscr.getch()

if __name__ == "__main__":
    curses.wrapper(main)

Ce qu’il faut retenir de cette version

  • On gère tout à la main : timer, lecture des touches, dessin ligne par ligne.
  • L’affichage est uniquement composé de caractères texte.
  • Pour un petit jeu, ça marche très bien… mais ce n’est pas très “sexy” 😄.

3. Version B – Snake en mode graphique sans Pygame (avec Tkinter)

Cette fois, on veut une vraie fenêtre graphique avec des rectangles colorés. On utilise le module standard tkinter, fourni avec Python.

On ajoute une étape : la carte de jeu est dessinée dans un Canvas (un espace de dessin) avec des rectangles :

  • un rectangle vert pour chaque partie du serpent ;
  • un rectangle rouge pour la pomme ;
  • un fond noir pour la grille.

Code complet – Version Tkinter

import tkinter as tk
import random

CELL_SIZE = 20
GRID_WIDTH = 20
GRID_HEIGHT = 20

WINDOW_WIDTH = GRID_WIDTH * CELL_SIZE
WINDOW_HEIGHT = GRID_HEIGHT * CELL_SIZE

# État du jeu (variables globales pour rester simple)
snake = [(10, 10), (9, 10), (8, 10)]
direction = (1, 0)  # vers la droite
apple = (5, 5)
running = True

def spawn_apple():
    """Place une pomme à un endroit où le serpent n'est pas."""
    while True:
        x = random.randint(0, GRID_WIDTH - 1)
        y = random.randint(0, GRID_HEIGHT - 1)
        if (x, y) not in snake:
            return (x, y)

def change_direction(dx, dy):
    """Changer la direction sans faire demi-tour instantané."""
    global direction
    # Empêche de repartir directement dans l'autre sens
    if (dx, dy) == (-direction[0], -direction[1]):
        return
    direction = (dx, dy)

def on_key(event):
    """Gérer les flèches du clavier."""
    key = event.keysym
    if key == "Up":
        change_direction(0, -1)
    elif key == "Down":
        change_direction(0, 1)
    elif key == "Left":
        change_direction(-1, 0)
    elif key == "Right":
        change_direction(1, 0)

def draw():
    """Dessiner la grille, le serpent et la pomme."""
    canvas.delete("all")

    # Pomme
    ax, ay = apple
    canvas.create_rectangle(
        ax * CELL_SIZE,
        ay * CELL_SIZE,
        (ax + 1) * CELL_SIZE,
        (ay + 1) * CELL_SIZE,
        fill="red",
    )

    # Serpent
    for i, (x, y) in enumerate(snake):
        color = "green" if i == 0 else "lightgreen"
        canvas.create_rectangle(
            x * CELL_SIZE,
            y * CELL_SIZE,
            (x + 1) * CELL_SIZE,
            (y + 1) * CELL_SIZE,
            fill=color,
        )

def game_loop():
    """Une 'étape' du jeu : avancer le serpent, vérifier les collisions, redessiner."""
    global snake, apple, running

    if not running:
        return

    head_x, head_y = snake[0]
    dx, dy = direction
    new_head = (head_x + dx, head_y + dy)

    # Collision avec les bords ?
    if not (0 <= new_head[0] < GRID_WIDTH and 0 <= new_head[1] < GRID_HEIGHT):
        running = False
        canvas.create_text(
            WINDOW_WIDTH // 2,
            WINDOW_HEIGHT // 2,
            text="GAME OVER",
            fill="white",
            font=("Arial", 24, "bold"),
        )
        return

    # Collision avec soi-même ?
    if new_head in snake:
        running = False
        canvas.create_text(
            WINDOW_WIDTH // 2,
            WINDOW_HEIGHT // 2,
            text="GAME OVER",
            fill="white",
            font=("Arial", 24, "bold"),
        )
        return

    # Avancer
    snake.insert(0, new_head)

    # Pomme ?
    if new_head == apple:
        apple = spawn_apple()
    else:
        snake.pop()

    draw()
    # Rappeler game_loop dans 120 ms
    root.after(120, game_loop)

# --- Programme principal Tkinter ---
root = tk.Tk()
root.title("Snake (Tkinter, sans Pygame)")

canvas = tk.Canvas(root, width=WINDOW_WIDTH, height=WINDOW_HEIGHT, bg="black")
canvas.pack()

apple = spawn_apple()
draw()

root.bind("<Key>", on_key)

# Lancer la boucle de jeu
root.after(120, game_loop)
root.mainloop()

Ce qu’il faut retenir de cette version

  • On gère la fenêtre et le dessin avec Tkinter.
  • On doit soi-même :
    • créer un Canvas ;
    • dessiner des rectangles à chaque frame ;
    • utiliser root.after(...) comme petite “boucle de jeu”.
  • On commence à se rapprocher du fonctionnement d’un vrai jeu 2D.

4. Version C – Snake avec Pygame

Maintenant, on refait le même jeu avec Pygame.

Ce que Pygame apporte par rapport à Tkinter :

  • une boucle de jeu très naturelle (événements + dessin + clock.tick()),
  • une gestion simple du clavier,
  • un dessin optimisé pour le jeu temps réel,
  • plus tard : sons, images, sprites, collisions plus avancées, etc.

Code complet – Version Pygame

💡 Installation : pip install pygame

import pygame
import random

CELL_SIZE = 20
GRID_WIDTH = 20
GRID_HEIGHT = 20

WINDOW_WIDTH = GRID_WIDTH * CELL_SIZE
WINDOW_HEIGHT = GRID_HEIGHT * CELL_SIZE

# Couleurs (R, G, B)
BLACK = (0, 0, 0)
GREEN = (0, 200, 0)
LIGHT_GREEN = (150, 255, 150)
RED = (255, 50, 50)
WHITE = (255, 255, 255)

def spawn_apple(snake):
    while True:
        x = random.randint(0, GRID_WIDTH - 1)
        y = random.randint(0, GRID_HEIGHT - 1)
        if (x, y) not in snake:
            return (x, y)

def main():
    pygame.init()
    screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
    pygame.display.set_caption("Snake (Pygame)")

    clock = pygame.time.Clock()

    snake = [(10, 10), (9, 10), (8, 10)]
    direction = (1, 0)
    apple = spawn_apple(snake)
    running = True

    font = pygame.font.SysFont("arial", 24, bold=True)

    while running:
        # --- 1) Événements (clavier, fermeture) ---
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_UP and direction != (0, 1):
                    direction = (0, -1)
                elif event.key == pygame.K_DOWN and direction != (0, -1):
                    direction = (0, 1)
                elif event.key == pygame.K_LEFT and direction != (1, 0):
                    direction = (-1, 0)
                elif event.key == pygame.K_RIGHT and direction != (-1, 0):
                    direction = (1, 0)

        # --- 2) Logique du serpent ---
        head_x, head_y = snake[0]
        dx, dy = direction
        new_head = (head_x + dx, head_y + dy)

        # Collisions
        if not (0 <= new_head[0] < GRID_WIDTH and 0 <= new_head[1] < GRID_HEIGHT):
            running = False
        if new_head in snake:
            running = False

        snake.insert(0, new_head)

        if new_head == apple:
            apple = spawn_apple(snake)
        else:
            snake.pop()

        # --- 3) Dessin ---
        screen.fill(BLACK)

        # Pomme
        ax, ay = apple
        pygame.draw.rect(
            screen,
            RED,
            (ax * CELL_SIZE, ay * CELL_SIZE, CELL_SIZE, CELL_SIZE),
        )

        # Serpent
        for i, (x, y) in enumerate(snake):
            color = GREEN if i == 0 else LIGHT_GREEN
            pygame.draw.rect(
                screen,
                color,
                (x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE),
            )

        pygame.display.flip()

        # --- 4) Contrôle de la vitesse ---
        clock.tick(8)  # 8 images par seconde

    # Affichage "Game Over"
    screen.fill(BLACK)
    text = font.render("GAME OVER", True, WHITE)
    rect = text.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2))
    screen.blit(text, rect)
    pygame.display.flip()

    pygame.time.wait(2000)
    pygame.quit()

if __name__ == "__main__":
    main()

Ce qu’il faut retenir de cette version

  • Pygame fournit la boucle de jeu standard : événements → logique → dessin → clock.tick().
  • Dès que l’on veut des animations rapides, des sprites, des sons, Pygame est bien plus confortable que Tkinter.
  • La logique du jeu (serpent, pommes, collisions) reste quasiment la même que dans les autres versions.

5. Comparaison des 3 approches

Version Type d’affichage Difficulté Idéal pour…
A – console (curses) Texte uniquement Intermédiaire Comprendre la logique pure d’un jeu sans graphique.
B – Tkinter Graphique simple (rectangles, Canvas) Intermédiaire Passer du texte au graphique en restant dans la bibliothèque standard.
C – Pygame Graphique optimisé pour le jeu Intermédiaire → avancé Créer de “vrais” jeux 2D, avec animations, sons, sprites…

6. Idées d’activités pour les élèves

  • Cycle 1 : observer la version Pygame, repérer la tête, la pomme, les collisions.
  • Cycle 2 : changer la taille de la grille, la couleur du serpent, la vitesse du jeu.
  • Collège : comparer le code Tkinter et Pygame, repérer les parties identiques (logique du serpent) et différentes (affichage, boucle de jeu).
  • Lycée : ajouter un score affiché à l’écran, des niveaux (vitesse qui augmente), ou des murs fixes à éviter.

L’objectif n’est pas seulement de “coder un Snake”, mais de comprendre les couches : la logique du jeu, la gestion de l’affichage, et le rôle des bibliothèques comme Pygame qui simplifient le travail du programmeur.

Commentaires

Posts les plus consultés de ce blog

Basthon.fr

mBot2 - programmation mBlock/python

Mario Kart 2D