Documente Academic
Documente Profesional
Documente Cultură
Tristan Cazenave
Intelligence
artificielle
Une approche ludique
Tristan CAZENAVE
Professeur au LAMSADE, université Paris-Dauphine
ISBN 978-2-7298-6408-8
© Ellipses Édition Marketing S.A., 2011
32, rue Bargue 75740 Paris cedex 15
Le Code de la propriélé in1ellec1Uelle n'au1orisan1, aux lennes de l'article L. 122-5.2° et 3°a), d'une
pan. que les «copies ou reproductions strictement réservées à l'usage privé du copiste et non
destinées à une utilisalion collective», et d'autre part, que les analyses et les counes citalions dans
un but d'exemple el d'illus1ration, «route représenta1ion ou reproduction intégrale ou partielle faile
sans le consentement de l'auteur ou de ses ayants droit ou ayants cause est illicite>) (art. L. 122-4).
Cette représentation ou reproduction, par quelque procédé que ce soit constituerait une contrefaçon
sanc1ionnée par les articles L. 335-2 el suivanlS du Code de la propriélé in1ellec1uelle.
www.editions-ellipses.fr
Avant-propos
Cet ouvrage traite d'intelligence artificielle et de jeux. Il s ' adresse aux étudiants, aux
élèves d'écoles d' ingénieurs, aux enseignants, aux chercheurs et à tous ceux qui sou
haitent s ' initier ou se perfectionner en intelligence artificielle pour les jeux. Les connais
sances requises pour aborder ce livre sont les notions de base d' informatique enseignées
en premier cycle : savoir lire et écrire un algorithme et savoir l ' implémenter dans un lan
gage informatique (pour ce livre le C++ ).
Le livre commence par traiter les algorithmes pour les jeux à deux joueurs, puis les
algorithmes pour les jeux à un joueur et finit par les approches alternatives.
Les sept premiers chapitres portent sur les algorithmes pour les jeux à deux joueurs.
Le premier algorithme traité est aussi le plus classique, à savoir ! ' Alpha-Bêta. Le pre
mier chapitre explique ce qu ' est une fonction d 'évaluation. Il prend comme exemple le
jeu du Virus qui est ensuite réutilisé pour les chapitres deux et trois. Le deuxième cha
pitre montre comment utiliser cette fonction d'évaluation dans un algorithme Alpha-Bêta
qui prévoit plusieurs coups de suite. De nombreuses heuristiques améliorant l' Alpha-Bêta
sont aussi décrites. Le troisième chapitre explique l ' utilisation d' une table de transposition
qui est l ' amélioration la plus importante de l 'Alpha-Bêta. Le quatrième chapitre clôt les
algorithmes de recherche en profondeur d' abord pour les jeux à deux joueurs avec les al
gorithmes de menaces. Ceux ci sont appliqués au Football des philosophes. Le cinquième
chapitre traite de la recherche arborescente Monte-Carlo qui est un algorithme récent qui
donne de meilleurs résultats que l 'Alpha-Bêta pour certains jeux comme le jeu de Go qui
sert d' illustration à ce chapitre. Le sixième chapitre traite des algorithmes de recherche
en meilleur d' abord. Le septième chapitre présente l ' analyse rétrograde qui permet de
résoudre parfaitement les fins de parties ainsi que certains jeux comme ! ' Awele.
Les chapitres huit à douze décrivent les algorithmes pour les jeux à un joueur. Le
huitième chapitre traite spécifiquement de la recherche du plus court chemin sur une carte.
Le neuvième chapitre porte sur la recherche d' une solution de coût minimal pour les
puzzles comme le Rubik's cube, le Taquin ou le voyageur de commerce. Le dixième
chapitre montre comment utiliser l ' analyse rétrograde pour les puzzles comme le Taquin
ou le Rubik's cube. Le onzième chapitre montre comment utiliser les méthodes de Monte
Carlo pour les puzzles pour lesquels on ne dispose pas de bonnes heuristiques comme
le Morpion Solitaire ou SameGame. Le douzième chapitre présente des rudiments de
programmation par contraintes avec comme exemple le Sudoku.
Les chapitres treize à seize sont des introductions à des approches scientifiques com
plémentaires. Le treizième chapitre est une brève introduction aux algorithmes pour les
jeux à information incomplète. Le quatorzième chapitre montre des rudiments de théorie
combinatoire des jeux. Le quinzième chapitre présente les bases de la théorie des jeux. Le
seizième chapitre évoque les jeux généraux.
Le principe de chaque chapitre est de commencer par des explications d' algorithmes
suivies d'exercices pratiques pour mettre en œuvre les algorithmes sur des jeux. Les exer
cices sont corrigés en C++ à la fin de chaque chapitre. Les source des corrigés sont dis-
ii
Je remercie Abdallah Saffidine et Annie Basséras pour leurs relectures attentives, Jean
Méhat pour avoir partagé avec moi un cours de Master sur les jeux qui est à l 'origine de ce
livre, Cristina Bazgan et Suzanne Pinson pour m ' avoir permis de donner ce cours en école
doctorale et en Master, Florent Michel et Alain Gourdin pour m ' avoir permis de le donner
à des élèves ingénieurs, ainsi que tous les étudiants qui ont suivi mes cours d ' intelligence
artificielle pour les jeux, sans oublier bien sûr Hélène, Clémentine, Cyprien et Charles.
Table des matières
1 Fonctions d'évaluation 1
1.1 Influence de la représentation du problème .
1.2 Existence de stratégies toujours gagnantes 3
1.3 Une fonction d'évaluation aux Échecs 4
1.4 Le jeu du virus . . . . . . . . . 5
1.5 Recherche contre connaissances 7
1.6 Corrigés des exercices . . . . . 7
1.6.1 Coups gagnants à Nim 7
1.6.2 Joueur parfait pour Nim 8
1.6.3 Fonction d'évaluation pour le jeu du virus 9
2. 14.3 Minimax 30
2. 1 4.4 Negamax 31
2. 14.5 Coupe bêta 32
2. 14.6 Développement de l ' arbre avec l' Alpha-Bêta 32
2. 1 4.7 Alpha-Bêta . . . . . 34
2. 1 4 . 8 Variation principale . 35
2. 14.9 Quiescence . . . . . 36
2. 1 4. 1 0 Approfondissement itératif . 37
2. 1 4. 1 1 Coups qui tuent . . . . . . 40
2. 14. 1 2 Heuristique de l'historique 41
2. 14. 1 3 Recherche aspirante . . . 44
2. 14. 1 4 Recherche avec variation principale 44
2. 14. 1 5 Heuristique du coup nul . . . . 45
2. 1 4. 1 6 Heuristique du coup nul vérifié . 46
3 Tables de Transposition 49
3. 1 Introduction . . . . 49
3.2 Hachage d 'une position . 50
3.3 Probabilité d'erreur . . . 51
3.4 Stratégies de remplacement . 52
3.5 Entrées d e l a table d e transposition . 52
3.6 Utilisation d e l a table d e transposition 53
3.7 Coupes d e transposition améliorées . 54
3.8 La recherche avec partition . . . . . 54
3.9 Corrigés des exercices . . . . . . . 55
3.9. l Hachage d' une position de Tic-Tac-Toe 55
3.9.2 Hachage d' une position aux Échecs et au jeu du virus . 56
3.9.3 Hachage incrémental au Tic-Tac-Toe . . . . . . . 56
3.9.4 Probabilité d'erreur . . . . . . . . . . . . . . . . . 56
3.9.5 Classes génériques pour les tables de transposition 57
3.9.6 Alpha-Bêta avec tables de transposition 58
3.9.7 Coupes de transposition améliorées 63
Bibliographie 234
Chapitre 1
Fonctions d'évaluation
La façon dont on représente un jeu et les coups de ce jeu peut avoir une grande in
fluence sur la difficulté d'un jeu. De même, elle peut avoir une grande influence sur les
niveaux des programmes de jeux. On peut l ' illustrer à l ' aide des trois jeux suivants [62] :
- Jeu numéro l : Les joueurs jouent chacun leur tour. Au début du jeu 9 cartes numé
rotées de l à 9 sont posées et prêtes à être prises. A son tour, un joueur prend une
des 9 cartes. Le premier joueur qui, parmi toutes ses cartes, a trois cartes dont la
somme fait 15 a gagné.
- Jeu numéro 2, TicTacToe : Les joueurs jouent chacun leur tour. Un joueur fait des
ronds, l' autre joueur fait des croix. A son tour, un joueur remplit avec un rond ou
une croix une case vide du quadrillage. Le premier joueur qui aligne 3 ronds (resp.
croix) a gagné.
4 9 2
3 5 7
8 1 6
Les trois jeux sont équivalents, la représentation n'est pas la même. Il nous est plus
facile de raisonner avec la représentation du deuxième jeu qu'avec la représentation du
premier. Le troisième jeu nous permet de jouer au jeu numéro 1 avec nos connaissances
du TicTacToe.
De manière plus générale, des études psychologiques ont montré que la représentation
d'un problème a une grande influence sur sa résolution. Par exemple on peut donner un
problème sous plusieurs formes [5] :
Problème numéro 1 : On dispose d' autant de dominos 2x 1 que l'on souhaite. Peut on
complètement remplir un carré 4x4 dont on a ôté deux coins opposés avec des dominos
2x l ?
La réponse est non, elle se trouve plus facilement que dans le cas du problème numéro
1 . On utilise une représentation qui guide naturellement vers la solution. En effet, on
peut observer que pour remplir le damier complet avec des dominos, cela ne pose pas de
problème. On observe aussi que chaque case noire est voisine de cases blanches et vice
versa. On est donc obligé de remplir une case blanche à chaque fois qu'on remplit une
case noire. Or dans le cas du damier tronqué, il manque deux cases blanches. On ne pourra
donc pas le remplir puisqu' il y a deux cases noires de plus que de cases blanches. Il arrive
souvent qu'un problème soit difficile avec une représentation et facile avec une autre
représentation. Les chercheurs sur la reformulation automatique de problèmes essaient
d'écrire des programmes qui se déplacent dans l'espace des représentations pour trouver
celle qui est la plus appropriée à la résolution d'un problème.
"- Un seul."
Richard Réti.
Un exemple de jeu résolu pour lequel il existe une stratégie simple pour gagner est
le jeu de Nim. Le jeu de Nim se joue à deux. On dispose de plusieurs tas d'allumettes,
chaque joueur prend à son tour autant d'allumettes qu'il le veut dans un tas. Le gagnant
est le joueur qui prend la dernière allumette. On joue habituellement le jeu de Nim en
commençant avec quatre tas de 1 , 3, 5 et 7 allumettes. La stratégie gagnante consiste
à transformer le nombre d'allumettes de chaque tas en sa représentation binaire. On fait
alors la somme binaire sur chaque bit de la représentation binaire des nombres sans utiliser
la retenue (ce qui est équivalent à faire un XOR des nombres). Si la somme binaire (ou le
XOR) vaut 0, on est dans une position sûre, ce sont les positions qu'on cherche à atteindre
lorsqu'on joue un coup. Si par contre on se retrouve après un coup dans une position non
sûre, on a perdu.
4 Fonctions d'évaluation
Exemple:
I 1 0001
III 3 0011
IIIII 5 0101
IIIIIII 7 0111
0000
La position de départ est une position sûre. Tous les coups qui peuvent être joués à
partir de cette position sont des coups qui amènent à une position non sûre.
I 1 0001
III 3 0011
IIII 4 0100
IIIIIII 7 0111
0001
Boris Spassky.
Une fonction d'évaluation prend en entrée une position dans un jeu et donne en sortie
une évaluation numérique pour cette position. L'évaluation est d'autant plus élevée que
la position est meilleure. Dans les programmes d'Échecs, la fonction d'évaluation est
composée d'une partie matérielle et d'une partie positionnelle.
- structure de pions
- mobilité
- contrôle du centre
- contrôle des cases importantes de la position
- placement des pièces
- sécurité du roi
La façon de calculer les facteurs positionnels peut être adaptée à la position avec
un oracle. Par exemple HITECH, de H. Berliner et C. Ebeling comporte un algorithme
d'oracle qui analyse en détail la position de départ et en déduit les points importants à
considérer. Par exemple si on n'est pas en fin de partie, il est inutile de perdre du temps à
examiner si un pion peut être promu s'il est loin de la 8ème ligne. Un autre exemple: avan
cer un pion couvrant un roque peut être catastrophique dans certaines situations, on peut
décider de donner un handicap à la fonction d'évaluation aux positions pour lesquelles ce
pion est avancé.
Le jeu du virus est une simplification du jeu vidéo Attaxx. Le plateau de jeu est une
grille carrée qui contient au début deux pions noirs et deux pions blancs placés sur des
coins opposés comme sur le plateau de la figure 1 .5 . Un coup consiste à placer un pion
de sa couleur sur une des huit cases voisines d'un pion déjà posé. La case doit être vide
pour qu'on puisse poser un pion dessus. De plus après avoir posé le pion, tous les pions
adverses sur les cases voisines de la case du pion posé changent de couleur et deviennent
de la couleur du pion posé. Un exemple de coup est donné dans la figure 1 .6. Le jeu se
termine lorsque le plateau de jeu est rempli. Le gagnant est celui qui a le plus de pions sur
le plateau. Le jeu du virus se joue habituellement sur un damier 7 x 7.
• 0
0 •
FIGURE 1 .5 - La position de départ au jeu du virus 4x4.
• 0 •• 0
00 ••
0 • 0 •
FIGURE 1. 6 - Un coup noir au jeu du virus 4x4.
Exercice : Écrire un programme qui représente un jeu du virus et qui comprend une
fonction d'évaluation pour ce jeu. Écrire une intelligence artificielle pour le jeu du virus
qui choisit le coup qui amène à la position ayant la meilleure évaluation.
Connaissances
Recherche
Lorsqu'on écrit un programme de jeu il est tentant d'écrire une fonction d'évaluation
élaborée comportant de nombreuses connaissances de façon à bien diriger la recherche
du meilleur coup. Toutefois la complexification de la fonction d'évaluation rend souvent
le programme plus lent pour faire des évaluations et ralentit donc la recherche. Il peut
donc arriver qu'un programme avec une fonction d'évaluation plus élaborée soit moins
bon qu'un programme avec une fonction d'évaluation simple qui fait plus de recherche. Il
devient donc naturel de se poser la question de la pertinence d'ajouter des connaissances
dans la fonction d'évaluation en fonction du ralentissement de la recherche qu'elles in
duisent.
La figure 1.7 donne les courbes de niveau auxquelles on peut s'attendre en fonction
des connaissances et de l'effort de recherche qu'on donne à un programme de jeu [6 1, 8].
Une courbe représente un niveau du programme constant. On peut remarquer qu'ajouter
des connaissances lorsqu'il y a peu de recherche ou ajouter de la recherche lorsqu'il y a
peu de connaissances améliore peu le programme. La conclusion est qu'il faut garder un
bon équilibre entre recherche et connaissances.
Un étude empirique des courbes recherche versus connaissances a été faite aux Échecs,
à Othello et au Checkers [47] mais aussi sur des finales d'Échecs [72] et sur le jeu Lines of
Action [10]. Les courbes sont assez proches des courbes théoriques de la figure 1.7. Tou
tefois, pour les abscisses de l'effort de recherche proches de zéro, les courbes associées
ne montent pas aussi haut que sur la courbe théorique.
I 1 0001
III 3 0011
III! 4 0100
IIIIIII 7 0111
0001
Les coups gagnants sont de retirer une allumette soit dans le tas à une allumette, soit
dans le tas à trois allumettes soit dans le tas à sept allumettes.
8 Fonctions d'évaluation
u s in g namespa ce std;
int t as [4] = {7 , 5, 3, 1} ;
int mai n () {
i nt t , nombre;
w h ile (tr u e ) {
cou t << tas [O] << " " << t as [1] << "...... " <<
.....,
D'une manière gé nérale on essaie de re ndre la fonction d'évaluation d'u n jeu plus
rapide e n la re ndant i ncréme ntale (e n évitant de recalculer ce qui ne change pas quand on
passe d'une position à la suivante). Pour notre exemple du jeu du virus c'est très simple,
il suffit de mémoriser l'évaluation dans u ne variable e ntière.
cla s s Cou p {
pu bli c:
char c oul e u r;
ÎDt X, y;
};
cla s s V i r us {
pu bl i c:
char d ami e r [Taill e] [Taill e ];
int e v al;
V i r us () {
i n i t ();
}
v o id in1t () {
for (i nt i = O; i < Taill e ; i++)
for (i nt j = O; j < Taill e ; j++)
d ami e r [ i] [j] = '+ ';
d ami e r [O] [O] = ' @ ' ;
d ami e r [Taill e 1] [Taill e - 1] = '@ ' ;
d ami e r [Taill e 1] [0] = 'O';
10 Fonctions d'évaluation
eval = O;
}
if ( f i n y > T a i l l e - 1 ) f i n y = T a i l l e l;
for ( i n t i = d e b u t x ; i <= f i n x ; i + + )
for (i n t j = d e b u t y ; j < = f i n y ; j + + )
if ( d a m i e r [ i ] [ j ] = = a u t r e ) {
damier [ i ] [ j ] = m. couleur ;
if (m . c o u l e u r == '@ ' ) e v a l += 2 ;
e l s e e v a l -= 2 ;
}
}
b o ol c o u p L e g a l ( Coup & c o u p ) {
if ( d a m i e r [ c o u p . x ] [ c o u p . y ] != ' + ' )
r eturn fal s e ;
for ( i n t x = max ( c o u p . x - 1 ,0) ;
x <= m i n ( c o u p . x + 1 , T a i l l e - l ); x + + )
for (int y = max ( c o u p . y - 1 , O ) ;
y <= m i n ( c o u p . y + 1 , T a i l l e - l ); y + + )
if ( d a m i e r [ x ] [ y ] == c o u p . c o u l e u r )
r eturn tr u e ;
return fa l s e ;
}
t.6 Corrigés des exercices 11
J i s t <Coup> c o u p s L e g a u x (char c o u l e u r ) {
1 i s t <Coup > 1 i s t e ;
Coup c o u p ;
coup . c o u l e u r = c o u l e u r ;
for (i nt i = O ; i < T a i l l e ; i + + )
for (int j = O ; j < T a i l l e ; j + + ) {
c o u p . x = i;
coup . y = j ;
if ( c o u p L e g a l ( c o u p ) )
l i s t e . p u s h_b a c k ( c o u p ) ;
}
return l i s t e ;
}
c o nst V i r u s & v ) ;
} ;
c o nst V i r u s & v ) {
s o r t i e << " " ;
......
.. ..
s o r t i e << e n d l ;
for (i nt j = O ; j < T a i l l e ; j + + )
Il Il
s o r t i e << v . d a m i e r [ j ) [ i ] << ......
•
,
s o r t i e << e n d l ;
}
s o r t i e << " e v a l u a t i o n ._. p o u r ._.@._.= ...... " <<
v . e v a l u a t i o n ( '@ ' ) << e n d l ;
r e turn s o r t i e ;
}
Virus virus ;
int m a i n () {
l i s t <Coup > I i s t e C o u p s ;
while (tr ue) {
c o u t << v i r u s ;
l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( '@ ' ) ;
if ( J i s t e C o u p s . e m p t y () )
break ;
c o u t << " D o n n e z._.v o t r e ...... c o u p ._. : ._. " ;
12 Fonctions d'évaluation
Coup c o u p ;
c o u p . c o u l e u r = '@ ' ;
do {
c i n >> c o u p . x >> c o u p . y ;
} wh ile ( !v i r u s . c o u p L e g a l ( c o u p ) ) ;
v i r u s . jo u e ( coup ) ;
c o u t << v i r u s ;
I i s t e C o u p s = v i r u s . c o u p s L e g a u x ( 'O') ;
if ( l i s t e C o u p s . e m p t y ( ) )
bre ak ;
int m e i l l e u r e E v a l u a t i o n = - T a i l l e * T a i l l e - 1;
for ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t != l i s t e C o u p s . e n d (); ++ i t ) {
Virus v = virus ;
v . jo u e ( * i t ) ;
int e v a l = v . e v a l u a t i o n ( 'O') ;
if ( e v a l > m e i l l e u r e E v a l u a t i o n ) {
m e i l l e u reEvaluation = eval ;
coup = * i t ;
}
}
c o u t << " J e �j o u e � e n � " << c o u p . X << " " <<
c o u p . y << e n d ! ;
v i r u s . jo u e ( coup ) ;
}
return O ;
}
Chapitre 2
Minimax, Alpha-Bêta et
heuristiques associées
"La vérité est comme le meilleur coup aux échecs : elle existe, mais il faut la chercher. "
Arturo Perez-Reverte.
Dans ce chapitre nous allons voir des algorithmes de recherche en profondeur d ' abord
qui permettent de prévoir le déroulement d ' une partie sur plusieurs coups. Un avantage
des algorithmes de recherche en profondeur d ' abord est qu ' ils sont peu coûteux en mé
moire. C'est un avantage important sur les algorithmes en meilleur d' abord ou en largeur
d' abord qui sont limités en pratique sur les problèmes de grande taille par la mémoire dis
ponible. Un autre avantage important des algorithmes de recherche en profondeur d' abord
est qu ' ils permettent d'évaluer une position rapidement en réutilisant les informations de
la position précédente qui n ' a changé que d ' un coup par rapport à la position à évaluer.
L'ordre de recherche des positions permet de connaître les informations sur la position
avant le coup qui a mené à cette position. On peut utiliser les informations de la position
précédente pour recalculer plus rapidement les informations sur la position courante, en
ne calculant que la différence avec la position précédente induite par le coup.
2.1 Le Minimax
On se place maintenant dans le cadre des jeux à deux joueurs. L'algorithme Mini max,
ses variantes et améliorations sont utilisés dans de nombreux programmes de jeux à deux
14 M inimax, Alpha-Bêta et heuristiques associées
4 7 7
8 4 10 8 7 11 10 5
joueurs. Il concerne les jeux à somme nulle, c ' est à dire dont la somme des gains des
deux joueurs est constante, et à information complète (les deux joueurs connaissent toute
la position).
On rappelle qu' une fonction d'évaluation prend en entrée une position dans un jeu et
donne en sortie une évaluation numérique pour cette position. L'évaluation est d' autant
plus élevée que la position est bonne pour le joueur.
Si une fonction d'évaluation est parfaite, il est inutile d'essayer de prévoir plusieurs
coups de suite. Toutefois, pour les jeux un peu complexes comme les Échecs, on ne
connaît pas de fonction d 'évaluation parfaite. Un programme est amélioré si à partir d' une
bonne fonction d'évaluation il prévoit les conséquences de ses coups sur plusieurs coups
de suite.
L' hypothèse fondamentale du Minimax est que l ' adversaire utilise la même fonction
d'évaluation que le programme.
Notre but est de trouver le coup qui maximise la fonction d'évaluation, alors que le but
de l ' adversaire est de choisir le coup qui minimise la fonction d 'évaluation. Or les deux
adversaires jouent chacun leur tour et en général c ' est le joueur ami qui joue en premier
puisqu 'on cherche le meilleur coup à jouer pour le joueur ami. On va donc choisir les
coups qui maximisent l' évaluation lorsque c 'est au joueur ami de jouer et les coups qui
minimisent l 'évaluation lorsque c'est au joueur ennemi de jouer.
On représente habituellement un arbre Minimax avec des rectangles pour les noeuds
Max et des ronds pour les noeuds Min. Les feuilles correspondent aux positions évaluées.
2.1 Le M inimax 15
1 8 16 12 24 920 13 6 5 8 4 2 10 1 1 7 9 10 5 7 3 2
La figure 2. 1 donne un arbre Minimax évalué. La racine de l ' arbre est un noeud Max.
On commence par évaluer les noeuds internes au dessus des feuilles puis on remonte les
évaluations dans l ' arbre. Le score d ' un noeud Min est le minimum des scores de ses fils
et le score d'un noeud Max est le maximum des scores de ses fils. Le score est ce qui
se trouve dans la partie supérieure d'un noeud. La partie inférieure montre l 'évaluation
courante du noeud au fur et à mesure d ' un parcours en profondeur d' abord de l ' arbre en
commençant par les fils les plus à gauche.
Exercice : Quelle est la valeur de la racine de l ' arbre de la figure 2.2 en utilisant
l ' algorithme Minimax ? Combien de noeuds a-t-on développé et combien d'évaluations
a-t-on effectué ?
Exercice : Écrire une classe Virus pour le jeu du virus qui permette d ' annuler le
dernier coup joué pour arriver à la position. Écrire de plus une fonction qui permette
d'évaluer une position lorsqu ' il n ' y a plus de coups possible pour l ' un des joueurs.
2.2 Le Negamax
Plutôt que d'écrire deux fonctions mini et maxi, on peut inverser le signe des évalua
tions à chaque niveau, et toujours chercher à maximiser. On a alors l ' algorithme Negamax
qui n ' utilise qu ' une seule fonction récursive qui maximise à tous les niveaux.
L' algorithme Negamax est en général celui qui est utilisé pour décrire des heuristiques
car il suffit d'écrire l ' heuristique pour le seul niveau max.
Exercice : En utilisant les mêmes fonctions prédéfinies que pour le Minimax pro
grammez l ' algorithme Negamax.
L' Alpha-Bêta est un algorithme qui renvoie toujours la même valeur que le Minimax,
mais qui n ' utilise jamais plus de noeuds pour effectuer sa recherche. En pratique, il déve
loppe beaucoup moins de noeuds car il coupe des parties entières de l ' arbre. Couper une
partie de l ' arbre signifie qu ' il n'explore pas cette partie. Faire une coupe dans l ' arbre si
gnifie qu ' il arrête la recherche d'un noeud avant d ' avoir exploré tous les fils de ce noeud.
L' Alpha-Bêta doit son nom aux deux formes de coupes qu 'il emploie : les coupes alpha
et les coupes bêta.
Les coupes alpha se font aux niveaux Min. Elles sont basées sur l 'observation que si
la valeur d'un noeud de niveau Min est plus petite que la valeur du noeud de niveau Max
supérieur, quelles que soient les valeurs suivantes au niveau Min, elles ne changeront pas
la valeur du niveau Max supérieur. Un exemple de coupe alpha est donné en figure 2.3.
La coupure bêta est la coupe symétrique de la coupe alpha pour les niveaux Max.
Exercice : Reprendre l' arbre développé avec le Minimax. Quelle est la valeur de la
racine en utilisant l ' algorithme Alpha-Bêta, combien de noeuds a-t-on développé et com
bien d'évaluations a-t-on effectuées ?
L'ordre dans lequel sont essayés les coups dans l ' Alpha-Bêta est très important. Il
faut commencer par les meilleurs. Du bon ordre des coups dépend le nombre de coupes.
Mieux les coups sont ordonnés, plus le nombre de coupes sera important, plus le nombre
de noeuds évalués sera petit et plus l ' algorithme donnera une réponse rapidement.
2.3 L' Alpha-Bêta 17
>=1 8
18
Propriété: Lorsque le Minimax trouve un coup en n noeuds, ! ' Alpha-Bêta peut trou
ver ce même coup en 2fo, 1 noeuds si les coups sont ordonnés du meilleur au moins
-
bon [5 1 ] . En pratique cela permet à temps constant de faire une recherche deux fois plus
profonde.
Algorithm 1 Alpha-Bêta
a(3 (depth, a, (3, joueur)
if depth= 0 then
retourner l 'évaluation de la position courante pour joueur
end if
for tous les coups possibles pour joueur do
jouer le coup
eval = -a(3 (depth - l,-(3, a , adversaire (joueur))
-
La variation principale d ' une recherche Alpha-Bêta est la suite des meilleurs coups
pour les deux adversaires. Elle permet de voir ce que le programme à prévu. Elle com
mence par le meilleur coup pour Max suivi de la meilleure réponse pour Min et ainsi de
suite jusqu ' au dernier coup. Le nombre de coups de la variation principale est la profon
deur de la recherche effectuée.
Exerc ice: Modifier l ' algorithme Alpha-Bêta pour qu ' il mémorise la variation princi
pale.
La variation principale peut être utilisée pour récupérer le meilleur coup : c ' est le coup
en tête de liste.
Il y a deux inconvénients au fait qu 'on doive fixer une profondeur maximum. Le pre
mier étant que le programme ne peut prévoir les effets d'un coup à une profondeur dépas
sant la profondeur maximum. Le second est que cela introduit des effets non désirés dans
les choix du programme : l ' horizon étant défini comme la profondeur maximum de la
recherche, le programme fera toutes les menaces qu 'il peut et qui sont pourtant inutiles,
voire néfastes, pour repousser au-delà de son horizon un événement qui lui est défavo
rable. C ' est ce qu'on appelle l 'effet d' horizon : le programme repousse les événements
défavorables au-delà de son horizon. Par exemple aux Échecs, si une dame est de toutes
façons prise mais que la prise peut être repoussée au delà de l ' horizon en sacrifiant un ca
valier, le programme choisira de sacrifier inutilement son cavalier pour repousser la prise
de la dame au delà de son horizon.
Une parade possible à l 'effet d'horizon est d' utiliser une méta-fonction d'évaluation
qui n 'évalue pas la position mais plutôt qui évalue le type de la position. Cette méta
fonction d'évaluation évalue si une position est stable (aux Échecs, essentiellement s ' il
reste des pièces en prise). Elle est utilisée pour savoir si une position est évaluable ou si
on doit continuer à la développer. Une position stable a une évaluation en laquelle on peut
avoir confiance alors qu ' une position instable n ' a pas une évaluation fiable.
j oueur) qui ne sélectionne que les coups liés à la quiescence (par exemple les coups
qui modifient plus de cinq cases au jeu du virus). Programmer ensuite une fonction qui
effectue une recherche de quiescence au jeu du virus. À chaque feuille de l ' arborescence
Alpha-Bêta principale, appeler la fonction quiescence pour évaluer la position.
Questions : Quelle est la complexité de l ' approfondissement itératif jusqu ' à la pro
fondeur d par rapport à une recherche directe en profondeur d' abord à la profondeur d ?
Pourquoi est-il intéressant d'effectuer un approfondissement itératif ?
Réponses :
A priori, l ' approfondissement itératif perd du temps dans les itérations précédant la
dernière itération. Toutefois, ce travail supplémentaire est généralement beaucoup plus
petit que la dernière itération. S ' il y a n coups explorés pour chaque position, le nombre
de feuilles à la profondeur k est nk . Le nombre de feuilles engendrées par un approfondis
sement itératif jusqu ' à la profondeur d est donc de nd + nd· I + nd-2 + nd·3 + . . . + n. Ce qui
est en O(nd). Si n est assez grand, le premier terme est nettement plus grand que tous les
autres, c'est donc la dernière itération qui prend la plus grande partie du temps. La com
plexité en espace de l' approfondissement itératif est linéaire en fonction de la profondeur
de recherche. Si on considère l ' asymptote, l ' approfondissement itératif est un algorithme
de recherche optimal aussi bien en temps qu 'en espace [52] .
Enfin l ' intérêt principal pour la programmation des jeux est qu ' il permet de récupérer
des informations de la recherche à la profondeur précédente. Ces informations sont très
20 M inimax, Alpha-Bêta et heuristiques assoc iées
position arbre
FIGURE 2.4 - La réponse noire B au coup blanc A est un coup qui tue au Go-Moku.
L'ordre dans lequel on considère les coups a une grande influence sur l ' efficacité de
l ' algorithme Alpha Bêta. Les coups·qui tuent (killer moves) amènent souvent à des coupes
Alpha-Bêta. Le principe des coups qui tuent est d'essayer en priorité pour une position
à profondeur donnée des coups qui ont déjà amenés à une coupe Alpha-Bêta pour des
positions à cette profondeur. On peut mémoriser un ou plusieurs coups qui tuent pour
chaque profondeur de recherche.
La figure 2. 4 donne un exemple de coup qui tuent au Go-Moku (le but est d' aligner
5 pierres en jouant chacun son tour sur une grille carrée). Si blanc joue en A, la réponse
noire en B gagne la partie. Elle amène donc à une coupe Alpha-Bêta, et on mémorise à la
profondeur de B le coup noir B comme coup qui tue. Pour tous les coups blancs à la racine
2.8 L'heuristique de l'historique 21
L'arbre à droite de la position de la figure 2.4 montre le fonctionnement des coups qui
tuent. L' Alpha-Bêta commence par jouer le coup blanc A, puis lorsque noir lui répond en
B, il y a une coupe Alpha-Bêta. Le coup noir en B est donc stocké comme coup qui tue
à la profondeur de B. Lorsque blanc essaie un autre coup à la racine, noir lui répond en
premier le coup qui tue en B .
Exercice : Modifier l ' Alpha-Bêta pour lui faire essayer e n priorité deux coups qui
tuent. Commencez par déclarer la structure de données qui permettra de mémoriser les
coups qui tuent. Puis écrivez la fonction qui les mémorise, en écrasant le coup le plus
anciennement mémorisé. Modifiez enfin l' Alpha-Bêta pour qu ' il joue en priorité deux
coups qui tuent tout en vérifiant qu' ils sont légaux. Faites attention à ce que l' Alpha-Bêta
ne les essayent pas deux fois ce qui serait inutile et coûteux.
L'heuristique de l 'historique (history heuristic [75, 76]) consiste à tenir à jour une
note globale pour chaque coup légal rencontré dans l ' arbre de recherche qui a amené à
au moins une coupe Alpha-Bêta. À chaque fois qu 'un coup amène à une coupe Alpha
Bêta, sa note est ajustée d'un montant qui est fonction de la profondeur du sous-arbre
exploré après ce coup. On peut par exemple ajouter 4depth à la note du coup ayant amené
à une coupe, depth étant la profondeur du sous arbre qui a été développé sous le coup.
On ordonne les coups à tester dans l' Alpha-Bêta en fonction de leur note. On commence
par essayer ceux ayant la note la plus élevée. Le principe d ' ajouter 4depih à la note du
coup est de privilégier les coups qui ont amené à des coupes proches de la racine : ces
coupes sont plus importantes que les coupes plus éloignées de la racine car elles coupent
des arbres plus profonds et donc plus volumineux. L' heuristique de l ' historique amène
à privilégier les coups qui coupent, et parmi ces coups, ceux qui ont coupé proche de
la racine. J. Schaeffer a montré que l ' heuristique de l ' historique associée aux tables de
transposition est responsable de 99% des réductions de recherche dans !' Alpha-Bêta [76) .
En général on trie les coups avec l ' heuristique de l 'historique après avoir essayé les autres
coup prioritaires comme le coup de transposition (voir chapitre suivant) et les coups qui
tuent.
Pour programmer l ' heuristique de l' historique, on supposera que la classe Coup a
une fonction membre nombre ( ) qui renvoie un nombre associé au coup. Ce nombre
est compris entre 0 et une constante MaxNombre. Dans les jeux où cela est possible, on
programmera la fonction nombre ( ) pour qu ' il y ait une bijection entre les coups et les
nombres (c 'est par exemple possible au jeu du virus, au Go-Moku, au Go et aux Échecs).
Quand ce n'est pas possible en pratique, par exemple aux Dames à cause du grand nombre
de coups de prise possibles, on fera correspondre le même nombre à des coups similaires,
par exemple aux dames en codant dans le nombre le type de pièce, la case de départ et la
case d' arrivée.
22 Minimax, Alpha-Bêta et heuristiques associées
Exercice : Modifier l' Alpha-Bêta pour prendre en compte l ' heuristique de l ' histo
rique. Commencer par déclarer les structures de données nécessaires. Puis écrire les fonc
tions d'initialisation, de mise à jour des scores, et de tri des coups. Modifier enfin l' Alpha
Bêta.
Plutôt que d' appeler l' Alpha-Bêta avec des valeurs initiales de la plus petite évalua
tion possible pour alpha et de la plus grande évaluation possible pour bêta, on peut lui
permettre de couper plus de branches, si on augmente (resp. diminue) la valeur initiale de
alpha (resp. bêta). Si la valeur retournée par l 'Alpha-Bêta est comprise entre les alpha et
bêta initiaux, le résultat sera tout de même juste, bien que l'on ait coupé plus de branches
inutiles qu' avec les valeurs extrêmes.
On appelle fenêtre de l' Alpha-Bêta l' ensemble des valeurs comprises entre alpha et
bêta.
La recherche aspirante est une amélioration de l ' approfondissement itératif. Les ré
sultats de la recherche à profondeur p sont généralement assez proches des résultats de la
recherche à profondeur p 1. On peut donc centrer la fenêtre sur la valeur retournée par
-
la recherche précédente.
En poussant l ' idée de fenêtre jusqu ' au bout, on obtient la recherche avec fenêtre nulle
(Null-Window Search). Cela consiste à appeler !' Alpha-Bêta avec une fenêtre [valeur,
valeur + l ] , sachant que la fonction d'évaluation est entière avec des différences d' éva
luation minimales de un point. Les arbres développés sont alors plus petits, et on peut
ajuster vers le haut ou vers le bas la valeur en fonction de ce que retourne l 'Alpha-Bêta. Il
a été montré que cet algorithme, qui associé aux tables de transposition s ' appelle MTD(f)
2.11 La recherche avec variation pr inc ipale 23
[68) , développe les feuilles de l ' arbre dans le même ordre qu'un algorithme en meilleur
d' abord qui a été prouvé meilleur qu ' Alpha-Bêta : SSS * [84) .
La recherche avec variation principale (Principal Variation Search) permet une petite
amélioration sur l' Alpha-Bêta simple en utilisant des recherches avec fenêtres nulles. Pour
cela, on divise les noeuds de la recherche Alpha-Bêta en trois types distincts :
- Les noeuds alpha pour lesquels tous les coups retournent une valeur plus petite ou
égale à alpha.
- Les noeuds bêta pour lesquels au moins un coup retourne une valeur supérieure ou
égale à bêta.
- Les noeuds de la variation principale (noeuds PV) pour lesquels au moins un des
coups a une valeur supérieure à alpha, mais aucun des coups ne retourne une valeur
supérieure ou égale à bêta.
En supposant que les coups sont envisagés des meilleurs aux moins bons, on peut
essayer de deviner le type d'un noeud en fonction de la valeur retournée par la recherche
sur le premier coup envisagé. Si la valeur est supérieure à bêta, on est sûr qu'on a un
noeud bêta. Si la valeur est inférieure à alpha, et que l ' on fait l ' hypothèse que le premier
coup a de grandes chances d'être le meilleur, on peut estimer qu ' on a de grandes chances
d'être en présence d'un noeud alpha. Si la valeur est comprise entre alpha et bêta, on a de
grandes chances d'être en présence d'un noeud PV.
Le principe de la recherche avec variation principale est de faire une recherche nor
male jusqu ' à ce qu 'on trouve un coup qui a une valeur comprise entre alpha et bêta. On
fait alors l 'hypothèse que ce coup est le meilleur et on ne cherche plus qu ' à prouver que
les coups suivants sont moins bons. Si ce n ' est pas le cas, il faudra refaire une recherche
pour le coup qui se révèle être meilleur. Faire des recherches avec une fenêtre nulle pour
prouver que les coups qui suivent le coup de la variation principale sont moins bons, est
moins coûteux que de faire des recherches avec la fenêtre normale. Toutefois, quand un
coup suivant le coup de la variation pincipale est meilleur, il faut refaire une recherche et
la recherche avec fenêtre nulle est du temps perdu. En pratique les gains apportés par les
recherches avec fenêtre nulle sont plus importants que les pertes dues aux coups qui se
révèlent être meilleurs que prévu.
L' heuristique du coup nul permet de détecter avec une recherche beaucoup moins
coûteuse que la recherche normale les positions qui vont très probablement amener à des
24 Minimax, Alpha-Bêta et heuristiques associées
coupes bêta. Cette heuristique permet de ne pas développer des arborescences qui ont de
grandes chances d'être inutiles.
L' heuristique du coup nul permet de gagner du temps en décidant de ne pas développer
des noeuds qui amène à une coupe bêta après une recherche à une profondeur inférieure
à la profondeur courante, même après avoir passé son tour.
Un coup nul (null move) consiste à changer le tour de jeu, ce qui est équivalent pour
un joueur à passer son tour. Le coup nul est un coup légal au jeu de Go où un joueur
peut passer ; c ' est même de cette façon que la fin de partie est décidée lorsque les deux
joueurs passent consécutivement. Aux Échecs par contre, et dans d' autres jeux, ce n ' est
pas un coup légal, or il existe des positions pour lesquelles ce serait le meilleur coup. Ce
sont les positions de zugzwang : le premier joueur qui joue dans une position de ce type
a perdu. Ces positions se retrouvent par exemple fréquemment dans les finales de pions
aux Échecs.
L' hypothèse sur laquelle est fondée l ' heuristique du coup nul est qu ' il y a au moins
un coup qui améliore la position (ceci suppose qu 'il n ' y ait pas de zugzwang) , et donc
qu 'il existe un coup meilleur que de passer. Pour utiliser l' heuristique le joueur passe puis
effectue une recherche à une profondeur inférieure à la profondeur courante. Si le résultat
de la recherche est supérieur à bêta, alors la position n'est pas étudiée plus profondément,
car on suppose qu ' il existe un coup meilleur que le coup nul qui amènera aussi à une
coupe bêta.
L' heuristique du coup nul est associée à un facteur de réduction R qui permet de
régler la profondeur de recherche après le coup nul. Si on est dans un noeud pour lequel
on s ' apprête à faire une recherche de profondeur P la profondeur de la recherche associée
au coup nul sera P R. On choisit en général R 2 ou R 3.
- = =
L' heuristique du coup nul peut parfois masquer une arborescence qui, si elle avait été
explorée, aurait changé le résultat de ! ' Alpha-Bêta. C'est par exemple le cas lorsque la
recherche à une profondeur inférieure utilisée par l 'heuristique ne permet pas de voir une
capture qui aurait été vue avec une recherche un peu plus profonde. Contrairement aux
coupes de l' Alpha-Bêta qui ne changent pas le résultat du Minimax, les coupes dues au
coup nul peuvent changer le résultat de l' Alpha-Bêta.
L' heuristique du coup nul est un compromis entre le temps passé à faire les recherches
après le coup nul et le temps gagné par les coupes qui permettent de ne pas faire les
recherches à profondeur normale quand la recherche après le coup nul renvoie une valeur
supérieure à bêta.
En pratique on veut éviter d' utiliser l ' heuristique du coup nul quand on est dans une
position de zugzwang. Aux Échecs une solution possible est d' éviter de l ' utiliser quand :
Une autre façon de se prémunir contre les zugzwangs est de faire une recherche ré
duite même quand l ' heuristique du coup nul amène à une coupe bêta [39, 69] . En plus
de détecter les zugzwangs, faire une recherche normale à profondeur réduite, après que
l 'heuristique du coup nul ait été vérifiée, peut aussi aider à contrer l 'effet d'horizon en
détectant des menaces tactiques que la recherche avec coup nul n ' avait pas détectées.
Toutefois faire cette recherche de vérification en milieu de partie quand il n ' y a pas ou
peu de zugzwangs paraît trop coûteux. Une solution à ce problème est l ' heuristique du
coup nul vérifié [86] . Une recherche avec coup nul avec un facteur de réduction R =3
est essayée à chaque noeud. Si cette recherche renvoie une valeur supérieure à bêta on
continue la recherche normale à une profondeur de moins. Pour cette recherche normale à
profondeur réduite l ' heuristique du coup nul simple est utilisée. Si la recherche à profon
deur réduite ne renvoie pas une valeur supérieure à bêta c ' est que le coup nul est meilleur
que tous les autres, on est donc en présence d ' un zugzwang. Dans ce cas l ' heuristique du
coup nul vérifié refait une recherche normale à la profondeur normale pour renvoyer une
valeur précise de la position en zugzwang. Aux Échecs, l ' heuristique du coup nul vérifié
avec R = 3 développe moins de noeuds que l ' heuristique du coup nul avec R = 2 et
résout correctement plus de problèmes [86] .
Exercice : Implémenter l ' heuristique du coup nul vérifié pour le jeu du virus.
L'approfondissement sélectif permet de ne pas étudier toutes les parties de l ' arbre à
la même profondeur. Si un coup semble intéressant on continue à chercher à une plus
grande profondeur que la profondeur habituelle. Si un coup semble mauvais on arrête de
chercher à une profondeur plus petite que la profondeur habituelle.
Par exemple, si Chinook analyse un coup qui perd 3 pions, plutôt que de continuer
à analyser la position jusqu ' à une profondeur 10 il va réduire son analyse à seulement 5
coups à l ' avance en faisant l ' hypothèse qu ' il y a de bonnes chances que le coup soit très
mauvais. Par contre, si le programme joue un coup qui paraît très bon, il augmentera la
profondeur de l ' analyse de 10 à 1 2 coups à l ' avance.
D' après J. Schaeffer [78], c ' est une décision d'investissement : on investit son capi
tal (le temps d' analyse) là où on espère avoir le meilleur bénéfice (on cherche le plus
d' information possible).
Un autre mécanisme d' approfondissement sélectif est utilisé dans Deep Blue. Il ana
lyse la structure de l' arborescence pour savoir si un noeud de l ' arbre est uniforme ou pas.
Ainsi un noeud dans lequel beaucoup de coups sont bons est considéré comme ayant une
évaluation sûre. Alors qu'un noeud pour lequel un seul coup parmi de nombreux coups
est bon, est considéré comme peu sûr. Deep Blue développe alors plus que les autres la
partie de l ' arborescence qui ne contient qu 'un seul bon coup.
26 Minimax, Alpha-Bêta et heuristiques associées
On peut considérer les évaluations utilisées pour activer l ' approfondissement sélectif
comme des méta-fonctions d' évaluation.
18
18 18 18
18 24 20 8 10 11 10 7
/\
1 8 1 6 1 2 24 920 13 6 5 8 4 2 10 1 1 7 9 10 5 7 3 2
class Virus {
public :
char damier [ Taille ] [ Taille ] ;
2.14 Corrigés des exercices 27
Virus ( ) {
init ();
}
void i n i t ( ) {
fo r ( i n t i = O ; i < T a i l l e ; i + + )
fo r ( i n t j = O ; j < T a i l l e ; j + + )
damier [ i ] [ j ] = ' + ' ;
d a m i e r [O ] [O ] = @ ; ' '
d a m i e r [ T a i l l e - 1 ] [O ] = ' O ' ;
d a m i e r [O ] [ T a i l l e - 1 ] = ' O ' ;
nbNoirs = 2 ;
nbBlancs = 2 ;
n b M o d i fi c a t i o n s = O ;
}
char a u t r e = a d v e r s a i r e (m. c o u l e u r ) ;
int debutx = m. x - 1 fi n x = m. x + 1 ;
'
i n t debuty = m. y - 1 fi n y = m. y + 1 ;
'
i f ( debutx < 0) debutx = O;
if ( debuty < 0) debuty = O;
i f ( fi n x > T a i l l e - 1 ) fi n x = T a i l l e 1;
i f ( fi n y > Taille -
1 ) fi n y = T a i l I e 1;
fo r ( i n t = d e b u t x ; i <= f i n X ; i + + )
28 Minimax, Alpha-Bêta et heuristiques associées
fo r ( i n t j = d e b u t y ; j <= f i n y ; j + + )
i f ( damier [ i ] [ j ] == a u t re ) {
damier [ i ] [ j ] = m. c o u l e u r ;
p i l e M o d i f i c a t i o n s [ n b M o d i fi c a t i o n s ] = i
'
n b M o d i fi c a t i o n s + + ;
p i l e M o d i f i c a t i o n s [ n b M o d i fi c a t i o n s ] = j
'
n b M o d i fi c a t i o n s + + ;
n b S w ap s + + ;
i f (m . c o u l e u r = = '@ ' ) {
nbNoirs ++;
nbBlancs --;
}
else {
nbNoirs --;
nbBlancs ++ ;
}
}
p i l e M o d i f i c a t i o n s [ n b M o d i f i c a t i o n s ] = n b S w ap s ;
n b M o d i fi c a t i o n s ++ ;
}
v o i d d ej o u e ( c o n s t Coup & m) {
i n t x , y , nbS waps ;
char a u t r e = a d v e r s a i r e (m. c o u l e u r ) ;
n b M o d i fi c a t i o n s --;
n b S w ap s = p i l e M o d i f i c a t i o n s [ n b M o d i f i c a t i o n s ] ;
fo r ( i n t i = O ; i < n b S w ap s ; i + + ) {
n b M o d i fi c a t i o n s --;
y = p i l e M o d i fi c a t i o n s [ nbModificatio n s ] ;
n b M o d i fi c a t i o n s --;
x = p i l e M o d i fi c a t i o n s [ nbModificatio n s ] ;
damier [ x ] [ y ] = au tre ;
i f ( m . c o u l e u r == '@ ' ) {
nbNoirs --;
nbBlancs ++;
}
else {
nbNoirs ++;
nbBlancs --;
}
}
n b M o d i fi c a t i o n s - - ;
y = p i l e M o d i f i c a t i o n s [ n b M o d i fi c a t i o n s ] ;
n b M o d i fi c a t i o n s --;
x = p i l e M o d i fi c a t i o n s [ n b M o d i fi c a t i o n s ] ;
damier [ x ] [ y ] = '+ ' ;
i f (m . c o u l e u r == '@ ' )
2.14 Corrigés des exercices 29
nbNoirs --;
el s e
n b B l ancs --;
}
i n t e v a l u a t i o n ( char c o u l e u r ) c o n s t {
i f ( c o u l e u r == '@ ' )
return nbNoirs - nbBlancs ;
return nbBlancs - nbNoirs ;
}
i n t e v a l u a t i o n S i P l u s D e C o u p s P o s s i b l e s ( char c o u l e u r ) {
i f ( c o u l e u r == '@ ' )
return nbNoirs - ( T a i l l e * T a i l l e - nbNoirs ) ;
else
return nbBlancs - ( T a i l l e * T a i l l e - nbBlancs ) ;
}
b o o l c o u p L e g a l ( Coup & c o u p ) {
i f ( d a m i e r [ c o u p . x ] [ c o u p . y ] != ' + ' )
r e t u r n fa l s e ;
fo r ( i n t x = max ( c o u p . x - 1 , 0 ) ;
x <= min ( c o u p . x + 1 , T a i l l e l ) ; x ++)
fo r ( i n t y = max ( c o u p . y - 1 , 0) ;
y <= m i n ( c o u p . y + 1 , T a i l l e - 1 ) ; y + + )
if ( damier [ x ] [ y ] == coup . c o u 1e u r )
return true ;
return fa l s e ;
}
l i s t <Coup> c o u p s L e g a u x ( c h a r c o u l e u r ) {
l i s t <Coup > l i s t e ;
Coup c o u p ;
coup . c o u l e u r = c o u l e u r ;
fo r ( i n t i = O ; i < T a i l l e ; i + + )
fo r ( i n t = 0; j < T a i 1 1 e ; j ++) {
coup . x = i ;
coup . y = j ;
i f ( coupLegal ( coup ) )
l i s t e . pu s h_back ( coup ) ;
}
return l i s t e ;
}
2. 14.3 Minimax
Virus virus ;
i n t m a x i ( i n t d e p t h , Coup & m e i l l e u r C o u p ) {
i f ( d e p t h == 0 )
r e t u r n v i r u s . e v a l u a t i o n ( j o u e u rM a x ) ;
l i s t <Coup> l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r M a x ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i ru s . e v a l u ati o n S i Pl u s D e C o u p s P o s s i b l e s
( joueurMax ) ;
int meilleureEvaluation = - Taille * Taille - 1;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t != l i s t e C o u p s . e n d ( ) ; ++ i t ) {
virus . joue (* i t ) ;
int eval = mini ( depth - 1);
i f ( eval > meilleureEvaluation ) {
meilleureEvaluation = eval ;
meilleurCoup = * i t ;
}
v i r u s . d ej o u e ( * i t ) ;
}
return meilleureEvaluation ;
}
l i s t <Coup > l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r M i n ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u ati o n S i P l u s De C o u p s P o s s i b l e s
( j o u e u rM a x ) ;
int m e i l l e u re E v a l u ation = T a i l l e * T a i l l e + 1 ;
Coup m e i l l e u r C o u p ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t != l i s t e C o u p s . e n d ( ) ; ++ i t ) {
virus . joue (* i t ) ;
i n t e v a l = maxi ( depth - 1 , m e i l l e u r C o u p ) ;
i f ( eval < m e i l l e u reEval uation ) {
meil leureEv aluation = eval ;
}
2.14 Corrigés des exercices 31
v i r u s . d ejo u e ( * i t ) ;
}
return meilleureEvaluation ;
}
i n t main ( ) {
l i s t <Coup> l i s t e C o u p s ;
white ( true ) {
c o u t << v i r u s ;
l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( '@ ' ) ;
i f ( l i s t e C o u p s . empty ( ) )
break ;
c o u t << " D o n n e z....., v o t r e ....., c o u p._.: ._." ;
Coup c o u p ;
c o u p . c o u l e u r = '@ ' ;
do {
c i n >> c o u p . x >> c o u p . y ;
} w h i t e ( !v i r u s . c o u p L e g a l ( coup ) ) ;
v i r u s . jo u e ( co u p ) ;
c o u t << v i r u s ;
l i s t e C o u p s = v i r u s . coupsLegaux ( 'O ' ) ;
i f ( l i s t e C o u p s . empty ( ) )
break ;
i n t e v a l = maxi ( 5 , coup ) ;
c o u t << " e v a l .....,= ....., " << e v a l << e n d l ;
c o u t << " J e ._.j o u e ....., e n ._. " << c o u p . x << " " <<
c o u p . y << en d l ;
v i r u s . jo u e ( coup ) ;
}
return 0 ;
}
2.14.4 Negamax
Attention : Il faut modifier l ' appel à la fonction d 'évaluation pour qu ' il renvoie l ' in
verse de la valeur habituelle lorsque c 'est au joueur Min de jouer. Au niveau 0 on appelle
donc l 'évaluation pour le joueur dont c ' est le tour. Lors de l ' appel récursif on change
le signe du résultat de l ' appel récursif et on prend toujours le coup qui a l 'évaluation
maximale.
l i s t <Coup> l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u a t i o n S i P l u s D e C o u p s P o s s i b l e s ( jo u e u r ) ;
32 Minimax, Alpha-Bêta et heuristiques associées
>=1 8
18
Réponse : La figure 2. 7 donne l ' arbre développé par l' Alpha-Bêta. Il comporte 1 8
noeuds au lieu de 3 9 pour le Minimax. La valeur trouvée et le meilleur coup sont les
mêmes que pour le Minimax.
2.14 Corrigés des exercices 33
18
18 18 18
18 24 20 8 11
/\
1 8 16 1 2 24 20 5 8 11 7
2.14.7 Alpha-Bêta
Virus virus ;
l i s t <Coup> l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u a t i o n S iPlus DeCou p s P o s s i b l e s ( j oueur ) ;
char a u t r e = v i r u s . a d v e r s a i r e ( j o u e u r ) ;
Coup c o u p ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t != l i s t e C o u p s . e n d ( ) ; ++ i t ) {
virus . joue (* i t ) ;
i n t eval = - a l p h ab e t a ( depth - 1 , -beta , -alpha , au tre ,
coup ) ;
i f ( eval > alpha) {
alpha = eval ;
meilleurCoup = * i t ;
}
v i r u s . d ej o u e ( * i t ) ;
i f ( a l p h a >= b e t a )
return beta ;
}
return alpha ;
}
i n t main () {
l i s t <Coup> l i s t e C o u p s ;
while ( true ) {
c o u t << v i r u s ;
l i s t e C o u p s = v i r u s . coupsLegaux ( @ ) ' ' ;
i f ( l i s t e C o u p s . empty ( ) )
break ;
c o u t << " D o n n e z ..... v o t r e ._. c o u p ._. : ._. " ;
Coup c o u p ;
coup . c o u l e u r = @ ; ' '
do {
c i n >> c o u p . x >> c o u p . y ;
} w h i l e ( !v i r u s . c o u p L e g a l ( coup ) ) ;
v i r u s . j o u e ( coup ) ;
c o u t << v i r u s ;
l i s t e C o u p s = v i r u s . coupsLegaux ( 'O ' ) ;
2.14 Corrigés des exercices 35
if ( l i s t e C o u p s . empty ())
break ;
int eval = alphabeta (7 , -Taille * Taille ,
T a i l l e * T a i l l e , 'O ' , coup ) ;
c o u t << " e v a l �=� " << e v a l << e n d l ;
c o u t << " J e �j o u e � e n � " << c o u p . X << " " <<
c o u p . y << e n d l ;
v i r u s . j o u e ( coup ) ;
}
return 0 ;
}
La variation principale de l ' arbre de la figure 2.5 est de toujours choisir le coup le plus
à gauche.
l i s t <Coup > l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u at i o n S i P l u s D e C o u p s P o s s i b l e s (joueur ) ;
char a u t r e = v i r u s . a d v e r s a i r e ( j o u e u r ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n
();
i t ! = l i s t e C o u p s . e n d ( ) ; ++ i t ) {
virus . joue ( * i t ) ;
l i s t <Coup> v p t e m p ;
int eval = - a l p h abeta ( depth - 1 , -beta , -alpha , au tre ,
vptemp ) ;
if ( eval > alph a ) {
alpha = eval ;
vp = v p te m p ;
vp . p u s h _ f r o n t ( * i t ) ;
}
v i r u s . d ej o u e ( * i t ) ;
i f ( a l p h a >= b e t a )
return beta ;
}
return alpha ;
}
36 Minimax, Alpha-Bêta et heuristiques associées
2.14.9 Quiescence
Pour ajouter la recherche de quiescence au jeu du virus, il faut tout d ' abord modifier la
classe Virus de façon à ce qu' elle puisse sélectionner les coups de quiescence. On ajoute
donc dans la classe Virus une fonction qui teste le nombre de cases modifiées par un coup
et une fonction qui renvoie la liste des coups qui modifient plus de cinq cases :
i n t n b C a s e s M o d i fi e e s ( Coup & m) {
i n t nb = 1 . '
c h a r a u t r e = a d v e r s a i r e (m . c o u l e u r ) ;
i n t debutx m. x - 1
· - fi n x = m. x + 1 ;
'
i n t debuty = m. y - 1 fin y = m. y + 1 .
' '
i f ( debutx < 0) debutx = o · '
fo r ( i n t i = d e b u t x ; i <= f i n x ; i + + )
fo r ( i n t j = d e b u t y ; j <= f i n y ; j + + )
i f ( damier [ i ] [ j ] --
au tre )
nb + + ;
r e t u r n nb ;
}
l i s t <Coup> c o u p s Q u i e s c e n c e ( c h a r c o u l e u r ) {
l i s t <Cou p > l i s t e , 1 = c o u p s L e g a u x ( c o u l e u r ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l . b e g i n ( ) ;
i t ! = 1 . e n d ( ) ; ++ i t )
i f ( n b C a s e s M o d i fi e e s ( * i t ) > 4 )
l i s t e . pu sh_back ( * i t ) ;
return l i s t e ;
}
Une fois ces fonctions ajoutées, on écrit une fonction qui fait une recherche Alpha
Bêta uniquement pour les coups de quiescence et on modifie I' Alpha-Bêta pour qu ' il
appelle la fonction de quiescence à la place de lévaluation statique :
int q u i e s c e n c e ( i n t a l p h a , i n t beta , char j o u e u r ) {
1 i s t <Coup> c o u p s = v i r u s . c o u p s Q u i e s c e n c e ( j o u e u r ) ;
i f ( c o u p s . empty ( ) )
return v i r u s . e v a l u at i o n (joueur ) ;
char a u t r e = v i r u s . a d v e r s a i r e ( j o u e u r ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = c o u p s . b e g i n ( ) ;
i t ! = c o u p s . e n d ( ) ; ++ i t ) {
virus . joue (* Î t ) ;
i n t eval = - quiescence (- beta , -alpha , au tre ) ;
v i r u s . d ej o u e ( * i t ) ;
2.14 Corrigés des exercices 37
int a l p h a b e t a ( i n t d e p th , i n t a l p h a , i n t b e t a , char j o u e u r ,
l i s t <Coup > & v p ) {
i f ( d e p t h == 0 )
return q u i e s c e n c e ( alpha , beta , j o u e u r ) ;
l i s t <Coup > l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v al u at i o n S i P l u s D e C o u p s Po s s i b l e s (joueur ) ;
char au t r e = v i r u s . a d v e r s a i r e ( j o u e u r ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n
();
i t ! = l i s t e C o u p s . e n d ( ) ; ++ i t ) {
virus . joue (* i t ) ;
1 i s t <Coup> v p t e m p ;
int eval = -alphabeta ( depth - 1 , -beta , -alpha , autre ,
vptemp ) ;
i f ( eval > alpha ) {
alpha = eval ;
vp = v p t e m p ;
vp . p u s h _ f r o n t ( * i t ) ;
}
v i r u s . d ej o u e ( * i t ) ;
i f ( a l p h a >= b e t a )
return beta ;
}
return alpha ;
}
On déclare une variable globale qui donne le temps maximum au bout duquel on
renvoie une réponse. Dès que l' Alpha-Bêta dépasse ce temps il est stoppé et on renvoie la
variation principale de l' itération précédente :
V i ru s virus ;
const int P r o fo n d e u r M a x = 6 4 ;
l i s t <Coup> c o u p s = v i r u s . c o u p s Q u i e s c e n c e ( j o u e u r ) ;
i f ( c o u p s . empty ( ) )
return v i r u s . e v a l u at i o n ( joueur ) ;
char a u t r e = v i r u s . a d v e r s a i re ( j o u e u r ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = c o u p s . b e g i n ( ) ;
i t ! = c o u p s . e n d ( ) ; ++ i t ) {
virus . joue ( * i t ) ;
int eval = -quiescence (- beta , -alpha , au tre ) ;
v i r u s . d ej o u e ( * i t ) ;
i f ( eval > alpha )
alpha = eval ;
i f ( a l p h a >= b e t a )
return beta ;
}
return alpha ;
}
if ( d e p t h == 0 )
return quiescence ( alpha , beta , joueur ) ;
char a u t r e = v i r u s . a d v e r s a i r e ( j o u e u r ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ();
i t ! = 1 i s t e C o u p s . e n d ( ) ; ++ i t ) {
virus . joue (* i t ) ;
1 i s t <Coup > v p te m p ;
i n t eval = - a l p h abeta ( depth - 1 , -beta , -alpha , au tre ,
v p te m p ) ;
i f ( eval > alpha ) {
alpha = eval ;
vp = vptemp ;
2.14 Corrigés des exercices 39
vp . p u s h _fro n t ( * i t ) ;
}
v i r u s . d ej o u e ( * i t ) ;
i f ( a l p h a >= b e t a )
return beta ;
}
return alpha ;
}
i n t a p p r o f o n d i s s e m e n t l t e r a t i f ( l i s t <Coup> & vp ) {
int eval = v i r u s . e v a l u a t i o n ( 'O ' ) ;
l i s t <Coup> vpTemp ;
clockS tart = clock ( ) ;
fo r ( i n t d = 1 ;
( c l o c k ( ) - c l o c k S t a r t < m a x C l o c k ) &&
( d < P r o fo n d e u r M a x ) ;
d++) {
i n t ev alTemp = a l p h a b e t a ( d , - T a i l l e * T a i l l e , T a i l l e *
T a i l l e , ' O ' , vpTemp ) ;
if ( clock ( ) c l o c k S t a r t < maxClock ) {
e v a l = ev alTemp ;
v p = vpTemp ;
}
}
return eval ;
}
i n t main () {
l i s t <Coup> l i s t e C o u p s ;
while ( true ) {
c o u t << v i r u s ;
l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( '@ ' ) ;
i f ( l i s t e C o u p s . empty { ) )
break ;
c o u t << " D o n n e z....., v o t r e ....., c o u p._. : ._. " ;
Coup c o u p ;
coup . c o u l e u r = '@ ' ;
do {
c i n >> c o u p . x >> c o u p . y ;
} while ( ! v i r u s . c o u p Le g a l ( coup ) ) ;
v i r u s . j o u e ( coup ) ;
c o u t << v i r u s ;
l i s t e C o u p s = v i r u s . coupsLegaux ( 'O ' ) ;
i f ( l i s t e C o u p s . empty ( ) )
break ;
l i s t <Coup> vp ;
i n t e v a l = a p p r o fo n d i s s e m e n t l t e r a t i f ( vp ) ;
40 Minimax, Alpha-Bêta et heuristiques associées
coup = * Yp . be g i n ( ) ;
c o u t << " e v a l �=� " << e v a l << e n d ! ;
c o u t << " V a r i a t i o n � : � " ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = vp . b e g i n ( ) ;
i t ! = vp . e n d ( ) ; ++ i t )
c o u t << * Î t << " � " ;
c o u t << e n d ! ;
c o u t << " J e �j o u e �e n � " << c o u p . x << " " <<
c o u p . y << e n d ! ;
v i r u s . j o u e ( coup ) ;
}
return 0 ;
}
Coup c o u p Q u i T u e [ P r o fo n d e u r M a x ] [2] ;
v o i d m i s e A J o u r C o u p Q u i T u e ( i n t d , Coup & c o u p ) {
i f ( coupQuiTue [ d ] [ O ] ! = coup ) {
coupQuiTue [ d ] [ 1 ] = coupQuiTue [ d ] [0] ;
coupQu iTue [ d ] [ O ] = coup ;
}
}
if ( depth == 0 )
/ * H e u r i s t i q u e d e s c o up s q u i t u e n t * /
Coup k i l l e r = c o u p Q u i T u e [ d e p t h ] [ 0 ] ;
Coup s e c o n d K i l l e r = c o u p Q u i T u e [ d e p t h ] [ 1 ] ;
i f ( k i 1 1 e r . c o u 1 e u r == j o u e u r )
i f ( v irus . coupLegal ( k i l l e r ) )
joueAlphaBeta ( k i l l e r , depth , alpha , beta , j o u e u r ,
vp ) ;
i f ( alpha < beta )
i f ( s e c o n d K i 1 1 e r . c o u 1 e u r == j o u e u r )
i f ( virus . coupLegal ( secondKiller ) )
j o u e A l p h a B e t a ( s e c o n d K i l l e r , depth , alpha , beta ,
j o u e u r , vp ) ;
/ * F i n de l ' h e u r i s t i q u e d e s c o up s q u i t u e n t * /
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
( i t ! = l i s t e C o u p s . e n d ( ) ) && ( a l p h a < b e t a ) ; ++ i t )
i f ( ( * i t ! = k i l l e r ) && ( * i t ! = s e c o n d K i l l e r ) )
j o u e A l p h a B e t a ( * i t , depth , a l p h a , b e t a , j o u e u r , vp ) ;
return alpha ;
}
Au jeu du virus on fait correspondre à un coup l ' indice de son intersection si c ' est un
coup noir, et le nombre de cases du damier plus l ' indice de son intersection si c'est un
coup blanc. Les indices des intersections commencent à 0 pour l ' intersection la plus en
haut à gauche puis s' incrémentent en avançant vers la droite et en descendant d' une ligne
au bout de chaque ligne de cases.
Dans le code ci-dessous MaxNombre code le nombre d' indices possibles et la fonc
tion nombre ( ) donne l ' indice du coup.
void i n i t H i s t o r i q u e ( ) {
fo r ( i n t j = O ; j < MaxNombre ; j ++)
scoreHistorique [j ] = O;
}
c l a s s Coup {
public :
char c o u l e u r ;
int X , y ;
b o o l o p e r a t o r ! = ( c o n s t Coup & c ) {
i f ( ( couleur != c . couleur ) Il
(x != c . x) I l
(y != c . y))
return true ;
return fa l s e ;
}
b o o l o p e r a t o r < ( Coup c ) {
r e t u r n s c o r e H i s t o r i q u e [ no mbre ()] >
s c o re H i s torique [ c . n o mbre ( ) ] ;
}
i n t n o mbre ( ) {
i f ( c o u l e u r == @ ) ' '
return x + y * T a i l l e ;
return T a i l l e * T a i l l e + x + y * T a i l l e ;
}
if ( d e p t h == 0 )
return q u i e s c e n c e ( alpha , beta , joueur ) ;
l i s t <Coup > l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u a t i o n S i P l u s D e C o u p s P o s s i b l e s (joueur ) ;
char a u t r e = v i r u s . a d v e r s a i r e ( j o u e u r ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n
();
i t ! = 1 i s t e C o u p s . end ( ) ; ++ i t ) {
v i r u s . j oue ( * i t ) ;
1 i s t <Coup > v p t e m p ;
i n t eval = - a l p h a b e t a ( depth - 1 , -beta , -alpha , au tre ,
vptemp ) ;
i f ( eval > alph a ) {
alpha = eval ;
vp = v p t e m p ;
vp . p u s h _ fr o n t ( * i t ) ;
}
v i r u s . d ej o u e ( * i t ) ;
i f ( a l p h a >= b e t a ) {
s c o r e H i s t o r i q u e [ i t ->n o mbre ( ) ] += 4 << ( d e p t h * 2 ) ;
return beta ;
}
}
return alpha ;
}
i n t a p p r o f o n d i s s e m e n t l t e r a t i f ( l i s t <Coup> & v p ) {
i n t eval = v i r u s . e v a l u a t i o n ( 'O ' ) ;
1 i s t <Coup > vpTemp ;
clockS tart = clock ( ) ;
initHistorique ( ) ;
fo r ( i n t d = 1 ;
( c l o c k ( ) - c l o c k S t a r t < m a x C l o c k ) &&
( d < P r o fo n d e u r M a x ) ;
d ++) {
i n t e v alTemp = a l p h a b e t a ( d , - T a i l l e * T a i l l e , T a i l l e *
T a i l l e , ' O ' , vpTemp ) ;
i f ( clock ( ) c l o c k S t a r t < maxClock ) {
e v a l = ev alTemp ;
v p = vpTemp ;
}
}
return e v a l ;
44 Minimax, Alpha-Bêta et heuristiques associées
i n t r e c h e r c h e A s p i r a n t e ( l i s t <Coup> & v p ) {
int eval = virus . evaluation ( O ) ; ' '
l i s t <Coup> vpTemp ;
c l o c k S t art = clock ( ) ;
initHistorique ( ) ;
int alpha = -Taille * Taille , beta = Taille * Taille ;
fo r ( i n t d = l ;
( c l o c k ( ) - c l o c k S t a r t < m a x C l o c k ) &&
( d < P r o fo n d e u r M a x ) ;
d++) {
i n t ev alTemp = a l p h a b e t a ( d , a l p h a , b e t a , O ' vpTemp ) ;
' ,
i f ( e v a l T e m p <= a l p h a )
ev alTemp = a l p h a b e t a ( d , - T a i l l e * T a i l l e , a l p h a ,
' 'O , vpTemp ) ;
e l s e i f ( e v al T e m p >= b e t a )
evalTemp = a l p h a b e t a ( d , b e t a , T a i l l e * T a i l l e ,
' O ' , vpTemp ) ;
i f ( c l o c k ( ) - c l o c k S t a r t < maxClock ) {
e v a l = e v alTemp ;
alpha = eval - 2;
beta = eval + 2 ;
v p = vpTemp ;
}
}
return eval ;
}
if ( d e p t h == 0 )
return q u i e s c e n c e ( alpha , beta , joueur ) ;
l i s t <Coup> l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
if ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u a t i o n S i P l u s DeCou p s P o s s i b l e s ( j o ueur ) ;
2.14 Corrigés des exercices 45
char a u t re = v i r u s . a d v e r s a i re ( j o u e u r ) ;
b o o l V a r i a t i o n P r i n c i p a l e T r o u v e e = fa l s e ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n
();
i t ! = l i s t e C o u p s . e n d ( ) ; ++ i t ) {
virus . joue (* i t ) ;
1 i s t <Coup > v p t e m p ;
int eval ;
if ( VariationPrincipaleTrouvee ) {
eval = -alphabeta ( depth - 1 , -alpha - 1 , -alpha ,
a u t r e , v p te m p ) ;
i f ( eval > alph a )
eval = -alphabeta ( depth - 1 , -beta , -alpha ,
a u t r e , v p te m p ) ;
}
else
e v a l = -alphabe t a ( depth - 1 , -beta , -alpha , au tre ,
vptemp ) ;
i f ( eval > alph a ) {
alpha = eval ;
V a r i a t i o n P r i n c i p a l e T r o u v e e = true ;
vp = v p t e m p ;
vp . p u s h _ f r o n t ( * i t ) ;
}
v i r u s . d ej o u e ( * i t ) ;
i f ( a 1 p h a >= b e t a ) {
s c o r e H i s t o r i q u e [ i t ->n o mbre ( ) ] += 4 « ( d e p t h * 2 ) ;
return beta ;
}
}
return alpha ;
}
if ( d e p t h == 0 )
return q u i e s c e n c e ( alpha , beta , joueur ) ;
if ( depth > R + 1 ) {
1 i s t <Cou p > v p te m p ;
int eval = -alphabeta ( depth - 1 - R, -beta , -beta + 1 ,
a u t r e , vptemp ) ;
if ( e v a 1 >= b e t a )
return beta ;
}
/ * fi n des c o up e s selectives avec c o up nul */
l i s t <Coup> l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u a t i o n S i Pl u s D e C o u p s P o s s i b l e s (joueur ) ;
l i s teCoups . s o r t ();
if ( d e p t h == 0 )
return quiescence ( alpha , beta , j o u e u r ) ;
char a u t r e = v i r u s . a d v e r s a i r e ( joueur ) ;
int R = 3 ;
2.14 Corrigés des exercices 47
b o o l f a i ! H i g h = fa l s e ;
i f ( depth > R + 1 ) {
1 i s t <Coup> v p t e m p ;
int eval = -alphabeta ( depth - 1 - R, -beta , -beta + 1 ,
a u t r e , v p temp , v e r i f y ) ;
if ( e v a 1 >= b e t a )
if ( v e r i fy ) {
depth --;
v e r i fy = fa l s e ;
fa i ! H i g h = true ;
}
else
return beta ;
}
I i s t <Coup> l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
i f ( I i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u at i o n S i P l u s D e C o u p s Po s s ib l e s ( j o ueur ) ;
l i s te C o u p s . s o r t ();
research :
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t ! = I i s t e C o u p s . e n d ( ) ; ++ i t ) {
virus . joue (* i t ) ;
1 i s t <Coup> v p t e m p ;
i n t eval = - a l p h a b e t a ( depth - 1 , -beta , -alpha , au tre ,
vptemp , v e r i f y ) ;
if ( eval > alph a ) {
alpha = eval ;
vp = v p t e m p ;
vp . p u s h _ f r o n t ( * i t ) ;
}
v i r u s . d ej o u e ( * i t ) ;
i f ( a l p h a >= b e t a ) {
s c o r e H i s t o r i q u e [ i t ->n o m b re ( ) ] += 4 << ( d e p t h * 2 ) ;
return beta ;
}
}
if ( f a i ! H i g h && a l p h a < b e t a ) {
depth ++;
f a i ! H i g h = fa l s e ;
v e r i fy = true ;
goto r e s e a r c h ;
}
48 Minimax, Alpha-Bêta et heuristiques associées
return alpha ;
}
1 i s t <Coup> vpTemp ;
clockS tart = clock ( ) ;
initHistorique ( ) ;
fo r ( i n t d = l ;
( c l o c k ( ) - c l o c k S t a r t < m a x C l oc k ) &&
( d < P r o fo n d e u r M a x ) ;
d++) {
i n t ev alTemp alphabeta (d , -Tai lle * Taille , Taille *
=
Tables de Transposition
"Même si l ' adversaire joue le coup analysé précédemment, recommencez l ' analyse
en voyant la nouvelle position. "
Benjamin Blumenfeld.
3.1 Introduction
Pour ne pas réexplorer un sous arbre qu 'on a déjà exploré à partir d' une position déjà
rencontrée on peut stocker dans une table de hachage les positions déjà rencontrées et
évaluées au cours du parcours de l ' arbre. Avant d'explorer une position, on commence
par regarder si elle n ' a pas déjà été évaluée et stockée dans la table de hachage, ce qui
permet dans le meilleur des cas d'éviter de refaire une deuxième fois la recherche sur
cette position. Dans les autres cas cela permet tout de même de réutiliser les informations
stockées pour accélérer la nouvelle recherche.
Une table de transposition est une table de hachage dont chaque entrée contient des
informations sur la recherche effectuée à partir d' une position.
Les tables de transposition sont utiles aussi bien pour les problèmes à un joueur en
combinaison avec A * (voir chapitres 8 et 9) par exemple que pour les jeux à deux joueurs
en combinaison avec l' Alpha-Bêta.
de I ' Alpha-Bêta est plus grand quand on cherche les meilleurs coups en premier. Les
tables de transposition associées à l ' approfondissement itératif permettent de se souvenir,
pour chaque position cherchée, du meilleur coup trouvé par la recherche précédente sur
cette position. En utilisant les tables de transposition on peut alors essayer en priorité le
meilleur coup trouvé lors de l ' itération précédente ce qui permet d' augmenter le nombre
de coupes Alpha-Bêta.
On veut stocker, au cours d' une recherche arborescente, toutes les positions rencon
trées afin de ne pas refaire plusieurs fois les mêmes calculs. On se heurte à un problème
de mémoire, la place nécessaire pour stocker toutes ces positions est généralement trop
grande pour la mémoire disponible. De plus, on veut pouvoir vérifier très rapidement si
une position a déjà été rencontrée. On doit donc associer une position à un nombre qui
sera stocké dans une table de hachage.
La méthode la plus courante pour hacher une position est le hachage de Zobrist [94] .
On associe un nombre aléatoire, fixé une fois pour toutes, à chaque valeur possible de
chaque emplacement possible du damier. On peut aussi utiliser un nombre aléatoire pour
coder la couleur du joueur qui a la main ainsi que d' autres propriétés de la position parti
culières au jeu. On code la position physique mais aussi certaines propriétés dûes à l ' his
torique de la position (par exemple les droits de roque aux Échecs). La valeur de hachage
d' une position est le XOR de tous les nombres aléatoires associés à la position.
L' indice auquel seront stockées les informations liées à une position est la valeur de
hachage tronquée aux n derniers bits pour une table de transposition de taille 2n .
Au Tic-Tac-Toe on a deux nombres aléatoires par case : un pour coder le cas où la case
est blanche, un autre pour coder le cas où elle est noire. On ne fait pas d'opérations pour
les cases vides. On utilise donc 9+9 = 1 8 nombres aléatoires. Le hachage d' une position
de Tic-Tac-Toe est égal au XOR de tous les nombres aléatoires correspondant à chaque
case.
Une position est représentée par le XOR des nombres aléatoires qui correspondent aux
propriétés de la position et à la valeur de chaque emplacement. Chaque nombre aléatoire
peut être codé sur 32 ou 64 bits selon la probabilité de collision et d'erreur que l'on
accepte (voir la section suivante sur la probabilité d' erreur).
3.3 Probabilité d'erreur 51
Exercice : Écrire une fonction qui joue un coup au Tic-Tac-Toe et une fonction qui
déjoue un coup, en mettant à jour incrémentalement la valeur de hachage.
"L' ordinateur vous permet de faire plus d' erreurs plus vite que n ' importe quelle autre
invention de l'histoire de l 'humanité, à l ' exception possible des armes à feu et de la te
quila. "
Mitch Ratcliffe.
- Une erreur de type 1 intervient quand deux positions différentes ont des valeurs de
hachage égales. Un moyen de détecter ces erreurs est de tester si le meilleur coup
mémorisé dans cette position est légal. La probabilité d' une erreur de type 1 est
diminuée quand on augmente le nombre de bits dans la valeur de hachage.
- Une erreur de type 2 intervient lorsque deux positions différentes ont le même in
dice dans la table de transposition mais pas la même valeur de hachage. Cette er
reur est définie comme une collision [50) . Lorsqu ' on a une collision, on doit choisir
entre les deux positions celle qui doit être gardée dans la table de transposition. La
probabilité d' avoir une collision est diminuée quand on augmente la taille de la
table de transposition.
Réponse :
En général on utilise la valeur de hachage pour calculer l ' indice dans la table de trans
position en tronquant la valeur de hachage aux n bits de poids faible. On obtient alors un
indice dans une table de transposition de taille 2".
Pour chaque entrée de la table de transposition on a une structure qui contient des
informations sur la position correspondante déjà explorée.
Réponse : Les informations suivantes sont généralement stockées dans une entrée de
la table de transposition :
- La clé : elle contient les bits tronqués de la valeur de hachage. Elle est utilisée pour
3.6 Utilisation de la table de transposition 53
différencier les positions qui ont le même indice dans la table de transposition mais
des valeurs de hachage différentes.
- Le meilleur coup trouvé dans cette position : c'est soit le coup qui a obtenu le
meilleur score, soit le coup qui a permis une coupe Alpha-Bêta. On l 'essaiera en
premier la prochaine fois qu ' on rencontrera la position.
- Le score : la valeur retournée par ! ' Alpha-Bêta dans cette position.
- Le drapeau : il indique si le score est le score exact, si c 'est une borne maximale ou
une borne minimale.
- La profondeur : elle indique la profondeur du sous arbre exploré pour évaluer la
position.
Exercice : Définir une classe GenericTranspo qui représente les entrées de la table
de transposition. Écrire ensuite un classe Table générique qui prend comme paramètres
template les classes Move, Board et Transpo. Écrire les méthodes de cette classe pour
détecter les transpositions et pour ajouter une entrée dans la table en utilisant la stratégie
de la profondeur.
Lorsqu 'on utilise l ' approfondissement itératif les tables de transposition réduisent
beaucoup l 'effort de recherche. On a vu qu'on pouvait selon les cas couper directement la
recherche à l ' aide des résultats stockés dans une entrée, ou de façon plus courante diriger
la recherche à l ' aide des informations stockées.
Question : Lorsqu 'on atteint dans une recherche une position qui a une entrée dans
la table de transposition, comment utilise-t-on les informations stockées dans cette entrée
pour optimiser la recherche ?
Réponse :
Il y a trois possibilités :
bonnes chances qu ' il soit le meilleur dans la recherche en cours puisqu 'il a déjà été
le meilleur dans une recherche précédente moins profonde.
Exercice : Adapter l ' algorithme Alpha-Bêta appliqué au jeu du virus à l ' aide des
classes GenericTranspo et Table pour qu ' il utilise une table de transposition.
Les coupes de transposition améliorées consistent à tester pour chaque fils de la po
sition courante s ' il est présent dans la table de transposition et si ses valeurs stockées
permettent de faire une coupe. Pour cela on va jouer chaque coup possible, voir si la po
sition résultante est contenue dans la table de transposition et, si c 'est le cas, regarder les
informations stockées pour savoir si elles permettent de faire une coupe. Les coupes de
transposition améliorées permettent de maximiser l' utilisation de l ' information contenue
dans les tables de transposition.
Cette optimisation a été utilisée dans Chinook [78] le meilleur programme de Che
ckers (Dames anglaises). Les arbres de recherche de Chinook ont 22% de noeuds en
moins pour des recherches de profondeur 17 lorsqu 'il utilise les coupes de transposition
améliorées [79] . Toutefois, si on essaie les coupes améliorées à tous les niveaux de l' arbre,
la réduction du nombre de noeuds est contrebalancée par le temps additionnel passé dans
chaque noeud à tester les coupes possibles. Les coupes améliorées ne sont donc pas tes
tées dans les deux dernières profondeurs de l' arbre, ce qui permet de réduire le temps
d'exécution tout en gardant les coupes les plus importantes.
Exercice : Intégrer les coupes de transposition améliorées dans l' Alpha-Bêta avec
tables de transpositions.
L' idée de la recherche avec partition [37] est de mémoriser un groupe de positions
qui ont des caractéristiques communes plutôt qu ' une position à la fois. Si par exemple,
on veut colorier une carte avec trois couleurs et qu 'une impossibilité impliquant 5 pays
est détectée, on peut se souvenir de la configuration des 5 pays qui donne toujours une
impossibilité. On peut alors déclarer impossible toutes les cartes qui contienne cette confi
guration et arrêter la recherche dès que la configuration est reconnue.
Au Bridge on stocke dans une partition les relations d'ordre entre les cartes plutôt que
les cartes elles mêmes. Ainsi une entrée de la table représente de nombreuses positions
pour lesquelles le résultat de la recherche est le même. La résolution de donnes ouvertes
au Bridge va de IO à 1 00 fois plus vite lorsqu ' on utilise les partitions.
Cette technique est liée à l' apprentissage par généralisation [67, 63, 3 1 ] qui sélec-
3.9 Corrigés des exercices SS
tionne un sous ensemble des éléments d' une position qui représente l 'explication d'un
succès ou d'un échec (l'ensemble des faits desquels on a pu déduire le succès ou l 'échec).
L' apprentissage par généralisation consiste alors à généraliser cet ensemble de faits (prin
cipalement en remplaçant les variables instanciées par les variables originales [ 1 6]), puis à
le transformer en une règle que l ' on peut vérifier efficacement (en réordonnant les condi
tions de la règle et en l ' insérant de façon optimisée dans une base de règles par exemple)
[ 1 5, 1 7).
c o n s t i n t Vide = O ;
c o n s t i n t Noir = 1 ;
c o n s t i n t B l anc = 2 ;
i n t damier [ 3 ] [ 3 ] ;
u n s i g n e d l o n g l o n g H a s h A rr a y [3] [3] [2] ;
unsigned long long hash = 0 ;
/ * i n i t i a l i s a t i o n d e s n o mbres a l é a t o i r e s * /
void initHash ( ) {
for ( i n t i = O ; i < 3 ; i ++)
fo r ( i n t j = 0 ; j < 3 ; j + + )
for ( i n t k = O ; k < 2 ; k++) {
HashArray [ i ] [ j ] [ k ] = 0 ;
fo r ( i n t b = O ; b < 6 4 ; b + + )
i f ( ( r a n d ( ) / ( RAND_MAX + 1 . 0 ) ) > 0 . 5 )
H a s h A r r a y [ i ] [ j ] [ k ] I = ( I ULL « b ) ;
}
}
! * c a l c u l de l a v a l e u r d e h a c h a g e * /
void calculeHash ( ) {
hash = O ;
fo r ( i n t i = O ; i < 3 ; i ++)
fo r ( i n t j = O ; j < 3 ; j ++)
i f ( damier [ i ] [ j ] == Noir )
h a s h "= H a s h A rr a y [ i ] [ j ] [ 0 ] ;
e 1 s e i f ( d a m i e r [ i ] [ j ] == B 1 a n c )
h a s h " = H a s h A rr a y [ i ] [ j ] [ 1 ] ;
}
56 Tables de Transposition
- Aux Échecs, on utilise 12 nombres aléatoires par case (u n par pièce différe nte), plus
4 nombres pour les droits de roques, plus 1 6 nombres pour les captures e n passant,
et u n pour lacouleur quijoue. S oit 64 x 1 2 + 4 + 16 + 1 = 789 nombres aléatoires.
- A u jeu du virus, on utilise seuleme nt 98 nombres aléatoires, deux par case. Il est
i nutile d'avoir u n nombre pour lacouleur dujoueur puisqu'il est impossible d'avoir
deux positions ide ntiques avec deux couleurs à jouer différe ntes lorsque le même
joueur comme nce à jouer (chaque cou p ajoute une pièce).
v o i d jou e ( i n t x, i n t y , i n t coul e u r ) {
d ami e r [x] [y] = coul e u r ;
i f (coul e u r == N oir)
h as h " = H ashA rray [x] [y] [0] ;
e l s e i f (coul e u r == B lanc)
h as h " = H ashA rray [ x] [y] [ 1] ;
}
v o i d d ejou e ( i n t x, i n t y , i n t coul e u r ) {
d ami e r [x] [y] = V ide ;
i f ( c ou 1 e u r == N oi r )
h as h " = H as hA rray [x] [y] [0] ;
e l s e i f (coul e u r == B la nc)
h as h " = H as hA rray [x] [y] [1] ;
}
p = (1 - -k ) X (1 - il ) X ... X (1 - MN l )
S i M est petit par rap port aN, on peut fa ire l'ap proximation suivante:
P ,...
- l
.., _ 1+2+ . . . + ( M - 1 ) ,....., l
N -
_
M
2N
2
Pour les x positifs proches de 0, on a log( l - x) '.::::'. - x, donc log(P) '.::::'. - M(�- l ) ,
M2 M(M-1)
d'où, pour des M assez grands P '.::::'. e - ----vv- '.::::'. e - 2 N
t e m p l a t e < c l a s s Move>
class GenericTranspo {
public :
bool sc oreEx act ;
short int score ;
unsigned char d e p t h ;
Move b e s t ;
unsigned long long hash ;
};
public :
Table ( ) {
t a b l e = new T r a n s p o [ S i z e T a b l e + 1 ] ;
}
T r a n s p o * l o o k ( B o a rd * b ) {
T r a n s p o * t r a n s = & t a b l e [ b ->h a s h ( ) & S i zeTable ] ;
i f ( t r a n s ->h a s h == b ->h a s h ( ) )
return t r a n s ;
r e t u r n NULL ;
}
b o o l add ( B o ard * b , u n s i g n e d c h a r d e p t h ,
short i n t score ,
c o n s t Move & b e s t , b o o l s c o r e E x a c t ) {
Tran spo * t r a n s = & t a b l e [ b ->h a s h ( ) & SizeTable ] ;
if ( t r a n s ->d e p t h >= d e p t h )
return fa l s e ;
trans ->h a s h = b ->h a s h ( ) ;
trans -> s c o r e = s c o r e ;
trans -> s c o r e E x a c t = s c o r e E x a c t ;
trans -> d e p t h = d e p t h ;
trans -> b e s t = b e s t ;
58 Tables de Transposition
return true ;
}
};
/* initialisation des n o m b re s a l éa t o i r e s */
void i n i t H a s h ( ) {
fo r ( i n t i = O ; i < T a i l l e ; i + + )
fo r ( i n t j = O ; j < T a i l l e ; j + + )
fo r ( i n t k = O ; k < 2 ; k + + ) {
H a s h A rr a y [ i ] [ j ] [ k ] = O ;
fo r ( i n t b = O ; b < 6 4 ; b + + )
i f ( ( r a n d ( ) / (RAND_MAX + 1 . 0 ) ) > 0 . 5 )
H a s h A r r a y [ i ] [ j ] [ k ] I = ( l ULL « b ) ;
}
}
class Virus {
public :
char damier [ T a i l l e ] [ T a i l l e ] ;
i n t nbNoirs , n b B l an c s ;
i n t n b M o d i fi c a t i o n s ;
i n t p i l e M o d i fi c a t i o n s [20 * T a i l l e * Taille ] ;
unsigned long long hashcode ;
Virus ( ) {
init () ;
}
void i n i t ( ) {
fo r ( i n t i = O ; i < T a i l l e ; i + + )
fo r ( i n t j = O ; j < T a i l l e ; j + + )
damier [ i ] [ j ] = ' + ' ;
hashcode = 0 ;
d a m i e r [ 0 ] [ O ] = '@ ' ;
3.9 Corrigés des exercices 59
h a s h c o d e A = H a s h A rr a y [ 0 ] [ O ] [ O ] ;
d a m i e r [ T a i l l e - 1 ] [ T a i l l e - 1 ] = '@ ' ;
h a s h c o d e A= H a s h A rr a y [ T a i l l e - l ] [ T a i l l e 1] [0] ;
damier [ T a i l l e - 1 ] [ O ] = 'O ' ;
h a s h c o d e A= H a s h A r r a y [ T a i l l e - 1 ] [ O ] [ 1 ] ;
damier [ O ] [ T a i l l e - 1 ] = 'O ' ;
h a s h c o d e A= H a s h A rr a y [ 0 ] [ T a i l l e - 1 ] [ 1 ] ;
nbNoirs = 2 ;
nbBlancs = 2 ;
n b M o d i fi c a t i o n s = 0 ;
}
i f ( fi n y > T a i l l e - 1 ) fi n y = T a i l l e 1;
fo r ( i n t i = d e b u t x ; i <= f i n x ; i + + )
fo r ( i n t j = d e b u t y ; j < = f i n y ; j + + )
if ( damier [ i ] [ j ] == a u t re ) {
damier [ i ] [ j ] = m. c o u l e u r ;
h a s h c o d e A = H a s h A rr a y [m . x ] [ m . y ] [0 ] ;
h a s h c o d e A = H a s h A rr a y [ m . x ] [ m . y ] [1];
p i l e M o d i fi c a t i o n s [ n b M o d i fi c a t i o n s ] = i '
n b M o d i fi c a t i o n s + + ;
p i l e M o d i fi c a t i o n s [ n b M o d i fi c a t i o n s ] = j '
n b M o d i fi c a t i o n s + + ;
n b S w ap s + + ;
i f ( m . c o u l e u r = = '@ ' ) {
60 Tables de Transposition
nbNoirs ++;
nbBlancs --;
}
else {
nbNoirs --;
n b B l a n c s ++ ;
}
}
p i l e M o d i f i c a t i o n s [ n b M o d i f i c a t i o n s ] = n b S w ap s ;
n b M o d i fi c a t i o n s + + ;
}
v o i d d ej o u e ( c o n s t Coup & m ) {
i n t x , y , n b S w ap s ;
char a u t r e = a d v e r s a i re (m. c o u l e u r ) ;
n b M o d i fi c a t i o n s - - ;
n b S w a p s = p i l e M o d i f i c a t i o n s [ n b M o d i fi c a t i o n s ] ;
fo r ( i n t i = O ; i < n b S w ap s ; i + + ) {
n b M o d i fi c a t i o n s --;
y = p i l e M o d i f i c a t i o n s [ n b M o d i fi c a t i o n s ] ;
n b M o d i fi c a t i o n s - - ;
x = p i l e M o d i f i c a t i o n s [ n b M o d i fi c a t i o n s ] ;
damier [ x ] [ y ] = au tre ;
h a s h c o d e A = H a s h A rr a y [ x ] [ y ] [ O ] ;
h a s h c o d e A= H a s h A r r a y [ x ] [ y ] [ 1 ] ;
i f ( m . c o u l e u r == '@ ' ) {
nbNoirs --;
nbBlancs ++;
}
else {
nbNoirs ++;
nbBlancs --;
}
}
n b M o d i fi c a t i o n s - - ;
y = p i l e M o d i fi c a t i o n s [ n b M o d i fi c a t i o n s ] ;
n b M odifications --;
x = p i l e M o d i f i c a t i o n s [ n b M o d i fi c a t i o n s ] ;
damier [ x ] [ y ] = ' + ' ;
i f ( m . c o u l e u r == '@ ' ) {
h a s h c o d e A= H a s h A r r a y [ x ] [ y ] [ O ] ;
nbNoirs --;
}
else {
h a s h c o d e A= H a s h A rr a y [ x ] [ y ] [ 1 ] ;
nbBlancs --;
}
3.9 Corrigés des exercices 61
}
} ;
Virus virus ;
if ( d ept h == 0 )
Coup t r a n s p o M o v e ;
G e n e r i c T r a n s p o <Coup> * l = TT . l o o k (& v i r u s ) ;
i f ( t ! = NULL) {
i f ( t ->d e p t h >= d e p t h ) {
i f ( t -> s c o r e E x a c t ) r e t u r n t -> s c o r e ;
e l s e a l p h a = max ( a l p h a , ( i n t ) t -> s c o r e ) ;
i f ( a l p h a >= b e t a ) r e t u r n b e t a ;
}
/ * on t e s t e l e c o up de t r a n s p o s i t i o n e n p r e m i e r * /
t r a n s p o M o v e = t -> b e s t ;
i f ( v i r u s . c o u p L e g a l ( transpoMove ) )
j o u e A l p h a B e t a ( t r a n s p o M o v e , d e p t h , a l p h a , b et a ,
j o u e u r , vp ) ;
}
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
( i t ! = l i s t e C o u p s . e n d ( ) ) && ( a l p h a < b e t a ) ; ++ i t )
i f ( ( t == NULL) 1 1 ( * i t ! = t r a n s p o M o v e ) )
j o u e A l p h a B e t a ( * i t , depth , a l p h a , b e t a , j o u e u r , vp ) ;
}
if ( a l p h a >= b e t a )
TT . a d d (& v i r u s , d e p t h , a l p h a , * V p . b e g i n ( ) , f a l s e ) ;
e l s e i f ( ! vp . empty ( ) )
TT . ad d (& v i r u s , d e p t h , a l p h a , * V p . b e g i n ( ) , t r u e ) ;
else i f ()
TT . a d d (& v i r u s , d e p t h , a l p h a , * l i s t e C o u p s . b e g i n ( ) ,
true ) ;
return alpha ;
}
3.9 Corrigés des exercices 63
if ( d e p t h == 0 )
return q u i e s c e n c e ( alpha , beta , j oueur ) ;
char a u t r e = v i r u s . a d v e r s a i r e ( j oueur ) ;
/* C o up e s selectives avec c o up n u l * /
int R = 2;
i f ( depth > R + 1 ) {
1 i s t <Coup> v p t e m p ;
int eval = -alphabeta ( depth 1 - R , -beta ,
- b e t a + 1 , a u t r e , v p te m p ) ;
if ( e v a l >= b e t a )
return beta ;
}
/* fi n des c o up e s selectives avec c o up nul */
Coup t r a n s p o M o v e ;
G e n e r i c T r a n s p o <Coup > * t = TT . l o o k (& v i r u s ) ;
i f ( t ! = NULL) {
i f ( t ->d e p t h >= d e p t h ) {
i f ( t -> s c o r e E x a c t ) r e t u r n t -> s c o r e ;
e l s e a l p h a = max ( a l p h a , ( i n t ) t -> s c o r e ) ;
i f ( a l p h a >= b e t a ) r e t u r n b e t a ;
}
t r a n s p o M o v e = t -> b e s t ;
}
l i s t <Coup > l i s t e C o u p s = v i r u s . c o u p s L e g a u x ( j o u e u r ) ;
i f ( l i s t e C o u p s . empty ( ) )
return v i r u s . e v a l u at i o n S i P l u s D e C o u p s Po s s ib l e s (joueur ) ;
/* c o up e s de t r a n s p o s i t i o n a m é l i o r é e s * /
if ( depth > 2 )
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t ! = l i s t e C o u p s . e n d ( ) && a l p h a < b e t a ;
++ i t ) {
virus . joue (* i t ) ;
G e n e r i c T r a n s p o <Coup > * t l = TT . l o o k (& v i r u s ) ;
i f ( t l ! = NULL)
i f ( t l ->d e p t h >= d e p t h - 1 )
i f ( t l -> s c o r e E x a c t )
64 Tables de Transposition
a l p h a = max ( a l p h a , - t l -> s c o r e ) ;
v i r u s . d ej o u e ( * i t ) ;
i f ( a l p h a >= b e t a ) r e t u r n b e t a ;
}
/* on teste le c o up de t r a n sp o s i t i o n en premier */
if (t ! = NULL && a l p h a < b e t a )
i f ( v i r u s . c o u p L e g a l ( transpoMove ) )
j o u e A l p h a B e t a ( tran spoMove , depth , alpha , beta ,
j o u e u r , vp ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
( i t ! = 1 i s t e C o u p s . e n d ( ) ) && ( a 1 p h a < b e t a ) ; ++ i t )
i f ( ( t == NULL) 1 1 ( * Ï t ! = t r a n s p o M o v e ) )
j o u e A l p h a B e t a ( * i t , depth , a l p h a , b e t a , j o u e u r , vp ) ;
}
i f ( a l p h a >= b e t a )
TI . a d d (& v i r u s , d e p t h , a l p h a , * Yp . b e g i n ( ) , fa I s e ) ;
else i f ( ! vp . empty ( ) )
TI . a d d (& v i r u s , d e p t h , a l p h a , * Y p . b e g i n ( ) , t r u e ) ;
e l s e i f ( ! l i s t e C o u p s . empty ( ) )
TI . a d d (& v i r u s , d e p t h , a l p h a , * l i s t e C o u p s . b e g i n ( ) ,
true ) ;
else
c e r r << " b u g \ n " ;
return alpha ;
}
Chapitre 4
Aaron Nimzowitsch.
4.1 I ntroduction
Contrairement à I 'Alpha-Bêta qui envisage tous les coups possibles, les raisonnements
humains n 'envisagent qu'un très petit nombre de coups et peuvent prévoir des séquences
très profondes en étant très sélectifs. Ils peuvent notamment lire des séquences de coups
forcés très profondes. Ce sont les algorithmes qui permettent d 'effectuer ce type de re
cherche étroite et profonde que nous allons décrire dans ce chapitre.
Les algorithmes de recherche avec menaces [89, 2 1 ] sont efficaces dans les jeux pour
lesquels jouer quelques coups de suite du même joueur permet très souvent de gagner. La
notion de menace sera détaillée et formalisée au cours du chapitre.
Il peut être très utile dans les jeux complexes qui ont un grand nombre de coups
possibles à chaque position d' être sélectif et de ne considérer qu 'un sous ensemble des
coups possibles. Lorsqu'on envisage tous les coups on a une explosion combinatoire qui
réduit la profondeur à laquelle on peut chercher dans un temps fixé. Il peut être bénéfique
de sélectionner les coups à envisager et d'éliminer des coups à priori inutiles. Toutefois,
lorsqu 'on choisit d' ignorer des coups on doit éviter deux revers :
- ne pas explorer des coups amis qui vont se révéler gagnants et donc sous évaluer
une position,
- ne pas explorer des coups ennemis qui nous font perdre, et donc croire qu ' une
position n'est pas perdante alors qu 'elle l'est.
66 Recherche avec menaces
Nous présentons dans ce chapitre des algorithmes de recherche qui permettent non
seulement de faire une sélection sévère des coups à envisager mais aussi de donner des
résultats plus fiables que les algorithmes sélectifs de recherche classiques comme la re
cherche avec coup nul, puisque ces résultats sont prouvés. En effet les algorithmes avec
menaces analysent les raisons pour lesquelles une menace marche et utilisent ces raisons
pour trouver l 'ensemble complet des coups qui invalident la menace. Ainsi ils n 'oublient
jamais de réfutation et sont tout de même sélectifs. Les algorithmes qui utilisent des véri
fications de menaces améliorent à la fois le temps de réponse des programmes de jeux et
leur précision.
4.2 Le Phutball
Le Football des philosophes (ou Phutball) est un jeu décrit dans Winning Ways [6] ,
un ouvrage sur la théorie combinatoire des jeux [29] appliquée à de nombreux jeux. Il a
été surnommé Phutball par J. H. Conway. Les auteurs de Winning Ways pensent que ce
jeu ne peut pas être totalement analysé par la théorie combinatoire des jeux (cf chapitre
1 4) car il est trop complexe.
Le Football des philosophes a été imaginé par Conway pour un damier 1 9x 1 5 . Les
buts étant les lignes de longueur 1 5 . Il est aussi joué sur des damiers de Go l 9x l 9. Une
pierre noire représente la balle et les pierres blanches représentent les joueurs de Football.
Toutes les pièces sont communes aux deux joueurs, et les deux joueurs ont les mêmes
coups légaux.
La partie commence avec un damier vide, la balle est placée sur l ' intersection centrale.
Ensuite, à chaque coup, chaque joueur doit (i) soit poser une nouvelle pierre blanche
sur une intersection vide (ii) soit faire sauter la balle par dessus des pierres blanches en
enlevant les pierres sautées au fur et à mesure de ses sauts.
Un saut peut être dans n ' importe laquelle des 8 directions. On peut prendre une ligne
continue de pierres en sautant par dessus. On peut effectuer plusieurs sauts dans des di
rections différentes dans le même coup. Le but du jeu est de faire parvenir la balle sur la
première ligne du camp adverse, ou derrière cette première ligne.
La figure 4. 1 donne une partie de Phutball 9x9. Après le coup numéro 1 1 , la balle est
sur la dernière ligne à droite et c ' est donc le joueur Gauche qui a gagné.
Exercice : Écrire une classe Phutball qui représente un damier et qui permet de jouer
des coups sur le modèle de la classe Virus du chapitre un. Écrire un programme de Phut
ball qui joue aléatoirement.
4.2 Le Phutball 67
gagnek (P, J) est vrai si pour tous les coups de l ' adversaire de J amenant chacun à
une position P ' , on peut vérifier après le coup que gagnantk' ( P' , J) avec k' < k.
gagnantk (P, J) est vrai s ' il existe un coup pour J tel qu ' après ce coup menant à une
position P', gagnek ( P' , J) soit vrai.
Exercice : Trouver des positions au Go-Moku (on rappelle que le but du jeu est
d' être le premier à aligner cinq croix horizontalement, verticalement ou en diagonale sur
une grille) qui sont prouvées avec des recherches gagnant0, g agnant1 , gagnant2 puis
gagnant3 .
Définies comme cela les fonctions n ' utilisent pas de menaces et correspondent à un
Minimax classique. Toutefois on peut observer que si un coup est gagnant au niveau k, la
position une fois le coup joué contient forcément un coup gagnant au niveau k 1. Donc
-
gagnantk (P, J) ne peut être vrai que si g agnantk - I (P' , J) est aussi vrai après le coup
gagnant. Donc si gagnantk - i (P' , J) n'est pas vrai après un coup, ce n'est pas la peine
d'essayer de vérifier gagnantk (P, J) et on peut couper la vérification.
Exercice : Écrire un fonction qui détecte les menaces directes d' ordre n au Phutball
en coupant les vérifications inutiles.
4.4 La recherche À
La recherche >. (,\ search) [89] fait appel à des arbres lambda et des coups lambda. Un
arbre lambda d' ordre n est une arbre de recherche qui contient des coups lambda d'ordre
n. Un coup lambda d' ordre n pour l' attaquant est un coup qui implique qu ' il existe au
moins un arbre lambda d ' ordre strictement inférieur à n qui suit le coup. Un coup d'ordre
n pour le défenseur est un coup qui implique qu 'il n ' y a aucun arbre gagnant d'ordre
strictement inférieur à n après le coup.
La recherche ,\ est une recherche qui est adaptée aux jeux qui contiennent de nom
breuses menaces. Elle joue plusieurs coups de suite de la même couleur et ne continue la
recherche que si la position est gagnante après ces plusieurs coups. De manière générale,
elle ne fait une recherche à l 'ordre n que s ' il existe un coup de Max qui est gagnant quand
il est suivi d'une recherche à l 'ordre n 1. Un coup de Min d' ordre n n'est envisagé que
-
4.5 La réduction aux coups prometteurs 69
si toutes les recherches lambda d'ordre strictement inférieur à n qui le suivent échouent.
return false
end if
for tous les coup légaux de joueur do
menaceVérifiée +-- false
jouer le coup
if lambda (joueur, ordre - 1) then
menaceVérifiée +-- true
for tous les coup légaux de l ' adversaire de joueur do
jouer le coup
if not lambda (joueur, ordre) then
menaceVérifiée +-- false
end if
retirer le coup
end for
end if
retirer le coup
if menaceVérifiée then
return true
end if
end for
return false
Au Phutball les coups gagnants sont très souvent des coups qui étendent le chemin
possible de la balle vers son but ou des coups qui déplacent la balle. Si on restreint à ces
coups la recherche de coups gagnants dans les menaces, on réduit énormément le facteur
de branchement sans pour autant omettre beaucoup de menaces.
On peut de plus observer que les coups d' ordre zéro (les coups immédiatement ga-
70 Recherche avec menaces
gnants) sont toujours des coups qui déplacent la balle. Par induction on peut déduire que
les coups d' ordre un ne sont jamais des coups qui déplacent la balle (sinon ils pourraient
être gagnants directement).
De façon plus générale, les heuristiques admissibles permettent d' améliorer significa
tivement la recherche avec menaces [22] . Une heuristique admissible donne un minorant
sur le nombre de coups de l ' attaquant restant à jouer avant de gagner. Il est clair que si
l' heuristique admissible donne une valeur h, il ne sera pas possible de vérifier des arbres
À d' ordre strictement inférieur à h.
De même que les heuristiques admissibles les connaissances sur les coups qui peuvent
atteindre un but sont très utiles. Par exemple, au Go, le nombre de libertés d' une chaîne
est une heuristique admissible pour la capture : il faudra au moins autant de coups pour
la capturer que son nombre de libertés. Une connaissance pour sélectionner les coups
d'ordre n de capture d'une chaîne à n libertés est que seuls les coups sur les libertés de la
chaîne sont à envisager.
L'élargissement itératif [ 1 8, 20] consiste à effectuer une recherche complète pour Max
pour un ordre donné avant d' accroître l' ordre de la recherche. En pratique on essaie une
recherche d'ordre un ; si elle échoue on essaie une recherche d' ordre deux ; si elle échoue
on essaie une recherche d' ordre trois ; et ainsi de suite jusqu ' à ce que Je problème soit
résolu ou jusqu ' à ce que le temps alloué soit dépassé.
La recherche À classique peut être modélisée avec les menaces limitées. Par exemple
développer un arbre À1 est équivalent à vérifier une menace ( oo,oo,O), À 2 avec ( 00,00,00,0)
4.8 Les coups qui tuent 71
et ainsi de suite. En effet la recherche >. d 'ordre n fait un appel récursif à la recherche >.
d' ordre n sans limitation de profondeur. Alors que la recherche >. limitée arrêtera sa re
cherche après un nombre limité d' appels récursifs d'ordre n.
De même que pour l 'Alpha-Bêta il peut être intéressant pour la recherche de menaces
de mémoriser des coups qui tuent. Ainsi par exemple si un coup a permis de vérifier une
menace, c'est un coup à essayer en priorité après le coup de l ' adversaire qui cherche à
parer la menace. On utilise donc deux tableaux de coups qui tuent (un pour Max et un
pour Min), et plutôt que de les indicer par la profondeur comme dans l' Alpha-Bêta on les
indice par le nombre de coups Max (respectivement Min) déjà joués de façon à réessayer
le coup Max gagnant après le coup Min.
Les zones pertinentes permettent d ' améliorer significativement la recherche >.. Une
zone pertinente est !'ensemble des cases, ou des intersections suivant le jeu, qui inter
viennent dans la preuve d'un arbre >.. Les seuls coups qui peuvent invalider cet arbre >.
sont les coups sur sa zone pertinente. Ainsi lorsqu ' on cherche des coups pour le défen
seur à un noeud, les seuls coups à essayer sont les coups de la zone pertinente de l ' arbre
>. prouvé au noeud.
4.10. 1 Phutball
On représente un coup comme une liste d'intersections. Si la liste ne comporte qu' une
seule intersection c'est ! ' emplacement de la pierre blanche qu 'on pose. Si la liste comporte
plusieurs intersections, c'est un coup de prise, la première intersection est l 'emplacement
initial de la balle, les intersections suivantes sauf la dernière sont les pierres blanches
prises et la dernière intersection est l 'emplacement final de la balle.
#include <iostream >
#include < l i s t >
#include <algorithm >
72 Recherche avec menaces
u s i n g namespace s t d ;
const i n t JoueurGauche = 0 ;
const i n t JoueurDroit = 1 ;
class Intersection {
public :
i n t X, y ;
bool l e g a l e ( ) {
r e t u r n ( ( x >= 0 ) && ( x < T a i 1 1 e ) &&
( y >= 0 ) && ( y < T a i 1 1 e ) ) ;
}
} ;
c l a s s Coup {
public :
list <Intersection > 1 ;
Il Il Il
s o r t i e << " ( " << i t ->x << << i t ->y << )" ;
return s o r t i e ;
}
class Phutball {
public :
char damier [ T a i l l e ] [ Taille ] ;
Intersection balle ;
Phutball () {
init ();
}
void 1 n 1 t ( ) {
fo r ( i n t i = O ; i < T a i l l e ; i + + )
fo r ( i n t j = O ; j < T a i l l e ; j + + )
damier [ i ] [ j ] = '+ ' ;
damier [ T a i l l e / 2] [ Taille 2] = '@ ' ;
balle . x = Taille / 2;
balle . y = Taille / 2;
}
bool gagne ( i n t j o u e u r , I n t e r s e c t i o n i n t e r ) {
i f ( ( ( j o u e u r == J o u e u r D r o i t ) && ( i n t e r . x < 0 ) ) 1 1
( ( j o u e u r == J o u e u r D r o i t ) && i n t e r . 1 e g a 1 e ( ) &&
( i n t e r . x == 0 ) ) 1 1
74 Recherche avec menaces
( ( j o u e u r == J o u e u r G a u c h e ) &&
( inter . X > Taille - 1)) Il
( ( j o u e u r == J o u e u r G a u c h e ) && i n t e r . l e g a l e ( ) &&
( i n t e r . x == T a i 1 1 e - 1)))
return true ;
return fa l s e ;
}
break ;
}
i f ( gagne ( j o u e u r , i n t e r ) ) {
coup l . 1 . push_back ( i n t e r ) ;
l i s t e . push_back ( coup l ) ;
coupGagnant = true ;
}
else if ( i nter . legale ( ) ) {
i f ( d a m i e r [ i n t e r . x ] [ i n t e r . y ] == ' + ' ) {
I l on m e m o r i s e l a p l a c e de l a b a l l e
Intersection ancienneB alle = balle ;
1 1 on d e p l a c e l a b a l l e
balle = inter ;
I l on e n l e v e l e s p i e r r e s s a u t e e s
fo r ( l i s t < l n t e r s e c t i o n > : : i t e r a t o r i t =
pi erresEnlevees . begin ( ) ;
i t ! = p i e r r e s E n l e v e e s . e n d ( ) ; ++ i t )
d a m i e r [ i t ->x ] [ i t ->y ] = ' + ' ;
1 1 o n aj o u t e l e n o u v e a u c o up
Coup c o u p 2 = c o u p l ;
coup2 . l . p u s h_back ( i n t e r ) ;
l i s t e . push_back ( coup2 ) ;
I l o n r e g a r d e l e s c o up s s u i v a n t s
i f ( c o u p s B a l l e ( j o u e u r , coup l , l i s t e ) )
coupGagnant = true ;
I l on remet l e s p i e r r e s e n l e v e e s
fo r ( l i s t < I n t e r s e c t i o n > : : i t e r a t o r i t =
p ierresEnlevee s . begin ( ) ;
i t ! = p i e r r e s E n l e v e e s . e n d ( ) ; ++ i t )
d a m i e r [ i t ->x ] [ i t ->y ] = ' O ' ;
I l on r e m e t l a b a l l e
balle = ancienneBalle ;
}
else
c e r r << " b u g ._. c o u p s B a l l e ....., \ n " ;
}
}
}
return coupGagnant ;
}
s o r t i e << e n d ! ;
fo r ( i n t i = O ; i < T a i l l e ; i ++) {
s o r t i e << i << " ....., " ;
fo r ( i n t j = O ; j < T a i l l e ; j + + )
Il Il
s o r t i e << v . d a m i e r [j ] [ i ] « ......
•
'
s o r t i e << e n d ! ;
}
return sortie ;
}
Phutball phutball ;
i n t main () {
l i s t <Coup > l i s t e C o u p s ;
while ( true ) {
c o u t << p h u t b a l l ;
IisteCoups . clear ( ) ;
i f ( p h u t b a l l . coup sLegaux ( JoueurGauche , listeCoups ) ) {
c o u t << " j ' a i .__. g a g n e ....., ! " << e n d ! ;
break ;
}
Coup c o u p A i e a t o i r e ;
i n t i = 0 , i n d i c e = rand ( ) % I i s te C o u p s . s i z e ( ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t ! = I i s t e C o u p s . e n d ( ) ; ++ i t ) {
i f ( i == i n d i c e ) {
c o u p A i e at o i re = * i t ;
break ;
}
i ++;
}
c o u t << " J e ....., j o u e ....., e n ....., " << c o u p A i e a t o i r e << e n d ! ;
p h u t b a l l . joue ( c o upAleato ire ) ;
c o u t << p h u t b a l l ;
IisteCoups . cl ear ( ) ;
i f ( p h u t b a l l . coupsLegaux ( JoueurDroit , I i s t e C o u p s ) ) {
c o u t << " v o u s ....., a v e z ....., g a g n e ....., ! " << e n d ! ;
break ;
}
Coup c o u p ;
c i n >> c o u p ;
phutball . joue ( coup ) ;
}
4.10 Corrigés des exercices 77
return 0 ;
}
X X
X X
X X XX . XX .
X X X X •
. XX .
Dans tous les algorithmes avec menaces qui suivent on utilise des fonctions récursives
de recherche qui maintiennent incrémentalement l 'état du damier. En plus de la fonction
pour jouer les coups on a donc besoin d' une fonction qui retire le dernier coup joué ; on
place donc la fonction suivante dans la classe Phutball :
v o i d d ej o u e ( Coup & c ) {
l i s t < I n tersection > : : i t e r a t o r i t = c . l . begin () ;
i f ( c . l . s i z e ( ) == 1 ) {
d a m i e r [ i t ->x ] [ i t ->y ] = ' + ' ;
}
else {
damier [ b a l l e . x ] [ b a l l e . y ] = ' + ' ;
balle = * i t ;
i t ++;
l i s t < Intersection > : : i t e r a t o r precedent ;
while ( true ) {
precedent = i t ;
i t ++;
i f ( i t == c . l . e n d ( ) )
break ;
d a m i e r [ p r e c e d e n t ->x ] [ p r e c e d e n t ->y ] = ' O ' ;
}
damier [ b a l l e . x ] [ b a l l e . y ] = '@ ' ;
}
}
78 Recherche avec menaces
bool lambda ( i n t j o u e u r , i n t o r d r e ) {
i f ( p h u t b a l l . gagne ( j o u e u r , p h u t b a l l . b a l l e ) )
return true ;
l i s t <Coup> l i s t e C o u p s , l i s t e C o u p s A d v e r s e s ;
if ( p h u t b a l l . coupsLegaux ( j oueur , l i s te C o u p s ) )
return true ;
if ( o r d r e == 0 )
r e t u r n fa l s e ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l isteCoups . begin ( ) ;
it ! = l i s t e C o u p s . e n d ( ) ; ++ i t ) {
b o o l m e n a c e V e r i fi e e = f a l s e ;
phutbal l . joue ( * i t ) ;
i f ( lambda ( j o u e u r , o rdre - 1 ) ) {
m e n a c e V e r i fi e e = t r u e ;
int au tre = advers a i re ( j oueur ) ;
i f ( p h u t b al l . coupsLegaux ( au tre , l i steCo u p s Adverses ) )
m e n a c e V e r i fi e e = f a l s e ;
f o r ( 1 i s t <Coup > : : i t e r a t o r i t 1 =
listeCoupsAdverses . begin ( ) ;
( i t l ! = l i s t e C o u p s A d v e r s e s . e n d ( ) ) &&
m e n a c e V e r i fi e e ; ++ i t l ) {
phutball . joue (* i t l ) ;
i f ( ! lambda ( j o u e u r , o rdre ) )
m e n a c e V e r i fi e e = f a l s e ;
p h u t b a l l . d ej o u e ( * i t l ) ;
}
}
p h u t b a l l . d ej o u e ( * i t ) ;
i f ( m e n a c e V e r i fi e e )
return true ;
}
return fa l s e ;
}
On ajoute les fonctions suivantes dans la classe Phutball de façon à trouver les coups
qui étendent le chemin de la balle (fonction coupsVoisinsBalle) et à prendre en
compte l'ordre de la menace pour réduire le nombre de coups à envisager (fonction
coupsLegaux) :
if ( ordre > 0)
coupsVoisinsB alle ( joueur , liste ) ;
i f ( ordre != ! ) {
Coup c o u p ;
coup . 1 . pu sh_b ack ( balle ) ;
damier [ b a l l e . X ] [ balle . y] = '+ ' ;
gagne = c o u p s B a l l e ( j o u e u r , coup , liste ) ;
damier [ b a l l e . X ] [ b a l l e . y ] = '@ ' ;
}
return gagne ;
}
bool aj o u t e l n t e r s e c t i o n
( Intersection & inter ,
list <Intersection > & l i s t e ) {
i f ( ! elementlntersection ( inter , l i s t e ) ) {
I i s t e . p u s h_back ( i n t e r ) ;
return true ;
}
return fa l s e ;
}
}
fo r ( i n t i = - 1 ; i <= 1 ; i + + )
fo r ( i n t j = - 1 ; j < = 1 ; j + + ) {
Intersection inter = * it ;
i n t e r . x += i ;
i n t e r . y += j ;
if ( inter . legale ( ) )
i f ( damier [ i nt e r . x ] [ i n t er . y ] == ' + ' ) {
i f ( ( ( j o u e u r == J o u e u r D r o i t ) &&
( i n ter . X < meilleu r ) ) I l
( ( j o u e u r == J o u e u r G a u c h e ) &&
( inter . X > meilleur ) ) )
meilleur = inter . x ;
}
}
}
fo r ( l i s t < I n t e r s e c t i o n > : : i t e r a t o r i t = l i s t e . b e g i n ();
i t ! = l i s t e . e n d ( ) ; ++ i t ) {
i f ( d a m i e r [ i t ->x ] [ i t ->y ] == ' + ' )
i f ( i t ->x == m e i l l e u r B a l l e ) {
Coup c o u p ;
c o up . l . p u s h_back ( * Î t ) ;
aj o u t e C o u p ( c o u p , l i s t e C o u p s ) ;
}
fo r ( i n t i = - 1 ; i <= 1 ; i + + )
fo r ( i n t j = 1 ; j <= 1 ; j ++) {
-
Intersection i nter = * it ;
i n t e r . x += i ;
i n t e r . y += j ;
if ( inter . legale ( ) )
i f ( d a m i e r [ i n t e r . x ] [ i n t e r . y ] -- ' + ' )
i f ( i n t e r . x == m e i 1 1 e u r ) {
Coup c o u p ;
coup . 1 . pu sh_b ack ( i n t e r ) ;
aj o u t e C o u p ( c o u p , l i s t e C o u p s ) ;
}
}
}
}
La modification de la recherche À est minime puisqu ' il suffit d ' ajouter l ' ordre du coup
pour les coups Max :
bool lambda ( i n t j ou e u r , i n t ordre ) {
i f ( p h u t b a l l . gagne ( j o ueur , p h u t b a l l . b a l l e ) )
return true ;
l i s t <Coup> l i s t e C o u p s , l i s t e C o u p s A d v e r s e s ;
i f ( p h u t b a l l . coupsLegaux (0 , j oueur , l i steCo u p s ) )
4.10 Corrigés des exercices 83
return true ;
if ( o r d r e == 0 )
return fa l s e ;
p h u t b a l l . coupsLegaux ( ordre , j o u e u r , l i steCo u p s ) ;
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t ! = l i s t e C o u p s . e n d ( ) ; ++ i t ) {
b o o l m e n a c e V e r i fi e e = f a l s e ;
phutball . joue (* i t ) ;
i f ( l ambda ( j o u e u r , o r d re - 1 ) ) {
m e n a c e V e r i fi e e = t r u e ;
int autre = advers aire ( j oueur ) ;
i f ( p h u t b a l l . coupsLegaux ( au tre , l i steCou p s A d v e r s e s ) )
m e n a c e V e r i fi e e = f a l s e ;
fo r ( l i s t <Coup > : : i t e r a t o r i t l =
l i s teCoupsAdverses . begin ( ) ;
( i t l ! = l i s t e C o u p s A d v e r s e s . e n d ( ) ) &&
m e n a c e V e r i fi e e ; ++ i t 1 ) {
phutball . joue (* i t l ) ;
i f ( ! lambda ( j o u e u r , o r d re ) )
m e n a c e V e r i fi e e = fa I s e ;
p h u t b a l l . d ej o u e ( * i t l ) ;
}
}
p h u t b a l l . d ej o u e ( * i t ) ;
i f ( menaceVerifiee )
return true ;
}
r e t u r n fa l s e ;
}
Pour effectuer l 'élargissement itératif on ajoute une boucle sur l ' ordre de l ' appel ré
cursif dans la recherche À :
bool lambda ( i n t j o u e u r , i n t o r d re ) {
i f ( p h u t b a l l . gagne ( j o u e u r , p h u t b a l l . b a l l e ) )
return true ;
int autre = adversaire (joueur ) ;
i f ( p h u t b a l l . gagne ( a u tre , p h u t b a l l . b a l l e ) )
r e t u r n fa l s e ;
l i s t <Coup > l i s t e C o u p s , l i s t e C o u p s A d v e r s e s ;
if ( p h u t b a l l . coupsLegaux (0 , joueur , l i steCou p s ) )
return true ;
if ( o r d r e == 0 )
return fa l s e ;
84 Recherche avec menaces
On commence par écrire une classe pour représenter les menaces limitées :
const i n t M a x O rdre = 5 ;
c l a s s Menace {
public :
i n t n b O r d r e [ M ax O rdre ] ;
Menace ( i n t nO = 0 , i n t n l = 0 , i n t n 2 = 0 , i n t n 3 = 0 ,
i n t n4 = 0 ) {
fo r ( i n t i = O ; i < Max Ordre ; i + + )
4.10 Corrigés des exercices 85
n b ürdre [ i] = O;
n b ü rdre [ O ] = no ;
n b ü rdre [ 1 ] = n1 ;
n b ürdre [ 2 ] = n2 ;
n b ü rdre [ 3 ] = n3 ;
n b O rdre [ 4 ] = n4 ;
}
void e c re t e ( i n t n ) {
fo r ( i n t i = n ; i < M a x ü rdre ; i ++)
n b ü rdre [ i ] = O ;
}
int ordre ( ) {
fo r ( i n t i = O ; i < M a x ü rdre - 1; i ++)
i f ( n b ü rdre [ i + 1 ) == 0 )
return i ;
r e t u r n M a x ü rdre 1;-
i t ! = l i s t e C o u p s . e n d ( ) ; ++ i t ) {
phutbal l . joue (* i t ) ;
b o o l m e n a c e V e r i fi e e = fa l s e ;
i n t ordre = O ;
fo r ( i n t o = O ; ( o < n ) && ! m e n a c e V e r i fi e e ; o + + ) {
Menace m l = m ;
ml . e c r e t e ( o + 1 ) ;
i f ( l a m b d a ( j o u e u r , ml ) ) {
m e n a c e V e r i fi e e = t r u e ;
ordre = o ;
}
}
m . n b ü rdre [ o r d re + 1 ) - - ;
b o o l g a g n e V e r i fi e = fa l s e ;
i f ( m e n a c e V e r i fi e e )
i f ( ! p h u t b a l l . coupsLegaux ( au tre ,
l i s teCoupsAdverses ) )
fo r ( i n t o = 0 ; ( o <= m . o r d r e ( ) ) &&
! g a g n e V e r i fi e ; o++) {
Menace ml = m ;
ml . e c r e t e ( o + l ) ;
g a g n e V e r i fi e = true ;
fo r ( l i s t <Coup > : : i t e r a t o r i t l =
listeCoupsAdverses . begin ( ) ;
( i t 1 ! = l i s t e C o u p s A d v e r s e s . e n d ( ) ) &&
g a g n e V e r i f i e ; ++ i t 1 ) {
phutball . joue ( * i t l ) ;
i f ( ! lambda ( j o u e u r , ml ) )
g a g n e V e r i fi e = fa l s e ;
p h u t b a l l . d ej o u e ( * i t l ) ;
}
i f ( g a g n e V e r i fi e )
break ;
}
p h u t b a l l . d ej o u e ( * i t ) ;
m. n b ü rdre [ o r d r e + l ] + + ;
i f ( g a g n e V e r i fi e )
return true ;
}
return fa l s e ;
4.10 Corrigés des exercices 87
Coup t u e u r M a x [ 1 0 0) , t u e u r M i n [ 1 0 0) ;
v o i d i n s e r e E n P r e m i e r ( Coup & c o u p ,
l i s t <Coup > & l i s t e C o u p s ) {
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t ! = 1 i s t e C o u p s . end ( ) ; ++ i t )
i f ( c o u p == * i t ) {
listeCoups . erase ( i t ) ;
l i s t e C o u p s . p u s h _ fr o n t ( c o u p ) ;
break ;
}
}
b o o l l a m b d a ( i n t j o u e u r , Menace m , i n t nbCoupsMax = 0,
i n t nbCoupsMin = 0 ) {
i f ( p h u t b a l l . gagne ( j o u e u r , p h u t b a l l . b a l l e ) )
return true ;
int au tre = advers aire ( j oueur ) ;
i f ( p h u t b a l l . gagne ( au tre , p h u t b a l l . b a l l e ) )
r e t u r n fa l s e ;
l i s t <Coup > l i s t e C o u p s , l i s t e C o u p s A d v e r s e s ;
i f ( p h u t b a l l . coupsLegaux (0 , joueur , l i s t e Co u p s ) )
return true ;
i n t n = m. ordre ( ) ;
i f ( n == 0 )
r e t u r n fa l s e ;
p h u t b a l l . coupsLegaux ( n , joueur , l i s t e C o u p s ) ;
i n s e r e E n P r e m i e r ( t u e u r M a x [ nb C o u p s M ax ] , l i s t e C o u p s );
fo r ( l i s t <Coup > : : i t e r a t o r i t = l i s t e C o u p s . b e g i n ( ) ;
i t ! = 1 i s t e C o u p s . end ( ) ; ++ i t ) {
phutball . joue (* i t ) ;
b o o l m e n a c e V e r i fi e e = f a l s e ;
int ordre = O ;
fo r ( i n t o = O ; ( o < n ) && ! m e n a c e V e r i fi e e ; o + + ) {
Menace m l = m ;
m l . e c r e t e ( o + I);
i f ( l a m b d a ( j o u e u r , m l , nbCoupsMax + 1 ,
nbCoupsMin ) ) {
m e n a c e V e r i fi e e = t r u e ;
ordre = o ;
}
}
m . n b O r d r e [ o r d r e + 1) - - ;
88 Recherche avec menaces
b o o l g a g n e V e r i f i e = fa Is e ;
i f ( m e n a c e V e r i fi e e )
i f ( ! p h u t b a l l . coupsLegaux ( au tre ,
l i s te C o u p s Adverses ) )
fo r ( i n t o = O ; ( o <= m . o r d r e ( ) ) &&
! g a g n e V e r i fi e ; o++) {
i n s e r e E n P re m i e r ( t u e u rM i n [ nbCoupsMin ] ,
listeCoupsAdverses ) ;
Menace ml = m ;
ml . e c r e t e ( o + l ) ;
g a g n e V e r i fie = true ;
fo r ( l i s t <Coup > : : i t e r a t o r i t l =
listeCoupsAdverses . begin ();
( i t 1 ! = l i s t e C o u p s A d v e r s e s . e n d ( ) ) &&
g a g n e V e r i f i e ; ++ i t 1 ) {
phutball . joue (* i t l ) ;
i f ( ! l a m b d a ( j o u e u r , m l , nbCoupsMax + 1 ,
nbCoupsMin + 1 ) ) {
t u e u r M i n [ nbCoupsMin ] = * i t 1 ;
g a g n e V e r i f i e = fa l s e ;
}
p h u t b a l l . d ej o u e ( * i t l ) ;
}
i f ( g a g n e V e r i fi e )
break ;
}
p h u t b a l l . d ej o u e ( * i t ) ;
m. n b ü rdre [ o r d r e + l ] + + ;
i f ( g a g n e V e r i fi e ) {
t u e u r M a x [ nbCoupsMax ] = * i t ;
return true ;
}
}
return fa l s e ;
}
On peut alors utiliser le coup qui tue de profondeur zéro pour avoir un joueur parfait
de Phutball 9x9 :
i n t main ( ) {
l i s t <Coup> l i s t e C o u p s ;
w h i te ( t r u e ) {
c o u t << p h u t b a l l ;
listeCoups . clear ( ) ;
i f ( p h u t b a l l . c o u p s L e g a u x ( Jo u e u r G a u c h e , listeCoups ) ) {
c o u t << " j ' a i ._.g a g n e ...... ! " << e n d l ;
break ;
}
4. 10 Corrigés des exercices 89
Menace m ( 1 , 4 , 1);
c o u t << " t e s t � l a m b d a� " << m << " �=� " <<
l a m b d a ( Jo u e u r G a u c h e , m) << en d l ;
Coup c o u p Q u i T u e = t u e u r M a x [ 0 ] ;
c o u t << " Je �j o u e � e n � " << c o u p Q u i T u e << e n d l ;
p h u t b a l l . j o u e ( coupQuiTue ) ;
c o u t << p h u t b a l l ;
listeCoups . clear ( ) ;
i f ( p h u t b a l l . c o u p s L e g a u x ( Jo u e u r D r o i t , l i s t e C o u p s ) ) {
c o u t << " v o u s � a v e z � g a g n e � ! " << e n d l ;
break ;
}
Coup c o u p ;
c i n >> c o u p ;
p h u t b a l l . j oue ( coup ) ;
}
return O ;
}
Chapitre 5
Recherche arborescente
Monte-Carlo
5.1 Introduction
Le problème typique pour lequel les méthodes de Monte-Carlo sont adaptées est le
jeu de Go : l 'espace de recherche est très grand et il n ' existe pas de fonction d'évaluation
simple, si ce n'est la moyenne de parties aléatoires.
5.2 Le jeu de Go
Maître Lim.
Le jeu de Go est un jeu chinois très ancien. Il est populaire en Corée, au Japon et en
Chine. Les générations successives de joueurs de Go ont accumulé une expérience énorme
du jeu tout au long de son histoire. Aujourd' hui on compte des dizaines de millions de
92 Recherche arborescente Monte-Carlo
Afin d ' écrire un programme de Monte-Carlo Go on doit effectuer des parties aléa
toires. La connaissance minimale à av oir pour jouer ces parties est de jouer des coups
légaux et ne pas se boucher les yeux. I! est tout à fait remarquable qu ' un programme qui
dispose de si peu de connaissances du jeu soit capable de mieux jouer que des programmes
qui ont de grandes quantités de connaissances.
Exercice : Écrire un classe Go qui permettent de jouer des parties aléatoires de Go.
Maintenant qu ' on dispose d ' une classe permettant de jouer des parties aléatoires de
Go, il dev ient simple d ' écrire un algorithme de Monte-Carlo basique. Cela consiste s im
plement à faire un certain nombre de parties aléatoires après chaque coup possible, à
mémoriser les résultats de ces parties et à faire une moyenne des résultats des parties pour
chaque coup possible. Le coup choisi est celui qui a la meilleur moyenne.
5.4 UCB
Pedro Damiano.
Plutôt que de faire un nombre égal de simulations pour chaque coup possible, il est
plus judicieux d ' utiliser les résultats des simulations précédentes pour savoir quels ont
été les meilleurs coups jusqu ' ici de façon à les essayer plus que les coups qui semblent
mauvais. Toutefois si on privilégie toujours le meilleur coup, on peut oublier des coups
pour lesquels les premières simulations se sont mal passées mais qui sont tout de même
bons et qui révélerait tout leur potentiel si on leur donnait plus de simulations. On est
fac e à un dilemme exploration/exploitation : on veut faire plus de simulations pour les
coups qui nous semble les meilleurs de façon à diminuer l' incertitude sur leur qualité, on
exploite donc les coups qui nous semblent les meilleurs, mais on veut aussi explorer ceux
qui nous semblent moins bons au cas où ils seraient en réalité meilleurs.
Un algorithme qui permet un bon équilibre entre exploration et exploitation est UCB
(Upper Confidence Bound). Il consiste à choisir le coup qui maximise la fonction µi +
C x Jlog(p)/Pi où µi est la moyenne des parties commençant par le coup Ci, p est le
nombre de parties jouées et Pi est le nombre de parties commençant par le coup Ci· C est
une constante qu ' il faut régler pour l ' adapter au domaine auquel l ' algorithme est appli
qué. Une constante élevée favorisera l ' exploration alors qu ' une petit constante favorisera
l ' exploitation. Pour des résultats compris entre 0 et 1 , choisir une constante de l ' ordre de
0.3 est un bon compromis dans de nombreux jeux.
5.5 UCT
Pour descendre l ' arbre on choisit à chaque niveau le coup à jouer avec la formule
UCB , c ' est pourquoi l ' algorithme s ' appelle UCT (UCB applied to Trees).
La figure 5 . 1 donne les quatre étapes de l ' algorithme UCT. La première étape consiste
à descendre l ' arbre en utilisant la politique UCB . La deuxième étape est d ' ajouter une
nouvelle feuille en dessous du dernier noeud atteint par la descente de l ' arbre. La troisième
étape est de jouer une partie aléatoire à partir de la position atteinte à cette nouvelle feuille.
94 Recherche arborescente Monte-Carlo
mise ajour
des noeuds
de l'arbre
descente de l'arbre
partie aleatoire
La quatrième étape est de remonter dans l ' arbre le résultat de la partie aléatoire.
Exercice : Écrire une classe Noeud qui permet de faire des parties aléatoires commen
çant par un descente de 1' arbre UCT. Puis écrire un algorithme qui choisit un coup au Go
en utilisant UCT.
Au Go, ainsi que dans de nombreux autres jeux, les transpositions sont fréquentes. II
est utile de les détecter pour améliorer le niveau de 1 ' algorithme.
5.7 RAVE
II existe une heuristique qui permet de disposer d ' une estimation rapide de la valeur
des coups lorsque peu de simulations ont été effectuées à un noeud. On effectue pour
cela à chaque noeud de nouvelles statistiques sur chaque coup possible. On mettra à jour
la valeur moyenne RAVE d ' un coup avec le résultat d ' une partie aléatoire quand cette
5.7 RAVE 95
Algorithm 3 UCT
UCT (partie, joueur)
if il existe un fils non exploré then
ajouter ce fils
jouer dans partie le coup qui amène au fils
jouer une partie aléatoire dans partie
nombre de parties aléatoires du fils +- l
somme des scores du fils+- score de partie
retourner
end if
meilleurScore+-- 1
meilleurCoup +-passe
meilleur Fils +-NULL
for chaque fils do
moyenne+-somme des scores du fils / nombre de parties aléatoires du fils
p+-nombre de parties aléatoires du père
Pi+-nombre de parties aléatoires du fils
score+-moyenne + Constante x /fiifii
if score> meilleurScore then
meilleurScore+-score
meilleurCoup +-coup qui amène au fils
meilleur Fils+-fils
end if
end for
jouer meilleurCoup dans partie
meilleurFils->UCT (partie, adversaire de joueur)
nombre de parties aléatoires du noeud+-nombre de parties aléatoires du noeud + l
somme des scores du noeud+-somme des scores du noeud + score de partie
96 Recherche arborescente Monte-Carlo
partie aléatoire contient le coup, à n ' importe quel moment de la partie. Cette heuristique
s ' appelle AMAF (Ail Moves As First).
L' algorithme RAVE (Rapid Action Value Estimation) [36) combine la valeur AMAF
d ' un coup avec la moyenne des parties commençant par ce coup en utilisant un paramètre
/3 qui décroît progressivement de 1 à O . Ceci permet de commencer par évaluer un coup
avec la valeur AMAF lorsqu ' il y a peu de parties aléatoires. En effet la valeur AMAF
donne de meilleures estimations que la moyenne lorsque le nombre de parties aléatoires
est petit. En revanche, lorsque le nombre de simulations augmente, la moyenne donne
alors une meilleur estimation. On passe donc progressivement de la valeur AMAF à la
moyenne lorsque le nombre de parties aléatoires augmente. L' intérêt d ' un coup est donc
estimé avec la formule suivante :
/3 X AM AF + (LO - /3) X µi
sa
/3 -
_
sa+s+C1 xsaxs
RAVE utilise cette formule à la place de la formule UCB pour choisir le coup à ex
plorer lors de la descente de l ' arbre au début de chaque simulation.
5.8 Développements
Une voie très prometteuse est d ' introduire des biais dans les partie aléatoires. Plutôt
que d ' utiliser des parties complètement aléatoires, il est meilleur de jouer les coups avec
des urgences différentes qui dépendent de la configuration locale au coup [44) .
Les techniques de Monte-Carlo ont aussi été appliquées avec succès à d ' autres jeux.
Par exemple au Hex, elles donnent de très bons résultats, l ' heuristique RAVE donne des
résultats encore meilleurs qu ' au Go car tous les coups commutent au Hex ce qui n ' est pas
le cas du Go.
La recherche arborescente Monte-Carlo peut être appliquée à des jeux pour lesquels
on dispose d ' une bonne fonction d ' évaluation. Le principe est de jouer un petit nombre
5.9 Corrigés des exercices 97
de coups aléatoires aux feuilles de l ' arbre UCT et de remonter l 'évaluation de la position
après ces quelques coups aléatoires. Cela a donné de très bons résultats à Lines of Action
[92) et Amazons [58).
Il existe toutefois des jeux pour lesquels la recherche Monte-Carlo ne donne pas de
bons résultats. C ' est le cas par exemple pour Dots and Boxes car il arrive souvent que des
positions aient un grand nombre de coups possibles et un seul coup gagnant. La recherche
Monte-Carlo qui fait la moyenne sur tous les coups des positions suivantes considère
la position comme perdue alors qu ' il y a un coup gagnant. Dans ce type de positions,
l ' Alpha-Bêta n ' a pas de problème pour trouver le coup gagnant.
De même au Go, les échelles sont des séquences très simples mais très profondes : il
n ' y a qu ' un seul coup à envisager à chaque noeud et on atteint couramment la profondeur
60. La recherche Monte-Carlo qui développe tous les coups possibles d ' un noeud avant
de développer plus profondément un coup est aussi perdue dans ces situations.
Toujours au Go, il existe des situations tactiques appelées Semeai pour lesquelles la
seule issue pour un groupe G1 est de tuer un groupe G2 , et la seule issue pour le groupe
G2 est de tuer le groupe G1. Dans les Semeais simples, compter le nombre de libertés des
groupes permet de connaître le gagnant du Semeai. Cependant les simulations Monte
Carlo perdent ou gagnent le Semeai dans approximativement 50% des cas si le nombre de
libertés des deux groupes est proche alors que le combat devrait toujours être gagné par
celui qui a le plus de libertés.
La classe pour les parties aléatoires de Go que nous présentons n ' est pas une classe
optimisée pour jouer très rapidement. Le choix qui a été fait est plutôt d' avoir un code
aussi simple que possible au détriment parfois de l ' efficacité. Pour le lecteur qui veut aller
plus avant dans la programmation du Go, Fuego [35) est un bon exemple de programme
de Go de haut niveau, qui est de plus un logiciel libre dont on peut lire de code.
Le code que nous allons utiliser commence par les en-têtes et les déclarations de
constantes :
# i n c l u d e < s t d i o . h>
# i n c l u d e < s t d l i b . h>
# i n c l u d e < m ath . h >
98 Recherche arborescente Monte-Carlo
u s i n g namespace s td ;
class Intersection {
public :
i n t _X , _y ;
La classe suivante dont nous avons besoin est une classe qui permet de savoir si une
intersection a déjà été visitée, ceci afin d ' éviter les boucles infinies mais aussi afin de
ne compter qu ' une seul fois chaque liberté. Le principe de cette classe est d ' utiliser une
initialisation paresseuse. Les intersections qui ont pour valeur le marqueur courant sont
comptées comme marquées . Ceci permet d ' initialiser très rapidementla marquage puis-
5.9 Corrigés des exercices 99
qu ' il suffit d ' incrémenter le compteur pour qu ' aucune intersection ne soit plus marquée.
Le code de la classe qui permet d ' initialiser et de mémoriser les intersections marquées
est :
c l a s s Marquage {
unsigned long long _marqueur ;
unsigned long long _marquee [ T a i l l e + 2] [ Taille + 2] ;
public :
M ar q u a g e ( ) {
_marqueur = l;
fo r ( i n t i = O ; i < T a i l l e + 2 ; i ++)
fo r ( i n t j = 0 ; j < T a i l l e + 2 ; j ++)
_ m a rq u e e [ i ] [j ] = O;
}
void i n i t ( ) {
_marq u e u r ++ ;
}
bool marquee ( i n t i , i n t j ) {
r e t u r n ( _ m a r q u e e [ i ] [ j ] == _ m a r q u e u r ) ;
}
v o i d marq u e ( i n t i , i n t j ) {
_marquee [ i ] [ j ] = _marqueu r ;
}
bool marquee ( I n t e r s e c t i o n i n t e r ) {
r e t u r n ( _ m a r q u e e [ i n t e r . _x ] [ i n t e r . _y ] -- _ m a r q u e u r ) ;
}
void marque ( I n t e r s e c t i o n i n t e r ) {
_ m a r q u e e [ i n t e r . _x ] [ i n t e r . _y ] = _ m a r q u e u r ;
}
};
M a r q u a g e d ej a v u , d ej a v u 2 ;
_
Nous pouvons maintenant attaquer la classe qui représente une partie de Go.
u n s i g n e d l o n g l o n g H a s h A rr a y [2] [ Ta i l l e + 2] [ Taille + 2] ;
unsigned long long HashTurn ;
c l a s s Go {
public :
char goban [ T a i l l e + 2 ] [ Taille + 2] ;
100 Recherche arborescente Monte-Carlo
i n t n b C o u p s Jo u e s ;
I n t e r s e c t i o n moves [ MaxCoups ] ;
unsigned long long hash ;
u n s i g n e d l o n g l o n g H a s h H i s t o r y [ M axCoups ] ;
f l o a t komi , s c o re [2] ;
Go ( ) {
komi = 7 . 5 ;
hash = 0 ;
n b C o u p s Jo u e s = 0 ;
fo r ( i n t i = 1 ; i <= T a i l l e ; i + + )
fo r ( i n t j = 1 ; j < = T a i 1 1 e ; j + + )
goban [ i ] [ j ] = Vide ;
fo r ( i n t i = O ; < T a i l l e + 2 ; i ++) {
goban [ 0 ] [ i ] = E x t e r i e u r ;
goban [ i ] [ O ] = E x t e ri e u r ;
goban [ T a i l l e + l ] [ i ] = E x t e r i e u r ;
goban [ i ] [ T a i l l e + 1 ] = E x t e r i e u r ;
}
}
void i n i t H a s h ( ) {
fo r ( i n t c = 0 ; c < 2 ; c + + )
fo r ( i n t i = l ; i <= T a i l l e ; i + + )
fo r ( i n t j = l ; j <= T a i l l e ; j + + ) {
H a s h A rr a y [ c ] [ i ] [ j ] = 0 ;
fo r ( i n t b = O ; b < 6 4 ; b + + )
i f ( ( r a n d ( ) / (RAND_MAX + 1 . 0 ) ) > 0 . 5 )
H a s h A rr a y [ c ] [ i ] [ j ] 1 = ( 1 ULL << b ) ;
}
HashTurn = 0 ;
fo r ( i n t j = O ; j < 6 4 ; j + + )
i f ( ( r a n d ( ) / (RAND_MAX + 1 . 0 ) ) > 0 . 5 )
H a s h T u r n I = ( 1 ULL << j ) ;
}
bool c o u p Legal ( I n t e r s e c t i o n i n t e r , i n t c o u l e u r ) {
i f ( ( i n t e r . _x == 0 ) && ( i n t e r . _y == 0 ) )
return true ;
i f ( g o b a n [ i n t e r . _x ] [ i n t e r . _y ] ! = Vide )
r e t u r n fa l s e ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
I n t e r s e c t i o n v o i s i n e = i n t e r . v o is in e ( i ) ;
i f ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] == V i d e )
5.9 Corrigés des exercices 101
return true ;
}
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection voisine = inter . voisine ( i ) ;
i f ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] == c o u l e u r )
i f ( minLib ( v o i s i n e , 2 ) > 1 )
return true ;
}
u n s i g n e d l o n g l o n g h = h a s h S i Jo u e ( i n t e r , c o u l e u r ) ;
fo r ( i n t i = n b C o u p s Jo u e s - l ; i >= O ; i - -)
i f ( H a s h H i s t o r y [ i ] == h )
r e t u r n fa l s e ;
r e t u r n ( m i n L i b I fP 1 a y ( i n t e r , c o u 1 e u r , 1 ) > 0) ;
}
u n s i g n e d l o n g l o n g h a s h S i Jo u e ( I n t e r s e c t i o n i n t e r ,
int couleur ) {
unsigned long long h = hash ;
int a d v e r s a i re = Noir ;
i f ( c o u 1 e u r == N o i r )
advers a i re = Blanc ;
d ej a v u 2 . i n i t ( ) ;
d ej a v u 2 . marq u e ( i n t e r ) ;
h 11 = H a s h A r r a y [ c o u l e u r ] [ i n t e r . _x ] [ i n t e r . _y ] ;
h "= H a s h T u r n ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection voisine = inter . voisine ( i ) ;
i f ( ! d ej a v u 2 . m a r q u e e ( v o i s i n e ) ) {
i f ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] == a d v e r s a i r e )
i f ( m i n L i b ( v o i s i n e , 2 ) == 1 ) {
stack < I n te r s e c t i o n > s t ;
d ej a v u 2 . m a r q u e ( v o i s i n e ) ;
s t . push ( v o i s i n e ) ;
while ( ! s t . empty ( ) ) {
I n t e rs e c t i o n c o u rante = s t . top ( ) ;
s t . pop ( ) ;
h "= H a s h A rr a y [ g o b a n [ v o i s i n e . _x ]
[ v o i s i n e . _y ] ] [ c o u r a n t e . _x ]
[ c o u r a n t e . _y ] ;
fo r ( i n t j = O ; j < 4 ; j + + ) {
I n t e r s e c t i o n p i e rre = courante . v o i s i n e ( j ) ;
i f ( g o b a n [ p i e r r e . _x ] [ p i e r r e . _y ] ==
advers aire )
102 Recherche arborescente Monte-Carlo
i f ( ! dej a v u 2 . marquee ( p i e r r e ) ) {
d ej a v u 2 . m a r q u e ( p i e r r e ) ;
s t . push ( p i e rre ) ;
}
}
}
}
}
}
return h · '
i n t minLib ( I n t e r s e c t i o n i n t e r , i n t min ) {
stack < Intersection > s t ;
i n t compteur = 0 , couleur =
g o b a n [ i nt e r . _x ] [ i n t e r . _y ] ;
d ej a v u . i n i t ( ) ;
d ej a v u . m a r q u e ( i n t e r ) ;
s t . push ( i n t e r ) ;
wh i l e ( ! s t . e m p t y ( ) ) {
I n t e r s e c t i o n i n t e r = s t . top ( ) ;
s t . pop ( ) ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection voisine = inter . voisine ( i ) ;
i f ( ! d ej a v u . m a r q u e e ( v o i s i n e ) ) {
d ej a v u . m a r q u e ( v o i s i n e ) ;
i f ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] -- V i d e ) {
c o mp t e u r + + ;
i f ( c o m p t e u r >= min )
r e t u r n c o mp t e u r ;
}
e l s e i f ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] -
couleur )
s t . push ( v o i s i n e ) ;
}
}
}
return compteur ;
}
i f ( g o b a n [ i n t e r s e c t i o n . _x ] [ i n t e r s e c t i o n . _y ] --
5.9 Corrigés des exercices 103
Vide ) {
d ej a v u 2 . i n i t ( ) ;
d ej a v u 2 . marq u e ( i n t e r s e c t i o n ) ;
s t . push ( i n t e r s e c t i o n ) ;
w h i l e ( ! s t . empty ( ) ) {
I n t e r s e c t i o n i n t e r = s t . top ( ) ;
s t . pop ( ) ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection voisine = inter . voisine ( i ) ;
i f ( ! d ej a v u 2 . m a r q u e e ( v o i s i n e ) ) {
dej a v u 2 . marque ( v o i s i n e ) ;
i f ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ]-
Vide ) {
compte u r + + ;
i f ( c o m p t e u r >= m i n )
return compteur ;
}
i f ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] -- c o u l e u r )
s t . push ( v o i s i n e ) ;
}
}
}
int a d v e r s a i re = Noir ;
i f ( c o u 1 e u r == N o i r )
a d v e r s a i re = B lanc ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection voisine = i ntersection . voisine ( i ) ;
i f ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] == a d v e r s a i r e )
i f ( m i n L i b ( v o i s i n e , 2 ) == 1 ) {
compteu r + + ;
i f ( c o m p t e u r >= m i n )
return compteur ;
}
}
}
return compteur ;
}
void j o u e ( I n t e r s e c t i o n i n t e r , i n t c o u l e u r ) {
H a s h H i s t o r y [ n b C o u p s Jo u e s ] = h a s h ;
moves [ n b C o u p s Jo u e s ] = i n t e r ;
h a s h "= H a s h T u r n ;
i f ( i n t e r . _x ! = 0 ) {
posePi erre ( i nter , couleur ) ;
enlevePri sonn iers ( i nter , couleur ) ;
}
104 Recherche arborescente Monte-Carlo
n b C o u p s Jo u e s + + ;
}
void p o s eP i erre ( I n t e r s e c t i o n i n t e r , i n t c o u l e u r ) {
g o b a n [ i n t e r . _x ] [ i n t e r . _y ] = c o u l e u r ;
h a s h A = H a s h A r r a y [ c o u l e u r ] [ i n t e r . _x ] [ i n t e r . _y ] ;
}
void e n l e v e P r i s o n n i e r s ( I n t e r s e c t i o n inter ,
int couleur ) {
stack < Intersection > s t ;
int a d v e r s a i re = Noir ;
i f ( c o u l e u r == N o i r )
a d v e r s a i re = B lanc ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
I n t e r s e c t i o n v o i s i n e = i n t e r . v o 1s 1n e ( i ) ;
i f ( ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] == a d v e r s a i r e ) )
i f ( m i n L i b ( v o i s i n e , 1 ) == 0 )
s t . push ( v o i s i n e ) ;
}
w h i l e ( ! s t . empty ( ) ) {
I n t e r s e c t i o n v o i s i n e = s t . top ( ) ;
s t . pop ( ) ;
i f ( ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] -- a d v e r s a i r e ) )
enleveChaine ( voisine ) ;
}
}
void enleveChaine ( I n t e r s e c t i o n i n t e r s e c t i o n ) {
stack < I nterse ction > st ;
i n t c o u l e u r = g o b a n [ i n t e r s e c t i o n . _x ]
[ i n t e r s e c t i o n . _y ] ;
s t . push ( i n t e r s e c t i o n ) ;
w h i l e ( ! s t . empty ( ) ) {
I n t e r s e c t i o n i n t e r = s t . top ( ) ;
s t . pop ( ) ;
h a s h A = H a s h A r r a y [ c o u l e u r ] [ i n t e r . _x ] [ i n t e r . _y ] ;
g o b a n [ i n t e r . _x ] [ i n t e r . _y ] = V i d e ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection voisine = inter . voisine ( i ) ;
i f ( ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] == c o u l e u r ) )
s t . push ( v o i s i n e ) ;
}
}
}
5.9 Corrigés des exercices 105
bool e n t o u ree ( I n t e r s e c t i o n i n t e r s e c t i o n , i n t c o u l e u r ) {
i f ( g o b a n [ i n t e r s e c t i o n . _x ] [ i n t e r s e c t i o n . _y ] ! = V i d e )
return fa l s e ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection voisine = i n tersection . voisine ( i ) ;
i f ( ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] ! = c o u l e u r ) &&
( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] ! = E x t e r i e u r ) )
return fa l s e ;
}
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection voisine = intersection . voisine ( i ) ;
i f ( ( g o b a n [ v o i s i n e . _x ] [ v o i s i n e . _y ] == c o u l e u r ) )
i f ( m i n L i b ( v o i s i n e , 2 ) == 1 )
return fa l s e ;
}
return true ;
}
bool p r o t e g e e ( I n t e r s e c t i o n i n te r ,
i n t c o u l e u r , b o o l & b o rd ) {
i f ( g o b a n [ i n t e r . _x ] [ i n t e r . _y ] == E x t e r i e u r ) {
bord = true ;
return true ;
}
i f ( g o b a n [ i n t e r . _x ] [ i n t e r . _y ] == V i d e )
i f ( entouree ( i nter , couleur ) )
return true ;
i f ( g o b a n [ i n t e r . _x ] [ i n t e r . _y ] == c o u l e u r )
return true ;
r e t u r n fa l s e ;
}
i n t n b D i agonalesProte g e e s = O ;
bool bord = fa l s e ;
fo r ( i n t i = O ; i < 4 ; i + + ) {
Intersection diagonale = i nter . diagonale ( i ) ;
i f ( p r o t e g e e ( d i a g o n a l e , c o u l e u r , bord ) )
nb Diago nalesProtegees ++;
}
i f ( b o r d && ( n b D i a g o n a l e s P r o t e g e e s -- 4))
return true ;
106 Recherche arborescente Monte-Carlo
i f ( ! b o r d && ( n b D i a g o n a l e s P r o t e g e e s > 2 ) )
return true ;
return fa l s e ;
}
fo r ( i n t i = 1 ; i <= T a i Il e ; i + + )
fo r ( i n t j = l ; j <= T a i l l e ; j + + )
i f ( g o b a n [ i ] [ j ] == V i d e ) {
Intersection inter ( i , j ) ;
i f ( entouree ( i n t e r , Noir ) )
s c o r e [ N o i r ] += 1 . 0 ;
e l s e i f ( e n t o u re e ( i n ter , Blanc ) )
s c o r e [ B l a n c ] += 1 . 0 ;
}
else
s c o r e [ g o b a n [ i ] [ j ] ] += 1 . 0 ;
i f ( true ) {
i f ( s c o re [ B lanc ] > s c o re [ No i r ] ) {
s c o re [ B lanc ] = 1 . 0 ;
s c ore [ Noir ] = 0 . 0 ;
}
else {
s c o re [ B lanc ] = 0 . 0 ;
s c ore [ Noir ] = 1 . 0 ;
}
}
}
b o o l gameüver ( ) {
i n t l a s t = n b C o u p s Jo u e s - 1 , n b P a s s = O ;
wh i l e ( ( l a s t > 0 ) && ( moves [ l a s t ] . _x 0)) {
--
l a s t --;
nbPas s ++ ;
}
i f ( n b P a s s >= 2 )
return true ;
return fa l s e ;
}
5.9 Corrigés des exercices 107
I n t e r s e c t i o n choisirUnCoup ( in t couleur ) {
i n t urgence [ T a i l l e + 2] [ T a i l l e + 2 ] , nbUrgences = O ;
Intersection inter ;
fo r ( i n t i = 1 ; i <= T a i l l e ; i + + )
fo r ( i n t j = 1 ; j < = T a i Il e ; j + + )
if ( goban [ i ] [ j ] == Vide ) {
urgence [ i ] [ j ] = l;
nbUrgence s ++;
}
else
urgence [ i ] [ j ] = O ;
do {
i f ( n b U r g e n c e s <= 0 )
return I n t e r s e c t i o n (0 , O ) ;
int index = ( in t ) ( nbUrgences *
( r a n d ( ) / (RAND_MAX + 1 . 0 ) ) ) ;
i n t somme = O ;
fo r ( i n t i = l ; ( ( i <= T a i 1 1 e ) && ( somme <= i n d e x ) ) ;
i ++)
fo r ( i n t j = l ; ( ( j <= T a i Il e ) &&
( somme <= i n d e x ) ) ; j + + )
if ( urgence [ i ] [ j ] > 0) {
somme += u r g e n c e [ i ] [ j ] ;
i f ( somme > i n d e x ) {
inter = Intersection ( i , j ) ;
}
}
n b U r g e n c e s -= u r g e n c e [ i n t e r . _x ] [ i n t e r . _y ] ;
u r g e n c e [ i n t e r . _x ] [ i n t e r . _y ] = 0 ;
}
while ( e n to u ree ( i n t e r , c o u l eu r ) 1 1
! coupLegal ( i n t e r , c o u l e u r ) ) ;
return i n t e r ;
}
void p l a y o u t ( i n t c o u l e u r ) {
fo r ( ; ; ) {
i f ( ( n b C o u p s Jo u e s >= MaxCoups ) Il
gameOver ( ) )
break ;
Go g o ;
I n t e r s e c t i o n m e i l l e u r C o u p S amp l i n g ( i n t c o u l e u r ) {
fl o a t m e i l l e u r S c o r e = O ;
Intersection meilleur (0 , O ) ;
fo r ( i n t i = l ; i <= T a i l l e ; i + + )
fo r ( i n t j = 1 ; j < = T a i 1 1 e ; j + + ) {
Intersection inter ( i , j ) ;
f l o a t somme = O ;
i f ( g o . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
! go . o e i l ( i n t e r , c o u l e u r ) ) {
fo r ( i n t k = O ; k < 1 0 0 ; k + + ) {
Go tmpgo = g o ;
tmpgo . j o u e ( i n t e r , c o u l e u r ) ;
i f ( c o u l e u r == N o i r )
tmpgo . p l a y o u t ( B l a n c ) ;
else
tmpgo . p l a y o u t ( No i r ) ;
somme += t m p g o . s c o r e [ c o u l e u r ] ;
}
}
i f ( somme > m e i Il e u r S c o r e ) {
m e i l l e u r S c o r e = somme ;
meilleur = inter ;
}
}
return m e i l l e u r ;
}
void i n t e r fa c e ( ) {
w hi l e ( t r u e ) {
I n t e r s e c t i o n i n t e r = m e i l l e u r C o u p S amp l i n g ( Noir ) ;
c o u t << " m e i l l e u r .....,c o u p ....e., n ...... " << i n t e r . _x << " " <<
i n t e r . _y << e n d ! ;
5.9 Corrigés des exercices 109
go . j o u e ( i n t e r , N o i r ) ;
fo r ( i n t i = O ; i < T a i l l e + 2 ; i + + ) {
fo r ( i n t j = O ; j < T a i l l e + 2 ; j + + )
Il .
i f ( g o . g o b a n [ j ] [ i ] == E x t e r i e u r ) c o u t << '
Il
....,
"'-'@Il;
....,
e l s e i f ( g o . g o b a n [ j ] [ i ] == N o i r ) c o u t <<
e l s e c o u t << " ....,O " ;
c o u t << e n d l ;
}
do {
c o u t << " V o t r e ....,c o u p...., : ...., " ;
c i n >> i n t e r . _x >> i n t e r . _y ;
} w hi l e ( ! g o . c o u p L e g a l ( i n t e r , B l a n c ) ) ;
go . j o u e ( i n t e r , B l anc ) ;
}
}
i n t main ( ) {
go . i n i t H a s h ( ) ;
interface ( ) ;
}
S.9.3 UCB
I n t e r s e c t i o n meilleurCoupUCB ( i n t c o u l e u r ) {
f l o a t s o m m e S core [ T a i l l e + 2 ) [ T a i l l e + 2 ] ;
i n t n b P ia y o u t s C o u p [ T a i l l e + 2 ) [ T a i l l e + 2 ) ;
fo r ( i n t i = O ; i <= T a i l l e ; i + + )
fo r ( i n t j = O ; j < = T a i l l e ; j + + ) {
s o m m e S core [ i ] [ j ] = 0 ;
nbPlayoutsCoup [ i ] [ j ] = O ;
}
I n t e r s e c t i o n meilleurUCB ( 0 , 0 ) ;
fo r ( i n t p = O ; p < n b P l a y o u t s ; p + + ) {
fl o a t m e i l l e u r S c o r e = O ;
fo r ( i n t i = 1 ; i <= T a i 1 1 e ; i + + )
fo r ( i n t j = 1 ; j < = T a i 1 1 e ; j + + ) {
Intersection inter ( i , j ) ;
fl o a t s c o r e = O ;
i f ( g o . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
! go . o e i l ( i n t e r , c o u l e u r ) ) {
i f ( n b P 1 a y o u t s C o u p [ i ] [ j ] == 0 )
s c o re = 1 00 0 ;
else
score =
s o m m e S core [ i ] [ j ) / n b P l a y o u t s C o u p [ i ] [j ] +
1 10 Recherche arborescente Monte-Carlo
C o n stante * s q rt ( lo g ( p ) /
nbPlayoutsCoup [ i ] [j ]) ;
}
i f ( sc o re > m e i l l e u r S c ore ) {
m e i l l e u r S c ore = score ;
meilleurUCB = i n t e r ;
}
}
Go tmpgo = g o ;
tmpgo . j o u e ( m e i l l e u r U C B , c o u l e u r ) ;
i f ( c o u 1 e u r == N o i r )
tmpgo . p l a y o u t ( B l a n c ) ;
else
tmpgo . p l a y o u t ( N o i r ) ;
s o m m e S c o re [ m e i l l e u r U C B . _x ] [ m e i l l e u r U C B . _y ] +=
tmpgo . s c o r e [ c o u l e u r ] ;
n b P l a y o u t s C o u p [ m e i l l e u r U C B . _x ] [ m e i l l e u r U C B . _y ] + + ;
}
fo r ( i n t i = O ; i < T a i l l e + 2 ; i + + ) {
fo r ( i n t j = O ; j < T a i l l e + 2 ; j + + )
Il .
i f ( go . goban [ j ] [ i ] == Ex t e r i e u r ) cout << -
'
"�®t t;
�
e l s e i f ( g o . g o b a n [ j ] [ i ] == N o i r ) cout <<
Il
e l s e i f ( g o . g o b a n [ j ] [ i ] == N o i r ) cout << O" ·
� '
fl o a t m e i l l e u r S c o r e = O ;
I n t e r s e c t i o n m e i l l e u r (0 , 0 ) ;
fo r ( i n t i = O ; i <= T a i l l e ; i + + )
fo r ( i n t j = 0 ; j <= T a i 11 e ; j + + ) {
Intersection i nter ( i , j ) ;
i f ( g o . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
! go . o e i l ( i n t e r , c o u l e u r ) )
i f ( nbPlayoutsCoup [ i ] [ j ] > meilleurS core ) {
meilleurScore = nbPlayoutsCoup [ i ] [ j ] ;
meilleur = inter ;
}
}
return m e i l l e u r ;
}
5.9 Corrigés des exercices 111
5.9.4 UCT
Go g o ;
c l a s s Noeud {
public :
f l o a t s o m m e S core [ T a i l l e + 2 ] [ T a i l l e + 2 ] ;
int nbPlayoutsCoup [ T a i l l e + 2] [ T a i l l e + 2 ] ;
unsigned long long hash ;
i n t a b s c i s s e , ordonnee ;
Noeud * f i l s [ T a i l l e + 2 ] [ T a i l l e + 2 ] ;
void i n i t ( ) {
abscisse = O;
ordonnee = 0 ;
fo r ( i n t i = 0 ; i <= T a i 1 1 e ; i + + )
fo r ( i n t j = 0 ; j < = T a i l ie ; j + + ) {
s o m m e S c o re [ i ] [ j ] = 0 ;
nbPlayoutsCoup [ i ] [ j ] = 0 ;
f i l s [ i ] [ j ] = NULL ;
}
}
fl o a t moyenne ( i n t i , i n t j ) {
i f ( nbPlayoutsCoup [ i ] [ j ] -- 0)
return 0 . 0 ;
r e t u r n s o m m e S core [ i ] [ j ] I n b P l a y o u t s C o u p [ i ] [j ] ;
}
int nbPlayouts () {
i n t nb = O ;
fo r ( i n t i = 0 ; <= T a i 1 1 e ; i + + )
fo r ( i n t j = O ; j <= T a i l l e ; j + + )
nb += n b P1 ay o u t s C o u p [ i ] [ j ] ;
r e t u r n nb ;
}
c o n s t i n t MaxNoeud = 1 0 0 0 0 0 ;
i n t n b N o e u d s = MaxNoeud ;
Noeud p i l e N o e u d [ MaxNoeud ] ;
1 12 Rec herc he arborescente Monte-Carlo
Noeud r a c i n e ;
i f ( ( g o b a n . n b C o u p s Jo u e s >= MaxCoup s ) 11
g oban . gameüver ( ) ) {
goban . c a l c u l e S c o r e s ( ) ;
return ;
}
w hi l e ( o r d o n n e e ! = T a i l l e + 1 ) {
I n t e r s e c t i o n i n t e r ( ab s c i s s e , ordonnee ) ;
a b s c i s s e ++ ;
i f ( a b s c i s s e -- T a i 11 e + 1 ) {
abscisse = 1 ;
ordonnee++;
}
i f ( g o b a n . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
! goban . o e i l ( i n t e r , c o u l e u r ) ) {
goban . j o u e ( i n t e r , c o u l e u r ) ;
nbNoeuds - - ;
Noeud * Il = &p i l e N o e u d [ n b N o e u d s ] ;
n -> i n i t ( ) ;
n ->h a s h = g o b a n . h a s h ;
f i 1 s [ i n t e r . _x ] [ i n t e r . _y ] = n ;
goban . p l a y o u t ( a u t r e ) ;
s o m m e S c o re [ i n t e r . _x ] [ i n t e r . _y ] =
goban . s c o re [ couleur ] ;
n b P l a y o u t s C o u p [ i n t e r . _x ] [ i n t e r . _y ] = 1 ;
return ;
}
}
fl o a t m e i l l e u r S c o r e = - 1 . 0 ;
I n t e r s e c t i o n meilleur (0 , O ) ;
fo r ( i n t i = 1 ; i <= T a i 11 e ; i + + )
fo r ( i n t j = 1 ; j <= T a i l l e ; j + + ) {
Intersection inter ( i , j ) ;
i f ( g o b a n . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
( f i l s [ i ] [ j ] ! = NULL ) ) {
f l o a t moy = m o y e n n e ( i , j ) ;
int p l a y o u t s F i l s = nbPlayoutsCoup [ i ] [ j ] ;
int playoutsPere = nbPlayouts ( ) ;
f l o a t s c o r e = moy +
5.9 Corrigés des exercices 113
I n t e r s e c t i o n m e i l l e u rC o u p U C T ( i n t c o u l e u r ) {
racine . i n i t ( ) ;
n b N o e u d s = MaxNoeud ;
fo r ( i n t p = O ; p < n b P l a y o u t s ; p + + ) {
Go tmpgo = go ;
r a c i n e . d e s c e n t e ( tmpgo , c o u l e u r ) ;
}
int meilleurScore = - 1 ;
Intersection meilleur (0 , 0 ) ;
fo r ( i n t i = O ; i <= T a i l l e ; i + + )
fo r ( i n t j = 0 ; j < = T a i 11 e ; j + + ) {
Intersection inter ( i , j ) ;
i f ( g o . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
! go . o e i l ( i n t e r , c o u l e u r ) ) {
i f ( racine . nbPlayoutsCoup [ i ] [ j ] >
meilleurScore ) {
meilleurScore = rac i n e . nbPlayoutsCoup [ i ] [j ] ;
meilleur = inter ;
}
}
}
return m e i l l e u r ;
}
c l a s s Noeud {
public :
f l o a t s o m m e S core [ T a i l l e + 2 ] [ T a i l l e + 2 ] ;
int nbPlayoutsCoup [ T a i l l e + 2] [ T a i l l e + 2 ] ;
unsigned long long hash ;
114 Recherche arborescente Monte-Carlo
int a b s c i s s e , ordonnee ;
Noeud * f i l s [ T a i l l e + 2 ) [ Taille + 2] ;
void i n i t ( ) {
abscisse = O ;
ordonnee = O ;
fo r ( i n t i = 0 ; i <= T a i l i e ; i + + )
fo r ( i n t j = 0 ; j < = T a i l ie ; j + + ) {
s o m m e S c o re [ i ] [ j ] = 0 ;
nbPlayoutsCoup [ i ] [ j ] = 0 ;
f i l s [ i ) [ j ) = NULL ;
}
}
fl o a t moyenne ( i n t p r o fo n d e u r ) {
i n t nb = 0 ;
f i o a t somme = 0 . 0 ;
fo r ( i n t i = 0 ; i <= T a i l ie ; i + + )
fo r ( i n t j = 0 ; j <= T a i l ie ; j + + )
i f ( p r o fo n d e u r = = 0 ) {
n b += n b P 1 a y o u t s C o u p [ i ] [ j ] ;
somme += s o m m e S c ore [ i ] [ j ] ;
}
else {
i f ( f i l s [ i ] [ j ] ! = NULL) {
int nbPlayoutFils =
f i l s [ i ] [ j ) - > n b P l a y o u t s ( p r o fo n d e u r - 1 ) ;
n b += n b P 1 a y o u t F i 1 s ;
somme += n b P 1 a y o u t F i 1 s * ( 1 . 0 -
f i l s [ i ] [ j ) ->moyenne ( p r o fo n d e u r - 1 ) ) ;
}
}
i f ( n b == 0 )
return 0 . 0 ;
r e t u r n somme / n b ;
}
fl o a t moyenne ( i n t i , i n t j , i n t p r o fo n d e u r ) {
i f ( p r o fo n d e u r == 0 ) {
i f ( n b P ia y o u t s C o u p [ i ] [ j ] == 0 )
return 0 . 0 ;
r e t u r n s o m m e S core [ i ] [ j ) / n b P l a y o u t s C o u p [ i ] [ j ] ;
}
e l s e i f ( f i l s [ i ] [ j ) ! = NULL)
r e t u r n 1 . 0 - f i 1 s [ i ] [ j ] - >m o y e n n e ( p r o fo n d e u r 1);
-
else {
i f ( n b P 1 a y o u t s C o u p [ i ] [ j ] == 0 )
5.9 Corrigés des exercices 115
return 0 . 0 ;
r e t u r n s o m me S c o re [ i ] [j ] / nbPlayoutsCoup [ i ] [j ] ;
}
}
i n t n b P l a y o u t s ( i n t p r o fo n d e u r ) {
i n t nb = O ;
fo r ( i n t i = O ; i <= T a i l l e ; i + + )
fo r ( i n t j = O ; j <= T a i l l e ; j + + )
i f ( p r o fo n d e u r == 0 )
n b += n b P l a y o u t s C o u p [ i ] [ j ] ;
e l s e i f ( f i l s [ i ] [ j ] ! = NULL) {
n b += f i 1 s [ i ] [ j ] - > n b P 1 a y o u t s ( p ro fo n de u r - 1 ) ;
}
r e t u r n nb ;
}
i n t n b P l a y o u t s F i l s ( i n t i , i n t j , i n t p r o fo n d e u r ) {
i f ( p r o f o n d e u r == 0 )
return n b P l ay o u t s C o u p [ i ] [ j ] ;
e l s e i f ( f i l s [ i ] [ j ] ! = NULL)
r e t u r n f i l s [ i ] [ j ] - > n b P l a y o u t s ( p r o f o n d e u r - l);
else
return nbPlayoutsCoup [ i ] [ j ] ;
}
c l a s s Table {
public :
l i s t <Noeud * > t a b l e [ TailleTable + 1 ] ;
Noeud * p r e s e n t ( u n s i g n e d l o n g l o n g h a s h ) {
fo r ( l i s t <Noeud * > : : i t e r a t o r i t e r =
t a b l e [ hash & T a i l l e T a b l e ] . begin ( ) ;
i t e r ! = t a b l e [ hash & T a i l l e T a b l e ] . end ( ) ; i t e r ++)
i f ( ( * i t e r ) -> h a s h == h a s h )
return * i t e r ;
r e t u r n NULL ;
}
v o i d aj o u t e ( Noeud * n ) {
t a b l e [ n ->h a s h & T a i l l e T a b l e ] . p u s h _ b a c k ( n ) ;
}
116 Recherche arborescente Monte-Carlo
void c l e a r ( ) {
fo r ( i n t i = O ; i < T a i l l e T a b l e + l ; i ++)
table [ i ] . clear ( ) ;
}
};
Table t a b l e ;
c o n s t i n t MaxNoeud = 1 0 0 0 0 0 ;
i n t n b N o e u d s = MaxNoeud ;
Noeud p i l e N o e u d [ MaxNoeud ] ;
Noeud r a c i n e ;
i n t nmoy = 0 , n p e r e = 0 , n f i l s = O ;
bool p a s DeTra n s p o = fa l s e ;
i f ( ( g o b a n . n b C o u p s Jo u e s >= MaxCoups ) 1 1
g o ban . gameOver ( ) ) {
goban . c a l c u l e S c o r e s ( ) ;
return ;
}
while ( ordonnee ! = T a i l l e + l ) {
I n t e r s e c t i o n i n t e r ( ab s c i s s e , ordonnee ) ;
a b s c i s s e ++ ;
i f ( a b s c i s s e -- T a i li e + 1 ) {
a b s c i s s e = l;
ordonnee ++;
}
i f ( g o b a n . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
! goban . o e i l ( i n t e r , c o u l e u r ) ) {
goban . j o u e ( i n t e r , c o u l e u r ) ;
Noeud * n = t a b l e . p r e s e n t ( g o b a n . h a s h ) ;
i f ( pasDeTranspo )
n = NULL ;
i f ( n == NULL) {
nbNoeuds - - ;
n = &p i l e N o e u d [ n b N o e u d s ] ;
n -> i n i t ( ) ;
n ->h a s h = g o b a n . h a s h ;
5.9 Corrigés des exercices 1 17
t a b l e . aj o u t e ( n ) ;
f i 1 s [ i n t e r . _x ] [ i n t e r . _y ] = n ;
goban . p l a y o u t ( a u t re ) ;
s o m m e S c o re [ i n t e r . _x ] [ i n t e r . _y ] =
goban . s c o re [ c o u l e u r ] ;
n b P ia y o u t s C o u p [ i n t e r . _x ) [ i n t e r . _y ) = 1 ;
return ;
}
f i 1 s [ i n t e r . _x ] [ i n t e r . _y ] = n ;
n -> d e s c e n t e ( g o b a n , a u t r e ) ;
s o m m e S core [ i n t e r . _x ) [ i n t e r . _y ) =
goban . s c o re [ couleur ] ;
n b P l a y o u t s C o u p [ i n t e r . _x ) [ i n t e r . _y ) = 1 ;
return ;
}
}
fl o a t m e i l l e u r S c o r e = - 1 . 0 ;
I n t e r s e c t i o n meilleur (0 , 0 ) ;
fo r ( i n t i = 1 ; i <= T a i 1 1 e ; i + + )
fo r ( i n t j = 1 ; j < = T a i 1 1 e ; j + + ) {
Intersection inter ( i , j ) ;
i f ( g o b a n . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
( f i l s [ i ] [ j ] ! = NULL ) ) {
f l o a t moy = m o y e n n e ( i , j , nmoy ) ;
int playoutsFils = nbPlayoutsFils ( i , j , n fi l s ) ;
i n t p l a y o u t s P e r e = n b P ia y o u t s ( n p e r e ) ;
f l o a t s c o r e = moy +
Con stante * s q rt ( log ( playouts Pere )
playoutsFils ) ;
i f ( s c o re > m e i l l e u r S c o r e ) {
meilleurScore = score ;
meilleur = inter ;
}
}
}
goban . j o u e ( me i l l e u r , c o u l e u r ) ;
f i l s [ m e i l l e u r . _x ) [ m e i l l e u r . _y ) - > d e s c e n t e ( g o b a n ,
autre ) ;
s o m m e S c o re [ m e i 1 l e u r . _x ] [ m e i l ie u r . _y ] +=
go b a n . s c o r e [ c o u l e u r ] ;
n b P l a y o u t s C o u p [ m e i l l e u r . _x ) [ m e i l l e u r . _y ) + + ;
}
118 Recherche arborescente Monte-Carlo
5.9.6 RAVE
c l a s s NoeudRave {
public :
f l o a t s o m m e S c o re [ T a i l l e + 2 ) [ T a i l l e + 2 ] ;
int nbPlayoutsCoup [ Ta i l l e + 2) [ T a i l l e + 2 ] ;
unsigned long long hash ;
int a b s c i s s e , ordonnee ;
NoeudRave * f i l s [ T a i l l e + 2 ) [ T a i l l e + 2 ] ;
f i o a t sommeScoreAMAF [ T a i 11 e + 2 ] [ T a i l 1 e + 2 ] ;
i n t nbPlayoutsCoupAMAF [ T a i l l e + 2 ) [ T a i l l e + 2 ) ;
void i n i t ( ) {
abscisse = O ;
ordonnee = 0 ;
fo r ( i n t i = 0 ; i <= T a i 11 e ; i ++)
fo r ( i n t j = O ; j <= T a i l l e ; j ++) {
s o m m e S c ore [ i ] [ j ] = 0 ;
nbPlayoutsCoup [ i ] [ j ] = 0;
f i l s [ i ] [ j ] = NULL ;
sommeScoreAMAF [ i ] [ j ] = 0;
n b P l a y o u t s C o u pAMAF [ i ] [ j ] = 0;
}
}
fl o a t moyenne ( i n t p r o fo n d e u r ) {
i n t nb = 0 ;
f i o a t somme = 0 . 0 ;
fo r ( i n t i = O ; i <= T a i l l e ; i + + )
fo r ( i n t j = O ; j < = T a i l l e ; j + + )
i f ( p r o fo n d e u r = = 0 ) {
nb += n b P l a y o u t s C o u p [ i ] [ j ] ;
somme += s o m m e S c ore [ i ] [ j ] ;
}
else {
i f ( f i l s [ i ] [ j ] ! = NULL) {
int nbPlayoutFils =
f i l s [ i ] ( j ) - > n b P l a y o u t s ( p r o fo n d e u r - l);
n b += n b P 1 a y o u t F i 1 s ;
somme += n b P 1 a y o u t F i 1 s *
(1 .0 -
f i l s [ i ] [ j ] - > m o y e n n e ( p r o fo n d e u r - l));
}
}
i f ( nb == 0 )
return 0 . 0 ;
5.9 Corrigés des exercices 119
r e t u r n somme / n b ;
}
f l o a t m o y e n n e ( i n t i , i n t j , i n t p r o fo n d e u r ) {
i f ( p r o f o n d e u r == 0 ) {
r e t u r n ( s o m m e S c o re [ i ] [ j ] + 1 )
( nbPlayoutsCoup [ i ] [ j ] + 2 ) ;
}
e l s e i f ( f i l s [ i ] [ j ] ! = NULL)
r e t u r n 1 . 0 - f i l s [ i ] [ j ] - >m o y e n n e ( p r o fo n d e u r - 1 ) ;
else {
i f ( n b P l a y o u t s C o u p [ i ] [ j ] == 0 )
return 0 . 0 ;
r e t u r n s o m m e S c o re [ i ] [ j ] / n b P l a y o u t s C o u p [ i ] [ j ] ;
}
}
i n t n b P l a y o u t s ( i n t p r o fo n d e u r ) {
i n t nb = O ;
fo r ( i n t i = O ; i <= T a i l l e ; i + + )
fo r ( i n t j = 0 ; j < = T a i 1 1 e ; j + + )
i f ( p r o fo n d e u r == 0 )
nb + = n b P 1 a y o u t s C o u p [ i ] [ j ] ;
e l s e i f ( f i l s [ i ] [ j ] ! = NULL) {
n b += f i 1 s [ i ] [ j ] - > n b P 1 a y o u t s ( p r o fo n d e u r - 1 ) ;
}
return nb ;
}
i n t n b P l a y o u t s F i l s ( i n t i , i n t j , i n t p r o fo n d e u r ) {
i f ( p r o fo n d e u r == 0 )
return nbPlayoutsCoup [ i ] [ j ] ;
e l s e i f ( f i l s [ i ] [ j ] ! = NULL)
r e t u r n f i l s [ i ] [ j ] - > n b P l a y o u t s ( p r o fo n d e u r - 1 ) ;
else
return nbPlayoutsCoup [ i ] [ j ] ;
}
v o i d modifieAMAF ( i n t n b C o u p s , Go & g o b a n ,
int couleur ) {
d ej a v u . i n i t ( ) ;
fo r ( i n t i = n b C o u p s ; i < g o b a n . n b C o u p s Jo u e s ; ++) {
I n t e r s e c t i o n i n t e r = g o b a n . moves [ i ] ;
i f ( i n t e r . _x ! = 0 ) {
i f ( ! d ej a v u . m a rq u e e ( i n t e r ) ) {
d ej a v u . marq u e ( i n t e r ) ;
i f ( ( ( i - n b C o u p s ) & 1 ) == 0 ) {
120 Recherche arborescente Monte-Carlo
sommeScoreAMAF [ i n t e r . _x ] [ i n t e r . _y ] +=
g o b an . s c o r e [ c o u l e u r ] ;
n b P l a y o u ts C o u p A M A F [ i n t e r . _x ] [ i n t e r . _y ] + + ;
}
}
}
}
}
c l a s s TableRave {
public :
l i s t <NoeudRave * > t a b l e [ TailleTable + l ] ;
v o i d aj o u t e ( No e u d R a v e * Il) {
t a b l e [ n ->h a s h & T a i l l e T a b l e ] . p u s h _ b a c k ( n ) ;
}
void c l e a r ( ) {
fo r ( i n t i = O ; i < T a i l l e T a b l e + l; i ++)
table [ i ] . clear ( ) ;
}
};
TableRave tableRav e ;
c o n s t i n t MaxNoeudRave = 1 0 0 0 0 0 ;
i n t n b N o e u d s R a v e = MaxNoeudRave ;
N o e u d R a v e p i l e N o e u d R a v e [ M axNoeudRave ] ;
NoeudRave r a c i n e R a v e ;
a u t r e = B lanc ;
fl o a t m e i l l e u r S c o r e = - 1 . 0 ;
I n t e r s e c t i o n meilleur (0 , 0 ) ;
fo r ( i n t i = l ; i <= T a i l l e ; i + + )
fo r ( i n t j = l ; j < = T a i l ie ; j + + ) {
Intersection inter ( i , j ) ;
i f ( g o b a n . c o u p L e g a l ( i n t e r , c o u l e u r ) &&
! goban . o e i l ( i n t e r , c o u l e u r ) ) {
f l o a t moy = m o y e n n e ( i , j , nmoy ) ;
int playoutsFils = nbPlayoutsFils ( i , j , n fi l s ) ;
f i o a t moyenneAMAF = ( sommeScoreAMAF [ i ] [ j ] + 1 ) /
( nbPlayoutsCoupAMAF [ i ] [ j ] + 2 ) ;
f l o a t b e t a = ( n b P l a y o u t s C o u p AMAF [ i ] [ j ) /
( l + n b P l ay o u ts C o u p A M A F [ i ] [ j ] +
p l ay o u t s F i l s + ConstanteRave *
nbPlayoutsCoupAMAF [ i ] [ j ] *
playoutsFils ) ) ;
f i o a t s c o r e = ( l . 0 - b e t a ) * moy +
b e t a * moyenneAMAF ;
i f ( s core > m e i l l e u r S c o re ) {
m e i l l e u r S c o re = s core ;
meilleur = inter ;
}
}
}
goban . j o u e ( me i l l e u r , c o u l e u r ) ;
i f ( f i l s [ m e i l l e u r . _x ) [ m e i l l e u r . _y ) ! = NULL )
f i l s [ m e i l l e u r . _x )
[ m e i l l e u r . _y ] - > d e s c e n t e ( g o b a n , a u t r e ) ;
else {
NoeudRave * n = t a b l e R a v e . p r e s e n t ( g o b a n . h a s h ) ;
i f ( pas DeTranspo )
n = NULL ;
i f ( n == NULL) {
nbNoeudsRave - - ;
n = &p i l e N o e u d R a v e [ n b N o e u d s R a v e ] ;
n -> i n i t ( ) ;
n ->h a s h = g o b a n . h a s h ;
t a b l e R a v e . aj o u t e ( n ) ;
122 Recherche arborescente Monte-Carlo
f i 1 s [ m e i 1 1 e u r . _x ] [ m e i 1 1 e u r . _y ] = n ;
g o b an . p l a y o u t ( a u t r e ) ;
}
else {
f i 1 s [ m e i 1 1 e u r . _x ] [ m e i 1 1 e u r . _y ] = n ;
n -> d e s c e n t e ( g o b a n , a u t r e ) ;
}
}
s o m m e S c o re [ m e i l l e u r . _x ] [ m e i l l e u r . _y ] +=
goban . s c o re [ c o u l e u r ] ;
n b P ia y o u t s C o u p [ m e i l l e u r . _x ] [ m e i l l e u r . _y ] + + ;
modifieAMAF ( n b C o u p s , g o b a n , c o u l e u r ) ;
}
Proof Number search [4] , Conspiracy Number search [59, 77) et B * [7, 9) sont des
algorithmes de recherche en meilleur d ' abord. Ils ont été utilisés pour résoudre certains
jeux comme Puissance 4 [2] , Go-Moku [3] ou plus récemment Fanorona [74) .
Le principe des algorithmes de recherche en meilleur d ' abord est de garder l ' arbre en
mémoire et de l ' analyser afin de choisir la feuille la plus intéressante à développer.
Proof Number Search (PN-search) permet de prouver qu ' un jeu ou qu ' une position
est gagnée pour un joueur. Le résultat de l ' algorithme est binaire ; il renvoie 1 s ' il réussit
à prouver le gain et 0 sinon.
PN-search marche particulièrement bien sur les arbres ET/OU quand le nombre de
coups légaux varie beaucoup et qu ' on peut élaguer de grandes parties de l ' arbre.
Chaque noeud comporte un Proof Number (PN) qui estime le coût de prouver 1 et un
Oisproof Number (ON) qui estime le coût de prouver O. Une feuille est terminale si elle
correspond à une position gagnée ou perdue. Une feuille non terminale a un PN de 1 et
un ON de 1 . Une feuille gagnée a un PN de 0 et un ON infini alors qu ' une feuille perdue
a un PN infini et un ON de O.
124 Rec herc he en meilleur d'abord pour les jeux à deux joueurs
Aux noeuds OU, pour prouver le noeud, il suffit qu ' un seul des fils ait la valeur l . Le
nombre minimum de coups pour prouver le noeud OU est donc le minimum sur tous les
fils du nombre de coups qu ' il faut pour prouver chacun des fils.
En revanche, pour prouver la valeur 0 à un noeud OU, il faut que tous les fils aient la
valeur O. Le nombre minimum de coups pour prouver la valeur 0 à un noeud OU (soit le
disproof number associé au noeud OU) est donc la somme des disproof numbers des fils.
De manière symétrique, aux noeuds ET, le proof number sera la somme des proof
numbers des fils (il faut que tous les fils soient à 1 pour que le noeud ET soit à 1 ) , et le
disproof number sera le minimum des disproof numbers des fils (il suffit qu ' un seul des
fils soit à 0 pour que le noeud ET soit à 0).
La remontée des valeurs de l ' arbre de la figure 6. 1 est donnée dans la figure 6.2.
Aux noeuds ET, si on veut prouver le noeud ET, il faudra de toutes façons prouver
tous ses fils. En revanche, si on veut prouver la valeur 0, il suffira de prouver la valeur
0 pour un seul de ses fils. Le choix qui minimise la taille des arborescences est donc de
6.1 L'algorit hme Proof Number Searc h 125
2, 1
Infini,O l, l
choisir le fils pour lequel on pourra prouver la valeur 0 le plus facilement. C ' est à dire le
fils qui a le disproof number minimal.
Lorsqu ' on développe une feuille qui correspond à un état gagné on initialise son proof
number à 0 et son disprof number à l ' infini. De manière symétrique, lorsqu ' on arrive à
une feuille qui correspond à un état perdu, on initialise son proof number à l ' infini et son
disproof number à O.
Plutôt que de recalculer à chaque développement de feuille toutes les valeurs de l ' ar-
126 Rec herc he en meilleur d'abord pour les jeux à deux joueurs
borescence, on peut évaluer les noeuds incrémentalement en ne recalculant que ceux par
lesquels on est passé.
De plus lorsqu ' on cherche à prouver la valeur d ' un jeu qui a plus de résultats pos
sibles que gagné ou perdu, par exemple à Fanorona qui a des résultats nuls, on utilise
une recherche dichotomique avec des tests sur les bornes des résultats plutôt que sur les
résultats : par exemple on commence par une recherche qui cherche à prouver le gain, et
si elle échoue on fait une autre recherche pour décider entre la perte et la nulle.
Exercice : Écrire un classe Connect qui joue au jeu d ' aligner 3 pions sur un goban
1 1 x 1 1 . Écrire ensuite un programme qui résout ce jeu avec l ' algorithme proof number
search.
L' alghorithme PN2 [ 1 3) est une extension de PN-search qui repousse ses limites de
mémoire. Il permet de résoudre des problèmes plus complexes au prix d ' une recherche
plus longue. Il réduit la mémoire nécessaire à une recherche PN en l ' échangeant contre
du temps de calcul. Au lieu d ' évaluer directement les feuilles de l ' arbre développé par
PN-search, PN2 effectue une deuxième recherche PN pour les feuilles de l ' arbre. Les
PN et les DN des feuilles de la recherche principale sont initialisés avec les PN et les
DN de la racine de la recherche secondaire. Cette astuce permet d ' explorer des arbres
beaucoup plus grands qu ' avec PN-search à taille mémoire constante, et moyennant une
perte de temps. Cependant, la limitation principale de PN-search est la capacité mémoire.
L' algorithme PN2 permet donc de résoudre des problèmes plus complexes que PN.
Exercice : Écrire un programme qui utilise PN2 pour résoudre le jeu d ' aligner quatre
pions sur un damier 1 5 x l 5 .
L' idée de PN * est d ' utiliser un seuil sur le nombre de feuilles restant à prouver. Si le
nombre de feuilles dépasse ce seuil il suffit alors de couper la branche correspondante.
6.4 Df-pn 127
6.4 Df-pn
Df-pn (Depth first proof number) [64) est une amélioration de PN * qui fait une re
cherche en profondeur d' abord en utilisant des seuils aussi bien pour les PN que pour les
DN. Il utilise moins de mémoire que PN-search. À chaque noeud on a deux variables :
Les seuils utilisés par Df-pn sont propres aux noeuds comme dans la recherche récur
sive en meilleur d' abord [55) . La fonction récursive MID [64, 49) explore ses fils tant que
les PN et DN ne dépassent pas leurs seuils et tant que le noeud n'est pas prouvé. Comme
les mêmes noeuds sont redéveloppés de nombreuses fois, la table de transposition est très
importante pour Df-pn. Elle stocke les PN et les DN du noeud en plus des informations
classiques, lorsqu 'une position n ' est pas dans la table son PN et son DN sont intialisés à
l.
i n t d fp n ( n o d e r o o t ) {
root . phi = I n fi n i t e ;
mot . d e l t a = I n fi n i t e ;
MID ( r o o t ) ;
i f ( r o o t . d e l t a == 1 n f i n i t e )
return 1 ;
else
return 0 ;
}
v o i d MID ( n o d e n ) {
TT . l o o k ( n , p h i , d e l t a ) ;
i f ( n . phi < phi I l n . d e l t a < d e l t a ) {
I l s e u i l d ép assé
n . phi = phi ;
n . delta = delta ;
return ;
}
i f ( fi n ( n ) ) {
i f ( gagne ( n ) ) {
n . phi = O ;
n . delta = I n i fi n i t e ;
return ;
}
else {
n . phi = I n fi n i t e ;
n . delta = O ;
return ;
}
}
128 Rec herc he en meilleur d'abord pour les jeux à deux joueurs
fi n d L e g a l M o v e s ( n , moves ) ;
I l m e m o r ise l e s ph i e t d e lt a p o u r e v i t e r l e s r e p e t i t i o n s
TI . a d d ( n , n . p h i , n . d e l t a ) ;
I l a p p r ofo n d i sse m e n t i t e r a t if
w hi l e ( n . p h i > d e l t a M i n ( n ) &&
n . d e l t a > phiSum ( n ) ) {
ne = s e l e c t C h i l d ( n , phic , d e l t a c , d e l t a 2 ) ;
ne . p h i = n . d e l t a + p h i c - phiSum ( n ) ;
n e . d e l t a = min ( n . p h i , d e l t a 2 + l);
MID ( n e ) ;
}
n . phi = deltaMin ( n ) ;
n . d e l t a = phiSum ( n ) ;
TI . a d d ( n , n . p h i , n . d e l t a ) ;
}
1 1 t r o u v e le f i ls le p lu s p r o m e t t e u r
node s e l e c t C h i l d ( node n , i n t & phic , i n t & d e l t a c ,
int & delta2 ) {
node n b e s t ;
deltac = I n fi n i t e ;
phic = I n fi n i te ;
pour chaque fi l s {
TI . l o o k ( f i l s , p h i , d e l t a ) ;
I l m e m o r ise le p lu s p e t i t d e lt a
I l e t le de u x i è m e p lu s p e t i t d e l t a
if ( delta < deltac ) {
nbest = fi l s ;
delta2 = deltac ;
phic = phi ;
deltac = delta ;
}
else if ( delta < delta2 )
delta2 = delta ;
i f ( p h i == 1 n f i n i t e )
return n b e s t ;
}
return nbest ;
}
i n t d e l t a M i n ( node n ) {
i n t min = I n f i n i t e ;
pour c h aque f i l s {
TI . l o o k ( f i l s , p h i , d e l t a ) ;
i f ( d e l t a < min )
min = d e l t a ;
}
6.5 Les nombres conspirants 129
r e t u r n min ;
}
i n t phiSum ( node n ) {
i n t sum = 0 ;
pour chaque fi 1 s {
TI . l o o k ( f i l s , p h i , delta ) ;
sum += p h i ;
}
r e t u r n sum ;
}
L' algorithme des nombres conspirants est dû à D.A. Mac Allester [59] . Une autre des
cription a été donnée par J. Schaeffer [77] . C ' est un algorithme qui utilise une fonction
d ' évaluation aux feuilles et qui construit un arbre de recherche de profondeur variable sans
connaissances du domaine. Le principe de l ' algorithme est de déterminer dans quelle me
sure l ' approfondissement de la recherche d 'un sous-arbre est utile. Cette mesure est faite
par les nombres conspirants qui représentent le nombre minimum de feuilles qui doivent
changer leur valeur (en approfondissant la recherche) pour que la valeur minimax du
sous-arbre change. Cette recherche est contrôlée par le seuil conspirant (CT), le nombre
minimum de nombres conspirants au dessus duquel il est considéré comme improbable
que la valeur du sous-arbre change.
Pour une feuille, changer sa valeur ne demande la conspiration que de cette feuille
elle-même, son nombre conspirant est alors de 1 . Si la valeur ne doit pas être changée,
alors le nombre conspirant est O. Si la feuille est une feuille terminale on ne peut pas
changer sa valeur, son nombre conspirant est alors Infini.
Pour un noeud interne à un niveau Max, augmenter sa valeur jusqu ' à v ne nécessite
d ' augmenter qu ' un seul fils jusqu ' à v. Le nombre minimum de nombres conspirants pour
augmenter la valeur du noeud est donc le minimum des nombres conspirants des fils pour
augmenter la valeur à v. Si v est inférieur ou égal à m la valeur minimax du noeud son
nombre conspirant est O.
Pour faire décroître la valeur d' un noeud Max à v on doit faire décroître les valeurs de
tous les fils qui ont une valeur supérieure à v. Le nombre minimum de conspirateurs pour
faire décroître la valeur d ' un noeud est la somme de tous les nombres conspirants des fils
pour faire décroître la valeur vers v. Si v est supérieur ou égal à m , le nombre conspirant
est O.
Pour un noeud interne Min, on prend les relations duales, à savoir la somme des
nombres conspirants pour faire croître vers une valeur v supérieure à m, et le minimum
des nombres conspirants pour faire décroître vers une valeur v inférieure à m, 0 sinon.
130 Recherche en meilleur d'abord pour les jeux à deux joueurs
L' algorithme continue jusqu ' à ce qu'il n ' y ait plus qu 'une seule valeur possible. C'est
à dire quand on pense que plus de recherche ne changera pas la valeur de la racine. Plus
le seuil est grand, plus on peut avoir confiance dans la valeur finale de la racine.
Étant donné un ensemble de valeurs possibles pour la racine, comment les élimine+
on toutes sauf une ? La façon la plus simple est de les ôter une par une en commençant
soit par tmax la plus grande valeur encore possible à la racine, soit par tmin la plus petite
valeur encore possible. Pour éliminer tmax, l' algorithme essaie soit de changer la valeur
de la racine à tmax, soit d' augmenter le nombre conspirant de tmax jusqu ' à SC (Augmen
terRacine), ce qui est fait en prouvant qu ' un élément de l'ensemble conspirant minimal ne
conspirera pas avec les autres éléments de l'ensemble pour changer la valeur de la racine
vers tmax. Une stratégie similaire est utilisée pour éliminer tmin (DiminuerRacine).
A chaque étape du développement de l' arbre, l' algorithme doit choisir soit de Aug
menterRacine ou de DiminuerRacine. Il choisit d'éliminer la valeur qui est la plus éloi
gnée de tracine. Si les valeurs sont à égales distances de tracine, il choisit DiminuerRa
cine. Si on a choisit d'éliminer tmax, par exemple, une feuille de l 'ensemble minimal de
conspirateurs doit être développée un coup de plus. Pour trouver cette feuille à développer,
l' algorithme descend de la racine en utilisant la procédure suivante :
- pour un noeud Max : Seul un successeur doit augmenter sa valeur à tmax pour
que le noeud père en fasse autant. La branche la plus à même de changer la valeur
6.6 L'algorithme B* 131
est celle qui demande le moins de conspirateurs pour avoir tmax. On choisit donc
la branche qui a le nombre minimum de conspirateurs, et la plus à gauche en cas
d'égalité.
- pour un noeud Min : On choisit, parmi toutes les branches qui doivent augmenter
leur valeur jusqu ' à tmax pour changer la valeur du noeud, celle qui est la plus à
gauche.
Lorsqu 'on atteint une feuille celle ci est développée. Comme chaque fils peut amener à
un résultat favorable ou défavorable, les fils sont ordonnés en fonction de leur évaluation.
En mettant les fils les plus favorables en premier, cela accroît les chances que le fils le
plus à gauche soit le meilleur, et favorise donc le choix du fils le plus à gauche dans
l' algorithme. Les valeurs minimax et les nombres conspirants sont alors remontés dans
l ' arbre.
6.6 L'algorithme B *
B * ( 7, 9] cherche à prouver qu ' un coup est meilleur que les autres. Il utilise deux
bornes sur la valeur heuristique de la position, une valeur pessimiste et une valeur opti
miste. B * se termine lorsqu 'il a prouvé que la valeur pessimiste d'un coup est supérieure
aux valeurs optimistes de tous les autres coups. H. Berliner a utilisé B * pour les Échecs.
L'idée à la base de l' algorithme B * est qu 'il n'est pas nécessaire de connaître les
valeurs minimax exactes des fils de la racine pour trouver le meilleur coup. Si on peut
trouver des limites pour les valeurs minimax de ces successeurs, puis prouver que la limite
inférieure du meilleur successeur est plus grande ou égale aux limites supérieures des
autres coups, cela suffit pour prouver qu 'il est le meilleur.
Remonter uniquement les bornes d' évaluation fait perdre des informations vitales,
comme par exemple le risque associé à ces bornes. C'est pourquoi on peut améliorer B *
en lui adjoignant des probabilités.
associé à ce noeud.
Dans B * , il y a toujours un des deux joueurs qui essaye de forcer la situation. Pendant
la phase de Sélection, c ' est le joueur. Pendant la phase de Vérification, c ' est son adver
saire. Le joueur qui essaye de forcer est appelé le Forceur. Quand on remonte un noeud
pour lequel le Forceur a le choix, on remonte toujours la meilleure alternative. Pour l ' autre
joueur, l ' Obstructeur, c 'est la conjonction des alternatives qui est remontée, puisqu ' elles
doivent toutes être réfutées.
int ValeurCib l e ;
S e l e c t ionner :
tant que ( Re a lVal ( Me i l leurCoupALaRac ine ) <
OptVa l ( AutreCoup ) ) {
ValeurC ible = ( OptVa l ( Sec ondMe i l leur ) + Rea lVa l ( Be s t ) ) / 2 ;
TrouverLeNoeudRac ineAvecOptProbMaximal ( ) ;
De s c endre l e sous arbre en s e l e c t i onnant
- l e f i l s avec OptProb max aux noeuds MAX
- l e f i l s avec la me i l leure RealVal aux noeuds MIN
C a l c u l e r RealVal pour chaque f i l s de l a f e u i l l e
S i c ' e s t un noeud MAX , c a l c u l e r OptVa l pour chaque f i l s
remonter l e s valeurs
S i p l u s de temps
s o r t i r de l a bouc l e
}
ValeurC ible = RealVa l ( SecondMe i l l eurCoupALaRac ine ) - 1
Ver i f ier :
wh i l e ( RealVal ( Me i l l eurCoupALaRac ine ) >= Va leurC ib l e ) {
s e l e c t ionner le noeud MIN avec le plus grand OptProb
D e s cendre le sous arbre en s e lect ionnant
- l e f i l s avec le OptProb max aux noeuds MIN
- l e f i l s avec avec le me i l leur RealVal aux noeuds MAX
C a l c u l e r RealVa l pour c haque f i l s de l a feui l l e
S i c ' e s t un noeud MIN , c a l c u l e r OptVa l s pour c h aque f i l s
remonter l e s valeurs
S i p l u s de temps
s o r t i r de l a bouc l e
}
6. 7 Corrigés des exercices 133
u s i n g namespace s t d ;
u n s i g n e d l o n g l o n g H a s h A rr a y [2] [ Ta i l l e + 2] [ Taille + 2] ;
c l a s s Connect {
public :
char goban [ T a i l l e + 2 ] [ T a i l l e + 2 ] ;
unsigned long long hash ;
Connect ( ) {
hash = O ;
fo r ( i n t i = 1 ; i <= T a i 1 1 e ; i + + )
fo r ( i n t j = 1 ; j < = T a i 1 1 e ; j + + )
goban [ i ] [ j ] = Vide ;
fo r ( i n t i = O ; < T a i l l e + 2 ; i ++) {
goban [ O ] [ i ] = E x t e r i e u r ;
goban [ i ] [ O ] = E x t e r i e u r ;
goban [ T a i l l e + 1 ] [ i ] = E x t e r i e u r ;
goban [ i ] [ T a i l l e + 1 ] = E x t e r i e u r ;
}
}
void i n i t H a s h ( ) {
fo r ( i n t c = O ; c < 2 ; c + + )
134 Recherche en meilleur d'abord pour les jeux à deux joueurs
fo r ( i n t i = 1 ; i <= T a i l l e ; i + + )
fo r ( i n t j = 1 ; j < = T a i l l e ; j + + ) {
H a s h A rr a y [ c ] [ i ] [ j ] = 0 ;
fo r ( i n t b = O ; b < 6 4 ; b + + )
i f ( ( r a n d ( ) / (RAND_MAX + 1 . 0 ) ) > 0 . 5 )
H a s h A rray [ c ] [ i ] [ j ] I = ( llJLL « b ) ;
}
}
void j o u e ( i n t x , i n t y , i n t c o u l e u r ) {
goban [ x ] [ y ] = c o u l e u r ;
h a s h A : H a s h A rr a y [ c o u l e u r ] [ x ] [ y ] ;
}
v o i d d ej o u e ( i n t x , i n t y , i n t c o u l e u r ) {
goban [ x ] [ y ] = V ide ;
h a s h A = H a s h A rr a y [ c o u l e u r ] [ x ] [ y ] ;
}
bool gagne ( i n t x , i n t y , i n t c o u l e u r ) {
i n t nb [ 8 ] = { O } ;
fo r ( i n t i = 1 ; i < T a i l l e A l i g n e m e n t ; i + + ) {
i f ( n b [ 0 ] == i - 1 )
i f ( g o b a n [ x ] [ y - i ] == c o u l e u r )
nb [ O ] + + ;
i f ( n b [ 1 ] == i - 1 )
i f ( g o b a n [ x + i ] [ y - i ] -- c o u 1 e u r )
nb [ 1 ] + + ;
i f ( n b [ 2 ] == i - 1 )
i f ( g o b a n [ x + i ] [ y ] -- c o u l e u r )
nb [ 2 ] + + ;
i f ( n b [ 3 ] == i - 1 )
i f ( g o b a n [ x + i ] [ y + i ] -- c o u 1 e u r )
nb [ 3 ] + + ;
i f ( n b [ 4 ] == i - 1 )
i f ( g o b a n [ x ] [ y + i ] -- c o u l e u r )
nb [ 4 ] + + ;
i f ( n b [ 5 ] == i - 1 )
i f ( g o b a n [ x - i ] [ y + i ] -- c o u 1 e u r )
nb [ 5 ] + + ;
i f ( n b [ 6 ] == i - 1 )
i f ( g o b a n [ x - i ] [ y ] -- c o u l e u r )
nb [ 6 ] + + ;
i f ( n b [ 7 ] == i - 1 )
i f ( g o b a n [ x - i ] [ y - i ] -- c o u l e u r )
nb [ 7 ] + + ;
}
6. 7 Corrigés des exercices 135
b o o l gameüver ( ) {
return fa l s e ;
}
};
Connect connect ;
const i n t I n fi n i = 1 000000 0 ;
int c o u l e u rPro u v ante = Noir ;
c l a s s Noeud {
public :
char x , y ;
i n t pn , d n ;
1 i s t <Noeud * > 1 i s t e F i 1 s ;
void i n i t ( ) {
l i s t e F i l s . c l e ar ();
}
c o n s t i n t MaxNoeud = 1 0 0 0 0 0 0 0 ;
i n t n b N o e u d s = MaxNoeud ;
Noeud r a c i n e ;
Noeud p i l e N o e u d [ MaxNoeud ] ;
i f ( l i s t e F i l s . s i z e ( ) == 0 ) {
fo r ( i n t i = 1 ; i <= T a i l l e ; i ++)
136 Rec herc he en meilleur d'abord pour les jeux à deux joueurs
fo r ( i n t j = 1 ; j <= T a i 1 1 e ; j + + )
i f ( c o n n e c t . g o b a n [ i ] [ j ] == V i d e ) {
connect . joue ( i , j , couleur ) ;
nbNoeuds - - ;
i f ( nbNoeuds < 0 ) {
c o u t << " p l u s ._. d e ._. m e m o i re " << e n d ! ;
exit (0) ;
}
Noeud * n = &p i l e N o e u d [ n b N o e u d s ] ;
n -> i n i t ( ) ;
n ->x = i ;
n ->y = j ;
i f ( c o n n e c t . gagne ( i , j , c o u l e u r ) ) {
i f ( c o u l e u r == c o u l e u r P r o u v a n t e ) {
n ->p n = O ;
n ->dn = I n f i n i ;
}
else {
n ->p n = I n f i n i ;
n ->d n = O ;
}
}
else {
n ->p n = 1 ;
n ->d n = 1 ;
}
l i s t e F i l s . p u s h_b a c k ( n ) ;
c o n n e c t . d ej o u e ( i , j , c o u l e u r ) ;
}
}
else {
Noeud * m e i l l e u r F i l s = NULL ;
fo r ( l i s t <Noeud * > : : i t e r a t o r i t e r = l i s t e F i l s . b e g i n ();
i t e r ! = 1 i s t e F i 1 s . e n d ( ) ; ++ i t e r ) {
i f ( c o u 1 e u r == c o u 1 e u r P r o u v a n t e ) {
i f ( ( * i t e r ) - > p n == p n ) {
meilleurFils = * Î ter ;
break ;
}
}
else
i f ( ( * i t e r ) - > d n == d n ) {
meilleurFils = * Î ter ;
break ;
}
}
c o n n e c t . j o u e ( m e i l l e u r F i l s ->x , m e i l l e u r F i l s ->y ,
6. 7 Corrigés des exercices 137
couleur ) ;
m e i l l e u r F i l s -> d e s c e n t e ( c o n n e c t , autre ) ;
c o n n e c t . d ej o u e ( m e i l l e u r F i l s ->x , m e i l l e u r F i l s ->y ,
couleur ) ;
}
i f ( c o u 1 e u r == c o u 1 e u r P r o u v a n t e ) {
pn = I n fi n i ;
dn = O ;
fo r ( l i s t <Noeud * > : : i t e r a t o r i t e r = l i s t e F i l s . b e g i n ( ) ;
i t e r ! = l i s t e F i l s . e n d ( ) ; ++ i t e r ) {
i f ( ( * i t e r )->pn < pn )
p n = ( * i t e r ) - >p n ;
dn += ( * i t e r ) - > d n ;
}
}
else {
pn = O ;
dn = I n f i n i ;
fo r ( l i s t <Noeud * > : : i t e r a t o r i t e r = l i s t e F i l s . b e g i n ( ) ;
i t e r ! = 1 i s t e F i 1 s . e n d ( ) ; ++ i t e r ) {
i f ( ( * i t e r ) - >d n < d n )
dn = ( * i t e r ) - >d n ;
pn += ( * i t e r ) - > p n ;
}
}
}
int solve () {
rac ine . i n i t ( ) ;
n b N o e u d s = MaxNoeud ;
rac i n e . d e s c e nte ( connect , Noir ) ;
i n t nb = l ;
w h i t e ( ( r a c i n e . p n ! = 0 ) && ( r a c i n e . p n ! = I n f i n i ) ) {
c o u t << r a c i n e . p n << " � " ;
rac i n e . descente ( connect , Noir ) ;
nb + + ;
}
c o u t << " r e s u l t a t � : �p n �=� " < < r a c i n e . p n < < " � e n � " <<
n b << " � d e s c e n t e s " << e n d l ;
r e t u r n r a c i n e . pn ;
}
i n t main ( ) {
connect . initHash ( ) ;
c o u t << s o l v e ( ) << e n d l ;
}
138 Recherche en meilleur d'abord pour les jeux à deux joueurs
c l a s s N o e u d C arre {
public :
char x , y ;
i n t pn , dn ;
l i s t < N o e u d C arre * > l i s t e F i l s ;
unsigned long long hash ;
void i n i t ( ) {
l i s t e F i l s . c l e ar ( ) ;
}
void d e s c e n t e ( Connect & connect , int couleur ) ;
};
c o n s t i n t M a x N o e u d C arre = 1 0 0 0 0 0 0 0 ;
i n t n b N o e u d s C a r r e = M a x N o e u d C arre ;
N o e u d C arre r a c i n e C a r r e , p i l e N o e u d C a r r e [ M a x N o e u d C arre ] ;
v o i d N o e u d C arre : : d e s c e n t e ( C o n n e c t & c o n n e c t ,
int couleur ) {
i n t a u tre = Noir ;
i f ( c o u 1 e u r == N o i r )
a u t r e = B lanc ;
i f ( 1 i s t e F i 1 s . s i z e ( ) == 0 ) {
fo r ( i n t i = 1 ; i <= T a i 11 e ; i + + )
fo r ( i n t j = 1 ; j < = T a i 1 1 e ; j + + )
if ( c o n n e c t . goban [ i ] ( j ] == V ide ) {
connect . joue ( i , j , couleur ) ;
n b N o e u d s C arre - - ;
i f ( n b N o e u d s C a rre < 0 ) {
c o u t << " p l u s ..., d e ..., m e m o ire " << e n d l ;
exit (O) ;
}
N o e u d C arre * n = &p i l e N o e u d C a r r e [ n b N o e u d s C a r r e ] ;
n -> i n i t ( ) ;
n ->x = i ;
n ->y = j ;
n ->h a s h = c o n n e c t . h a s h ;
i f ( co n n e c t . gagne ( i , j , c o u l e u r ) ) {
i f ( c o u l e u r == c o u l e u r P r o u v a n t e ) {
n ->p n = 0 ;
n ->dn = I n f i n i ;
}
else {
6. 7 Corrigés des exercices 139
n ->p n = I n f i n i ;
n ->dn = O ;
}
}
else {
racine . in i t ( ) ;
n b N o e u d s = MaxNoeud ;
racine . descente ( connect , couleur ) ;
i n t nb = 1 ;
w h i l e ( ( r a c i n e . p n ! = 0 ) &&
( r a c i n e . p n ! = I n f i n i ) && ( n b < 1 0 0 ) ) {
rac i n e . descente ( connect , couleur ) ;
nb + + ;
}
n ->p n = r a c i n e . p n ;
n ->d n = r a c i n e . d n ;
}
l i s t e F i l s . p u s h_back ( n ) ;
c o n n e c t . d ej o u e ( i , j , c o u l e u r ) ;
}
}
else {
N o e u d C arre * m e i l l e u r F i l s = NULL ;
fo r ( l i s t < N o e u d C arre * > : : i t e r a t o r iter =
l i s t e F i l s . begin ();
i t e r ! = l i s t e F i l s . e n d ( ) ; ++ i t e r ) {
i f ( c o u l e u r == c o u l e u r P r o u v a n t e ) {
i f ( ( * i t e r ) - > p n == p n ) {
meilleurFi l s = * i t e r ;
break ;
}
}
else
i f ( ( * i t e r ) ->dn = = dn ) {
meilleurFils = * iter ;
break ;
}
}
c o n n e c t . j o u e ( m e i l l e u r F i l s ->x , m e i l l e u r F i l s ->y ,
couleur ) ;
m e i l l e u r F i l s -> d e s c e n t e ( c o n n e c t , a u t r e ) ;
c o n n e c t . d ej o u e ( m e i l l e u r F i l s ->x , m e i l l e u r F i l s ->y ,
couleur ) ;
}
i f ( c o u l e u r == c o u l e u r P r o u v a n t e ) {
pn = I n fi n i ;
dn = O ;
140 Recherche en meilleur d'abord pour les jeux à deux joueurs
-2 2
-1 1
OO
0 1 1
2 1
-1 1
-1 1 -1 1
OO -1 0
1 1 0 1
22 1 1
2 1
-2 1 -2 1
-1 1 -1 1
0 1 1 OO
1 0 L______J 1 1
2 1 2 1
La figure 6.5 donne pour chaque noeud les nombres conspirants correspondant à
chaque valeur possible. On voit que pour la racine, le nombre de conspirateurs pour la
valeur -2 est de 2.
Chapitre 7
"Les ouvertures vous apprennent les ouvertures, les finales vous apprennent les Échecs. "
Anonyme.
L' objectif d ' une base de données de finales est de calculer le résultat exact d ' un en
semble de positions ayant des caractéristiques communes. Par exemple on construit l 'en
semble des positions qui contiennent six pièces ou moins aux Échecs, et à chaque position
on associe son résultat exact. Le résultat exact correspond au résultat si les deux joueurs
jouent parfaitement à partir de la position.
Pour pouvoir stocker efficacement les résultats des positions, il est nécessaire de
concevoir une bijection entre les positions de la base et les nombres allant de 0 au nombre
de positions de la base. À l ' indice de la position dans la base on stockera son résultat.
Un algorithme d ' analyse rétrograde simple, qui calcule les résultats de chaque posi
tion, consiste à commencer par évaluer toutes les positions terminales (le roi est échec
et mat), puis à parcourir toutes les positions pour chercher les positions gagnantes et
perdantes, et à continuer à parcourir toutes les positions tant qu ' on trouve de nouveaux
résultats. Cet algorithme est donné dans l ' algorithme 4. Lorsqu ' on a trouvé toutes les po
sitions gagnantes et perdantes pour les deux joueurs, les positions restantes sont étiquetées
comme nulles.
Les algorithmes qui engendrent les positions antérieures des positions dont on connaît
le résultat sont plus efficaces que cet algorithme simple.
142 Bases de données de finales
7. 1 Les Échecs
7. 1 . 1 Principe de l 'algorithme
En sus d ' être l ' auteur du système d ' exploitation UNIX, du langage C, et d ' avoir ob
tenu le prestigieux prix Turing, K. Thompson est aussi connu pour avoir créé des bases de
données de finales aux Échecs (87, 88] qui ont permis de découvrir de nouvelles connais
sances échiquéennes et de jouer les fins de parties mieux que les grands maîtres. Son
programme est bien sûr écrit en langage C.
L' algorithme consiste à effectuer un Minimax à l ' envers en partant des positions ga
gnantes. Pour chaque position terminale, on trouve les positions qui y mènent par un gé
nérateur de positions antérieures ; on enregistre les nouvelles positions comme étant des
positions gagnantes en un coup pour Blanc. On trouve alors les positions qui y mènent
par un coup Noir, et on vérifie que tous les coups Noirs de ces positions sont perdants ;
on a alors des positions gagnées pour Blanc à un coup. On réitère le processus jusqu ' à ce
qu ' on ne trouve plus de nouvelles positions.
L' algorithme convertit une position en un nombre unique, ce nombre est utilisé pour
indicer un tableau de positions d ' Échecs. On cherche à trouver un codage qui ait un
nombre maximum le plus petit possible afin de minimiser la taille de la base de données.
Exercice : Si on ne considère que les finales sans pions, les positions ont le mêmes
propriétés par symétrie horizontale, verticale ou diagonale. Combien de façons différentes
y a-t-il de placer le roi noir et le roi blanc ? Pour une finale à 6 pièces, quelle est la taille
7.1 Les Échecs 143
Nombre de positions
10 20 30 40 50 60
La base de données de K. Thompson contient pour chaque position d' une finale le
nombre de coups minimum avant le gain. Elle a permis d' analyser les fin de parties de
grands maîtres à la lumière de ces résultats exacts. On obtient ainsi des résultats surpre
nants, notamment pour les finales difficiles comme la finale Roi-Fou-Fou-Roi-Cavalier
[65, 66] .
Les grands maîtres ne jouent pas toujours les coups qui gagnent le plus rapidement
même quand ils sont proches du but. De plus dans cette finale particulière et en utilisant ce
défaut des grands maîtres, un programme peut repousser sans cesse le mat en choisissant à
chaque fois la perte la plus longue ce qui repousse le mat lorsque le joueur humain ne joue
pas le coup qui amène le plus rapidement au gain. La figure 7 . 1 donne la répartition du
nombre de positions gagnées en fonction du nombre de coups minimum nécessaires pour
gagner. On voit deux cloches dans cette courbe, ce qui signifie que de nombreuses posi
tions dans la cloche de droite sont gagnées mais éloignées du gain par un grand nombre
de coups. On peut remarquer que pour passer de la deuxième cloche à la première cloche
on doit passer par un goulot d' étranglement qui sont des positions clés. Comme il y a peu
144 Bases de données de finales
de positions gagnantes dans ce goulot d'étranglement il n'y a souvent qu'un seul coup
gagnant ou un seul coup qui réduit la distance au gain. Ce sont des position très difficiles
à jouer. C'est la raison pour laquelle les positions dans la cloche de droite ont longtemps
été réputées comme étant des parties nulles par de grands analystes. Dans ces position, le
mat est très difficile à trouver. Dans d' autres finales, les analyses aboutissaient au résultat
correct mais pour de mauvaises raisons, réfutées par la base de données.
Les Dames anglaises sont l 'équivalent anglais des Dames françaises. Elles sont jouées
sur un damier 8x8, et les pions promus qui sont des Rois ont moins de libertés de dépla
cement que les dames dans les dames françaises.
Des machines parallèles et des réseaux de stations ont été utilisés pour engendrer ces
bases de données.
A peu près la moitié des positions dans une base de données sont des positions de
capture ; elles sont donc résolues lors de la première passe. Par contre, de nombreuses po
sitions sans capture nécessitent des dizaines de passes avant d'être résolues. La technique
itérative qui consiste à reparcourir toutes les positions résolues à chaque fois devient alors
coûteuse puisque de nombreuses positions déjà résolues sont alors reparcourues à chaque
passe. Pour éviter ces parcours inutiles, Chinook utilise une liste des positions trouvées
lors de la dernière itération, et déjoue les coups à partir de ces positions.
Pour les Échecs et les Dames anglaises, les bases de données consistent en des ta
bleaux associant à chaque indice le résultat de la position représentée par cet indice. Pour
les Dames anglaises, un résultat est représenté par deux bits ; il peut avoir une des trois
valeurs : GAGNEE, PERDUE ou NULLE. Pour les Échecs, en plus de cette information,
d ' autres bits sont utilisés pour donner le nombre de coups minimum au gain d ' une po
sition gagnée, le nombre de coups maximum avant la perte d ' une position perdue. Cette
information permet de jouer optimalement les finales de la base de données, en jouant
tous les coups possibles à partir de la position courante, et en choisissant celui qui amène
à la position qui maximise ou minimise la distance à la fin de la partie, suivant qu ' on est
en train de perdre ou de gagner.
Les bases de données de finales aux Dames anglaises prennent beaucoup de place mé
moire. Pour les stocker efficacement, elles sont compressées et décompressées dynami
quement lorsqu ' on en a besoin. L' algorithme de compression est le run-length encoding :
une suite de mêmes caractères est remplacée par le caractère suivi du nombre de carac
tères de la suite. Les taux de compression en utilisant cet algorithme varient entre 1 8 et
52 sur ces bases de données.
7.3 L'Awele
L' Awele est un jeu africain. Un plateau d' Awele comporte deux rangées de 6 trous
contenant des graines. Chaque joueur contrôle la rangée qui est de son coté du plateau.
Un trou supplémentaire est utilisé par chaque joueur pour garder ses prisonniers. Au début
du jeu, chaque trou contient quatre graines, il y a donc 48 graines sur le plateau.
A chaque coup un joueur choisit un trou de sa rangée contenant des graines. En com
mençant avec le voisin de ce trou, il sème alors toutes les graines du trou, une par une,
dans le sens contraire des aiguilles d ' une montre. Si le trou contient 12 graines ou plus,
le trou d ' origine est passé et le semage continue ensuite normalement. Après un coup,
le trou auquel on a enlevé les graines est donc toujours vide. Après la fin du semage, les
prisonniers sont retirés du jeu. Des graines sont prisonnières si la dernière graine semée se
retrouve dans un trou ennemi, qui après le semage contient 2 ou 3 graines. Si une capture
est effectuée et que le trou précédent contient aussi 2 ou 3 graines, ces graines sont aussi
capturées. Cette procédure est répétée pour les autre trous qui précèdent, et elle s ' arrête
lorsqu ' un trou contient un nombre de graines différent de 2 ou 3, ou lorsque le bout de la
rangée adverse est atteint.
Le but du jeu est de capturer plus de graines que l' adversaire. Le jeu se termine dès
qu ' un adversaire a gagné 25 graines ou plus. Le jeu se termine aussi lorsqu ' un joueur ne
peut plus jouer ; toutes les graines restantes sur le plateau sont alors capturées par son
adversaire. Si une même position est rencontrée pour la troisième de fois, avec le même
joueur ayant la main, les graines restantes sont divisées également entre les deux joueurs.
Si les deux joueurs ont 24 graines, le jeu est déclaré nul.
L' Awele a été complètement résolu grâce à l ' analyse rétrograde en 2002 [70] . Toutes
146 Bases de données de finales
74. WoodPush
Woodpush est un jeu qui a été inventé récemment pour analyser les jeux avec ko (un
ko consiste à interdire la répétition de la position précédente, on les retrouve souvent au
jeu de Go par exemple). Le jeu est volontairement simple de façon à faciliter son analyse.
Il se joue avec des pièces de couleurs différentes sur une ligne de cases. La position initiale
pour 7 cases est :
Le joueur gauche (dont les pièces sont représentées par un G) déplace ses pièces vers
la droite, et saute par dessus les pièces qui sont devant lui. S ' il y a une pièce du joueur
droit à gauche d'une de ses pièces, il a alors le droit de reculer en poussant les pièces vers
la gauche. Lorsqu 'une pièce dépasse le bord elle est perdue.
L' analyse rétrograde a permis de résoudre Fanorona [74] . Elle peut être utilisée pour
d' autres jeux qui ont une structure similaire. Ainsi, le Breakthrough 5x5 et le Surakarta
partagent avec Fanorona la propriété qu 'une base de finales comporte n pions noirs et b
pions blancs dont on veut évaluer toutes les configurations possibles. Supposons qu'on
ait C cases numérotées de 0 à C 1 sur lesquelles on peut avoir soit un pion blanc soit
-
un pion noir. Pour stocker la valeur de la position dans un tableau, il faut construire une
fonction qui calcule l' indice de chaque position dans le tableau, si possible en évitant
d'avoir des indices inoccupés.
Exercice : Trouver une fonction qui associe un indice à chaque position sans laisser
d'indices inoccupés dans le tableau.
7 .6 Corrigés des exercices 147
Une meilleure solution consiste à forcer le roi noir dans un huitième de l 'échiquier,
il n ' a donc que I O positions possibles. Pour les quatre positions sur la diagonale on peut
forcer le roi blanc dans une moitié de l 'échiquier.
Le nombre de positions du roi blanc pour chaque position du roi noir est donc :
/ \
1 1
1 1
1 1
1 1
1 30 1
1 55 30 1
1 55 55 30 1
1 58 58 58 33 1
\ /
Remarque : on ne peut faire les rotations et les symétries que pour les finales sans
pions.
Pour les finales à 6 pièces chacune des 4 pièces restantes peut être placée sur 64 cases,
on a donc 64 4 ( 1 6M) combinaisons de pièces, donc pour une seule configuration de 6
pièces on a 7.75 * 10 9 positions ce qui occuppe 8 Go. Si chaque position est stockée
comme un bit, pour un seul tableau, on a besoin de 970 Mo de mémoire.
On peut noter au passage que K. Thompson n ' a pas écrit lui même les générateurs de
positions antérieures mais qu 'il a écrit un programme qui les écrivaient pour chaque finale
différente.
Les finales à 6 pièces sont 64 fois plus grandes que les finales à 5 pièces. De plus il
y a 5 fois plus de finales à 6 pièces que de finales à 5 pièces et les finales à 6 pièces sont
à peu près deux fois plus longues que les finales à 5 pièces. Donc les finales à 6 pièces
prennent 1 000 fois plus de temps à construire que les finales à 5 pièces. Des algorithmes
de gestion mémoire comme LRU (Least Recently Used = Décharger la zone mémoire la
moins récemment utilisée) peuvent être utiles pour optimiser l' utilisation de la mémoire
en analyse rétrograde.
148 Bases de données de finales
Chaque position est associée à un octet qui donne le nombre de coups nécessaires pour
gagner ; cette donnée est nécessaire pour jouer les coups qui amènent le plus rapidement
au gain dans les finales.
762
. . Indice d'une position
Pour calculer l ' indice d ' une position nous allons reprendre la méthode utilisée par M.
Schadd pour Fanorona [74) . Cette fonction est composée de deux sous fonctions : une
fonction pour coder la place des pièces et une fonction pour coder la couleur des pièces
présentes dans l 'ordre d ' apparition des pièces.
Cette fonction code les positions sans laisser d ' espaces inoccupés dans le tableau ;
mais elle est aussi inversible : à partir d ' un indice on peut retrouver la position correspon
dante.
Soit Pi la position d ' un pion sur le plateau de jeu (compris entre 0 et C - 1). On peut
coder la configuration des pions à l ' aide de :
f ""'"' n +b ( P; )
ui=l
=
1 i
On veut aussi coder le nombre de façons qu ' il y a d ' alterner des pions noirs et des
pions blancs. Si bi est l ' indice du ième pion blanc dans la suite de pions, on peut utiliser :
Comme il y a ( n� b) façons de placer les pièces sur les C cases on peut utiliser comme
indice :
Chapitre 8
La recherche de plus court chemin sur une carte est une étape importante pour de
nombreuses applications comme les jeux vidéo ou la planification de mouvements de
robots. Nous nous intéressons ici à la recherche de plus court chemin sur une grille. Elle
est souvent utilisée pour les jeux de stratégie temps réel par exemple, pour trouver le
chemin d'un agent jusqu ' à son but sur la carte.
8. 1 L'algorithme de Dijkstra
Le principe de l ' algorithme de Dijkstra est de développer la position qui a le plus petit
chemin déjà parcouru. Pour cela, on associe à chaque position la longueur du chemin déjà
parcouru que nous appellerons g. Pour la position de départ g O. Puisqu ' un déplacement
=
coûte un, les fils de la position de départ ont tous un g = 1 . L' algorithme de Dijkstra
commence donc par développer la position de départ qui est marquée comme visitée, et à
insérer dans une structure de donnée tous les fils de cette position comme décrit dans la
figure 8.2.
Une fois ces nouvelles positions développées, le principe de l ' algorithme est de dé-
150 Recherche de plus court chemin sur une carte
g= I
FIGURE 8.2 - O n insère dans les positions à développer tous les fi l s d e l a position de
départ.
velopper la position non encore développée qui a un g minimal. Dans notre cas, toutes
les positions aux feuilles (c 'est à dire non encore développées) ont un g = 1 . Il choisit la
première feuille et la développe comme indiqué en figure 8 . 3 .
O n peut noter que l ' algorithme n e développe pas l e déplacement vers l a droite car il
amène à une position déjà visitée. Nous nous retrouvons maintenant avec un arbre qui
comporte des feuilles avec g = 2 et des feuilles avec g = 1. Le principe de l ' algorithme
étant de développer les feuilles de g minimal, il développe alors une feuille avec g = 1 ,
comme indiqué en figure 8.4.
Lorsqu ' on continue l ' algorithme, il ne reste qu ' une seule feuille avec g = 1 , c'est
donc celle là qui est développée en figure 8.5.
L' algorithme continue ainsi jusqu ' à visiter la position d' arrivée et à s ' assurer que
toutes les feuilles de l ' arbre ont un g supérieur ou égal au g de la position d' arrivée.
L' algorithme de Dijkstra pour les cartes est donné dans l ' algorithme 6.
8.1 L'algorithme de Dijkstra 151
FIGURE 8.4 - On continue à développer les feuilles de g minimal en évitant les positions
déjà visitées.
Exercice : Écrire un programme qui calcule le plus court chemin entre deux points
sur une carte à l ' aide de l ' algorithme de Dijkstra. On s ' attachera à utiliser des structures
de données efficaces qui permettront de trouver le plus court chemin avec une complexité
linéaire en fonction de la taille de la carte.
152 Recherche de plus court chemin sur une carte
8.2 L'algorithme A*
L' algorithme A * est une amélioration d e l ' algorithme d e Dijkstra. E n plus d e mémo
riser pour chaque position le coût du chemin jusqu ' à cette position (la variable g) on va
évaluer avec une heuristique la longueur du chemin qu ' il reste à parcourir. On aura ainsi
une évaluation de la longueur totale du chemin. Au lieu de développer la feuille qui a le
plus petit g comme dans l ' algorithme de Dijkstra, on développera la feuille qui a le plus
petit chemin estimé. Si on veut garantir que A * trouve le plus court chemin, il est né
cessaire que l ' heuristique qui évalue la longueur du chemin restant soit admissible, c ' est
à dire qu ' elle soit optimiste et qu ' elle donne dans tous les cas une longueur de chemin
inférieure ou égale à la longueur réelle du chemin restant.
L' heuristique admissible la plus utilisée avec A * est l ' heuristique de Manhattan. Elle
consiste à considérer que le chemin vers le but est sans obstacles, de façon à calculer
rapidement un minorant du chemin restant à faire. En effet pour des déplacements hori
zontaux et verticaux, l ' heuristique de Manhattan revient à additionner la valeur absolue
de la différence des abscisses entre la position courante et la position d ' arrivée et la valeur
absolue de la différence des ordonnées entre les mêmes points.
Algorithm 6 Recherche du plus court chemin sur une carte avec l ' algorithme de Dijkstra
dijkstra (depart, arrivee)
for tous les points de la carte do
g [point] f- - 1
end for
positions à développer f- 0
point f- depart
g [point] f- 0
while point =/:- arrivee do
for tous les voisins de point do
if le voisin est accessible then
if g [voisin] = - 1 then
g [voisin] f- g [point] + 1
ajouter voisin dans les positions à développer
end if
end if
end for
retirer le point qui a le plus petit g des positions à développer
end while
retourner g [arrivee]
g=O
h=2
f=2
g=I g= I
g= I
h=3 h=3
h=3
f=4 f=4
f=4
FIGURE 8.6 - On insère dans les positions à développer tous les fils de la position de
départ.
Si on prend le même exemple que pour l ' algorithme de Dijkstra, on commence par
développer la position initiale comme décrit dans la figure 8.6. On calcule pour chaque
nouvelle feuille les valeurs pour g , h et f.
i-------1
g=O
1-------1 h=2
t--+--+--<
f=2
g=I g= I
g= I
h=3 h=3
h=3
[=4 [=4
D
[=4
g=2 g=2
h=2 h=4
[=4 f=6
une position déjà visitée avec un plus petit g et le déplacement vers la droite amène à un
obstacle. Encore une fois la nouvelle feuille a un f = 4 qui est minimal.
g=O
h=2
1---t--+--l f=2
g=I g= I
g= I
h=3 h=3
h=3
[=4 [=4
D
[=4
g=2 g=2
h=2 h=4
[=4 f=6
g=3
1-------1 h=I
>---+---+--+----< [=4
FIGURE 8 . 8 - On développe la feuille ayant le plus petit f en évitant les positions déjà
visitées.
g=O
1----� h=2
1--f----<f--< f=2
g= I g= I
g= I
h=3 h=3
h=3
D f=4 f:4
f=4
g=2 g=2
h=2 h=4
f=4 f=6
g=3
1--_____, h=I
1--f----<f--< f=4
FIGURE 8 . 9 - On arrive au but et toutes les autres feuilles ont des f plus grands ou égaux.
L' algorithme 7 décrit la recherche de plus court chemin sur une carte avec A * .
Exercice : Écrire un programme qui trouve un plus court chemin sur une carte en
utilisant l ' algorithme A * . On utilisera l ' heuristique de Manhattan pour estimer de façon
optimiste la longueur du chemin restant à parcourir. On utilisera des structures de données
efficaces de façon à obtenir une complexité linéaire en fonction de la taille de la carte.
ALT est une bonne heuristique pour les cartes routières [40] . Elle consiste à pré cal
culer les distances d ' un point donné à tous les autres points. Ces distances pré calculées
156 Recherche de plus court chemin sur une carte
Algorithm 7 Recherche du plus court chemin sur une carte avec l ' algorithme A *
A * (depart, arrivee)
for tous les points de la carte do
g [point]+-- 1
end for
positions à développer+-0
point+-depart
point.g+-0
g [point] +-0
while point =f. arrivee do
if point.g ::; g [point] then
for tous les voisins de point do
if voisin est accessible then
f+-point.g + 1 + manhattan (voisin, arrivee)
if g [voisin] = -1 ou g [voisin]> point.g + 1 then
g [voisin] +-point.g + 1
voisin.g+-point.g + 1
ajouter voisin dans les positions à développer de rang f
end if
end if
end for
end if
retirer le point qui a le plus petit f des positions à développer
end while
retourner g [arrivee]
8.4 Recherche de plus court chemin moiti-agents 157
peuvent alors être utilisées pour calculer une heuristique admissible. L' heuristique utilise
l ' inégalité triangulaire. Si par exemple JI X , Y l l est la longueur du plus court chemin entre
X et Y, et si les distances sont pré calculées depuis P , que le noeud courant est en D , et
que le but à atteindre est en A , on a les inégalités suivantes :
JI P, A l i S l l P, D l l + l l D , A l i (8. 1 )
Si les distances sont pré-calculées depuis plusieurs points, l ' heuristique choisit pour
h le maximum sur tous les points de l ' heuristique fournie par l ' heuristique triangulaire et
de l ' heuristique de Manhattan.
Pour accélérer le calcul de l' heuristique avec P points, au lieu de prendre l ' heuristique
maximum sur les P points, on peut sélectionner à la racine le point qui donne le h maxi
mum et ne plus utiliser que celui ci pour calculer l' heuristique par la suite. Pour chaque
noeud de la recherche, on prend alors le maximum de l ' heuristique de Manhattan et de
l ' heuristique triangulaire pour ce point.
L' heuristique triangulaire avec seulement le meilleur point donne des valeurs plus
petites et donc moins bonnes que l ' heuristique triangulaire avec plusieurs points. La re
cherche développera plus de noeuds. Cependant le temps de calcul de l ' heuristique est
plus faible, ce qui finalement lui permet souvent d' avoir de meilleurs temps de réponse.
Exercice : Implémenter la recherche de plus court chemin avec l ' heuristique triangu
laire pour A * .
La plupart des recherches sur ce problème concernent des algorithmes inexacts car le
problème est considéré comme trop difficile pour être résolu exactement. Les algorithmes
inexacts consistent en général à combiner des chemins individuels comme par exemple
dans la recherche de plus court chemin coopérative [82) .
158 Recherche de plus court chemin sur une carte
ai a2 a3
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0
0 0 0 0 0 0 0
a4 as a6
TABLE 8 . 1 - Échange de places dans un couloir pour 6 agents
L' algorithme exact standard est A * . La facteur de branchement de l ' algorithme est
5 nbAgen t s si l' agent a cinq actions possibles (haut, bas, droite, gauche et attendre). La
taille de l 'espace d'états est de l ' ordre de tailleCarte n bAgen t s où tailleCarte est le
nombre de positions sur la carte.
Lorsque le nombre d' agents est élevé, il peut être très intéressant de décomposer les
déplacements des agents en déplacements individuels et d'évaluer après chaque déplace
ment individuel si la recherche ne dépasse pas le seuil autorisé en combinaison avec un
algorithme d' approfondissement itératif [26] .
Une autre approche qui permet de trouver des chemins optimaux à moindre coût dans
de nombreux cas est décrite dans [90, 9 1 ] .
L' algorithme le plus simple pour résoudre efficacement la recherche de plus court
chemin multi-agents est de calculer le plus court chemin de chaque agent indépendam
ment des autres agents. Si les chemins trouvés ne créent pas de collisions, on a alors
un résultat optimal. Toutefois, il arrive souvent sur des cartes difficiles que les chemins
se croisent, surtout lorsque la carte comporte des passages étroits et qu 'il y a beaucoup
d ' agents. Une solution est d' utiliser quand même les chemins individuels et de chercher à
nouveau lorsque le chemin est impraticable à cause des autres agents. Le problème avec
cet algorithme est qu ' il ne résout pas les interblocages et les répétitions d'états.
Un problème difficile de planification multi-agents est donné figure 8 . 1 ; le but est que
ai , a2 et a3 échangent de places avec a4, as et a6. L' agent a4 doit aller à la place de ai , as
en a2 et a6 en a3. L' algorithme optimal est capable de résoudre le problème de la figure
8. 1 alors que la replanification n'en est pas capable.
Concernant le problème de la figure 8.2 l ' agent ai doit aller en 9i et l ' agent a2 doit
aller en g2. Or ai est sur le chemin de a2 et réciproquement. Chaque agent va alors
8.4 Recherche de plus court chemin moiti-agents 159
91
0 0 0 0 0 0
0 0 0 0 0 0
0 0 az 0 0 0 0
0 0 a1 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
92
replanifier son chemin en prenant en compte la position de l ' autre agent. Le nouveau
plus court chemin de a 1 commencera par se déplacer vers la droite, de même pour az .
Après ce premier déplacement, les deux agents seront de nouveau en interblocage. Ils se ·
déplaceront alors vers la gauche et se retrouveront dans la même position qu ' auparavant.
On a alors un cycle d ' interblocages.
Pour éviter les cycles on peut utiliser un niveau d' agitation de l ' agent. À chaque fois
qu ' il doit replanifier, son niveau d ' agitation augmente ce qui revient à ajouter du bruit à
son heuristique. Les agents agiront ainsi de plus en plus aléatoirement au fur et à mesure
des interblocages ce qui peut permettre de les débloquer. Le problème avec cette stratégie
est qu ' elle peut rester bloquée longtemps lorsqu ' il y a beaucoup d ' agents et que chaque
agent exécute un A * à chaque déplacement [85] ce qui conduit à des comportements peu
intelligents et lents.
Il arrive toutefois que le chemin réservé par un agent empèche les agents suivants de
trouver un chemin. La recherche coopérative ne peut pas résoudre le problème de la figure
8 . 1 par exemple. Elle permet toutefois de résoudre le problème de la figure 8 .2.
Les problèmes rencontrés par la recherche coopérative sont par exemple quand un
agent atteint son but dans un couloir et empèche les autres agents de passer, ou quand
l ' ordre des agents fixé une fois pour toutes empèche de trouver une solution. De plus cette
recherche est coûteuse en temps. L' algorithme peut être amélioré en variant l ' ordre des
agents et en intercalant la planification avec les actions. La recherche avec une fenêtre [82]
est une solution à ces problèmes. Elle consiste à faire la recherche à une profondeur fixée
160 Recherche de plus court chemin sur une carte
puis à éxécuter pendant quelques pas de temps le plan partiel trouvé par cette recherche,
puis à replanifier de nouveau à profondeur fixée.
RTA * consiste à évaluer heuristiquement tous les voisins et à se déplacer vers le voisin
qui a la meilleure évaluation.
Une amélioration de RTA * est Learning Real Time A * qui met à jour l ' évaluation
heuristique de chaque état lorsque les déplacements amènent à se rendre compte que
l ' heuristique admissible était trop optimiste. Ainsi si un état a une évaluation de 3 et que
le meilleur déplacement coute l et se déplace vers un état qui a une évaluation de 3, on
peut augmenter ! ' évaluation du premier état de 3 à 4. LRTA * est sûr de trouver le but dans
un espace d ' états fini.
Algorithm 8 Recherche d ' un chemin sur une carte avec l ' algorithme LRTA *
nouvellePosition (prevPos, pos)
if prevPos -:/:- pos then
best +--- heuristique [pos] + 1
for tous les voisins de prevPos do
if heuristique [voisin] + 1 < best then
best +--- heuristique [voisin] + 1
end if
end for
heuristique [prevPos] +--- best
end if
retourner le voisin qui a la plus petite valeur pour heuristique[ voisin]
LRTA * (pas)
for tous les points de la carte do
heuristique [point] +--- h(point)
end for
prev +--- pos
white pos -:/:- arrivee do
p +--- pos
pos +--- nouvellePosition (prev, pos)
prev +--- p
end white
Une preuve de complétude de LRTA * [45] utilise la fonction h* (x) qui donne pour
l ' état x le coût du plus court chemin vers le but. On a toujours h( x) < h * ( x) puisque h est
8.5 Corrigés des exercices 161
admissible. De plus chaque mise à jour du tableau heuristique conserve l ' admissibilité ; on
a donc toujours heuristique [x] < h* (x) . Si on définit l ' erreur globale comme la somme
pour tous les états de h* (x) - heuristique [x] et la disparité comme la somme de l ' erreur
globale et de heuristique [x] pour l ' état courant de LRTA * . A chaque coup, cette valeur
ne peut que décroitre. Or lorsqu ' elle atteint 0 le problème est résolu. Le problème sera
donc résolu par LRTA * .
La recherche avec cible mouvante est une généralisation de LRTA * au cas où la cible
bouge. La recherche doit alors avoir un heuristique pour chaque position possible de la
cible. On utilise donc une table heuristique(x, y ) qui donne l ' heuristique pour atteindre
la cible y à partir de la position x. heuristique (x, y ) est initialisée avec l ' heuristique
admissible initiale h.
Algorithm 9 Recherche d' un chemin sur une carte avec cible mouvante
nouvellePosition (cible, pas)
for tous les voisins de pas do
if heuristique (voisin, cible) + 1 < best then
best +--- heuristique (voisin, cible) + 1
meilleur Voisin +--- voisin
end if
end for
if heuristique (pas, cible) + 1 < best then
affecteHeuristique (pas, cible, best)
end if
retourner meilleur Voisin
8.5.1 Dijkstra
# i n c l u d e < s t d i o . h>
# i n c l u d e < s t r i n g . h>
# i n c l u d e < s t d l i b . h>
162 Recherche de plus court chemin sur une carte
# i n c l u d e < t i m e . h>
# i n c l u d e < m ath . h >
#include < l i s t >
u s i n g namespace s t d ;
c o n s t i n t MaxEdge = 1 0 0 0 ;
class Point {
public :
int X , y ;
void s e t ( in t x l , int yl ) {
X = Xl ;
y = yl ;
}
v o i d random ( ) {
x = ( r a n d ( ) / (RAND_MAX + 1 . 0 ) ) * w i d t h ;
y = ( rand ( ) (RAND_MAX + 1 . 0 ) ) * h e i g h t ;
}
v o i d p r i n t ( FILE * fp ) {
f p r i n t f ( fp , " (%d , % d ) \ n " , x , y ) ;
}
b o o l o p e r a t o r == ( P o i n t p ) {
r e t u r n ( ( x == p . x ) && ( y -- p . y ) ) ;
}
bool operator ! = ( Po i n t p ) {
r e t u r n ( ( x ! = p . x ) Il ( y ! = p . y ) ) ;
}
};
int X , y ;
fo r ( i n t i = O ; i < n b P o i n t s ; i ++) {
x = ( rand ( ) / (RAND_MAX + 1 . 0 ) ) * w i d t h ;
y = ( rand ( ) I (RAND_MAX + 1 . 0 ) ) * h e i g h t ;
w h i l e ( map [ x ] [ y ] == 1 ) {
8.5 Corrigés des exercices 163
x = ( rand ( ) (RAND_MAX + 1 . 0 ) ) * w i d t h ;
y = ( rand ( ) (RAND_MAX + 1 . 0 ) ) * h e i g h t ;
}
map [ x ] [y] = 1 ;
}
}
i n t g [ MaxEdge ] [ MaxEdge ] ;
i n t nodes = O ;
fo r ( i n t i = O ; i < h e i g h t * w i d t h ; i ++)
stackAt [ i ] . clear ( ) ;
nodes = 1 ;
int c u rrentg = O ;
Point c u rre n t = s t a r t ;
g [ start . x] [ start . y] = O;
while ( c u r r e n t ! = goal ) {
nodes + + ;
Point p [ 4 ] ;
p [ 0 ] . s e t ( c u rre n t . x + l , c u rre n t . y ) ;
p [ l ] . s e t ( c urre n t . x - 1 , c urre n t . y ) ;
p [ 2 ] . set ( current . x , c u rrent . y + 1 ) ;
p [ 3 ] . s e t ( c urre n t . x , c u rr e n t . y - l ) ;
fo r ( i n t i = O ; i < 4 ; i + + )
i f ( ( p [ i ] . x >= 0 ) && ( p [ i ] . x < w i d t h ) &&
( p [ i ] . y >= 0 ) && ( p [ i ] . y < h e i g h t ) )
i f ( map [ p [ i ] . x ] [ p [ i ] . y ] -- 0 ) {
i f ( g ( p ( i ) . X ) ( p ( i ) . y ) == - 1 ) {
g [ p [ i ] . x ] [p [ i ] . y ] = c u rrentg + 1 ;
s t a c k A t [ c u r r e n t g + l ] . p u s h_back ( p [ i ) ) ;
}
else if ( g [ p [ i ] . x ] [p [ i ] . y ] > currentg + 1 ) {
g [ p [ i ] . x ] [p [ i ] . y ] = c u rrentg + 1 ;
s t a c k A t [ c u r r e n t g + 1 ] . p u s h_back ( p [ i ] ) ;
fp r i n t f ( s tderr , " + " ) ;
}
}
164 Recherche de plus court chemin sur une carte
8.5.2 A*
Ne pas ré-explorer les points déjà atteints par un chemin plus court ou de longueur
égale au chemin courant est important dans la recherche de plus court chemin sur une
carte.
Une méthode simple pour ne pas ré-étudier ces points est de parcourir la liste des
noeuds de A * pour vérifier si le point a déjà été atteint avec un chemin plus court. Toute
fois cette méthode devient vite inefficace lorsque le nombre de noeuds de A * grandit.
On peut tirer partie de la taille réduite en mémoire des cartes de jeux pour utiliser un
tableau de la taille de la carte qui mémorise le plus court chemin étudié pour chaque point.
On appelle ce tableau g puisqu ' il contient pour chaque point le plus petit g qui a conduit
à ce point. A chaque passage par le point, la valeur de g de la feuille est comparée à ce
plus court chemin. Si le g de la feuille est strictement plus petit, la valeur est remplacée
et la recherche continue. Si g est plus grand ou égal au plus court chemin, la recherche
est arrétée. Notez qu 'on peut encore accélerer le programme en utilisant une initialisation
paresseuse pour le tableau g.
i n t manhattan ( Po i n t p , P o i n t goal ) {
return abs ( p . x - goal . x ) + abs ( p . y - goal . y ) ;
}
c l a s s Node {
public :
Point p ;
int g ;
} ;
nodes = l ;
int currentf = O ;
Node c u r r e n t , tmp ;
c u rr e n t . p = s t a r t ;
c u rrent . g = O ;
g [ start . x] [ start . y] = O;
while ( c u rr e n t . p ! = goal ) {
i f ( c u r r e n t . g <= g [ c u r r e n t . p . x ] [ c u r r e n t . p . y ] ) {
1 1 if ( t r u e ) {
nodes ++;
Point p [4] ;
p [ O ] . s e t ( c u rre n t . p . x + 1 , c u rrent . p . y ) ;
p [ l ] . s e t ( c u rre n t . p . x - 1 , c u rr e n t . p . y ) ;
p [ 2 ] . set ( current . p . x , c urrent . p . y + l ) ;
p [ 3 ] . s e t ( c u r re n t . p . x , c u rre n t . p . y - l ) ;
fo r ( i n t i = O ; i < 4 ; i + + )
i f ( ( p [ i ] . x >= 0 ) && ( p [ i ] . x < w i d t h ) &&
( p [ i ] . y >= 0 ) && ( p [ i ] . y < h e i g h t ) )
i f ( map [ p [ i ] . x ] [ p [ i ] . y ] -- 0) {
int f = c urrent . g + l +
manhattan ( p [ i ] , goal ) ;
i f ( f < c u rre n t f )
fp r i n t f ( s t d e r r , " not� c o n s i s te n t " ) ;
i f ( g ( p ( i ) . X ) ( p ( i ) . y ) == - l ) {
g [p [ i ] . x ] [p [ i ] . y ] = current . g + l ;
tmp . p = p [ i ] ;
tmp . g = c u r r e n t . g + l ;
s t a c k A t f [ f ] . p u s h _ b a c k ( tmp ) ;
}
else i f ( g [ p [ i ] . x] [ p [ i ] . y] >
c u rrent . g + 1 ) {
g [ p [ i ] . x ] [ p [ i ] . y ] = c u rrent . g + l ;
tmp . p = p [ i ] ;
tmp . g = c u r r e n t . g + l ;
s t a c k A t f [ f ] . p u s h _ b a c k ( tmp ) ;
}
}
}
166 Recherche de plus court chemin sur une carte
w h i l e ( s t a c k A t f [ c u r r e n t f ] . s i z e ( ) == 0 ) {
c u r r e n t f ++;
i f ( c u r r e n t f >= h e i g h t * w i d t h )
return c u rr e n t f ;
}
c u rr e n t = s t a c k A tf [ c u rr e n t f ] . back ( ) ;
s t a c k A t f [ c u r r e n t f ] . pop_back ( ) ;
}
return c u rr e n t f ;
}
Lorsque les déplacements diagonaux sont autorisés, en supposant que les déplace
ments horizontaux et verticaux coûtent 2 et que les déplacements diagonaux coûtent 3 ,
l ' heuristique d e Manhattan devient :
i n t manhattan ( Po i n t p , P o i n t g o a l ) {
i n t dx = a b s ( p . x - g o a l . x ) ;
i n t dy = a b s ( p . y - g o a l . y ) ;
r e t u r n 3 * m i n ( dx , dy ) +
2 * ( max ( dx , dy ) - min ( dx , d y ) ) ;
}
l l D , A l l 2 l ll D , P l l - l l A , P l l l (8.3)
D ' où une heuristique admissible qui utilise les distances pré calculées :
h = l ll D , P l l - l l A , P ll l (8.4)
void s e t Tr i a n g u l a r P o i n t ( i n t n , P o i n t p ) {
poin tTriangular [ n ] = p ;
Point noPoint ;
noPo i n t . s e t ( - 1 , - 1 ) ;
d ij k s t r a ( p o i n tTri a n g u l a r [ n ] , noPoint ) ;
fo r ( i n t i l = O ; i l < h e i g h t ; i l + + )
fo r ( i n t j l = O ; j l < w i d t h ; j l + + )
distFrom [ n ] [ i l ] [ j l ] = g [ i l ] [ j l ] ;
}
Chapitre 9
9. 1 Introduction
Des problèmes très connus de recherche de solution la plus courte que nous abordons
sont par exemple le Rubik ' s Cube, le Taquin ou le voyageur de commerce.
Pour résoudre ces problèmes on peut utiliser des algorithmes de force brute qui n ' uti
lisent pas de connaissances du domaine. Pour les problèmes comme le Rubik ' s cube, le
Taquin et Sokoban il est très utile d ' utiliser des heuristiques spécifiques au domaine pour
accélérer la recherche.
Nous commencerons par décrire les algorithmes de recherche qui n ' utilisent pas de
connaissances du domaine. Puis nous continuerons par les algorithmes de recherche heu
ristique comme A * et IDA * qui utilisent des connaissances du domaine.
Une recherche exhaustive de tous les trajets possibles considérera donc (N 1 ) ! trajets
-
possibles.
Un cycle hamiltonien d' un graphe connecté est un chemin qui visite tous les noeuds
du graphe, sans passer deux fois par le même et qui se termine sur le noeud par lequel il
170 Recherche de la solution la plus courte pour les puzzles
a commencé.
Le problème du voyageur de commerce est donc de trouver le plus petit cycle hamil
tonien débutant sur une ville donnée, dans le graphe des villes.
Exercice : On dispose d ' une matrice NxN appelée distance qui donne les distances
entre les villes. La distance de la ville i à la ville j est donnée par distance [i] [j]. Le
voyageur de commerce se trouve au début dans la ville numéro O. Écrire un programme
qui trouve le plus court trajet et le stocke dans un tableau meilleurTrajet de taille N + 1 .
L' espace du problème est l ' ensemble des états que peut prendre le problème et l' en
semble des coups qui permettent de passer d ' un état à un autre. Un problème est un espace
de problème associé à un état initial et à un état final. L'état initial est l ' état dans lequel
on commence la recherche, l 'état final est celui que l' on cherche à atteindre.
On peut représenter l ' espace d ' un problème par un graphe. Les états du problème sont
les noeuds du graphe, et les coups sont les arêtes du graphe. Le but de la résolution de
problème est de trouver un chemin, si possible optimal, de l ' état initial à l ' état final.
Certains des algorithmes de ce chapitre développent des arbres de recherche dont la ra
cine est l ' état initial, et non des graphes. Cette simplification amène à recalculer plusieurs
fois les noeuds du graphe que l ' on peut atteindre par plusieurs chemins différents. Pour se
retrouver dans le cas de graphes, et éviter ces recalculs, on peut ajouter aux algorithmes
que nous verrons des tables de transposition.
9.4 Le Taquin
Un problème typique de recherche de plus court chemin dans un graphe d ' états est le
Taquin. Dans sa version 3x3, le but est de trouver une séquence minimale de coups qui
permet d ' atteindre la position de la figure 9 . 1 .
9.5 Les heuristiques admissibles 171
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15
Un coup consiste à remplacer la case vide, par une de ses cases adjacentes qui devient
alors à son tour vide. Trouver une solution optimale aux Taquins NxN est un problème
NP-difficile.
Exercice : Définir une classe Position qui représente une position au Taquin 4x4 à
l ' aide d ' un tableau unidimensionnel. On définira un coup dans une position par un entier
qui correspond à la case que l ' on veut bouger. Écrire les fonctions membres de cette classe
pour initialiser la position, jouer un coup, et une fonction qui pour mélanger la position
joue au hasard un nombre de coups donné en paramètre, à partir de la position finale.
Les heuristiques que nous utiliserons pour le calcul de plus court chemin dans les jeux
évaluent la plupart du temps le nombre de coups qui reste à jouer.
Une heuristique est admissible si elle ne surestime jamais la longueur du plus court
chemin qui reste à parcourir avant d ' atteindre la position finale. Une heuristique admis
sible donne toujours une valeur plus petite ou égale à la valeur du plus court chemin.
Pour chaque noeud on peut calculer une fonction f qui estime une borne minimale sur la
longueur totale du chemin qui passe par ce noeud pour aller à la position finale.
Pour chaque position p on calcule une évaluation : f (p) = g (p) + h(p) . La fonction
g (p) est la longueur du chemin parcouru pour atteindre la position courante à partir de la
position initiale ; h(p) est une heuristique admissible qui estime la longueur minimale du
chemin qui reste à parcourir avant d ' arriver à la position finale depuis la position courante.
f (p) est donc une borne inférieure sur la longueur finale minimale du chemin qui passe
par la position courante.
Une heuristique admissible simple et efficace pour le Taquin est la distance de Man
hattan. Pour la calculer on fait la somme, pour chaque case non vide, du nombre de coups
qu ' il faudrait pour déplacer la case vers sa case finale si toutes les autres cases étaient
vides.
172 Recherche de la solution la plus courte pour les puzzles
2 6 9 4
14 10 15
5 1 11 8
7 3 13 12
9.6 L'algorithme A*
L' algorithme A * [4 1 ] accélère la recherche de plus court chemin à l ' aide d ' heuris
tiques admissibles. Le choix d ' une bonne heuristique admissible est le paramètre qui
influence le plus l ' efficacité de A * . D ' une manière générale, on cherche à définir des
heuristiques admissibles qui renvoient les plus grandes valeurs possibles tout en restant
admissibles (c ' est à dire en renvoyant toujours une valeur plus petite que la valeur du plus
court chemin entre la position courante et la position finale).
Exercice : Écrire une classe pour représenter un noeud de l ' arbre de recherche.
Exercice : On veut disposer d ' une fonction qui insère un noeud à la bonne place
dans l ' ensemble des ouverts trié par rapport à f. Programmer la fonction void i n s ere
( Noeud * noeud ) de façon à ce que l ' insertion et la recherche d ' un noeud de plus
petit f se fasse le plus rapidement possible.
9.7 L'algorithme IDA* 173
On veut aussi disposer d' une fonction qui développe un noeud. Elle crée tous ses fils,
les insère dans l 'ensemble des ouverts, et met le noeud développé dans l 'ensemble des
fermés.
L' algorithme A * commence par initialiser l ' ensemble des ouverts avec la position de
départ. L'ensemble des fermés est vide. L' algorithme s ' arrête lorsqu ' on a atteint la po
sition finale ou lorsque la fonction bool deve l oppe ( Noeud * noeud ) renvoie
f al se.
Exercice : Écrire l ' algorithme A * pour le Taquin 4x4 à l ' aide des fonctions précé
dentes.
Le seuil est initialisé avec l 'évaluation heuristique h de l ' état initial. A chaque itéra
tion, le seuil est incrémenté. L' algorithme se termine lorsque un état final est atteint.
Le Rubik ' s cube peut être résolu pratiquement instantanément par un programme qui
utilise des macro coups. Ce sont des suites de coups qui échangent deux cubes sans mo
difier la place des autres. Nous nous intéressons à la résolution optimale du Rubik ' s cube
qui est plus difficile. La résolution optimale consiste à trouver une solution contenant le
174 Recherche de la solution la plus courte pour les puzzles
Le Rubik's Cube se prête bien à une résolution par IDA * [56] . Le coût des recherches
exhaustives à une profondeur inférieure à celle du seuil n'est que de 8% du coût de la
recherche à la profondeur du seuil. L' utilisation de IDA * dans ce cas est donc justifiée.
Elle est même nécessaire étant donné le très grand nombre de noeuds à développer pour
les profondeurs utiles.
Exercice : Trouver une heuristique admissible simple basée sur la distance de Man
hattan pour le Rubik 's cube. Est il possible de l ' améliorer simplement ?
const int N = 1 0 ;
i n t d i s t a n c e [N] [N ] ;
b o o l v i s i t e e [N ] ;
i n t m e i l l e u r T r aj e t [N + 1 ] ;
int meilleurCout ;
v o i d p l u s C o u r t T r aj e t ( i n t d e p t h , i n t c o u t C h e m i n ,
i n t t r a j e t [N + 1 ] ) {
i f ( d e p t h == N ) {
t r aj e t [N] = O ;
c o u t C h e m i n += d i s t a n c e [ t r a j e t [ N - 1 ] ] [ O ] ;
9.9 Corrigés des exercices 175
i f ( c o u tChemin < m e i l l e u r C o u t ) {
m e i l l e u r C o u t = coutChemin ;
fo r ( i n t i = O ; i <= N ; i + + )
m e i l l e u r T r aj e t [ i ] = t r a j e t [ i ] ;
}
}
else
fo r ( i n t i = 1 ; i < N ; i + + )
if ( ! v i s i t e e [ i ] ) {
t r aj e t [ depth ] = i ;
v i s i t e e [ i ] = true ;
p l u s C o u r t T r aj e t ( d e p t h + 1 , c o u t C h e m i n +
d i s t a n c e [ t r aj e t [ depth - 1)) [i] ,
t r aj e t ) ;
v i s i t e e [ i ] = fa l s e ;
}
}
class Position {
char _pos [ 1 6 ) ;
char _vide ;
public :
init () {
fo r ( i n t i = O ; i < 1 5 ; i + + )
_pos [ i ] = i + 1 ;
_pos [ 1 5 ] = 0 ;
vide = 1 5 ;
}
void joue ( i n t coup ) {
_pos [ _v i d e ] = _pos [ coup ] ;
_pos [ coup ] = O ;
vide = coup ;
176 Recherche de la solution la plus courte pour les puzzles
}
void melange ( i n t nbCoups ) {
init ();
fo r ( i n t i = O ; i < n b C o u p s ; i + + ) {
i n t coupChoisi = 1 + rand ( ) %
c o u p s P o s s i b l e s [ _vide ] [ 0 ] ;
joue ( c o u p s P o s s i b l e s [ _vide ] [ coupChoisi ] ) ;
}
}
char v i de ( ) { return _vide ; }
};
Il y a un coup à jouer pour amener la case 2 vers sa position finale, un coup pour la
case 6, 4 coups pour la case 9, etc.
Réponse : h = 1 + 1 + 4 + 0 + 3 + 1 + 0 + 3 + 1 + 3 + 0 + 1 + 4 + 4 + 2 + 1 = 29.
On modifie la classe Position comme suit pour avoir une heuristique admissible :
# d e fi n e a b s o l u e ( x ) ( ( X ) >0 ? ( X ) : - ( X ) )
int X [ 1 6] = {O , 1 2, 3 0, 1 2, 3
' ' ' '
0, 1 2, 3 0, 1 2 3};
' ' ' '
int y [ 1 6] = {O ' 0 , 0, 0, 1 1 1 1
' ' ' '
2 2, 2, 2, 3 3 3 3};
' ' ' '
class Position {
char _pos [ 1 6 ] ;
char _vide ;
c h a r _h ;
public :
Position () {
fo r ( i n t i = O ; i < 1 5 ; i + + )
_pos [ i ] = 1 + 1 ;
_pos [ 1 5 ] = 0 ;
vide = 1 5 ;
h = O;
}
void j o u e ( i n t coup ) {
h = a b s o l u e ( x [ c o u p ] - x [ _p o s [ coup ] - l]) +
a b s o l u e ( y [ c o u p ] - y [ _p o s [ coup ] - l]);
h += a b s o l u e ( x [ _ v i d e ] - x [ _p o s [ coup ] 1]) +
a b s o l u e ( y [ _ v i d e ] - y [ _p o s [ coup ] - 1]);
_pos [ _v i d e ] = _pos [ coup ] ;
_pos [ coup ] = O ;
_vide = coup ;
}
bool fi n a l e ( ) { return h O; }
--
9.9 Corrigés des exercices 177
};
public :
Noeud * p a r e n t , * S u i v a n t ;
Noeud ( ) {
s u i v a n t = NULL ;
}
void i n i t ( const P o s i t i o n & p , i n t g ) {
_p = p ;
_g = g ;
h = p.h ();
}
i n t f ( ) { r e t u r n _g + _h ; }
b o o l f i n a l ( ) { r e t u r n _p . f i n a l e ( ) ; }
void j o u e ( i n t coup ) {
_p . j o u e ( c o u p ) ;
_g + + ;
h = _p . h ( ) ;
}
};
On utilise un tableau de piles pour représenter l ' ensemble des ouverts. Un indice dans
le tableau correspond à une valeur de f. L' insertion se fait en temps constant, et la re
cherche dans le tableau du noeud qui a le plus petit f est aussi très rapide.
1 1 v a l e u r maximum de f
c o n s t i n t M axLength = 1 0 0 0 ;
v o i d i n s e r e ( Noeud * n o e u d ) {
Noeud * tmp = & o u v e r t s [ n o e u d -> f ( ) ] ;
On suppose que f est toujours croissante, ce qui est vrai pour de nombreuses fonc
tions h, notamment dans le cas du Taquin. Les fonctions meilleur et developpe s ' écrivent
178 Recherche de la solution la plus courte pour les puzzles
comme suit :
i n t fc o u r a n t = O ;
Noeud fe r m e s ;
Noeud * m e i l l e u r ( ) {
w h i l e ( o u v e r t s [ f c o u r a n t ] . s u i v a n t -- NULL &&
f c o u r a n t < M ax L e n g t h )
fc o u r a n t ++ ;
return o u v e r t s [ fc o u r a n t ] . s u i v a n t ;
}
b o o l d e v e l o p p e ( Noeud * n o e u d ) {
I I On o t e l e n o e u d d é v e l opp é de l ' e n s e m b l e d e s O u v e r t s
o u v e r t s [ n o e u d ->f ( ) ] . s u i v a n t = n o e u d -> s u i v a n t ;
I l o n i n s e r e l e s f i l s da n s l e s o u v e r t s
fo r ( i n t i = 1 ; i < = c o u p s P o s s i b l e s [ n o e u d -> v i d e ( ) ] [0] ;
i ++) {
Noeud * tmp = new Noeud ( n o e u d ) ;
i f ( tmp == NULL) {
f p r i n t f ( s t d e r r , " Abandon , � m é m o i re� s a t u r é e \ n " ) ;
return fa l s e ;
}
tmp -> p a r e n t = n o e u d ;
tmp ->j o u e ( c o u p s P o s s i b l e s [ n o e u d ->v i d e ( ) ] [ i ] ) ;
i n s e r e ( tmp ) ;
}
1 1 On l ' aj o u t e à l ' e n s e m b l e d e s fe r m e s
n o e u d -> s u i v a n t = fe r m e s . s u i v a n t ;
fe r m e s . s u i v a n t = n o e u d ;
return true ;
}
r a c i n e -> i n i t ( p , 0);
i n s e re ( racine ) ;
Noeud * n o e u d = m e i l l e u r ( ) ;
w h i l e ( tr u e ) {
i f ( ! developpe ( noeud ) )
return - 1 ;
9.9 Corrigés des exercices 179
noeud = m e i l l e u r ( ) ;
i f ( n o e u d == NULL)
return - 1 ;
i f ( n o e u d -> f i n a l ( ) )
break ;
}
r e t u r n n o e u d ->f ( ) ;
}
P o s i t i o n pos ;
int seuil ;
bool IDAEtoileRecursif ( i n t g ) ;
int IDAEtoile ( ) {
bool t r o u v e = fa I s e ;
fo r ( s e u i l = p o s . h ( ) ; s e u i l < M a x L e n g t h && ! t r o u v e ;
s e u i l ++)
tro u v e = IDAEtoileRecursif ( 0 ) ;
return s e u i l ;
}
bool I D A E t o i l e Re c u r s i f ( i n t g ) {
i f ( g + pos . h ( ) > s e u i l )
r e t u r n fa l s e ;
i f ( pos . fi n a l e ( ) )
return true ;
char v i d e = pos . v i d e ( ) ;
fo r ( i n t i = 1 ; i <= c o u p s P o s s i b l e s [ v i d e ] [0] ; i ++) {
pos . joue ( c o u p s Po s s ib l e s [ v ide ] [ i ] ) ;
if ( IDAEtoileRecursif ( g + 1 ))
return true ;
pos . j oue ( vide ) ;
}
return fa l s e ;
}
Le Rubik ' s cube contient 20 cubes, 8 cubes de coin et 12 cubes de bord. Une heuris
tique admissible pour la fonction h utilise la somme des distances de Manhattan entre les
180 Recherche de la solution la plus courte pour les puzzles
cubes et leurs états finaux [56] . On doit donc calculer pour chaque cube le nombre mini
mal de coups nécéssaires pour le mettre à sa place dans la bonne orientation. On fait alors
la somme de tous ces nombres de coups pour tous les cubes. Toutefois, chaque rotation
fait bouger 4 cubes de coin et 4 cubes de bord, donc pour que la valeur soit admissible,
on doit la diviser par 8 .
h1 _ L: cE Cu bc M anhattan(c)
-
8
Bases de patterns
L' analyse rétrograde (cf chapitre 7) peut être utilisée pour calculer des bases de
patterns qui permettent d' accélérer la résolution de problèmes. Un pattern est un sous
ensemble d'une position.
1 0. 1 Le Taquin
La fonction h a une grande influence sur 1 ' efficacité de A * : plus les valeurs renvoyées
par h sont élevées (tout en restant admissibles), plus A * trouvera la solution rapidement.
La fonction utilisée traditionnellement pour calculer une heuristique admissible pour le
Taquin est la distance de Manhattan. On peut améliorer h en utilisant les bases de données
de patterns.
Pour construire une base de patterns pour le Taquin, on choisit un sous-ensemble des
pièces, et on calcule pour chaque arrangement possible de ce sous ensemble le nombre
minimal de déplacements qu 'il est nécessaire de faire pour remettre toutes les pièces du
sous-ensemble à leurs places.
Les patterns engendrés donnent un chemin minimal jusqu ' à la position finale qui est
plus grand que la distance de Manhattan, mais plus petit que le nombre réel. Pour évaluer
le chemin qui reste à parcourir jusqu ' à la position finale depuis une position donnée, le
programme reconnaît tous les sous-buts qu ' il a calculé sur la position, et l 'heuristique
admissible consiste à prendre le maximum de toutes les valeurs calculées pour ces sous
buts. L' utilisation de bases de données de patterns permet de résoudre les problèmes de
Taquin 4x4 avec mille fois moins de noeuds [30] .
Prenons pour exemple le Taquin 4x4. On décide de calculer toutes les configurations
des 7 premières pièces.
182 Bases de patterns
Exercice : Écrire un algorithme qui permet d' engendrer cette base de patterns. Écrire
tout d' abord les variables nécessaires. On veut aussi une fonction qui code une position,
c 'est à dire qui fasse une bijection entre entiers et positions. Écrire ensuite un algorithme
qui engendre toutes les configurations possibles et qui teste tous les coups possibles de
chaque configuration pour trouver si la configuration est à une distance donnée. Écrire
ensuite la fonction principale d ' analyse rétrograde.
Dans son article de 1 985 [53) , R. Korf donne une méthode qui permet à un programme
d ' apprendre à résoudre presque instantanément des problèmes de Rubik ' s Cube. Pour cela
son programme apprend des macro-coups qui échangent des cubes à certaines positions
sans changer la configuration des autres cubes. Le programme résout instantanément après
apprentissage tous les problèmes de Rubik's Cube en utilisant en moyenne 85 coups.
Un problème beaucoup plus difficile est de trouver des solutions optimales au Rubik ' s
Cube. Une solution optimale est la plus petite séquence de coups permettant de résoudre
un cube. La recherche de solutions optimales au Rubik ' s Cube est un problème résoluble
par IDA * . On peut pour cela utiliser des bases de données de patterns afin de mieux
évaluer la fonction h [56) .
Par exemple, On peut créer des patterns qui ne contiennent que les 8 cubes de coins,
la position et l ' orientation du dernier cube est déterminée par la position et l ' orientation
des cubes précédents.
On peut énumérer ces patterns dans une base de données et leur associer le nombre de
coups nécessaires pour atteindre la position finale. Le nombre de coups varie de 0 à 1 1 ,
on utilise donc 4 bits pour stocker un nombre, la table utilise alors 42 Mo de mémoire.
La valeur moyenne de h pour cette table est de 8.764 comparée a 5.5 pour la distance de
Manhattan.
Pour utiliser ces deux heuristiques à la fois, la seule façon de faire est de prendre le
maximum des deux pour h. La combinaison d ' IDA * et des bases de données de patterns
permet de résoudre optimalement pratiquement tous les problèmes de Rubik ' s cube. Sans
les bases de données de patterns la résolution est beaucoup plus lente.
10.3 Les bases de patterns additives 183
On peut faire mieux que prendre le maximum des distances précalculées. Si deux
bases de patterns ne contiennent que des pièces différentes, on peut additionner les dis
tances (33].
Lorsqu ' une base de patterns a une taille trop grande pour être contenue en mémoire
vive, on peut la compresser ( 34] . Pour cela, on choisit un sous ensemble des pièces de la
base la plus grande, et on calcule pour chaque configuration du sous ensemble la distance
minimale sur toutes les configurations du sur ensemble contenant la configuration du sous
ensemble. Par exemple, si on a calculé toutes les distances des 9 premières pièces du
Taquin, on calcule pour chaque configuration de 7 pièces la distance minimum sur toutes
les configurations à 9 pièces qui contiennent la configuration à 7 pièces. Cette distance
compressée est plus grande que la distance de la base de 7 pièces et donne donc une
meilleur heuristique admissible.
10.5 Sokoban
Sokoban est un jeu a un joueur, amusant mais difficile. Le principe est qu 'un robot
cherche à ranger des caisses dans un labyrinthe en les poussant vers des emplacements
cible. Il n ' est possible de pousser qu ' une seule caisse à la fois. Si deux caisses sont voi
sines au bord cela forme un interblocage, aucune des deux caisses ne peut plus être dépla
cée et le problème devient insoluble. Il existe une multitude d ' autres interblocages. Une
façon de se prémunir contre les interblocages d' un labyrinthe et de les énumérer avec de
l ' analyse rétrograde et de construire une base de patterns des interblocages spécifiques à
un labyrinthe (27]. Détecter ainsi les interblocages permet à IDA* d 'être plus efficace en
arrêtant la recherche dès qu ' une position est prouvée insoluble.
Avec l ' analyse rétrograde, on peut aussi construire des bases de patterns pour les jeux
à deux joueurs. Ainsi pour accélérer la résolution de problèmes de vie et de mort au jeu de
Go, on peut engendrer des patterns relativement petits répertoriant les positions vivantes
plusieurs coups à l ' avance ( 1 4, 1 9, 23] . Ces patterns permettent d ' accélérer notablement
la résolution de problèmes de vie et de mort.
184 Bases de patterns
10.7.1 Le Taquin
La taille de la base de patterns engendrée est de 167 = 228 = 268435456, soit 256
Mo.
On peut diminuer la taille à 19�1 mais l ' indice d' une position est alors plus difficile
à calculer. On peut toutefois précalculer les indices de début pour chaque position de la
première pièce.
Pour coder les positions et initialiser la base on écrit les fonctions suivantes :
int Pieces = 7 ;
int casePiece [ Size ] ;
unsigned char d i s t a n c e [ Taille ] ;
void i n i t ( ) {
fo r ( i n t i = O ; i < T a i l l e ; i + + )
d i s t a n c e [ i ] = 255 ; Il d i s ta n c e inconnue
fo r ( i n t i = O ; i < P i e c e s ; i + + )
casePiece [ i + 1 ] = i ;
d i s t a n c e [ code ( ) ] = O ;
}
unsigned char d i s t a n c e C o u r a n t e = O ;
bool t e s t e C o n fi g u r a t i o n ( ) {
bool t ro u v e = fa l s e ;
i f ( di s ta n c e [ code ( ) ] ! = 255)
return fa l s e ;
10. 7 Corrigés des exercices 185
fo r ( i n t i = O ; i < S i z e ; i + + )
if ( contenuCase [ i ] != 0 ) {
i f ( ( i > 3 ) && ( c o n t e n u C a s e [ i - 4 ) == 0 ) ) {
c asePiece [ contenuCase [ i ] ] = i - 4 ;
i f ( d i s t a n c e [ c o d e ( ) ] == d i s t a n c e C o u r a n t e )
tro u v e = true ;
casePiece [ contenuCase [ i ] ] = i ;
}
i f ( ( i < 1 2 ) && ( c o n t e n u C a s e [ i + 4 ] == 0 ) ) {
c a s e P i e c e [ c o n te n u C a s e [ i ] ] = i + 4 ;
i f ( d i s t a n c e [ c o d e ( ) ] == d i s t a n c e C o u r a n t e )
tro u v e = true ;
casePiece [ contenuCase [ i ] ] = i ;
}
i f ( ( i % 4 ! = 0 ) && ( c o n t e n u C a s e [ i - 1 ] == 0 ) ) {
casePiece [ contenuCase [ i ] ] = i - 1;
i f ( d i s t a n c e [ c o d e ( ) ] == d i s t a n c e C o u r a n t e )
trouve = true ;
c asePiece [ contenuCase [ i ] ] = i ;
}
i f ( ( ( i + l) % 4 ! = 0 ) && ( c o n t e n u C a s e [ i + 1 ) -- 0 ) ) {
casePiece [ contenuCase [ i ] ] = i + l;
i f ( d i s t a n c e [ c o d e ( ) ] == d i s t a n c e C o u r a n t e )
tro u v e = true ;
casePiece [ contenuCase [ i ] ] = i ;
}
}
i f ( tro u v e )
d i s t a n c e [ code ( ) ] = d i s t a n c e C o u ra n t e + l ;
return tro u v e ;
}
bool engendre ( i n t p i e c e ) {
b o o l t r o u v e = fa l s e ;
i f ( p i e c e == P i e c e s )
fo r ( i n t i = O ; i < S i z e ; i + + )
c o n te n u C a s e [ i ] = O ;
i f ( p i e c e == 0 )
return t e s t e C o n fi g u r a t i o n ( ) ;
fo r ( i n t i = O ; i < S i z e ; i + + )
i f ( contenu Case [ i ] == 0 ) {
contenuCase [ i ] = piece ;
c asePiece [ piece ] = i ;
i f ( engendre ( p i e c e - l ) )
tro u v e = true ;
c o n te n u C a s e [ i ] = 0 ;
}
186 Bases de patterns
return tro u v e ;
}
Pour les cubes de bord, le nombre de combinaisons possibles pour 6 des 12 cubes est
1 2 ! / 6 ! . 26 = 42577920, d ' où une table de 20Mo.
11.1 Introduction
Au niveau zéro, la recherche Monte-Carlo joue des parties aléatoires (des playouts,
cf algorithme 1 0). Lorsqu'une partie aléatoire est terminée l'algorithme renvoie le score
obtenu.
Pour les niveau supérieurs à zéro, !'algorithme est !'algorithme 1 1 . A chaque coup de
niveau n, on joue le coup qui a donné le meilleur résultat au niveau n 1.-
3 2 1 1 0 0 0 0
FIGURE 1 1 .2 - Le score d'une partie est le nombre de coups sur le chemin le plus à gauche
11.3 Le problème du choix du coup à g auche 189
end if
if score du playout apres le coup move > score du meilleur playout then
meilleur playout +--- playout apres le coup move
end if
position +--- joue (position, coup du meilleur playout)
end while
return score (position)
Exercice: Quelle est la probabilité d'un playout de trouver la solution d'un problème
de profondeur n ? Quelle est cette probabilité pour une recherche de niveau un ? Quelle
est la complexité d'une recherche de niveau un ?
3 2 2 1 2 1 1 0
Un problème qui a une répartition plus proche des problèmes rééls est le problème du
nombre de coups à gauche. Le score d'une feuille est le nombre de coups à gauche joués
pour atteindre cette feuille. La figure11.3 donne un problème de profondeur trois.
Exercice: Quelle est la probabilité d'un playout de trouver la solution d'un problème
de profondeur n ? Quelle est cette probabilité pour une recherche de niveau l ?
190 Méthodes de Monte-Carlo pour les jeux à un joueur
1 1 .4 SameGame
SameGame est un puzzle NP-complet [48]. Une position est une grille de cases colo
rées. Un coup consiste à retirer des régions connexes de la même couleur pour marquer
des points. Le nombre de points marqués par un coup est (nombre de cases retires- 2) 2.
Les cases au dessus des cases retirées tombent après un coup, de plus si une colonne de
vient vide, les colonnes sur sa droite sont décalées vers la gauche pour la combler. Lorsque
toutes les cases ont été retirées à la fin de la partie, on gagne un bonus de 1000 points.
Le nombre de coups d'un playout est t0(h, l) = h. Le nombre de coups d ' un playout
de niveau l est t1(h, a) =a x Lo<i<h t1-1 (i, a). Une recherche de niveau un jouera
1
a x h2 /2 coups. Une recherche de niveau lest en O(a hl+1 ).
La probabilité qu ' un playout trouve la solution est la même que pour le problème
précédent : 2-n.
Pour un arbre de profondeur d, le nombre de feuilles qui ont un scores est (d). Pour
le sous arbre gauche ce nombre vaut (d= i). Pour le sous arbre droit ce nombre vaut (d- I ) .
La probabilité qu 'un playout trouve le score s après un coup à gauche est donc
P1eftscore(s, d, 0) = ��a��(.
La probabilité qu ' un playout trouve le scores après un coup
.
a' dro1te est done Prightscore(s, d, 0) = <;�_,)
2d-1 ·
La probabilité qu 'un score s trouvé à gauche permette d' aller à gauche est donc
D Priohtscore(s,d,O) + ..,s-lp
r/eftmove ( S, d, 0) =
2 Lli=O rightscore( i, d, 0) (Je premier terme es t
· ·
On peut remarquer que la distribution des scores sous un noeud de l ' arbre à hauteur
d est la même à une constante près. La probabilité de choisir un coup à gauche est donc
indépendante de la place du noeud dans l ' arbre. La probabilité Ptett(d, 0) est donc valable
pour tous les noeuds de l ' arbre de hauteur d.
On peut tenir un raisonnement similaire pour les niveaux plus élevés de recherche.
Soit Pteftscore(s, d, l) la probabilité qu ' une recherche de niveau l commençant par un
coup à gauche trouve le scores à hauteur d. Soit Prightscore(s, d, l) la probabilité pour
le coup droit. La probabilité qu ' un score trouvé à gauche permette de jouer le coup
(s,d,l) + ..,s-lp .
n
gauc he est a1 ors rteftmove (s, d, l) - Prightscore
_
babilité qu 'un coup gauche soit choisi est donc Pteft(d, l) = ��=O ( Ptettscore(s, d, l) x
Pteftmove(s, d, l)).
La probabilité qu ' une recherche de niveau l trouve le score s à hauteur d est alors
Pscore(s, d, l) = Pzett(d, l - 1) X Pscore(s - l, d - 1, l) + ( 1 - Pzett(d, l - 1)) X
Pscore(s, d - 1, l).
#include<s t d i o . h>
#inclu de <m ath . h>
#inc lu d e <s t d l i b . h>
flo a t P [ M a x S i z e] [ M a x S i z e] [ M a x L e v e l ] ;
void i n i t ( ) {
for (in t i = O; i < M a x S i z e ; i ++ )
for (in t j = O; j < M a x S i z e ; j ++ )
C o e ff [ i ] [ j ]= 0;
for (in t i = O; i < M a x S i z e ; i ++ )
C o e ff [ i ] (0) = 1;
for (in t i = 1; i < M a x S i ze ; i ++ )
for (in t j = 1; j < i + 1 ; j ++ )
C o e ff [ i ] [ j ]= C o e ff [ i 1] [ j-
1] + -
C o e ff [ i 1] [ j ] ;
-
P s c o re ( s - 1 , d - 1 , 1 ) +
( 1 - P l e ft ( d , 1 1)) * -
P s c o re ( s , d 1 , 1 ); -
}
re turn P [ s] [ d] [ ! ] ;
}
void i n i t (in t de ) {
d e p t h = de ;
d = O;
s c o r e = o· .
}
void p l a y M o v e (int m) {
if (m == 0 )
s c o r e ++ ;
v a r i a t i o n [ d] = m;
d++ ;
}
bool l e a f ( ) { re turn ( d >= d e p t h ) ; }
int p l a y o u t ( ) {
white ( d < d e p t h ) {
194 Mét hodes de Monte-Carlo pour les jeux à un joueur
in t s c o r e B e s t R o l l o u t [ M axLevel ];
int b e s t R o l l o u t [ M axLevel ] [ 1 0 1 );
int n e s te d M o v e s [ M axLevel ] [ 1 0 1 );
bool r e c a l l B e s t S e q u e n c e = f a l s e;
in t n e s t e d R o l l o u t ( Pr o b l e m & pb , int n ) {
int b e s t S c o r e , s c o r e R o l l o u t ;
int b e s t M o v e;
s c o r e B e s t R o l l o u t [ n ] = O;
white ( true) {
if ( p b . 1 e a f ( ) )
break;
b e s t S c o r e = - 1;
if ( r e c a l l B e s t S e q u e n c e ) {
b e s t S c o r e = s c o r e B e s t R o l l o u t [ n ];
b e s t M o v e = b e s t R o l l o u t [ n ] [ pb . d ];
}
for (int i = O; i < 2; i ++ ) {
if ( n == l ) {
P ro b l e m p = p b;
p . p l a y M o v e ( i );
p . p l a y o u t ( );
if ( p . s c o r e > b e s t S c o r e ) {
b e s t S c o r e = p . s c o re;
bestMove = i;
if ( r e c a l l B e s t S e q u e n c e ) {
s c o r e B e s t R o l l o u t [ n ] = b e s t S c or e;
for (in t j = O; j < p . d e p t h ; j ++ )
bestRollout [n] [ j ) = p . v ariation [ j ];
}
}
el s e if ( p . s c o r e == b e s t S c o r e ) {
in t move = (int) (2 * ( r a n d ( ) /
(RAND_MAX + 1 . 0 ) ) ) ;
if ( move == 1 ) {
b e s t S c ore = p . s c o re;
bestMove = i;
}
if ( r e c a l l B e s t S e q u e n c e ) {
11.5 Corrigés des exercices 195
s c o r e B e s t R o l l o u t [ n ] = b e s t S c o r e;
for (int j = O; j < p . d e p t h; j ++ )
bestRollout [ n ] [ j ] = p . v ariation [ j ];
}
}
}
el s e {
Pro b l e m p = p b;
p . p l ay M o v e ( i );
scoreRollout = nestedRollout (p , n - 1 );
if ( s c o r e R o l l o u t > b e s t S c o r e ) {
b e s t S c o re = s c o reR o l l o u t;
bestMove = i ;
if ( r e c a l l B e s t S e q u e n c e ) {
s c o r e B e s t R o l l o u t [ n ] = b e s t S c o r e;
for (in t j = O; j < p . d e p t h; j ++ )
b e s t R o l l o u t [ n ] [ j ] = p . v a r i a t i o n [ j ];
}
}
el s e if ( s c o r e R o l l o u t == b e s t S c o r e ) {
int move = (in t ) (2 * ( r a n d ( ) /
(RAND_MAX + 1 . 0 ) ) ) ;
if ( move == 1 ) {
b e s t S c ore = s c oreRo l l o u t;
bestMove = i ;
if ( r e c a l l B e s t S e q u e n c e ) {
s c o r e B e s t R o l l o u t [ n ] = b e s t S c o r e;
for (int j = O; j < p . d e p t h ; j ++ )
b e s t R o l l o u t [ n ] [ j ] = p . v a r i a t i o n [ j ];
}
}
}
}
}
pb . p l a y M o v e ( b e s t M o v e );
}
re turn pb . s c o r e ;
}
11.5.4 SameGame
u s ing namespace s t d ;
c ons t in t M a x S ize = 1 5;
cons t int M axProblem = 2 0;
c l a s s Move {
p u blic:
in t n b L o c a t i o n s ;
int l o c a t i o n s [ M a x S ize * M a x S i z e ];
Move ( ) {
n b L o c a t i o n s = O;
}
void sort () {
s t d : : s o r t ( l o c a t i o n s , l o c a t i o n s + n b L o c a t i o n s );
}
};
cla s s S een {
p u blic:
c h ar se en [ M a x S i z e * M a x S i z e ];
void init () {
memset ( s e e n , 0 , M a x S i z e * MaxS ize * s ize o f (ch ar));
}
bool t e s t (in t !o c ) {
r e t u r n ( s e e n [ 1 o c ] == 0 );
}
void s e t ( in t ! o c ) {
s e e n [!o c ] = l;
}
};
c la s s Pro b l e m {
p u blic:
int c o l o r [ M ax S i ze * M a x S i z e ];
int nbMoves;
int s c o r e ;
int l e n g t h V a r i a t i o n ;
Move v a r i a t i o n [ M a x S i z e * M ax S i ze ];
s t a c k [ O ]++;
stack [ stack [ O ] ] = n e i g h;
}
}
if ( ( l % MaxS ize ) ! = 0 ) {
neigh = l - 1;
if ( c o l o r [ n e i g h ] == c )
if ( s e e n . t e s t ( n e i g h ) ) {
s e e n . s e t ( n e i g h );
move. add ( n e i g h );
s t a c k [ O ]++;
s t a c k [ s t a c k [ O ] ] = n e i g h;
}
}
if ( ( l % MaxS ize ) ! = MaxS ize - 1) {
n e i g h = l + 1;
if ( c o l o r [ n e i g h ] == c )
if ( s e e n . t e s t ( n e i g h ) ) {
s e e n . s e t ( n e i g h );
move. add ( n e i g h );
s t a c k [ 0 ]++;
s t a c k [ s t a c k [ 0 ] ] = n e i g h;
}
}
}
}
void fi n d M o v e s ( int t a b u = 9) {
S een seen;
nbMoves = O;
s e e n . i n i t ( );
11.5 Corrigés des exercices 199
if ( ! moreThanOneMove ( t a b u ) )
t a b u = 9;
for (inti = O; i < M a x S i z e * M a x S i ze; i ++ )
if ( ( c o l o r [ i ] ! = 9)&& ( c o l o r [ i ] ! = t a b u ))
if ( s e e n . t e s t ( i ) ) {
b u i l d M o v e ( i , s e e n , moves [ nbMoves ] );
if ( moves [ nbMoves ] . n b L o c a t i o n s > 1 )
nbMoves++;
}
if ( nbMoves == 0) {
for (int i = O; < M a x S i z e * M a x S i ze; i ++ )
if ( c o l o r [ i ] ! = 9)
if ( s e e n . test ( i )) {
b u i l d M o v e ( i , s e e n , moves [ nbMoves ] );
if ( moves [ nbMoves ] . n b L o c a t i o n s > 1 )
nbMoves++;
}
}
}
int c o l u m n = O;
for (int i = O; i < M a x S i z e; i ++ ) {
if ( c o l o r [ M a x S ize * M a x S i z e - M a x S i z e + c o l u m n ]
== 9)
remo veColumn ( c o l u m n );
else
c o l u m n++;
}
s c o r e += ( move . n b L o c a t i o n s 2) *
( move . n b L o c a t i o n s 2 );
if ( c o l o r [ M a x S i z e * M a x S i z e - M a x S ize ] -- 9)
s c o r e += 1 0 0 0;
v a r i a t i o n [ 1 e n g t h V a r i a t i o n ] = move;
l e n g t h V a r i a t i o n ++;
}
int b e s t C o l o r ( ) {
int n b C o l o r s [ IO ];
for (inti = O; i < 1 0; i ++ )
n b C o l o r s [ i ] = O;
for (int i = O; i < M a x S i z e * M a x S i ze; i ++ )
if ( c o l o r [ i ] ! = 9)
n b C o l o r s [ c o l o r [ i ] ] ++;
int b e s t = 0 , b e s t S c o r e = O;
for (int i = O; i < 1 0; i ++ )
if ( n b C o l o r s [ i ] > b e s t S c o r e ) {
b e s t S c o r e = n b C o l o r s [ i ];
best = i;
}
ret urn b e s t ;
}
void p l a y o u t ( ) {
int t a b u = b e s t C o l o r ( );
fi n d M o v e s ( t a b u );
while ( nbMoves > 0 ) {
int i n d e x = nbMoves * ( r a n d ( ) / (RAND_MAX+ 1 . 0 ) );
p l a y M o v e ( moves [ i n d e x ] );
fi n d M o v e s ( t a b u );
}
}
};
Pro b l e m p r o b l e m [ M axProblem ];
11.5 Corrigés des exercices 201
pb . fi n d M o v e s ( t a b u );
for (int i = O; i < p b . nbMoves; i ++ )
n e s te d M o v e s [ n ] [ i ] = moves [ i ] ;
Je n g t h B e s t R o l l o u t [ n ] = O;
s c o r e B e s t R o l l o u t [ n ] = O;
while (t rue) {
if ( pb . nbMoves == 0 )
break;
b e s t S c o r e = s c o r e B e s t R o JJo u t [ n ];
b e s t M o v e = b e s t R o JJo u t [ n ] [ pb . Je n g t h V a r i a t i o n ];
for (inti = O; i < p b . nbMoves; i + + ) {
if ( n == 1 ) {
Pro bJe m p = pb;
p . pJa y M o v e ( n e s t e d M o v e s [ n ] [ i ] ) ;
p . pJa y o u t ( );
s c o r e R o JJo u t = p . s c o r e ;
if ( s c o r e R o JJo u t > b e s t S c o r e ) {
b e s t S c o r e = s c o r e R o JJo u t ;
b e s t M o v e = n e s t e d M o v e s [ n ] [ i ];
s c o r e B e s t R o JJo u t [ n ] = b e s t S c o r e;
Je n g t h B e s t R o JJo u t [ n ] = p . Je n g t h V a r i a t i o n ;
for (inti = O; i < p . Je n g t h V a r i a t i o n ; i ++ )
b e s t R o JJo u t [ n ] [ i ] = p . v a r i a t i o n [ i );
}
}
els e {
202 Méthodes de Monte-Carlo pour les jeux à un joueur
P r o b l e m p = pb;
p . p l a y M o v e ( n e s t e d M o v e s [ n ] [ i ] );
s c o r e R o l l o u t = n e s t e d R o l l o u t ( p , n - 1 );
if ( s c o r e R o l l o u t > b e s t S c o r e ) {
b e s t S core = score R o l l o u t;
b e s t M o v e = n e s te d M o v es [ n ] [ i ] ;
s c o r e B e s t R o l l o u t [ n ] = b e s t S c o r e;
I e n g t h B e s t R o l l o u t [ n ] =p . I e n g t h V a r i a t i o n ;
for (int i = O; i < p . l e n g t h V a r i a t i o n ; i ++ )
b e s t R o l l o u t [ n ] [ i ] =p . v a r i a t i o n [ i ];
if ( n > 0 ) {
for (int t = O; < n - 1; t ++ )
c o u t << "\ t ";
c o u t << "n._.= .. ......" <<
......" << n << " ,._.p r o g r e s ....=
pb . l e n g t h V a r i a t i o n << " ,._.le n g t h._.= ......" <<
I e n g t h B e s t R o l l o u t [ n ] << " ,._. s c o r e ._.= ......" <<
s c o r e B e s t R o l l o u t [ n ] << " ,._.nbMoves ....= .. ......" <<
p b . nbMoves << "\ n";
}
}
}
}
p b . p l a y M o v e ( b e s t M o v e );
p b . fi n d M o v e s ( t a b u );
for (int i = O; i < p b . nbMoves; i ++ )
n e s t e d M o v e s [ n ] [ i ] = moves [ i ] ;
}
re turn p b . s c o r e ;
}
Problèmes de satisfaction de
contraintes
"La programmation par contraintes est une des techniques qui se rapproche le plus du
Saint-Graal de la programmation : l'utilisateur définit le problème, l'ordinateur le résout"
Eugene Freuder.
12.1 Introduction
- un ensemble de variables,
- un ensemble de valeurs possibles pour chaque variable,
- un ensemble de contraintes qui relient les variables entre elles.
Un autre problème classique est les N reines : le but est de placer sur un échiquier
NxN, N reines de façon à ce qu'aucune reine ne puissent prendre une autre reine directe
ment.
Exercice : Donnez les variables, leurs valeurs possibles et les contraintes pour le pro
blème des N reines. Trouver une solution au problème des 4 reines.
12.3 Le Backtrack
Chaque fois qu'une variable est instanciée, toutes les contraintes qui la contiennent et
qui ne contiennent que des variables instanciées sont testées. Si une contrainte n'est pas
vérifiée, l'algorithme arrête sa recherche et essaie la valeur suivante pour la variable (c'est
ce qu'on appelle le Backtrack).
Un ensemble de variables instanciées est consistant s'il vérifie toutes les contraintes
vérifiables. Si un sous-ensemble de l'ensemble des contraintes n'est pas consistant, alors
tous les ensembles de variables instanciées qui le contiennent ne sont pas consistants.
C'est pourquoi on arrête la recherche dès qu'une contrainte n'est pas vérifiée.
On se place dans le cas particulier où le problème peut être modélisé à l'aide d'un
domaine de valeurs entières comprises entre zéro et une taille fixée.
12.4 Le Sodoku
Un problème de Sodoku est une matrice 9x9 telle que chaque ligne et chaque colonne
ne contient qu'une seule fois les entiers de 1 à 9. Une grille 9x9 est décomposée en
carrés 3x3. En plus des contraintes sur les lignes et les colonnes, chaque carré 3x3 ne doit
contenir qu'une seule fois les chiffres de 1 à 9.
Dans l'algorithme Forward Checking, après chaque affectation d'une valeur à une va
riable, on ôte des domaines des variables restantes les valeurs qui ne sont pas compatibles
avec l'affectation. Cette mise à jour à chaque affectation des domaines de chaque variable
permet de détecter les domaines contraints et les domaines vides.
Une bonne heuristique statique est l'heuristique de cardinalité maximum qui consiste
à choisir la variable qui est liée par des contraintes au plus grand nombre de variables déjà
choisies. On peut aussi prendre en compte la taille du domaine de la variable, le nombre de
contraintes dans lesquelles la variable est présente ou encore la difficulté des contraintes
contenant la variable.
Une bonne heuristique dynamique est de choisir la variable ayant le nombre minimal
de valeurs vérifiant les contraintes étant donné les instanciations déjà effectuées.
L'ordre d'instanciation des valeurs n'a pas d'influence sur les problèmes inconsistant
ou sur les problèmes où l'on cherche toutes les solutions, car il faudra vérifier toutes les
206 Problèmes de satisfaction de contraintes
Exercice : Modifier le programme de Sudoku pour qu'il commence par instancier les
variables de plus petit domaine.
La recherche avec déviations limitées recherche tous les chemins qui comporte un
nombre limité de déviations. Elle commence par le chemin avec zéro déviation, puis celui
avec une déviation, et incrémente ensuite le nombre de déviations permises à chaque
recherche infructueuse.
Un exemple de contrainte globale est la contrainte all-diff qui porte sur un ensemble
de variables et qui vérifie qu'elles sont toutes différentes. Les contraintes globales per
mettent de formuler les problèmes plus élégamment et permettent aussi de mieux propa
ger les contraintes. Un exemple d'utilisation des contraintes globales est l'utilisation de
2n contraintes n-aire all-diff pour modéliser les lignes et les colonnes du Sudoku.
12.9 La recherche locale 207
L'heuristique la plus naturelle consiste à déplacer la reine qui est en prise avec le plus
d'autres reines, et de la déplacer vers une position qui est en prise avec le moins de reines
possibles.
Cette heuristique est simple mais elle marche très bien, beaucoup mieux que le Back
track. Les algorithmes de Backtrack peuvent résoudre des problèmes contenant des cen
taines de reines alors que la recherche locale permet de résoudre des problèmes contenant
beaucoup plus de reines.
On utilise une variable par colonne. Les variables sont notées C1 , C2, ... , CN. Les
domaines de valeurs des variables sont { 1, 2, . . . , N}. les contraintes sont :
208 Problèmes de satisfaction de contraintes
12.11.2 Le Backtrack
void d e s a f fe c t e ( ) {
a f fe c t e e = fal se;
}
};
u sing namespa ce s t d ;
int n o m b r e V a r i a b l e s;
D o m a i n e l n t e r v a l l e *V a r i a b l e s ;
int n b R e i n e s = 8;
void i n i t R e i n e s ( );
void a f f i c h e S o l u t i o n ( );
bool b a c k t r a c k ( int n umVar = O );
/* une v a r i a b l e p a r c o l o nn e */
/* a v e c une v a l e u r p a r c a s e de l a c o l o nn e */
void i n i t R e i n e s ( ) {
n o m b r e V a r i a b l e s = n b R e i n e s;
v a r i a b l e s = new D o m a i n eln t e r v a l l e [ n b R e i n e s ];
for ( int i = O; i < n o m b r e V a r i a b l e s; i ++ )
v a r i a b l e s [ i ] . a l l o u e ( n b R e i n e s );
}
void a f f i c h e S o l u t i o n ( ) {
for ( int i = O; i < n o m b r e V a r i a b l e s; i ++ )
c o u t << v a r i a b l e s [ i ] . v a l e u r ( ) << "...," ;
210 Problèmes de satisfaction de contraintes
c o u t << e n d l ;
}
12.11.3 Le Sudoku
u s ing namespace s t d ;
c onst int T a i l l e M a x = 2 5;
D o m a i n eln t e r v a l l e v a r [ T a i l l e M a x ] [ T a i l l e M a x ];
void i n i t ( ) {
for (int i = O; i < t a i l l e ; i ++ ) {
for (int j = 0; j < t a i 1 1 e ; j ++ )
var [ i ] [ j ] . a l l o u e ( t a i l l e );
}
}
bool b a c k t r a c k () {
in t i , j ;
D o m a i n eln t e r v a l l e *d = c h o i s i t V a r i a b l e ( i , j );
if ( d == NULL) re turn true;
in t v a l [ T a i l l e M a x ] , n b v a l s =
212 Problèmes de satisfaction de contraintes
e n u m e r e V a l e ur s ( i , j , v a l );
for (int k = O; k < n b_ v a l s ; k++ ) {
v a r [ i ) [ j ] . a f f e c t e ( v a l [ k ] );
if ( c o n s i s t a n t ( i , j , v a l [ k ] ) )
if ( b a c k t r a c k ( ) )
ret urn t rue;
var [ i ] [ j ] . d e s a ffe c t e ( );
}
ret urn fals e;
}
void a f f i c h e S o l u t i o n ( ) {
for (int i = O; i < t a i l l e ; i ++ ) {
for (int j = O; j < t a i l l e ; j++ )
if ( v a r [ i ] [ j ] . a f f e c t e e ( ) )
Il Il .
c o u t << v a r [ i ] [ j ] . v a l e u r ( ) << � '
el s e
Il .
c o u t <<
c o u t << e n d!;
}
}
int m a i n (int a r g c , c h ar ** a r g v ) {
i n i t ( );
if ( b a c k t r a c k ( ) )
a f f i c h e S o l u t i o n ( );
}
s t a c k <int > p i l e ;
void r e m e t ( ) {
white ( p i l e . t o p () != - 1 ) {
12.11 Corrigés des exercices 213
int v a l = p i l e . t o p ( );
p i 1 e . pop ( ) ;
int j = p i l e . t o p ( );
pile . pop ( );
int i = p i l e . t o p ( );
pi1e . pop ( ) ;
var [ i ] [ j ] . r e m e t ( v a l );
}
p i l e . pop ( );
}
bool fc ( ) {
int i , j ;
D o m a i n e l n t e r v a l l e *d = c h o i s i t V a r i a b l e ( i , j );
if ( d == NULL) ret urn t rue;
214 Problèmes de satisfaction de contraintes
int v a l [ T a i l l e M a x ] , n b_ v a l s =
e n u m e r e V a l e u r s ( i , j , v a l );
for (int k = O; k < n b_ v a l s ; k++ ) {
v a r [ i ] [ j ] . a f f e c t e ( v a l [ k ] );
if ( c o n s i s t a n t ( i , j , v a l [ k ] ) )
if ( fc ( ) )
ret urn t rue;
r e m e t ( );
v a r [ i ] [ j ] . d e s a ff e c t e ( );
}
ret urn fa l s e;
}
bool I d s ( int o r d r e );
bool I d s (in t o r d r e ) {
in t i , j;
D o m a i n e l n t e r v a l l e *d = c h o i s i t V a r i a b l e ( i , j );
if ( d == NU LL) ret urn t rue;
in t v a l [ T a i l l e M a x ] , n b_ v a l s =
e n u m e r e V a l e u r s ( i , j , v a l );
if ( o r d r e == 0 ) {
if ( t r y l d s ( i , j , v a l [ 0 ] , o r d r e ) )
ret urn t rue;
}
else {
for (in t k = 1; k < n b_ v a l s ; k++ ) {
if ( t r y l d s ( i , j , v a l [ k ] , o r d r e - 1))
ret urn t rue;
}
if ( n b_ v a l s > 0 )
if ( t r y l d s ( i , j , val [ 0 ] , o rdre ) )
ret urn t rue;
}
ret urn fa l s e;
}
bool i d_ l d s ( ) {
for (in t o r d r e = O; o r d r e < t a i l l e * t a i l l e ; o r d r e ++ ) {
if ( I d s ( o r d r e ) )
ret urn t rue;
}
ret urn fa l s e;
}
On peut tester que pour des Sudokus de taille 25x25, la recherche avec déviations
limitées trouve une gri lle en 10 secondes alors que le forward checking met beaucoup
plus de temps. Le succès de la méthode n'est pas seulement dû à un bon ordonnancement
des valeurs puisque dans ce cas, elles sont ordonnées Iexicographiquement.
c l a s s Move {
pu blic :
in t _ i , _j , _v a l u e;
Move (in t i = 0, in t j = 0, in t v a 1 u e = 0) {
216 Problèmes de satisfaction de contraintes
= i;
_j = j;
v a l u e = v a l u e;
}
};
Move v a r i a t i o n [ 1 0 0 0 ];
int s a m p l e (int d e p t h ) {
int i , j , max d = d e p t h ;
D o m a i n e l n t e r v a l l e *d = c h o i s i t V a r i a b l e ( i , j );
if ( d == NULL)
ret urn d e p t h;
int v a l [ T a i l l e M a x ] ,
n b_ v a l s = e n u m e re V a l e u r s ( i , j , v a l );
if ( n b _ v a 1 s == 0 )
ret urn d e p t h;
int i n d i c e = r a n d ( ) % n b_ v a l s ;
Move m ( i , j , v a 1 [ i n d i c e ] );
v a r i a t i o n [ d e p t h ] = m;
v a r [ i ] [ j ] . a f f e c t e ( v a l [ i n d i c e ] );
if ( c o n s i s t a n t ( i , j , v a l [ i n d i c e ] ) )
maxd = s a m p l e ( d e p t h + 1 );
if ( maxd == t a i 1 1 e * t a i 1 1 e )
ret urn maxd;
r e m e t ( );
v a r [ i ] [ j ] . d e s a f fe c t e ( );
ret urn maxd;
}
int n b B e s t R o l l o u t [ 4 ];
Move b e s t R o 1 1 o u t [ 4 ] [ 1 0 0 0 ] ;
v a r [ i ] [ j ] . a f f e c t e ( v a l [ k ] );
if ( c o n s i s t a n t ( i , j , v a l [ k ] ) ) {
if ( n == 1 ) {
int l e n g t h P l a y o u t = s a m p l e ( n b P r e f i x + 1 );
if ( l e n g t h P l a y o u t > b e s t ) {
b e s t = l e n g t h P l a y o u t;
b e s t M o v e = m;
n b B e s t R o l l o u t [ n ] = b e s t;
b e s t R o l l o u t [ n ] [ n b P r e f i x ] = m;
for (int 1 = n b P r e f i x + 1;
1 < l e n g t h P l a y o u t ; !++ )
bestRollout [ n ] [ l ] = variation [ ! ];
}
}
el s e {
int l e n g t h P l a y o u t = n e s t e d ( n b P r e fi x + 1 ,
p r e f i x , n - l) ;
if ( l e n g t h P l a y o u t > b e s t ) {
b e s t = l e n g t h P l a y o u t;
b e s t M o v e = m;
n b B e s t R o l l o u t [ n ] = b e s t;
b e s t R o l l o u t [ n ] [ n b P r e f i x ] = m;
for (int 1 = n b P r e f i x + 1;
l < l e n g t h P l a y o u t; 1 ++ )
b e s t R o l l o u t [ n ] [! ] =
b e s t R o l l o u t [ n - 1 ] [ ! ];
}
}
}
if ( b e s t == t a i 1 1 e * tai11e)
ret urn b e s t ;
r e m e t ( );
v a r [ i ] [ j ] . d e s a ffe c t e ( );
}
var [ bestMove . _i ]
[ b e s t M o v e . _j ] . a f f e c t e ( b e s t M o v e . _ v a l u e );
if ( c o n s i s t a n t ( b e s t M o v e . _ i , b e s t M o v e . _j ,
bestMove . _v a l u e ) ) {
p r e f i x [ n b P r e f i x ] = b e s t M o v e;
n b P r e f i x++;
}
el s e
break;
}
if ( n b P r e f i x == t a i l l e * t a i l l e )
ret urn n b P r e f i x;
for (int n = n b P r e f i x - 1; n >= n b P r e f i x S t a r t ; n - - ) {
218 Probl èmes de satisfaction de contraintes
r e m e t ();
v a r [ p r e fi x [ n ] . _ i ] [ p r e f i x [ n ] . _j ] . d e s a f f e c t e ( );
}
ret urn n b P r e f i x;
}
int m a i n (int a r g c , c h ar ** a r g v ) {
i n i t ();
while ( true) {
Move p r e f i x [ 1 0 0 0 ];
int nb = n e s t e d ( 0 , p r e f i x , 1 );
c o u t << n b << "�";
if ( n b == t a i 1 1 e * t a i 1 1 e )
break;
}
c o u t << e n d !;
a ff i c h e S o l u t i o n ( );
}
Chapitre 13
13. 1 Introduction
Les algorithmes de Monte-Carlo sont bien adaptés pour ce style de jeu car il per
mettent d'échantillonner simplement les distributions possibles des positions adverses.
13.2 Le Go fantôme
Au Go fantôme, il y a deux joueurs et un arbitre. Chaque joueur ne voit que son propre
goban et l'arbitre signale au joueur s'il a joué un coup illégal. Dans ce cas il est autorisé
à rejouer.
13.3 Le Bridge
Les programmes de Bridge utilisent des algorithmes différents pour les annonces et
pour le jeu de la carte. La partie annonce est typiquement gérée par un système de règles
220 Jeux à information incomplè te
qui permet aux partenaires d'échanger des informations sur leur jeu avant de décider du
contrat à faire. La partie jeu de la carte est généralement traitée avec un algorithme de
Monte-Carlo.
13.4 Le Poker
"Le poker est un jeu passionnant permettant de perdre son argent, son temps et ses
amis."
Philippe Bouvard.
La variante la plus communément jouée au Poker est le Texas Hold'em. Chaque joueur
à deux cartes et peut combiner ses deux cartes avec les cartes dévoilées au centre de la
table. On dévoile d'abord trois cartes, puis une quatrième et une cinquième. Les dévoile
ments sont suivis d'enchères. Le Texas Hold'em est la variante sur laquelle les chercheurs
en intelligence artificielle ont le plus travaillé. Un des objectifs des programmes de Poker
est de trouver une stratégie qui soit un équilibre de Nash, ou du moins qui s'en rapproche.
Le Texas Hold'em a un espace d'états trop grand pour trouver un équilibre de Nash à
l'aide de la théorie des jeux et des ordinateurs et programmes actuels. Une méthode com
munément utilisée pour rendre le jeu plus facile à résoudre est d'utiliser des abstractions,
ce qui consiste à regrouper les mains similaires dans les mêmes états. Ceci permet alors
de trouver l'équilibre du jeu ainsi simplifié à l'aide de la programmation linéaire [ 1 1 ] .
Une autre méthode classique est d'approximer l a stratégie optimale e n s e rapprochant de
l'équilibre de Nash ou d'utiliser des méthodes de Monte-Carlo [57] .
Chapitre 14
14.1 Domineering
Afin de présenter succinctement la théorie des nombres surréels qui représentent des
jeux, nous allons utiliser le jeu Domineering. Domineering se joue sur un plateau rectan
gulaire composé initialement de cases vides. Il y a deux joueurs, Horizontal et Vertical.
Horizontal a comme coup possible de noircir deux cases blanches voisines horizontale
ment. Vertical a comme coup possible de noircir deux case blanches voisines verticale
ment. Le premier joueur qui ne peut plus jouer a perdu.
FIGURE 14. l - Les deux premiers coups d'une partie de Domineering 5x5.
La figure 1 4.2 donne un exemple de fin de partie à Domineering 5x5 après trois coups.
On voit que cette position est composée de trois régions indépendantes. Le principe de la
théorie combinatoire des jeux est d'associer un nombre à chacune de ces régions et de
faire ensuite la somme de ces nombres pour évaluer la position entière.
Cette décomposition en sous jeux est illustrée par la figure 14.3 qui montre la somme
des trois régions indépendantes de la position.
222 Théorie combinatoire des jeux
+ +
EE
FIGURE 14.3 - Décomposition en sous jeux de la fin de partie.
Les nombres surréels les plus simples comptent pour une région le nombre de coups
qu'un joueur peut jouer dans cette région. Le nombre le plus simple est O. Pour représenter
les nombres plus élaborés, on représente un doublet composé d'une partie gauche qui
correspond à la position atteinte après le meilleur coup du joueur Gauche (dans notre cas
on choisira Vertical), et d'une partie droite qui correspond à la position atteinte après le
meilleur coup du joueur Droit (dans les exemples qui suivent Horizontal). Par exemple le
nombre 1 se représente avec {OI} : si Vertical joue il n'a plus de coup, et Horizontal n'a
pas de coup. Le nombre 0 se représente {}. Le nombre 2 se représente { 1 1} = { { OI} 1} =
{ { {} 1 } 1 }. De même il existe des nombres négatifs pour les points de Vertical. On a ainsi
- 1 = { I O}et - 2 = { 1 - 1}.
On peut aussi avoir des jeux qui correspondent à des fractions ainsi { O l 1} = � . On a
' sur 1 + 1 = 1 .
b ien A
2 2
II existe aussi des infinitésimaux comme {OIO} = * qui est plus petit que tout les
nombres positifs et plus grand que tous les nombres négatifs. D'autres infinitésimaux
courants sont { O I *} =t et son opposé { * IO} = .!. .
Des exemples de régions associées à leurs nombres sont données dans la figure 14.4.
Pour aller plus loin dans la théorie combinatoire des jeux, la lecture de "Lessons in
14.3 Corrigés des exercices 223
-1
Eb
*
2 1 12
14.3.1 t à Domineering
"Le fossé séparant théorie et pratique est moins large en théorie qu 'il ne l ' est en pra
tique"
Anonyme.
La théorie des jeux est l 'étude de comportements rationnels dans des situations où les
acteurs dépendent les uns des autres.
- séquentiels ou simultanés.
- de compétition ou de coordination.
- joués une seul fois ou répétés.
- à information complète ou non.
De nombreuses ressources sur la théorie des jeux sont disponibles sur le site web
http ://www.gametheory.net/
226 Théorie des jeux
On connaît les actions possibles des autres et les gains associés à toutes les combinai
sons d ' actions mais on ne connaît pas les coups que les autres vont jouer.
Pour décider il faut écrire la matrice des gains qui donne les gains pour toutes les
combinaisons d ' actions. On peut alors chercher une combinaison pour laquelle personne
ne regrette son choix : un équilibre de Nash.
Dans un jeu à deux joueurs, chaque ligne correspond à une action possible du pre
mier joueur, nommons ce premier joueur Ligne. Chaque colonne correspond à une action
possible du deuxième joueur, nommons ce deuxième joueur Colonne.
Dans chaque case de la matrice on représente Je gain de Ligne suivi du gain de Co
lonne.
Une équilibre de Nash est une situation dans laquelle aucun des joueurs n ' a intérêt à
modifier son action si les autres ne modifient pas non plus Jeurs actions.
Le résultat d'un jeu n ' est pas toujours un équilibre de Nash même lorsque les joueurs
sont rationnels. Par exemple au jeu de la poule mouillée.
15.2 Les jeux simultanés à information complète 227
Deux prisonniers sont interrogés séparément par la police. Si aucun des deux ne dé
nonce l ' autre ils auront tous deux un an de prison. Si l ' un dénonce et pas l ' autre, celui qui
a dénoncé est libre et l ' autre à dix ans. S ' ils dénoncent tous les deux ils ont tous les deux
4 ans de prison.
Une manière de trouver un équilibre de Nash est de tracer des flèches des cases domi
nées vers les cases dominantes de la même colonne ou de la même ligne. Pour les cases
de la même ligne on comparera les gains de Colonne, pour les cases de la même colonne
on comparera les gains de Ligne. Un équilibre de Nash est une case qui ne comporte que
des flèches entrantes.
On peut aussi trouver des lignes et de colonnes dominées et les éliminer au fur et à
mesure de la matrice.
Deux personnes ont le choix entre deux restaurants différents, elles préfèrent être en
semble que seules. La matrice est donnée par la table 1 5 .2. Il y a deux équilibres de Nash.
Deux pilotes se font face sur une route qui n ' est pas assez large pour deux voitures.
Le premier qui dévie de la route a perdu, toutefois si aucun des deux ne dévie ils meurent
tous les deux. La matrice est donnée par la table 1 5 .3. Il y a deux équilibres de Nash.
228 Théorie des jeux
Il n ' existe pas toujours d'équilibre en stratégie pure. Le jeu pierre feuille ciseaux est
une exemple de ce type de jeux. Le principe du jeu est que deux joueurs font simulta
nément un signe de la main correspondant soit à pierre, soit à feuille, soit à ciseaux. La
pierre bat le ciseaux, le ciseau bat la feuille et la feuille bat la pierre.
Dans un jeu de stratégie temps réel, on connaît la carte sans forcément connaître la
place des ennemis. Supposons qu 'une base puisse être attaquée par deux chemins pos
sibles, un premier chemin qui prend 20 secondes à parcourir et un autre qui prend 10
secondes. Le joueur attaqué peut choisir d' envoyer ses défenses sur un des deux chemins,
toutefois s ' il se trompe de chemin cela lui aura coûté 8 secondes. Le gain du défenseur
correspond au temps pendant lequel il peut attaquer en dehors de sa base. C'est un jeu à
somme nulle.
Le principe d' une stratégie mixte est d' associer chaque action à une probabilité et de
choisir les actions avec cette probabilité.
La recherche d'un équilibre en stratégie mixte consiste à choisir les probabilités pour
ses actions qui rendent l ' autre joueur indifférent à ses propres actions.
Pour cela on calcule l 'espérance de gain de chaque action et on cherche les probabili
tés pour lesquelles les espérances sont égales.
15.4 Jeux répétés 229
Lorsqu ' un jeu est répété, il existe deux grands types de stratégies, les stratégies Oeil
pour oeil (Tit for Tat) et les stratégies Sévères (Grim). Pour le dilemme du prisonnier, la
stratégie Oeil pour oeil consiste à dénoncer l ' autre joueur s ' il a dénoncé au coup précédent
et à ne pas dénoncer autrement (y compris au premier coup). C'est une stratégie qui
pardonne les écarts. Au contraire la stratégie sévère ne pardonne pas ; elle consiste à
dénoncer tout le temps dès que l ' autre joueur a dénoncé ne serait-ce qu 'un seule fois.
Exercice : O n suppose que les gains sont soumis à u n taux d' intérêt ti d ' un coup sur
l ' autre. Un gain g au temps 0 vaut g x (1 + ti) au temps l, et ainsi de suite. Analyser les
stratégies Oeil pour oeil et Sévère pour le jeu de la matrice de gain 1 5 . 5 . A partir de quel
taux d' intérêt amènent elles à la coopération ?
Si on trace des flèches des cases dominées vers les cases dominantes, on trouve que
l 'équilibre de Nash de la matrice de la table 1 5 . 1 est en (Z,B) avec des gains de ( 1 0,5).
C'est la seule case qui n ' a aucune flèche sortante.
On peut aussi trouver des lignes et de colonnes dominées et les éliminer au fur et à
mesure de la matrice.
Il n ' y a pas d'équilibre de Nash en stratégie pure (en stratégie mixte l ' équilibre de
Nash correspond à une probabilité de � sur chaque stratégie).
L'équilibre de Nash est que les deux joueurs utilisent le deuxième chemin.
15.5 Corrigés des exercices 231
Esperance(A) = 0 x p + 85 x ( 1 - p) = 85 x ( 1 - p)
Esperance(B) = 15 x p+0 x ( 1 - p) = 15 x p
85 X ( 1 - p) = 15 X p
85 = 100 X p
p = 0.85
Esperance(X) = Esperance(Y)
20 X q+0 X (1 - q ) = 0 X q + 15 X (1 - q)
20 X q = 1 5 - 15 X q
q- 15
35 -
- �
7
Contre Oeil pour oeil, si on coopère tout le temps on obtient comme gain au temps t :
soit 1 + ti > �
.
soit ti. 8
> -9
Jeux généraux
16. 1 Introduction
Une des critiques faite à la recherche en Intelligence Artificielle est la trop grande spé
cialisation de ses applications. Par exemple les meilleurs programmes d'Échecs ne savent
jouer qu ' aux Échecs. Si on leur demande de jouer à un autre jeu, ils en sont incapables.
Pour faire avancer la recherche sur des programmes plus généraux, une compétition
annuelle de General Game Playing a été lancée depuis 2005 . Le principe est que les pro
grammes ne connaissent les règles du jeu auquel il vont jouer que juste avant d ' y jouer.
Ils ne peuvent donc pas être spécialisés sur le jeux, à moins de se spécialiser automatique
ment et rapidement.
Le langage GDL utilisé pour décrire les jeux est fondé sur la logique des prédicats du
premier ordre. Il est habituellement converti vers Prolog. Il comporte un certain nombre
de mots clés et de convention qui permettent par exemple de retrouver les coups possibles
ou de savoir si une partie est finie et quel est son résultat.
Les meilleurs programmes de General Game Playing comme Ary [ 60) utilisent I ' algo
rithme UCT. Une autre approche qui a eu moins de succès est la génération automatique
de fonction d 'évaluation pour un algorithme Alpha-Bêta.
Bibliographie
[5] C. Berge. Packing problems. Studies on Graphs and Discrete Programming, Ann.
Dise. Math . , 1 1 : 1 5, 1 98 1 .
[6] E. Berlekamp, J. H. Conway, and R. K . Guy. Winning Ways. Academic Press, 1 982.
[7] H. Berliner. The B * Tree Search Algorithm : a best-first proof procedure. Artificial
Intelligence, 12 :23-40, 1 979.
[28] T. Cazenave and Abdallah Saffidine. Score bounded Monte-Carlo tree search. In
Computers and Garnes, 20 10.
(45) T. Ishida. Real-time search for autonomous agents and multiagent systems. Autono
mous Agents and Multi-Agent Systems, 1 (2) : 1 39-1 67, 1 998.
(46) T. Ishida and R. E. Korf. Moving target search. In IJCAI, pages 204-2 1 1 , 1 99 1 .
(47) A . Junghanns and J . Schaeffer. Search versus knowledge in game-playing programs
revisited. In IJCAI, pages 692-697, 1 997.
(48) G. Kendall, A. Parkes, and K. Spoerer. A survey of NP-complete puzzles. ICGA
Journal, 3 1 ( 1 ) : 1 3-34, 2008.
(53) R. E. Korf. Macro-operators : A weak method for learning. Artif. Intel!. , 26( 1 ) : 35-
77, 1 985.
[54) R. E. Korf. Real-time heuristic search. Artif. Intel!. , 42(2-3) : 1 89-2 1 1 , 1 990.
(55) R. E. Korf. Linear-space best-first search. Artif. lntell., 62( 1 ) :41-78, 1 993.
(56) R. E. Korf. Finding optimal solutions to rubik's cube using pattern databases. In
AAAI-97, pages 700-705 , 1 997.
[57) M. Lanctot, K. Waugh, M. Zinkevich, and M. Bowling. Monte carlo sampling for
regret minimization in extensive games. In Advances in Neural Information Proces
sing Systems 22 (NIPS), pages 1 078-1086, 2009.
238 BIBLIOGRAPHIE
[7 1 ] D.J. Rosenkrantz, R.E. Stearns, and P.M. Lewis. An analysis of several heuristics
for the traveling salesman problem. Fundamental Problems in Computing, pages
45-69, 2009.
[72] A. Sadikov, I. Bratko, and 1 . Kononenko. Search versus knowledge : an empirical
study of minimax on krk. In Advances in Computer Garnes : Many Garnes, Many
Challenges, pages 33-44, 2003.
[75] J. Schaeffer. The history heuristic. ICCA Journal, 6(3) : 1 6-19, 1 983.
[76] J. Schaeffer. The history heuristic and alpha-beta search enhancements in practice.
IEEE Transactions on Pattern Analysis and Machine Intelligence, 1 1 ( 1 1 ) : 1 203-
1 2 1 2, 1 989.
BIBLIOGRAPHIE 239
[94] A. Zobrist. A new hashing method with application for game playing. Technical
Report 88, Computer Science Department, University of Wisconsin, Madison, 1 970.
Cet ouvrage a été achevé d ' imprimer en mai 20 l l
dans les ateliers de Normandie Roto Impression s . a . s .
6 1 250 Lonrai
N° d ' impression : 1 1 1 803
Dépôt légal : mai 20 1 1
Imprimé en France
Ce l ivre tra ite d ' i ntell igence artificielle p o u r les j e u x .
I l s'a d r e s s e à des p e r s o n n e s aya nt des r u d i ments d e
programmation.
I l aborde a u ssi bien les jeux de réflexion que les puzzles
o u la r e cherche d u plus court chem i n cl a n s les j e u x
v i d é o . C h a q u e a lg o r it h m e fa it l 'o b j e t d ' u n c h a p i t r e .
C h a q u e chapitre comporte u n c o u r s , des exercices et
leur correction en C++.
Le contenu du l ivre fa it l 'objet de cours e n M aster et en
école d 'i ngénieurs depuis plus de dix ans. Les algorit h mes
abordés p o u r les jeux à cieu x joueurs sont ! 'A lpha-Bêta et
ses opt i misations pour le jeu du virus, la recherche arbo
rescente Monte- C a rlo pour le jeu de Go, l a recherche en
meilleur d 'abord pour le Gomoku et l 'a n a lyse rétrograde
p o u r les Échecs et pour d 'autres jeux. Pour les jeux à u n
joueur, l 'a lgorithme A* est appliqué à l a recherche du plus
court chemi n s u r une ca rte a i nsi qu'à l a résolution de
p u zzles comme le Rubik's cube. L'a n a lyse rétrograde, les Tristan Cazenave
est professeu r
méthodes de Monte-Carlo et la satisfaction de contra i ntes
.:ra.u la(?m'atoire L A IVI SA D E
sont a u s s i abordées p o u r des problèmes à u n j o u e u r
à l ' u n iversité Pa ris-Dauph i n e
comme S a meGame o u le Sudoku . E n f i n l e s j e u x à i n for où i l fa i t d e l a rec herche
mation i ncomplète, la théorie combinatoire des jeux, la en i ntell igence a r t i ficie l le
théorie des jeux et les j e u x généraux sont présentés. dans le doma i n e des j e u x .
Crédits photographiques :
© Beboy Fotolia,com, © mweber67 Fotolia.com,
· ·