SPE - 2021/2022 TP 3 : Labyrinthes parfaits Informatique Un labyrinthe est dit parfait s’il satisfait la propriété suivante : “Pour tout couple de cases, il existe un unique chemin les reliant.” On modélisera un labyrinthe comme un tableau à double entrée (liste de listes) d’objets appartenant à la classe Case. Chaque instance de Case est définie par cinq attributs booléens : • les attributs N, S, E et W indiquent la présence de murs au nord, sud, est et ouest de la case considérée, • l’attribut etat indique que la case a déjà été visitée. Vous trouverez sur la page web du cours un fichier laby.py contenant un squelette de code que vous devez télécharger et ensuite compléter. 1 2 3 4 5 6 7 class Case: def __init__(self): self.N = True self.W = True self.S = True self.E = True self.etat = False # indique si la case a été visitée 8 9 10 11 12 13 class Labyrinthe: def __init__(self, larg, haut): self.haut = haut self.larg = larg self.tab = [[Case() for j in range(haut)] for i in range(larg)] La classe Labyrinthe est construite de manière à ce que, pour toute instance maze de la classe Labyrinthe, maze.tab[x][y] désigne la case du labyrinthe dont le coin inférieur gauche a pour coordonnées (x, y). Exercice 1 : Tester les commandes suivants : 1 2 new = Labyrinthe(25,30) new.show() 1. Le labyrinthe généré vous semble-t-il parfait ? 2. Quelle est sa forme ? 3. Compléter la définition de la classe Labyrinthe par une fonction clean qui réinitialise l’attribut etat de chaque case du labyrinthe à False. Construction d’un labyrinthe parfait Il existe plusieurs types d’algorithmes générant des labyrinthes parfaits. On s’intéresse ici à un algorithme de construction par exploration exhaustive du labyrinthe. L’algorithme est décrit ci-dessous. • On crée un labyrinthe dont tous les murs sont fermés et dont aucune case n’a été visitée, • On crée une pile (initialement vide) destinée à contenir l’ensemble des cases à traiter par l’algorithme. • On choisit une case au hasard dans le labyrinthe. On modifie alors l’état de cette case pour indiquer qu’elle est visitée et on l’empile dans la pile des cases à traiter. • Tant que la pile est non vide, on répète la suite des instructions ci-dessous. A. Lick 1 Janson de Sailly SPE - 2021/2022 TP 3 : Labyrinthes parfaits Informatique • On visite la case au sommet de la pile (sans la dépiler). • On détermine la liste de ses cases adjacentes non visitées. • Si la case n’a aucune voisine non visitée, on la dépile de la pile des cases à traiter. • Sinon, on en choisit une au hasard, on modifie son état pour indiquer qu’elle est désormais visitée, on “casse” le mur entre les deux cases et on empile la nouvelle case dans la pile des cases à traiter. Exercice 2 : Écrire une fonction creationLabyrinthe qui génère (renvoie) selon l’algorithme précédent une instance de la classe Labyrinthe (i.e. un labyrinthe parfait), dont la taille (largeur et hauteur) sont fournis en argument d’entrée. On pourra utiliser la fonction random.randint(a,b) qui renvoie un entier choisi aléatoirement entre a et b inclus. Vous prendrez des initiatives en introduisant des fonctions auxiliaires (par exemple pour casser un mur ou déterminer la liste des cases adjacentes). Tester votre fonction avec le code ci-dessous. 1 2 maze = creationLabyrinthe(30 ,30) maze.show() Exercice 3 (Si vous êtes en avance) : • Démontrer que l’algorithme précédent génère des labyrinthes parfaits. • Combien de murs internes a-t-on cassé ? Combien de murs internes reste-t-il ? Recherche du chemin solution On décide (arbitrairement) de fixer l’entrée et la sortie du labyrinthe aux coins respectivement en bas à gauche et en haut à droite 1 . On présente ici un algorithme de recherche du chemin solution par backtracking (retour en arrière). Le principe de cet algorithme est simple 2 : on suit un chemin jusqu’à la solution ou une impasse et, dans le deuxième cas, on revient sur ses pas jusqu’à la dernière intersection. Les étapes de cet algorithme de recherche sont similaires à celles de l’algorithme de construction de labyrinthes parfaits précédemment décrit. • On crée une pile (initialement vide) des cases visitées. • On empile les coordonnées de l’entrée du labyrinthe, et on marque la case comme visitée. • Tant que la sortie n’a pas été trouvée, on visite le sommet de la pile (sans le dépiler). • On le marque comme visité. • Si c’est la sortie, on sort de la boucle et on renvoie le contenu de la pile. • Sinon, on teste une par une toutes les cases adjacentes accessibles : si l’une d’entre elle n’a pas encore été visitée, on l’empile dans la pile des cases visitées. Dans le cas contraire, on est au bout d’une impasse ; on dépile donc la dernière case visitée. Cet algorithme d’exploration s’appelle algorithme de parcours en profondeur. Exercice 4 : 1. Écrire une fonction depthFirstSearch qui parcourt en profondeur le labyrinthe selon l’algorithme présenté ci-dessus et qui renvoie le chemin solution (sous forme de liste des coordonnées des cases à visiter). 2. Compléter la définition de la classe Labyrinthe par une fonction solution qui affiche (dans une autre couleur que celle utilisée pour tracer le labyrinthe) le chemin solution calculé par la fonction précédente. 1. Si maze est une instance de Labyrinthe, l’entrée est sur la case maze.tab[0][0] et la sortie sur maze.tab[maze.larg-1][maze.haut-1]. 2. C’est la méthode du fil d’Ariane qui permit à Thésée de sortir du Labyrinthe (créé par Dédale) dans lequel le roi Minos a enfermé le Minautore. A. Lick 2 Janson de Sailly