Vous êtes sur la page 1sur 41

1.

Introduction
Le but de ce projet est l’implémentation d’un logiciel pour la résolution automatique des
puzzles du type Sokoban – un jeu de réflexion inventé par le japonais Hiroyuki
Imabayashi dans les années ’80 du siècle dernier. Le jeu se déroule dans un labyrinthe
divisé en cases carrées, dans lequel se trouvent des pierres et dont certaines cases sont
marquées comme cases-cibles. Le joueur contrôle un personnage qui peut se déplacer à
l’intérieur du labyrinthe et qui peut pousser les pierres ; le but du jeu est de ramener
toutes les pierres sur les cases-cibles. Comme restrictions, les pierres peuvent être
seulement poussées et une seule pierre peut être deplacée à un moment donné. (Le
personnage ne peut donc ni remorquer les pierres, ni en déplacer deux à la fois).

Fig. 1 : Puzzle du Sokoban. Le but est de ramener toutes les pierres sur les cibles ;
Les pierres sont déplacées en les poussant, une seule pierre à la fois.

Ce jeu est intéressant car, malgré la grande simplicité des règles, les problèmes qu’il
pose sont souvent des vrais casse-têtes. Pour bien de niveaux du jeu la solution implique
des centaines de mouvements ; cette grande profondeur de l’arbre de recherche s’ajoute à
un grand degré de ramification – le nombre d’alternatives disponibles à chaque
mouvement rend le jeu comparable au jeu d’échecs.

Dans ce qui suit, les cases du labyrinthe seront nommées cellules ; les cases-cible seront
appelées simplement cibles. Le personnage est nommé pousseur.

1
2. Etat du domaine de l’intelligence artificielle.

2
3. Présentation de l’application
Le logiciel présenté a un caractère didactique : c’est une application Java contenant une
collection d’une vingtaine de puzzles Sokoban, de plus simples jusqu'à des puzzles de
complexité moyenne. L’utilisateur peut choisir parmi les labyrinthes proposés et peut
essayer de les résoudre lui-même ou bien il peut démarrer une analyse automatique de
n’importe quel puzzle de la collection. Si l’analyse automatique mène à une solution,
l’utilisateur pourra ensuite visualiser la solution trouvée (qui pourra être parcourue
automatiquement ou pas à pas, arrêtée ou parcourue en sens inverse). Apres toute analyse
automatique, l’utilisateur aura aussi l’option d’examiner la ‘pensée’ du programme : il
aura l’option d’examiner les états parcourus par l’analyseur automatique, afin d’en
évaluer la performance.

L’application se présente comme une collection d’une soixantaine de fichiers binaires


(classes Java) (Le grand nombre de fichiers s’explique par la multitude des composantes
de l’interface graphique, chacune ayant une classe Java correspondante). La classe
principale de l’application est PusherMain ; pour exécuter l’application on va donc
lancer la commande :
java PusherMain
qui va faire apparaître l’écran principal du programme.

Dans ce qui suit, on va présenter un à un les écrans du programme et on va détailler leur


fonctionnement.

3
3.1. Ecran principal

Dans l’image suivante on présente un instantané de la fenêtre principale de l’application


(en l’occurrence, celle correspondant à un des puzzles les plus simples). On y retrouve le
dessin du labyrinthe avec les pierres et le pousseur ; le nombre du labyrinthe dans la
collection (6, dans notre cas) ; et des indications sur les commandes disponibles.

Fig. 2 : La fenêtre principale de l’application

Dans cet écran, l’utilisateur peut essayer de résoudre lui-même le puzzle ; pour cela, il se
sert des flèches pour déplacer le pousseur dans le labyrinthe. Pour la commodité, et
comme une extension au jeu original, l’utilisateur a aussi la possibilité de revenir en
arrière sur ses pas, en utilisant la touche <backspace>, s’il se rend compte qu’il a fait une
erreur. es autres commandes disponibles dans cet écran sont les suivantes (toute

4
commande peut être exécutée en cliquant sur le bouton de la commande ou par appui de
la touche correspondante):

- la touche <Entrée> peut être appuyée à tout moment pour démarrer une analyse
automatique du puzzle dans l’état où il se trouve (par exemple, si l’utilisateur essaye de
résoudre lui-même le puzzle et se retrouve dans un impasse, il a l’option de demander
une analyse automatique de la situation) ;

- les touches <Page Up> / <Page Down> servent à passer au labyrinthe précédent ou au
labyrinthe suivant de la collection ;

- pour sortir du programme, on utilise les touches <Echapper> ou <Q>.

5
3.2. Ecran d’analyse en cours

Lorsque l’utilisateur demande une analyse automatique de la situation (en appuyant sur
<Entrée> dans l’écran principal), le programme va afficher la fenêtre ‘analyse en cours’.
Dans ce mode, le labyrinthe reste affiché mais l’utilisateur ne peut plus déplacer le
pousseur ; le programme va afficher continuellement le nombre d’états analysés jusqu'à
présent. A tout moment, l’utilisateur peut cliquer sur ‘stop analysis’ (ou appuyer sur
<Echapper> ou sur <Q>) pour arrêter l’analyse, ce qui conduit à l’écran des résultats de
l’analyse.

Fig. 3 : Ecran d’analyse en cours

6
3.3. Ecran de résultats de l’analyse

Quand l’analyse d’un puzzle s’achève – après l’atteinte d’une solution, après avoir épuisé
tous les mouvements possibles sans trouver de solution ou après l’arrêt de l’analyse par
l’utilisateur – le programme va afficher une fenêtre avec les résultats de l’analyse. Cette
fenêtre contiendra, outre le labyrinthe lui-même, le nombre du labyrinthe analysé dans la
collection, le nombre total d’états parcourus, le nombre de blocages trouvés (voir section
sur l’architecture de l’application pour des explications sur les blocages). Si l’analyse a
mené à une solution, cette fenêtre va contenir l’indication ‘found solution’ (‘solution
trouvée’).

Fig. 4 : L’écran des résultats de l’analyse du labyrinthe 6


(notez la mention ‘found solution’, qui indique que l’on a trouvé une solution)

7
Les commandes disponibles dans cette fenêtre sont les suivantes :

- Défilage automatique de la solution, en appuyant sur <Entrée>. Cette commande va


conduire à la fenêtre de défilage automatique de la solution (voir plus bas) ;

- Exécution pas-à-pas de la solution, en appuyant sur <Espace> pour avancer et sur


<Backspace> pour aller en arrière. De cette manière l’utilisateur peut exécuter pas à pas
la solution trouvée par le programme, en insistant s’il le désire sur certaines successions
de mouvements ;

- Reprise de la solution à partir de l’état initial, en appuyant sur <Home>. Cela va


remettre le labyrinthe dans son état initial, et l’utilisateur pourra faire défiler la solution
encore une fois ;

- Visualisation des états parcourus dans l’analyse, en appuyant sur <S>. Cette commande
va ouvrir la fenêtre de visualisation des états, dans laquelle l’utilisateur peut examiner, un
à un, les états parcourus par le programme pendant l’analyse;

- Visualisation des blocages, en appuyant sur <D>. Cette commande va ouvrir la fenêtre
de visualisation des blocages, où l’utilisateur peut examiner, un à un, les blocages
découverts par le programme ;

- En appuyant sur <Q> ou <Echapper>, l’utilisateur peut rentrer à l’écran principal du


programme.

8
3.4. Ecran de défilage automatique de la solution

Cette fenêtre (présentée dans la figure 5) va afficher, de façon animée, la succession de


mouvements qui conduit à la solution du puzzle. On y trouve aussi un pourcentage
indiquant la proportion de la solution qui a été déjà parcourue. Pour arrêter l’animation
on appuie sur <Q> ou sur <Echapper>, ce qui nous retourne conduit à l’écran des
résultats de l’analyse. La vitesse de l’animation est fixe, choisie afin d’assurer une
visualisation commode de la solution. Selon la complexité du puzzle analysé, le défilage
complet de la solution peut durer plusieurs minutes.

Fig. 5 : Ecran de défilage automatique de la solution pour le labyrinthe 6

9
3.5. Ecran de visualisation des états

Cet écran permet à l’utilisateur de visualiser un à un les états du labyrinthe examinés par
le programme pendant l’analyse. Cette fenêtre va afficher le nombre total d’états
examinés, le numéro de l’état affiché à présent, et le labyrinthe avec les positions des
pierres et du pousseur dans l’état considéré. En cliquant avec le bouton de droite de la
souris sur une pierre, l’utilisateur va visualiser le domaine admissible de la pierre en
question (voir section sur l’architecture de l’application pour une définition du domaine
admissible).

Fig. 6 : La fenêtre de visualisation des états parcourus. On examine un état


rencontré pendant l’analyse du labyrinthe 6.

10
Fig. 7 : Fenêtre de visualisation des états parcourus avec affichage
du domaine admissible d’une pierre (en bleu)

Les autres commandes accessibles dans cette fenêtre sont les suivantes :

- les touches <Page Up> / <Page Down> réalisent l’affichage de l’état précédent ou de
l’état suivant (notons qu’il ne s’agit pas de l’état précédent ou suivant au sens de
l’évolution du labyrinthe, mais de l’état précédent ou suivant dans la collection globale
des états examinés) ;

- la touche <F> va créer un fichier sur le disque contenant tous les états examinés par le
programme ; c’est une fonction utile surtout pour le dépannage du programme ou pour
l’évaluation de ses performances ;

- pour rentrer a l’écran précédent on appuie sur <Echapper> ou sur <Q>.

11
3.6. Ecran de visualisation des blocages

Cet écran permet à l’utilisateur de visualiser un à un les blocages qui ont été découverts
pendant l’analyse du labyrinthe (voir chapitre sur l’architecture de l’application pour plus
des détails sur les blocages). Cette fenêtre va afficher le nombre total de blocages, le
numéro du blocage affiché à présent, et le labyrinthe avec les positions des pierres et du
pousseur dans le blocage considéré.

Comme dans l’écran de visualisation des états, on appuie sur <PageUp> ou


<PageDown> pour afficher le blocage précédent ou le blocage suivant, et sur
<Echapper> ou sur <Q> pour rentrer a l’écran des résultats de l’analyse.

Fig. 8 : Ecran de visualisation des blocages. Ici, un blocage


survenu pendant l’analyse du labyrinthe 6.

12
4. Architecture interne de l’application

Dans les paragraphes suivants on va présenter en détail l’application, en commençant par


la description des algorithmes qui interviennent et en continuant par l’implémentation des
algorithmes et la description des structures de données employées.

4.1. Aspects théoriques et algorithmes

Comme pour la plupart des problèmes d’intelligence artificielle, le cas du Sokoban va


être traité comme une recherche dans l’espace des états. Le système dont on étudie les
états sera le labyrinthe dans son ensemble ; les états seront donc constitués par les
positions des pierres et la position du pousseur ; les changements d’état vont
correspondre aux déplacements du pousseur, qui peuvent aussi entraîner des changements
de position des pierres. L’espace des états sera donc un réseau, un graphe (en
l’occurrence, un graphe orienté) dont les nœuds vont correspondre aux états du labyrinthe
et les arcs vont correspondre aux déplacements du pousseur.

Dans ce qui suit, on va utiliser les termes suivants pour décrire le problème :
- par cellule on va entendre toute position de l’intérieur du labyrinthe, pouvant être
occupée par une pierre ou par le pousseur. L’ensemble de cellules constitue un graphe
non-orienté qu’on désigne par M (voir figure 9). La position d’une pierre ou du pousseur
est indiquée par la position la cellule occupée ;
- par cible on entend chacune des cellules marquées de intérieur du labyrinthe, qui
doivent être occupées par des pierres à la fin.

Tout état du labyrinthe va être identifié par l’ensemble des cellules occupées par des
pierres et par la cellule occupée par le pousseur. Les déplacements des pierres seront
identifiés par la cellule de départ et la cellule d’arrivée. Par le ‘développement’ ou
‘l’exploration’ d’un état on entend la construction de la liste des mouvements possibles
dans l’état donné et la construction des états résultants – c'est-à-dire une extension du
graphe des états.

Toute tentative de réaliser un programme pour la résolution automatique des puzzles doit
commencer par l’analyse de la manière dont l’intelligence humaine traite ces problèmes.
En analysant l’approche du joueur humain face aux puzzles du Sokoban on remarque que
le premier problème auquel le joueur se trouve confronté – et qu’il apprend bientôt à
gérer – est le problème des situations bloquées. Ces sont les situations où une ou
plusieurs pierres sont coincées, ne pouvant plus être déplacées, quoi qu’il se passe avec
les autres pierres du puzzle – ce qui empêche effectivement toute résolution (voir fig. 10).
Dans de telles situations il n’y a plus qu’à reprendre l’analyse, en tâchant d’éviter de
retomber dans la même situation bloquée. Les joueurs humains développent assez vite
une capacité de reconnaître et de prévenir ces situations ; un joueur avancé peut identifier
aisément, au premier regard, si une positions impliquant une ou plusieurs pierres est

13
bloquée ou pas. De même, les joueurs humains ont la capacité d’imaginer les
développements possibles d’une certaine position, ce qui leur permet d’identifier
aisément les situations qui, sans être encore bloquées, mènent inévitablement a un
blocage.

Fig. 9 : Un labyrinthe et son graphe M correspondant

Fig. 10 : Diverses situations bloquées qu’on peut rencontrer

14
La deuxième observation qu’on fait en analysant la manière dont la pensée humaine
fonctionne devant les puzzles Sokoban est que les mouvements réalisés par le joueur
humain ne sont presque jamais au hasard ; de manière spontanée et intuitive, le joueur
humain dirige ses recherches vers le but du jeu – le placement de toutes les pierres sur
des cibles. Il parait que l’intelligence humaine évalue continuellement l’utilité des
mouvements possibles, et n’en choisit que les mouvements vraiment prometteurs.

On peut donc affirmer que la recherche dans l’espace des états, telle qu’elle est réalisée
par la pensée humaine, est soumise à ces deux principes intuitifs :
- éviter les situations bloquées ;
- préférer les mouvements qui tendent vers une solution.

Un mécanisme de résolution automatique sera soumis, lui aussi, à ces deux principes
d’orientation de la recherche dans l’espace des états. Bien que la puissance de calcul d’un
ordinateur soit considérable – du point de vue de la vitesse d’évaluation des états et du
nombre d’états qui peuvent être stockés – le nombre de situations possibles est bien trop
grand pour qu’on puisse les évaluer toutes dans un temps raisonnable, même avec des
ordinateurs puissants. Par exemple, même un labyrinthe trivial comme celui de la fig. 11,
avec juste trois pierres et 18 cellules, a un nombre de 816 situations possibles, dont
seulement trois sont gagnantes ; il est évident que, pour des labyrinthes de plus grandes
dimensions, le nombre états à explorer devient impossible à gérer Il est donc important
que la recherche soit orientée aussi précisément que possible, afin de réduire le nombre
d’états à explorer.

Fig. 11 : Un labyrinthe ‘trivial’. Il a néanmoins plus de 800 états possibles !

Les deux principes dirigeants exprimés plus haut, bien qu’assez clairs pour un joueur
humain, sont bien trop vagues pour être utilisés directement dans un programme. Pour la
réalisation d’un système de résolution automatique on aura besoin, d’une part, d’un mode
de représentation du problème qui permette une modélisation facile de ces principes et,
d’autre part, d’un ensemble d’algorithmes qui réalisent de façon ‘mécanique’ ce que
l’intelligence humaine peut réaliser de manière intuitive. Des notions telles que ‘situation
bloquée’, ‘situation résolue’ etc. devront être définies rigoureusement, avec une précision
mathématique.

15
Dans ce qui suit, on va examiner en détail l’approche aux deux problèmes décrits plus
hauts : l’identification et des blocages et l‘orientation de l’exploration.

4.1.1. Identifier et éviter les blocages

On va commencer par l’analyse du premier principe de recherche – le besoin d’éviter les


situations bloquées. Comme on l’a vu plus haut, par une ‘situation bloquée’ on entend
une configuration ‘perdante’ du labyrinthe, dans laquelle au moins une pierre est
irréversiblement coincée, ne se trouvant pas sur une cible et ne pouvant plus être
déplacée, n’importe comment on déplacerait les autres pierres. Il est évident qu’une
pierre se trouvant dans cette situation va empêcher la résolution du puzzle, donc toute
exploration à partir d’un tel état sera inutile. Dans une telle situation il faut revenir en
arrière sur nos pas jusqu'à un état qui ne soit plus bloqué. La figure 10 indique quelques
types de situations bloquées. On remarque que les blocages rencontrés sont de plusieurs
types :
- simples (image 10, a), dans lesquels une pierre singulière se trouve dans une
position d’où elle ne peut plus être déplacée ;
- doubles (image 10, b), quand deux pierres s’empêchent mutuellement le
déplacement ;
- multiples (image 10, c et d), où plusieurs pierres sont bloquées entre elles.

Quel que soit le nombre de pierres impliquées, le résultat sera toujours le même : le
blocage est une configuration coincée, qui ne peut être débloquée par aucun déplacement,
et qui va se retrouver inchangée dans tous les état ultérieurs.

Du point de vue mathématique, si on note par En l’état ‘n’, par Pn l’ensemble des cellules
occupées par des pierres dans l’état ‘n’ et par C l’ensemble des cibles, la définition
rigoureuse du blocage sera la suivante : « un état En est bloqué si il existe un sous
ensemble B inclus dans Pn et non inclus dans C, tel que, pour tout état E m qui est
accessible de En par un nombre quelconque de déplacements, B soit inclus dans P m »
(définition 1).

Notons au passage quelques observations utiles : les blocages simples (à une seule pierre)
vont déterminer des ‘cellules interdites’ – cellules dont l’occupation par une pierre mène
automatiquement a une situation bloquée. Les coins du labyrinthe seront de telles cellules
interdites (voir figure 12). La plupart de ces cellules interdites peuvent être déterminées
dès le commencement de l’analyse, étant donc marquées et évitées par la suite. Pour les
blocages multiples, il résulte de leur définition même que toutes les pierres impliquées
doivent être bloquées Remarquons aussi que la position du pousseur est importante : une
même configuration de pierres peut être ou non un blocage, selon la position du pousseur
(voir figure 13).

16
Fig. 12 : Les cellules en rouges sont ‘interdites’ : une pierre qui s’y trouve
devient automatiquement coincée ou ne peut plus être ramenée sur une cible.

Fig. 13 : Ces deux situations diffèrent uniquement par la position du pousseur ;


la première est bloquée alors que la deuxième ne l’est pas.

Une fois définie la notion de blocage, une autre classe de situations qui s’impose à notre
attention est celle où des pierres, bien que pas encore coincées, ne peuvent plus réaliser

17
que des déplacements qui mènent nécessairement a des situations bloquées, quoi qu’il se
passe avec les autres pierres. Le joueur humain apprend bien vite à reconnaître ces
situations aussi, avant d’arriver dans un blocage proprement dit. Dans ce qui suit on va
nommer ces situations des ‘blocages d’ordre supérieur’, comme suit :
- un blocage d’ordre 0 sera un blocage proprement dit (simple, double ou
multiple), conformément à la définition (1) ci-dessus ;
- un blocage d’ordre 1 sera une situation dans laquelle il existe un groupe de
pierres tel que tout mouvement d’une pierre du groupe mène a un blocage d’ordre 0,
quels que soient les déplacements des pierres non incluses dans le groupe (voir figure
14) ;
- de façon semblable, un blocage d’ordre 2 sera une situation dans laquelle il
existe un groupe de pierres tel que tout mouvement d’une pierre du groupe mène a un
blocage d’ordre 1, quels que soient les déplacements des pierres non incluses dans le
groupe (voir figure 14). On définit ensuite de la même manière les blocages d’ordre de
plus en plus grand.

Fig. 14 : La première situation est un blocage proprement dit (d’ordre 0).


Les autre situations sont des blocages d’ordre 1, 2 et 3 – ils mènent inévitablement
à la situation de la première image.

Du point de vue pratique, de tels blocages on la même signification que les blocages
proprement dits : il indiquent que le puzzle est arrivé dans un état où la solution n’est plus
possible, et il ne reste plus qu’à revenir sur nos pas jusqu'à une situation non bloquée
pour chercher des alternatives.

Du point de vue mathématique, la définition (1) donnée plus haut indique un blocage
d’ordre 0, et servira comme point de départ pour la définition récursive des blocages
d’ordre supérieur, comme suit : « un état En est un blocage d’ordre n > 0 s’il existe un
sous-ensemble B inclus dans Pn et non inclus dans C tel que, pour tout état Em qui soit
accessible de En par un nombre quelconque de déplacements, l’une des affirmations
suivantes soit vraie :
- B soit inclus dans Pm, ou
- Pm soit un blocage d’ordre n-1 faisant intervenir les pierres de B »
(définition 2).

18
Bien que ces définitions mathématiques soient assez difficiles, les situations qu’elles
décrivent sont faciles à saisir intuitivement. Notons enfin qu’il peut apparaître des
situations où une pierre, sans être coincée, se trouve confinée à un ensemble de cellules
d’où elle ne peut plus sortir, ce qui empêche son placement sur une cible (voir figure 15).
Ce type de situation va déterminer, lui aussi, des cellules interdites.

Fig. 15 : Situation sans issue. La pierre d’en bas, sans être coincée,
n'a aucune possibilité d’arriver sur une cible.

A partir de cette analyse théorique, on introduit les notions suivantes :

- le domaine de visibilité (noté par V) sera le domaine des cellules que le pousseur peut
visiter sans devoir déplacer aucune pierre. V sera donc un sous graphe de M, bordé par
des murs ou par des pierres. V sera toujours un graphe bidirectionnel, car les mouvements
du pousseur qui n’impliquent pas de déplacement de pierres sont toujours réversibles.
(voir figure 16)

- le domaine admissible d’une pierre (noté par A) sera l’ensemble de cellules qu’une
pierre (considérée seule dans le labyrinthe) peut visiter sans perdre la possibilité d'être
placée sur une cible. Evidemment, les ‘cellules interdites’ identifiées plus haut ne seront
pas incluses dans A. En général, A sera un graphe dont les arcs seront bidirectionnels ou
unidirectionnels, selon que le déplacement considéré est réversible ou pas (certains
déplacements de pierres, par exemple l’adossement d’un pierre à un mur, ne sont pas
réversibles). (voir figure 17).

19
Fig. 16 : Le domaine de visiblilité du pousseur, V, est représenté
en gris sur ces deux exemples.

Fig. 17 : Le graphe A (domaine admissible) pour deux pierres, A et B

20
Avec ces notions on fait les remarques suivantes, qui vont faciliter notre travail dans ce
qui suit :

- les états qui ont les mêmes positions de pierres et des domaines de visibilité identiques
peuvent être considérées identiques : le passage entre tels états va se faire uniquement par
des mouvements du pousseur, sans aucun déplacement de pierre, et sera donc
parfaitement réversible Cette observation permet de réduire considérablement le nombre
d’états à explorer.

- pour chaque pierre, le domaine admissible peut être calculé une seule fois, au début de
l’analyse ; pour éviter tout blocage simple il sera suffisant, par la suite, de ne pousser
aucune pierre hors de son domaine admissible.

Pour ce qui suit on va donc identifier les états par l’ensemble des positions des pierres et
par le domaine visible du pousseur ; les états ainsi définis vont constituer les nœuds du
graphe des états, dont les arcs seront les déplacements de pierres.

Bien que le modèle théorique proposé plus haut soit assez rigoureux, l’identification
automatique des blocages n’est pas simple ; ce sont surtout les blocages d’ordre
supérieur, que le joueur humain apprend assez vite à reconnaître de manière intuitive, qui
sont difficiles à identifier à priori : pour les identifier il serait nécessaire d’explorer
intégralement l’espace des ‘sous états’ du groupe des pierres impliquées Pour
l’identification des blocages d’ordre 0 il existe néanmoins une méthode assez efficace et
élégante qui fonctionne par l’élimination des pierres non bloquées L’algorithme est
constitué des pas suivants :
1) identification des pierres qui peuvent être déplacées dans l’état donne ;
2) élimination des pierres identifiées au pas 1;
3) calcul du nouveau domaine de visibilité ;
4) les pas 1-3 sont répétés jusqu'à ce qu’on ne peut plus éliminer aucune pierre ;
5) à ce moment, deux situations peuvent apparaître :
- s’il reste encore des pierres dans le labyrinthe, on a un blocage d’ordre 0 qui
implique les pierres qui sont restées dans le labyrinthe (ou une partie d’entre
elles);
- s’il n’y a plus aucune pierre dans le labyrinthe, la situation n’est pas un
blocage d’ordre 0 (bien qu’elle puisse être un blocage d’ordre supérieur)
Les pas de l’algorithme, pour un cas simple, sont indiques dans la figure 18.

Si l’application de cet algorithme indique qu’on a affaire à un blocage, on a besoin d’un


pas supplémentaire d’identification des pierres qui produisent le blocage ; on peut avoir
des situations comme dans la figure 19, où les pierres impliquées dans le blocage sont A
et B alors que la pierre C, bien que pas éliminée par l’algorithme, n’est pas réellement
impliquée dans le blocage – elle n’en est qu’une ‘victime’. Il faut aussi tenir compte du
fait qu’une pierre qui se trouve sur une cible peut être coincée sans que la situation
résultante en soit, pour autant, bloquée.

21
Fig. 18 : L’algorithme d’élimination pour l’identification des blocages. A chaque pas on
élimine les pierres qui peuvent être déplacées (marquées d’un astérisque). Les pierres
qui nous restent à la fin sont celles qui sont coincées.

Fig. 19 : Les pierres coincées sont A et B ; la pierre C n’intervient pas


dans le blocage, elle n’en est qu’une « victime ».

L’algorithme proposé résout le problème des blocages proprement dits (d’ordre 0) ; il


reste néanmoins à résoudre le problème des blocages d’ordre supérieur. Comme on l’a
montré plus haut, il convient d’identifier ces blocages aussi tôt que possible, afin d’éviter
de perdre du temps en explorant des branches du graphe d’états qui ne mènent pas à la

22
solution. Pour construire un algorithme d’identification des blocages d’ordre supérieur,
on part de la définition (2) donnée plus haut et on en conçoit l’approche suivante : on
note tous les blocages qu’on identifie dans un ‘tableau de blocages’ et on interdit tous les
mouvements qui conduisent à des configurations de ce tableau. L’algorithme donné plus
haut va donc être modifie comme suit :
1) identification des pierres qui peuvent être déplacées sans conduire a une
configuration du tableau de blocages ;
2) élimination des pierres identifiées au pas 1 ;
3) calcul du nouveau domaine de visibilité ;
4) les pas 1-3 sont répétés jusqu'à ce qu’on ne peut plus éliminer aucune pierre ;
5) à ce moment, deux situations peuvent apparaître :
- s’il reste des pierres dans le labyrinthe, on a un blocage (d’ordre 0 ou d’ordre
supérieur) qui implique les pierres restées dans le labyrinthe ou une partie
d’entre elles. On détermine quelles sont les pierres impliquées et on ajoute le
blocage ainsi identifié au tableau de blocages ;
- s’il ne reste pas de pierres dans le labyrinthe, la situation ne peut pas être,
pour le moment, identifiée comme blocage (bien que, plus tard, elle pourrait
s’avérer un blocage).

Cet algorithme va donc identifier tous les blocages, mais ‘à rebours’, en commençant par
les blocages d’ordre 0 pour monter ensuite aux ordres supérieurs. Il en résulte un mode
d’exploration du graphe d’états comme suit : on avance, en explorant des nouveaux états,
jusqu'à l’atteinte d’un état bloqué ; lorsqu’un blocage est identifié, on le note dans le
tableau de blocages et on revient sur nos pas, en re-examinant les états déjà visites afin
d’identifier les blocages d’ordre supérieur, qui sont, eux aussi, ajoutés au tableau de
blocages. (voir figures 20 et 21) Les blocages sont représentés dans une manière similaire
aux états : en indiquant les positions des pierres et le domaine visible du pousseur.

Fig. 20 : Identification des blocages d’ordre supérieur. On a exploré les situations dans
l’ordre 1, 2, 3, 4, 5, 6. Après l’atteinte d’un blocage d’ordre 0 dans la situation 6, on
revient sur nos pas afin d’identifier les blocages d’ordre supérieur. Les situations 5 et 4
seront, elles aussi, identifiées comme des blocages.

23
Fig. 21 : Evolution du tableau de blocages au cours de l’exploration de la fig. 20 :
1 - après état 6 ; 2 - après retour à l’état 5 ; 3 – après retour à l’état 4.

24
Une optimisation possible de l’algorithme serait la suivante : au lieu de commencer avec
le tableau de blocages vide, on pourrait l’initialiser avec un nombre de blocages simples,
typiques, qu’on peut calculer à priori. Ceci réduirait le temps passé par l’algorithme à
explorer des états qui vont être ensuite identifiés comme blocages.

Fig. 22 : Exemples d’enclaves. En gris, les cellules inaccessibles au pousseur.


Remarques que toutes ces situations sont des blocages d’ordre supérieur.

L’algorithme proposé résout le problème de l’identification des situations bloquées, qui


pourront être ensuite évitées. L’inconvénient est que les blocages d’ordre supérieur ne
seront identifiés qu’après avoir réalisé une exploration en profondeur jusqu'à l’atteinte
d’un blocage proprement dit Cet inconvénient est assez gênant car, en pratique,
l’exploration des états n’est pas exhaustive ; on n’explore que les successeurs les plus
intéressants d’un état donné et il peut bien arriver qu’on ne découvre pas à temps un
blocage d’ordre supérieur. Pour résultat, des états qui sont réellement des blocages vont
être traités comme des états utiles, et on va perdre beaucoup de temps à explorer des
branches du graphe d’états qui ne mènent nulle part. Il serait donc très utile, lors de
l’exploration d’un état, d’entamer d’abord les mouvements qui ont une plus grande
probabilité de mener à un blocage – cela permettrait d’identifier les blocages aussi tôt que
possible. Au premier regard, il parait qu’il n’y ait aucun moyen d’indiquer quels
mouvements ont une plus grande probabilité de bloquer. Mais une analyse de quelques
situations typiques mène à un critère heuristique d’identification des situation plus
susceptibles de mener à un blocage : on remarque que les blocages d’ordre supérieur sont
associés aux ‘enclaves’ – domaines bornés par des murs et des pierres, ou le pousseur ne
peut pénétrer qu’en déplaçant une pierre (voir figure 22). Intuitivement, l’explication est

25
claire : les blocages d’ordre 0 sont généralement produits par des pierres qui se trouvent
en contact ; si des pierres ne se trouvant pas en contact ne forment pas une enclave, c’est
que le pousseur peut se glisser entre elles, et peut donc les séparer, les éloigner l’une de
l’autre, ce qui les empêche de se coincer. Au contraire, les pierres qui déterminent des
enclaves sont susceptibles à ne pouvoir pas être séparées et éloignées l’une de l’autre ; il
est possible que l’enclave ne puisse pas être ‘ouverte’, et le pousseur sera forcé de
pousser les pierres l’une contre l’autre, avec une grande probabilité de les coincer. Bien
que pas rigoureux, ce critère suggère une approche qui maximise les chances d’identifier
les blocages : dans tout état, on commence par explorer les mouvements qui ont tendance
à ‘restreindre’ les enclaves.

4.1.2. Fixer les buts

Le mécanisme décrit plus haut permet l’identification des situations ‘sans espoir’ et
permet éviter de perdre du temps à explorer de telles situations. Ce critère ‘défensif’ est
néanmoins insuffisant pour résoudre le puzzle. Une exploration arbitraire dont l’unique
restriction serait d’éviter les blocages aurait besoin d’un temps énorme pour arriver à la
solution. On a donc besoin d’un critère supplémentaire, ‘offensif’, qui puisse diriger la
recherche vers les états qui nous rapprochent de l’état résolu.

Du point de vue mathématique, la définition de l’état résolu est simple : il s'agit d’un état
Es pour lequel Ps = C, c'est-à-dire toutes les pierres se trouvent sur des cibles. Néanmoins,
quand il s’agit de déterminer les pas à suivre pour arriver à la solution, l’analyse se
complique : même pour les puzzles les plus simples, le remplissage des cibles doit se
faire dans le bon ordre, afin éviter qu’une ou plusieurs cibles ne deviennent inaccessibles
(voir figure 23). Pour les puzzles plus compliqués, les cibles ne forment plus des zones
rectangulaires compactes, mais des domaines de forme plus complexe, qui doivent être
occupés dans un ordre qui est loin être évident (voir figures 24 et 25).

Fig. 23 : Un exemple de succession très simple de finalisations

26
Fig. 24 : Un situation plus complexe, où le remplissage des cibles ne s’est pas effectué
dans le bon ordre ; le résultat est qu’une des cibles est devenue inaccessible.

Fig. 25 : Le remplissage correct de la situation de la figure 24.

Ici aussi, on commence par l’analyse de la pensée d’un joueur humain. On observe que,
lors du choix des cibles à occuper, le critère suivi intuitivement est l’identification des
cibles qui peuvent être occupées sans nuire à l’accessibilité des cibles encore vides. Une
telle cible – dont l’occupation laisse accessibles les cibles encore vides – peut être
occupée sans qu’on doive s’en soucier ensuite ; elle devient effectivement ‘hors jeu’,
n’intervenant plus dans l’analyse ultérieure. Dans ce qui suit on va appeler finalisation
cette opération par laquelle une cible est occupée par une pierre, de manière à ne plus
influencer l’analyse ultérieure. Par la finalisation on réduit donc la complexité du puzzle
à analyser – en passant de ‘n’ à ‘n-1’ pierres. La finalisation d’une cible peut donc être
modelée par le remplacement de la cible en question par un mur, comme le montre la
figure 26.

27
Fig. 26 : La finalisation d’une pierre nous permet de réduire le cas d’un
puzzle à ‘n’ pierres au cas d’un puzzle a ‘n-1’ pierres. Du point de vue de
l’analyse, la pierre finalisée peut être assimilée à un mur.

Dans tout état du puzzle, tel qu’on l’a défini plus haut, on va désormais faire la différence
entre les pierres finalisées (qu’on peut considérer sur leur position définitive et qu’il ne
faut plus déplacer) et les pierres actives (ne se trouvant pas encore sur leur position
définitive et donc censées à être déplacées). Notons au passage qu’une pierre finalisée
doit nécessairement se trouver sur une cible ; la réciproque n’est évidemment pas
valable : une pierre se trouvant sur une cible peut avoir encore besoin de déplacements
avant d’arriver à sa position finale.

Avec ces définitions, on peut conclure que, dans un état quelconque, une cible va pouvoir
être finalisée (occupée par une pierre et ignorée par la suite dans l’analyse) si après son
occupation avec une pierre, toutes les cibles non encore finalisées sont encore
accessibles pour au moins une pierre chacune. Si, dans un état quelconque, on identifie
une telle cible et on trouve une pierre qui puisse y être déplacée, alors, en déplaçant la
pierre sur la cible on va ‘avancer’ vers la solution du puzzle.

Comme exemple, considérons la situation présentée dans les figures 24 et 25 :on observe
aisément qu’on n’y peut pas finaliser aucune des deux cibles situées à gauche sur la
première ligne, dont l’occupation rendrait inaccessible la cible située derrière le mur
central.

Il faut remarquer que – pour les puzzles plus compliqués – on peut rencontrer des états où
il n’existe aucune cible qui remplisse la condition ci-dessus. Dans de tels cas, des
déplacements ‘préparatoires’ seront nécessaires avant qu’une cible puisse être finalisée
De telles situations ne se trouvent quant même pas dans les premiers niveaux du jeu, et
elles ne sont pas couvertes par mon algorithme (bien qu’une extension de l’algorithme
proposé soit possible).

28
Dans tout état, les finalisations trouvées peuvent être utilisées pour diriger la recherche
dans le graphe des états : parmi tous les mouvements possibles dans un état donné, on va
toujours préférer ceux qui mènent à une finalisation. Le problème qui se pose est donc
l’identification, pour un état donné, de toutes les cibles qui puissent être finalisées.

Une première méthode est de procéder à rebours, à partir de état final désiré: on considéré
le puzzle résolu – avec toutes les cibles occupées – et on commence à ‘éliminer une à une
les pierres accessibles pour le pousseur, en numérotant les cibles à fur et à mesure de leur
‘libération’ : les cibles dont on a éliminé les premières pierres auront le numéro 1, les
suivantes auront le numéro 2 et ainsi de suite jusqu'à l’élimination de toutes les pierres.
(voir figure 27). Une succession possible de finalisations sera donc dans l’ordre inverse
des numéros des cibles : le cibles de numéro maximum seront les premières à être
finalisées et les cibles de numéro minimum seront les dernières à être finalisées Dans
notre exemple, on commence par la cibles ayant le numéro 4 (celle située derrière le mur
central), puis on continue par les cibles au numéro 3 (celle de la colonne de gauche) etc.
Cette approche est suffisante pour les puzzles les plus simples, ou les cibles forment
généralement un bloc rectangulaire compact. L’inconvénient de cette méthode est de
n’indiquer qu’une partie des successions de finalisation possibles ; dans notre cas, par
exemple, cet algorithme nous a indique qu’il faut commencer par la finalisation de la
cible située derrière le mur central, mais il est évident qu’on aurait pu commencer aussi
par la finalisation de la cible située en bas a gauche. Pour les puzzles plus compliqués, il
existe des cas ou l’on a réellement besoin de toutes les successions possibles de
finalisations.

Fig. 27 : L’algorithme de détermination de l’ordre de remplissage des cibles par


élimination : on commence avec toutes les cibles occupées et, à chaque pas, on élimine
toutes les pierres situées sur des cibles accessibles pour le pousseur. On numérote à
chaque pas toutes les cellules libérées. Le remplissage se fera dans l’ordre décroissant
des numéros des cibles.

Une autre méthode serait la suivante : on commence encore avec toutes les cibles
occupées ; pour chaque cible accessible, on en enlève la pierre, et on note les cibles qui
deviennent accessibles par cette élimination. En répétant de manière récursive
l’opération, on obtient un ‘arbre de finalisations’ qu’on peut ensuite parcourir en sens
inverse pour obtenir toutes les successions possibles de remplissage des cibles (voir
figure 28 avec un exemple d’application de cette méthode à un groupe de 2x2 cibles).
Cette méthode est exhaustive – elle indique toutes les voies possibles de remplissage –
mais elle a l’inconvénient de la taille énorme de l’arbre obtenu : pour le cas simple d’une
domaine rectangulaire de cibles, la profondeur de l’arbre sera égale au nombre de cibles

29
et la complexité de l’arbre va augmenter par une progression factorielle. Pour un domaine
de 2x2 cibles on va avoir 6 voies possibles de remplissage (voir figure 28) alors que pour
un domaine de 3x2 cibles on a déjà plus de 100 variantes possibles. Pour des dimensions
plus grandes, la complexité devient bientôt impossible à gérer.

Fig. 28 : Construction de l’arbre de remplissage pour un domaine de 2x2 cibles.


(a) domaine de cibles étudié ; (b) numérotation des cibles ; (c) arbre de remplissage
résultant. On observe que, pour un domaine très restreint de cibles, on a déjà 6 voies
possibles de remplissage. (2-4-1-3 ; 2-1-4-3 ; 1-2-4-3 ; 1-3-2-4 ; 2-1-3-4 ; 1-2-3-4)

On remarque donc que les différentes voies de remplissage des cibles forment un arbre.
Dans tout état il y aura un nombre de cibles qui puissent être finalisées ; la finalisation de
chacune de ces cibles permettra la finalisation d’un nombre additionnel de cibles, et ainsi
de suite. Comme on l’a vu, la complexité de cet arbre peut être très grande, on est donc
intéressés de trouver une solution qui indique toutes les finalisations possibles dans un
état donné, sans pour autant construire l’arbre entier. Cela revient à une exploration
exhaustive de toutes les ramifications de l’arbre sur une profondeur d’un seul pas, suivie
par une exploration non exhaustive pour les pas suivants. Cette méthode permet
d’identifier tous les finalisations possible dans un état donné, sans devoir calculer toutes
les successions possibles de finalisations (qui peuvent être très nombreuses). On emploie
donc conçu l’algorithme suivant, qui vérifie si une certaine cible peut être finalisée dans
un état donné :

30
1) on commence avec toutes les cibles occupées ;

2) la pierre située sur la cible a vérifie et toutes les pierres finalisées déjà sont
considérées ‘figées’ ; on ne va pas essayer de les éliminer par la suite ;

3) on élimine une a une les pierres situées sur les cibles, sauf les pierres
considérées figées aux pas 2. Un pierre est éliminée si la cible sur laquelle elle se trouve
est accessible pour au moins une pierre non finalisée présente dans le labyrinthe ;

4) on répète le pas 3 jusqu'à ce qu’on ne peut plus éliminer aucune pierre.

5) on examine la situation des cibles après les éliminations :


- si les seules cibles encore occupées sont celles considérées ‘figées’ au
pas 2, alors la cible à vérifier est une finalisation possible ;
- sinon, la cible à vérifier ne peut pas être finalisée – son occupation
rendrait inaccessibles d’autres cibles.

Cette méthode nous permet de déterminer, dans chaque état, toutes les finalisations
possibles ; sans être parfait ou exhaustif, cet algorithme nous permet de résoudre un
nombre significatif de puzzles. Les niveaux avec des configurations plus compliquées des
cibles auront néanmoins besoin de améliorations supplémentaires de l’algorithme.

31
4.2. Implémentation de l’application

Dans les paragraphes suivants on va présenter la façon dont les principes théoriques
introduits plus haut sont mis à l’œuvre. On ne s’occupe que de la partie de l’application
qui réalise effectivement l’analyse du puzzle, sans insister sur la partie d’interface
utilisateur – qui est très typique : une interface graphique en langage Java, utilisant le
module AWT (Abstract Window Toolkit).

4.2.1 Représentation des données

L’élément de base dans la représentation du labyrinthe sera la classe MazeCell ; à toute


cellule accessible de l’intérieur du labyrinthe on associe un objet du type MazeCell. Cet
objet va contenir :

1) Des informations sur la topologie du labyrinthe – des références (up, down, left,
right) aux objets MazeCell correspondant aux cellules voisines de la cellule
considérée. Si la cellule considérée est bordée dans une certaine direction par un
mur, la référence correspondante aura la valeur null. L’ensemble des objets
MazeCell avec leurs références up, down, left, right vont constituer, au fait, le
graphe M, celui qui correspond au labyrinthe entier (les cellules sont des nœuds
dans ce graphe, et les références aux cellules voisines sont des arcs du graphe).
Comme la structure du labyrinthe est fixe, tous les objets MazeCell et leurs
références topologiques seront donc crées au début de l’analyse et resteront
inchangés par la suite. MazeCell contient aussi une variable booléenne target qui
indique si la cellule considérée est une cible. Toutes ces variables restent
inchangées pendant l’analyse.

2) Les coordonnées (numériques) de la cellule considérée ; ces coordonnées ne sont


pas employées dans l’analyse, mais elles sont utiles pour l’affichage des
informations pour l’utilisateur.

3) Des informations sur la situation de la cellule considérée dans un état donne du


labyrinthe : la variable booléenne stone va indiquer si la cellule est occupée ou
non par une pierre ; pour les cibles, la variable booléenne done va indiquer si la
cible est ‘finalisée’ (il s’agit de finalisation au sens de l’analyse du labyrinthe,
voir section précédente, pas de finalisation au sens du langage Java). Le valeurs
de ces variables vont changer pendant l’analyse, selon l’état qu’on est en train
d’examiner a un certain moment.

4) Des informations sur les possibilités de mouvement d’une pierre qui se trouverait
sur cette cellule : chaque objet MazeCell contient une référence à un objet du type
MazeGraph (voir plus bas) correspondant au graphe A d’une pierre qui se
trouverait dans la cellule considérée (cette référence a la valeur null si la cellule
considérée est une cellule interdite). Le graphes A de cellules restent inchangés
pendant l’analyse.

32
5) Des informations sur les blocages dans lesquels intervient la cellule
correspondante : tout MazeCell contient une liste de références à des objets
DeadEnd (voir plus bas), correspondant à tous les blocages qui font intervenir
une pierre sur la cellule considérée. Ce mécanisme permet de vérifier très vite si
le déplacement d’une pierre sur une certaine cellule mène à un blocage déjà
connu: il suffit de faire la comparaison entre l’état résultant et tous les blocages
déjà connus où intervient la cellule qu’on occupe par une pierre. Si on trouve un
blocage qui correspond aux pierres qu’on trouve dans le labyrinthe, c’est qu’on
est tombés sur un blocage connu.

Les figures 29 et 30 montrent la correspondance entre un labyrinthe et sa représentation


au moyen des objets MazeCell. Pour ne pas compliquer trop la figure, on a choisi un
labyrinthe des plus simples et on n’a présenté qu’une partie des membres des objets
MazeCell : on n’a pas inclus sur la figure ni le graphe A contenu dans chaque objet
MazeCell, ni la collection de blocages associée à chaque cellule.

Les objets MazeCell peuvent être arrangés en des graphes au moyen de la classe
auxiliaire MazeNode : celle-ci contient, d’une part, une référence à un objet MazeCell et,
d’autre part, des références up, down, left, right correspondant aux cellules voisines du
graphe. A tout objet MazeCell peuvent correspondre plusieurs objets MazeNode – un
pour chaque graphe qui contient la cellule considérée. Notons que les graphes dont il est
question ici sont des graphes utilisés pour la description du labyrinthe ; leur structure sera
donc semblable à celle du labyrinthe – toute cellule en aura au maximum quatre cellules
voisines.

De tels graphes, utiles pour représenter le domaines qu’on a définis dans la partie
théorique (domaine visible V, domaine admissible A) seront représentés au moyen de la
classe MazeGraph, qui est une collection de références à des objets MazeNode
correspondant chacun à une cellule incluse dans le graphe. La classe MazeGraph
contient aussi des méthodes pour ajouter une cellule à un graphe, pour ajouter des liens
(arcs) entre cellules et des méthodes pour vérifier si une cellule appartient à un graphe
donné. La classe MazeGraph permet aussi de vérifier si un graphe réalise ou pas un lien
entre deux cellules quelconques.

Fig. 29 : Labyrinthe simple et son graphe M correspondant. Voir dans la


figure suivante sa représentation au moyen des objets MazeCell

33
MazeCell (x==1, y==1) MazeCell (x==2, y==1) MazeCell (x==3, y==1)
done == false done == false done == false
stone == false stone == false stone == false
target == false target == false target == false
up == null up == null up == null
right right right == null
left == null left left
down down down

MazeCell (x==1, y==2) MazeCell (x==2, y==2) MazeCell (x==3, y==2)


done == false done == false done == false
stone == false stone == true stone == true
target == false target == false target == false
up up up
right right right == null
left == null left left
down == null down == null down

MazeCell (x==3, y==3)


done == false
stone == false
target == false
up
right == null
left == null
down

MazeCell (x==1, y==4) MazeCell (x==2, y==4) MazeCell (x==3, y==4) MazeCell (x==4, y==4)
done == false done == false done == false done == false
stone == false stone == false stone == false stone == false
target == true target == false target == false target == false
up == null up == null up up == null
right right right right == null
left == null left left left
down down down down

MazeCell (x==1, y==5) MazeCell (x==2, y==5) MazeCell (x==3, y==5) MazeCell (x==4, y==5)
done == false done == false done == false done == false
stone == false stone == false stone == false stone == false
target == false target == false target == false target == false
up up up up
right right right right == null
left == null left left left
down down == null down down == null

MazeCell (x==1, y==6) MazeCell (x==3, y==6)


done == false done == false
stone == false stone == false
target == true target == false
up up
right == null right == null
left == null left == null
down down

MazeCell (x==1, y==7) MazeCell (x==2, y==7) MazeCell (x==3, y==7)


done == false done == false done == false
stone == false stone == false stone == false
target == false target == false target == false
up up == null up
right right right == null
left == null left left
down == null down == null down == null

Fig. 30 : Représentation interne, au moyen des objets MazeCell, du labyrinthe


de la figure 29 dans son état initial. Les valeurs en bleu varient selon l’état examiné, les
autres sont constantes. Notons que chaque objet MazeCell contient aussi des références
au graphe A et à la collection de blocages où intervient la cellule considerée.

34
Ces trois classes – MazeCell, MazeGraph et la classe auxiliaire MazeNode – sont
utilisées pour décrire l’aspect statique, topologique du problème : la forme du labyrinthe
et les domaines admissibles des pierres.

L’aspect ‘dynamique’ du problème est représenté au moyen des classes MazeState et


StoneMove.

Les objets de la classe MazeState vont chacun correspondre à un état du puzzle. Comme
on l’a vu plus haut, les états sont indiqués par les positions des pierres (un vecteur de
références aux objets MazeCell correspondant aux cellules occupées) et par le domaine
visible du pousseur, V (celui est représenté comme référence à la première cellule du
domaine, c'est-à-dire la cellule située le plus en haut et à gauche dans le domaine – voir
figure 31). MazeState va contenir aussi des informations supplémentaires sur le puzzle
dans l'état considéré : le pierres actives et celles finalisées ; la liste in des mouvements
qui conduisent à l'état considéré ; la liste out des mouvements possibles dans état
considéré ; et la liste des cibles qui peuvent être finalisées dans état considéré. Pour
l’orientation de la recherche, MazeState va aussi contenir une variable booléenne
indiquant si l’état a été déjà visite, et un référence a un objet DeadEnd pour indiquer si
l’état considéré est bloqué ou pas (si l’état n’est pas bloqué, cette référence aura la valeur
null).

Fig. 31 : Les quatre situations présentées ont les mêmes positions des pierres et le même
domaine de visibilité du pousseur (en gris) – elles seront donc représentées par le
programme comme un même état. Par convention, le programme utilise la
représentation (d), où le pousseur se trouve sur la première cellule de la première ligne
du domaine de visibilité.

Les mouvements des pierres – c'est-à-dire les transitions d’un état du puzzle à un autre –
sont représentés par des objets StoneMove. Ces objets contiennent une paire de
références aux objets MazeCell correspondant à la cellule de départ et à la cellule
destination de la pierre et une paire de références aux objets MazeState correspondant

35
à l’état de départ et à l’état d’arrivée. On observe que la classe StoneMove peut être
utilisée pas seulement pour représenter des déplacements entre cellules juxtaposées, mais
aussi pour représenter des successions plus longues de déplacements d’une certaine
pierre.

Les blocages, tels qu’on les a définis plus haut, sont représentés par des objets DeadEnd.
Ces objets ont une structure semblable aux objets MazeState : ils contiennent un vecteur
de références à des objets MazeCell, correspondant aux cellules occupées qui
interviennent dans le blocage, et une représentation du domaine visible du pousseur
(toujours comme référence a l’objet MazeCell correspondant a la première cellule du
domaine). Les autres membres de la classe MazeState ne sont néanmoins plus présents
dans DeadEnd. Les objets DeadEnd seront référencés, d’une part, par les objets
MazeState correspondant aux états bloques, et, d’autre part, par les objets MazeCell –
comme on l’a montré plus haut, tout objet MazeCell contient la liste des objets DeadEnd
dans lesquels il intervient. La classe DeadEnd va contenir aussi une méthode matches()
qui réalise la comparaison d’une configuration bloquée avec la configuration actuelle du
labyrinthe. Cela permet d’identifier rapidement si le déplacement d’une pierre mène à un
blocage : il suffit de comparer la configuration résultant après le déplacement avec
chacun des blocages dans lesquels intervient la cellule sur laquelle on a déplacé la pierre
en question. (On observe que ce type de fonctionnement constitue une espèce de mémoire
associative, où chaque cellule du labyrinthe ‘connaît’ les blocages dans lesquels elle
intervient ; ce mécanisme imite au fait le fonctionnement de l’intelligence humaine qui
apprend, à fur et à mesure de l’analyse du puzzle, à reconnaître des blocages de plus en
plus complexes.)

Les relations entre les classes MazeState et StoneMove, pour une petite portion initiale
de l’analyse d’un labyrinthe simple, sont représentées dans la figure 32.

L’analyse d’un puzzle consiste en l’exploration des états auxquels on arrive à partir de
l’état initial par des successions de déplacements de pierres. A fur et à mesure de cette
exploration, le programme va créer des objets MazeState, un pour chaque nouvel état
exploré. L’organisation et l’ordonnancement de ces objets se fait de deux manières :

- premièrement, les objets MazeState et les liaisons qui s’établissent entre eux au moyen
des objets StoneMove vont décrire une portion du graphe des états du puzzle. Bien
évidemment, le graphe ne sera décrit en totalité que dans les cas extrêmement simples, où
le nombre d’états est petit. Dans les cas réels, on n’arrive à décrire qu’une portion
relativement restreinte du graphe des états – idéalement, ce sera une portion qui contient
un état résolu. Du point de vue de la définition mathématique du graphe des états, les
objets MazeState vont constituer des nœuds alors que les objets StoneMove vont
constituer des arcs.

- deuxièmement, le programme maintient, au moyen de la classe StateMap, une


collection linéaire des états explorés. Le but de cette collection est l’identification des
états dupliqués – lorsque l’exploration des états nous mène à un état déjà exploré, ce
mécanisme va nous permettre d’éviter de créer deux objets MazeState identiques. De

36
plus, ce mécanisme assure le fonctionnement correcte de l’algorithme d’exploration
lorsqu’il apparaît des cycles (successions d’états qu’on peut parcourir indéfiniment, voir
figure 33). La classe StateMap est utile aussi car elle permet d’obtenir des statistiques
comme le nombre total d’états explorés.

de (6,3) à (5,3)

de (5,3) à (4,3) de (6,2) à (6,3)

de (4,3) à (3,3) de (6,2) à (6,3)

de (3,3) à (2,3) de (6,2) à (6,3)


de (6,3) à (5,3)
de (3,3) à (3,2) de (3,3) à (4,3)

Fig. 32 : Graphe formé par les objets MazeState (en couleur) et les objets StoneMove
(petits rectangles), pour quelques pas d’exploration d’un puzzle simple.

37
de (6,2) à (6,3)

de (3,3) à (4,3)

de (3,3) à (3,2)

de (3,3) à (2,3)

de (2,3) à (3,3)

de (6,2) à (6,3)

Fig. 33 : Un ‘cycle’ formé par deux états entre lesquels on peut passer dans les deux
sens. Notons qu’il peut exister des cycles beaucoup plus complexes, faisant intervenir
plusieurs états. De tels cycles imposent le besoin de identifier les états ‘dupliqués’.

38
4.2.2 L’algorithme de recherche de la solution

La recherche proprement dite de la solution se réalise dans la classe Analyzer. On y


effectue une exploration continuelle du graphe d’états, en évitant les états bloques et en
cherchant les états qui paraissent les plus proches de la solution.

La méthode Analyzer.thoroughAnalysis(), qui reçoit comme argument un objet


MazeState correspondant à un état initial, va construire le graphe des états ayant pour
origine l’état initial indiqué. Cette méthode va maintenir une liste frontLine des objets
MazeState correspondant aux états encore inexplorés – c’est la ‘frontière’ du graphe déjà
exploré, que la méthode va tâcher continuellement d’étendre. Au début, frontLine
contient un seul état – l’état initial indiqué. Le fonctionnement de la méthode
thoroughAnalysis() va consister à parcourir continuellement les pas suivants :

1) identification de l'état suivant à explorer, par l’extraction d’un objet MazeState


du commencement de la liste frontLine ;
2) exploration de l’état choisi au pas 1, c'est-à-dire :
a. Vérifie si état est bloqué ou pas ; s’il est bloqué, on vérifie de manière
récursive les états précédents, pour y identifier les éventuels blocages
d’ordre supérieur. Les états bloqués sont éliminés de frontLine afin de ne
plus perdre du temps avec leur exploration. Si l’état est bloqué,
thoroughAnalysis() va sauter au pas 1, pour examiner l’état suivant
contenu dans frontLine ;
b. Détermine tous les mouvements possibles dans l’état donne et, pour
chaque mouvement, construit l’état résultant Tout état qui n’est pas
identique à un état déjà exploré sera ajouté à la fin de frontLine, afin
d’être exploré ultérieurement ;
c. Détermine toutes les finalisations possibles dans l’état donné,
conformément à l’algorithme de détermination des finalisations (voir plus
haut).

La méthode thoroughAnalysis() va donc parcourir continuellement la frontière du


graphe des états déjà explorés, en l’étendant à chaque itération. Cela va durer jusqu'à ce
que l’une des conditions suivantes soit remplie :

1) on arrive à un état résolu ; dans ce cas l’analyse s’arrête, le graphe des états sera
parcouru en sens inverse (de l’état final vers l’état initial) afin de construire la
succession de mouvements gagnante, qui est ensuite présentée à l’utilisateur ;

2) dans l’un des états explorés on découvre qu’une pierre peut être ramenée, par une
succession de déplacements, sur une des cibles qui peuvent être finalisées La
pierre sera alors déplacée sur la cible en question, la cible sera marquée comme
finalisée et thoroughAnalysis() va s’appeler de manière récursive avec l’état
résultant comme nouveau état initial. Cela signifie effectivement qu’on s’est
débarrassé d’une pierre, qu’on a réussi à placer dans une position finale, et qu’on

39
s’est rapproché de la solution. On saute de cette manière une portion du graphe
des états, et on reprend l’analyse à partir d’un état plus proche de la solution ;

3) la liste frontLine devient vide. Cela signifie qu’on a exploré tous les
développements possibles de l’état initial pour lequel on a appellé
thoroughAnalysis(). Dans ce cas thoroughAnalysis() va retourner une
indication d’échec, ce qui a pour effet :

a. Si on se trouve dans un appel imbriqué de thoroughAnalysis() (suite à


une finalisation), l’échec signifie que la finalisation réalisée n’est pas
correcte ou que l’état ou l’on a réalise la finalisation est bloqué ; on
reprend l’analyse à partir de l’état précédant la finalisation ;

b. Si on se trouve dans l’appel originaire, non imbriqué, de


thoroughAnalysis() (c'est-à-dire l’appel pour l’état initial du puzzle),
l’échec signifie que le puzzle n’a pas de solution.

On réalise donc deux types d’exploration : une exploration exhaustive, pas à pas, tant
qu’on n’a aucune finalisation accessible (pour un joueur humain cela va correspondre a la
phase de déblocage du puzzle) ; et une exploration en profondeur, lorsqu’on réalise la
finalisation d’une paire pierre-cible, en sautant certains mouvements intermédiaires.
Finalement, le graphe des états (tant qu’on en a exploré) aura l’aspect d’un ‘archipel’,
avec des portions explorées exhaustivement branchées par des ‘raccourcis’ qui vont
correspondre aux finalisations. (XXX image).

L’identification d’un blocage aura comme effet, du point de vue de l’exploration, un


‘reflux’ : on revient sur nos pas, en invalidant certains états déjà explorés et en cherchant
des mouvements alternatifs. Pour reprendre la comparaison du paragraphe précédent, ce
sera une ‘renonciation’ à certaines ‘îles’ déjà explorées.

40
5. Conclusions

6. Références

41

Vous aimerez peut-être aussi