Vous êtes sur la page 1sur 127

Université Paris 8

IA 1 pour les Jeux

Nicolas Jouandeau
n@ai.up8.edu

mars 2019

1. Intelligence Artificielle
Table des matières
1 Introduction 3

2 Résolution par exploration 5


2.1 Recherche en profondeur . . . . . . . . . . . 5
2.2 Recherche en largeur . . . . . . . . . . . . 9
2.3 Recherche à profondeur itérative . . . . . . . . 10
2.4 Application au Taquin . . . . . . . . . . . 11
2.5 Application au Solitaire . . . . . . . . . . . 18
2.6 Application à Sokoban . . . . . . . . . . . 23

3 Minimax et Alpha-beta 24
3.1 Minimax . . . . . . . . . . . . . . . 24
3.2 Negamax . . . . . . . . . . . . . . . 25
3.3 Alpha-beta (αβ) . . . . . . . . . . . . . 25
3.4 Minimax Tree Optimization (MMTO) . . . . . . . 26
3.5 Application aux arbres ternaires . . . . . . . . . 26
3.6 Application aux Dames . . . . . . . . . . . 28
3.7 Application aux Echecs . . . . . . . . . . . 28
3.8 Application à Breakthrough . . . . . . . . . . 28
3.9 Application à Kamisado . . . . . . . . . . . 28

4 Monte-Carlo et Monte-Carlo Tree Search 29


4.1 Monte Carlo Tree Search (MCTS ) . . . . . . . . 31
4.2 Progressive Bias (PB) . . . . . . . . . . . 34
4.3 Early Playout Termination (EPT) . . . . . . . . 34
4.4 L’hybridation minimax de MCTS . . . . . . . . 34
4.5 Rapid Action Value Estimation (RAVE) . . . . . . 35
4.6 Generalized RAVE (GRAVE) . . . . . . . . . 38
4.7 MAST, TO-MAST, PAST . . . . . . . . . . 39
4.8 Playout Policy Adaptation (PPA) . . . . . . . . 39
4.9 Sequential Halving applied to Trees (SHOT) . . . . . 39
4.10 Application à Nonogram . . . . . . . . . . . 39
4.11 Application à Breakthrough . . . . . . . . . . 39
4.12 Application à AngryBirds . . . . . . . . . . . . . . . . . . . . . . 39

5 Recherche enveloppée 40
5.1 Nested Monte Carlo Search (NMCS) . . . . . . . 40
5.2 Nested Rollout Policy Adaptation (NRPA) . . . . . . 43

6 Preuve 47
6.1 Proof Number Search (PNS) . . . . . . . . . . 47
6.2 Proof Number 2 Search (PN2S) . . . . . . . . . 47
6.3 Depth-First PNS (DFPNS) . . . . . . . . . . 47
6.4 Problème GHI . . . . . . . . . . . . . . 47
6.5 Variantes de PNS . . . . . . . . . . . . . 47
6.6 Application à Breakthrough . . . . . . . . . . 47

1
7 Parallélisation 48
7.1 A la racine (Root Parallel) . . . . . . . . . . 48
7.2 Aux feuilles (Leaf Parallel) . . . . . . . . . . 48
7.3 Dans l’arbre (Tree Parallel) . . . . . . . . . . 48
7.4 Parallel NMCS . . . . . . . . . . . . . 48
7.5 Distributed NRPA . . . . . . . . . . . . 48
7.6 PNS Randomisé (RP-PNS) . . . . . . . . . . 48
7.7 Parallel DFPNS . . . . . . . . . . . . . 48
7.8 Job-Level PNS (JLPNS) . . . . . . . . . . . 48
7.9 Parallel PN2S (PPN2S) . . . . . . . . . . . 48
7.10 Application au Morpion Solitaire . . . . . . . . 48

8 Problèmes à information incomplète 49


8.1 Théorie des jeux . . . . . . . . . . . . . 49
8.2 Déterminisation et MCTS . . . . . . . . . . 49
8.3 Regret contrefactuel . . . . . . . . . . . . 49
8.4 Résolution par convention . . . . . . . . . . 49
8.5 Application à Hannabi . . . . . . . . . . . 49
8.6 Application au Go Fantôme . . . . . . . . . . 49
8.7 Application au Poker . . . . . . . . . . . . 49

9 Apprentissage 50
9.1 Processus de décision Markovien . . . . . . . . 50
9.2 Heuristique et compromis . . . . . . . . . . 52
9.3 Modes de progression d’apprentissage . . . . . . . 53
9.4 Programmation dynamique . . . . . . . . . . 57
9.5 Application à Grid-world . . . . . . . . . . . 59
9.6 Apprentissage par renforcement . . . . . . . . . 66
9.7 Apprentissage profond . . . . . . . . . . . 70
9.8 Application aux jeux Atari 2600 . . . . . . . . . 70
9.9 Application à StarCraft-II . . . . . . . . . . 71
9.10 Application à Dota2 . . . . . . . . . . . . 113
9.11 Application au Blackjack . . . . . . . . . . . 114

10 Annexes 125
10.1 DataFrame de pandas en Python . . . . . . . . 125

2
1 Introduction
L’IA est un domaine identifié par les questions relatives à la compréhension et
la construction d’entités intelligentes ; la notion d’intelligence est associée au
processus de la pensée dont l’objectif est de percevoir, comprendre, prévoir,
manipuler un monde plus étendu que soi-même ; l’objectif de l’IA est de systé-
matiser et d’automatiser les tâches intellectuelles.

Deux définitions de l’IA sont admises :


— Penser et agir rationnellement (i.e. conformément à ses connaissances et
à une fonction d’évaluation) ; cette définition suppose l’existance de lois
de la pensée ; initiée par l’étude des syllogismes 2 , elle a été étendue à l’en-
semble de la logique et au développement d’agents rationnels ; avec une
définition centrée sur la notion de rationalité, des problèmes apparaissent
pour isoler les connaissances, exprimer formellement les connaissances et
passer de la théorie à la pratique ; avec la notion d’agents rationnels, il
s’agit d’agir pour obtenir la meilleure solution, en utilisant l’expérimen-
tation et l’observation du comportement de l’agent intelligent ; avec cette
définition, l’incertitude et la complexité rendent certaines approches im-
praticables.
— Penser et agir comme un humain ; cette définition est fondée sur le test
de Turing 3 ; classée dans les approches cognitives, cette définition com-
bine un modèle de l’information et un modèle psychologique, avec pour
objectif de théoriser et/ou vérifier le fonctionnement de l’esprit humain ;
avec cette définition, l’apprentissage d’un comportement humain devient
une idée centrale de l’IA.

Les principaux domaines d’application concernés sont regroupés dans les sys-
tèmes décisionnels dont : 1) la planification, 2) la programmation d’agents et
de systèmes autonomes, 3) les jeux 4 , 4) le diagnostic, 6) la robotique et 7) la
compréhension des langages ; le présent document se focalise sur l’IA pour les
jeux.

Les principales conférences, revues et associations concernant l’IA pour les


2. Un syllogisme est un raisonnement logique à trois propositions : deux prémisses et une
conclusion ; pour exemple, un syllogisme concernant Socrate : « Tous les hommes sont mortels,
or Socrate est un homme donc Socrate est mortel. ».
3. Le test de Turing est proposé par A. Turing en 1950 ; son principe est de ne pas pouvoir
distinguer une machine intelligente d’un humain dans une conversation ; une personne est en
conversation à l’aveugle avec un interlocuteur, qui peut être une machine ; si la personne est
incapable de dire si son interlocuteur est une machine ou une personne, la machine est admise
comme IA ; une variante propose d’ajouter aux machines la possibilité de regarder, commenter
et percevoir une vidéo, et de manipuler des objets dans la conversation.
4. Les jeux proposent des problèmes intéressants, dans des espaces d’états et de commandes
discrets, dont la complexité ne permet pas toujours d’avoir une représentation complète en
machine ; les faits marquants dans les jeux concernant les confrontations entre l’homme et la
machine sont : 1) la victoire de l’ordinateur d’IBM, Deep Blue, en 1997 contre Garry Kasparov,
champion du monde aux Echecs et 2) la victoire d’AlphaGo en 2016 contre Lee Sedol, ayant
remporté plus de dix titres mondiaux au Jeu de Go.

3
jeux sont IJCAI 5 , ECAI 6 , AAAI 7 , SIGAI 8 , AISB 9 , AI magazine 10 , RFIA 11 ,
GDC 12 , CG 13 , ACG 14 , ICGA 15 , TCIAIG 16 , CIG 17 , CGW 18 , TAAI 19 , GIGA 20 .

5. International Joint Conference on Artificial Intelligence.


6. European Conference on Artificial Intelligence.
7. Association for the Advancement of Artificial Intelligence.
8. Special Interest Group on Artificial Intelligence.
9. Artificial Intelligence and Simulation of Behaviour.
10. Artificial Intelligence magazine.
11. Reconnaissance des Formes et Intelligence Artificielle.
12. Game Developers Conference.
13. Computer Games.
14. Advanced Computer Games.
15. International Computer Games Association.
16. Transactions on Computational Intelligence and AI in Games.
17. Computational Intelligence and Games.
18. Computer Games Workshop.
19. Technologies and Applications of Artificial Intelligence.
20. General Intelligence in Game-Playing Agents.

4
2 Résolution par exploration
2.1 Recherche en profondeur
La recherche en profondeur est en pratique réalisée à profondeur limitée (i.e.
DLS, pour Depth Limited Search 21 ) ; Alg. 1 et 2 présentent une recherche DLS ;
dans ces deux algorithmes, la recherche s’arrête quand la profondeur maximale
est atteinte ou quand une solution est trouvée.
Les fonctions utiles sont les suivantes :
— h retourne la valeur heuristique de la position courante ;
— nextMoves retourne la liste des coups possibles à partir de la position
courante ;
— play joue un coup ;
— unplay déjoue un coup.

La constante DLS_MAX_DEPTH définit la profondeur max de la recherche ; Fig 1


présente les deux possibilités concernant cette constante ; fixer cette constante
au delà de la profondeur max conduit à l’utilisation de h uniquement au niveau
des feuilles ; la fixer en deçà conduit à l’utilisation de h sur des positions non-
terminales ; on stocke les résultats de l’évaluation des positions dans un table
notée H ; la valeur dans H sont indexées sur les positions ; la meilleure position
associée à la meilleure évaluation est stockée dans la variable best_s, maintenue
en mémoire globale.

Fig. 1 – La valeur DLS_MAX_DEPTH définit la profondeur max de la recherche ;


le triangle représentant l’espace de recherche du problème considéré, fixer cette
valeur en 1 (i.e. au dessus de la profondeur max réelle du problème), équivaut à
borner la recherche et fixer cette valeur en 2 (i.e. au delà de la profondeur max
réelle du problème) est sans effet.

21. Sans la valeur limite DLS_MAX_DEPTH, DLS est une recherche en profondeur d’abord,
abrégée DFS pour Depth-First Search.

5
Dans Alg. 1, la position courante, notée current_s, est maintenue en variable
globale, qui implique l’utilisation des fonctions play et unplay pour maintenir
une position cohérente ; on utilise une table de hashage H pour connaître les
positions précédemment évaluées à plus petite profondeur ; la ligne 6 vérifie si
la position courante est dans H ou si elle a déjà été évaluée à une profondeur
plus grande (ci c’est le cas, il faut refaire la recherche en espérant trouver une
solution ; si la position courante a déjà été évaluée à plus petite profondeur, il
est inutile de poursuivre la recherche à partir de cette position) ; pour résoudre
un problème, on fixe current_s et on appelle DLS(0).

1 fonction DLS ( d ) :
2 if solved then return ;
3 H[current_s] ← d ;
4 if h (best_s) > h (current_s) then
5 best_s ← current_s ;
6 if current_s == W IN then solved ← true ; return ;
7 if current_s == LOST or d == DLS_MAX_DEPTH then return ;
8 M ← nextMoves ( ) ;
9 for each m in M do
10 play ( m ) ;
11 if current_s ∈/ H or H[current_s] > d then
12 DLS ( d + 1 ) ;
13 unplay ( m ) ;
14 if solved then break ;

Alg. 1: Recherche en profondeur, à profondeur limitée, avec une position


courante maintenue en variable globale ; d définit la profondeur courante ;
initialement, d est égal à zéro et solved est égal à f alse.

Initialement, H est vide et best_s est la valeur par h de la position initiale.

Après terminaison de la recherche DLS, si solved est f alse, la recherche est un


échec ; si solved est true, la recherche est un succès ; dans les deux cas, best_s
contient la position avec la meilleure évaluation h (best_s) à la profondeur
H[best_s].

6
Dans Alg. 2, la position courante, notée s, est une variable locale ; s0 est la
position suivante de s par application du coup m ; avoir une variable locale
permet d’éviter de déjouer les coups ; pour résoudre un problème à partir d’une
position p, on appelle DLS(p,0).

1 fonction DLS ( s , d ) :
2 if solved then return ;
3 H[s] ← d ;
4 if h (best_s) > h (s) then
5 best_s ← s ;
6 if s == W IN then solved ← true ; return ;
7 if s == LOST or d == DLS_MAX_DEPTH then return ;
8 M ← nextMoves ( s ) ;
9 for each m in M do
10 s0 ← play ( m , s) ;
11 if s0 ∈/ H or H[s0 ] > d then
12 DLS ( s0 , d + 1 ) ;
13 if solved then break ;

Alg. 2: Recherche en profondeur, à profondeur limitée, avec une position


courante s en variable locale.

La suppression de l’appel à la fonction unplay (de Alg. 1 à Alg. 2) permet de


déplacer le test de terminaison prématurée de la boucle for (la ligne 14 qui est
hors du bloc commençant ligne 11 dans Alg. 1, passe dans le bloc commençant
ligne 11 dans Alg. 2).

7
Alg. 3 ajoute la gestion de la solution dans le tableau solution, qui stocke les
commandes utilisées ; le tableau est de taille DLS_MAX_DEPTH ; la solution est
de longueur solution_size ; la taille de la solution solution_size est initialisée
à zéro ; quand la recherche en profondeur trouve une solution, solution_size
prend pour valeur la taille de la solution.

1 fonction DLS ( s , d ) :
2 if solution_size 6= 0 then return ;
3 H[s] ← d ;
4 if h (best_s) > h (s) then
5 best_s ← s ;
6 if s == W IN then
7 solution_size ← d ;
8 return ;
9 if s == LOST or d == DLS_MAX_DEPTH then return ;
10 M ← nextMoves ( s ) ;
11 for each m in M do
12 s0 ← play ( m , s) ;
13 if s0 ∈/ H or H[s0 ] > d then
14 solution [ d ] ← m ;
15 DLS ( s0 , d + 1 ) ;
16 if solution_size 6= 0 then break ;

Alg. 3: Recherche en profondeur, à profondeur limitée, avec une position


courante s en variable locale et construction de la solution dans le tableau
solution.

Alg. 4 présente l’utilisation de la recherche DLS comme présentée dans Alg. 3,


pour la résolution d’un problème p limitée à la profondeur 10.

1 DLS_MAX_DEPTH ← 10 ;
2 H←∅;
3 solution_size ← 0 ;
4 best_s ← p ;
5 DLS ( p, 0 ) ;

Alg. 4: Utilisation de DLS pour la résolution d’un problème p avec une


profondeur maximale de 10.

Après terminaison de la recherche DLS, si solution_size est nulle, la recherche


est un échec ; si solution_size est non-nulle, la recherche est un succès et la
suite des coups correspondant à la solution est dans le tableau solution ; dans
tous les cas, best_s contient la position avec la meilleure évaluation h (best_s)
à la profondeur H[best_s].

8
2.2 Recherche en largeur
La recherche en largeur est en pratique également réalisée à profondeur limitée
(i.e. BFS, pour Breadth First Search) ; Alg. 5 présente une recherche BFS ; à
chaque appel, une liste de positions L devient la liste des positions suivantes S,
desquelles sont exclues les positions précédemment évaluées via H.

1 fonction BFS ( L , d ) :
2 if solution_size 6= 0 then return ;
3 S←∅;
4 for each s in L do
5 M ← nextMoves ( s ) ;
6 for each m in M do
7 s0 ← play ( m , s ) ;
8 S ← S + (s0 , m) ;
9 L0 ← ∅ ;
10 for each (s, m) in S do
11 if s ∈/ H or H[s] > d then
12 solution[d] ← m ;
13 H[s] ← d ;
14 if h (best_s) > h (s) then best_s ← s ;
15 if s == W IN then solution_size ← d ; return ;
16 if s == LOST or d == BFS_MAX_DEPTH then return ;
17 L0 ← L0 + s ;
18 BFS ( L0 , d + 1 ) ;
Alg. 5: Recherche en largeur, à profondeur limitée ; la variable
BFS_MAX_DEPTH définit la profondeur maximale.

Une recherche correspond à BFS({p},0), avec {p} une liste contenant une posi-
tion p correspondant au problème à résoudre.

Après terminaison de la recherche BFS, si solution_size est nulle, la recherche


est un échec ; si solution_size est non-nulle, la recherche est un succès et la
suite des coups correspondant à la solution est dans le tableau solution ; dans
tous les cas, best_s contient la position avec la meilleure évaluation h (best_s)
à la profondeur H[best_s].

9
2.3 Recherche à profondeur itérative
La recherche à profondeur itérative, présentée dans Alg. 6, est avant tout une
recherche en profondeur ; c’est une recherche à profondeur limitée dont on aug-
mente itérativement la profondeur limite (i.e. IDS, pour Iterative Deepening
Search 22 ) ; la recherche résultante est équivalente à la recherche en largeur, ce
qui permet d’obtenir le chemin optimal vers la solution, tout en utilisant moins
d’allocation mémoire (car n’utilisant pas de liste de positions en cours et pas de
relations entre les positions pour obtenir la solution).

1 fonction IDS ( p ) :
2 solution_size ← 0 ;
3 best_s ← p ;
4 for depth in 1 to IDS_MAX_DEPTH do
5 H←∅;
6 DLS_MAX_DEPTH ← depth ;
7 DLS ( p , 0 ) ;
8 if solution_size 6= 0 then break ;

Alg. 6: Recherche à profondeur iterative.

Cette recherche permet donc de programmer une recherche en largeur à moindre


coût (i.e. sans utiliser de liste).

22. IDS est également abrégé IDDFS pour Iterative Deepening Depth-First Search.

10
2.4 Application au Taquin
Le Taquin est un jeu à un joueur en forme de damier 23 ; il est composé de blocs
et d’une case vide (notée *) ; l’objectif est d’ordonner les blocs par glissement à
la place de la case vide ; ci-dessous un Taquin 3x3 résolu :
1 2 3
4 5 6
7 8 *
Pour un problème donné, il n’existe pas toujours de solution (i.e. certaines solu-
tions ne sont pas atteignables sans démonter le jeu) ; pour savoir si un problème
possède une solution, il est possible de compter le nombre de valeurs mal pla-
cées après chaque bloc en suivant le parcours théorique possible de la case vide
(ce parcours est défini par les cases 1,2,3,6,5,4,7,8,* dans le Taquin résolu) ; si
la somme est paire, une solution existe ; ci-dessous un exemple de problème de
Taquin 3x3 :
1 2 3
4 * 5
6 7 8
Dans le problème ci-dessus, en suivant le parcours théorique de la case vide,
on obtient la séquence 1,2,3,5,*,4,6,7,8 ; on a des erreurs de placement pour les
valeurs 5 et *, qui possèdent respectivement une (le 4) et quatre (le 4,6,7 et 8)
valeurs mal placées ; ce qui fait 5 valeurs mal placées ; 5 étant impair, ce pro-
blème n’a pas de solution.

Le fichier mytaq.h contient la représentation du plateau et ses manipulations


(mouvements de blocs, jouer un coup, déjouer un coup, jouer aléatoirement) :
1 #ifndef MYTAQ_H
2 #define MYTAQ_H
3 #include <cstdio>
4 #include <cstdlib>
5 #include <string.h>
6 #include <string>
7 #define NBL 3
8 #define NBC 3
9 int board[NBL][NBC];
10 #define MOVE_U 0
11 #define MOVE_D 1
12 #define MOVE_L 2
13 #define MOVE_R 3
14 void init_board() {
15 for(int i = 0; i < NBL; i++)
16 for(int j = 0; j < NBC; j++)
17 board[i][j] = i*NBC+j+1;
18 }

23. La taille de l’espace d’états d’un Taquin à n cases semble être égale à n!/2 ; la difficulté
(disons trois étoiles) du Taquin vient : 1) de la taille de l’espace d’états ; 2) des cycles possibles ;
3) et de la possibilité d’avoir des transpositions d’une position à des hauteurs différentes.

11
19 void print_board() {
20 printf("%d %d ", NBL, NBC);
21 for(int i = 0; i < NBL; i++)
22 for(int j = 0; j < NBC; j++) {
23 if(board[i][j] == NBL*NBC) printf("* ");
24 else printf("%d ", board[i][j]);
25 }
26 printf("\n");
27 }
28 bool can_move_U(int _i, int _j) {
29 if(_i <= 0) return false; else return true;
30 }
31 void move_U(int _i, int _j) {
32 board[_i][_j]=board[_i-1][_j]; board[_i-1][_j]=NBL*NBC;
33 }
34 bool can_move_D(int _i, int _j) {
35 if(_i >= (NBL-1)) return false; else return true;
36 }
37 void move_D(int _i, int _j) {
38 board[_i][_j]=board[_i+1][_j]; board[_i+1][_j]=NBL*NBC;
39 }
40 bool can_move_L(int _i, int _j) {
41 if(_j<=0) return false; else return true;
42 }
43 void move_L(int _i, int _j) {
44 board[_i][_j]=board[_i][_j-1]; board[_i][_j-1]=NBL*NBC;
45 }
46 bool can_move_R(int _i, int _j) {
47 if(_j>=NBC-1) return false; else return true;
48 }
49 void move_R(int _i, int _j) {
50 board[_i][_j]=board[_i][_j+1]; board[_i][_j+1]=NBL*NBC;
51 }
52 void set_next(int *_moves, int& _size, int& _line, int& _col) {
53 for(int i = 0; i < NBL; i++)
54 for(int j = 0; j < NBC; j++) {
55 if(board[i][j] == NBL*NBC) {
56 _line = i; _col = j; break;
57 }
58 }
59 _size = 0;
60 if(can_move_U(_line, _col)) { _moves[_size]=MOVE_U; _size++; }
61 if(can_move_D(_line, _col)) { _moves[_size]=MOVE_D; _size++; }
62 if(can_move_L(_line, _col)) { _moves[_size]=MOVE_L; _size++; }
63 if(can_move_R(_line, _col)) { _moves[_size]=MOVE_R; _size++; }
64 }

12
61 void play(int _move, int _line, int _col) {
62 if(_move == MOVE_U) move_U(_line, _col);
63 if(_move == MOVE_D) move_D(_line, _col);
64 if(_move == MOVE_L) move_L(_line, _col);
65 if(_move == MOVE_R) move_R(_line, _col);
66 }
67 void unplay(int _move, int _line, int _col) {
68 if(_move == MOVE_U) move_D(_line-1, _col);
69 if(_move == MOVE_D) move_U(_line+1, _col);
70 if(_move == MOVE_L) move_R(_line, _col-1);
71 if(_move == MOVE_R) move_L(_line, _col+1);
72 }
73 int rand_move() {
74 int next_moves[4];
75 int next_size;
76 int next_i_line;
77 int next_i_col;
78 set_next(next_moves, next_size, next_i_line, next_i_col);
79 int r = ((int)rand())%next_size;
80 play(next_moves[r], next_i_line, next_i_col);
81 return next_moves[r];
82 }
83 bool final_position() {
84 for(int i = 0; i < NBL; i++)
85 for(int j = 0; j < NBC; j++)
86 if(board[i][j] != i*NBC+j+1) return false;
87 return true;
88 }
89 std::string mkH() {
90 char strh[1024];
91 strh[0] = ’\0’;
92 for(int i = 0; i < NBL; i++)
93 for(int j = 0; j < NBC; j++) {
94 char stre[16];
95 sprintf(stre, "%d-", board[i][j]);
96 strcat(strh, stre);
97 }
98 return std::string(strh);
99 }
100 #endif

13
Le programme suivant présente la résolution DLS du taquin 3x3 :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <unordered_map>
4 #include "mytaq.h"
5 std::unordered_map<std::string, int> H;
6 int DLS_MAX_DEPTH = 30;
7 int* sol;
8 int sol_size;
9 void dls_solve(std::string _strboard, int _depth) {
10 if(sol_size != 0) return;
11 H[_strboard] = _depth;
12 if(final_position()) {
13 sol_size = _depth;
14 return;
15 }
16 if(_depth == DLS_MAX_DEPTH) return;
17 int next_moves[4];
18 int next_size = 0;
19 int next_start_line = 0;
20 int next_start_col = 0;
21 set_next(next_moves, next_size, next_start_line, next_start_col);
22 for(int j = 0; j < next_size; j++) {
23 play(next_moves[j], next_start_line, next_start_col);
24 std::string new_strboard = mkH();
25 std::unordered_map<std::string, int>::iterator ii = H.find(new_strboard);
26 if(ii == H.end() || ii->second > _depth) {
27 sol[_depth] = next_moves[j];
28 dls_solve(new_strboard, _depth+1);
29 }
30 unplay(next_moves[j], next_start_line, next_start_col);
31 if(sol_size != 0) break;
32 }
33 }
34 int main(int _ac, char** _av) {
35 sol_size = 0;
36 sol = new int[DLS_MAX_DEPTH];
37 board[0][0] = 1; board[0][1] = 2; board[0][2] = 3;
38 board[1][0] = 4; board[1][1] = 9; board[1][2] = 5;
39 board[2][0] = 6; board[2][1] = 7; board[2][2] = 8;
40 std::string strboard = mkH();
41 dls_solve(strboard, 0);
42 if(sol_size != 0) {
43 for(int i = 0; i < sol_size; i++) printf("%d ", sol[i]);
44 printf("\n");
45 } else { printf("-1\n"); }
46 H.clear();
47 return 0;
48 }

14
La solution retournée par le programme réalisant une recherche DLS est :
0 2 1 1 3 0 0 2 1 3 0 2 1 3 0 3 1 1 2 0 3 1 2 0 3 0 2 1 3 1

Ce qui correspond à la séquence suivante :


123 1*3 *13 413 413 413 413 4*3 *43
4*5 -> 425 -> 425 -> *25 -> 625 -> 625 -> 6*5 -> 615 -> 615
678 678 678 678 *78 7*8 728 728 728

643 643 6*3 *63 163 163 1*3 13* 135


*15 -> 1*5 -> 145 -> 145 -> *45 -> 4*5 -> 465 -> 465 -> 46*
728 728 728 728 728 728 728 728 728

135 135 135 135 135 135 135 135 13*


468 -> 468 -> 4*8 -> 48* -> 482 -> 482 -> 4*2 -> 42* -> 425
72* 7*2 762 762 76* 7*6 786 786 786

1*3 123 123 123


425 -> 4*5 -> 45* -> 456
786 786 786 78*

Pour réaliser une recherche itérative en profondeur, le main programme pré-


cédent devient :
1 int IDS_MAX_DEPTH = 30;
2 void ids_solve() {
3 sol_size = 0;
4 std::string strboard = mkH();
5 for(int k = 1; k < IDS_MAX_DEPTH; k++) {
6 H.clear();
7 DLS_MAX_DEPTH = k;
8 dls_solve(strboard, 0);
9 if(sol_size != 0) break;
10 }
11 }
12 int main(int _ac, char** _av) {
13 sol_size = 0;
14 sol = new int[IDS_MAX_DEPTH];
15 board[0][0] = 1; board[0][1] = 2; board[0][2] = 3;
16 board[1][0] = 4; board[1][1] = 9; board[1][2] = 5;
17 board[2][0] = 6; board[2][1] = 7; board[2][2] = 8;
18 ids_solve();
19 if(sol_size != 0) {
20 for(int i = 0; i < sol_size; i++)
21 printf("%d ", sol[i]);
22 printf("\n");
23 }
24 delete[] sol;
25 return 0;
26 }

15
La solution retournée par le programme réalisant une recherche IDS est :
3 1 2 2 0 3 1 3 0 2 2 1 3 3

Ce qui correspond à la séquence suivante :


123 123 123 123 123 123 123 123
4*5 -> 45* -> 458 -> 458 -> 458 -> *58 -> 5*8 -> 568 ->
678 678 67* 6*7 *67 467 467 4*7

123 123 123 123 123 123 123


568 -> 56* -> 5*6 -> *56 -> 456 -> 456 -> 456
47* 478 478 478 *78 7*8 78*

On note que la solution retournée par IDS est plus courte que la solution re-
tournée par DLS.

Pour étudier expérimentalement la complexité du problème, on doit générer


aléatoirement des problèmes de différentes tailles ; une solution simple consiste
à partir de la solution et à appliquer un nombre aléatoire de déplacements
(comme présenté dans la fonction shuffle_board).

void shuffle_board(int _n) {


init_board();
for(int i = 0; i < _n; i++) rand_move();
}
Ce qui permet d’obtenir le générateur de problèmes suivant (associé à une table
de hashage pour assurer la génération de n problèmes distincts entre eux et de
la position finale) :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <string.h>
4 #include <string>
5 #include <unordered_map>
6 #include "mytaq.h"
7 std::unordered_map<std::string, int> H;
8 std::string mkH() { /* ... */ }
9 bool final_position() { /* ... */ }
10 void shuffle_board(int _n) { /* ... */ }

La solution qui consiterait à générer aléatoirement des problèmes et à valider


chacun de ces problèmes en vérifiant la parité du nombre de valeurs mal placées
(comme défini précédemment) ne semble pas intuitivement bonne ; les raisons
sont que :
— générer des problèmes sans solution est une perte de temps.
— le nombre de problèmes correspondant à des positions purement aléa-
toires est égal à n!, dont la moitié est sans solution 24 .

24. Dans un Taquin 3x3, on a 362880 positions, dont 181440 positions sans solution.

16
11 void generate(int _srand, int _n, int _nb_ite) {
12 srand(_srand);
13 board = new int[nbl*nbc];
14 for(int i = 0; i < _n; i++) {
15 shuffle_board(_nb_ite);
16 std::string strboard = mkH();
17 std::unordered_map<std::string, int>::iterator ii = H.find(strboard);
18 if(ii == H.end() && !final_position()) {
19 H.insert(std::unordered_map<std::string, int>::value_type(strboard, 1));
20 print_board();
21 }
22 }
23 delete[] board;
24 }
25 int main(int _ac, char** _av) {
26 nbl = 3; nbc = 3;
27 generate(1, 4, 100);
28 return 0;
29 }

Chaque problème est généré en 100 itérations ; en redirigeant la sortie dans


un fichier, on obtient le fichier suivant :
3 3 2 3 4 1 * 7 5 8 6
3 3 4 1 * 7 5 8 3 6 2
3 3 3 8 1 2 * 5 7 6 4
3 3 * 4 6 3 5 2 1 7 8

Pour 100 problèmes 3x3, la taille moyenne des solutions est de 18, avec un
écart-type de 5 ; en utilisant une recherche IDS qui utilise DLS, on obtient un
temps de calcul moyen de 0.77sec avec un écart-type inférieur à 1.2sec ; en utili-
sant une recherche IDS qui utilise DLSbis , on obtient un temps de calcul moyen
de 0.38sec avec un écart-type inférieur à 0.3sec (calculs réalisés sur i7-6820HQ
CPU @ 2.70GHz).

17
2.5 Application au Solitaire
Le solitaire est un jeu à un joueur dont la forme diffère selon la variante et
la connexité du plateau ; dans le cas de la variante anglaise, c’est un plateau
4-connexe en forme de croix, sur lequel les pions se déplacent par saut au dessus
d’un pion ; sauter au dessus d’un pion, vers une position vide, supprime le pion
sauté du plateau ; l’objectif est de finir avec un seul pion sur le plateau et selon
les variantes, sur une position imposée ou libre ; ci dessous, avec des pions « o »,
les positions finales « $ » et les positions vides « + », un cas 4-connexe et un cas
3-connexe (avec la position initiale au dessus et la position finale en dessous 25 ) :
o o o + + +
o o o + + + + $
o o o o o o o + + + + + + + o o + +
o o o + o o o + + + $ + + + o o o + + +
o o o o o o o + + + + + + + o o o o + + + +
o o o + + + o o o o o + + + + +
o o o + + +

A la différence du Taquin, ce problème est sans cycle ; cependant l’exemple si


dessous (avec des pions venant de se déplacer notés « * ») montre que des trans-
positions semblent possibles 26 :
+ o * * + + + + + + + +
-> o + + -> o + + -> + + + -> + + +
+ o + + o + * o + + + *
+ o +
o + o
+ o o
+ o + * o + + + * + + +
-> o + o -> + + o -> + + o -> + + +
* + + + + + + + + + + *

Plus simplement, les transpositions apparaissent quand deux mouvements (ou


plus) sont possibles à partir d’une position ; l’ordre, dans lequel on exécute ces
mouvements, crée deux chemins menant à la même position, comme ci-dessous :
+ + * + * o
-> + o + -> + + +
+ o + + + +
+ + +
+ o o
+ o o
+ * + + o *
-> + + o -> + + +
+ + o + + +

25. Ici, les positions finales sont initialement des positions sans pion mais ce n’est pas tou-
jours le cas.
26. L’identification de transpositions dans un problème peut se résumer à l’identification de
chemins différents menant à une position identique en partant d’une position commune.

18
Le fichier mypeg.h contient la représentation du plateau et ses manipulations
(jouer un coup, jouer aléatoirement) :
1 #ifndef MYPEG_H
2 #define MYPEG_H
3 #include <cstdio>
4 #include <cstdlib>
5 #include <vector>
6

7 #define OUT_OF_BOARD 0
8 #define FREE_POS 1
9 #define PIECE_POS 2
10

11 #define MOVE_U 0
12 #define MOVE_D 1
13 #define MOVE_L 2
14 #define MOVE_R 3
15

16 char cmove[] = {’U’, ’D’, ’L’, ’R’};


17 char cpos[] = {’.’, ’+’, ’o’};
18

19 struct peg_t {
20 int board[49];
21 int nbl;
22 int nbc;
23 std::vector<int> move_pos;
24 std::vector<int> move_dir;
25

26 void init(int _nbl, int _nbc) {


27 nbl = _nbl; nbc = _nbc;
28 for(int i = 0; i < nbl*nbc; i++) board[i] = OUT_OF_BOARD;
29 move_pos.reserve(10);
30 move_dir.reserve(10);
31 }
32 void add_move(int _pos, int _dir) {
33 move_pos.push_back(_pos);
34 move_dir.push_back(_dir);
35 }
36 void copy(peg_t _b) {
37 if(nbl != _b.nbl || nbc != _b.nbc) return;
38 for(int i = 0; i < nbl*nbc; i++) board[i] = _b.board[i];
39 for(int i = 0; i < (int)_b.move_pos.size(); i++)
40 add_move(_b.move_pos[i], _b.move_dir[i]);
41 }

19
42 void load(char* _file) {
43 FILE *fp = fopen(_file, "r");
44 char *line = NULL;
45 size_t len = 0;
46 ssize_t nread;
47 if (fp == NULL) { perror("fopen"); exit(EXIT_FAILURE); }
48 int nbline = 0;
49 while ((nread = getline(&line, &len, fp)) != -1) {
50 if(((int)nread) == (nbc+1)) {
51 for(int i = 0; i < nbc; i++) {
52 if(line[i] == cpos[FREE_POS]) board[nbline*nbc+i] = FREE_POS;
53 if(line[i] == cpos[PIECE_POS]) board[nbline*nbc+i] = PIECE_POS;
54 }
55 nbline++;
56 }
57 if(nbline == nbl) break;
58 }
59 free(line); fclose(fp); init_moves();
60 }
61 std::string mkH() {
62 char strh[1024];
63 int strh_size = 0;
64 for(int i = 0; i < nbl; i++)
65 for(int j = 0; j < nbc; j++) {
66 if(board[i*nbc+j] == OUT_OF_BOARD) { /* nop */ }
67 else if(board[i*nbc+j] == FREE_POS) { strh[strh_size] = ’+’; strh_size++; }
68 else { strh[strh_size] = ’o’; strh_size++; }
69 }
70 strh[strh_size] = ’\0’;
71 return std::string(strh);
72 }
73 bool can_move_U(int _p) {
74 if(_p >= nbl*nbc || _p < 2*nbc) return false;
75 if(board[_p] != PIECE_POS) return false;
76 if(board[_p-nbc] != PIECE_POS) return false;
77 if(board[_p-2*nbc] != FREE_POS) return false;
78 return true;
79 }
80 void move_U(int _p) {
81 board[_p]=FREE_POS; board[_p-nbc]=FREE_POS; board[_p-2*nbc]=PIECE_POS;
82 }
83 bool can_move_D(int _p) {
84 if(_p < 0 || _p > ((nbl*nbc)-2*nbc)) return false;
85 if(board[_p] != PIECE_POS) return false;
86 if(board[_p+nbc] != PIECE_POS) return false;
87 if(board[_p+2*nbc] != FREE_POS) return false;
88 return true;
89 }
90 void move_D(int _p) {
91 board[_p]=FREE_POS; board[_p+nbc]=FREE_POS; board[_p+2*nbc]=PIECE_POS;
92 }
20
93 bool can_move_L(int _p) {
94 if(_p < 0 || _p >= nbl*nbc || (_p%nbc) <= 1) return false;
95 if(board[_p] != PIECE_POS) return false;
96 if(board[_p-1] != PIECE_POS) return false;
97 if(board[_p-2] != FREE_POS) return false;
98 return true;
99 }
100 void move_L(int _p) {
101 board[_p]=FREE_POS; board[_p-1]=FREE_POS; board[_p-2]=PIECE_POS;
102 }
103 bool can_move_R(int _p) {
104 if(_p < 0 || _p >= nbl*nbc || (_p%nbc) >= (nbc-2)) return false;
105 if(board[_p] != PIECE_POS) return false;
106 if(board[_p+1] != PIECE_POS) return false;
107 if(board[_p+2] != FREE_POS) return false;
108 return true;
109 }
110 void move_R(int _p) {
111 board[_p]=FREE_POS; board[_p+1]=FREE_POS; board[_p+2]=PIECE_POS;
112 }
113 void try_add_move_from(int _p) {
114 if(can_move_U(_p)) add_move(_p, MOVE_U);
115 if(can_move_D(_p)) add_move(_p, MOVE_D);
116 if(can_move_L(_p)) add_move(_p, MOVE_L);
117 if(can_move_R(_p)) add_move(_p, MOVE_R);
118 }
119 void try_add_move_to(int _p) {
120 if(can_move_U(_p+2*nbc)) add_move(_p+2*nbc, MOVE_U);
121 if(can_move_D(_p-2*nbc)) add_move(_p-2*nbc, MOVE_D);
122 if(can_move_L(_p+2)) add_move(_p+2, MOVE_L);
123 if(can_move_R(_p-2)) add_move(_p-2, MOVE_R);
124 }
125 void init_moves(){
126 for(int i = 0; i < nbc*nbl; i++) {
127 if(board[i] == FREE_POS) try_add_move_to(i);
128 }
129 }
130 void update_moves() {
131 move_pos.clear(); move_dir.clear(); init_moves();
132 }
133 int score() {
134 int ret = 0;
135 for(int i = 0; i < nbc*nbl; i++) if(board[i] == PIECE_POS) ret++;
136 return ret;
137 }
138 void play_move(int _pos, int _dir) {
139 if(_dir == MOVE_U) { move_U(_pos); }
140 else if(_dir == MOVE_D) { move_D(_pos); }
141 else if(_dir == MOVE_L) {move_L(_pos); }
142 else if(_dir == MOVE_R) {move_R(_pos); }
143 update_moves();
144 } 21
145 };
Le programme suivant utilise mypeg.h et résoud les problèmes de solitaire :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <string.h>
4 #include <string>
5 #include <unordered_map>
6 #include "mypeg.h"
7

8 int DLS_MAX_DEPTH = 35;


9 std::unordered_map<std::string, int> H;
10 int solution_pos[36];
11 int solution_dir[36];
12 int sol_size = 0;
13 peg_t dls_board[36];
14

15 void dls_solve(peg_t _board, std::string _strboard, int _depth) {


16 if(sol_size != 0) return;
17 H[_strboard] = _depth;
18 if(_board.score() == 1 && _board.board[24] == PIECE_POS) {
19 sol_size = _depth; return;
20 }
21 if(_depth == DLS_MAX_DEPTH) return;
22 for(int j = 0; j < (int)_board.move_pos.size(); j++) {
23 dls_board[_depth].copy(_board);
24 dls_board[_depth].play_move(_board.move_pos[j], _board.move_dir[j]);
25 std::string strboard = dls_board[_depth].mkH();
26 std::unordered_map<std::string, int>::iterator ii = H.find(strboard);
27 if(ii == H.end()) {
28 solution_pos[_depth] = _board.move_pos[j];
29 solution_dir[_depth] = _board.move_dir[j];
30 dls_solve(dls_board[_depth], strboard, _depth+1);
31 if(sol_size != 0) break;
32 }
33 }
34 }
35 int main(int _ac, char** _av) {
36 if(_ac != 2) { printf("usage: %s PEG_FILE\n", _av[0]); return 0; }
37 peg_t G;
38 G.init(7,7);
39 G.load(_av[1]);
40 for(int i = 0; i < DLS_MAX_DEPTH+1; i++) dls_board[i].init(G.nbl, G.nbc);
41 std::string strboard = G.mkH();
42 dls_solve(G, strboard, 0);
43 printf("sol_size: %d\n", sol_size);
44 for(int i = 0; i < sol_size; i++)
45 printf("%d-%c ", solution_pos[i], cmove[solution_dir[i]]);
46 printf("\n");
47 return 0;
48 }

22
Considérons le problème suivant :
1 ..ooo..
2 ..ooo..
3 ooooooo
4 ooo+ooo
5 ooooooo
6 ..ooo..
7 ..ooo..

On obtient en 3.27 sec la solution suivante :


1 sol_size: 31
2 38-U 17-D 3-D 26-L 24-U 19-L 4-D 9-R 23-U 2-D 17-R 15-R 20-L /
3 17-R 34-U 20-L 37-U 11-D 32-L 46-U 23-D 28-R 14-D 25-D 44-R /
4 46-U 33-L 31-L 28-R 37-U 22-R

2.6 Application à Sokoban

23
3 Minimax et Alpha-beta
3.1 Minimax
L’algorithme Minimax correspond à l’imbrication mutuelle d’une fonction maxi
et d’une fonction mini, en commençant par la fonction maxi ( s , 0) pour une
position courante s. Alg. 7 présente la fonction maxi. Alg. 8 présente la fonction
mini. Ces deux fonctions utilisent 3 variantes de la fonction f d’évaluation d’une
position s pour le joueur root 27 :
— f’ ( s ) la variante de f pour une position sans mouvement possible 28 .
— fmini () la variante de f retournant la valeur minimale de f.
— fmaxi () la variante de f retournant la valeur maximale de f.
Les fonctions mini et maxi retournent le meilleur coup (i.e. best_m) et la
meilleure évaluation (i.e. best_v).

1 fonction maxi ( s , d ) :
2 if d == MINIMAX_MAX_DEPTH then return { ∅ , f ( s ) } ;
3 M ← nextMoves ( s ) ;
4 if size ( M ) == 0 then return { ∅ , f’ ( s ) } ;
5 { best_m , best_v } ← { ∅ , fmini ( ) −1 } ;
6 for each m in M do
7 s0 ← applyMove ( s , m ) ;
8 v ← mini ( s0 , d + 1) ;
9 if v > best_v then
10 { best_m , best_v } ← { m , v } ;
11 return { best_m , best_v } ;
Alg. 7: Fonction maxi pour l’évaluation d’une position s.

1 fonction mini ( s , d ) :
2 if d == MINIMAX_MAX_DEPTH then return { ∅ , f ( s ) } ;
3 M ← nextMoves ( s ) ;
4 if size ( M ) == 0 then return { ∅ , f’ ( s ) } ;
5 { best_m , best_v } ← { ∅ , fmaxi ( ) +1 } ;
6 for each m in M do
7 s0 ← applyMove ( s , m ) ;
8 v ← maxi ( s0 , d + 1) ;
9 if v < best_v then
10 { best_m , best_v } ← { m , v } ;
11 return { best_m , best_v } ;
Alg. 8: Fonction mini pour l’évaluation d’une position s.

27. Le joueur root est celui dont la position est à la racine de l’arbre, position à partir de
laquelle on cherche le meilleur coup à jouer.
28. Pour une position terminale, il est courant d’avoir des évaluations complètes, ce qui
donne lieu à une évaluation f’ différente de l’ évaluation heuristique f.

24
3.2 Negamax
Alg. 9 correspond à un parcours Negamax, basé sur l’utilisation d’une fonction
d’évaluation f dépendant du tour de jeu, retournant la valeur max d’une position
pour le joueur courant. La variation f’ reste utile.

1 if depth ( s ) == D then return { ∅ , f ( s ) } ;


2 M ← nextMoves ( s ) ;
3 if size ( M ) == 0 then return { ∅ , f’ ( s ) } ;
4 mval ← −∞ ; best ← ∅ ;
5 for each m in M do
6 s0 ← applyMove ( s , m ) ;
7 {m0 , v} ← negamax ( s0 ) ;
8 if v > mval then { best , mval } ← { m , v } ;
9 return { best , mval } ;
Alg. 9: Fonction negamax pour l’évaluation d’une position s.

Avec Negamax, la fonction f retourne l’évaluation pour le joueur courant associé


à l’état s.

3.3 Alpha-beta (αβ)


Alg. 10 correspond à un parcours αβ, autrement dit un parcours Negamax
auquel sont ajoutées des coupes α et des coupes β. Les coupes α sont appliquées
aux fils d’un noeud min s par comparaison avec la valeur min d’un noeud voisin
de s complètement évalué et les coupes β sont appliquées aux fils d’un noeud
max s par comparaison avec la valeur max d’un noeud voisin de s complètement
évalué. Une valeur min en cours inférieure à une valeur min voisine, n’influencera
pas le max d’un noeud parent (et respectivement inversement pour les max vis
à vis d’un min d’un noeud parent).

1 if depth ( s ) == D then return { ∅ , f ( s ) } ;


2 M ← nextMoves ( s ) ;
3 if size ( M ) == 0 then return { ∅ , f’ ( s ) } ;
4 { best , mval } ← { ∅ , ∅ } ;
5 for each m in M do
6 s0 ← applyMove ( s , m ) ;
7 v ← αβ ( s0 , a, b ) ;
8 if best == ∅ then { best , mval } ← { m , v } ;
9 else
10 if v > mval then { best , mval } ← { m , v } ;
11 if cut ( depth ( q ) , mval , a, b ) break ;
12 return { best , mval } ;
Alg. 10: Fonction αβ pour l’évaluation d’une position s. L’appel corres-
pondant est αβ ( s , a , b ). La fonction cut décide des coupes selon les
valeurs courantes et en fonction de la profondeur.

25
Alg. 11 présente une autre possibilité de parcours αβ, utilisant une variable lo-
cale AB représentant les valeurs des coupes α et β. En fonction de la profondeur,
les valeurs seront des coupes α ou des coupes β.

1 if depth ( s ) == D then return { ∅ , f ( s ) } ;


2 M ← nextMoves ( s ) ;
3 if size ( M ) == 0 then return { ∅ , f’ ( s ) } ;
4 { best , mval } ← { ∅ , ∅ } ;
5 for each m in M do
6 s0 ← applyMove ( s , m ) ;
7 v ← αβ ( s0 ) ;
8 if best == ∅ then
9 { best , mval } ← { m , v } ;
10 AB ← v ;
11 else
12 if v > mval then { best , mval } ← { m , v } ;
13 if cut ( depth ( s ) , v , AB ) break ;
14 return { best , mval } ;
Alg. 11: Fonction αβ pour l’évaluation d’une position s. L’appel corres-
pondant est αβ ( s ). La valeur AB mémorise la première valeur d’éva-
luation αβ des noeuds voisins. La fonction cut décide des coupes selon
la profondeur, les valeurs courantes et la valeur AB mémorisée.

3.4 Minimax Tree Optimization (MMTO)


3.5 Application aux arbres ternaires
Lire le programme suivant et :
— Dessiner l’arbre construit par ce programme (i.e. écrire le contenu de
mydata).
— Ecrire la fonction Minimax pour la structure S_t.
— Ecrire la fonction Negamax pour la structure S_t.
— Ecrire la fonction αβ pour la structure S_t.
— Tester ces fonctions sur l’instance T présentée dans le main (et constater
une coupe α).
— Proposer une instance T2 permettant de présenter une coupe β.
— Ecrire une fonction de génération pour S_t et donner une approximation
expérimentale du gain moyen d’un parcours αβ par rapport à un parcours
Minimax.

26
#i n c l u d e <c s t d l i b >
#i n c l u d e <c s t d i o >
#i n c l u d e < s t r i n g . h>
#i n c l u d e <deque>

s t r u c t S_t {
u n s i g n e d i n t myseed ;
u n s i g n e d i n t myiseed ;
i n t ∗ mydata = 0 ;
i n t myalloc = 0 ;
i n t mysize = 0 ;
i n t mydepth = 0 ;

// t r e e with f i x e d b r a n c h i n g f a c t o r
// o r d e r i n g data i n a s i n g l e s t r u c t u r e
// [ . . . , e v a l , p a r e n t , c h i l d r e n 0 , c h i l d r e n 1 , c h i l d r e n 2 , ... ]

// ID∗5 = e v a l o f ID node
// ID∗5+1 = p a r e n t i d o f ID node
// ID∗5+2 = c h i l d r e n 0 i d o f ID node
// ID∗5+3 = c h i l d r e n 1 i d o f ID node
// ID∗5+4 = c h i l d r e n 2 i d o f ID node
i n t myget ( i n t i , i n t d ) { r e t u r n mydata [ ( i ∗5)+d ] ; }
v o i d myset ( i n t i , i n t d , i n t v ) { mydata [ ( i ∗5)+d]=v ; }
i n t g e t _ v a l ( i n t i ) { r e t u r n myget ( i , 0 ) ; }
i n t g e t _ p a r e n t ( i n t i ) { r e t u r n myget ( i , 1 ) ; }
i n t g e t _ c h i l d r e n ( i n t i , i n t c ) { r e t u r n myget ( i , 2+c ) ; }
v o i d s e t _ v a l ( i n t i , i n t v ) { myset ( i , 0 , v ) ; }
v o i d s e t _ p a r e n t ( i n t i , i n t p ) { myset ( i , 1 , p ) ; }
v o i d s e t _ c h i l d r e n ( i n t i , i n t c i , i n t cv ) { myset ( i , 2+c i , cv ) ; }

i n t get_depth ( i n t i ) {
i n t p = g e t _ p a r e n t ( i ) ; i f ( p == −1) r e t u r n 0 ; r e t u r n 1+get_depth ( p ) ;
}
v o i d i n i t ( u n s i g n e d i n t _seed , i n t _max_nb_nodes ) {
myseed = _seed ; myiseed = _seed ;
m y a l l o c = _max_nb_nodes ;
i f ( m y a l l o c != 0 ) d e l e t e [ ] mydata ;
mydata = new i n t [ m y a l l o c ∗ 5 ] ;
memset ( mydata , −1, m y a l l o c ∗5∗ s i z e o f ( i n t ) ) ;
}
void destroy ( ) {
i f ( m y a l l o c != 0 ) { d e l e t e [ ] mydata ; m y a l l o c = 0 ; }
}
i n t a d d _ c h i l d r e n ( i n t _node , i n t _ c h i l d r e n _ i d ) {
i n t r e t = mysize ;
s e t _ c h i l d r e n ( _node , _ chi ld ren _i d , m y s i z e ) ;
s e t _ p a r e n t ( mysize , _node ) ;
m y s i z e ++;
return ret ;
}
v o i d p r i n t ( FILE∗ _fp ) {
f p r i n t f ( _fp , "# b f s s i z e %d depth %d\n " , mysize , mydepth ) ;
f p r i n t f ( _fp , "# ID VAL PARENT C0 C1 C3\n " ) ;
f o r ( i n t i = 0 ; i < m y s i z e ; i ++) {
f p r i n t f ( _fp , "%d %d %d " , i , g e t _ v a l ( i ) , g e t _ p a r e n t ( i ) ) ;
f o r ( i n t j = 0 ; j < 3 ; j ++) f p r i n t f ( _fp , " %d " , g e t _ c h i l d r e n ( i , j ) ) ;
f p r i n t f ( _fp , "\n " ) ;
}
}
};

27
/∗ g++ −s t d=c++11 minimax . cpp
∗/
i n t main ( i n t _ac , c h a r ∗∗ _av ) {
S_t T ; T . i n i t ( 1 , 1 0 ) ;
T. mysize = 1 ;
T. add_children (0 , 0 ) ; T. add_children (0 , 1 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 1 , 0 ) , 1 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 1 , 1 ) , 2 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 2 , 0 ) , 0 ) ;
T. add_children (2 , 1 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 6 , 0 ) , 1 0 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 6 , 1 ) , 1 1 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 6 , 2 ) , 1 2 ) ;
T. p r i n t ( stdout ) ;
T. destroy ( ) ;
return 0;
}

3.6 Application aux Dames


3.7 Application aux Echecs
3.8 Application à Breakthrough
3.9 Application à Kamisado

28
4 Monte-Carlo et Monte-Carlo Tree Search
Le principe de Monte-Carlo (MC) est de réaliser des moyennes à partir de plu-
sieurs parties ; dans le cas d’un jeu, les parties aléatoires sont appelées playout
ou rollout.

Pour un problème dans une position ou d’un état s, on peut réaliser cette évalua-
tion avec N itérations ; Alg. 12 présente la solution avec N itérations et Alg. 12
présente la solution avec calcul de l’écart-type.

1 sum ← 0 ;
2 for N times do
3 r ← playout ( s ) ;
4 sum ← rq + sum ;
5 rq ← sum/N ;
Alg. 12: Evaluation MC après N itérations.

1 sum ← 0 ;
2 sum2 ← 0 ;
3 for N times do
4 r ← playout ( p ) ;
5 sum ← r + sum ;
6 sum2 ← (r ∗ r) + sum2 ;
7 rp ← sum/N ;
p
8 σrp ← sum2/N − (rp ∗ rp ) ;
Alg. 13: Evaluation MC avec écart-type.

29
L’utilisation de MC pour évaluer une décision (i.e. la possibilité de choisir entre
plusieurs actions) implique dans le cas d’un jeu, de savoir jouer un coup aléa-
toirement, à partir d’une position, détecter la fin de partie, décider si une
position de fin de partie est gagnée et implicitement d’être capable de jouer
des parties aléatoires rapidemment pour établir des taux de réussite.

1 M ← nextMoves ( s ) ;
2 if size ( M ) == 0 then return P ASS ;
3 best ← first ( M ) ; max ← 0 ;
4 for each m in M do
5 s0 ← applyMove ( s , m ) ;
6 wi ← 0 ;
7 for N times do
8 r ← playout ( s0 ) ;
9 if r == W IN then wi ← wi + 1 ;
10 if wi > max then { best , max } ← { m , wi } ;
11 return best ;
Alg. 14: Programme MC pour l’évaluation d’un état s.

1 while not terminal ( s ) do


2 M ← nextMoves ( s ) ;
3 m ← random ( M ) ;
4 s ← applyMove ( s , m ) ;
5 return score ( s ) ;
Alg. 15: Fonction playout pour jouer des parties aléatoires à partir d’un
état s.

Le principe d’un programme MC (présenté dans Alg.14) est de jouer N parties


aléatoires pour chaque position suivante de s. wi est le nombre de parties gagnées
obtenues avec N parties aléatoires en partant d’une position suivante de s. Le
meilleur coup à partir de la position s est le coup menant à la position suivante
ayant le meilleur score wi .
Alg.15 présente le fonctionnement d’un playout ; cette fonction retourne le score
de l’état terminal s0 obtenu en fin de partie.

30
4.1 Monte Carlo Tree Search (MCTS )
La figure 2 29 présente les 4 étapes de construction d’une recherche MCTS. Ces 4
étapes appliquées itérativement permettent de construire un arbre de décision à
l’aide de statistiques établis itérativement ; au fil des itérations, les descentes sé-
lectionnent les meilleurs noeuds de l’arbre, les expansions ajoutent de nouveaux
noeuds dans l’arbre, les playouts ajoutent des résultats et les rétropropagations
permettent de prendre en compte les résultats dans les noeuds parent.

Fig. 2 – 4 étapes de construction d’une recherche MCTS.

1 while not-interrupted do
2 s0 ← selection ( s ) ;
3 s00 ← expansion ( s0 ) ;
4 r ← playout ( s00 ) ;
5 backpropagate ( s00 , r ) ;
6 return bestNext ( s ) ;
Alg. 16: MCTS pour l’évaluation d’un état s.

Alg. 16 présente l’algorithme d’une recherche MCTS, dont l’avantage est de ne


pas avoir besoin d’une fonction d’évaluation en cours de partie mais uniquement
de savoir jouer aléatoirement et d’évaluer une fin de partie ; l’évaluation d’une fin
de partie est réalisée par la fonction score (Alg. 15 ligne 7) ; la correspondance
de Alg. 16 avec les 4 étapes présentées dans la figure 2 est la suivante :
— La fonction selection (Alg. 16 ligne 2) correspond à l’étape de sélec-
tion d’un nœud s0 de l’arbre en suivant la politique appelée Tree Po-
licy ; cette étape de descente dans l’arbre est guidée par une évalua-
tion statistique des nœuds, évaluation permettant de sélectionner un
nœud i pour laquelle
p il est courant d’utiliser la formule U CT définie
par (Wi /Ni ) + K log(N )/Ni avec Wi le nombre de victoires au noeud i,
Ni le nombre de playouts au noeud i, K la constante U CT souvent fixée
29. Figure extraite de A Survey of Monte Carlo Tree Search Methods par C. Browne et al
(IEEE Trans. On Computational Intelligence and AI in Games, Vol. 4, No. 1, 2012).

31
à proximité de 0.4, N le nombre de playouts réalisés au nœud parent ;
Alg. 17 présente cette fonction en détails.
— La fonction expansion (Alg. 16 ligne 3) corresponds à l’étape d’expansion
de s0 en s00 ; Alg. 18 présente cette fonction en détails.
— La fonction playout (Alg. 16 ligne 4) correspond à l’étape de simulation ;
cette fonction est identique à la fonction de playout d’une recherche MC.
— La fonction backpropagate (Alg. 16 ligne 5) correspond à l’étape de
rétropropagation des scores dans l’arbre ; Alg. 19 présente cette fonction
en détails.

Cet algorithme est dit anytime dans le sens ou il est interruptible à tout ins-
tant pour nous retourner son meilleur résultat courant ; c’est un algorithme qui
construit itérativement un arbre à partir de playouts ; l’interruption peut être
en nombre d’itérations ou en temps ou réalisée par la découverte d’une solution.

1 N ← getN ( s ) ;
2 if N == 0 then return s ;
3 M ← nextMoves ( s ) ;
4 {max, best} ← {−1, ∅} ;
5 for each m in M do
6 N i ← getNi ( s , m ) ;
7 if N i == 0 then return s ;
8 new_eval ← uct ( s , m ) ;
9 if new_eval > max then {max, best} ← {new_eval, m} ;
10 s0 ← applyMove ( s , best ) ;
11 return selection ( s0 ) ;
Alg. 17: Fonction selection(s) de sélection à partir d’un nœud s.

1 N ← getN ( s ) ;
2 if N == 0 then makeMoves ( s ) ;
3 M ← nextMoves ( s ) ;
4 {max, best} ← {−1, ∅} ;
5 for each m in M do
6 N i ← getNi ( s , m ) ;
7 if N i == 0 then
8 s0 ← applyMove ( s , m ) ;
9 then return s0 ;

Alg. 18: Fonction expansion(s) d’expansion d’un nœud s.

32
1 if parent ( s ) == ∅ then return ;
2 addScore ( s , score ) ;
3 return backpropagate ( parent ( s ) , score ) ;
Alg. 19: Fonction backpropagate (score) de rétropropagation d’un
score dans un noeud s.

Cette solution présente un algorithme MCTS avec 1 sélection, 1 expansion,


1 simulation et 1 rétropropagation par itération. En pratique on peut utiliser
une fonction selection-expansion (présentée dans Alg. 20, dans laquelle les
lignes 2 et 7 correspondent à la partie expansion et les autres lignes à la partie
sélection) et la formule associant une valeur à un élément diffère en fonction de :
— son utilisation dans la descente (Alg. 20 ligne 6) ; cette politique de sé-
lection des coups suivants est appelée Tree Policy (alors que la sélection
des coups dans un playout est appelée Default Policy).
— son utilisation dans la définition finale du coup le plus prometteur (Alg. 16
ligne 6) ou il est courant d’utiliser Wi /Ni ;

1 N ← getN ( s ) ;
2 if N == 0 then makeMoves ( s ) ;
3 M ← nextMoves ( s ) ;
4 {max, best} ← {−1, ∅} ;
5 for each m in M do
6 N i ← getNi ( s , m ) ;
7 if N i == 0 then
8 s0 ← applyMove ( s , m ) ;
9 return s0 ;
10 new_eval ← uct ( s , m ) ;
11 if new_eval > max then {max, best} ← {new_eval, m} ;
12 s0 ← applyMove ( s , best ) ;
13 return selection-expansion ( s0 ) ;
Alg. 20: Fonction selection-expansion(s) à partir d’un nœud s.

L’utilisation d’une fonction selection-expansion modifie Alg. 16 en Alg. 21.

1 while not-interrupted do
2 s0 ← selection-expansion ( s ) ;
3 r ← playout ( s0 ) ;
4 if r == W IN then backpropagate ( s0 , r ) ;
5 return bestNext ( s ) ;
Alg. 21: MCTS pour l’évaluation d’un état s.

33
4.2 Progressive Bias (PB)
La variante appelée Progressive Bias consiste à ajouter un terme de valeur
p des coups dans la formule U CT et en
heuristique en fonction des positions
appliquant la formule (Wi /Ni )+K log(N )/Ni +Hi /(1+Ni −Wi ) dans laquelle
Hi est une valeur précalculée.

4.3 Early Playout Termination (EPT)


La variante appelée Early Playout Termination consiste à ajouter une table
précalculée permettant de prédire la fin d’un playout sans jouer entièrement ce
dernier ; les états de cette table peuvent être définis par connaissance experte du
problème ou par des précalculs ; aux échecs par exemple, on pourrait considérer
qu’avoir quatre pièces de moins que son adversaire correspond à une défaite ;
tout comme les évaluations heuristiques, la prise de décision peut utiliser le
matériel, les positions globales ou relatives entre pièces, la mobilité, le contrôle
d’une partie du plateau (i.e. le centre aux échecs), la connectivité, les pièces
capturées, les menaces de défaite, l’espace et le temps.

4.4 L’hybridation minimax de MCTS


Le principe de l’hybridation minimax consiste à réaliser des coupes quand les
victoires et les défaites sont assurées. Cette variante est appelée MCTS solver.
Il est important de différencier les fins de parties assurées avec les fins de parties
des playouts. Le principe est de réaliser des parcours en largeur à profondeur
prédéfinie et dans les cas favorables de rétropropager les informations sur les
coups permettant des victoires et des défaites assurées. Lors d’un parcours en
largeur, si le cas n’est pas favorable, la valeur retournée n’indique rien sur la
position en cours, et un coup aléatoire est utilisé. Les victoires et les défaites
assurées sont calculées pour le joueur courant. Le principe d’utilisation de noeud
minimax avec des valeurs prouvées implique des modifications selon les phases :
— La phase de descente est modifiée pour la profondeur 0, à laquelle il
est possible d’interrompre la recherche MCTS et de décider de jouer un
coup avec une victoire assurée ; les autres descentes sont interrompues
et impliquent la rétropropagation d’une victoire ou d’une défaite dans
l’arbre MCTS ; cette coupe dans la descente implique des différences
entre le nombre de playout d’un noeud et la somme des noeuds de ses
fils, modifiant ainsi l’intégrité des valeurs statistique de l’arbre.
— Lors de la phase de rétropropagation d’une victoire assurée : à profondeur
paire, une victoire assurée dans l’un des noeuds fils implique une victoire
dans le noeud courant ; à profondeur impaire, une victoire assurée dans
tous les noeuds fils implique une victoire dans le noeud courant.
— Lors de la phase de rétropropagation d’une défaite assurée : à profondeur
paire, une défaite assurée dans tous les noeuds fils implique une défaite
dans le noeud courant ; à profondeur impaire, une défaite assurée dans
l’un des noeuds fils implique une défaite dans le noeud courant.
— Lors de la sélection du meilleur coup (Alg. 16 ligne 12), les coups avec des
victoires assurées sont sélectionnés en priorité ; les coups avec des défaites
assurées sont uniquement informatifs et peuvent être sélectionnés afin de
ne pas surestimer l’adversaire.

34
Trois variantes d’hybridation sont possibles :
— La variante MCTS-MR (pour Minimax Rollout) précède les itérations du
playout par un parcours en largeur à profondeur prédéfinie. Si le parcours
en largeur détermine la valeur de la position courante, le playout est
interrompu et la valeur trouvée est rétropropagée.
— La variante MCTS-MS (pour Minimax Selection) précède l’étape de sé-
lection et d’expansion par un parcours en largeur quand le nombre de
visite d’un noeud dépasse un seuil prédéfini ; seul un seuil de 0 implique
la phase d’expansion ; l’étape de sélection et d’expansion est interompue
si le parcours en largeur permet d’évaluer la position courante.
— La variante MCTS-MB (pour Minimax Backpropagate) ajoute à l’étape
de rétropropagation une évaluation de noeuds voisins d’un noeud prouvé
perdant ou gagnant afin de rétropropager la preuve d’une victoire ou
d’une défaite dans le parent de ce noeud.

4.5 Rapid Action Value Estimation (RAVE)


L’algorithme RAVE combine le terme d’exploitation de la formule UCT avec
l’estimation heuristique AMAF ; l’estimation AMAF (pour All Move As First)
d’un coup i est définie par
(
AM AF _Wi /AM AF _Ni si AM AF _Ni > 0
AM AFi =
∞ si AM AF _Ni = 0
avec AM AF _Wi le nombre de victoires obtenues dans les séquences de coups
contenant le coup i (une séquence de coups réunit ici sélection, expansion et
playout) et AM AF _Ni le nombre de séquences de coups contenant le coup i ;
la programmation d’AMAF implique l’ajout de ces deux variables pour cha-
cun des coups i permettant de passer d’un état s vers un état s0 ; ces valeurs
sont locales à chaque état s ; pour un état s, on aura donc autant de couples
(AM AF _Ni , AM AF _Wi ) que de coups possibles ; la valeur par défaut associée
à AM AFi , ici égale à l’infini, est appelée First Play Urgency (FPU) 30 ; Alg. 22
présente RAVE en détails.

1 while not-interrupted do
2 {s0 , L1 , M1 } ← selection_RAVE ( s , ∅, ∅) ;
3 expansion_RAVE ( s0 ) ;
4 {r, L2 , M1 } ← playout_RAVE ( s0 , ∅, M1 ) ;
5 backpropagate_RAVE ( r , L1 , L2 , M1 ) ;
Alg. 22: RAVE pour l’évaluation d’un état s.

Dans le cas de l’utilisation d’une table de hashage H, on aura pour un coup


i permettant de passer de s à s0 : H[s].AM AF _W [i] (abrégé AM AF _Wi en
30. La notion de FPU est également possible avec UCT ; précédemment, nous avions pour
UCT une valeur FPU égale à l’infini ; avec une valeur par défaut de 0.5 : 1) un nouveau nœud
n’est développé que quand tous ses voisins ont des scores inférieurs à 50% de victoires ; 2) un
nœud est sélectionné sans considérer ses voisins tant que son score est supérieur à 50% de
victoires.

35
s) et H[s].AM AF _N [i] (abrégé AM AF _Ni en s), soit une table de valeurs
AM AF _W et une table de valeurs AM AF _N dans un état s, pour évaluer
les valeurs des liens vers les états suivants s0 ; de la même façon, H[s].N est
abrégé Ns et H[s].W est abrégé Ws ; la mise à jour de ces valeurs implique
la construction de trois listes : L1 la liste des états rencontrés pendant la sé-
lection et l’expansion, L2 la liste des états rencontrés pendant le playout et
M1 la liste des coups recontrés pendant la sélection, l’expansion et le playout ;
la prise en compte de coups issus de la partie playout de la descente 31 im-
plique que des états auront des valeurs AMAF positives et des valeurs N et W
nulles ; Alg. 23, 24, 25 et 26 présentent respectivement la sélection, l’expansion,
le playout et la rétropropagation de RAVE.

Après construction des listes L1 , L2 et M1 , la fonction de rétropropagation


backpropagate_RAVE met à jour les valeurs UCT des états de L1 et les valeurs
AMAF des coups de M1 qui sont également des coups dans chacun des états
de L1 et de L2 .

La combinaison entre les valeurs AMAF et UCT, pour un état s et un coup i,


est réalisée par

rave(s, i) = β ∗ AM AFi + (1 − β) ∗ Ws /Ns


p
avec β = k/(3 ∗ Ns + k), k étant une constante dont la valeur varie de 20 à
16k selon les jeux ; on pourra fixer k = Ns pour obtenir une contribution à 50%
des valeurs AMAF et UCT dans le calcul du score.

La variante appelée MC-RAVE améliore la formule de RAVE avec une meilleure


valeur de β définie par

β = AM AF _Ni /(AM AF _Ni + Ns + (C1 ∗ AM AF _Ni ∗ Ns ))

avec C1 une constante variant de 10−1 à 10−20 selon l’équilibrage recherché


entre AMAF et UCT .

1 if terminal ( s ) then return {s , L1 + s , M1 };


2 if H[s] == ∅ then return {s , L1 + s , M1 };
3 if H[s].N == 0 then return {s , L1 + s , M1 };
4 best ← firstMove ( s ) ; max ← 0 ;
5 for each m in nextMoves ( s ) do
6 eval ← rave ( s , m ) ;
7 if eval > max then { best , max } ← { m , eval } ;
8 s0 ← applyMove ( s , best ) ;
9 return selection_RAVE ( s0 , L1 + s , M1 + best) ;
Alg. 23: Fonction selection_RAVE ( s , L1 , M1 ).

31. Appelons descente la réunion des étapes de sélection, expansion et playout.

36
1 if H[s] == ∅ then
2 add ( H, s ) ;
3 {Ws , Ns } ← {0, 0} ;
4 for each m in nextMoves ( s ) do
5 add_move ( s, m ) ;

Alg. 24: Fonction expansion_RAVE d’un noeud s.

1 if terminal ( s ) then return { score ( s ) , L2 + s, M1 } ;


2 M ← nextMoves ( s ) ;
3 m ← random ( M ) ;
4 s0 ← applyMove ( s , m ) ;
5 return playout_RAVE ( s0 , L2 + s, M1 + m) ;
Alg. 25: Fonction playout_RAVE ( s , L2 , M1 ).

1 for each s in L1 do
2 H[s].N + + ;
3 if r == W IN then H[s].W + + ;
4 for each e in M1 do
5 if ∃ H[s].AM AFe then
6 H[s].AM AF _N [e] + + ;
7 if r == W IN then H[s].AM AF _W [e] + + ;

8 for each s in L2 do
9 for each e in M1 do
10 if ∃ H[s].AM AFe then
11 H[s].AM AF _N [e] + + ;
12 if r == W IN then H[s].AM AF _W [e] + + ;

Alg. 26: Fonction backpropagate_RAVE (r, L1 , L2 , M1 ).

37
4.6 Generalized RAVE (GRAVE)
L’algorithme GRAVE fixe un seuil d’utilisation des valeurs AMAF ; en dessous
de ce seuil, les valeurs AMAF d’un nœud deviennent celles du plus proche pa-
rent conformes à ce seuil ; les valeurs AMAF à prendre en compte sont évaluées
lors de la phase de sélection (Alg. 27) ; la valeur ref définit ce seuil, qui est à
comparer avec le nombre de playouts réalisés (i.e. Ns pour l’état s) ; les phases
d’expansion, de playout et de rétropropagation sont identiques à RAVE.

La fonction d’évaluation des nœuds pendant la sélection devient :

grave(sref, i, s) = β ∗ H[sref ].AM AF [i] + (1 − β) ∗ Ws /Ns

1 if terminal ( s ) then return {s, L + s};


2 M ← nextMoves ( s ) ;
3 for each m in M do
4 s0 ← applyMove ( s , m ) ;
5 if H[s0 ] == ∅ then return {s0 , L + s + s0 } ;
6 best ← first ( M ) ; max ← 0 ;
7 for each m in M do
8 s0 ← applyMove ( s , m ) ;
9 eval ← grave ( s0 , sref ) ;
10 if eval > max then { best , max } ← { s0 , eval } ;
11 if Ns > ref then sref ← s ;
12 return selection_GRAVE ( best , L + s, ref , sref ) ;
Alg. 27: Fonction selection_GRAVE ( s , L, ref , sref ).

1 while not-interrupted do
2 {s0 , L} ← selection_GRAVE ( s , ∅, ref , s) ;
3 expansion_RAVE ( s0 ) ;
4 {r, L} ← playout_RAVE ( s0 , L ) ;
5 backpropagate_RAVE ( r , L) ;
Alg. 28: GRAVE pour l’évaluation d’un état s.

Alg. 28 présente GRAVE en détails ; pour des valeurs de ref de 25 à 400, et


selon la valeur de β, GRAVE obtient contre RAVE dans des jeux à deux joueurs,
un taux de succès de 55 à 95% ;

38
4.7 MAST, TO-MAST, PAST
L’algorithme MAST (pour Move-Average Sampling Technique) identifie les coups
les plus prometteurs ; les coups sont considérés indépendamment des états, pen-
dant les phases de sélection, d’expansion et de playout ; une table en mémoire
globale stocke le nombre de playouts réalisés avec le coups i (i.e. M AST _Ni )
et le nombre de playouts ayant menés à une victoire en utilisant le coup i (i.e.
M AST _Wi ).
La fonction d’évaluation des coups pendant la sélection devient :

eM ASTi /τ
mast(i) = Pn M ASTj /τ
j=1 e

avec M ASTi = M AST _Wi /M AST _Ni et τ un paramètre permettant de


contrôler la distribution des résultats en fonction des valeurs M AST ; quand
τ tend vers zéro, les écarts entre les petites et les grandes valeurs augmentent ;
quand τ tend vers l’infini, les écarts diminuent et la distribution est uniforme,
indépendamment des résultats.

La variante TO-MAST (pour Tree-Only MAST) ne prend en compte que les


coups rencontrés pendant les phases de sélection et d’expansion.

La variante PAST (pour Predicate-Average Sampling Technique) utilise les pré-


dicats définissant chaque état pour identifier les coups les plus prometteurs ; un
seuil minimum de playouts est fixé pour prendre en compte une valeur PAST.

4.8 Playout Policy Adaptation (PPA)


4.9 Sequential Halving applied to Trees (SHOT)
4.10 Application à Nonogram
4.11 Application à Breakthrough
4.12 Application à AngryBirds

39
5 Recherche enveloppée
5.1 Nested Monte Carlo Search (NMCS)
L’algorithme NMCS est défini par une fonction à boucles for imbriquées 32 ;
une fonction nested_MC appelle récursivement la fonction nested_MC de niveau
inférieur ; au dernier niveau (ici associé à level == 0), la fonction appelle sim-
plement un playout ; entre chaque niveau, on joue un coup ; cette imbrication
est réalisée à une profondeur définie par le premier appel à nested_MC.

Prenons pour exemple une variante du problème du solitaire qui se limite l’ob-
jectif de finir avec le moins de pièces possibles ; pour certains problèmes, il sera
possible de finir avec une seule pièce ; pour d’autres problèmes, avec des formes
de plateaux plus ésothériques, il sera possible de finir avec n pièces ; tout comme
au taquin (pour lequel on générait des problèmes en mélangeant aléatoirement
le taquin en partant d’un taquin résolu), on peut générer des problèmes au so-
litaire en partant de la solution finale et en déjouant des coups tant que c’est
possible ; partant du problème ainsi généré, dont on sait qu’il possède au moins
une solution de score n, il s’agit de retrouver une solution ; pour certains de ces
problèmes, finir avec moins de pièces (i.e. n − m) sera possible ; dans tous les
cas, finir avec une seule pièce correspond à une solution ; avec une ou n pièces,
une solution ne correspond pas forcément optimale, dans le sens ou ce n’est
pas forcément la séquence la plus courte permettant d’arriver à cette solution ;
ci-dessous un exemple de problème 4x4 avec une position finale à une pièce :

oooo ..$.
oooo ....
+oo+ ....
o+oo ....

1 M ← nextMoves ( s ) ;
2 if size ( M ) == 0 then return score ( s ) ;
3 min ← score ( s ) ;
4 for each m in M do
5 s0 ← applyMove ( s , m ) ;
6 if level == 0 then r ← playout ( s0 ) ;
7 else r ← nested_MC ( s0 , level − 1) ;
8 if r < min then min ← r ;
9 return min ;
Alg. 29: La fonction nested_MC(s, level) réalisant une recherche NMC
avec un niveau d’enroulement level.
32. Dans la famille des fonctions récursives, f est récursive enveloppée pour f (x) =
g(y, f (x0 )), f est récursive terminale pour f (x) = f (y, x0 ), f est imbriquée dans g pour
g(x) = h(y, f (f (x0 ))), f est à boucles imbriquées pour f (x) = f or[0 : n]{f (x0 )}, f est ré-
cursive imbriquée pour f (x) = h(y, f (f (x))) et enfin f et g sont mutuellement récursives
pour f (x) = g(x0 ) et g(x) = f (x0 ), avec g et h des fonctions différentes de f , y une variable
indépendante de x, et x0 une variable dépendante de x.

40
Alg. 29 retourne le nombre de pièces restantes sur la plateau pour le meilleur
coup trouvé ; l’appel aux playouts est réalisé au niveau défini par la valeur level ;
comme le score retourné dépend des playouts, deux appels à la fonction avec les
mêmes paramètres peuvent ne pas retourner le même résultat ; Alg. 30 permet
d’obtenir la meilleure valeur sur N appels à NMCS.

1 best ← score ( s ) ;
2 for N loops do
3 r ← nested_MC ( s , level ) ;
4 if r < best then best ← r ;
5return best ;
Alg. 30: La fonction anytime_NMC(s, level) permettant d’obtenir la
meilleure valeur sur N itérations de NMC ou sur un temps maximum
prédéfini.

Les fonctions nested_MC et anytime_NMC ci-dessous présentent respectivement


des implémentations de Alg. 29 et Alg. 30 avec la structure mypeg_t précédem-
ment présentée :
1 int nested_MC(peg_t& _p, int _level) {
2 if(_p.moves.size() == 0) return _p.score();
3 peg_t tmp_board;
4 tmp_board.init(_p.nbl, _p.nbc);
5 int min = _p.score();
6 for(int i = 0; i < (int)_p.moves.size(); i++) {
7 tmp_board.copy(_p);
8 tmp_board.play_move(_p.moves[i]);
9 int r = min+1;
10 if(_level == 0) {
11 tmp_board.playout();
12 r = tmp_board.score();
13 } else {
14 r = nested_MC(tmp_board, _level-1);
15 }
16 if(r < min) min = r;
17 }
18 tmp_board.clear();
19 return min;
20 }
21 int anytime_NMC(peg_t& _p, int _level, int _N) {
22 int best = _p.score();
23 for(int i = 0; i < _N; i++) {
24 int r = nested_MC(_p, _level);
25 if(r < best) best = r;
26 }
27 return best;
28 }

41
Exemple d’utilisation de la fonction nested_MC pour la résolution d’un problème
de solitaire sur une grille 6x6 avec une solution à deux pierres ou moins :
1 genpeg_t P(6,6,2);
2 P.gen(1);
3 P.mp->print();
4

5 solvepeg_t S(*(P.mp));
6 S.init();
7

8 int level = 3;
9 P.mp->update_moves();
10 int res = S.nested_MC(*(P.mp), level);
11 printf("res %d \n", res);

Les résultats présentés dans la table 1 ont été réalisés sur des grilles de taille
variant de 4x4 à 7x7.

Alg. grille taux de succès temps moyen écart-type


4x4 1.0 < 0.01 < 0.01
5x5 1.0 < 0.01 < 0.01
M CS
6x6 1.0 0.13 0.22
7x7 0.9 8.64 5.81
8x8 — — —
4x4 1.0 < 0.01 < 0.01
5x5 1.0 4.25 9.02
DLS
6x6 — — —
4x4 1.0 < 0.01 < 0.01
5x5 1.0 < 0.01 0.01
N M CS (lvl=3)
6x6 0.8 0.26 0.10
7x7 0.3 2.67 1.74
8x8 — — —
4x4 1.0 < 0.01 < 0.01
N M CSanytime 5x5 1.0 < 0.01 0.01
6x6 1.0 0.69 0.34
(lvl=3, ite=3) 7x7 0.6 6.37 4.23
8x8 — — —

Table 1 – Résultats de M CS, DLS, N M CS et N M CSanytime pour 10 pro-


blèmes ; pour utiliser un nombre de playouts similaires, M CS utilise N ×N ×10k
playouts et N M CS est réalisé avec level à 3 et N M CSanytime avec level à 3 et
avec 3 itérations.

Les temps de calculs présentés dans la table 1 ont été obtenus avec un processeur
Intel(R) Xeon(R) E5-4610 @ 2.40GHz (4804 bogomips) ; avec DLS, les tests
sur 6x6 ont été interrompus après une centaine d’heures de calculs ; pour M CS,
N M CS et N M CSanytime , les tests prennent de quelques secondes à quelques
minutes ; la profondeur maximale de récursivité choisie est de 20.

42
5.2 Nested Rollout Policy Adaptation (NRPA)
Le principe de NRPA reprend le principe de NMCS en ajoutant 1) une mémo-
risation de la meilleure séquence pour orienter les playouts et 2) une série de
playouts pour augmenter les chances de bonne orientation des futurs playouts ;
une probabilité est associée à chaque coup ; les probabilités des coups de la
meilleure séquence sont renforcées à chaque itération ; Alg. 31 présente NRPA ;
la fonction playout joue la partie aléatoirement et retourne 1) le nombre de
pièces sur le plateau après le dernier coup et 2) la séquence jouée pendant ce
playout ; P est la politique de choix des coups ; un playout est réalisé à partir
d’une position s et en fonction de cette politique P ; la mise à jour de P est
réalisée par la fonction adapt, qui est appelée à chaque itération en prenant en
compte la meilleure séquence en cours.

1 if level == 0 then
2 return playout ( s ) ;
3 else
4 {min , best_seq} ← {score ( s ) , ∅} ;
5 for N times do
6 {r, seq} ← nested_RPA ( s, level − 1, P) ;
7 if r == 1 then return {1 , seq} ;
8 if r ≤ min then {min, best_seq} ← {r, seq} ;
9 adapt (P, best_seq) ;
10 return {min , best_seq} ;
Alg. 31: La fonction nested_RPA(s, level, N ) réalisant un parcours
NRPA avec N playouts à chaque niveau et avec MAJ de la politique P de
sélection des coups suivants.

Gestion d’une politique de choix des coups


On appelle P une politique de coups, un ensemble de coups associés à des pro-
babilités ; la fonction considere permet d’ajouter un coup dans P ; la fonction
select permet de sélectionner un coup en fonction de sa probabilité dans P ; la
fonction adapt augmente les probabilités des coups d’une séquence dans P ; la
probabilité de tirage d’un coup est içi fonction de l’apparition des coups dans la
meilleure séquence au fil des itérations ; il est possible de réduire l’influence de la
meilleure séquence en fixant la probabilité de tirage d’un coup à l’exponentielle
de l’apparition des coups dans la meilleure séquence.

43
s t r u c t pdp_t { // Posi , Dir , Proba
i n t p o s i ; i n t d i r ; i n t proba ;
};
struct rollout_policy_t {
s t d : : map<s t d : : s t r i n g , double> proba ;
pdp_t∗ i n p u t ;
int input_size ;
i n t nb_input ;
i n t sum ;

rollout_policy_t ( i n t _size ) {
input_size = _size ;
i n p u t = new pdp_t [ _ s i z e ] ;
nb_input = 0 ;
sum = 0 ;
}
~rollout_policy_t () {
i f ( i n p u t != 0 ) d e l e t e [ ] i n p u t ;
input = 0;
}
void i n i t ( ) {
sum = 0 ;
nb_input = 0 ;
}
v o i d c o n s i d e r e ( peg_move_t& _m) {
s t d : : map<s t d : : s t r i n g , double > : : i t e r a t o r i i = proba . f i n d (_m. t o _ s t r ( ) ) ;
i f ( nb_input == i n p u t _ s i z e ) {
f p r i n t f ( s t d e r r , " r o l l o u t _ p o l i c y _ t c o n s i d e r e ALLOC ERROR ! ! ! \ n " ) ;
return ;
}
i f ( i i != proba . end ( ) ) {
i n p u t [ nb_input ] . p o s i = _m. p o s i ;
i n p u t [ nb_input ] . d i r = _m. d i r ;
i n p u t [ nb_input ] . proba = ( i n t ) i i −>s e c o n d ;
sum += i i −>s e c o n d ;
} else {
i n p u t [ nb_input ] . p o s i = _m. p o s i ;
i n p u t [ nb_input ] . d i r = _m. d i r ;
i n p u t [ nb_input ] . proba = 1 ;
proba [_m. t o _ s t r ( ) ] = ( d o u b l e ) i n p u t [ nb_input ] . proba ;
sum += i n p u t [ nb_input ] . proba ;
}
nb_input ++;
}
peg_move_t s e l e c t ( ) {
i n t r = rand ()%sum ;
i n t s e l e c t e d = −1;
f o r ( i n t i = 0 ; i < nb_input ; i ++) {
i f ( r < i n p u t [ i ] . proba ) { s e l e c t e d = i ; b r e a k ; }
r −= i n p u t [ i ] . proba ;
}
i n t new_posi = 0 ;
i n t new_dir = 0 ;
i f ( s e l e c t e d < 0 | | s e l e c t e d >= nb_input ) s e l e c t e d = nb_input −1;
new_posi = i n p u t [ s e l e c t e d ] . p o s i ;
new_dir = i n p u t [ s e l e c t e d ] . d i r ;
r e t u r n peg_move_t ( new_posi , new_dir ) ;
}
v o i d adapt ( s t d : : v e c t o r <peg_move_t>& _l ) {
f o r ( i n t i = 0 ; i < ( i n t ) _l . s i z e ( ) ; i ++) {
d o u b l e c u r r e n t _ v a l = proba [ _l [ i ] . t o _ s t r ( ) ] ;
i f ( c u r r e n t _ v a l < 1 0 . 0 ) proba [ _l [ i ] . t o _ s t r ( ) ] += 1 . 0 ;

44
else i f ( c u r r e n t _ v a l < 1 0 0 . 0 ) proba [ _l [ i ] . t o _ s t r ( ) ] += 2 . 0 ;
else i f ( c u r r e n t _ v a l < 1 0 0 0 . 0 ) proba [ _l [ i ] . t o _ s t r ( ) ] += 0 . 5 ;
}
}
};

Politique et séquence de coups dans peg_t


Une nouvelle structure peg_RP_t est définie.
s t r u c t peg_RP_t : peg_t {
s t d : : v e c t o r <peg_move_t> h i s t o ;
s t a t i c r o l l o u t _ p o l i c y _ t ∗ RP;

peg_RP_t ( i n t _nbl , i n t _nbc ) : peg_t ( _nbl , _nbc ) {}


peg_RP_t ( c o n s t peg_t& _p) : peg_t (_p) {}
~peg_RP_t ( ) {}
s t a t i c v o i d RP_init ( c o n s t peg_t& _p) {
RP = new r o l l o u t _ p o l i c y _ t (_p . n b l ∗_p . nbc ) ;
}
s t a t i c void RP_finalize ( ) {
i f (RP != 0 ) d e l e t e RP;
RP = 0 ;
}
void print_histo ( ) {
p r i n t f ( " h i s t o [%d ] " , ( i n t ) h i s t o . s i z e ( ) ) ;
f o r ( i n t i = 0 ; i < ( i n t ) h i s t o . s i z e ( ) ; i ++) {
histo [ i ] . print ( ) ;
}
p r i n t f ("\n " ) ;
}
v o i d copy ( c o n s t peg_RP_t& _p) {
peg_t : : copy (_p ) ;
f o r ( i n t i = 0 ; i < _p . h i s t o . s i z e ( ) ; i ++) {
h i s t o . push_back (_p . h i s t o [ i ] ) ;
}
}
v o i d a p p l y ( peg_move_t& _m) {
peg_t : : a p p l y (_m) ;
h i s t o . push_back (_m) ;
}
b o o l rand_move ( ) {
set_moves ( ) ;
i f ( moves . s i z e ( ) == 0 ) r e t u r n f a l s e ;
RP−>i n i t ( ) ;
f o r ( s t d : : l i s t <peg_move_t > : : i t e r a t o r i i = moves . b e g i n ( ) ;
i i != moves . end ( ) ; i i ++) {
RP−>c o n s i d e r e ( ∗ i i ) ;
}
peg_move_t r = RP−>s e l e c t ( ) ;
apply ( r ) ;
return true ;
}
void playout ( ) {
while (1) {
i f ( rand_move ( ) == f a l s e ) b r e a k ;
}
}
void clear_histo ( ) {
histo . clear ();
}
v o i d adapt ( ) {

45
RP−>adapt ( h i s t o ) ;
}
};
r o l l o u t _ p o l i c y _ t ∗ peg_RP_t : : RP;

Les résultats présentés dans la table 2 ont été réalisés sur des grilles de taille
variant de 5x5 à 7x7.

solveur grille taux de succès temps moyen écart-type


4x4 1.0 < 0.01 < 0.01
5x5 1.0 0.02 0.02
100 playouts/niveau
6x6 0.7 0.14 0.09
7x7 0.7 2.44 0.80
4x4 1.0 < 0.01 < 0.01
5x5 1.0 < 0.01 < 0.01
200 playouts/niveau
6x6 1.0 0.35 0.34
7x7 0.9 2.93 2.34

Table 2 – Résultats de NRPA pour 10 problèmes avec 100 et 200 playouts par
niveau

Les temps de calculs présentés dans la table 2 ont été obtenus avec un proces-
seur Intel(R) Xeon(R) E5-4610 @ 2.40GHz (4804 bogomips). La profondeur
maximale de récursivité choisie est de 20.

Pour les problèmes 7x7, on obtient NRPA > NMCS ; NRPA résoud en moyenne 33 1.5
fois plus rapidemment que NMCS (cf support de cours NMCS). Pour les problèmes
plus faciles, NMCS trouve une solution plus rapidemment que NRPA. Les résultats
obtenus dépendent directement du nombre de playouts réalisés par niveau et du
niveau de « nesting » choisis.

33. Résoudre 9 ou 10 problèmes sur 10 est içi considéré comme équivalent. Des tests sur
100 problèmes permettraient de quantifier plus précisément la différence entre NMCS avec 100
playouts et NRPA avec 200 playouts et surtout la différence entre problèmes résolus avec NRPA
contre 10 avec NMCS du support de cours précédent. Un peu de tunning des paramètres
nb_playouts, depth et allure de la fonction adapt serait à faire pour améliorer les temps de
réponse du solveur de peg-solitaire.

46
6 Preuve
6.1 Proof Number Search (PNS)
6.2 Proof Number 2 Search (PN2S)
6.3 Depth-First PNS (DFPNS)
6.4 Problème GHI
6.5 Variantes de PNS
6.6 Application à Breakthrough

47
7 Parallélisation
7.1 A la racine (Root Parallel)
7.2 Aux feuilles (Leaf Parallel)
7.3 Dans l’arbre (Tree Parallel)
7.4 Parallel NMCS
7.5 Distributed NRPA
7.6 PNS Randomisé (RP-PNS)
7.7 Parallel DFPNS
7.8 Job-Level PNS (JLPNS)
7.9 Parallel PN2S (PPN2S)
7.10 Application au Morpion Solitaire

48
8 Problèmes à information incomplète
8.1 Théorie des jeux
La théorie des jeux présente les jeux sous l’angle des interactions entre les joueurs
et essaie de définir les stratégies optimales d’un jeu ; cette question, de définition
de stratégie optimale, vue sous l’angle des interactions, implique que maximi-
ser des gains et minimiser des pertes dépend des décisions des autres joueurs ;
cette question est considérée en théorie des jeux pour les jeux à coups simultanés.

Un jeu à deux joueurs est représenté en forme normale par une tableau à deux
dimensions (également appelé matrice des gains). Dans le jeu Roshambo 34 , les
deux joueurs choisissent un coup parmi les trois Pierre, Papier, Ciseaux ; on
notera R le coup Pierre, P le coup Papier et S le coup Ciseaux ; les joueurs
jouent simultanément ; le tableau 3 présente la résolution des décisions de chaque
joueur ; le gain du joueur J1 est la première valeur ; le gain du joueur J2 est la
deuxième valeur ; une victoire est notée 1 et une défaite est notée −1 ; quand J1
joue R et J2 joue S, on trouve « 1 ;-1 », donc J1 gagne et J2 perd.

Table 3

J2
R P S
R 0 ;0 -1 ;1 1 ;-1
J1 P 1 ;-1 0 ;0 -1 ;1
S -1 ;1 1 ;-1 0 ;0

Pour chaque case de la matrice de gain, si la somme des gains des joueurs est
nulle, le jeu est dit à somme nulle ; si la somme des gains des joueurs vaut une
valeur constante, le jeu est dit à somme constante et est reformulable en jeu à
somme nulle par soustraction de cette valeur constante à tous les gains.

8.2 Déterminisation et MCTS


8.3 Regret contrefactuel
8.4 Résolution par convention
8.5 Application à Hannabi
8.6 Application au Go Fantôme
8.7 Application au Poker

34. Pierre-Papier-Ciseaux, Rock-Paper-Scissors.

49
9 Apprentissage
9.1 Processus de décision Markovien
Dans le cadre de la prise de décision, les processus de décision Markovien (MDP)
permettent de définir des politiques de décision et de les améliorer itérativement ;
dans un MDP, un environnement complètement observable est défini par un état
courant ; à partir de cet état, une action définit l’état suivant ; à une action est
associée une récompense ; un MDP est ainsi une suite d’états, définie par une
suite d’actions, associée à une suite de récompenses ; résoudre un MDP équi-
vaut à définir la suite des actions qui maximise la récompense totale ; on appelle
politique la fonction qui définit à chaque instant l’action à suivre dans chaque
état ; le principe d’un MDP s’appuie sur la propriété suivante : « considérant le
présent, le futur est indépendant du passé », qui s’écrit en considérant la pro-
babilité de l’état à t + 1 :

P[St+1 |St ] = P[St+1 |S1 , ..., St ]


Autrement dit, l’état St+1 dépend uniquement de St , indépendamment des états
précédents S1 à St−1 ; autrement dit, l’historique est inutile et seul l’état à l’ins-
tant t est utile pour définir l’état futur à l’instant t + 1 ; par opposition à une
politique Markovienne, on peut avoir une politique histoire-dépendante ; ht est
l’historique des états rencontrés, avec ht = {St , St−1 , St−2 , . . . , S0 }.

On note pa la probabilité de l’action a, ou encore PSS 0 la probabilité de la


transition d’un état S vers S 0 :

pa = PSS 0 = P[St+1 = S 0 |St = S]

Pour un problème à n états, il s’agit donc de définir la matrice suivante :


 
P11 . . . P1n
P =  ... .. .. 

. . 
Pn1 ... Pnn

dans laquelle la somme des valeurs de chaque ligne est égale à 1.

Un MDP est noté (S, A, P, R, γ) avec S l’ ensemble des états, A l’ensemble des
actions, P l’ensemble des probabilités de transition, R l’ensemble des récom-
penses immédiates, et γ un facteur de dépréciation tel que γ ∈ [0, 1].

Une récompense RaSS 0 , par application d’une action a, à partir d’un état S pour
obtenir un état S 0 peut être positive ou négative ; RaSS 0 appliquée à l’instant t
est abrégée rt ; en pratique rt est constaté à l’instant t + 1 (i.e. après application
d’une action at à l’instant t).

Une politique Markovienne d’action déterministe (notée πt (st )) définit l’action à


appliquer à chaque instant ; une politique Markovienne aléatoire (notée πt (a, st ))
définit la probabilité de sélection de l’action a ; dans le cas d’une politique
histoire-dépendante, on note respectivement πt (ht ) et πt (a, ht ).

50
Plus généralement, on note une politique π et on a :
— π(St , at ) = πSt → at pour une politique Markovienne déterministe
— π(St , pa ) = πSt → [0, 1] pour une politique Markovienne aléatoire
— π(ht , at ) = πht → at pour une politique histoire-dépendante déterministe
— π(ht , pa ) = πht → [0, 1] pour une politique histoire-dépendante aléatoire
— π(St , at ) ⊆ π(St , pa )
— π(St , at ) ⊆ π(ht , at )
— π(St , pa ) ⊆ π(ht , pa )
— π(ht , at ) ⊆ π(ht , pa )

Pour évaluer une politique, on utilise le cumul espéré, qui correspond au cumul
des valeurs contenues dans une suite de récompenses de taille n ; on appelle
critère de performance le mode de cumul choisi, avec r0 la récompense obte-
nue dans l’état courant S0 , r1 la récompense dans l’état suivant, et rn−1 la
dernière récompense obtenue avant obtention d’un état terminal ; le critère de
performance peut être défini par :
— une somme de taille fixe avec k < n − 1, égale à {r0 + r1 + · · · + rk |S0 }
— une somme gamma-pondérée, égale à {r0 +γr1 +γ 2 r2 +· · ·+γ n−1 rn−1 |S0 }
— une somme totale, égale à {r0 + r1 + · · · + rn−1 |S0 }
— une moyenne, égale à {(r0 + r1 + · · · + rn−1 )/n|S0 }

Quand γ (le facteur de dépréciation) est proche de zéro, l’évaluation est à courte
échéance ; quand γ est proche de un, l’évaluation est à longue échéance.

Pour un ensemble d’états associé à un ensemble d’actions, il s’agit de définir


la politique optimale π(a|s) = P[At = a|St = s] correspondant au comporte-
ment de l’agent ; dans le contexte Markovien, π(a|s) ne dépend que de s ; si les
actions de A mènent toujours aux mêmes états de S, π est dite stationnaire
(At ∼ π(.|St ), ∀t > 0) ; pour une politique π, on obtient un processus Markovien
(S, P π ), un processus de récompense (S, P π , Rπ , γ), une fonction de valeur des
état vπ (s), une fonction de valeur des actions qπ (s, a), et des fonctions optimales
v∗ (s) = argmaxπ vπ (s) (maximisant v avec la meilleure politique π pour un état
s) et q∗ (s, a) = argmaxπ qπ (s, a) ; un MDP est résolu quand on connaît P et R
(autrement dit, quand on connait les fonctions v∗ (s) et q∗ (s, a) et la politique
optimale π∗ ).

51
9.2 Heuristique et compromis
Quand une fonction heuristique h (S,A) donne une valeur de l’action A à partir
de l’état S, on appelle approche gloutonne (ou encore approche greedy) le fait
de suivre le choix proposé par la fonction h pour trouver la meilleure solution.

Pour améliorer une approche gloutonne, il est possible de pondérer l’action pro-
posée par une heuristique associée à un choix aléatoire ; ces heuristiques s’ap-
pliquent avant l’état terminal, pour maximiser l’évaluation de la position termi-
nale ; plusieurs méthodes de pondération sont possibles, dont les plus courantes
sont d’alterner choix heuristique et choix aléatoire :
— Selon une fonction hN, utilisant une valeur N1 , comme le présente Alg. 32 ;
la valeur N1 diminue au fil de sa sélection ; après N1 appels à cette
fonction heuristique, N1 est nul et toutes les actions choisies deviennent
aléatoires.
— Selon une fonction -greedy, comme le présente Alg. 33, avec  la pro-
babilité d’exploration (i.e. de sélection de la fonction randomMove).
— Selon une fonction softmax, comme le présente Alg. 34.

Alg. 32, 33 et 34 retournent la meilleure action à partir d’un état S.

1 if N1 >= 0 then
2 N1 ← N1 − 1 ;
3 return argmaxA h(S, A) ;
4 else
5 return randomMove (S);
Alg. 32: Fonction hN définisant la meilleure action à partir d’un
état S par alternance entre valeur heuristique h et fonction aléatoire
randomMove ; cette solution dépend de la valeur initiale de N1 .

1 if  < random ( ) then return randomMove (S);


2 else return argmaxA h(S, A) ;
Alg. 33: Fonction -greedy définissant un compromis entre exploration
et exploitation, selon un paramètre  ; la fonction random retourne une va-
leur entre 0 et 1 ; en pratique, on a S 0 ← -greedy(S, H) avec la fonction
H définissant les valeurs h(S, a) pour les actions a de A.

52
1 rnd ← random ( ) ;
2 M ← nextMoves ( S ) ;
3 best ← ∅ ;
4 sum ← 0 ;
5 for each m in M do
6 sum ← sum + eh(S,m) ;
7 for each m in M do
8 evalm ← eh(S,m) /sum ;
9 if rnd ≤ evalm then
10 best ← m ;
11 break ;
12 rnd ← rnd − evalm ;
13 return best ;
Alg. 34: Fonction softmax ; M définit l’ensemble des actions A possibles
à partir de S.

La fonction random retourne une valeur aléatoire entre 0 et 1 ; la méthode soft-


max équivaut à sélectionner une action A à partir d’un état S selon une proba-
bilité p(S, A) et une distribution de Boltzmann, définie par la formule suivante :
eh(S,A)
p(S, A) = P h(S,A0 )
A0 e
Une fonction d’évaluation est dite -soft si la probabilité de sélection de cha-
cune des actions est au moins égale à .

9.3 Modes de progression d’apprentissage


L’apprentissage par renforcement propose quatre modes de progression d’ap-
prentissage : 1) par méthode Monte-Carlo (abrégé MC), 2) par différence tem-
porelle (abrégé TD), 3) par différence temporelle λ (abrégé T D(λ)), et 4) par
différence temporelle avec traces d’éligibilité ; ces modes permettent de définir
des classes d’algorithmes.

L’apprentissage en mode MC est réalisé à partir de N scénarios ; un scénario


est obtenu par application de π en partant d’un état aléatoire de S ; chaque
scénario se termine par une valeur finale Gt (i.e. pour un scénario correspondant
à une séquence seq, on a Gt = eval(seq)) ; pour un état final st de valeur Gt ,
on applique à chaque état de la séquence ayant conduit à st une procédure
d’évaluation ; Alg. 35 présente cet apprentissage de la fonction d’évaluation du
problème étudié ; une variation est possible sur le mode de MAJ des évaluations
des états rencontrés dans les scénario : 1) MAJ en considérant uniquement la
première visite d’un état selon sa position dans la séquence seq, 2) MAJ en
considérant uniquement la première visite d’un état associée à la séquence seq,
3) MAJ à chaque visite d’un état ; dans le cas de jeux à information incomplète,
l’état initial est soit évalué exhaustivement (si le nombre de positions initiales
le permet), soit choisi au hasard ; on appelle Ŝ l’ensemble des états rencontrés
au cours des N séquences aléatoires ; si Ŝ diffère de S, certains états sont sans
évaluation.

53
1 Ŝ ← ∅ ;
2 for N times do
3 seq ← π(random (S)) ;
4 Gt ← eval(seq) ;
5 for each s in seq do
6 if s ∈
/ Ŝ then (sums , Ns ) ← (Gt , 1) ;
7 else (sums , Ns ) ← (sums + Gt , Ns + 1) ;
8 Ŝ ← Ŝ ∪ seq ;
9 for each s in Ŝ do
10 scores ← sums /Ns ;
Alg. 35: Mode d’apprentissage MC.

D’une manière abstraite, l’évaluation d’un état s suit un processus d’évaluation


après k itérations (présenté dans Alg. 36), avec sums initialisé à zéro.

1 sums ← 0 ;
2 for k times do sums ← sums + Gt ;
3 scores ← sums /k ;
Alg. 36: Abstraction de l’évaluation MC d’un état s.

Cette évaluation d’un état peut se faire incrémentalement, par correction de


l’erreur associée à la valeur du score à l’instant t − 1 (présenté dans Alg. 37).

1 scores ← 0 ;
2 for k times do scores ← scores + (Gt − scores )/k ;
Alg. 37: Abstraction de l’évaluation MC incrémentale d’un état s.

Pour des cas d’évaluation instable (i.e. ne donnant pas toujours le même ré-
sultat Gt pour deux séquences), il sera judicieux de pondérer le score avec
le dernier résultat Gt en remplaçant 1/k par une valeur α avec scores =
scores + α(Gt − scores ) ; ainsi on peut réduire ou augmenter l’influence les
nouveaux résultats Gt en augmentant ou en diminuant la valeur α autour de
1/k ; scores n’est plus une moyenne des scores obtenus dans ces cas ; ce mode
de calcul est adapté aux problèmes non-stationnaires (i.e. variant dans le temps).

L’apprentissage en mode TD est réalisé à partir de N scénarios avec ou sans état


terminal et avec un retour immédiat des actions effectuées ; on obtient dans l’état
St un retour immédiat Rt ; la valeur Gt devient une estimation Rt+1 +γV (St+1 ),
appelée TD-target ; la valeur Rt+1 + γV (St+1 ) − V (St ) notée δt est appelée TD-
error.

54
L’apprentissage en mode TD est en fait paramétré avec λ qui définit la profon-
deur de l’estimation de TD-target ; Alg. 38 présente une abstraction de l’éva-
luation TD(0) d’un état s avec s = St , s0 = St+1 (autrement dit, s0 suivant de
s), α = k −1 , rets0 = Rt+1 , scores = V (St ) et scores0 = V (St+1 ).

1 scores ← 0 ;
2 for k times do scores ← scores + α(rets0 + γscores0 − scores ) ;
Alg. 38: Abstraction de l’évaluation TD(0) d’un état s.

Pour TD(0), on a :
TD-target = rets0 + γscores00
Pour TD(1), avec s00 suivant de s0 , on a :

TD-target = rets0 + γrets00 + γ 2 scores00

Pour TD(2), avec s000 suivant de s00 , on a :

TD-target = rets0 + γrets00 + γ 2 rets000 + γ 3 scores000

L’apprentissage en mode TD(λ) avec λ > 1 permet de réduire l’influence de


l’horizon sur l’apprentissage ; avec λ = 1, les actions ayant des effets sur un
horizon de deux pas de simulation seront mieux considérées ; Alg. 39 présente
une abstraction de l’évaluation TD(λ).

1 scores ← 0 ;
2 for k times do
scores ← scores + α(rets0 + γ a π(a|S 0 )scores0 − scores ) ;
P
3

Alg. 39: Abstraction de l’évaluation TD(λ) d’un état s.

Disposant de récompenses immédiates, il s’agit içi de maximiser le cumul des


récompenses ; les récompenses immédiates sont des signes positifs d’évolution
du modèle ; si les scénarios sont finis, les séquences d’états sont des épisodes ou
des tâches épisodiques ; si les scénarios sont infinis, les séquences d’états sont
dites continues, l’état final est infini et la valeur du score est également poten-
tiellement infini ; selon la valeur de γ fixée, le retour cumulé est borné et est
appelé retour déprécié.

En pratique, γ varie entre 0 et 1 ; la valeur de γ fixe l’échéance de l’évaluation


d’une séquence ; plus la valeur de γ est proche de zéro, plus les récompenses à
courte échéance comptent ; plus la valeur de γ est grande, plus les récompenses
à longue échéance comptent.

55
Les traces d’éligibilité correspondent au stockage des valeurs de retour des états
suivants de s0 (dans le cas de TD(2) : rets00 et rets000 ) ; Alg. 40 présente une abs-
traction de l’évaluation TD avec les traces d’éligibilité ; chaque MAJ concerne
les scores de tous les états ; les traces d’éligibilité de l’état S sont notées zs ; S
définit l’ensemble des états possibles.

1 for each S in S do
2 scores ← 0 ;
3 zs ← 0 ;
4 for k times do
5 S ← get_current_state ( ) ;
6 update ( zs ) ;
7 for each s in S do
8 scores ← scores + αzs (rets0 + γscores0 − scores ) ;
9 zs ← γλzs ;

Alg. 40: Abstraction de l’évaluation TD avec traces d’éligibilité.

Au travers de la fonction update ( zs ), deux méthodes de MAJ des traces


d’éligibilité sont possibles :
— avec des traces d’éligibilité accumulatives :

zs + = 1

— avec des traces d’éligibilité et la réinitialisation des états voisins de S :



zsiblings(s) = 0
zs = 1

56
9.4 Programmation dynamique
Dans le cas d’espaces d’états complètement connus, dans lesquels il est possible
de procéder à rebours en partant de l’état final, les trois solutions courantes
pour la définition d’une politique optimale (notée π∗ ) sont :
— Par exécution de N simulations, selon le principe policy-evaluation
— Par exécution de N itérations de MAJ inter-dépendantes de politique et
d’évaluation, selon le principe value-iteration.
— Par exécution de N itérations de MAJ de politique toutes les k itérations
d’évaluation, selon le principe policy-iteration.

Dans le cas d’une politique prédéfinie, il est possible de l’évaluer avec N ité-
rations en suivant le principe policy-evaluation ; cette solution répond à des
questions de prédiction (autrement dit, quelle récompense vπ obtenir ou espérer
pour une politique π) ; pour une politique π, on calcule la valeur vπ ; les valeurs
des états sont initialisées à zéro ; les valeurs des états sont définies selon leurs
états précédents ; la valeur d’un état est MAJ selon la moyenne des états précé-
dents plus une récompense ; on définit ainsi v1 , v2 , . . . vN ; après N itérations,
les valeurs des états convergent vers vπ ; la convergence vers vπ est indépendante
de la politique π1 initialement choisie ; Alg. 41 présente ce calcul d’évaluation
d’une politique π dans un espace S ; à chaque itération, on applique une récom-
pense r ; à la fin de chaque itération, vi est dans S.

1 for N times do
2 S0 ← ∅ ;
3 for each s in S do
4 sum_score ← 0 ;
5 s0 ← s ;
6 P ← prev(s, π) ;
7 for each p in P do
8 sum_score ← sum_score + p.eval ;
9 s0 .eval ← r + sum_score/sizeof(P) ;
10 S 0 ← S 0 ∪ s0 ;
11 S ← S0 ;
Alg. 41: Principe policy-evaluation.

Selon le principe value-iteration, π∗ est définie par amélioration itérative de π,


elle-même MAJ selon vπ à chaque itération ; π converge vers π∗ et v vers v∗ ;
Alg. 42 présente ce principe dans un espace S avec une politique π ; les évalua-
tions vπ sont MAJ selon l’heuristique greedy et la politique π ; les évaluations
v sont initialisées aléatoirement ; πt est définie par vt−1 .

La terminaison en N itérations est remplaçable par une condition d’arrêt, par


observation des valeurs ou de leurs différences.

57
1 for N times do
2 S ← update-value (π, greedy) ;
3 π ← update-policy (S) ;
Alg. 42: Principe value-iteration.

Selon le principe policy-iteration, la politique v∗ est définie par amélioration de


π toutes les k MAJ de vπ ; Alg. 43 présente ce principe dans un espace S pour
une politique π avec l’heuristique greedy.

1 for N times do
2 for k times do
3 S ← update-value (π, greedy) ;
4 π ← update-policy (S) ;
Alg. 43: Principe policy-iteration.

58
9.5 Application à Grid-world
Le problème Grid-world consiste en une grille (figure 3) avec des cases de sortie
(représentées en gris) et des cases (représentées en blanc) sur lesquelles il est
possible d’appliquer une commande haut-bas-gauche-droite ; en appliquant une
politique aléatoire sur chaque case, la question est de trouver la valeur de la
politique de déplacement aléatoire dans une telle grille ; ce qui équivaut à définir
la longueur moyenne des chemins permettant d’atteindre les cases de sortie.

Fig. 3 – Environnement du problème Grid-world.

A chaque itération, l’application d’une commande implique une récompense sup-


plémentaire de −1 ; la valeur d’un état est égale à la moyenne des récompenses
des états précédents plus la récompense supplémentaire ; pour un état final (i.e.
case de sortie), la valeur est égale à la récompense supplémentaire ; les actions
sortant de la grille sont sans effet ; chaque commande haut-bas-gauche-droite
est équiprobable.

Par application du principe policy-evaluation, en représentant la grille en ligne,


on trouve les valeurs suivantes (dont l’évaluation v1000 est présentée en figure 4) :
i: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
i:1 :: 0.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 \
-1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0
i:2 :: 0.0 -1.8 -2.0 -2.0 -1.8 -2.0 -2.0 -2.0 \
-2.0 -2.0 -2.0 -1.8 -2.0 -2.0 -1.8 0.0
i:10 :: 0.0 -6.1 -8.4 -9.0 -6.1 -7.7 -8.4 -8.4 \
-8.4 -8.4 -7.7 -6.1 -9.0 -8.4 -6.1 0.0
i:100 :: 0.0 -13.9 -19.9 -21.9 -13.9 -17.9 -19.9 -19.9 \
-19.9 -19.9 -17.9 -13.9 -21.9 -19.9 -13.9 0.0
i:1000 :: 0.0 -14.0 -20.0 -22.0 -14.0 -18.0 -20.0 -20.0 \
-20.0 -20.0 -18.0 -14.0 -22.0 -20.0 -14.0 0.0

Après 1000 itérations, on constate la convergence de v1000 en vπ ; l’applica-


tion du principe policy-evaluation est présenté dans le programme suivant :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <string.h>
4 #include <math.h>
5
6 struct grid_t {
7 bool end_state[16] = {false};
8 double value[16] = {0.0};
9
10 void init () {
11 end_state[0]=true;
12 end_state[15]=true;
13 }

59
14 void print() {
15 for(int i = 0; i < 16; i++)
16 printf("%.1f ", value[i]);
17 printf("\n");
18 }
19 void iterate() {
20 double tmp[16];
21 for(int i = 0; i < 16; i++) {
22 if(end_state[i]) {
23 value[i]=0.0;
24 } else {
25 int i_up = i-4; if(i_up<0) i_up=i;
26 int i_down = i+4; if(i_down>15) i_down=i;
27 int i_left = i-1; if((i%4)==0) i_left=i;
28 int i_right = i+1; if((i%4)==3) i_right=i;
29 tmp[i] = (value[i_up]+value[i_down]+
30 value[i_left]+value[i_right])/4.0-1.0;
31 }
32 }
33 memcpy(value, tmp, 16*sizeof(double));
34 }
35 void generate() {
36 init();
37 for(int i = 0; i < 1001; i++) {
38 if(i<=2 || i == 10 || i == 100 || i == 1000) {
39 printf("i:%d :: ", i); print();
40 }
41 iterate();
42 }
43 }
44 };
45 /* g++ -std=c++11 grid-world-policy-evaluation.cpp */
46 int main(int _ac, char** _av) {
47 grid_t G;
48 G.generate();
49 return 0;
50 }

Fig. 4 – Evaluation v1000 de Grid-world.

La moyenne des valeurs est de 16 ; Pour chaque case, ces valeurs permettent
également de définir une politique optimale (présentée en figure 5) ; la politique
obtenue est partielle pour certains états (représentés en rouge dans cette figure).

60
Fig. 5 – Politique optimale π1000 par policy-evaluation sur Grid-world.

Pour appliquer le principe value-iteration, il est nécessaire d’ajouter une liste


représentant les états précédents ; pour grille-world, on définit un ensemble d’ac-
tions prev, qui est mis à jour à chaque itération ; prev associe une à quatre ac-
tions à chaque état de S (içi une grille 4 × 4) ; le programme précédent devient :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <string.h>
4 #include <math.h>
5 #include <cfloat>
6 enum {UP=0, DOWN, LEFT, RIGHT};
7 const char str_prev[] {’U’,’D’,’L’,’R’};
8 struct grid_t {
9 bool end_state[16] = {false};
10 double value[16] = {0.0};
11 bool prev[16][4];
12
13 void init () {
14 end_state[0]=true;
15 end_state[15]=true;
16 for(int i = 0 ; i < 16; i++)
17 for(int j = 0; j < 4; j++)
18 prev[i][j] = true;
19 }
20 void print() {
21 for(int i = 0; i < 16; i++) {
22 printf("%.1f ", value[i]);
23 if(i%4==3) printf("\n");
24 }
25 }
26 void print_prev() {
27 for(int i = 0; i < 16; i++) {
28 for(int j = 0; j < 4 ; j++) {
29 if(!prev[i][j]) printf("-");
30 else printf("%c", str_prev[j]);
31 }
32 printf(" ");
33 if(i%4==3) printf("\n");
34 }
35 }

61
36 void update_value() {
37 double tmp[16];
38 for(int i = 0; i < 16; i++) {
39 if(end_state[i]) {
40 value[i]=0.0;
41 } else {
42 int i_up = i-4; if(i_up<0) i_up=i;
43 int i_down = i+4; if(i_down>15) i_down=i;
44 int i_left = i-1; if((i%4)==0) i_left=i;
45 int i_right = i+1; if((i%4)==3) i_right=i;
46 int N = 0;
47 tmp[i] = 0.0;
48 if(prev[i][UP]) { tmp[i] += value[i_up]; N++; }
49 if(prev[i][DOWN]) { tmp[i] += value[i_down]; N++; }
50 if(prev[i][LEFT]) { tmp[i] += value[i_left]; N++; }
51 if(prev[i][RIGHT]) { tmp[i] += value[i_right]; N++; }
52 tmp[i] /= N;
53 tmp[i] -= 1.0;
54 }
55 }
56 memcpy(value, tmp, 16*sizeof(double));
57 }
58 void update_policy() {
59 bool tmp[16][4] = {false};
60 for(int i = 0; i < 16; i++) {
61 if(!end_state[i]) {
62 double best_value = -DBL_MAX;
63 int i_up = i-4; if(i_up<0) i_up=i;
64 int i_down = i+4; if(i_down>15) i_down=i;
65 int i_left = i-1; if((i%4)==0) i_left=i;
66 int i_right = i+1; if((i%4)==3) i_right=i;
67 // compute best_value
68 if(prev[i][UP]) if(value[i_up] > best_value) best_value = value[i_up];
69 if(prev[i][DOWN]) if(value[i_down] > best_value) best_value = value[i_down];
70 if(prev[i][LEFT]) if(value[i_left] > best_value) best_value = value[i_left];
71 if(prev[i][RIGHT]) if(value[i_right] > best_value) best_value = value[i_right];
72 // compute the new policy
73 if(prev[i][UP]) if(value[i_up] == best_value) tmp[i][UP] = true;
74 if(prev[i][DOWN]) if(value[i_down] == best_value) tmp[i][DOWN] = true;
75 if(prev[i][LEFT]) if(value[i_left] == best_value) tmp[i][LEFT] = true;
76 if(prev[i][RIGHT]) if(value[i_right] == best_value) tmp[i][RIGHT] = true;
77 }
78 }
79 memcpy(prev, tmp, 16*4*sizeof(bool));
80 }
81 void generate() {
82 init();
83 for(int i = 0; i < 101; i++) {
84 if(i==1 || i == 100) {
85 print();
86 print_prev();
87 }
88 update_value();
89 update_policy();
90 }
91 }
92 };
93 /* g++ -std=c++11 grid-world-value-iteration.cpp */
94 int main(int _ac, char** _av) {
95 grid_t G;
96 G.generate();
97 return 0;
98 }

62
A la première itération, avec les appels à print() et print_prev, on obtient :
0.0 -1.0 -1.0 -1.0
-1.0 -1.0 -1.0 -1.0
-1.0 -1.0 -1.0 -1.0
-1.0 -1.0 -1.0 0.0
---- --L- UDLR UDLR
U--- UDLR UDLR UDLR
UDLR UDLR UDLR -D--
UDLR UDLR ---R ----

A la centième itération, on obtient :


0.0 -1.0 -2.0 -3.0
-1.0 -2.0 -3.0 -2.0
-2.0 -3.0 -2.0 -1.0
-3.0 -2.0 -1.0 0.0
---- --L- --L- -DL-
U--- U-L- UDLR -D--
U--- UDLR -D-R -D--
U--R ---R ---R ----

Fig. 6 – Politique optimale π100 avec value-iteration sur Grid-world.

La politique obtenue π100 (présentée en figure 6) est la politique optimale π∗ et


les évaluations des états v100 sont les valeurs optimales v∗ .

Pour appliquer le principe policy-iteration, avec k = 2, il suffit de modifier la


fonction generate comme suit :
1 void generate() {
2 init();
3 for(int i = 0; i < 101; i++) {
4 for(int j = 0; j < 2; j++) {
5 update_value();
6 }
7 update_policy();
8 }
9 }

63
En ajoutant pour condition de terminaison, une égalité à 10−2 prêt de l’évalua-
tion vi ou de la politique πi , le programme correpondant à value-iteration est
modifié comme suit :
1 // ...
2 struct grid_t {
3 bool end_state[16] = {false};
4 double value[16] = {0.0};
5 bool prev[16][4];
6
7 bool value_stop_condition = false;
8 bool policy_stop_condition = false;
9 bool convergence_detected = false;
10
11 void init () { // ...
12 }
13 void print() { //...
14 }
15 void print_prev() { //...
16 }
17 void update_value() {
18 double tmp[16];
19 for(int i = 0; i < 16; i++) { //...
20 }
21 if(value_stop_condition) {
22 convergence_detected = true;
23 for(int i = 0; i < 16; i++) {
24 if(!(value[i]-1E-2 < tmp[i] && value[i]+1E-2 > tmp[i]))
25 convergence_detected = false;
26 }
27 }
28 memcpy(value, tmp, 16*sizeof(double));
29 }
30 void update_policy() {
31 bool tmp[16][4] = {false};
32 for(int i = 0; i < 16; i++) { //...
33 }
34 if(policy_stop_condition) {
35 convergence_detected = true;
36 for(int i = 0; i < 16; i++)
37 for(int j = 0; j < 4; j++) {
38 if(!(prev[i][j]-1E-2 < tmp[i][j] &&
39 prev[i][j]+1E-2 > tmp[i][j])) convergence_detected = false;
40 }
41 }
42 memcpy(prev, tmp, 16*4*sizeof(bool));
43 }
44 void generate() {
45 init();
46 if(value_stop_condition || policy_stop_condition) {
47 convergence_detected = false;
48 }
49 for(int i = 0; i < 10000; i++) {
50 update_value();
51 update_policy();
52 if(convergence_detected) { printf("convergence at %d\n", i); break; }
53 }
54 print();
55 print_prev();
56 }
57 };
58 //...

64
Dont l’appel devient dans le cas d’une condition d’arrêt sur l’évaluation vi :
1 int main(int _ac, char** _av) {
2 grid_t G;
3 G.value_stop_condition=true;
4 G.generate();
5 return 0;
6 }

Ou encore dans le cas d’une condition d’arrêt sur la politique πi :


1 int main(int _ac, char** _av) {
2 grid_t G;
3 G.policy_stop_condition=true;
4 G.generate();
5 return 0;
6 }

Dans les deux cas, la convergence est atteinte à la quatrième itération, avec
l’évaluation vi et la politique πi pour (i = 3).

En appliquant le même principe à policy-iteration, la fonction generate de-


vient :
1 void generate() {
2 init();
3 for(int i = 0; i < 101; i++) {
4 if(value_stop_condition || policy_stop_condition) {
5 convergence_detected = false;
6 }
7 for(int j = 0; j < 2; j++) {
8 update_value();
9 if(convergence_detected) {
10 update_policy();
11 printf("convergence value at %d %d\n", i, j); return;
12 }
13 }
14 update_policy();
15 if(convergence_detected) {
16 printf("convergence policy at %d\n", i); break;
17 }
18 }
19 }

Dans ce cas, la convergence est atteinte pour (i = 2, j = 1) dans le cas d’une


condition d’arrêt sur l’évaluation vj avec la politique πi ; la convergence est at-
teinte pour (i = 2) dans le cas d’une condition d’arrêt sur la politique πi .

Avec un paramètre k adéquat, la convergence de policy-iteration est plus rapide


que celle de value-iteration ; dans les deux cas, ces principes alternent évaluation
(selon le principe policy-evaluation) et MAJ de π ; dans les cas, les fréquences
de MAJ de π diffèrent.

65
9.6 Apprentissage par renforcement
Dans le cas d’espaces d’états trop grands pour être complètement énumérés, les
quatres solutions courantes combinant modes de progression et principes des
MDPs pour établir une estimation de la politique à suivre pour résoudre un
problème de décision sont : 1) SARSA, 2) Q-learning, 3) SARSA-λ, et 4) le
double apprentissage.

La taille de l’espace d’états implique l’impossibilité d’un raisonnement par ré-


currence (partant de l’état final) ; on dit que P est inconnu ; le système est donc
étudié par observation ; par application d’une action, on obtient un état S ; se-
lon les systèmes observés, on associe des valeurs heuristiques v aux états s ou
des valeurs heuristiques q aux couples état-action (noté {S, A}) ; dans le cas
de valeurs heuristiques v, on essaie de s’orienter vers les états futurs de valeurs
max ; dans le cas de valeurs heuristiques q, on applique l’action de valeur max.

Alg. 44 présente l’apprentissage SARSA 35 ; à la ligne 8, il reprend le principe pré-


senté dans Alg. 38 (qui définissait une relation entre scores et scores0 ) en l’appli-
quant au couple état-action pour définir une relation entre q(S, A) et q(S 0 , A0 ) ;
Q définit l’ensemble des évaluations des couples état-action possibles ; la fonc-
tion d’évaluation Q définit les valeurs heuristiques q des couples état-action ;
en suivant les valeurs heuristiques q, on veut suivre la politique optimale ; la
fonction heuristique -greedy reprend le principe présenté dans Alg. 33 pour
obtenir un compromis entre exploration et exploitation.

1 for each q in Q do q ← random ( ) ;


2 for nb_episodes do
3 S ← initialize ( ) ;
4 A ← -greedy (S, Q) ;
5 for nb_loop do
6 {S 0 , R0 } ← apply (S, A) ;
7 A0 ← -greedy (S 0 , Q) ;
8 q(S, A) ← q(S, A) + α (R0 + γq(S 0 , A0 ) − q(S, A)) ;
9 {S, A} ← {S 0 , A0 } ;
10 if S == terminal then break ;

Alg. 44: Apprentissage SARSA.

SARSA permet d’apprendre une politique à l’aide de simulations selon un taux


d’apprentissage α, une probabilité d’exploration , et un facteur de dépréciation
γ ; couramment, on utilise α = 0.1,  = 0.9 et γ = 0.9 ; l’initialisation des valeurs
q avec des valeurs aléatoires permet d’accélérer le processus d’apprentissage ;
le défaut de SARSA est de produire un apprentissage dédié à l’environnement
considéré ; si l’environnement change, la fonction apply et le test de terminaison
changent, et la fonction Q apprise n’est alors plus valable.

35. SARSA vient du quintuplet {S, A, R0 , S 0 , A0 } définissant la politique optimale ; pour


une action A appliquée sur un état S, on obtient une récompense R0 et un nouvel état S 0 (cf.
Alg. 44 ligne 6).

66
L’apprentisage Q-learning reprend le principe de SARSA en séparant politique
apprise et politique suivie ; cette solution augmente la vitesse de convergence de
l’apprentissage ; la ligne 8 de Alg. 44 devient :

q(S, A) ← q(S, A) + α(R0 + γ maxa (q(S 0 , a)) − q(S, A))

L’ensemble Q n’est pas toujours utile dans sa totalité ; selon les conditions défi-
nies par les états et les actions, certains états sont non atteignables ; pour éviter
de définir initialement l’ensemble Q, Alg. 45 présente une variante d’apprentis-
sage Q-learning dans lequel Q est défini incrémentalement.

1 for nb_episodes do
2 S ← initialize ( ) ;
3 if {S, randomMove ( ) } ∈
/ Q then
4 for each m in nextMove ( S ) do
5 update ( Q, {S, m}, random ( )) ;
6 A ← -greedy ( S, Q) ;
7 for nb_loop do
8 {S 0 , R0 } ← apply ( S, A) ;
9 if {S, A} ∈ / Q then
10 for each m in nextMove ( S ) do
11 update ( Q, {S, m}, random ( )) ;
12 A0 ← -greedy ( S 0 , Q) ;
13 q(S, A) ← q(S, A) + α ( R0 + γ maxa (q(S 0 , a)) − q(S, A)) ;
14 {S, A} ← {S 0 , A0 } ;
15 if S == terminal then break ;

Alg. 45: Apprentissage Q-learning ; dans la variante présentée içi, seuls


les états rencontrés pendant l’apprentissage sont insérés dans Q.

La fonction update met à jour Q en y ajoutant une évaluation pour un couple


état-action ; comme dans Alg. 44, l’initialisation des valeurs q avec des valeurs
aléatoires permet d’accélérer le processus d’apprentissage.

L’apprentisage SARSA-λ reprend le principe de SARSA en remplaçant l’esti-


mation T D(0) par l’estimation T D(λ) ; deux implémentations sont possibles :
1) la première selon la formule originelle présentée dans Alg. 39 ; 2) la seconde
selon la combinaison de TD avec les traces d’éligibilité présentée dans Alg. 40.

67
Alg. 46 présente l’apprentissage SARSA-λ selon la formule originelle de l’esti-
mation T D(λ).

1 for each q in Q do q ← random ( ) ;


2 for nb_episodes do
3 S ← initialize ( ) ;
4 A ← -greedy (S, Q) ;
5 for nb_loop do
6 {S 0 , R0 } ← apply (S, A) ;
7 A0 ← -greedy (S 0 , Q) ;
q(S, A) ← q(S, A) + α(R0 + γ 0 0
P
8 a π(a|S )q(S , a) − q(S, A)) ;
0 0
9 {S, A} ← {S , A } ;
10 if S == terminal then break ;

Alg. 46: Apprentissage SARSA-λ dans sa forme originelle.

En ligne 8, les états suivants de S sont définis par un critère de performance 36


selon l’application de la politique π à partir de S 0 ; cette solution implique de
stocker l’historique des transitions utilisées.

Alg. 47 présente l’apprentissage SARSA-λ selon la combinaison de TD avec les


traces d’éligibilité maintenues dans une table définie par la fonction z.

1 for each s ∈ S , a ∈ A do q(s, a) ← random ( ) ;


2 for nb_episodes do
3 S ← initialize ( ) ;
4 A ← -greedy (S, Q) ;
5 for each s ∈ S , a ∈ A do z(s, a) ← q(s, a) ;
6 for nb_loop do
7 {S 0 , R0 } ← apply (S, A) ;
8 A0 ← -greedy (S 0 , Q) ;
9 update ( zs ) ;
10 for each s ∈ S, a ∈ A do
11 q(s, a) ← q(s, a) + α z(s, a) (R0 + γq(S 0 , A0 ) − q(s, a)) ;
12 z(s, a) ← γλ z(s, a) ;
13 {S, A} ← {S 0 , A0 } ;
14 if S == terminal then break ;

Alg. 47: Apprentissage SARSA-λ par combinaison de TD avec les traces


d’éligibilité.

La fonction update appelée ligne 9 réalise une MAJ accumulative ou une MAJ
avec réinitialisation (conformément à sa définition page 56).

36. Les quatres valeurs courantes du critère de performance sont la somme de taille fixe, la
somme gamma-pondérée, la somme totale et la moyenne (cf. section 9.1) ; pour T D(2) avec
la somme gamma-pondérée pour critère de performance de la politique π appliquée à partir
de S 0 , on a : a π(a|S 0 )q(S 0 , a) = (R0 + γ R00 + γ 2 q(S 00 , A00 )).
P

68
Le double apprentissage propose d’apprendre deux fonctions d’évaluation Q1 et
Q2 simultanément pour réduire les écarts entre les valeurs estimées et les valeurs
optimales et ainsi accélerer la convergence de la politique vers la politique op-
timale ; ce principe s’applique à SARSA, au Q-learning et à SARSA-λ ; Alg. 48
présente ce principe appliqué à SARSA.

1 for each q in Q1 do q ← random ( ) ;


2 for each q in Q2 do q ← random ( ) ;
3 for nb_episodes do
4 S ← initialize ( ) ;
5 if 0.5 < random ( ) then
6 A ← -greedy (S, Q1 ) ;
7 else
8 A ← -greedy (S, Q2 ) ;
9 for nb_loop do
10 {S 0 , R0 } ← apply (S, A) ;
11 if 0.5 < random ( ) then
12 A0 ← -greedy (S 0 , Q1 ) ;
13 qtarget ← R0 + γ q2 (S 0 , argmaxa q1 (S 0 , a)) ;
14 q1 (S, A) ← q1 (S, A) + α (qtarget − q1 (S, A)) ;
15 else
16 A0 ← -greedy (S 0 , Q2 ) ;
17 qtarget ← R0 + γ q1 (S 0 , argmaxa q2 (S 0 , a)) ;
18 q2 (S, A) ← q2 (S, A) + α (qtarget − q2 (S, A)) ;
19 {S, A} ← {S 0 , A0 } ;
20 if S == terminal then break ;

Alg. 48: Double apprentissage appliqué à SARSA.

La fonction argmaxa q1 (S 0 , a) retourne la meilleure action appliquée à S 0 selon


Q1 (respectivement selon Q2 pour argmaxa q2 (S 0 , a)).

Les fonctions d’évaluation Q1 et Q2 sont selectionnées aléatoirement et avec des


probabilités de 50%.

69
9.7 Apprentissage profond
9.8 Application aux jeux Atari 2600

70
9.9 Application à StarCraft-II
StarCraft-II (abrégé SC2) est un jeu de stratégie temps-réel dans lequel trois
races s’affrontent pour le contrôle de cartes ; les trois races sont les Terran,
les Protoss et les Zerg ; les cartes contiennent des minerais qui permettent de
construire des bâtiments et de produire des unités ; chaque unité possède une
capacité particulière ; les pages suivantes présentent 37 les unités de chaque race,
leurs particularités, leur coût en ressources, leur coût en support et leur temps
de production ; on note « attaque T » pour les unités pouvant attaquer les unités
terrestres adverses et « attaque A » pour les unités pouvant attaquer les unités
aériennes adverses ; les coûts sont en M, G et E, pour Minerai, Gaz et Energie ;
le support (abrégé Sup.) est noté en places dans les unités de ravitaillement (i.e.
les Supply depot pour les Terran, les Overlord pour les Zerg et les Pylon pour
les Protoss) ; une unité est caractérisée par des points de vie (abrégés Pv.), une
armure (abrégée Arm.), une vitesse de déplacement (abrégée Vit.), des dégats
par seconde (abrégés Dps.), un temps de cooldown 38 (abrégé Coold.), une portée
et une vision (abrégée Vis.) ; les temps sont en secondes ; les unités Protoss ont
en plus une caractéristique de shield (abrégé Shd.), dépendant d’une gestion
d’énergie.

Les unités sont regroupées en deux catégories : les unités légères (abrégées Leg.)
et les unités blindées (abrégées Bld.) ; selon ces deux catégories, les dégats infli-
gés peuvent varier.

La résolution d’une attaque dépend des unités et des cas ; dans le cas général,
les dommages minimum d’une attaque sont de 1 ; les dommages maximum sont
les dégats de l’unité attaquante moins l’armure de l’unité recevant l’attaque ;
pour une unité Protoss, on commence par prendre sur le bouclier (i.e. Shd.) ;
quand le bouclier n’est plus, on utilise le cas général 39 :

dommages = max(1, dégats - armure)

Concernant le bouclier des unités Protoss, la vitesse de régénération dépend des


unités et du temps écoulé depuis la dernière attaque ; le bouclier ne se régénère
pas pendant les combats ; la régénération est accélérée après 10 secondes sans
dommage.

Les vitesses de déplacement et les temps de cooldown sont içi volontairement


présentés avec un seul chiffre après la virgule (qui semble suffisant).
37. Il existe à ce jour trois campagnes, correspondant aux trois races : Wings of liberty
centrée sur les Terran qui est sortie en 2010 ; Heart of the swarm centrée sur les Zerg qui
est sortie en 2013 ; Legacy of the void centrée sur les Protoss qui est sortie en 2015 ; la
présentation qui est faite dans les tableaux suivants prend en compte les unités standard et
pas les unités partculières de ces campagnes ; les caractéristiques des unités données içi sont
issues de https://liquipedia.net/starcraft2.
38. Le temps de rechargement est défini par un temps de refroidissement inter-attaque,
nommé weapon_cooldown ; après une attaque, une unité doit attendre un temps de cooldown
avant de pouvoir attaquer de nouveau.
39. Selon les armes utilisées, on aura également des dommages de zone (abrégé AOE).

71
Les unités Terran

Unité Coût Sup. Tps. Fonction


SCV 50M 1 12 récolte le minerai et le gaz,
construit et répare les bâtiments,
attaque T
MULE - - - unité minière temporaire
Marine 50M 1 18 attaque T et A
Reaper 50M+50G 1 32 monte et descend les falaises,
attaque T
Marauder 100M+25G 2 21 attaque T
Ghost 150M+125G 2 29 attaque T et A,
attaque furtive et nucléaire
Hellion 100M 2 21 attaque T enflammée
Hellbat 100M 2 21 attaque T enflammée
Widow mine 75M+25G 2 21 attaque T et A camouflée
Siege tank 150M+125G 3 32 attaque T
Cyclone 150M+100G 3 32 attaque T
Thor 300M+200G 6 43 attaque T et A
Viking 150M+75G 2 30 attaque T et A
Medivac 100M+100G 2 30 soigne et transporte
Liberator 150M+150G 3 43 attaque T et A
Raven 100M+200G 2 43 détecte les furtifs et attaque A
Banshee 150M+100G 3 43 attaque T
Battlecruiser 400M+300G 6 64 attaque T et A
Auto-turret 50E 0 0 attaque T et A

Table 4 – Coûts et fonctions des unités Terran.

Le bâtiment Command center supporte les unités, stocke minerai et gaz, per-
met de construire le bâtiment Engineering bay et produit des unités SCV ;
les bâtiments Planetary Fortress et Orbital Command sont des évolutions du
Command center ; le Planetary Fortress attaque T et A ; le Orbital Command
peut produire l’unité MULE, demander un Extra supplies et réaliser des Scanner
Sweep ; le bâtiment Supply depot supporte les unités et permet de construire des
Barracks ; le bâtiment Refinery permet la récolte du gaz ; le Barracks permet
de construire les bâtiments Orbital command, Bunker, Factory, Ghost academy
et de produire les unités Marine, Marauder, Reaper et Ghost ; le Engineering
bay permet de construire les bâtiments Missile turret, Sensor tower, Planetary
fortress et de développer les améliorations Infantry weapons, Armors, Hi-sec,
Neosteel ; le Sensor tower possède une vision avec une portée de 12 ; le Missile
turret possède des attaque A avec une portée de 7, avec 39.3 de Dps., un Cool-
down de 0.6 et une vision de 11 ; le Factory permet de construire les bâtiments
Starport, Armory, Tactical nuke, et de produire les unités Hellion, Widow mine,
Siege tank, Cyclone, Hellbat et Thor ; le Ghost academy permet de produire les
unités Ghost et les capacités associées ; le Starport permet de construire le bâ-
timent Fusion core et de produire les unités Viking, Medivac, Raven, Banshee,
Liberator et Battlecruiser ; le bâtiment Armory permet de produire les unités
Thor, Hellbat et de développer Vehicule weapons, Vehicule skills, Ship weapons ;

72
le Fusion Core permet de produire l’unité Battlecruiser ; le Tech lab permet de
développer des technologies relatives aux unités et aux bâtiments ; le bâtiment
Reactor permet d’augmenter la capacité de production de certains bâtiments.

Les bâtiments Command center, Orbital command, Barracks, Factory et Star-


port ont la particularité de pouvoir voler à une vitesse de 1.31 afin de replacer un
bâtiment ou d’échapper à une unité ennemie ; le Command center peut charger
5 SCV et 10 avec un upgrade.

Le support est de 15 pour un Command center et de 8 pour un Supply depot.

Les bâtiments Terran ont également la particularité d’être réparables par les
SCV.

Bâtiments Coût Tps Pv. Arm.


Command center 400M 21 400 1:3
Planetary Fortress 150M+150G 36 1500 3:5
Orbital Command 150M 25 1500 1:3
Supply depot 100M 21 400 1:3
Refinery 75M 21 500 1:3
Barracks 150M 46 1000 1:3
Engineering bay 125M 25 850 1:3
Bunker 100M 29 400 1:3
Sensor tower 125M+100G 18 200 0:2
Missile turret 100M 18 250 0:2
Factory 150M+100M 43 1250 1:3
Ghost academy 150M+50G 29 1250 1:3
Starport 150M+100G 36 1300 1:3
Armory 150M+100G 46 750 1:3
Fusion Core 150M+150G 46 750 1:3
Tech lab 50M+25G 18 400 1:3
Reactor 50M+50G 36 400 1:3

Table 5 – Coûts et caractéristiques des bâtiments Terran.

Les unités peuvent voir leurs capacités augmentées avec les stimpack (abrégés
Spk.), les Infernal Pre-Igniter (abrégés Ipi.) ; les Spk. durent 11 sec et les at-
taques splash (abrégées Spl.) augmentent les dégats ; l’utilisation de Spk. coûtent
10 Pv. aux unités Marines et 20 aux unités Marauder.

La stratégie classique avec les Terran est de déplacer les bâtiments, de multiplier
les bases et de pratiquer l’harassement des adversaires sur la carte.

73
Unité Pv. Arm. Vit. Dps. Coold. Portée Vis.
SCV 45 0:1 3.9 5 1 0.1 8
MULE 60 0:1 3.9 - - - 8
Marine 45:55 0:1 3.1 9.8 : 11.4 0.6:0.8 5 9
(+ Spk.) +1.6 +5.9
Reaper 60 0:1 5.2 10.1 : 12.6 0.8 5 9
Marauder 125 1:2 3.1:4.7 9.3 : 10.4 0.7:1 6 10
(+Spk.) +1.4 +5.9
(vs. Bld.) +9.3 : 10.2
(vs. Bld. + Spk.) +14.1 : 16.9
Ghost 100 0:1 3.9 9.3 : 10.2 1.1 6 11
(vs. Leg.) +9.3 : 11
Hellion 90 0:1 6 4.5 : 5 1.8 5 10
(vs. Leg.) +3.4 : 4
(vs. Leg. + Ipi.) +6.2 : 6.7
Hellbat 135 0:1 3.2 12.6 : 14 1.4 2 10
(vs. Leg.) +0 : 0.7
(vs. Leg. + Ipi.) +8.4 : 9.1
Widow mine 90 0:1 3.9 125 - 5 7
(+Spl.) (+40)
(vs. Shd. (+Spl.)) +35(+25)
Siege tank 175 1:2 3.15 20.3 : 23 1 7 11
(vs. Bld.) +13.5 : 14.8
(siège) 0 18.7 : 20.5 2.1 13 11
(siège vs. Bld.) +14 : 14.5
Thor 400 2:3 2.6 65.9.2 : 72.5 0.9 7 10
(expl.) 11.2 : 13.1 2.1 10
(expl. vs. Leg.) +11.2 : 13.1
(impact.) 16.3 : 17.7 2.1
(impact. vs. Bld.) +7 : 7.9 2.1
Viking 135 0:1
(att. T) 3.1 16.8 : 18.2 0.7 6 10
(att. T. vs. Mec.) +11.3
(att. A) 3.8 14 : 15.4 1.4 9
(att. A. vs. Bld.) +5.6
Medivac 150 1:2 3.5:5.9 - - - 11
Liberator 180 0:1 4.7
(att. A) 65.8 : 70.2 1.1 10 : 14 10
(mode def. att. T) 7.8 : 9.2 1.3 5 13:17
Raven 140 1:2 3.9 - - - 11
Banshee 140 0:1 3.9:5.2 27 : 29.2 0.9 6 10
Battlecruiser 550 3:4 2.6
(att. T) 50 : 56.2 0.2 6 12
(att. A) 37.5 : 43.7 0.2 6 12
Auto-turret 150 1:3 - 31.2 0.6 6:7 7

Table 6 – Caractéristiques des unités Terran.

74
Les unités Zerg

Unité Coût Sup. Tps. Fonction


Larva - - - se transforme en unité
Drone 50M 1 12 récolte le minerai et le gaz,
se transforme en bâtiment,
attaque T
Queen 150M 2 36 attaque T et A, infecte
Zergling 25M 1 17 attaque T, évolue en Baneling
Baneling 25M+25G 0.5 14 attaque T suicide
Roach 75M+25G 2 19 attaque T, se soigne enfouie
évolue en Ravager
Ravager 25M+75G 3 9 attaque T, se soigne enfouie
Hydralisk 100M+50G 2 24 attaque T et A, évolue en Lurker
Lurker 50M+100G 3 18 attaque T
Viper 100M+200G 3 29 lance des sorts
Mutalisk 100M+100G 2 24 attaque T et A
Corruptor 150M+100G 2 29 attaque A suicide,
évolue en Brood lord
Swarm host 100M+75G 3 29 crée des Locust
Locust - - 3.6 attaque T
Infestor 100M+150G 2 36 infeste les unités ennemies
Ultralisk 300M+200G 6 39 attaque T
Brood lord 150M+150G 4 24 attaque T
Broodling - - - attaque T, suit le Brood lord
Overlord 100M 0 18 supporte les unités,
évolue en Overseer
Overseer 50M+50G 0 12 supporte les unités,
crée des Changeling
Changeling 50E 0 0 se transforme en unité ennemy
Infested terran 25E 0 3 attaque T
Nydus worm 100M+100G 0 14 crée un tunnel

Table 7 – Coûts et fonctions des unités Zerg.

Pour chaque unité Larva, il est possible d’obtenir : un Drone ou deux Zergling
ou un Hydralisk ou un Roach ou un Infestor ou un Swarm Host ou un Ultralisk
ou un Overlord ou un Mutalisk ou un Corruptor ou un Viper ; les Larva appa-
raissent automatiquement autour des bâtiments Hatchery, Lair, et Hive ; leur
fréquence d’apparition est de une Larva toutes les 11 secondes ; le nombre de
Larva non transformée est limité à 3 ; pendant sa transformation, la Larva de-
vient un cocon, dont avec 200 Pv. et 10 d’armure (sauf pour le Baneling 50 Pv.
et 2 en Arm., pour Overser et Broodlord 2000 Pv. et 2 en Arm., pour le Ravager
100 Pv. et 5 en Arm. et pour le Lurker 100 Pv. et 1 en Arm. ; une Queen est
une unité produite par un bâtiment Hatchery après avoir construit un Spawning

75
pool ; la Queen possède le sort Spawn larva qui permet d’augmenter la produc-
tion de Larva ; le Brood lord est obtenu par évolution de Corruptor ; il apparait
avec des Broodling ; le Infested terran est obtenu par lancement d’un sort sur un
Terran par un Imphestor ; la transformation prend 3 secondes ; l’oeuf possède
70 Pv. et 2 d’armure ; le Nydus worm est constructible dans les parties visibles
de la carte, après construction d’un Nydus network.

Bâtiments Coût Tps Pv. Arm.


Hatchery 300M 71 1500 1
Spine crawler 100M 36 300 2
Spore crawler 75M 21 400 1
Extractor 25M 21 500 1
Spawning pool 200M 46 1000 1
Evolution chamber 75M 25 750 1
Roach warren 150M 39 850 1
Baneling Nest 100M+50G 43 850 1
Creep tumor - 11 50 0
Lair 150M+100G 57 2000 1
Hydralisk den 100M+100G 29 850 1
Lurker den 100M+150G 86 850 1
Spire 200M+200G 71 850 1
Nydus network 150M+200G 36 850 1
Infestation pit 100M+100G 36 850 1
Hive 200M+150G 71 2500 1
Greater spire 100M+150G 71 1000 1
Ultralisk cavern 150M+200G 46 850 1

Table 8 – Coûts et caratéristiques des bâtiments Zerg.

Le bâtiment Hatchery supporte les unités, stocke minerai et gaz, permet de


construire les bâtiments Spawning Pool et Evolution Chamber, et de produire
des Larva et une Queen ; les bâtiments Lair et Hive sont des évolutions du Hat-
chery ; le bâtiment Spine crawler est une défense T de 18.9 Dps. avec un bonus
de 3.8 Dps contre les blindés ; la portée est de 7, la vision de 11, le Cooldown de
1.32 ; le bâtiment Spore crawler est une défense A de 24.4 Dps. (doublé contre
les unités biologiques) ; la portée est de 7, la vision de 11 et le Cooldown de 0.6 ;
le bâtiment Extractor permet la récolte du gaz ; le Spawning pool permet de
construire Spine crawler, Spore crawler, Lair, Roach warren, Baneling nest et
de produire les unités Zergling et Queen ; le Evolution chamber permet d’amélio-
rer les unités ; le Roach warren permet de produire les unités Roach et Ravager ;
le Baneling nest permet de produire des unités Baneling ; le Creep tumor per-
met d’augmenter la vitesse de transformation et les caractéristiques (telles que
la vitesse des déplacement) des unités ; le Hydralisk den permet de construire le
bâtiment Lurker den et de produire des unités Hydralisk ; le Lurker den permet
de produire des unités Lurker ; le Spire permet de produire des unités Mutalisk
et Corruptor, d’améliorer les unités volantes et évolue en Greater spire ; le Ny-
dus network permet d’obtenir des Nydus worm qui permettent des déplacements
rapides sur la carte entre Nydus worm ; le Infestation pit permet de construire

76
les bâtiments Hive, Infestor et Swarm host ; le Hive permet de construire les
bâtiments Ultralisk caverne, Greater spire et de produire des unités Viper ; le
Greater spire permet de produire des unités Brood lord, et d’améliorer les unités
volantes ; le Ultralisk cavern permet de produire des unités Ultralisk.

Le support est de 6 pour une Hatchery, de 8 pour un Overlord et de 8 pour un


Overseer.

Unité Pv. Arm. Vit. Dps. Coold. Portée Vis.


Larva 25 10 0.8 - - - 5
Drone 40 0:1 3.9 4.7 1.1 0.1 8
Queen 175 1:2 1.3 : 3.5 9
att. T 11.2 : 14 0.7 5
att. A 12.6 : 14 0.7 8
Zergling 35 0:1 4.1 : 6.8 10 : 12 0.3:0.5 0.1 8
(Ad. Glands) 14.3 : 17.2
Baneling (+Spl.) 30:35 0:1 3.5 : 4.1 10(+2) 8
(vs. Leg.) +15(+2)
(vs. Bât.) 80(+5)
Roach 145 1:2 3.2 : 4.2 11.2 : 12.5 1.4 4 9
Ravager 120 1:2 3.9 14 : 15.7 1.1 6 9
Hydralisk 90 0:1 3.2 : 3.9 22.4 : 24.2 0.5 5:6 9
Lurker 200 1:2 4.1 : 4.6 14 : 15 1.4 9 9
(vs. Bld) +7 : 7.7
Viper 150 1:2 4.1 - - - 11
Mutalisk 120 0:1 5.6 8.3 : 9.3 1.1 3 11
(2nd rebond) 2.8 : 3.1
(3ème rebond) 0.9 : 1
Corruptor 200 2:3 4.7 10.3 : 11.1 1.4 6 10
(vs. Mass.) +4.4 : 5.1
Swarm host 160 1:2 3.2 - - - 10
Locust 50 0:1 2.6 23.3 : 25.6 0.4 6 6
Infestor 90 0:1 3.2 - - - 10
Ultralisk 500 2:5 4.1 57.4 : 62.3 0.6 1 9
Brood lord 225 1:2 2 11.2 : 12.3 1.8 10 12
Broodling 30 0:1 5.4 8.7 : 10.9 0.5 0.1 7
Overlord 200 0:1 0.9 : 2.6 - - - 11
Overseer 200 1:2 2.6 : 4.7 - - - 11
Changeling 5 0 3.2 - - - 8
Infested terran 50 0:1 1.3 9
Gau. Rif. 10 : 11.6 0.6 5
Inf. Roc. 14.7 : 15.8 1 6

Table 9 – Caractéristiques des unités Zerg.

77
Les attaques du Baneling sont suicidaires et leur explosion implique une aire
d’effet ; leur dommage dépend du type de la cible ; ainsi les dommages sur les
unités légères seront de 25 si une unité légère était la cible et seront de 15 si
la cible était un bâtiment proche ; chaque attaque est sujette à des bonus splash.

Les bâtiments Zerg produisent un mucus qui se répand sur la carte ; Roach,
Ravager, Hydralisk, Lurker, Swarm host, Locust, Infestor, Ultralisk obtiennent
un bonus en vitesse de 30% sur ce mucus.

Les caractéristiques du Changeling varient légèrement selon les unités qu’il copie.

La stratégie classique avec les Zerg est de réaliser des rush avec des groupes de
Zergling.

Les unités Protoss

Unité Coût Sup. Tps. Fonction


Probe 50M 1 12 collecte le minerai et le gaz,
construit les bâtiments,
attaque T
Zealot 100M 2 27 attaque T
Stalker 125M+50G 2 30 attaque T et A
Sentry 50M+100G 2 26 attaque T et A
Adept 100M+25G 2 27 attaque T
High templar 50M+150G 2 39 attaque T
Dark templar 125M+125G 2 39 attaque T
Archon (variable) 4 9 attaque T et A
Observer 25M+75G 1 21 espionne et détecte les furtifs
Warp prism 200M 2 36 transporte des unités
Immortal 250M+100G 4 39 attaque T
Colossus 300M+200G 6 54 attaque T
Disruptor 150M+150G 3 36 invoque une purification
Phoenix 150M+100G 2 25 attaque A
Void ray 250M+150G 4 43 attaque A et T
Oracle 150M+150G 3 37 attaque T
Tempest 300M+200G 6 43 attaque T et A
Carrier 350M+250G 6 86 transporte des Interceptor
(max 8)
Interceptor 15M - - attaque T et A

Table 10 – Coûts et fonctions des unités Protoss.

Le coût de l’unité Archon varie selon les cas : 1) avec 2 High templar, il est de
100M + 300G, 2) avec 1 High templar et 1 Dark templar, il est de 175M + 275G,
3) avec 2 Dark templar, il est de 250M + 250G.

Le coût des unités pour leur transport via Warp prism varie selon les unités :
il est de 1 pour une unité Probe, de 2 pour une unité Zealot, Sentry, Stalker,

78
Adept, High Templar, Dark Templar, de 4 pour une unité Immortal, Disruptor,
Archon, et de 8 pour une unité Colossus ; le Warp prism possède une capacité
de transport de 8.

Bâtiments Coût Tps Pv. Shd. Arm.


Nexus 400M 71 1000 1000 1
Pylon 100M 18 200 200 1
Assimilator 75M 21 450 450 1
Gateway 150M 46 500 500 1
Forge 150M 32 400 400 1
Cybernetics core 150M 36 550 550 1
Photon cannon 150M 29 150 150 1
Robotics facility 200M+100G 46 450 450 1
Warp gate - 7 500 500 1
Stargate 150M+150G 43 600 600 1
Twilight council 150M+100G 36 500 500 1
Robotics bay 200M+200G 46 500 500 1
Shield battery 100M 29 200 200 1
Fleet beacon 300M+200G 43 500 500 1
Templar archives 150M+200G 36 500 500 1
Dark shrine 150M+150G 71 500 500 1

Table 11 – Coûts et caractéristiques des bâtiments Protoss.

Le bâtiment Nexus supporte les unités, stocke minerai et gaz, permet de construire
les bâtiments Gateway et Forge, et produit des unités Probe et Mothership ; le
bâtiment Pylon supporte les unités et fournit de l’énergie aux autres bâtiments
situés jusqu’à une distance de 9 ; sans énergie, les bâtiments Protoss sont hors-
service ; le bâtiment Assimilator permet la récolte du gaz ; le Gateway permet de
construire le bâtiment Cybernetics core, et de produire les unités Zealot, Stalker,
Sentry, Adept, High templar et Dark templar ; le Forge permet de construire le
bâtiment Photon cannon, et d’améliorer les unités terrestres ; le Photon cannon
est une défense T et A de 22.4 Dps ; la portée est de 7, la vision de 11, le Co-
oldown de 0.9 ; le Cybernetics core permet de construire les bâtiments Twilight
council, Stargate, Robotics facility, Shield battery et Warp gate, et de produire
les unités Sentry et Adept, et d’améliorer les unités volantes ; le Stargate permet
de construire le bâtiment Fleet beacon, et de produire les unités Phoenix, Oracle,
Void ray, Tempest et Carrier ; le Twilight council permet de construire les bâti-
ments Templar archives et Dark shrine ; le Robotics bay permet de produire les
unités Colossus et Disruptor ; le Shield battery permet de recharger les boucliers
des unités et des bâtiments plus rapidement ; le Fleet beacon permet de produire
les unités Carrier, Mothership et Tempest ; le Templar archives permet de pro-
duire les unités High templar, et Archon ; le Dark shrine permet de produire les
unités Dark templar, et Archon.

Le support est de 15 pour un Nexus et de 8 pour un Pylon.

79
Unité Pv. Shd. Arm. Vit. Dps. Coold. Portée Vis.
Probe 20 20 0:1 3.9 4.7 1.1 0.1 8
Zealot 100 50 1:2 3.2 18.6 : 20.9 0.9 0.1 9
(+ Charge) 4.1:4.6
Stalker 80 80 1:2 4.1 9.7 : 10.5 1.3 6 10
(vs. Bld.) +3.7 :4.5
Sentry 40 40 1:2 3.2 8.4 : 9.8 0.7 5 10
Adept 70 70 1:2 3.5 6.2 : 6.8 1.1:1.6 4 9
(vs. Leg.) +7.5:8.1
High templar 40 40 0:1 2.6 3.2 : 4 1.2 6 10
Dark templar 40 80 1:2 3.9 37.2 : 41.3 1.2 0.1 8
Archon 10 350 0:1 3.9 20 : 22.4 1.3 3 9
(vs. Bio) +8:8.8
Observer 40 20 0:1 2.6:3.9 - - - 11:14
Warp prism 80 100 0:1 4.1:5.4 - - - 10
Immortal 200 100 1:2 3.1 19.2 : 21.1 1.1 6 9
(vs. Bld.) +28.9 : 31.8
Colossus 200 150 1:2 3.2 18.7 : 20.6 1.1 7:9 10
(vs. Leg.) +9.3 : 11.2
Disruptor 100 100 1:2 3.2 9
Phoenix 120 60 0:1 6 12.7 : 15.2 0.8 5:7 10
(vs. Leg.) +12.7
Void ray 150 100 0:1 3.5 16.8 : 19.6 0.4 6 10
(+ Prism.) -1.4 +11.2
(vs. Bld.) +28
Oracle 100 60 0:1 5.6 24.4 0.6 4 10
Tempest 300 150 2:3 2.6 12
(att. T) 17 : 18.7 2.4 10
(att. A) 12.7 : 14.0 2.4 15
(vs. Mass.) +9.3 : 10.2
Carrier 250 150 2:3 2.6 8 12
Interceptor 40 40 0:1 2.1 4.7 : 5.6 2.1 +2 7
Mothership 350 350 2:3 2.6 22.8 : 26.6 1.6 7 14
Stasis ward 30 30 0:1

Table 12 – Caractéristiques des unités Protoss.

Les unités Zealot peuvent accélerer avec la capacité Charge ; les Archon ont des
bonus contre les unités biologiques (abrégées Bio.) ; les Tempest ont des bonus
contre les unités massives (abrégées Mass.).

La stratégie classique de Protoss est de profiter de la téléportation entre Pylon,


ce qui permet de transférer des unités et d’envahir les bases ennemies.

80
PySC2
Le kit PySC2 2.0.1 permet d’écrire en Python des IA jouant à SC2 ; ce kit per-
met le lancement de missions SC2, la récupération des perceptions (içi appelées
observations), la prise de décision (içi appliquée au travers d’actions) et la ges-
tion de replay ; ce kit contient également des problèmes à un joueur, appelés
minigames ; avec Python 3.5.2 et PySC2 2.0.1, on peut lancer un joueur vide
(présenté ci-dessous) dans un minigame :
python3 -m pysc2.bin.agent --map BuildMarines \
--agent empty.Simple
1 from pysc2.agents import base_agent
2 from pysc2.lib import actions
3

4 class Simple(base_agent.BaseAgent):
5 def step(self, obs):
6 super(Simple, self).step(obs)
7 return actions.FUNCTIONS.no_op()

Pour le minigame CollectMineralShards, il faut préciser une version de proto-


col :
python3 -m pysc2.bin.agent --map CollectMineralShards \
--agent empty.Simple --sc2_version 3.16.1

Pour les minigames CollectMineralsAndGas, DefeatRoaches, DefeatZerglingsAnd-


Banelings, FindAndDefeatZerglings, et MoveToBeacon, il suffira de faire comme
pour BuildMarines ; les missions des minigames sont définies dans le fichier
docs/mini_games.md de PySC2.

L’option ––step_mul permet de passer N observations ; la valeur par défaut est


de 8 ; ce qui signifie que le joueur programme agit toutes les 8 itérations de si-
mulation ; avec ––step_mul 1, on augmente la réactivité du joueur programme.

Pour avoir la liste des options possibles et leur valeur par défaut :
python3 -m pysc2.bin.agent --helpfull

Pour avoir la liste des 541 actions possibles 40 :


python3 -m pysc2.bin.valid_actions

Pour avoir la liste des cartes disponibles :


python -m pysc2.bin.map_list

40. Le lecteur concerné pourra faire une doc txt des fonctions correspondants aux actions
avec une simple redirection : cmd > doc.txt.

81
Dans un joueur programme, on pourra visualiser la liste des actions possibles à
chaque instant avec :
1 for action in obs.observation.available_actions:
2 print(actions.FUNCTIONS[action])

La correspondance entre actions et fonctions suit le format :


<fun_id>/<fun_name>(<type id>/<type name> [<value size>, *]; *)

Ce qui produit par exemple :


0/no_op ()
1/move_camera (1/minimap [0, 0])
12/Attack_screen (3/queued [2]; 0/screen [0, 0])
13/Attack_minimap (3/queued [2]; 1/minimap [0, 0])

On note que la commande pysc2.bin.valid_actions retourne en plus les va-


leurs max des coordonnées :
0/no_op ()
1/move_camera (1/minimap [64, 64])
12/Attack_screen (3/queued [2]; 0/screen [84, 84])
13/Attack_minimap (3/queued [2]; 1/minimap [64, 64])

Les types sont : screen pour un point sur l’écran ; minimap pour un point
sur la minimap ; screen2 pour un second point 41 sur l’écran ; queued pour
une possibilité d’exécution différée ; control_group_act pour une action de
groupe ; control_group_id pour un groupe ; select_point_act pour une ac-
tion d’une unité en un point ; select_add pour ajouter une unité à une sélection ;
select_unit_act pour une action d’une sélection ; select_unit_id pour une
sélection par id ; select_worker pour une action d’un mineur ; build_queue_id
pour une file d’attente de construction ; unload_id pour une sélection à déchar-
ger.

Les observations sont une abstraction des situations vues à l’écran et de données
supplémentaires (telles que le score et la reward ).

41. Dans PySC2, les fonctions n’acceptent pas deux arguments de même type ; cette restric-
tion semble provenir du principe qu’une fonction comportant deux arguments de même type
est source de confusion et de bug ; en imposant cette restriction, les concepteurs de PySC2
ont probablement pensé réduire les confusions entre les arguments de même type.

82
Système de coordonnées
Dans PySC2, les unités sont positionnées dans deux vues (i.e. deux repères) :
— le screen, correspondant à la vue principale ; la dimension du screen par
défaut est de 84 × 84 ; le screen correspond une vue d’une partie de
la minimap, donnée par la caméra ; pour déplacer la caméra, utiliser la
fonction move_camera.
— la minimap, correpondant à une vue complète de la carte, en basse ré-
solution, située en bas à gauche sur l’interface classique ; elle montre les
hauteurs du terrain, le brouillard, les unités, la partie correspondant à
la vue du screen ; pour les unités, on a le joueur associé avec son type
(ami/ennemi) ; la dimension de la minimap par défaut est de 64 × 64.
Dans le screen comme dans la minimap, le point de coordonnées (0,0) est en
haut à gauche ; les axes sont orientés comme suit :
*---Ox
|
Oy

Les lignes suivantes déplacent la caméra dans le coin supérieur gauche :


1 dest = [int(0), int(0)]
2 return actions.FUNCTIONS.move_camera(dest)
Les lignes suivantes déplacent la caméra dans le coin en haut à droite d’une
minimap de 64 × 64 :
1 dest = [int(63), int(0)]
2 return actions.FUNCTIONS.move_camera(dest)

Pour les observations 42 , les coordonnées 43 sont lues dans l’ordre (y,x) :
1 _UNIT_TYPE = features.SCREEN_FEATURES.unit_type.index
2 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
3 unit_y, unit_x = (unit_type == units.Terran.SCV).nonzero()
On notera l’utilisation de la fonction nonzero qui permet de passer d’une ma-
trice de labels à une liste de coordonnées dans la matrice considérée.

Pour les actions, les coordonnées 44 sont données dans l’ordre (x,y) :
1 dest = [int(unit_x[0]), int(unit_y[0])]
2 return actions.FUNCTIONS.move_screen("now", dest)

42. Les observations sont données en features dont la définition dépend de la résolution du
screen et de la minimap ; de plus, le nombre de ces features dépend de la taille de la fenêtre du
screen ; la position précise d’une unité est définie par le barycentre des positions des features
de cette unité.
43. Les identifiants des unités considérées (içi SCV ) sont définies dans pysc2/lib/units.py.
44. Indiquer des coordonnées négatives est incohérent ; déplacer le screen en dehors de la
minimap est incohérent.

83
Observations
Les observations regroupent un ensemble de matrices et de vecteurs de données ;
Le contenu de cet ensemble est défini dans pysc2/lib/features.py ; le vecteur
score_cumulative contient 13 valeurs ordonnées comme suit :
— score : le score (qui cumule les reward 45 )
— idle_production_time : le temps de production inactive
— idle_worker_time : le temps inactif des unités de collecte de ressources
— total_value_units : la valeur totale des unités
— total_value_structures : la valeur totale des bâtiments
— killed_value_units : la valeur des unités tuées
— killed_value_structures : la valeur des bâtiments détruits
— collected_minerals : le minerai collecté
— collected_vespene : le gaz collecté
— collection_rate_minerals : le taux de collecte du minerai
— collection_rate_vespene : le taux de collecte du gaz
— spent_minerals : le minerai dépensé
— spent_vespene : le gaz dépensé
Après 200 tours dans le scénario Simple64, on obtient :
1 print(obs.observation["score_cumulative"])
[1110 12 0 600 400 0 0 60 0 403 0 0 0]
Ce qui correspond à un score de 1110, pour un temps de production inactive de
12, pour 60 de minerai collecté ; on accède également au minerai collecté comme
suit :
1 print(obs.observation["score_cumulative"]["collected_minerals"])
60

De façon similaire, observation contient un vecteur des choses vues par le


joueur (i.e. obs.observation["player"]).

Concernant les unités en jeu, les observations 46 sont :


— dans le screen via obs.observation["feature_screen"]
— dans la minimap via obs.observation["feature_minimap"]

Pour obtenir la liste des features des SCV présents dans le screen, on utilise une
matrice des types des unités (içi unit_type), restreinte aux unités SCV (dans
unit_scv) puis convertie en coordonnées des unités du type SCV à l’aide de la
fonction nonzero :
1 _UNIT_TYPE = features.SCREEN_FEATURES.unit_type.index
2 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
3 unit_scv = (unit_type == units.Terran.SCV)
4 unit_y, unit_x = unit_scv.nonzero()

45. Les reward sont les récompenses immédiates obtenues au cours des missions ; pour De-
featRoaches, les reward sont de -1 par Marine perdu et de +10 par Roach éliminé ; les valeurs
de reward sont visibles aux instants concernés via la variable obs.reward.
46. Dans la version 1.2 de PySC2, feature_screen était screen et feature_minimap était
simplement minimap.

84
Pour une unité, le nombre de features dépend du type de l’unité et de la ré-
solution de la vue considérée (i.e. le screen avec 84 × 84 ou la minimap avec
64 × 64) ; dans la mission DefeatRoaches (qui comporte initialement 9 Marines
et 4 Roaches), avec les résolutions par défaut, on a 81 features Marine et 46
features Roaches 47 :
1 _UNIT_TYPE = features.SCREEN_FEATURES.unit_type.index
2 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
3 m_y, m_x = (unit_type == units.Terran.Marine).nonzero()
4 r_y, r_x = (unit_type == units.Zerg.Roach).nonzero()
5 print("m:%d r:%d" %(len(m_x), len(r_x)))
m:81 r:46

Il est conseillé de conserver la résolution par défaut.

Ci-dessous trois exemples de répartition des features pour des minerais (les
features des minerais sont représentés par des « x » et les espaces par des « . ».)
dans la résolution par défaut (i.e. 84 × 84 pour le screen et 64 × 64 pour la
minimap) :

......
..xx.. un minerai
.xxxx.
.xxxx.
..xx..
......

............
..xx....xx.. deux minerais espacés
.xxxx..xxxx.
.xxxx..xxxx.
..xx....xx..
............

.........
..xx.xx.. deux minerais serrés
.xxxxxxx.
.xxxxxxx.
..xx.xx..
.........

47. Pour des unités espacées, un Marine équivaut à 9 features et un Roach à 12 features ;
pour des unités serrées, ces valeurs peuvent diminuer.

85
Fig. 7 présente l’IHM du kit PySC2 à l’exécution de la mission DefeatRoaches ;
screen et minimap sont présentés dans la partie gauche ; la minimap est une
vue de l’ensemble de la carte, presentée dans un cadre en rouge ; la position
screen dans la minimap est identifié par un cadre blanc ; le screen est présenté
en détail au dessus, avec 4 Marines et 9 Roaches ; dans la partie droite de l’IHM,
différentes features 48 sont présentées ; Fig. 8 montre Fig. 7 dans SC2.

Fig. 7 – Visualisation de la situation et des features au départ de la mission


DefeatRoaches.

Fig. 8 – Situation de Fig. 7 dans SC2.

48. De gauche à droite, sur la première ligne : minimap height_map, minimap visibi-
lity_map, minimap creep, minimap camera, minimap player_id, et sur la deuxième ligne :
minimap player_relative, minimap selected, screen height_map, screen visibility_map, screen
creep, et sur la troisième ligne : screen power, screen player_id, screen player_relative,
screen unit_type, screen selected, et sur la quatrième ligne : screen unit_hit_points, screen
unit_hit_points_ratio, screen unit_energy, screen unit_energy_ratio, screen unit_shields, et
sur la dernière ligne : screen unit_shields_ratio, screen unit_density, screen unit_density_aa,
screen effects.

86
Simplifier les observations
Afin de simplifier la présentation des informations relatives aux unités présentes
dans le screen, il est possible d’activer l’option use_feature_units qui permet
d’obtenir une interprétation des

87
Missions, scénarios et épisodes
Les missions associées aux minigames sont définies dans docs/mini_games.md.

Un scénario décrit le déroulement d’une mission ; un scénario est composé de


un à N épisodes ; le joueur programme ci-dessous (nommé print_steps.py)
présente les étapes d’un scénario :
1 from pysc2.lib import actions
2

3 class SimpleAgent(object):
4 def __init__(self):
5 self.episodes = -1
6 self.steps_episode = -1
7 self.steps_game = -1
8 print("-- __init__")
9

10 def __del__(self):
11 print("-- __del__")
12

13 def setup(self, obs_spec, action_spec):


14 print("-- setup")
15

16 def reset(self):
17 self.steps_episode = -1
18 self.episodes += 1
19 print("-- -- reset")
20

21 def step(self, obs):


22 self.steps_episode += 1
23 self.steps_game += 1;
24 if((self.steps_game % 1000) == 0):
25 print("-- -- step %d episode %d (%d)" /
26 %(self.steps_game, self.episodes, self.steps_episode))
27 return actions.FUNCTIONS.no_op()

Les fonctions __init__ et setup sont appelées une seule fois au début de l’en-
semble des épisodes ; la fonction __init__ initialise les variables locales et la
fonction setup définit les observations et actions possibles ; la fonction reset
est appelée au début de chaque nouvel épisode ; la fonction step est appelée à
chaque itération de simulation 49 ; la fonction __del__ est appelée à la fin d’un
scénario.

49. La fréquence d’appel de la fonction step est liée avec la valeur de step_mul ; pour un
step_mul de 1, selon les spécifications de PySC2, le simulateur appelle de 16 à 22 fois la
fonction step par seconde ; pour un temps de 10 secondes, on a un nombre théorique d’appels
à la fonction step compris entre 160 et 220 ; en pratique, on observe parfois des variations de
10 à 30 appels par seconde.

88
L’exécution de la mission DefeatRoaches avec 2 épisodes produit le scénario sui-
vant 50 :
python3 -m pysc2.bin.agent --map DefeatRoaches \
--agent print_steps.SimpleAgent --max_episodes 2
-- __init__
-- setup
-- -- reset
-- -- step 0 episode 0 (0)
-- -- step 1000 episode 0 (1000)
-- -- reset
-- -- step 2000 episode 1 (80)
-- -- step 3000 episode 1 (1080)
-- __del__

Les épisodes permettent de répéter une même mission, correspondant à une suite
d’appels aux fonctions reset et step, en suivant le schéma défini par Alg. 49.

1 call __init__ ;
2 call setup;
3 for nbepisodes do
4 call reset;
5 while episode not ended do
6 call step;
7 call __del__;
Alg. 49: Boucle d’exécution d’une mission.

Afin de visualiser pas à pas le déroulement d’une mission, il est possible de


ralentir son exécution comme suit :
1 from pysc2.agents import base_agent
2 from pysc2.lib import actions
3 import time
4

5 class SimpleAgent(base_agent.BaseAgent):
6 def reset(self):
7 self.step_cnt = -1
8 def step(self, obs):
9 super(SimpleAgent, self).step(obs)
10 self.step_cnt += 1
11 time.sleep(1)
12 print("think %d" %(self.step_cnt))
13 return actions.FUNCTIONS.no_op()

50. Dans la mission DefeatRoaches, un épisode s’achève après 1920 steps ou quand les Ma-
rines sont morts ; comme le joueur print_steps laisse les Marines inactifs, les épisodes s’ar-
rêtent après 1920 steps ; avec un step_mul de 8, on a 8 fois moins d’appels à la fonction step
et les lignes "step 1000", "step 2000" et "step 2000" ne s’affichent plus.

89
Exercices
1. Scanner exhaustivement Simple64 avec la fenêtre définie par le screen
2. Scanner les positions clés de Simple64 avec la fenêtre définie par le screen
3. Envoyer un SCV dans chaque coin de Simple64
4. Envoyer trois SCV aux positions clés de Simple64
5. Envoyer trois SCV aux positions clés de Simple64 et scanner leur posi-
tion avec la fenêtre définie par le screen
6. Définir une stratégie défensive dans Simple64 (Terran, Protoss, Zerg)
7. Définir une stratégie défensive générique (Terran, Protoss, Zerg)
8. Définir une stratégie d’expansion dans Simple64 (Terran, Protoss, Zerg)
9. Définir une stratégie d’expansion générique (Terran, Protoss, Zerg)
10. Définir une stratégie de rush dans Simple64 (Terran, Protoss, Zerg)
11. Définir une stratégie de rush générique (Terran, Protoss, Zerg)
12. Résoudre MoveToBeacon
13. Résoudre CollectMineralShards
14. Résoudre FindAndDefeatZerglings
15. Résoudre DefeatRoaches
16. Résoudre DefeatZerglingsAndBanelings
17. Résoudre CollectMineralsAndGas
18. Résoudre BuildMarines

90
1ère solution pour MoveToBeacon
Cette solution utilise un FSM 51 à deux états.

1 from pysc2.agents import base_agent


2 from pysc2.lib import actions
3 from pysc2.lib import features
4 import numpy
5

6 #Functions
7 _MOVE_SCREEN = actions.FUNCTIONS.Move_screen.id
8

9 # Features
10 _PLAYER_NEUTRAL = features.PlayerRelative.NEUTRAL
11

12 def _xy_locs(mask):
13 y, x = mask.nonzero()
14 return list(zip(x, y))
15

16 #
17 # python3 -m pysc2.bin.agent --map MoveToBeacon \
18 # --agent simple_agent.SimpleAgent --max_episodes 1
19 #
20 class SimpleAgent(base_agent.BaseAgent):
21

22 def step(self, obs):


23 super(SimpleAgent, self).step(obs)
24 if _MOVE_SCREEN in obs.observation.available_actions:
25 relative = obs.observation.feature_screen.player_relative
26 beacon = _xy_locs(relative == _PLAYER_NEUTRAL)
27 if not beacon:
28 return actions.FUNCTIONS.no_op()
29 beacon_center = numpy.mean(beacon, axis=0).round()
30 return actions.FUNCTIONS.Move_screen("now", beacon_center)
31 else:
32 return actions.FUNCTIONS.select_army("select")

Dans SC2, les unités neutres sont assez rares ; balises et mineraux sont des élé-
ments neutres.

Cette mission est composée de deux actions : la sélection des unités Marines et
leur déplacement vers le Beacon (i.e. une balise lumineuse).

A l’exécution de cette mission, on notera les indications de sélection de la souris


par la présence de ronds bleus sur le screen.
51. Un FSM (pour Finite State Machine) est un automate à états finis définissant le com-
portement d’un système.

91
2ème solution pour MoveToBeacon
Cette solution utilise le même FSM que la 1ère solution ; seule l’écriture du pro-
gramme diffère ; en réalisant les appels de fonction avec la procédure FunctionCall,
on obtient un message d’erreur indiquant les arguments attendus et les argu-
ments utilisés en cas de mauvaise utilisation ; ainsi FUNCTIONS.no_op() est rem-
placé par l’appel FunctionCall(_NO_OP, []).

1 from pysc2.agents import base_agent


2 from pysc2.lib import actions
3 from pysc2.lib import features
4

5 #Functions
6 _NO_OP = actions.FUNCTIONS.no_op.id
7 _MOVE_SCREEN = actions.FUNCTIONS.Move_screen.id
8 _SELECT_ARMY = actions.FUNCTIONS.select_army.id
9

10 # Features
11 _PLAYER_RELATIVE = features.SCREEN_FEATURES.player_relative.index
12 _PLAYER_NEUTRAL = features.PlayerRelative.NEUTRAL
13

14 # Parameters
15 _NOT_QUEUED = [0]
16 _SELECT_ALL = [0]
17

18 #
19 # python3 -m pysc2.bin.agent --map MoveToBeacon \
20 # --agent simple_agent.SimpleAgent --max_episodes 1
21 #
22 class SimpleAgent(base_agent.BaseAgent):
23

24 def step(self, obs):


25 super(SimpleAgent, self).step(obs)
26 if _MOVE_SCREEN in obs.observation["available_actions"]:
27 player_relative = obs.observation["feature_screen"][_PLAYER_RELATIVE]
28 neutral_y, neutral_x = (player_relative == _PLAYER_NEUTRAL).nonzero()
29 if not neutral_y.any():
30 return actions.FunctionCall(_NO_OP, [])
31 target = [int(neutral_x.mean()), int(neutral_y.mean())]
32 return actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, target])
33 else:
34 return actions.FunctionCall(_SELECT_ARMY, [_SELECT_ALL])

92
1ère solution pour DefeatRoaches
La mission DefeatRoaches est un problème de micro-gestion 52 , dans lequel il
s’agit de définir la politique optimale pour les actions des unités contre des uni-
tés ennemis.

Dans cette mission, il s’agit d’affronter des Roaches à l’aide d’un ensemble de
Marines ; les Roaches sont des unités terrestres Zerg ; les Marines sont des unités
terrestres Terran ; par groupe de 4, des Roaches apparaissent face aux Marines ;
pour chaque victoire, les Marines obtiennent un renfort de 5 unités ; l’objectif
des Marines est de tenir le plus longtemps possible.

Dans un cadre générique, sans considération d’optimisation selon les caracté-


ristiques des unités, l’état des unités, les positions des unités et les possibles
obstacles, il est possible de suivre le FSM défini par Alg. 50, représenté Fig. 9.

1 if state == 0 then
2 if select_all is available then
3 state ← 1 ;
4 return select_all ( );
5 else return noop ( );
6 if state == 1 then
7 {Y, X} ← getEnemies ( Obs ) ;
8 if X == ∅ then return noop ( );
9 target ← bestEnemy ( X , Y ) ;
10 return attack ( target );
Alg. 50: Une solution à deux états pour accomplir la mission Defea-
tRoaches.

Fig. 9 – FSM défini par Alg. 50.

52. Dans les jeux de stratégie temps-réel, la micro-gestion (i.e. micro-ing) concerne la ges-
tion des unités et la macro-gestion (i.e. macro-ing) concerne la gestion des batiments ; dans
SC2, il apparaît également des stratégies de découverte de la carte (i.e. scouting), d’ordre de
construction (i.e. build orders), d’épuisement par agression permanente (i.e. harassment), de
contrôle de positions clés sur la carte (i.e. map control) et de composition des squads (i.e.
unit composition).

93
2ème solution pour DefeatRoaches
Pour ajouter un peu de micro-gestion dans l’automate de la Fig. 9, en prenant
en compte les portées de tir et les temps de cooldown, il est possible de mainte-
nir les Marines à distance des Roaches, de façon à être hors de la portée de tir
des Roaches tout en gardant les Roaches à portée de tir des Marines ; ce com-
portement, qui vise à attaquer en reculant, appelé hit-and-run 53 , est défini par
Alg. 51, représenté Fig. 10 ; les Marines reculent quand ils sont à une distance
des Roaches inférieure à drun .

1 if state == 0 then
2 if select_all is available then
3 state ← 1 ;
4 return select_all ( );
5 return noop ( );
6 if state == 1 then
7 {Y, X} ← getPositions ( Obs ) ;
8 if X == ∅ then return noop ( );
9 A ← average ( X, Y ) ;
10 state ← 2 ;
11 return noop ( );
12 if state == 2 then
13 {Y 1, X1} ← getEnemies ( Obs ) ;
14 {Y 2, X2} ← getPositions ( Obs ) ;
15 if X1 == ∅ || X2 == ∅ then return noop ( );
16 if new_episode then
17 state ← 0 ;
18 return noop ( );
19 if dist (X1, Y 1, X2, Y 2) < drun then
20 return run ( A);
21 return attack ( X1, Y 1);
Alg. 51: Comportement hit-and-run à trois états pour accomplir la mis-
sion DefeatRoaches.

Fig. 10 – FSM défini par Alg. 51.

53. Par opposition, le comportement hit-and-attack est d’attaquer en avançant.

94
Ci-dessous les fonctions correspondants aux principales fonctions du compor-
tement hit-and-run de Fig. 10 ; la distance visant à tenir les Roaches hors de
portée est içi fixée à 19.7.

1 def reset(self):
2 super(SimpleAgent, self).reset()
3 self.state = 0
4 self.initial_marines_x = 0.0
5 self.initial_marines_y = 0.0
6 self.nb_roaches = 100
7

8 def define_A(self, obs):


9 player_relative = obs.observation["feature_screen"][_PLAYER_RELATIVE]
10 marines_y, marines_x = (player_relative == _PLAYER_SELF).nonzero()
11 if not marines_y.any():
12 return actions.FUNCTIONS.no_op()
13 self.state = 2
14 self.initial_marines_x = numpy.sum(marines_x)/marines_x.size
15 self.initial_marines_y = numpy.sum(marines_y)/marines_y.size
16

17 def hit_and_run(self, obs):


18 player_relative = obs.observation["feature_screen"][_PLAYER_RELATIVE]
19 marines_y, marines_x = (player_relative == _PLAYER_SELF).nonzero()
20 if not marines_y.any():
21 return actions.FUNCTIONS.no_op()
22 roach_y, roach_x = (player_relative == _PLAYER_HOSTILE).nonzero()
23 if not roach_y.any():
24 return actions.FUNCTIONS.no_op()
25 barycenter_marines_x = numpy.sum(marines_x)/marines_x.size
26 barycenter_marines_y = numpy.sum(marines_y)/marines_y.size
27 for x in numpy.nditer(roach_x, op_flags=[’readwrite’]):
28 x[...] = x - barycenter_marines_x
29 for y in numpy.nditer(roach_y, op_flags=[’readwrite’]):
30 y[...] = y - barycenter_marines_y
31 roach_x2 = numpy.square(roach_x)
32 roach_y2 = numpy.square(roach_y)
33 dist = numpy.sqrt(numpy.add(roach_x2, roach_y2))
34 index = numpy.argmin(dist)
35 if dist[index] < 19.7 :
36 dest = [int(self.initial_marines_x), int(self.initial_marines_y)]
37 return actions.FUNCTIONS.move_screen("now", dest)
38 target_y, target_x = (player_relative == _PLAYER_HOSTILE).nonzero()
39 target = [target_x[index], target_y[index]]
40 return actions.FUNCTIONS.Attack_screen("now", target)

95
Evaluation MC de drun pour DefeatRoaches
Cette section présente une solution MC (cf. Alg. 13 page 29) pour l’évaluation
des valeurs de drun dans le comportement hit-and-run appliqué à la mission
DefeatRoaches ; le programme définissant le comportement hit-and-run est mo-
difié, en ajoutant la fonction set_d_run pour pouvoir redéfinir la valeur de drun
comme suit :
1 def reset(self):
2 self.d_run = 10.0
3

4 def set_d_run(self, new_d_run):


5 self.d_run = new_d_run
6

7 def hit_and_run(self, obs):


8 //...
9 if dist[index] < self.d_run :
10 //...
11 return actions.FUNCTIONS.move_screen("now", dest)
12 //...
13 return actions.FUNCTIONS.Attack_screen("now", target)
La valeur par défaut de drun est arbitrairement fixée à 10.0.

En utilisant absltest, on définit EvalMcDrunDefeatRoaches avec une méthode


test qui est appelée automatiquement ; pour cette fonction test, on définit un
contexte d’exécution correspondant à la mission associée au test ; le programme
(nommé testDefeatRoaches.py) est comme suit :

1 from absl.testing import absltest


2 from pysc2.env import run_loop
3 from pysc2.env import sc2_env
4 from pysc2.tests import utils
5 import numpy as np
6 import defeat_roaches
7

8 MISSION = "DefeatRoaches"
9 AGENT = defeat_roaches.SimpleAgent()
10 VISUAL = False
11 NB_DIST = 1
12 NB_TEST = 2
13 SMALLEST_DIST = 19.7
14 NB_STEPS = 1920
15

16 class EvalMcDrunDefeatRoaches(utils.TestCase):
17 step_mul = 1
18 steps = NB_STEPS/step_mul
19 max_episodes = 1
20 sum_scores = np.zeros(NB_DIST)
21 sum_scores2 = np.zeros(NB_DIST)
22 nb_scores = np.zeros(NB_DIST)

96
Selon la valeur de visualize, l’interface SC2 affiche le déroulement des scéna-
rios ; le nombre de scénarios correspond au nombre d’appels de run_loop avec
pour paramètre un programme joueur.

23 def setUp(self):
24 super(EvalMcDrunDefeatRoaches, self).setUp()
25 sc2_env.stopwatch.sw.enabled = False
26

27 def test(self):
28 with sc2_env.SC2Env(
29 map_name=MISSION,
30 visualize=VISUAL,
31 agent_interface_format=sc2_env.AgentInterfaceFormat(
32 feature_dimensions=sc2_env.Dimensions(
33 screen=84, minimap=64)),
34 step_mul=self.step_mul,
35 game_steps_per_episode=self.steps * self.step_mul) as env:
36 agent = AGENT
37 for j in range(0,NB_TEST) :
38 for i in range(0,NB_DIST) :
39 agent.set_d_run(float(SMALLEST_DIST+i))
40 run_loop.run_loop([agent], env, self.steps, self.max_episodes)
41 self.sum_scores[i] += agent.score
42 self.sum_scores2[i] += (agent.score*agent.score)
43 self.nb_scores[i] += 1
44 print("----------------------------------------")
45 print("dist sum_scores nb_scores => avg std_dev")
46 for i in range(0,NB_DIST) :
47 avg = float(self.sum_scores[i]) / float(self.nb_scores[i])
48 avg2 = float(self.sum_scores2[i]) / float(self.nb_scores[i])
49 std_dev = np.sqrt(avg2 - (avg*avg))
50 print("%d %d %d => %.2f %.2f" %(SMALLEST_DIST+i,
51 self.sum_scores[i],
52 self.nb_scores[i],
53 avg,
54 std_dev))
55 print("----------------------------------------")
56

57 if __name__ == "__main__":
58 absltest.main()

Avec le programme précédent, on obtient la moyenne des scores sur 2 scénarios


de DefeatRoaches avec le comportement hit-and-run avec drun = 19.7.

97
En utilisant le programme de test précédent (i.e. testDefeatRoaches.py), avec
les paramètres suivants,
MISSION = "DefeatRoaches"
AGENT = defeat_roaches.SimpleAgent()
VISUAL = False
NB_DIST = 20
NB_TEST = 10
SMALLEST_DIST = 10.0
NB_STEPS = 1920
on obtient les résultats présentés en Fig. 11, qui confirme que la meilleure valeur
de drun est proche de 19.

Fig. 11 – Scores de testDefeatRoaches.py avec drun dans [11, 29].

En remplaçant float(SMALLEST_DIST+i) par float(SMALLEST_DIST+0.1*i)


dans testDefeatRoaches.py et avec SMALLEST_DIST = 19.0, on obtient les
résultats présentés en Fig. 12 qui placent la meilleure valeur de drun à 19.7.

Fig. 12 – Scores de testDefeatRoaches.py avec drun dans [19, 21].

98
1ère solution pour CollectMineralsAndGas
La mission CollectMineralsAndGas consiste à définir la suite des actions per-
mettant d’optimiser la collecte de minerai et de gaz ; dans cette mission, on
dispose initialement de 12 SCV, un Command center, à proximité de 16 sources
de minerai et 4 sources de gaz ; les ressources initialement possédées sont de 50
minerais ; le Command center supporte 15 unités SCV ; pour supporter plus
d’untiés, il faut construire un Supply depot ; le temps est limité à 300 secondes ;
la mission consiste donc à choisir entre les actions suivantes : collecter du mine-
rai, collecter du gaz, construire un ou plusieurs SCV, construire un ou plusieurs
Supply depot et construire un ou plusieurs un Command center 54 .

Une première solution consiste à suivre la politique définie par Alg. 52 ; nbSCV
définit le nombre de SCV ; nbharvest_gather définit le nombre de SCV col-
lecteurs ; nb_trainSCV définit le nombre de SCV en cours d’entrainement ;
nbsupply_depot définit le nombre de Supply depot ; nb_buildsupply−depot définit
le nombre de Supply depot en cours de construction.

1 if nbharvest_gather < 12 then return add_harvest_gather ( );


2 if nbSCV + nb_trainSCV < 15 then
3 if minerals > 50 then return train_SCV ( );
4 return noop ( );
5 if nbSCV < 15 then return noop ( );
6 if nbharvest_gather < 15 then return add_harvest_gather ( );
7 if nbharvest_gather == 15 and nbsupply_depot = 0 then
8 return build_supply_depot ( 35, 55);
9 if nbsupply_depot == 1 then
10 if nbSCV + nb_trainSCV < 23 and nbSCV < nbharvest_gather + 1
then
11 if minerals > 50 and nb_trainSCV < 5 then
12 return train_SCV ( );
13 return noop ( );
14 if nbharvest_gather < nbSCV then return add_harvest_gather ( );
15 if nbharvest_gather == 23 and nb_buildsupply−depot == 0 then
16 return build_supply_depot ( 45, 55);
17 if nbsupply_depot == 2 then
18 if nbSCV + nb_trainSCV < 31 and nbSCV < nbharvest_gather + 1
then
19 if minerals > 50 then return train_SCV ( );
20 return noop ( );
21 if nbharvest_gather < nbSCV then return add_harvest_gather ( );
22 return noop ( );
Alg. 52: Collecter exclusivement du minerai.

54. La description de la mission indique que la solution optimale requiert la construction de


SCV et d’un Command center supplémentaire

99
La politique définie dans Alg. 52 permet d’obtenir un score de 4645 ; après 6720
step, il y a 28 SCV tous impliqués dans la collecte de minerai, 2 Supply depot
constuits et 3 SCV en cours d’entrainement.

La fin de la fonction step présente l’implémentation de Alg. 52 :


1 collected_minerals = obs.observation["score_cumulative"]["collected_minerals"]
2 self.update_units(obs)
3 self.update_buildings(obs)
4 if self.nb_harvest_gather < 12 :
5 return self.add_harvest_gather(obs)
6 if (self.nb_scv+self.nb_scv_in_train) < 15 :
7 if collected_minerals > 50:
8 return self.train_SCV(obs)
9 if self.nb_scv < 15:
10 return actions.FUNCTIONS.no_op()
11 if self.nb_harvest_gather < 15 :
12 return self.add_harvest_gather(obs)
13 if self.nb_harvest_gather == 15 and self.nb_supply_depot == 0:
14 return self.build_supply_depot(obs, 35, 55)
15 if self.nb_supply_depot == 1:
16 if (self.nb_scv+self.nb_scv_in_train) < 23 \
17 and self.nb_scv < self.nb_harvest_gather+1:
18 if collected_minerals > 50 and self.nb_scv_in_train < 5:
19 return self.train_SCV(obs)
20 return actions.FUNCTIONS.no_op()
21 if self.nb_harvest_gather < self.nb_scv:
22 return self.add_harvest_gather(obs)
23 if self.nb_harvest_gather == 23 and self.nb_supply_depot_in_build == 0:
24 return self.build_supply_depot(obs, 45, 55)
25 if self.nb_supply_depot == 2:
26 if (self.nb_scv+self.nb_scv_in_train) < 31 \
27 and self.nb_scv < self.nb_harvest_gather+1:
28 if collected_minerals > 50:
29 return self.train_SCV(obs)
30 return actions.FUNCTIONS.no_op()
31 if self.nb_harvest_gather < self.nb_scv:
32 return self.add_harvest_gather(obs)
33 return actions.FUNCTIONS.no_op()

Les fonctions update_units, update_buildings, add_harvest_gather, train_scv


et build_supply_depot sont détaillées dans les pages suivantes.

100
Les variables sont initialisées comme suit :
1 nb_scv = 12
2 nb_harvest_gather = 0
3 nb_supply_depot = 0
4 nb_scv_in_train = 0
5 nb_supply_depot_in_build = 0
6 prev_total_value_structures = 400
7 inactive_scv_selected = False
8 random_scv_selected = False
9 commandcenter_selected = False

La fonction update_units compte les SCV ; elle exploite le fait que les unités
de cette mission sont uniquement des SCV dont la valeur est de 50 :
1 def update_units(self, obs):
2 tvu = obs.observation["score_cumulative"]["total_value_units"]
3 new_nb_scv = int((tvu - self.prev_tvu)/50)
4 if new_nb_scv != self.nb_scv:
5 diff = new_nb_scv - self.nb_scv
6 self.nb_scv += diff
7 self.nb_scv_in_train -= diff
8 self.prev_tvu = tvu

La fonction update_buildings compte les Supply depot ; sachant que les ordres
se limitent à construire des Supply depot, elle observe la différence avec la valeur
précédente et recherche une différence de 100 (qui est la valeur d’un Supply
depot) ; la mémorisation de la valeur courante de total_value_structures est
imposée par la présence d’un Command center dont la valeur est de 400 :
1 def update_buildings(self, obs):
2 tvs = obs.observation["score_cumulative"]["total_value_structures"]
3 if tvs != self.prev_total_value_structures:
4 diff = (tvs - self.prev_total_value_structures)/100
5 self.nb_supply_depot += diff
6 self.nb_supply_depot_in_build -= diff
7 self.prev_total_value_structures = tvs

101
La fonction erode_with_min réalise une érosion sur une image de la taille du
screen :
1 def erode_with_min(screen):
2 output = []
3 for i in range(84):
4 output.append([0] * 84)
5 for i in range(1,83) :
6 for j in range(1,83) :
7 if(screen[i][j] == 1):
8 output[i][j] = int(min(screen[i][j], screen[i+1][j],
9 screen[i-1][j], screen[i][j+1],
10 screen[i][j-1]))
11 return output

La fonction get_mineral_coord retourne une coordonnée d’un mineral présent


sur la carte (l’utilisation de la fonction erode_with_min permet de bien sélec-
tionner un mineral 55 ) :
1 def get_mineral_coord(obs):
2 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
3 mine = (unit_type == units.Neutral.MineralField)
4 mine = erode_with_min(mine)
5 mineral_y, mineral_x = np.array(mine).nonzero()
6 return [mineral_x[0], mineral_y[0]]

La fonction add_harvest_gather fonctionne en deux temps ; dans le premier


temps, il faut sélectionner un SCV inactif ; dans le deuxième temps, le SCV
sélectionné est envoyé à la collecte de minerai :
1 def add_harvest_gather(self, obs):
2 if self.inactive_scv_selected == False:
3 if _SELECT_IDLE_WORKER in obs.observation["available_actions"]:
4 self.inactive_scv_selected = True
5 self.commandcenter_selected = False
6 return actions.FUNCTIONS.select_idle_worker("select")
7 if self.inactive_scv_selected == True:
8 if _HARVEST_GATHER_SCREEN in obs.observation["available_actions"]:
9 target = get_mineral_coord(obs)
10 self.inactive_scv_selected = False
11 self.commandcenter_selected = False
12 self.nb_harvest_gather = self.nb_harvest_gather +1
13 return actions.FUNCTIONS.Harvest_Gather_screen("now", target)
14 return actions.FUNCTIONS.no_op()

55. La sélection d’un mineral à l’aide d’un pixel situé en bordure peut se solver par un échec.

102
La fonction train_scv fonctionne en deux temps ; dans le premier temps, il
faut sélectionner le Command center ; dans le deuxième temps, on sélectionne
un ordre de construction d’un SCV :
1 def train_scv(self, obs):
2 if self.commandcenter_selected == False :
3 if _SELECT_POINT in obs.observation["available_actions"]:
4 self.commandcenter_selected = True
5 self.inactive_scv_selected = False
6 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
7 cc = (unit_type == units.Terran.CommandCenter)
8 commandcenter_y, commandcenter_x = cc.nonzero()
9 cc_center_x = np.mean(commandcenter_x, axis=0).round()
10 cc_center_y = np.mean(commandcenter_y, axis=0).round()
11 target = [cc_center_x, cc_center_y]
12 return actions.FUNCTIONS.select_point("select", target)
13 if self.commandcenter_selected == True :
14 if _TRAIN_SCV_QUICK in obs.observation["available_actions"]:
15 self.nb_scv_in_train = self.nb_scv_in_train +1
16 return actions.FUNCTIONS.Train_SCV_quick("now")
17 return actions.FUNCTIONS.no_op()

La fonction build_supply_depot fonctionne en deux temps ; dans le premier


temps, il faut sélectionner un SCV ; le SCV est soit un des SCV inactifs, soit
un SCV choisi aléatoirement ; dans le deuxième temps, on sélectionne un ordre
de construction d’un Supply depot :
1 def build_supply_depot(self, obs, coord_x, coord_y):
2 if self.inactive_scv_selected == False:
3 if _SELECT_IDLE_WORKER in obs.observation["available_actions"]:
4 self.inactive_scv_selected = True
5 self.random_scv_selected = False
6 self.commandcenter_selected = False
7 return actions.FUNCTIONS.select_idle_worker("select")
8 if self.random_scv_selected == False:
9 self.inactive_scv_selected = False
10 self.random_scv_selected = True
11 self.commandcenter_selected = False
12 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
13 scv_y, scv_x = (unit_type == units.Terran.SCV).nonzero()
14 target = [scv_x[0], scv_y[0]]
15 return actions.FUNCTIONS.select_point("select", target)
16 if self.inactive_scv_selected == True or self.random_scv_selected == True:
17 if _BUILD_SUPPLYDEPOT_SCREEN in obs.observation["available_actions"]:
18 self.nb_supply_depot_in_build = self.nb_supply_depot_in_build +1
19 self.nb_harvest_gather = self.nb_harvest_gather -1
20 target = [coord_x, coord_y]
21 return actions.FUNCTIONS.Build_SupplyDepot_screen("now", target)
22 return actions.FUNCTIONS.no_op()

103
2ème solution pour CollectMineralsAndGas
Dans le cas d’une mission correspondant à une recherche de séquence optimale
de constructions 56 , il est possible d’approximer le processus de décision à l’aide
d’un MDP (tel que défini section 9.1).

Un état est défini par un nombre de SCV et un nombre de Supply depot ; les
SCV sont soient en cours d’entrainement, soient inactifs, soient collecteurs de
minerai ; les Supply depot sont soient en cours de construction, soient construits ;
un état est donc défini comme suit :
1 new_state = [self.nb_harvest_gather, self.nb_scv, \
2 self.nb_supply_depot, self.nb_scv_in_train, \
3 self.nb_supply_depot_in_build]
Les actions sont définies comme suit :
1 ACTION_DO_NOTHING = ’donothing’
2 ACTION_ADD_HARVEST_GATHER = ’addharvestgather’
3 ACTION_TRAIN_SCV = ’trainsvc’
4 ACTION_BUILD_SUPPLY_DEPOT = ’buildsupplydepot’
5 my_actions = [ ACTION_DO_NOTHING, ACTION_ADD_HARVEST_GATHER, \
6 ACTION_TRAIN_SCV, ACTION_BUILD_SUPPLY_DEPOT, ]
Les actions ont un délai de répercution effective sur l’état courant :
— construire un SCV demande 272 steps
— collecter du minerai pour un SCV demande au moins 125 steps
— construire un Supply depot demande au moins 462 steps

Comme l’état courant inclus SCV en cours d’entrainement et Supply depot en


cours de construction, la fréquence des décisions est uniquement fonction du
temps de collecte du minerai ; afin de différencier la contribution d’un SCV de
tous les autres SCV en cours de collecte (et donc d’avoir une valeur de reward
juste), on utilise un pas de temps de 300 steps pour la prise de décision (qui inclut
une majoration du temps de collecte de 25 steps selon la position des minerais et
le doublement de ce temps majoré pour différencier les contributions de chaque
SCV ) ; ce principe, présentée dans Alg. 53, est appliqué à la fonction step.

1 stepdecision ← stepdecision + 1 ;
2 if stepdecision == 300 then
3 stepdecision ← 0 ;
4 R0 ← get_reward ( );
5 S 0 ← get_current_state ( ) ;
6 A0 ← -greedy (S, Q) ;
7 SARSA-update (S, A, R0 , S 0 , A0 ) ;
8 {S, A} ← {S 0 , A0 } ;
9 return apply_decision (A0 ) ;
10 return do noop ( );
Alg. 53: Construction de la fonction step.

56. Types de problèmes dits de macro-gestion.

104
La fonction apply_decision est implémentée comme suit :
1 def apply_decision(self, action):
2 self.do_add_harvest_gather = False
3 self.do_train_scv = False
4 self.do_build_supply_depot = False
5 if(action == ACTION_DO_NOTHING):
6 return
7 if(action == ACTION_ADD_HARVEST_GATHER):
8 self.do_add_harvest_gather = True
9 if(action == ACTION_TRAIN_SCV):
10 self.do_train_scv = True
11 if(action == ACTION_BUILD_SUPPLY_DEPOT):
12 self.do_build_supply_depot = True

La fonction get_reward est implémentée comme suit :


1 def get_reward(self, obs):
2 cm = obs.observation["score_cumulative"]["collected_minerals"]
3 cv = obs.observation["score_cumulative"]["collected_vespene"]
4 score_added = cm+cv - self.prev_score_cumul
5 reward = score_added - self.prev_score_added
6 self.prev_score_cumul = cm+cv
7 self.prev_score_added = score_added
8 if reward > 1:
9 reward = 1
10 return reward

Le décompte des SCV et des Supply depot est implémenté comme suit :
1 def update_units(self, obs):
2 tvu = obs.observation["score_cumulative"]["total_value_units"]
3 tvu_diff = tvu - self.prev_tvu
4 new_scv = int(tvu_diff/50)
5 if new_scv != 0:
6 self.nb_scv += new_scv
7 self.nb_scv_in_train -= new_scv
8 self.prev_tvu = tvu
9

10 def update_buildings(self, obs):


11 tvs = obs.observation["score_cumulative"]["total_value_structures"]
12 tvs_diff = tvs - self.prev_tvs
13 new_supply_depot = int(tvs_diff/100)
14 if new_supply_depot != 0:
15 self.nb_supply_depot += new_supply_depot
16 self.nb_supply_depot_in_build -= new_supply_depot
17 self.prev_tvs = tvs

105
La classe Qtable implémente l’apprentissage des évaluations des actions pos-
sibles à partir de chaque état selon l’apprentissage SARSA (cf. Alg. 53 page 66).
1 class QTable:
2 def __init__(self, actions, alpha=0.1, gamma=0.9, epsilon=0.9):
3 self.actions = actions
4 self.alpha = alpha
5 self.gamma = gamma
6 self.epsilon = epsilon
7 self.data = pd.DataFrame(columns=self.actions, dtype=np.float64)
8

9 def add_state(self, x, y, z, a, b, max_x, max_y, max_z, max_a, max_b):


10 raw_index = []
11 raw_index.append(0)
12 if x < max_x:
13 raw_index.append(1)
14 if (y+a) < max_y and a < max_a:
15 raw_index.append(2)
16 if (z+b) < max_z and b < max_b:
17 raw_index.append(3)
18 raw_value = []
19 for i in range(0, len(raw_index)):
20 raw_value.append(np.random.uniform())
21 new_data = str([x,y,z,a,b])
22 if new_data not in self.data.index:
23 new_entrie = pd.Series(raw_value, index=raw_index, name=new_data)
24 self.data = self.data.append(new_entrie)

La classe DataFrame de la librairie pandas (cf. annexes, section 10.1) est utilisée
pour maintenir la relation entre états et actions ; une action non sélectionnable
est associée à la valeur NaN ; dans la fonction add_state :
— x est le nombre de SCV collecteurs de minerai,
— y est le nombre de SCV,
— z est le nombre de Supply depot,
— a est le nombre de SCV en cours d’entrainement,
— b est le nombre de Supply depot en cours de construction,
— max_x est le nombre max de SCV collecteurs,
— max_y est le nombre de max de SCV (incluant les SCV en cours d’en-
trainement),
— max_z est le nombre max de Supply depot (incluant les Supply depot en
cours de construction),
— max_a est le nombre max de SCV en cours d’entrainement,
— max_b est le nombre max de Supply depot en cours de construction,
— l’action donothing est toujours possible (ligne 11),
— l’action addharvestgather dépend du nombre de SCV collecteurs (ligne
12),
— l’action trainsvc dépend du nombre de SCV en cours d’entrainement
et du nombre de SCV (ligne 14),
— l’action buildsupplydepot dépend du nombre de Supply depot en cours
de construction et du nombre de Supply depot déjà construits (ligne 16).

106
Les fonctions e_greedy et sarsa_update sont définies comme suit :
25 def e_greedy(self, state):
26 if self.epsilon < np.random.uniform():
27 new_actions = []
28 state_action = self.data.loc[state]
29 for x in range(0, len(state_action)):
30 if np.isnan(state_action[x]) == False:
31 new_actions.append(x)
32 action = np.random.choice(new_actions)
33 else:
34 state_action = self.data.loc[state]
35 action = state_action.idxmax()
36 return action
37

38 def sarsa_update(self, state1, action1, reward, state2, action2):


39 s1a1 = self.data.loc[str(state1)].loc[action1]
40 s2a2 = self.data.loc[str(state2)].loc[action2]
41 inc_val = self.alpha*(reward + self.gamma*s2a2 - s1a1)
42 self.data.loc[str(state1)].loc[action1] += inc_val

La table des correspondances entre états et actions est initialisée comme suit :
1 self.qtable = QTable(actions=list(range(len(my_actions))))
2 for x in range(0, 1+MAX_NB_HARVEST_GATHER):
3 for y in range(self.nb_scv, 1+MAX_NB_SCV):
4 for z in range(0, 1+MAX_NB_SUPPLY_DEPOT):
5 for a in range(0, 1+MAX_NB_SCV_IN_TRAIN):
6 for b in range(0, 1+MAX_NB_SUPPLY_DEPOT_IN_BUILD):
7 self.qtable.add_state(x,y,z,a,b, \
8 MAX_NB_HARVEST_GATHER, \
9 MAX_NB_SCV, \
10 MAX_NB_SUPPLY_DEPOT, \
11 MAX_NB_SCV_IN_TRAIN, \
12 MAX_NB_SUPPLY_DEPOT_IN_BUILD)

107
Fig. 13 présente l’évolution des scores avec l’apprentissage SARSA pour la mis-
sion CollectMineralsAndGas ; la progression du score se traduit par une amélio-
ration de la politique suivie ; à l’itération 90, le score obtenu est de 4335, avec
23 SCV dont 20 impliqués dans la collecte de minerai, 1 Supply depot construit
et 1 Supply depot en cours de construction.

Fig. 13 – Evolution des scores avec l’apprentissage SARSA sur 100 itérations.

Entre les itérations 70 et 100, le score repasse parfois encore autour de 2300 ;
cette limite correspond à la construction d’un Supply depot qui permet d’entrai-
ner plus de SCV pour les impliquer dans la collecte.

Fig. 14 – Résultats avec différents step_mul (smX abrège içi step_mul=X).

Il est possible d’accélérer les simulations en augmentant le step_mul des simu-


lations ; en augmentant les simulations, on diminue également les observations
et donc les résultats ; Fig. 14 présente l’évolution des scores avec l’apprentis-
sage SARSA pour la mission CollectMineralsAndGas pendant les 30 premières
itérations de Fig. 13, avec différentes valeurs de step_mul.

108
En utilisant un processus aléatoire, on peut utiliser les récompenses des actions
pour définir les valeurs des états ; dans ce cas, on définit la valeur d’un état par la
moyenne des sommes des récompenses établies par les actions suivants cet état ;
pour les deux premières séquences précédentes, en appliquant le modèle d’un
processus de récompense Markovien, avec γ = 0.5, on obtient les récompenses
suivantes :
— C1 C2 C3 Pass Sleep : V 1 = −2 − 2 ∗ 21 − 2 ∗ 14 + 10 ∗ 81 = −2.25
— C1 Flip Flip C1 C2 Sleep : V 1 = −2 − 1 ∗ 21 − 1 ∗ 41 − 2 ∗ 18 = −3

Alg 54 présente le calcul des valeurs d’un processus de récompense Markovien


en utilisant la moyenne des scénarios aléatoires.

1 for each s in S do
2 sum_score ← 0 ;
3 for N times do
4 seq ← init (s) ;
5 seq ← random (seq, S, P) ;
6 sum_score ← sum_score+ eval (seq, R, γ) ;
7 scores ← sum_score/N ;
Alg. 54: Définition des valeurs des états par moyenne de récompenses
Markoviennes.

Avec le programme C/C++ suivant, les valeurs des états sont estimées en pre-
nant la moyenne des valeurs des scénarios aléatoires commençant par un état :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <string.h>
4 #include <math.h>
5
6 #define gamma 0.5
7 enum {C1=0, C2, C3, Pass, Pub, Flip, Sleep};
8 double state_score[] = {-2, -2, -2, +10, +1, -1, 0};
9 const char* str_state[] = {"C1", "C2", "C3", "Pass", "Pub", "Flip", "Sleep" };
10
11 struct mk_t {
12 int size = 0;
13 int scenario[1024] = {0};
14
15 void next(){
16 double myrand = ((double)rand())/RAND_MAX;
17 if(scenario[size-1]==C1) {
18 if(myrand < 0.5) scenario[size] = C2; else scenario[size] = Flip;
19 size++; return;
20 }
21 if(scenario[size-1]==C2) {
22 if(myrand < 0.8) scenario[size] = C3; else scenario[size] = Flip;
23 size++; return;
24 }

109
25 if(scenario[size-1]==C3) {
26 if(myrand < 0.6) scenario[size] = Pass; else scenario[size] = Pub;
27 size++; return;
28 }
29 if(scenario[size-1]==Pass) {
30 scenario[size] = Sleep; size++; return;
31 }
32 if(scenario[size-1]==Pub) {
33 if(myrand < 0.2) scenario[size] = C1;
34 if(myrand < 0.6) scenario[size] = C2; else scenario[size] = C3;
35 size++; return;
36 }
37 if(scenario[size-1]==Flip) {
38 if(myrand < 0.1) scenario[size] = C1; else scenario[size] = Flip;
39 size++; return;
40 }
41 }
42 void generate() {
43 while(1) {
44 if(size==2014) break;
45 if(scenario[size-1]==Sleep) break;
46 next();
47 }
48 }
49 double score() {
50 double res = 0.0;
51 if(scenario[size-1]!=Sleep) return res;
52 res = state_score[scenario[0]];
53 for(int i = 1; i < size; i++) {
54 res += pow(gamma,(double)i)*state_score[scenario[i]];
55 }
56 return res;
57 }
58 };
59 /* g++ -std=c++11 journee-etudiante.cpp */
60 int main(int _ac, char** _av) {
61 for(int i = C1; i < Sleep; i++) {
62 double sum_score = 0.0;
63 for(int j = 0; j < 1000; j++) {
64 mk_t MK; MK.scenario[0]=i; MK.size=1; MK.generate();
65 sum_score += MK.score();
66 }
67 printf("score %s : %f\n", str_state[i], sum_score/1000);
68 }
69 return 0;
70 }

Avec 1000 itérations, on trouve :


score C1 : -2.969297
score C2 : -1.757836
score C3 : 1.247894
score Pass : 10.000000
score Pub : 0.745732
score Flip : -2.087817

110
Avec le programme Racket suivant, les valeurs des états sont estimées en pre-
nant la moyenne des valeurs des scénarios aléatoires commençant par un état :
1 #lang racket
2 (define gamma 1.0)
3 (define C1 0)
4 (define C2 1)
5 (define C3 2)
6 (define PASS 3)
7 (define PUB 4)
8 (define FLIP 5)
9 (define SLEEP 6)
10 (define (eval-S s)
11 (cond ((equal? s C1) -2)
12 ((equal? s C2) -2)
13 ((equal? s C3) -2)
14 ((equal? s PASS) 10)
15 ((equal? s PUB) 1)
16 ((equal? s FLIP) -1)
17 (else 0)))
18 (define (next L)
19 (let ((f (first L)) (r (random)))
20 (cond ((equal? f C1)
21 (if(< r 0.5) (cons C2 L) (cons FLIP L)))
22 ((equal? f C2)
23 (if(< r 0.8) (cons C3 L) (cons FLIP L)))
24 ((equal? f C3)
25 (if(< r 0.6) (cons PASS L) (cons PUB L)))
26 ((equal? f PASS) (cons SLEEP L))
27 ((equal? f PUB)
28 (if(< r 0.2) (cons C1 L)
29 (if(< r 0.6) (cons C2 L)
30 (cons C3 L))))
31 (else (if(< r 0.5) (cons C2 L) (cons FLIP L))) )))
32 (define (generate L)
33 (if(equal? (first L) SLEEP) (reverse L) (generate (next L))))
34 (define (score L)
35 (define (f L ret ite)
36 (if(empty? L) ret
37 (f (rest L) (+ ret (* (eval-S (first L)) (expt gamma ite))) (+ 1 ite) )))
38 (f L 0 0))
39 (define (init s) (list s))
40 (define (n-generate n s)
41 (define (f n s sum)
42 (if(equal? n 0) sum
43 (f (- n 1) s (+ sum (score (generate (init s))))) ))
44 (/ (f n s 0) n))
45 (string-append "C1: " (real->decimal-string (n-generate 1000 C1) 2))
46 (string-append "C2: " (real->decimal-string (n-generate 1000 C2) 2))
47 (string-append "C3: " (real->decimal-string (n-generate 1000 C3) 2))
48 (string-append "PASS: " (real->decimal-string (n-generate 1000 PASS) 2))
49 (string-append "PUB: " (real->decimal-string (n-generate 1000 PUB) 2))
50 (string-append "FLIP: " (real->decimal-string (n-generate 1000 FLIP) 2))

Avec 1000 itérations, on trouve (des valeurs similaires aux précédentes) :


"C1: -2.91"
"C2: -1.72"
"C3: 1.13"
"PASS: 10.00"
"PUB: 0.60"
"FLIP: -1.93"

111
En partant des récompenses supposées sur les actions, les programmes précé-
dents permettent de calculer une évaluation des récompenses obtenues dans les
états (présenté en figure 15 avec les résultats du programme C/C++) ; partant
de ces évaluations, on peut définir une politique optimale par recherche d’un
chemin maximisant les évaluations des états futurs (présenté en figure 16) ; la
propriété de Markov permet de ne tenir compte que de la valeur de l’état à
l’instant t + 1 pour définir la meilleure action partant d’un état à l’instant t ;
le chemin résultant est différent du chemin étabi par évaluation complète du
graphe ; cette différence est une conséquence des valeurs de récompense établies
à priori ; la recherche de ce chemin est de complexité linéaire.

Fig. 15 – Evaluation des états d’une journée étudiante avec récompenses.

Fig. 16 – Politique optimale π∗ par recherche d’un chemin maximisant les éva-
luations des états futurs ; en appliquant cette politique optimale, l’état Flip ne
comporte que des actions vers lui-même et est dit absorbant.

112
9.10 Application à Dota2

113
9.11 Application au Blackjack
1 /* blackjack avec 1 jeu de 52 cartes
2 * version americaine avec une carte cachee pour le croupier */
3 #include <cstdio>
4 #include <cstdlib>
5 #include <string.h>
6 #include <math.h>
7 #include <vector>
8 #include <string>
9 #include <algorithm>
10 #include <cfloat>
11 int card_val[13];
12 char get_card_number(int _card) { return 1+_card%13; }
13 int get_card_value(int _card) { return card_val[_card%13]; }
14 struct bj_t {
15 std::vector<int> all_cards;
16 std::vector<int> player_cards[2]; // player0 est le croupier
17 int player_sums[2]; // en comptant AS=1
18 int player_max_sums[2]; // en comptant AS=11
19 bj_t() {
20 for(int i = 0; i < 52; i++) all_cards.push_back(i);
21 std::random_shuffle( all_cards.begin(), all_cards.end());
22 for(int i = 0; i < 10; i++) card_val[i]=i+1;
23 for(int i = 10; i < 13; i++) card_val[i]=10;
24 }
25 void new_game() {
26 for(int j = 0; j < 2; j++) { // 2 players (croupier inclus)
27 player_cards[j].clear();
28 player_sums[j] = 0;
29 player_max_sums[j] = 0;
30 for(int i = 0; i < 2; i++) { // 2 cartes
31 player_cards[j].push_back(all_cards.back());
32 all_cards.pop_back();
33 int v = get_card_value(player_cards[j][i]);
34 player_sums[j] += v;
35 if(v == 1) v = 11;
36 player_max_sums[j] += v;
37 }
38 }
39 }
40 void twist(int _pid) { // pour player 0 et 1
41 player_cards[_pid].push_back(all_cards.back());
42 all_cards.pop_back();
43 int v = get_card_value(player_cards[_pid].back());
44 player_sums[_pid] += v;
45 if(v == 1) v = 11;
46 player_max_sums[_pid] += v;
47 }
48 // completer les cartes du croupier
49 // et calculer des sommes les + proches de 21
50 void finalize(){
51 int s1 = player_sums[1];
52 int s2 = player_max_sums[1];
53 if(s2 < 21 && s2 > s1) player_sums[1] = s2;
54 while(1) {
55 if(player_sums[0] > player_sums[1]) break;
56 if(player_max_sums[0] > player_sums[1]) break;
57 if(player_sums[0] <= 16 && player_max_sums[0] <= 16) twist(0);
58 else break;
59 }
60 s1 = player_sums[0]; s2 = player_max_sums[0];
61 if(s2 <= 21 && s2 > s1) player_sums[0] = s2;
62 }

114
61 bool win() {
62 if(player_sums[1] > 21) return false; // defaite car joueur > 21
63 if(player_sums[0] > 21) return true; // victoire car croupier > 21
64 if(player_sums[1] >= player_sums[0]) return true; // victoire aux points
65 return false; // defaite aux points
66 }
67 };

Définition d’une politique


1 #define DO_STICK 1
2 #define DO_TWIST 2
3 int policy_stick_19(bj_t& _BJ) {
4 if(_BJ.player_sums[1] >= 19) return DO_STICK;
5 if(_BJ.player_max_sums[1] >= 19) return DO_STICK;
6 return DO_TWIST;
7 }

Evaluation MC d’une politique


On évalue les actions DO_STICK et DO_TWIST ; on applique des récompenses aux
séquences menant à la victoire ; dans ces séquences, on récompense les états en
augmentant leur score ; pour chaque états, on distingue les gains obtenus avec
l’action DO_STICK (dans stick_scores et stick_effectif) et les gains obte-
nus avec l’action DO_TWIST (dans twist_scores et twist_effectif) ; le score
moyen d’une action pour un état sera obtenu en divisant scores par effectif (i.e.
pour l’action DO_TWIST, avec twist_scores / twist_effectif) ; afin de pou-
voir comparer les résultats avec des évaluations TD, les scores sont normalisés
dans la fonction normalize_scores_MC ; pour les états ayant conservé l’effectif
initial après l’évaluation MC, on fixe un score nul ; après évaluation MC, les
scores sont dans twist_scores et dans stick_scores.

1 double stick_scores[10][22];
2 double twist_scores[10][22];
3 int stick_effectif[10][22];
4 int twist_effectif[10][22];
5 #define INITIAL_SCORE -1.0
6 #define INITIAL_EFFECTIF 1
7 void init_scores() {
8 for(int i = 0; i < 10; i++)
9 for(int j = 0; j < 22; j++){
10 stick_scores[i][j] = INITIAL_SCORE;
11 twist_scores[i][j] = INITIAL_SCORE;
12 stick_effectif[i][j] = INITIAL_EFFECTIF;
13 twist_effectif[i][j] = INITIAL_EFFECTIF;
14 }
15 }

115
12 // jouer contre le croupier
13 void play_game() {
14 bj_t BJ;
15 BJ.new_game();
16 std::vector<int> p_scores_seq; // scores du joueur
17 int c_card_seq = get_card_value(BJ.player_cards[0][0]); // carte visible du croupier
18 std::vector<int> actions_seq;
19 while(1) {
20 int decision = policy_stick_19(BJ);
21 if(decision == DO_STICK) {
22 actions_seq.push_back(DO_STICK);
23 p_scores_seq.push_back(BJ.player_sums[1]);
24 break;
25 } else { // DO_TWIST
26 actions_seq.push_back(DO_TWIST);
27 p_scores_seq.push_back(BJ.player_sums[1]);
28 BJ.twist(1);
29 }
30 if(BJ.player_sums[1] > 21) break;
31 }
32 BJ.finalize();
33 double reward = 1.0;
34 if(!BJ.win()) reward = -1.0;
35 for(int i = 0; i < (int)actions_seq.size(); i++) {
36 int card_id = c_card_seq-1;
37 int sum_id = p_scores_seq[i];
38 if(actions_seq[i] == DO_STICK) {
39 stick_scores[card_id][sum_id] += reward;
40 stick_effectif[card_id][sum_id] += 1;
41 } else { // action est DO_TWIST
42 twist_scores[card_id][sum_id] += reward;
43 twist_effectif[card_id][sum_id] += 1;
44 }
45 }
46 }
47 void normalize_scores_MC(double(*T) [22], int(*N) [22]) {
48 double min_score = DBL_MAX;
49 double max_score = -DBL_MAX;
50 for(int i = 0; i < 10; i++)
51 for(int j = 0; j < 22; j++) {
52 if(N[i][j] != INITIAL_EFFECTIF) T[i][j] = T[i][j]/N[i][j];
53 if(T[i][j] < min_score) min_score = T[i][j];
54 if(T[i][j] > max_score) max_score = T[i][j];
55 }
56 double ratio = fabs(min_score-max_score);
57 for(int i = 0; i < 10; i++)
58 for(int j = 0; j < 22; j++) {
59 if(N[i][j] == INITIAL_EFFECTIF) T[i][j] = 0.0;
60 else T[i][j] = (T[i][j] - min_score)/ratio;
61 }
62 }
63 void MC_evaluation(int _k){
64 init_scores();
65 for(int i = 0; i < _k; i++) play_game();
66 normalize_scores_MC(twist_scores, twist_effectif);
67 normalize_scores_MC(stick_scores, stick_effectif);
68 }
69 /* g++ -std=c++11 blackjack-MC.cpp */
70 int main(int _ac, char** _av) {
71 MC_evaluation(1000); // evaluation MC avec 1k playouts
72 }

116
Résultats de l’évaluation MC
Les figures 17 et 18 présentent les résultats de l’évaluation MC de la politique
policy_stick_19 ; ces matrices présentent les chances de succès dans la prise
de décision DO_STICK (i.e. passer) ou DO_TWIST (i.e. demander une carte) ; pour
des chances de succès de 100%, la case est bleue ; pour des chances de succès
nulle, la case est rouge ; pour prendre cette décision, on utilise en x, la carte
visible du croupier et en y, la somme des cartes du joueur ; l’évaluation MC est
plus précise avec 100k playouts qu’avec 1k playouts.

Fig. 17 – Evaluation MC pour DO_TWIST et DO_STICK avec 1k playouts.

Fig. 18 – Evaluation MC pour DO_TWIST et DO_STICK avec 100k playouts.

Les valeurs en y étant des sommes de cartes calculées avec des valeurs de 1 pour
les As, la forte probabilité de gain pour l’action DO_STICK autour de 10 sur la
figure 18 correspond à des séquences de cartes contenant un As.

117
Evaluation TD d’une politique
1 void play_game_TD() {
2 // ...
3 BJ.finalize();
4 double gamma = 0.9;
5 double reward = 1.0;
6 if(!BJ.win()) reward = -1.0;
7 for(int i = 0; i < (int)actions_seq.size(); i++) {
8 int card_id = c_card_seq-1;
9 int sum_id = p_scores_seq[i];
10 double TD_target = 0.0;
11 if(actions_seq[i] == DO_STICK) {
12 stick_effectif[card_id][sum_id] += 1;
13 double alpha = 1.0/(double)stick_effectif[card_id][sum_id];
14 if(i != ((int)actions_seq.size()) -1)
15 TD_target = 1.0+gamma*stick_scores[card_id][p_scores_seq[i+1]];
16 else TD_target = reward+gamma*reward;
17 stick_scores[card_id][sum_id] += alpha*(TD_target - stick_scores[card_id][sum_id]);
18 } else { // action est DO_TWIST
19 twist_effectif[card_id][sum_id] += 1;
20 double alpha = 1.0/(double)twist_effectif[card_id][sum_id];
21 if(i != ((int)actions_seq.size()) -1)
22 TD_target = 1.0+gamma*twist_scores[card_id][p_scores_seq[i+1]];
23 else TD_target = reward+gamma*reward;
24 twist_scores[card_id][sum_id] += alpha*(TD_target - twist_scores[card_id][sum_id]);
25 }
26 }
27 }
28 void normalize_scores_TD(double(*T) [22], int(*N) [22]) {
29 double min_score = DBL_MAX;
30 double max_score = -DBL_MAX;
31 for(int i = 0; i < 10; i++)
32 for(int j = 0; j < 22; j++) {
33 if(N[i][j] != INITIAL_EFFECTIF) {
34 if(T[i][j] < min_score) min_score = T[i][j];
35 if(T[i][j] > max_score) max_score = T[i][j];
36 }
37 }
38 double ratio = fabs(min_score-max_score);
39 for(int i = 0; i < 10; i++)
40 for(int j = 0; j < 22; j++) {
41 if(N[i][j] == INITIAL_EFFECTIF) T[i][j] = 0.0;
42 else T[i][j] = (T[i][j] - min_score)/ratio;
43 }
44 }
45 void evaluation_TD(int _k){
46 init_scores();
47 for(int i = 0; i < _k; i++) play_game_TD();
48 normalize_scores_TD(twist_scores, twist_effectif);
49 normalize_scores_TD(stick_scores, stick_effectif);
50 }
51 /* g++ -std=c++11 blackjack-TD.cpp */
52 int main(int _ac, char** _av) {
53 evaluation_TD(1000); // evaluation TD avec 1k iterations
54 }

118
Résultats de l’évaluation TD
Les figures 19 et 20 présentent les résultats de l’évaluation TD(0) de la politique
policy_stick_19.

Fig. 19 – Evaluation TD pour DO_TWIST et DO_STICK avec 1k itérations.

Fig. 20 – Evaluation TD pour DO_TWIST et DO_STICK avec 100k itérations.

L’évaluation TD(0) est plus fidèle à la réalité du blackjack que l’évaluation MC


dans le sens où : 1) l’action DO_TWIST est favorable à la victoire pour les valeurs
de 4 à 10 en y ; 2) l’action DO_STICK reste clairement favorable à la victoire pour
une valeur de somme proche de 10 dans le cas d’une main contenant un As ; 3)
l’intérêt de l’action DO_STICK diminue avec des valeurs croissantes de la carte
du croupier ; la position la plus favorable à la victoire pour le joueur correspond
aux cas dans lesquels le croupier possède une carte visible de valeur 3 ou 4.

119
Evaluation TD(λ) d’une politique

1 void play_game_TD_lambda() {
2 // ...
3 BJ.finalize();
4 double gamma = 0.9;
5 double reward = 1.0;
6 if(!BJ.win()) reward = -1.0;
7 for(int i = 0; i < (int)actions_seq.size(); i++) {
8 int card_id = c_card_seq-1;
9 int sum_id = p_scores_seq[i];
10 double TD_target = 0.0;
11 double ret_acc = 1.0;
12 double TD_error;
13 int last_id = p_scores_seq[(int)actions_seq.size()-1];
14 for(int j = i+1; j < (int)actions_seq.size()-1; j++) {
15 ret_acc *= gamma;
16 TD_target += ret_acc;
17 }
18 ret_acc *= gamma;
19 TD_target += (ret_acc*reward);
20 if(actions_seq[i] == DO_STICK) {
21 stick_effectif[card_id][sum_id] += 1;
22 double alpha = 1.0/(double)stick_effectif[card_id][sum_id];
23 if(i != ((int)actions_seq.size()) -1) {
24 TD_target += gamma*stick_scores[card_id][last_id];
25 } else {
26 TD_target += gamma*reward;
27 }
28 stick_scores[card_id][sum_id] += alpha*(TD_target - stick_scores[card_id][sum_id]);
29 } else { // action est DO_TWIST
30 twist_effectif[card_id][sum_id] += 1;
31 double alpha = 1.0/(double)twist_effectif[card_id][sum_id];
32 if(i != ((int)actions_seq.size()) -1) {
33 TD_target += gamma*twist_scores[card_id][last_id];
34 } else {
35 TD_target += gamma*reward;
36 }
37 twist_scores[card_id][sum_id] += alpha*(TD_target - twist_scores[card_id][sum_id]);
38 }
39 }
40 }
41 void evaluation_TD_lambda(int _k){
42 init_scores();
43 for(int i = 0; i < _k; i++) play_game_TD_lambda();
44 normalize_scores_TD(twist_scores, twist_effectif);
45 normalize_scores_TD(stick_scores, stick_effectif);
46 }
47 /* g++ -std=c++11 blackjack-TD-lambda.cpp */
48 int main(int _ac, char** _av) {
49 evaluation_TD_lambda(1000); // evaluation TD-lambda avec 1k iterations
50 }

120
Résultats de l’évaluation TD(λ)
Les figures 21 et 22 présentent les résultats de l’évaluation TD(λ) de la politique
policy_stick_19.

Fig. 21 – Evaluation TD(λ) pour DO_TWIST et DO_STICK avec 1k itérations.

Fig. 22 – Evaluation TD(λ) pour DO_TWIST et DO_STICK avec 100k itérations.

L’évaluation TD(λ) est plus fidèle à la réalité du blackjack que les évaluations
MC et TD dans le sens où : 1) l’évaluation TD(λ) donne plus d’importance à
l’action DO_STICK autour de la valeur 10 (correspondant à une main avec un
As) ; 2) l’évaluation TD(λ) pour des valeurs supérieures à 12 est similaire à
l’évaluation TD ; 3) les constats précedemment établis entre l’évaluation MC et
TD, restent valablent entre les évaluations MC et TD(λ).

121
Construction d’une politique avec VI et MC
L’objectif est de définir une politique de décision en choisissant l’action DO_TWIST
ou l’action DO_STICK pour une carte visible de croupier et une somme des cartes
du joueur ; pour chaque action, on maintient une valeur qui définit l’action la
plus importante ; dans la fonction policy_MC_stick, si stick_value est su-
périeure à twist_value, la décision est d’appliquer l’action DO_STICK ; alter-
nativement, l’action à appliquer est DO_TWIST ; la politique est initialisée avec
une décision d’appliquer DO_TWIST partout ; la MAJ de la politique incrémente
twist_value pour les premières cartes et incrémente stick_value pour la der-
nière carte en cas de victoire et l’avant dernière carte en cas de défaite ; la
politique est itérativement MAJ en suivant le principe value-iteration dans la
fonction play_game_value_iteration.

1 void init_MC_stick() {
2 for(int i = 0; i < 10; i++)
3 for(int j = 0; j < 22; j++) {
4 stick_value[i][j] = 0; twist_value[i][j] = 1;
5 }
6 }
7 void update_MC_stick(bool _new_win, int _c, int _s) {
8 if(_new_win) stick_value[_c][_s] ++;
9 else twist_value[_c][_s] ++;
10 }
11 int policy_MC_stick(int _p0_card, int _p1_sum) {
12 if(stick_value[_p0_card][_p1_sum] >= twist_value[_p0_card][_p1_sum])
13 return DO_STICK;
14 return DO_TWIST;
15 }
16 void play_game_value_iteration() {
17 bj_t BJ;
18 BJ.new_game();
19 std::vector<int> p_scores_seq; // scores du joueur
20 int c_card_seq = get_card_value(BJ.player_cards[0][0]); // carte visible du croupier
21 int card_id = c_card_seq-1;
22 while(1) {
23 int decision = policy_MC_stick(BJ.player_sums[1], card_id);
24 p_scores_seq.push_back(BJ.player_sums[1]);
25 if(decision == DO_STICK) break;
26 BJ.twist(1);
27 if(BJ.player_sums[1] > 21) break;
28 }
29 BJ.finalize();
30 if(BJ.win()) {
31 for(int i = 0; i < ((int)p_scores_seq.size())-1; i++)
32 update_MC_stick(false, card_id, p_scores_seq[i]);
33 int last_sum = p_scores_seq[((int)p_scores_seq.size())-1];
34 update_MC_stick(true, card_id, last_sum);
35 } else {
36 for(int i = 0; i < ((int)p_scores_seq.size())-2; i++)
37 update_MC_stick(false, card_id, p_scores_seq[i]);
38 if(((int)p_scores_seq.size()) > 1) {
39 int before_last_sum = p_scores_seq[((int)p_scores_seq.size())-2];
40 update_MC_stick(true, card_id, before_last_sum);
41 }
42 }
43 }

122
La figure 23 présente la politique définie (l’action DO_STICK étant en rouge et
l’action DO_TWIST étant en bleu) en utilisant les principes MC et value-iteration ;
la précision de l’évaluation augmente avec le nombre d’itérations ; la politique
construite suggère d’appliquer l’action DO_STICK pour des mains de valeurs 11
et de 21.

Fig. 23 – Politique policy_MC_stick avec 1k et 100k itérations.

La figure 24 présente la politique définie en utilisant les proportions entre les


valeurs définies avec policy_MC_stick ; le ratio r entre DO_STICK et DO_TWIST
est défini par r[i][j] = max(stick_value[i][j]/twist_value[i][j], 2)/2 ; cette fi-
gure présente un choix moins tranché que la figure précédente et donne une
espérance de victoire pour chaque décision ; ces espérances établies à partir de
valeurs MC sont très optimistes pour les valeurs de main proches de 20 ; ces
espérances permettent de réduire le poids de l’action DO_STICK pour des mains
de petites valeurs.

Fig. 24 – Politique proportionnelle aux valeurs définies par policy_MC_stick


avec 1k et 100k itérations.

123
Si on regarde la convergence des valeurs de la politique définie par la fonction
policy_MC_stick, conditionnée par 100 itérations donnant des choix d’actions
identiques, la politique converge après 2178 itérations ; la figure 25 présente la
politique définie avec les valeurs retournées par la politique policy_MC_stick
et avec les ratios r comme défini précédemment.

Fig. 25 – Politique définie après 2178 itérations, définies par policy_MC_stick,


par convergence des valeurs sur 100 itérations.

124
10 Annexes
10.1 DataFrame de pandas en Python
La classe DataFrame de la librairie panda permet de définir des tableaux à deux
dimensions, indexés selon des clés.

1 my_actions = ["A","B","C",]
2 table = pd.DataFrame(columns=list(range(len(my_actions))), \
3 dtype=np.float64)
4 # ajouter un élément
5 new_element = [0,1]
6 table = table.append(pd.Series([0] * len(my_actions), \
7 index=table.columns, name=str(new_element)))
8 new_element = [0,3]
9 rand_val = []
10 for i in range(0, len(my_actions)):
11 rand_val.append(np.random.uniform())
12 table = table.append(pd.Series(rand_val, index=table.columns, \
13 name=str(new_element)))
14 # modifier une des valeur d’un élément
15 table.loc[str([0,1]), 2] = 0.1
16 # associer une valeur NaN
17 table.loc[str([0,1]), 1] = np.nan
18 # afficher
19 print(table)
0 1 2
[0, 1] 0.000000 NaN 0.100000
[0, 3] 0.850929 0.552237 0.320382

En considérant les valeurs comme les scores des actions A,B et C pour chacun
des états, notre table indique que l’action B (qui est l’action d’indice 1) n’est
pas possible dans l’état [0,1].

Il est possible de définir directement l’état [0,1] comme suit :


1 new_element = [0,1]
2 table = table.append(pd.Series([0,0.1], index=[0,2], \
3 name=str(new_element)))

Il est important de ne pas ajouter dans la table deux fois le même élément, qui
sera la cause d’erreur à l’exécution.

Avant d’ajouter un élément dans la table, vérifier qu’il n’est pas déjà dedans :
1 new_element = [0,4]
2 if str(new_element) not in table.index:
3 print("%s is not in table" % (str(new_element)))
[0, 4] is not in table

125
Il est possible de récupérer la valeur d’une action d’un élément, la valeur max
d’un élément et l’indice de cette valeur max (autrement dit, le score de la
meilleure action d’un élément et l’indice de cette meilleure action) :
1 #lire une valeur d’un élément
2 new_element = [0,1]
3 val = table.loc[str(new_element), 2]
4 print("in %s at 2 : %f" %(str(new_element), val))
5 #obtenir la valeur max d’un élément
6 maxval = table.loc[str(new_element), :].max()
7 print("maxval: %f" %(maxval))
8 #obtenir d’index de la valeur max d’un élément
9 maxid = table.loc[str(new_element), :].idxmax()
10 print("maxid: %d" %(maxid))
in [0, 1] at 2 : 0.100000
maxval: 0.100000
maxid: 2

126

Vous aimerez peut-être aussi