TP NSI : Implémentation du Démineur et Propagation Récursive

L'objectif de ce TP est de réaliser un jeu de Démineur fonctionnel sous Pyxel en adoptant une architecture modulaire. Vous allez concevoir le moteur logique du jeu dans un premier fichier en pur Python, le valider via des tests unitaires (assert), puis l'importer dans un second fichier gérant l'interface graphique.

Notion de modularité : Séparer le moteur logique (calculs, récursivité) de l'interface graphique (Pyxel) permet d'isoler le code, de le tester indépendamment et de réutiliser le module logique dans un autre contexte si besoin.
💻 Partie 1 : Moteur logique dans moteur_demineur.py

Créez un fichier nommé moteur_demineur.py. Tout le travail d'écriture algorithmique de cette partie se fera exclusivement dans ce fichier.

1. Modélisation de la grille

Pour représenter le plateau de jeu de taille taille × taille, nous utilisons une liste de listes de dictionnaires. Chaque case du plateau est modélisée par un dictionnaire possédant exactement 4 clés :

Fonction 1 : creer_grille_vide(taille)

Écrivez la fonction creer_grille_vide(taille: int) -> list qui prend en paramètre la dimension du plateau et renvoie une grille 2D initialisée. Par défaut, aucune case ne contient de mine, aucune n'est découverte, aucun drapeau n'est posé, et le nombre de voisins vaut 0.

Fonction 2 : placer_mines(grille, nb_mines)

Écrivez la fonction placer_mines(grille: list, nb_mines: int) -> None. Elle doit modifier la grille reçue en argument en plaçant aléatoirement nb_mines distinctes (attention à ne pas placer deux mines sur la même case !).

Fonction 3 : calculer_tous_les_voisins(grille)

Écrivez la fonction calculer_tous_les_voisins(grille: list) -> None. Cette fonction parcourt l'ensemble des cases de la grille. Pour chaque case qui ne contient pas de mine, elle compte combien de mines se trouvent dans les 8 cases adjacentes (horizontalement, verticalement et en diagonale) et met à jour la valeur associée à la clé "voisins".

Attention aux bords ! Lors de l'inspection des 8 voisins autour d'une case (x, y), assurez-vous de ne pas générer d'erreur d'indice (IndexError) en sortant des limites de la grille.

2. Le cœur du TP : Dévoilement récursif

Fonction 4 : devoiler(grille, x, y)

Lorsque l'utilisateur clique sur une case (x, y) sans mine, la case est dévoilée. Si cette case n'a aucune mine dans son voisinage (voisins == 0), le jeu propage automatiquement le dévoilement aux cases environnantes.

Voici l'enchaînement exact à coder dans la fonction devoiler(grille: list, x: int, y: int) -> None :

Algorithme de la fonction récursive devoiler(grille, x, y)
  1. Conditions d'arrêt (Cas de base) : Ne faites rien et interrompez immédiatement la fonction (return) si :
    • Les coordonnées (x, y) sont hors de la grille.
    • La case est déjà découverte.
    • La case porte un drapeau.
    • La case contient une mine.
  2. Action : Dans tous les autres cas, marquez la case (x, y) courante comme découverte (case["decouverte"] = True).
  3. Propagation aux voisins (Appels récursifs) :
    • Si la case courante comporte 0 mine voisine : appelez récursivement devoiler sur chacun de ses 8 voisins (en évitant la case elle-même).
    • Si la case comporte 1 mine voisine ou plus : ne relancez aucun appel récursif. La fonction s'arrête naturellement ici (la case reste découverte, affichant son chiffre, mais la vague de propagation s'arrête).
ATTENTION :
Si vous n'indiquez pas que la case est découverte avant de relancer la fonction sur ses voisins, les voisins rappelleront à leur tour la case de départ, provoquant une boucle récursive infinie (RecursionError: maximum recursion depth exceeded).
🧪 Partie 2 : Validation par Tests Unitaires

Ajoutez le bloc de tests unitaires suivant à la fin de votre fichier moteur_demineur.py et exécutez le script. Vos 4 fonctions doivent valider l'ensemble des assertions sans lever d'erreur.

def tester_moteur():
    """Suite de tests unitaires pour valider les fonctions du moteur."""
    print("Lancement des tests unitaires...")

    # --- TEST 1 : Initialisation de la grille ---
    g_vide = creer_grille_vide(5)
    assert len(g_vide) == 5 and len(g_vide[0]) == 5, "Test 1 Échec : Dimensions incorrectes"
    assert g_vide[0][0]["mine"] is False, "Test 1 Échec : Case mal initialisée"

    # --- TEST 2 : Placement des mines ---
    placer_mines(g_vide, 4)
    nb_mines = sum(1 for x in range(5) for y in range(5) if g_vide[x][y]["mine"])
    assert nb_mines == 4, "Test 2 Échec : Mauvais nombre de mines placées"

    # --- TEST 3 : Calcul des voisins ---
    g_test = creer_grille_vide(3)
    g_test[0][0]["mine"] = True
    calculer_tous_les_voisins(g_test)
    
    assert g_test[0][1]["voisins"] == 1, "Test 3 Échec : Voisinage (0,1)"
    assert g_test[1][1]["voisins"] == 1, "Test 3 Échec : Voisinage (1,1)"
    assert g_test[2][2]["voisins"] == 0, "Test 3 Échec : Voisinage (2,2)"

    # --- TEST 4 : Récursivité devoiler() ---
    g_rec = creer_grille_vide(4)
    g_rec[3][3]["mine"] = True
    calculer_tous_les_voisins(g_rec)

    devoiler(g_rec, 0, 0)

    assert g_rec[0][0]["decouverte"] is True, "Test 4 Échec : Origine non dévoilée"
    assert g_rec[2][2]["decouverte"] is True, "Test 4 Échec : Frontière (2,2) non dévoilée"
    assert g_rec[3][3]["decouverte"] is False, "Test 4 Échec : La mine en (3,3) a été dévoilée !"

    devoiler(g_rec, -1, 0)
    devoiler(g_rec, 10, 10)

    print("TOUS LES TESTS SONT PASSÉS AVEC SUCCÈS !")

if __name__ == "__main__":
    tester_moteur()
Résultat attendu dans la console :
Lancement des tests unitaires...
TOUS LES TESTS SONT PASSÉS AVEC SUCCÈS !
🎮 Partie 3 : Intégration Graphique Pyxel

Une fois la totalité des tests validée, téléchargez le fichier d'interface graphique ci-dessous et enregistrez-le dans le même dossier que votre fichier moteur_demineur.py :

Télécharger demineur.py

Remarque : Ce fichier importe les fonctions déjà programmées grâce à la ligne :

from moteur_demineur import (
    creer_grille_vide,
    placer_mines,
    calculer_tous_les_voisins,
    devoiler
)

Lancez l'exécution de demineur.py pour jouer au Démineur !