CSC_41011 · TD4
Ce TD traite de la résolution et la génération de labyrinthes dans une grille rectangulaire. Nous utiliserons la technique du rebroussement (cours 4, chapitre 12 du poly).
Avant de commencer :
TD4
pour le TD de cette semaine.TD4.zip
et
extrayez les fichiers dans le dossier du projet.Le but de ce TP est de compléter la classe Maze, de sorte à faire
fonctionner l’interface graphique src/MazeGUI.java
(que
vous pouvez déjà lancer) et le fichier test
src/Test.java
.
On décrit ici la structure de données utilisée par
src/Maze.java
pour représenter un labyrinthe.
rows
× cols
cellules (voir
ci-contre un exemple 9×9).R × C
avec :
R = 2*rows + 1
C = 2*cols + 1
Maze.Cell[] grid
.(i, j)
de la grille augmentée, ligne
i
, colonne j
, se trouve à l’indice
i + R*j
.(i,j)
peut être de l’un des types suivants :
i
et j
sont impairs ;i == 0 || i == R-1 || j == 0 || j == C-1
;i
et j
sont pairs. Ces cases ne sont jamais utilisées.Chacune des cases de la grille a un état parmi les valeurs suivantes.
enum Cell {
, WALL, REMOVABLE, RIGHT, UP, LEFT, DOWN, MARK
VOID}
WALL
.VOID
ou marquées par la marque neutre MARK
ou
une marque directionnelle (RIGHT
, UP
,
LEFT
, DOWN
).VOID
(passage ouvert) ou REMOVABLE
(passage obstrué par un mur
amovible).Les cases d’entrée (bleu) et de sortie (jaune) sont spécifiées par
start
et exit
et ne correspondent pas à un
état.
final int[] dirs = { 1, -R, -1, R }
encode les
4 directions sous forme de décalage d’indice:
+1
→ bas (DOWN)-R
→ gauche (LEFT)-1
→ haut (UP)+R
→ droite (RIGHT)p
:
p
et la cellule voisine dans la direction
shift
se trouve à l’indice w = p + shift
. La
valeur de cette case indique s’il y a un mur ou non. S’il y a un mur
WALL
(inamovible), on a atteint le bord du labyrinthe, il
n’y a pas de cellule voisine au-delà.q = p + 2*shift
.Pour itérer sur les voisins accessibles de la cellule à l’indice
int cell
, on peut donc écrire
for (int shift: dirs) {
int passage = cell + shift;
int next = cell + 2*shift; // indice de la cellule voisine
if (grid[passage] == WALL || grid[passage]==REMOVABLE)
continue; // Il y a un mur: on ne passe pas
// ...
}
La condition sur grid[passage]
peut bien sûr être
adaptée selon la manière dont on veut traiter les murs amovibles.
Les cellules sont les cases (i,j)
avec i
et
j
impairs. Donc on peut itérer sur toutes les cellules
comme suit :
for (int i = 1; i < R; i += 2) {
for (int j = 1; j < C; j += 2) {
int cell = i + R*j;
// ...
}
}
Les méthodes que vous developperez seront accessibles depuis
l’interface MazeGUI.java
.
La classe Maze
fournit aussi toString()
, ce
qui permet d’afficher l’état du labyrinthe dans la console.
Dans cette première question, nous voulons trouver un chemin entre deux cellules dans un labyrinthe donné, comme dans l’image ci-contre.
Résoudre un labyrinthe entre une cellule de départ (en bleu) et une cellule d’arrivée (en jaune) signifie marquer un chemin avec les marqueurs de direction. La cellule d’arrivée n’est pas marquée. Les cellules en dehors du chemin ne sont pas marquées.
NB:
grid[cell] = UP
: marquer la cellule à l’indice
cell
. Marche aussi avec LEFT
,
RIGHT
et DOWN
.grid[cell] != VOID
: teste si une cellule est
marquée.Implémentez la méthode boolean solve(int from, int to)
,
où from
et to
sont des indices de cellules, de
sorte que :
true
s’il existe entre from
et to
un chemin ne passant par aucune case marquée
false
sinon,
grid
doit être inchangé.Déposez Maze.java
.
La méthode solve
s’écrit naturellement de manière
récursive. Pour transformer un décalage (1
,
-R
, -1
, R
) en marque
directionnelle (DOWN
, LEFT
, UP
,
RIGHT
), on peut utiliser la méthode
Maze.Cell markOfShift(int shift)
.
Testez votre code avec Test.java
et
MazeGUI.java
.
Vous remarquez sans doute que la méthode commence avec l’appel
step()
. C’est utilisé pour ralentir l’animation de la
recherche de chemin pour qu’elle soit observable à l’œil nu.
Dans le reste du TD, nous allons travailler sur la génération de labyrinthe. Ils doivent être parfaits, c’est-à-dire qu’il y un et un seul chemin entre chaque paire de cellules.
Lors de la génération, on partira d’un labyrinthe dont toutes les
cellules sont isolées, c’est-à-dire entourées de quatre murs, et leur
marque est VOID
. Les murs internes sont amovibles. La
génération de labyrinthe consiste à supprimer des murs internes jusqu’à
obtenir un labyrinthe parfait. On supprime le mur d’une cellule
cell
dans la direction shift
avec
grid[cell + shift] = VOID
. On pourra marquer certaines
cellules avec la marque neutre MARK
:
grid[cell] = MARK
: marquer la cellule à l’indice
cell
grid[cell] == MARK
: teste si une cellule est
marquée.Tout au long de la génération, on maintiendra les invariants suivants :
start
par
un chemin.Et la suppression des murs est soumise à la règle :
Démontrer que :
alors le labyrinthe est parfait.
Complétez la méthode void generateRec(int cell)
. La
méthode doit marquer toutes les cellules accessibles depuis
cell
par des cellules non marquées uniquement (en passant
au travers des murs amovibles). Elle doit respecter les contraintes
ci-dessus.
Déposez Maze.java
.
Pour ne pas toujours aboutir au même labyrinthe, on peut vouloir parcourir les voisins dans un ordre aléatoire. On peut, par exemple, procéder ainsi:
int[] random_dirs = dirs.clone();
shuffle(random_dirs);
for (int shift : random_dirs) {
int next = cell + 2 * shift;
//...
}
Testez votre code avec Test.java
et
MazeGUI.java
.
Nous allons maintenant traduire l’algorithme de génération récursif
en une version itérative. Cela veut dire que nous échangeons la pile
d’appels contre un conteneur abstrait
Bag<Integer> bag
. Ce conteneur ne fournit que trois
méthodes:
boolean isEmpty()
void push(Integer p)
: ajoute un élémentInteger pop()
: retire un élément et le renvoieOn impose la règle supplémentaire suivante:
bag
.Démontrer que :
start
est marquée,bag
est vide,alors le labyrinthe est parfait.
Complétez la méthode void generateIter(Bag bag)
. Elle
prend en argument un bag
vide et génère un labyrinthe
parfait.
Déposez Maze.java
.
Testez votre code avec Test.java
et
MazeGUI.java
. Dans l’interface graphique, un menu permet de
sélectionner le comportement de bag.pop()
:
pop
retire un élément aléatoire
;pop
retire le plus grand
élément.N’oubliez pas d’appeler step()
à chaque itération.
Nous venons d’implémenter des algorithmes de génération de labyrinthes aléatoires. La forme des labyrinthes générés par le rebroussement itératif dépendait fortement du choix de la méthode de sélection de la prochaine case à traiter. En particulier, les labyrinthes générés ne sont pas uniformément distribués dans l’ensemble de tous les labyrinthes parfaits de la taille donnée.
Dans cet exercice nous allons donc implémenter un algorithme de génération uniforme. La méthode de génération choisie s’appelle l’algorithme de Wilson. Il procède comme suit :
Marquer la cellule start
avec
MARK
.
Tant qu’il y a une cellule non marquée, choisir une cellule non marquée uniformément au hasard, et
L’algorithme est beaucoup plus simple à implémenter qu’il n’y paraît
! Pour tirer des cellules uniformes aléatoires successivement, il suffit
de mettre toutes les cellules dans un tableau et de le mélanger avec
shuffle
.
Pour effacer les boucles d’une marche aléatoire, il suffit
d’enregistrer dans grid
la direction prise par la marche à
chaque étape. On utilise pour cela les marques directionnelles
(RIGHT
, UP
, LEFT
,
DOWN
) et on écrase les valeurs précédente. Ainsi, quand la
marche est terminée, on la reprend depuis son point de départ et on suit
les flèches. Elles amènent jusqu’à l’arrivée sans faire de boucles.
Pour transformer un décalage (1
, -R
,
-1
, R
) en marque directionnelle
(DOWN
, LEFT
, UP
,
RIGHT
), on peut utiliser la méthode
Maze.Cell markOfShift(int shift)
. Pour la transformation
inverse, on peut utiliser
int shiftOfMark(Maze.Cell mark)
.
Testez votre code avec Test.java
et
MazeGUI.java
.