Démineur
Le démineur est un classique des jeux de logique. Une grille dissimule des mines ; le joueur doit révéler toutes les cases sans mines en s'aidant des chiffres qui indiquent combien de mines se trouvent dans le voisinage immédiat de chaque case. Un clic droit permet de poser un drapeau pour marquer une mine supposée.
Pré-requis
Cette activité fait suite à l'activité sur le voisinage. On réutilise les mêmes notions : grille (i, j), déplacements (di, dj), vérification des bords, voisinage à 8.
Installez la bibliothèque : uv add metagrid
Sur Debian/Ubuntu, des dépendances systèmes sont nécessaires :
Code à compléter
Copiez ce fichier dans votre éditeur et complétez les ... en suivant les étapes ci-dessous.
import metagrid
from metagrid import AbstractEngine
from random import randint
HAUTEUR = 10
LARGEUR = 10
NB_MINES = 10
#? STRUCTURE DE CELLULE
class Cellule:
est_mine: bool
nb_voisins: int
revelee: bool
drapeau: bool
def __init__(self):
self.est_mine = ... # (1)
self.nb_voisins = ... # (1)
self.revelee = ... # (1)
self.drapeau = ... # (1)
#? ETAT DU JEU
grille: list[list[Cellule]]
game_over: bool
game: AbstractEngine
#? INITIALISATION
def init():
global grille, game_over
game_over = False
grille = ... # (2) grille vierge de HAUTEUR × LARGEUR Cellule()
compteur = 0
while compteur < NB_MINES:
i = randint(0, HAUTEUR - 1)
j = randint(0, LARGEUR - 1)
if ...: # (3) la case n'est pas encore une mine
... # (3) la marquer comme mine
compteur += 1
for i in range(HAUTEUR):
for j in range(LARGEUR):
grille[i][j].nb_voisins = 0 # Pour l'instant
#? FONCTIONS UTILITAIRES
def get_mines_voisines(i: int, j: int) -> int:
... # (4)
def reveler_toutes_mines():
... # (5)
def est_gagne() -> bool:
... # (6)
def decouvre(i: int, j: int):
cell = grille[i][j]
cell.revelee = ... # (7) révéler la case
if ...: # (7) propager seulement si aucun voisin miné
for di in range(-1, 2):
for dj in range(-1, 2):
if ... and ...: # (7) exclure le centre ET vérifier les bords
voisin = grille[i + di][j + dj]
if ...: # (7) voisin non révélé ET pas une mine
decouvre(...) # (7) appel récursif
#? CALLBACKS
def click(i: int, j: int, button: str):
global game_over
if ...: # (8) ignorer si partie terminée
return
case = grille[i][j]
if button == "left" and not case.drapeau and not case.revelee:
if case.est_mine:
game_over = True
reveler_toutes_mines()
print("Game over !")
else:
decouvre(i, j)
if est_gagne():
... # (8) signaler la victoire
elif button == "right" and not case.revelee:
case.drapeau = not case.drapeau
def draw():
for i in range(HAUTEUR):
for j in range(LARGEUR):
case = grille[i][j]
if case.drapeau:
game.set_cell_color(i, j, "#555555")
game.set_cell_char(i, j, "F", "#FF0000")
elif ...: # (9) non révélée
game.set_cell_color(i, j, "#555555")
game.set_cell_char(i, j, "", "#000000")
elif ...: # (9) mine révélée
game.set_cell_color(i, j, "#FF4444")
game.set_cell_char(i, j, "X", "#FFFFFF")
elif ...: # (9) case chiffrée
game.set_cell_color(i, j, "#FFFFFF")
game.set_cell_char(i, j, str(case.nb_voisins), "#3418D4")
else:
game.set_cell_color(i, j, "#FFFFFF")
game.set_cell_char(i, j, "", "#000000")
if __name__ == "__main__":
game = metagrid.create(HAUTEUR, LARGEUR, 50, 1)
game.on_init(init)
game.on_click(click)
game.on_draw(draw)
game.start()
Étape 1 - La classe Cellule
Au démineur, chaque case possède plusieurs informations indépendantes. On les regroupe dans une classe.
Initialiser les attributs (1)
Chaque case a 4 attributs :
| Attribut | Type | Signification |
|---|---|---|
est_mine |
bool |
La case cache une mine |
nb_voisins |
int |
Nombre de mines dans les 8 cases adjacentes |
revelee |
bool |
La case a été découverte par le joueur |
drapeau |
bool |
Le joueur a posé un drapeau sur cette case |
Pour commencer, initialisez revelee à True et les trois autres attributs à leurs valeurs par défaut. Cela révèle toute la grille dès le lancement et vous permettra de tester votre code au fur et à mesure sans attendre d'avoir implémenté les clics.
Vous remettrez revelee à False plus tard.
Étape 2 - La grille vierge
Compréhension de liste imbriquée (2)
On veut une grille de HAUTEUR lignes et LARGEUR colonnes, chaque case étant un objet Cellule() distinct. Utilisez une double compréhension de liste. Complétez (2).
Étape 3 - Placer les mines
Boucle de placement (3)
On tire une position aléatoire ; si la case n'est pas encore une mine, on la marque et on avance le compteur. Complétez les deux lignes (3).
Pourquoi une boucle while plutôt qu'une boucle for ?
Premier test
Lancez le programme. Avec revelee = True dans __init__, toutes les cases sont visibles : vérifiez que les mines apparaissent en rouge avec une croix, et les cases sans mine en blanc. Les chiffres affichent tous 0 pour l'instant, c'est normal.
Étape 4 - Compter les mines voisines
Double boucle avec bords (4)
Écrivez entièrement la fonction en vous appuyant sur ce que vous avez vu dans l'activité sur le voisinage. La fonction retourne le nombre de mines dans le voisinage à 8 de (i, j).
Rappel : en Python, un booléen vaut 1 ou 0 en arithmétique.
Brancher dans init
Dans init, remplacez grille[i][j].nb_voisins = 0 par un appel à get_mines_voisines. Relancez : les chiffres doivent maintenant apparaître correctement.
Étape 5 - Révéler toutes les mines
reveler_toutes_mines (5)
En cas de game over, on révèle toutes les mines pour que draw puisse les afficher. Écrivez entièrement la fonction.
Étape 6 - Vérifier la victoire
On gagne quand toutes les cases sans mine ont été révélées.
est_gagne (6)
Écrivez entièrement la fonction. Elle retourne True si toutes les cases sans mine sont révélées, False dès qu'une ne l'est pas.
Deux approches possibles :
- une double boucle avec
return Falsedès qu'on trouve une case sans mine non révélée, puisreturn Trueà la fin ; all()avec une expression génératrice qui parcourt les cases sans mine et vérifie que chacune est révélée.
Étape 7 - Le flood fill
Quand le joueur clique sur une case sans voisin miné, on révèle automatiquement toute la zone vide adjacente. C'est un algorithme récursif appelé flood fill.
Compléter decouvre (7)
Cinq ... sont à compléter dans la fonction :
cell.revelee = ...— que faut-il affecter pour marquer la case comme révélée ?if ...:— à quelle condition propage-t-on aux voisins ? (regardez l'attribut qui indique le nombre de mines voisines)if ... and ...:— même condition qu'à l'étape 4 : exclure le centre et vérifier les bords.if ...:— deux conditions à réunir avecand: le voisin n'est pas encore révélé, et ce n'est pas une mine.decouvre(...)— quels arguments passer pour appeler la fonction sur le voisin(i+di, j+dj)?
Récursion et terminaison
Pourquoi la condition not voisin.revelee est-elle indispensable avant l'appel récursif ?
Étape 8 - Gérer les clics
click (8)
Deux ... sont à compléter :
- La garde en début de fonction : quelle condition doit-on tester pour ignorer les clics quand la partie est terminée ?
- Que faire quand
est_gagne()retourneTrue? (Mettezgame_overàTrueet affichez un message.)
Étape 9 - Afficher la grille
Plusieurs états peuvent coexister ; l'ordre if / elif / elif / else établit la priorité d'affichage.
Conditions de draw (9)
| Priorité | Condition | Fond | Caractère |
|---|---|---|---|
| 1 | Drapeau | gris #555555 |
F rouge — déjà écrit |
| 2 | Non révélée | gris #555555 |
rien |
| 3 | Mine révélée | rouge #FF4444 |
X blanc |
| 4 | Chiffre (nb_voisins > 0) |
blanc #FFFFFF |
chiffre bleu — déjà écrit |
| 5 | Case vide | blanc #FFFFFF |
rien — déjà écrit |
Complétez les trois conditions (9) manquantes.
Une case avec drapeau est-elle révélée ? Pourquoi faut-il tester case.drapeau en premier ?
Étape 10 - Test final
Vérifier le jeu complet
Remettez revelee à False dans __init__ et testez :
- Clic gauche sur une case vide : la zone se propage-t-elle ?
- Clic gauche sur une mine : toutes les mines s'affichent-elles en rouge ?
- Clic droit : le drapeau
Fapparaît-il puis disparaît-il au deuxième clic ? - Révéler toutes les cases sans mine affiche-t-il le message de victoire ?
Pour aller plus loin
- Ajoutez la touche
rpour relancer une partie (game.on_key(touche)avecdef touche(key: str)qui appelleinit()sikey == 'r'). - Affichez un compteur de mines restantes (NB_MINES moins le nombre de drapeaux posés).
- Les couleurs des chiffres varient dans le vrai démineur : 1 = bleu, 2 = vert, 3 = rouge... Utilisez un dictionnaire dans
draw. - Empêchez un game over au premier clic : si la première case cliquée est une mine, relancez
init()jusqu'à ce qu'elle ne le soit plus.