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 :
- Version A : Snake en mode console (texte) avec le module
curses. - Version B : Snake en mode graphique mais sans Pygame (avec
tkinter). - 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 corps → Game Over.
Dans les 3 versions, on garde la même logique de base :
- Lire le clavier (pour la direction).
- Mettre à jour la position du serpent et de la pomme.
- Tester les collisions (mur / soi-même / pomme).
- 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 mursOpour la tête du serpent,opour 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
Enregistrer un commentaire