Projets SCRATCH avancés
Générateur de labyrinthe
Description
Dans ce projet, nous allons voir comment programmer un générateur de labyrinthe avec SCRATCH. Générer des labyrinthes de manière dynamique sera, par exemple, particulièrement utile dans le développement de certains jeux car il permettra au programmeur de créer automatiquement différents tableaux, sans avoir à les concevoir lui-même ! Une fois encore, le code nous sert à faire accomplir à un ordinateur des tâches répétitives, longues et laborieuses à notre place !
D’un point de vue pédagogique ce projet demande une manipulation plus poussée des tableaux (ou listes), des boucles et des tests. Il s’agit également, ici, de réaliser un algorithme plus complexe que ceux qui ont pu être programmés dans les projets intermédiaires. Nous nous approchons plus d’une programmation « réelle ».
A notre sens, les élèves arrivant au terme de la programmation de ce type de projets sont très certainement prêts à aborder de « vrais » langages de programmation. (C’est d’ailleurs dans ce type de projet que les limites de Scratch commencent à se faire sentir)
Générer, ou modéliser, un labyrinthe.
Génération de labyrinthe – théorie
Il existe plusieurs algorithmes permettant de générer des labyrinthes parfaits (où chaque cellule est reliée à toutes les autres).
Dans ce projet, nous utiliserons la méthode la plus simple, qui fonctionne très bien pour les labyrinthes de taille modeste : une méthode d’exploration exhaustive. En effet, cette dernière ne demande pas d’opération ou de raisonnement complexe.
Pour comprendre comment générer un labyrinthe, revenons d’abord sur sa définition.
Info
Nous ne nous intéressons, ici, qu’à des labyrinthes de forme rectangulaire ou carrée, en 2 dimensions.
Un labyrinthe rectangulaire, ou carré, est un ensemble de cellules. Chaque cellule est composée de murs ouverts ou fermés. Et lorsqu’elles sont toutes reliées les unes aux autres de manière unique nous avons un labyrinthe parfait.
Voici un exemple de petit labyrinthe :
figure-1
Maintenant affichons ce même labyrinthe avec toutes ses cellules, que leurs murs respectifs soient ouverts ou non, nous obtenons ceci :
figure-2
Chaque cellule dispose de murs et chaque mur et soit ouvert, soit fermé.
figure-3
Nous avons une grille de 4 cellules par 4 cellules, soit 16 cellules au total.
L’algorithme de génération
Voyons maintenant comment, avec une méthode d’exploration exhaustive d’une grille, nous allons pouvoir construire un labyrinthe à partir de cette dernière. Une fois le principe compris, nous n’aurons plus qu’à le programmer dans SCRATCH.
Les tâches que nous devons faire accomplir à notre programme seront donc les suivantes :
Dessiner une grille de m * n cellules (dans l’exemple en vidéo nous avons 4 x 4 ). Prendre une cellule de départ dans cette grille
Répéter de manière itérative les opérations suivantes :
marquer la cellule en cours comme étant visitée;
regarder si la cellule en cours dispose de cellules voisines qui n’ont pas été visitées;
Si elle en a :
Prendre une cellule au hasard;
Avancer jusqu’à cette cellule;
Considérer le mur entre la cellule d’où l’on vient et celle où l’on va comme ouvert.
Sinon
revenir sur nos pas jusqu’à que l’on soit dans une cellule qui dispose d’au moins une cellule voisine non visitée et recommencer.
Note
Pour qu’ils comprennent bien le processus, qui n’est finalement pas très complexe, il peut être intéressant de faire réaliser aux élèves un labyrinthe sur papier quadrillé. Au sein d’une même classe (et suivant le format de la grille) plusieurs labyrinthes différents pourront être produits. Une fois réalisé de cette manière, le passage au code devrait être plus aisé, car le raisonnement sera acquis.
Comment exprimer cet algorithme
Voyons comment il nous est possible de programmer ces différentes étapes.
Info
Ici nous décrirons les grands points de notre algorithme et les outils pour les implémenter dans SCRATCH. La réalisation complète du projet est quant à elle disponible dans la vidéo (voir la vidéo en fin de fiche).
La grille de départ
Nous ne nous en soucierons pas car ce n’est pas l’enjeu de ce projet. Nous vous donnons, avec le projet, un bloc appelé « dessiner la grille » qui fera cela automatiquement (importer une grille déjà dessinée dans SCRATCH cause plusieurs problèmes dus au manque de précision du logiciel au pixel près, ce qui, ici, serait cause d’erreur).
La cellule exploratrice
L’approche que nous allons avoir pour programmer cela dans SCRATCH va être de créer un lutin du format d’une cellule, posée dans la grille, et qui va explorer cette dernière suivant le principe décrit plus haut. Elle se déplacera jusqu’à avoir exploré toutes les possibilités. A chaque mouvement vers une cellule non visitée, elle ouvrira un mur dans la grille, c’est à dire que concrètement elle effacera un mur de la grille.
Tout le code que nous allons détailler maintenant sera donc placé dans ce lutin cellule exploratrice.
Les variables nécessaires
Comme vous le constatez en voyant la vidéo décrivant la méthode de génération, nous devons pouvoir stocker différents types de données :
la position de la cellule exploratrice, afin de gérer ses déplacements dans la grille;
la ou les directions possibles si des cellules non visitées sont à proximité;
les cellules visitées ou non dans la grille;
le trajet effectué au fur et à mesure afin de pouvoir revenir sur nos pas lorsque nous nous trouvons dans la situation où nous sommes dans une cellule qui ne dispose plus de cellules voisines non visitées.
Pour cela nous allons devoir utiliser différentes variables dont plusieurs de type liste (tableau dans SCRATCH)
Une grille et des listes
Ce dont nous avons réellement besoin, en premier lieu, est de pouvoir identifier chaque cellule. Pour cela, voyons comment nous pouvons tirer profit de notre structure en grille pour gérer nos données .
Nous pouvons identifier chaque cellule en utilisant sa position dans cette grille, suivant son indice de position horizontale et de position verticale. Nous nous trouvons sur un écran et plaçons notre grille en fonction de coordonnées x et y, notre premier élément sera donc 0,0 pour correspondre aux contraintes de positionnement :
figure-4
La structure de la grille nous permet de déterminer la position de nos différentes cellules à l’écran, elle nous permet également de gérer celle de notre cellule “d’exploration”, ainsi que ses déplacements au cours de l’itération.
Prenons un exemple :
figure-5
La figure-5 représente notre grille, dans laquelle nous avons :
en 1,3 la cellule courante (ou cellule d’exploration);
en 0,4 une cellule déjà visitée;
et en 1,2 et 2,3 deux cellules voisines de notre cellule courante pouvant être potentiellement choisies pour la prochaine étape.
La position de chaque cellule à l’écran sera égale à:
son indice x multiplié par la taille de la cellule pour son abscisse;
son indice y multiplié par la taille de la cellule pour son ordonnée.
Admettons que nos cellules fassent 28 pixels de coté (c’est le cas dans le projet SCRATCH) :
la cellule 0,0 se trouvera donc à x=0 et y=0 sur le rapport orthonormé;
la cellule 0,1 se trouvera donc en x=0 et y=28;
la cellule 1,1 en x=28 et y=28, etc.
De la même manière, si nous souhaitons trouver la position sur l’écran de notre cellule exploratrice, elle sera de :
3 multiplié par la taille en pixels d’une cellule pour son abscisse;
1 multiplié par la taille en pixels d’une cellule pour son ordonnée.
Et si nous la déplaçons vers le haut, sa nouvelle position sera donc :
3 multiplié par la taille en pixels d’une cellule sur l’axe des abscisses;
2 multiplié par la taille en pixels d’une cellule sur l’axe des ordonnées (soit sa position initiale + 1).
Nous voyons donc qu’en parvenant à stocker les indices des positions verticales et horizontales de chaque cellule et en stockant la position de la cellule courante, nous pouvons effectuer toutes les opérations dont nous avons besoin pour faire fonctionner notre méthode d’exploration.
Quel type de variable utiliser
Rappel
Comme nous l’avons vu dans les ressources théoriques sur les tableaux, un tableau, ou liste dans SCRATCH, est une structure de données qui permet de stocker plusieurs éléments du même type. Ces éléments sont accessibles et identifiés par leur index de position dans ce tableau.
C’est exactement ce dont nous avons besoin et nous allons donc créer 4 listes.
Une liste pour gérer les cellules visitées
Nous stockerons ces cellules par la valeur de leurs positions horizontales et verticales dans la grille. Nous la nommerons cellules_visitees.
A chaque fois qu’une cellule aura été visitée, nous stockerons sa position dans la grille sous la forme d’une chaîne de caractères que nous créerons en concaténant : sa position x +’,’+ sa position y, et nous l’ajouterons à la liste cellules_visitees.
Par exemple, dans le cas de la figure-5, la liste contiendrait un seul élément : ‘0,4’.
Cette liste nous permettra de savoir quelles cellules n’ont pas été visitées et de voir dans quelle direction notre cellule exploratrice pourra se déplacer. En effet nous avons un nombre fini de cellules puisque nous sommes dans une grille. Logiquement, toute cellule ne se trouvant pas dans la liste des cellules visitées sera donc “non visitée”.
Les listes pour gérer la cellule exploratrice
Pour gérer la position de la cellule en cours, qui est en fait notre « cellule exploratrice », et son déplacement il nous faut créer 3 listes.
tableau_x
tableau_y
directions_possibles
Info
SCRATCH ne permet de créer des listes qu’à une seule dimension. Nous allons donc devoir créer plusieurs listes là où, dans un langage classique, nous n’utiliserions, par exemple, qu’un tableau à 2 dimensions (un tableau dont chaque élément contient lui-même un tableau)
stocker la position de la cellule exploratrice
A chacun de ses mouvements dans la grille, nous stockerons sa nouvelle position. Pour cela nous utiliserons 2 listes. En effet, lorsque nous devrons revenir sur nos pas, à certains moments de notre algorithme, nous n’aurons plus qu’à prendre les valeurs de ces 2 listes en partant de la dernière vers la première.
sa position x dans la grille dans une liste que nous appellerons tableau_x
sa position y dans la grille dans une liste que nous appellerons tableau_y
Les directions possibles que peut prendre la cellule exploratrice lors d’un déplacement
Lorsque nous trouvons des cellules non visitées adjacentes à notre cellule exploratrice, nous en prenons une au hasard et nous nous déplaçons vers elle. Pour cela, nous devrons la déplacer soit vers le haut, soit vers le bas, soit vers la gauche, soit vers la droite, suivant la position de chaque cellule voisine disponible. A chaque passage de notre itération nous allons donc définir les directions possibles, en fonction de chaque cellule disponible, et les stocker temporairement dans un tableau, ou liste, grâce à une suite de tests. Nous appellerons cette liste directions_possibles.
Si la cellule au dessus de la cellule courante n’est pas contenue dans le tableau des cellules visitées, alors on ajoute “haut” au tableau directions_possibles.
Si la cellule en dessous de la cellule courante n’est pas contenue dans le tableau des cellules visitées, alors on ajoute “bas” au tableau directions_possibles.
Nous ferons des tests pour les 4 directions possibles, puis nous en prendrons une au hasard. Ce tableau sera bien sûr vidé de ses valeurs à chaque nouvelle itération.
Par exemple, dans le cas de la figure-5, les deux directions temporairement stockées seront “haut” et “gauche” puisque les cellules disponibles se trouvent au dessus et à gauche de notre cellule exploratrice.
Les autres variables
Il nous faudra aussi stocker :
la taille en pixels d’une cellule puisque nous en avons besoin pour gérer les déplacements comme nous l’avons vu ci-dessus. Nous l’appellerons taille_cellule
La direction prise par notre cellule exploratrice lorsqu’une cellule non visitée aura été choisie : elle pourra avoir comme valeur haut, bas, droite ou gauche. Nous l’appellerons direction
La position x de la cellule exploratrice, pour gérer ses déplacements, que nous appellerons posx
La position y de la cellule exploratrice , pour gérer ses déplacements, que nous appellerons posy
Le pseudo code du lutin qui jouera le rôle de la cellule exploratrice
posx←0 // sa position x de départ posy←0 //sa position y de départ //nous commençons l’exploration dans le coin en bas à droite donc en 0,0 de notre grille //ce tableau contient notre position de départ comme cellule visitée Tableau cellules_visitees("0,0") //ce tableau contient la position x actuelle de notre cellule exploratrice Tableau tableau_x(posx) //ce tableau contient la position y actuelle de notre cellule exploratrice Tableau tableau_y(posy) //ce tableau contiendra toutes les directions possibles à chaque exploration Tableau directions_possibles() TantQue nombre d’éléments de Tableau cellules_visitees<largeur_grille*hauteur_grille //on vide le tableau contenant les directions possibles Tableau directions_possibles() Si posx>0 && Tableau cellules_visitées ne contient pas (posx-1)+","+posy Alors Ajouter "gauche" à Tableau directions_possibles Si posx<largeur_grille-1 && Tableau cellules_visitees ne contient pas (posx+1)+","+posy Alors Ajouter "droite" à Tableau directions_possibles Si posy>0 && Tableau cellules_visitees ne contient pas posx+","+(posy-1) Alors Ajouter "bas" à Tableau directions_possibles Si posx<hauteur_grille-1 && Tableau cellules_visitees ne contient pas (posy+1)+","+posy Alors Ajouter "haut" à Tableau directions_possibles Si longueur Tableau directions_possible>0 Alors direction←élément aléatoire de Tableau direction_possibles avancer d’une case et ouvrir mur dans : direction // nous avons créée un bloc spécifique dans Scratch, vous n’aurez pas à coder cette partie ajouter posx à Tableau tableau_x ajouter posy à Tableau tableau_y ajouter posx+","+posy à Tableau cellules_visitees Sinon //on prend la dernière position connue pour revenir d’un cran en arrière posx←dernier élément de Tableau tableau_x posy←dernier élément de Tableau tableau_y //on supprime la dernière position connue de nos 2 tableaux puisque nous revenons sur nos pas supprimer dernier élément de Tableau tableau_x supprimer dernier élément de Tableau tableau_y //on affecte la nouvelle position à notre lutin position x du lutin=taille_cellule*posx position y du lutin=taille_cellule*posy Fin Si Sinon Fin TantQue
La programmation de ce projet dans SCRATCH
.
.