version 20240309103128
# encoding:utf-8
# _ __ __ _ _______ _ _ _
# /_\ | \/ | /_\ |_ / __| (_)_ _ _ __ _ _| |_| |_ ___ _ _
# / _ \ | |\/| |/ _ \ / /| _| | | ' \ | '_ \ || | _| ' \/ _ \ ' \
# /_/ \_\ |_| |_/_/ \_\/___|___| |_|_||_| | .__/\_, |\__|_||_\___/_||_|
# |_| |__/
# sae@butinfo:~$
[NDLR : ce sujet est une réécriture de celui imaginé par É. Coutant et étendu par O. Nocent, pendant le premier confinement, puis transformé en SAÉ par É. Coutant en 2022]
Enseignants référents:
- Frédéric Blanchard email: frederic.blanchard@univ-reims.fr
- Étienne Coutant email: etienne.coutant@univ-reims.fr
- Nicolas Passat email: nicolas.passat@univ-reims.fr
Dans cette Situation d’Apprentissage et d’Évaluation, vous allez devoir implémenter des algorithmes de génération et de résolution de labyrinthes.
1 Préambule
Dates importantes
- Date limite de rendu du projet : Dimanche 17 mars 2024
- Évaluation : Semaine du 18 mars 2024 (sauf mention contraire de votre enseignant référent)
Notes sur l’organisation et l’évaluation
- Le projet est à réaliser en binômes. Vous devez versionner votre travail avec
git
. - Le dépôt distant sera sur l’instance
Gitlab
du département informatique de l’IUT de Reims : https://iut-info.univ-reims.Fr/gitlab/. - Il faut effectuer des
commits
réguliers (à chaque fois que vous travaillez sur votre projet). - Vous devez déposer le
notebook
avant le dimanche 17 mars, 23h59, dans l’espace moodle dédié (un seul dépôt pour les deux membres du binôme ; indiquez en commentaire, lors du dépôt les noms des deux auteurs) - Lors de votre dernière séance de TP de “graphes”, vous serez évalué avec un QCM, avec votre enseignant référent.
- Un petit entretien oral avec votre enseignant référent permettra de compléter la note.
La note est donc composée de :
- celle obtenue au QCM
- celle obtenue à l’entretien (qui prendra en compte votre rendu)
L’essentiel du travail est donc à réaliser dans un notebooks
jupyter
. Vous pouvez utiliser l’instance jupyterhub
du département : https://iut-info.univ-reims.fr/notebook/.
Si vous avez des questions, vous pouvez joindre votre enseignant référent par mail. Un canal discord éphémère (qui sera supprimé à la fin de la SAÉ) est aussi disponible sur le serveur discord
du département : https://discord.gg/BTfTwAxR.
Conseils et recommandations
- Votre code devra respecter quelques règles d’hygiène élémentaire :
- contenir des commentaires (a minima un
docstring
informatif pour chaque méthode), - être lisible (en essayant d’utiliser des noms de variables explicites).
- contenir des commentaires (a minima un
- Respectez la progression du sujet (procédez dans l’ordre des questions qui vous ont été posées).
- Chaque membre du binôme doit être capable de répondre seul à des questions portant sur ce qui a été réalisé par le binôme.
2 Modélisation d’un labyrinthe
Un labyrinthe est un graphe non-orienté (ou orienté-symétrique) dont la représentation planaire prend la forme d’une grille. Chaque cellule du labyrinthe est un sommet du graphe, et l’absence de mur entre deux cellules contiguës constitue une arête.
On parle de labyrinthe parfait lorsque le graphe est un arbre (donc connexe et sans cycle). Par la suite, sauf mention du contraire, un labyrinthe sera considéré comme parfait.
.
La modélisation retenue dans ce projet consiste à représenter un sommet par un couple (indice de ligne, indice de colonne) permettant de localiser la cellule, et d’utiliser un dictionnaire pour lister les voisines d’une cellule (et donc les arêtes).
Dans l’exemple précédent, les sommets du graphe sont les cellules :
(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3)
Les arêtes sont :
{(0, 0), (1, 0)}, {(1, 0), (2, 0)}, {(2, 0), (3, 0)}, {(2, 0), (2, 1)}, ...
Les voisinages (les successeurs, en quelque sorte) correspondants :
{
(0, 0): {(1, 0)},
(0, 1): {(0, 2), (1, 1)},
(0, 2): {(0, 1), (0, 3)},
(0, 3): {(0, 2), (1, 3)},
(1, 0): {(2, 0), (0, 0)},
(1, 1): {(0, 1), (1, 2)},
(1, 2): {(1, 1), (2, 2)},
(1, 3): {(2, 3), (0, 3)},
(2, 0): {(1, 0), (2, 1), (3, 0)},
(2, 1): {(2, 0), (2, 2)},
(2, 2): {(1, 2), (2, 1)},
(2, 3): {(3, 3), (1, 3)},
(3, 0): {(3, 1), (2, 0)},
(3, 1): {(3, 2), (3, 0)},
(3, 2): {(3, 1)},
(3, 3): {(2, 3)}
}
3 Implémentation
Nous allons définir la classe Maze
à l’aide des attributs :
height
, le nombre de lignes (int
) de la grille du labyrinthe (autrement dit, la hauteur, en nombre de cellules),width
, le nombre de colonnes (int
) de la grille du labyrinthe (autrement dit, la hauteur, en nombre de cellules),neighbors
: un dictionnaire (dict
) qui associe à chaque cellule, unset
contenant ses voisines (c’est-à-dire les cellules qu’on peut atteindre en un déplacement1, sans être bloqué par un mur).
Voici donc la définition sommaire de la classe Maze
, pour laquelle nous vous fournissons, un constructeur par défaut, une méthode d’affichage (en ASCII), et une méthode qui résume les infos du labyrinthe :
class Maze:
"""
Classe Labyrinthe
Représentation sous forme de graphe non-orienté
dont chaque sommet est une cellule (un tuple (l,c))
et dont la structure est représentée par un dictionnaire
- clés : sommets
- valeurs : ensemble des sommets voisins accessibles
"""
def __init__(self, height, width):
"""
Constructeur d'un labyrinthe de height cellules de haut
et de width cellules de large
Les voisinages sont initialisés à des ensembles vides
Remarque : dans le labyrinthe créé, chaque cellule est complètement emmurée
"""
self.height = height
self.width = width
self.neighbors = {(i,j): set() for i in range(height) for j in range (width)}
def info(self):
"""
**NE PAS MODIFIER CETTE MÉTHODE**
Affichage des attributs d'un objet 'Maze' (fonction utile pour deboguer)
Retour:
chaîne (string): description textuelle des attributs de l'objet
"""
= "**Informations sur le labyrinthe**\n"
txt += f"- Dimensions de la grille : {self.height} x {self.width}\n"
txt += "- Voisinages :\n"
txt += str(self.neighbors)+"\n"
txt = True
valid for c1 in {(i, j) for i in range(self.height) for j in range(self.width)}:
for c2 in self.neighbors[c1]:
if c1 not in self.neighbors[c2]:
= False
valid break
else:
continue
break
+= "- Structure cohérente\n" if valid else f"- Structure incohérente : {c1} X {c2}\n"
txt return txt
def __str__(self):
"""
**NE PAS MODIFIER CETTE MÉTHODE**
Représentation textuelle d'un objet Maze (en utilisant des caractères ascii)
Retour:
chaîne (str) : chaîne de caractères représentant le labyrinthe
"""
= ""
txt # Première ligne
+= "┏"
txt for j in range(self.width-1):
+= "━━━┳"
txt += "━━━┓\n"
txt += "┃"
txt for j in range(self.width-1):
+= " ┃" if (0,j+1) not in self.neighbors[(0,j)] else " "
txt += " ┃\n"
txt # Lignes normales
for i in range(self.height-1):
+= "┣"
txt for j in range(self.width-1):
+= "━━━╋" if (i+1,j) not in self.neighbors[(i,j)] else " ╋"
txt += "━━━┫\n" if (i+1,self.width-1) not in self.neighbors[(i,self.width-1)] else " ┫\n"
txt += "┃"
txt for j in range(self.width):
+= " ┃" if (i+1,j+1) not in self.neighbors[(i+1,j)] else " "
txt += "\n"
txt # Bas du tableau
+= "┗"
txt for i in range(self.width-1):
+= "━━━┻"
txt += "━━━┛\n"
txt
return txt
Exemples d’utilisation :
= Maze(4, 4)
laby print(laby.info())
**Informations sur le labyrinthe**
- Dimensions de la grille : 4 x 4
- Voisinages :
{(0, 0): set(), (0, 1): set(), (0, 2): set(), (0, 3): set(), (1, 0): set(), (1, 1): set(), (1, 2): set(), (1, 3): set(), (2, 0): set(), (2, 1): set(), (2, 2): set(), (2, 3): set(), (3, 0): set(), (3, 1): set(), (3, 2): set(), (3, 3): set()}
- Structure cohérente
print(laby)
┏━━━┳━━━┳━━━┳━━━┓
┃ ┃ ┃ ┃ ┃
┣━━━╋━━━╋━━━╋━━━┫
┃ ┃ ┃ ┃ ┃
┣━━━╋━━━╋━━━╋━━━┫
┃ ┃ ┃ ┃ ┃
┣━━━╋━━━╋━━━╋━━━┫
┃ ┃ ┃ ┃ ┃
┗━━━┻━━━┻━━━┻━━━┛
Cassons quelques murs en redéfinissant manuellement les voisinages des cellules concernées :
= {
laby.neighbors 0, 0): {(1, 0)},
(0, 1): {(0, 2), (1, 1)},
(0, 2): {(0, 1), (0, 3)},
(0, 3): {(0, 2), (1, 3)},
(1, 0): {(2, 0), (0, 0)},
(1, 1): {(0, 1), (1, 2)},
(1, 2): {(1, 1), (2, 2)},
(1, 3): {(2, 3), (0, 3)},
(2, 0): {(1, 0), (2, 1), (3, 0)},
(2, 1): {(2, 0), (2, 2)},
(2, 2): {(1, 2), (2, 1)},
(2, 3): {(3, 3), (1, 3)},
(3, 0): {(3, 1), (2, 0)},
(3, 1): {(3, 2), (3, 0)},
(3, 2): {(3, 1)},
(3, 3): {(2, 3)}
(
}
print(laby)
┏━━━┳━━━┳━━━┳━━━┓
┃ ┃ ┃
┣ ╋ ╋━━━╋ ┫
┃ ┃ ┃ ┃
┣ ╋━━━╋ ╋ ┫
┃ ┃ ┃
┣ ╋━━━╋━━━╋ ┫
┃ ┃ ┃
┗━━━┻━━━┻━━━┻━━━┛
Ajoutons maintenant un murs entre la cellule (1,3) et la cellule (2,3) que nous venons de retirer (autrement dit : supprimons une arête que nous avons ajoutée juste avant) :
1,3)].remove((2,3))
laby.neighbors[(2,3)].remove((1,3))
laby.neighbors[(print(laby)
┏━━━┳━━━┳━━━┳━━━┓
┃ ┃ ┃
┣ ╋ ╋━━━╋ ┫
┃ ┃ ┃ ┃
┣ ╋━━━╋ ╋━━━┫
┃ ┃ ┃
┣ ╋━━━╋━━━╋ ┫
┃ ┃ ┃
┗━━━┻━━━┻━━━┻━━━┛
et cassons-le :
1, 3)].add((2, 3))
laby.neighbors[(2, 3)].add((1, 3))
laby.neighbors[(print(laby)
┏━━━┳━━━┳━━━┳━━━┓
┃ ┃ ┃
┣ ╋ ╋━━━╋ ┫
┃ ┃ ┃ ┃
┣ ╋━━━╋ ╋ ┫
┃ ┃ ┃
┣ ╋━━━╋━━━╋ ┫
┃ ┃ ┃
┗━━━┻━━━┻━━━┻━━━┛
Comme vous pouvez le constater dans l’exemple qui précède :
- ajouter un mur entre une cellule c1 et une cellule c2 revient à diminuer deux voisinages, d’une cellule chacun ; il faut retirer c1 du voisinage de c2 et retirer c2 du voisinage de c1 ;
- casser un mur entre une cellule c1 et une cellule c2 revient à augmenter deux voisinages ; il faut ajouter c1 au voisinage de c2 et ajouter c2 au voisinage de c1.
La méthode info()
fournie teste la cohérence des voisinages en vérifiant que, dès lors qu’une cellule c1 est dans le voisinage d’une cellule c2, alors c2 est aussi dans le voisinage de c1.
Dans l’exemple qui suit, un mur est ajouté entre (1,3) et (2,3). Si on a bien retiré (2,3) des voisins de (1,3) on a oublié de retirer (1,3) des voisins de (2,3).
On constate que le labyrinthe est visiblement bon, toutefois, la méthode info()
détecte l’incohérence entre les cellules concernées.
1, 3)].remove((2, 3))
laby.neighbors[(print(laby)
print(laby.info())
┏━━━┳━━━┳━━━┳━━━┓
┃ ┃ ┃
┣ ╋ ╋━━━╋ ┫
┃ ┃ ┃ ┃
┣ ╋━━━╋ ╋━━━┫
┃ ┃ ┃
┣ ╋━━━╋━━━╋ ┫
┃ ┃ ┃
┗━━━┻━━━┻━━━┻━━━┛
**Informations sur le labyrinthe**
- Dimensions de la grille : 4 x 4
- Voisinages :
{(0, 0): {(1, 0)}, (0, 1): {(1, 1), (0, 2)}, (0, 2): {(0, 1), (0, 3)}, (0, 3): {(0, 2), (1, 3)}, (1, 0): {(2, 0), (0, 0)}, (1, 1): {(0, 1), (1, 2)}, (1, 2): {(1, 1), (2, 2)}, (1, 3): {(0, 3)}, (2, 0): {(1, 0), (2, 1), (3, 0)}, (2, 1): {(2, 0), (2, 2)}, (2, 2): {(1, 2), (2, 1)}, (2, 3): {(3, 3), (1, 3)}, (3, 0): {(3, 1), (2, 0)}, (3, 1): {(3, 2), (3, 0)}, (3, 2): {(3, 1)}, (3, 3): {(2, 3)}}
- Structure incohérente : (2, 3) X (1, 3)
Corrigeons ça :
2, 3)].remove((1,3)) laby.neighbors[(
Testons maintenant s’il y a un mur entre deux cellules :
= (1, 3)
c1 = (2, 3)
c2 if c1 in laby.neighbors[c2] and c2 in laby.neighbors[c1]:
print(f"Il n'y a pas de mur entre {c1} et {c2} car elles sont mutuellement voisines")
elif c1 not in laby.neighbors[c2] and c2 not in laby.neighbors[c1]:
print(f"Il y a un mur entre {c1} et {c2} car {c1} n'est pas dans le voisinage de {c2} et {c2} n'est pas dans le voisinage de {c1}")
else:
print(f"Il y a une incohérence de réciprocité des voisinages de {c1} et {c2}")
Il y a un mur entre (1, 3) et (2, 3) car (1, 3) n'est pas dans le voisinage de (2, 3) et (2, 3) n'est pas dans le voisinage de (1, 3)
Le même code permet de tester si on peut accéder à une cellule depuis l’autre et vice-versa :
= (1, 3)
c1 = (2, 3)
c2 if c1 in laby.neighbors[c2] and c2 in laby.neighbors[c1]:
print(f"{c1} est accessible depuis {c2} et vice-versa")
elif c1 not in laby.neighbors[c2] and c2 not in laby.neighbors[c1]:
print(f"{c1} n'est pas accessible depuis {c2} et vice-versa")
else:
print(f"Il y a une incohérence de réciprocité des voisinages de {c1} et {c2}")
(1, 3) n'est pas accessible depuis (2, 3) et vice-versa
Parcourons maintenant la grille du labyrinthe pour lister l’ensemble des cellules :
= []
L for i in range(laby.height):
for j in range(laby.width):
L.append((i,j))print(f"Liste des cellules : \n{L}")
Liste des cellules :
[(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3)]
4 Manipulation de labyrinthes
Nous allons avoir besoin de méthodes d’instance essentielles, pour construire et résoudre ces problèmes de labyrinthes.
Construisons d’abord, à titre d’exemple, la méthode add_wall(c1, c2)
qui ajoute un mur entre entre la cellule c1
et la cellule c2
.
Ajouter un mur entre deux cellules revient à couper la possibilité de se déplacer de l’une à l’autre et inversement. Il s’agit donc de retirer c1
des voisines de c2
, et de retirer c2
des voisines de c1
.
Ce qui donne :
def add_wall(c1, c2):
self.neighbors[c1].remove(c2)
self.neighbors[c2].remove(c1)
On aurait aussi pu vérifier que les cellules passées en paramètres ont des coordonnées cohérentes avec la grille, et que les cellules sont bien voisines l’une de l’autre :
def add_wall(self, c1, c2):
# Facultatif : on teste si les sommets sont bien dans le labyrinthe
assert 0 <= c1[0] < self.height and \
0 <= c1[1] < self.width and \
0 <= c2[0] < self.height and \
0 <= c2[1] < self.width, \
f"Erreur lors de l'ajout d'un mur entre {c1} et {c2} : les coordonnées de sont pas compatibles avec les dimensions du labyrinthe"
# Ajout du mur
if c2 in self.neighbors[c1]: # Si c2 est dans les voisines de c1
self.neighbors[c1].remove(c2) # on le retire
if c1 in self.neighbors[c2]: # Si c3 est dans les voisines de c2
self.neighbors[c2].remove(c1) # on le retire
Exemple d’utilisation :
= Maze(5, 5)
laby print(laby)
┏━━━┳━━━┳━━━┳━━━┳━━━┓
┃ ┃ ┃ ┃ ┃ ┃
┣━━━╋━━━╋━━━╋━━━╋━━━┫
┃ ┃ ┃ ┃ ┃ ┃
┣━━━╋━━━╋━━━╋━━━╋━━━┫
┃ ┃ ┃ ┃ ┃ ┃
┣━━━╋━━━╋━━━╋━━━╋━━━┫
┃ ┃ ┃ ┃ ┃ ┃
┣━━━╋━━━╋━━━╋━━━╋━━━┫
┃ ┃ ┃ ┃ ┃ ┃
┗━━━┻━━━┻━━━┻━━━┻━━━┛
= Maze(5, 5)
laby
laby.empty()print(laby)
┏━━━┳━━━┳━━━┳━━━┳━━━┓
┃ ┃
┣ ╋ ╋ ╋ ╋ ┫
┃ ┃
┣ ╋ ╋ ╋ ╋ ┫
┃ ┃
┣ ╋ ╋ ╋ ╋ ┫
┃ ┃
┣ ╋ ╋ ╋ ╋ ┫
┃ ┃
┗━━━┻━━━┻━━━┻━━━┻━━━┛
0,0), (0,1))
laby.add_wall((print(laby)
┏━━━┳━━━┳━━━┳━━━┳━━━┓
┃ ┃ ┃
┣ ╋ ╋ ╋ ╋ ┫
┃ ┃
┣ ╋ ╋ ╋ ╋ ┫
┃ ┃
┣ ╋ ╋ ╋ ╋ ┫
┃ ┃
┣ ╋ ╋ ╋ ╋ ┫
┃ ┃
┗━━━┻━━━┻━━━┻━━━┻━━━┛
5 Génération
Nous allons maintenant nous intéresser aux algorithmes permettant de générer des labyrinthes parfaits.
Nous allons commencer par implémenter deux classiques assez simples, l’un reposant sur les arbres binaires et le second, appelé sidewinder. Nous verrons aussi deux algorithmes un peu plus avancés, qu’on retrouve notamment dans l’article wikipedia consacré à la modélisation des labyrinthes :
- l’algorithme de génération par fusion de chemins (qui peut-être vu comme une forme de l’algorithme de Kruskal, qui permet de déterminer un arbre couvrant de poids minimal dans un graphe non-orienté valué)
- l’algorithme de génération par exploration exhaustive (qui utilise un parcours de graphe, en profondeur ou en largeur)
On terminera par l’algorithme de Wilson.
5.1 Arbre binaire
L’algorithme de génération par arbre binaire consiste à générer… un arbre binaire comme support du labyrinthe.
La procédure est assez simple :
Initialisation : un labyrinthe plein (contenant tous les murs possibles)
Pour chaque cellule du labyrinthe :
- Supprimer aléatoirement le mur EST ou le mur SUD (s’il n’en possède qu’un, supprimer ce mur ; s’il n’en possède aucun des deux, ne rien faire)
Remarque : on utilisera dans la suite de ce document les points cardinaux (NORD, SUD, EST, OUEST) pour l’orientation sur la grille.
5.2 Sidewinder
L’algorithme de génération sidewinder ressemble beaucoup au précédent. L’idée est de procéder ligne par ligne, d’OUEST en EST, en choisissant aléatoirement de casser le mur EST d’une cellule. Pour chaque séquence de cellules voisines (connectées) créée sur la ligne, on casse un mur SUD au hasard d’une de ces cellules (une séquence peut être constituée d’une seule cellule).
On pourrait formaliser le pseudo code de la façon suivante :
Initialisation : création d’un labyrinthe plein
Pour i allant de 0 à hauteur-2 :
- Initialiser une variable séquence comme liste vide
- Pour j allant de 0 à largeur-2 :
- Ajouter la cellule (i, j) à la séquence
- Tirer à pile ou face :
- Si c’est pile : Casser le mur EST de la cellule (i, j)
- Si c’est face :
- Casser le mur SUD d’une des cellules (choisie au hasard) qui constituent le séquence qui vient d’être terminée.
- Réinitialiser la séquence à une liste vide
- Ajouter la dernière cellule à la séquence
- Tirer une cellule au sort dans la séquence et casser son mur SUD
Casser tous les murs EST de la dernière ligne
Retourner le labyrinthe
= Maze.gen_sidewinder(4, 4)
laby print(laby)
┏━━━┳━━━┳━━━┳━━━┓
┃ ┃
┣━━━╋━━━╋━━━╋ ┫
┃ ┃ ┃
┣━━━╋ ╋━━━╋ ┫
┃ ┃ ┃
┣━━━╋ ╋ ╋━━━┫
┃ ┃
┗━━━┻━━━┻━━━┻━━━┛
Si vous essayez les générateurs précédents plusieurs fois, vous devriez observer des patterns (au delà de la dernière ligne, qui est un défaut évident). Ces caractéristiques sont des défauts majeurs pour un labyrinthe.
5.3 Fusion de chemins
L’algorithme de fusion de chemins consiste à partir d’un labyrinthe « plein », puis à casser des murs au hasard en évitant de créer des cycles. Puisqu’un labyrinthe parfait est un arbre, et qu’un arbre à \(n\) sommets a exactement \(n-1\) arêtes, il suffira d’abattre \(n-1\) murs (soit \(h\times w-1\) si \(h\) et \(w\) désignent respectivement le nombre de lignes et le nombre de colonnes).
Pour éviter de créer des cycles, on utilise un mécanisme de labélisation des cellules (avec des entiers). Lorsqu’on casse un mur depuis une cellule, le label de la cellule « se propage » dans la zone découverte. Mais on n’ouvrira un mur que lorsque le label de la cellule courante est différent du label de la cellule qui est de l’autre côté du mur.
Voici la description de l’algorithme (dans sa version la plus naïve2) :
- Initialisation :
- on remplit le labyrinthe avec tous les murs possibles
- on labélise les cellules de 1 à \(n\)
- on extrait la liste de tous les murs et on les « mélange » (on les permute aléatoirement)
- Pour chaque mur de la liste :
- Si les deux cellules séparées par le mur n’ont pas le même label :
- casser le mur
- affecter le label de l’une des deux cellules, à l’autre, et à toutes celles qui ont le même label que la deuxième
- Si les deux cellules séparées par le mur n’ont pas le même label :
5.4 Exploration exhaustive
Une deuxième idée consiste à « explorer » aléatoirement le labyrinthe, à la manière d’un parcours en profondeur, en cassant les murs à mesure qu’on avance :
- Initialisation :
- Choisir une cellule au hasard
- Marquer cette cellule comme étant visitée
- Mettre cette cellule dans sur une pile
- Tant que la pile n’est pas vide :
- Prendre la cellule en haut de la pile et l’en retirer
- Si cette cellule a des voisins qui n’ont pas encore été visités :
- La remettre sur la pile
- Choisir au hasard l’une de ses cellules contigües qui n’a pas été visitée
- Casser le mur entre la cellule (celle qui a été dépilée) et celle qui vient d’être choisie
- Marquer la cellule qui vient d’être choisie comme visitée
- Et la mettre sur la pile
5.5 L’algorithme de Wilson
Terminons avec un algorithme plus amusant, qui donne des labyrinthes très intéressants : l’algorithme de Wilson. Il repose sur les marches aléatoires.
L’idée est la suivante : on va construire le labyrinthe en essayant des chemins aléatoires, jusqu’à obtention d’une arborescence…
- Choisir une cellule au hasard sur la grille et la marquer
- Tant qu’il reste des cellules non marquées :
- Choisir une cellule de départ au hasard, parmi les cellules non marquées
- Effectuer une marche aléatoire jusqu’à ce qu’une cellule marquée soit atteinte (en cas de boucle, si la tête du snake se mord la queue, « couper » la boucle formée [autrement dit, supprimer toutes étapes depuis le précédent passage])
- Marquer chaque cellule du chemin, et casser tous les murs rencontrés, jusqu’à la cellule marquée
Exemple :
= Maze.gen_wilson(12, 12)
laby print(laby)
┏━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┓
┃ ┃ ┃ ┃ ┃ ┃ ┃
┣ ╋ ╋━━━╋━━━╋━━━╋ ╋━━━╋━━━╋ ╋━━━╋ ╋ ┫
┃ ┃ ┃ ┃ ┃
┣━━━╋━━━╋ ╋ ╋━━━╋━━━╋ ╋ ╋━━━╋━━━╋ ╋━━━┫
┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣ ╋ ╋ ╋ ╋━━━╋ ╋━━━╋ ╋ ╋ ╋━━━╋ ┫
┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣━━━╋ ╋━━━╋ ╋━━━╋ ╋ ╋━━━╋ ╋ ╋ ╋━━━┫
┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣ ╋━━━╋ ╋ ╋ ╋ ╋━━━╋ ╋━━━╋━━━╋ ╋ ┫
┃ ┃ ┃ ┃ ┃ ┃ ┃
┣ ╋━━━╋━━━╋ ╋ ╋━━━╋ ╋━━━╋━━━╋━━━╋━━━╋ ┫
┃ ┃ ┃ ┃ ┃ ┃
┣ ╋━━━╋ ╋━━━╋ ╋ ╋━━━╋ ╋ ╋━━━╋━━━╋━━━┫
┃ ┃ ┃ ┃ ┃ ┃ ┃
┣ ╋ ╋━━━╋ ╋━━━╋━━━╋ ╋ ╋ ╋━━━╋ ╋ ┫
┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣ ╋━━━╋━━━╋ ╋━━━╋ ╋━━━╋━━━╋━━━╋ ╋━━━╋ ┫
┃ ┃ ┃ ┃
┣━━━╋━━━╋ ╋ ╋ ╋ ╋━━━╋━━━╋━━━╋ ╋━━━╋ ┫
┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣ ╋━━━╋ ╋ ╋ ╋━━━╋ ╋━━━╋ ╋ ╋ ╋ ┫
┃ ┃ ┃ ┃ ┃ ┃ ┃
┗━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━┛
6 Résolution
Avant d’écrire des méthodes de résolution de labyrinthe, ajoutez cette méthode à votre classe Maze
:
def overlay(self, content=None):
"""
Rendu en mode texte, sur la sortie standard, \
d'un labyrinthe avec du contenu dans les cellules
Argument:
content (dict) : dictionnaire tq content[cell] contient le caractère à afficher au milieu de la cellule
Retour:
string
"""
if content is None:
= {(i,j):' ' for i in range(self.height) for j in range(self.width)}
content else:
# Python >=3.9
#content = content | {(i, j): ' ' for i in range(
# self.height) for j in range(self.width) if (i,j) not in content}
# Python <3.9
= {(i, j): ' ' for i in range(self.height) for j in range(self.width) if (i,j) not in content}
new_content = {**content, **new_content}
content = r""
txt # Première ligne
+= "┏"
txt for j in range(self.width-1):
+= "━━━┳"
txt += "━━━┓\n"
txt += "┃"
txt for j in range(self.width-1):
+= " "+content[(0,j)]+" ┃" if (0,j+1) not in self.neighbors[(0,j)] else " "+content[(0,j)]+" "
txt += " "+content[(0,self.width-1)]+" ┃\n"
txt # Lignes normales
for i in range(self.height-1):
+= "┣"
txt for j in range(self.width-1):
+= "━━━╋" if (i+1,j) not in self.neighbors[(i,j)] else " ╋"
txt += "━━━┫\n" if (i+1,self.width-1) not in self.neighbors[(i,self.width-1)] else " ┫\n"
txt += "┃"
txt for j in range(self.width):
+= " "+content[(i+1,j)]+" ┃" if (i+1,j+1) not in self.neighbors[(i+1,j)] else " "+content[(i+1,j)]+" "
txt += "\n"
txt # Bas du tableau
+= "┗"
txt for i in range(self.width-1):
+= "━━━┻"
txt += "━━━┛\n"
txt return txt
Cette méthode permettra d’afficher un labyrinthe en mode texte en lui ajoutant des caractères dans les cellules (afin de visualiser les solutions trouvées, par exemple). Il suffit pour ça de fournir à overlay
un dictionnaire content
dont les clés sont des cellules et les valeurs sont les caractères à afficher dans les cellules correspondantes.
Exemples :
= Maze(4,4)
laby
laby.empty()print(laby.overlay({
0, 0):'c',
(0, 1):'o',
(1, 1):'u',
(2, 1):'c',
(2, 2):'o',
(3, 2):'u',
(3, 3):'!'})) (
┏━━━┳━━━┳━━━┳━━━┓
┃ c o ┃
┣ ╋ ╋ ╋ ┫
┃ u ┃
┣ ╋ ╋ ╋ ┫
┃ c o ┃
┣ ╋ ╋ ╋ ┫
┃ u ! ┃
┗━━━┻━━━┻━━━┻━━━┛
= Maze(4,4)
laby
laby.empty()= {(0, 0): '@',
path 1, 0): '*',
(1, 1): '*',
(2, 1): '*',
(2, 2): '*',
(3, 2): '*',
(3, 3): '§'}
(print(laby.overlay(path))
┏━━━┳━━━┳━━━┳━━━┓
┃ @ ┃
┣ ╋ ╋ ╋ ┫
┃ * * ┃
┣ ╋ ╋ ╋ ┫
┃ * * ┃
┣ ╋ ╋ ╋ ┫
┃ * § ┃
┗━━━┻━━━┻━━━┻━━━┛
6.1 Résolution par parcours
L’algorithme le plus évident pour résoudre un problème de labyrinthe, consiste à adapter le parcours « en profondeur d’abord » de l’arborescence associée au labyrinthe :
Parcours du graphe jusqu’à ce qu’on trouve A
- Initialisation :
- Placer D dans la struture d’attente (file ou pile) et marquer D
- Mémoriser l’élément prédécesseur de D comme étant D
- Tant qu’il reste des cellules non-marquées :
- Prendre la « première » cellule et la retirer de la structure (appelons c, cette cellule)
- Si c correspond à A :
- C’est terminé, on a trouvé un chemin vers la cellule de destination
- Sinon :
- Pour chaque voisine de c :
- Si elle n’est pas marquée :
- La marquer
- La mettre dans la structure d’attente
- Mémoriser son prédécesseur comme étant c
- Si elle n’est pas marquée :
- Pour chaque voisine de c :
Reconstruction du chemin à partir des prédécesseurs
- Initialiser c à A
- Tant que c n’est pas D :
- ajouter c au chemin
- mettre le prédécesseur de c dans c
- Ajouter D au chemin
Retourner le chemin
6.2 Résolution en aveugle : « la main droite »
L’algorithme, bien connu, dit « de la main droite » peut-être vu comme une recherche « en profondeur d’abord » mais sans vision globale du labyrinthe. C’est la situation dans laquelle serait un individu qu’on abandonnerait dans un labyrinthe et qui devrait trouver la sortie.
Implémentez cet algorithme dans une méthode d’instance solve_rhr(start, stop)
qui retourne le chemin trouvé pour aller de start
à stop
.
7 Évaluation
Nous sommes désormais capables de générer des labyrinthes parfaits, et de les résoudre.
Il nous faudrait maintenant quelques outils d’évaluation des labyrinthes produits :
- le labyrinthe généré est-il « facile » ?
- la meilleure solution est-elle longue à trouver (relativement aux points de départ et d’arrivée) ?
8 Difficulté
Pour en faire un petit jeu, il est nécessaire de pouvoir de quantifier et graduer la difficulté d’un labyrinthe.
Pour compliquer la résolution « à l’oeil » d’un labyrinthe, on peut utiliser différentes métriques et différentes stratégies, quitte à casser certaines propriétés du labyrinthe (on n’est pas obligé d’avoir des labyrinthes parfaits).