Morpion
Le morpion (ou tic-tac-toe) se joue sur une grille 3×3. Deux joueurs alternent : X place son pion, puis O, jusqu'à ce que l'un aligne trois pions sur une ligne, une colonne ou une diagonale — ou que la grille soit pleine (match nul).
Le code s'appuie sur la bibliothèque metagrid, qui gère la fenêtre, les clics et l'affichage. Tu fournis quatre fonctions de rappel (callbacks) que metagrid appelle automatiquement :
init: une fois au démarrage — tu initialises l'état du jeu.draw: à chaque image (~60 fois par seconde) — tu colories chaque cellule.cliquer: quand le joueur clique sur une cellule — tu reçois(i, j).touche: quand le joueur appuie sur une touche — tu reçois le caractère pressé.
Installer metagrid dans le terminal : uv add metagrid
Crée un fichier morpion.py avec ce squelette complet :
"""
Morpion (Tic-tac-toe)
X joue en premier. Les joueurs alternent en cliquant sur une case vide.
Gagner : aligner 3 pions sur une ligne, une colonne ou une diagonale.
Touche r : recommencer.
"""
import metagrid
NB_LIGNES = 3
NB_COLONNES = 3
TAILLE_CASE = 180
COULEUR_VIDE = "#DDDDDD"
COULEUR_X = "#2255FF"
COULEUR_O = "#FF3333"
COULEUR_VICTOIRE = "#44FF88"
grille: list[list[str | None]] # None = vide, "X" ou "O" = occupée
joueur_actif: str # "X" ou "O"
flag_game_over: bool
jeu: metagrid.AbstractEngine
def ligne_gagnante(i1: int, j1: int, i2: int, j2: int, i3: int, j3: int, joueur: str) -> bool:
"""Renvoie True si les trois cases données appartiennent toutes à joueur."""
pass
def a_gagne(joueur: str) -> bool:
"""Renvoie True si joueur a aligné trois pions."""
pass
def est_pleine() -> bool:
"""Renvoie True si toutes les cases sont occupées."""
pass
def init():
"""Callback - appelé au démarrage. Remet le jeu à zéro."""
global grille, joueur_actif, flag_game_over
pass
def cliquer(i: int, j: int, _modif: str):
"""Callback - appelé avec (i, j) quand le joueur clique une cellule."""
global joueur_actif, flag_game_over
pass
def touche(key: str):
"""Callback - appelé avec la touche pressée. r pour recommencer."""
pass
def draw():
"""Callback - appelé à chaque frame. Affiche la grille."""
pass
if __name__ == "__main__":
jeu = metagrid.create(NB_LIGNES, NB_COLONNES, TAILLE_CASE, 4)
jeu.on_init(init)
jeu.on_click(cliquer)
jeu.on_key(touche)
jeu.on_draw(draw)
jeu.start()
Étape 1 - La grille
La grille est une liste de listes. Chaque cellule contient None (case vide), "X" ou "O".
Représentation
-
Considère cette grille en cours de partie :
Que vaut
grille[0][0]?grille[1][1]?grille[0][2]? Retrouve chaque valeur sur le schéma ci-dessous : -
Comment placer
"O"dans la case(2, 2)en une seule instruction ? -
Complétez
init:
Étape 2 - Afficher la grille
metagrid fournit deux fonctions pour dessiner une cellule :
jeu.set_cell_color(i, j, "#RRGGBB")— colorie le fond de la cellule.jeu.set_cell_char(i, j, "X", "#RRGGBB")— affiche un caractère par-dessus le fond. Passer""efface le caractère.
Fonction draw
Complétez draw pour parcourir toutes les cellules et les afficher :
def draw():
for i in range(NB_LIGNES):
for j in range(NB_COLONNES):
jeu.set_cell_color(i, j, COULEUR_VIDE)
if grille[i][j] == "X":
jeu.set_cell_char(i, j, "X", ...)
elif grille[i][j] == "O":
jeu.set_cell_char(i, j, ..., COULEUR_O)
else:
jeu.set_cell_char(i, j, "", "#000000")
Lancez le programme. Vous devez voir une grille vide grise. Rien ne se passe encore au clic, c'est normal.
Étape 3 - Poser un pion
Alterner les joueurs
joueur_actif vaut "X" ou "O". Pour passer au joueur suivant :
Fonction cliquer
Quand le joueur clique sur (i, j) :
- si la partie est terminée (
flag_game_overestTrue), ne rien faire, - si la case est déjà occupée (
grille[i][j]n'est pasNone), ne rien faire, - sinon, placer le pion du joueur actif dans
grille[i][j], - puis alterner le joueur.
def cliquer(i: int, j: int, _modif: str):
global joueur_actif, flag_game_over
if ...:
return
if grille[i][j] is not None:
return
grille[i][j] = ...
if joueur_actif == "X":
joueur_actif = ...
else:
joueur_actif = ...
Testez : les clics doivent alterner X et O. Cliquer deux fois sur la même case ne doit rien faire.
Étape 4 - Détecter la victoire
Un joueur gagne s'il aligne trois pions sur une ligne, une colonne ou une diagonale.
Fonction ligne_gagnante
ligne_gagnante(i1, j1, i2, j2, i3, j3, joueur) renvoie True si les trois cases (i1,j1), (i2,j2), (i3,j3) contiennent toutes joueur.
Testez mentalement : si toute la ligne 0 est à "X", que renvoie ligne_gagnante(0,0, 0,1, 0,2, "X") ?
Fonction a_gagne
Sur une grille 3×3, il y a exactement 8 alignements possibles. Complétez les 8 cas :
def a_gagne(joueur: str) -> bool:
return (
ligne_gagnante(0, 0, 0, 1, 0, 2, joueur) or # ligne 0
ligne_gagnante(1, 0, 1, 1, 1, 2, joueur) or # ligne 1
ligne_gagnante(2, 0, 2, 1, 2, 2, joueur) or # ligne 2
ligne_gagnante(0, 0, 1, 0, 2, 0, joueur) or # colonne 0
... # colonne 1
... # colonne 2
ligne_gagnante(0, 0, 1, 1, 2, 2, joueur) or # diagonale \
ligne_gagnante(0, 2, 1, 1, 2, 0, joueur) # diagonale /
)
Quelles sont les coordonnées de la diagonale / ? Vérifiez sur papier.
Étape 5 - Match nul et fin de partie
Fonction est_pleine
La grille est pleine quand toutes les cases sont occupées. Dès qu'on en trouve une vide, la grille n'est pas pleine.
Gérer la fin de partie dans cliquer
Après avoir posé un pion, il faut tester la victoire avant d'alterner le joueur (car c'est le joueur qui vient de jouer qui a peut-être gagné). Remplacez la version de l'étape 3 par :
def cliquer(i: int, j: int, _modif: str):
global joueur_actif, flag_game_over
if flag_game_over or grille[i][j] is not None:
return
grille[i][j] = joueur_actif
if a_gagne(joueur_actif) or est_pleine():
flag_game_over = True
else:
if joueur_actif == "X":
joueur_actif = "O"
else:
joueur_actif = "X"
Pourquoi teste-t-on a_gagne(joueur_actif) avant d'alterner, et non après ?
Afficher la victoire dans draw
Quand flag_game_over est True, colorier toutes les cellules en COULEUR_VICTOIRE au lieu de COULEUR_VIDE. Les symboles X et O restent affichés par-dessus.
def draw():
for i in range(NB_LIGNES):
for j in range(NB_COLONNES):
if flag_game_over:
couleur_fond = ...
else:
couleur_fond = COULEUR_VIDE
jeu.set_cell_color(i, j, couleur_fond)
if grille[i][j] == "X":
...
elif grille[i][j] == "O":
...
else:
...
Jouez quelques parties. Appuyez sur r pour rejouer.
Pour aller plus loin
- Distinguer victoire et match nul : conservez le résultat final dans une variable (
"X","O", ou"nul") et choisissez une couleur différente pour chaque cas. - Mettre en évidence les trois cases gagnantes : au lieu de colorier toute la grille, colorier uniquement la ligne gagnante en
COULEUR_VICTOIRE. - Généraliser la détection de victoire : au lieu de lister 8 appels à la main, construire une liste
LIGNES_GAGNANTES = [[(0,0),(0,1),(0,2)], ...]contenant les 8 alignements, puis parcourir cette liste dansa_gagne. C'est l'occasion de manipuler des listes de listes autrement.