Consignes

Vous devrez utiliser l'interface TPLab pour envoyer vos TP aux encadrants de TP.

La note ne valide pas seulement le résultat de votre programme, mais également son style :

Vérifiez ces points avant de demander à votre intervenant de valider votre code.

Liens utiles

Pour le TP, vous aurez besoin de :

Objectifs du TP

  1. Découvrir :
    • l'interface graphique
    • les fonctions recursives
  2. se familiariser avec :
    • les structures de données complexes (listes de listes de dictionnaires)

1. Préliminaires : fonctions récursives

Pour cette partie, n'écrivez rien dans le fichier demineur.py, mais testez directement les fonctions dans l'interprète ou dans un autre fichier.

Une fonction est récursive si elle s'appelle elle même. Le risque avec ce genre de définition, c'est qu'elles risquent de ne jamais s'arrêter. C'est un peu comme une boucle while : le programmeur doit se débrouiller pour mettre une condition d'arrêt.

Écrivez la définition suivante dans l'interprète et testez la :

def affiche(n):
    if n>0:
        print(n)
        affiche(n-1)  # appel récursif

S'agit il d'une fonction, ou d'une procédure ?

Que se passe t'il si on donne un argument négatif à affiche ?

Testez la définition suivante :

def fonction_mystere(n):
    if n <= 1:
        return 1
    else:
        resultat = n * fonction_mystere(n-1)  # appel récursif
        return resultat

S'agit il d'une fonction, ou d'une procédure ?

Que calcule cette définition ?

2. Se familiariser avec la structure du plateau

2.1. Initialisation

Chaque case du démineur est représentée par un dictionnaire avec deux cases :

Par exemple :

>>> c1 = { "mine" : True , "etat" : -2 }
>>> c2 = { "mine" : False , "etat" : -1 }

Pour simplifier la vie, des constantes sont définies au début du fichier :

Le déclarations précédentes seront donc écrites comme :

>>> c1 = { "mine" : True , "etat" : PERDU }
>>> c2 = { "mine" : False , "etat" : INCONNU }

Le plateau est un tableau de tableaux de cases. (On parle de tableau à deux dimensions.) Chaque élément du tableau représente un colonne, qui est elle même un tableau de cases :

>>> c0 = { "mine" : False , "etat" : INCONNU }
>>> plateau = [
  [ {"mine": False, "etat": INCONNU}, {"mine": False, "etat" : 1},      {"mine": False, "etat": INCONNU} ],
  [ {"mine": False, "etat": INCONNU}, {"mine": True, "etat" : INCONNU}, {"mine": False, "etat": INCONNU} ],
  [ {"mine": False, "etat": INCONNU}, {"mine": True, "etat" : INCONNU}, {"mine": False, "etat": INCONNU} ]
]

est un plateau de 9 cases (3 colonnes et 3 lignes), avec 2 mines (milieu de la deuxième colonne et milieu de la troisième colonne) et une case découverte (milieu de la première colonne).

On accède donc à la case (i,j) d'un tels plateau de la manière suivante :

>>> plateau[1][2]
{'etat': -1, 'mine': False}

Il s'agit de la deuxième colonne, troisième ligne...

Si l'on dispose d'un plateau (liste de listes de dictionnaires), comment peut-on obtenir sa largeur (en nombre de cases) et sa hauteur (en nombre de cases) ?

Servez-vous de ceci pour compléter la fonction "dans_plateau" qui renvoie "True" si "(x,y)" est bien une case du plateau, et "Faux" si "(x,y)" n'est pas dans le tableau,

Une fois que vous avez écrit cette fonction, vous pouvez commencer à jouer au démineur. Le seul problème, c'est qu'il n'y a pas de mine sur le plateau !

Le fichier demineur.py contient une fonction "genere_plateau(largeur,hauteur,probabilite_mine=0)" qui crée un plateau de la taille donnée. L'argument probabilite_mine n'est pas utilisé.

Modifiez cette fonction pour utiliser l'argument "probabilite_mine" qui est un flottant compris entre 0 et 1. Ce nombre donne la probabilité que chaque case contienne une mine. Par exemple, si on appelle la fonction avec les arguments genere_plateau(10,20,0.25), le résultat aura 10 colonnes et 20 lignes, et chaque case aura une chance sur 4 de contenir une mine. (En moyenne, il y aura donc 50 mines sur un tels plateau.)

Remarque : la fonction "random.random()" génère un nombre flottant aléatoire entre 0 et 1.

Pour vérifier que votre fonction fonctionne correctement, vous pouvez lancer le programme et appuyer sur la touche "m" qui montre les positions des mines pendant un instant.

2.2. Compter les mines voisines

Lorsque l'on clique sur une case vide (qui ne contient pas de mine), il faut compter le nombre de mines voisines de la case pour pouvoir l'afficher. Il y a huit cases voisines de la case "(x,y)".

  1. écrivez la fonction "cases_voisines" qui renvoie la liste des coordonnées des voisins de "(x,y)",

  2. écrivez la fonction "compte_mines_voisines" qui compte le nombre de mines dans les cases voisines de "(x,y)".

Testez bien votre fonction, notamment sur les bords du plateau.

3. Calcul de la composante connexe

La partie complexe du démineur est la propagation des cases sûres : si une case sans mine n'a aucun voisin avec une mine, on peut automatiquement découvrir les cases voisines, etc. C'est ceci qui permet d'obtenir la configuration de droite en un seul clic sur le coin supérieur gauche :

On parle de composante connexe : on découvre toutes les cases qui ne contiennent rien (ni de mine, ni un entier strictement supérieur à 0), en s'arrêtant aux cases qui contiennent un entier strictement supérieur à 0.

Le plus simple pour programmer cette opération est d'utiliser une fonction récursive. Le calcul de la composante connexe au démineur se fait de la manière suivante : pour une case donnée (sans mine),

Cette fonction s'arrête forcement au bout d'un moment car on ne fait pas d'appel récursif si la case de départ était déjà découverte. Comme toutes les cases qu'on explore sont découvertes, il ne peut pas y avoir de boucle infinie...

En suivant les instructions si dessus, programmez la procédure composante_connexe.

Attention : il s'agit d'une procédure qui modifie son argument plateau au fur et à mesure de la propagation des cases vides. Vous pouvez cependant employer l'instruction "return" (sans argument) pour arrêter la fonction.

Pour tester si votre fonction marche, il suffit de jouer au démineur !

4. Interface graphique

4.1. Drapeau

Dans la version originale du démineur, on peut également poser un drapeau ou un point d'interrogation sur une case non découverte. Le drapeau signale qu'on est sûr qu'une mine est présente alors que le point d'interrogation signale qu'il est possible que la case contienne une mine. Pour poser un drapeau, il suffit de cliquer avec le bouton droit de la souris sur une case. Pour transformer le drapeau en point d'interrogation, il suffit d'un deuxième clic droit. Un troisième clic droit remettra la case à l'état inconnu.

La procédure d'affichage gère les états INCONNU / DRAPEAU / QUESTION. Il faut donc simplement rajouter un évènement « clic droit » avec la commande associée...

La gestion du clic pour découvrir une case se fait grâce aux lignes

def __action_clic(clic):
    """Fonction appelée quand on fait un clic sur la fenêtre."""
    ...

grille.bind("<Button-1>", __action_clic)

qui déclarent une procédure et la lie à l'évènement « clic ».

Ajoutez un évènement clic droit et la commande correspondante pour gérer les drapeaux / points d'interrogation.

Remarque : les boutons de la souris sont numérotés :

4.2. Fin du jeu

Pour le moment, lorsqu'on découvre une case qui contient une mine, celle-ci s'affiche en rouge et la partie continue.

Cherchez la partie du programme qui détecte si le joueur vient de découvrir une case minée, et modifiez le code pour que la partie s'arrête à ce moment.

Écrivez également une fonction qui permet de tester si le joueur a mis les drapeaux exactement sur les cases minées. Si c'est le cas, il faut aussi arrêter la partie car le joueur a gagné.

BONUS

Modifiez la fonction affiche_case pour qu'elle utilise l'image (l'image s'appelle mauvais_drapeau_img dans le code Python fourni) lorsqu'elle affiche une mine injustement repérée.

4.3. bouton du milieu

Dans le jeu original, un clic du milieu sur une case découverte était équivalent à des clics sur toutes les cases voisines. Ceci ne fonctionnait que lorsque le nombre de mines voisines et le nombre de drapeaux voisins coïncidaient. Dans ce cas, le joueur pense avoir repérer toutes les mines voisines et veut donc découvrir les cases voisines restantes.

Ajoutez la gestion du bouton du milieu.

4.4. Améliorations (Bonus)

Apportez des améliorations :