Theorie Des Jeux

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 14

Éléments de la théorie des jeux PSI* 22-23

Éléments de la théorie des jeux

Dans ce chapitre, on donne une introduction à l’étude de la théorie des jeux. Les jeux que l’on va étudier
sont des jeux à information complète, ce qui exclut la plupart des jeux de cartes (chaque joueur n’a pas
connaissance du jeu adverse et éventuellement du reste du paquet), et sans hasard (ce qui exclut les jeux de
dés par exemple).
On ne s’intéresse qu’à des jeux d’accessibilité à deux joueurs, qui peuvent être modélisés par des graphes
orientés, où jouer un coup consiste à suivre un arc. Dans un premier temps, on discute de jeux qui sont
suffisamment simples pour pouvoir être résolus complètement : l’espace des états possibles n’est pas trop
grand. Dans un second temps, on aborde la notion de stratégie avec heuristique, qui peut s’appliquer aux
jeux qu’il est impossible (à l’heure actuelle, au vu des contraintes matérielles) de résoudre complètement,
comme le jeu d’échecs ou le jeu de go.

I. Jeu d’accessibilité sur un graphe


I.1. Exemple : le jeu de Nim
Le principe du jeu de Nim est le suivant :
– on dispose au départ de N allumettes (ou bâtonnets, ou jetons, ou cailloux...) ;
– deux joueurs A et B jouent tour à tour, en prélevant 1, 2 ou 3 allumettes dans le tas ;
– le joueur qui enlève la dernière allumette a perdu.
Il est facile de voir que, si N . 1 (mod 4) , le joueur qui commence gagne, sa stratégie étant de laisser à
l’adversaire un nombre d’allumettes congru à 1 modulo 4 .
Modélisation par un graphe
À un tel jeu, on peut associer un graphe orienté G = (S, A) ( A ⊂ S × S ) :
– les sommets S de G sont les positions atteignables dans le jeu. Pour le jeu précédent, on pourrait
utiliser N + 1 sommets numérotés de 0 à N , indiquant le nombre de bâtonnets restants.
– les arcs A de G indiquent quel sommet est atteignable depuis un autre en jouant un unique coup.

Figure 1 - Un jeu de Nim avec 9 allumettes au départ


Une partie sur un tel graphe est un chemin où le premier joueur, depuis le sommet de départ, suit un arc,
et ensuite chaque joueur suit à tour de rôle un arc, si c’est possible. Une partie est finie si ce chemin termine
sur un sommet sans successeur, infinie sinon.
Il est plus utile dans la pratique de ≪ dédoubler ≫ un graphe comme le précédent en un graphe biparti,
de sorte qu’une partie dans le jeu se résume simplement à un chemin dans le graphe biparti, sans avoir à
distinguer quel joueur joue.
Par exemple, le graphe biparti associé au précédent est le suivant :

Figure 2 - Graphe biparti pour le jeu de Nim avec 9 allumettes


En Python, un tel graphe peut être représenté par sa liste d’adjacence, mais il est plus performant d’utiliser
un dictionnaire : les clés en seront les couples (joueur, sommet), et les valeurs la liste des sommets
successeurs. La fonction ci-dessous construit ce dictionnaire.

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 1/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

def Construction_Graphe_Nim(N, J1, J2):


# N = nombre d'allumettes
# J1, J2 : symboles représentant les joueurs
G={}
for i in range(N, 2, -1):
G[(J1, i)] = [ (J2, i-1), (J2, i-2), (J2, i-3) ]
G[(J2, i)] = [ (J1, i-1), (J1, i-2), (J1, i-3) ]
G[(J1, 2)] = [ (J2, 1), (J2, 0) ]
G[(J2, 2)] = [ (J1, 1), (J1, 0) ]
G[(J1, 1)] = [ (J2, 0) ]
G[(J2, 1)] = [ (J1, 0) ]
G[(J1, 0)] = []
G[(J2, 0)] = []
return G

I.2. Vocabulaire
Définition :
Un graphe orienté G = (S, A) est dit biparti s’il existe une partition de S en deux sous-ensembles S A
et SB , telle qu’aucun arc du graphe ne relie deux éléments de S A , ou deux éléments de SB . Autrement
dit, si l’origine d’un arc est dans S A , l’extrémité est dans SB et réciproquement.
Lorsque le graphe biparti est associé à un jeu à deux joueurs A et B , il s’appelle une arène, et les
éléments de S A (resp. SB ) sont dits contrôlés par le joueur A (resp. B ).
Par exemple, dans le graphe figure 2, les sommets contrôlés par le joueur A sont en gris clair et ceux
contrôlés par le joueur B en gris foncé.
Définition :
– Un chemin dans un graphe orienté G est dit maximal s’il est infini ou si l’extrémité de son dernier arc
est un sommet terminal, c’est-à-dire qui n’a pas de successeur.

– Soit G = (S, A), S A , SB un graphe de jeu. Une partie débutant en s ∈ S est un chemin maximal
d’origine s .
Définition :
– Une stratégie pour le joueur
 A est une application σ qui à tout sommet s ∈ S A associe un sommet
σ (s) ∈ SB avec s, σ (s) ∈ A (on définit de la même façon une stratégie pour le joueur B ).
– Une partie P = (s0 , s1 , . . . , s p , . . .) est dite conforme à la stratégie σ pour le joueur A si, pour tout
s k ∈ P ∩ S A , s k +1 = σ ( s k ) .
Définition :

Soit G = (S, A), S A , SB un graphe de jeu. On dit qu’il s’agit d’un jeu d’accessibilité si
– il n’y a pas de match nul ;
– le joueur A (resp. B ) gagne une partie si et seulement si cette partie contient un sommet d’un
certain sous-ensemble VA (resp. VB ). VA et VB sont les conditions de victoire pour le joueur A
(resp. B ).
Définition :
Dans un jeu d’accessibilité, on dit qu’une stratégie σ est une stratégie gagnante depuis un sommet
s0 pour un joueur J si ce joueur gagne la partie lorsque cette partie est conforme à la stratégie σ , et
cela, quelle que soit la stratégie suivie par l’autre joueur.
Dans le jeu de Nim, puisque la partie est perdue dès qu’un joueur prend la dernière allumette, on peut
enlever du graphe les sommets 0 A et 0B ; dans ce cas, VA = {1B } et VB = {1 A } .

Figure 3 - En gras, les arcs d’une stratégie gagnante pour le joueur A

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 2/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

I.3. Attracteur
Il s’agit ici de trouver un algorithme permettant de déterminer l’ensemble des positions gagnantes pour un
joueur.

Soit donc un jeu d’accessibilité, représenté par un graphe de jeu G = (S, A), S A , SB , et VA une condition
de victoire pour le joueur A . On définit par récurrence l’attracteur de VA de la façon suivante :
 on pose A0 (VA ) = VA ;
 pour tout entier j l’ensemble A j+1 (VA ) contient :
– les sommets de A j (VA ) ;
– les sommets contrôlés par le joueur A pour lesquels il existe au moins un arc d’extrémité un sommet
de A j (VA )
– les sommets contrôlés par le joueur B dont tous les arcs aboutissent à des sommets de A j (VA ) .
La suite des A j (VA ) est évidemment croissante, et elle est stationnaire puisque les A j (VA ) sont inclus dans
S , ensemble fini.
Par définition, l’attracteur de VA est A(VA ) = A j (VA ) (réunion finie en fait). Cet ensemble contient
S
j ∈N
l’ensemble des positions gagnantes pour le joueur A .
Programmation
On commence par écrire une fonction Inverse Graphe(G) qui permet de construire la liste (sous forme d’un
dictionnaire) des prédécesseurs des sommets du graphe, c’est-à-dire d’inverser le sens des arêtes du graphe.

def Inverse_Graphe(G):
G_inv = {s: [] for s in G}
for s in G:
for s1 in G[s]:
G_inv[s1].append(s)
return G_inv

On peut ensuite, soit donner directement les conditions de victoire pour chaque joueur, soit les déterminer,
en considérant qu’un sommet est victorieux pour un joueur si l’autre ne peut plus jouer (en effet, on a
supposé qu’il n’y a pas de match nul). C’est ce que fait la fonction Victoire(G, joueur).

def Victoire(G, Joueur):


V = []
for s in G:
if G[s] == [] and s[0] != Joueur:
V.append(s)
return V

Enfin la fonction Attracteur(G, Joueur) détermine l’ensemble des positions gagnantes pour un joueur
donné, en construisant la suite des A j (V ) comme ci-dessus.

def Attracteur(G, Joueur):


G_inv = Inverse_Graphe(G)
V = Victoire(G, Joueur)
# récurrence, il y a au maximum len(G) étapes
A = V # A contient A_j
for j in range(1, len(G)):
Aj = A # contiendra A_{j+1}
for s in A:
for s1 in G_inv[s]:
# tous les prédecesseurs conviennent
# s'ils sont controlés par le joueur
if s1[0] == Joueur:
if s1 not in Aj: # pas de doublon
Aj.append(s1)
else:
# si sommet prédecesseur controlé par adv
# tous ses successeurs doivent etre dans A
convient = True

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 3/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

for s2 in G[s1]:
if not s2 in Aj:
convient = False
break
if convient:
if s1 not in Aj:
Aj.append(s1)
if len(Aj) == len(A):
break
else:
A = Aj
return(A)

Rem: Pour obtenir une stratégie gagnante pour un joueur à partir d’un sommet s , il suffit, une fois
déterminé l’attracteur A(V ) , de chercher un successeur de s dans G qui soit dans cet attracteur.

II. Algorithme minimax


II.1. Généralités
Les jeux précédents, que l’on peut résoudre intégralement à l’aide d’un parcours de graphe, ne sont pas
très intéressants, puisqu’une stratégie optimale est connue ! Des jeux plus compliqués sont par exemple les
échecs ou le jeu de go : ces jeux ne sont pas résolus entièrement dans le sens où on ne connaı̂t pas de stratégie
optimale. En effet, le nombre de positions possibles est de l’ordre de 1032 pour le jeu de dames, 1043 à 1050
pour le jeu d’échecs et 10100 pour le jeu de Go, ce qui rend une exploration exhaustive impossible.
Malgré cette impossibilité, les humains sont aujourd’hui dépassés par la machine 1
Cette section vise à donner un aperçu d’un programme informatique capable de jouer à de tels jeux où
l’espace des positions est trop grand pour être exploré exhaustivement.
On étudie les jeux à information complète, à deux joueurs, à somme nulle, et à coups asynchrones en nombre
fini. Précisons un peu ces termes :
– un jeu à information complète est un jeu où chaque joueur connaı̂t les actions qu’il peut entreprendre,
ainsi que ses adversaires, et les gains résultants de telles actions.
– un jeu à coups asynchrones est un jeu où chaque joueur joue alternativement.
En nombre fini signifie qu’une partie ne peut être infinie (aux échecs, une règle impose que si aucun
pion n’a avancé ou aucune pièce n’a été capturée en 50 coups, la partie est nulle).
– un jeu à somme nulle est un jeu où le gain du premier joueur correspond à la perte de l’autre joueur. En
pratique, le gain du premier joueur est souvent réduit aux trois valeurs +∞ (il gagne), 0 (partie nulle),
−∞ (le deuxième joueur gagne), mais dans la suite ce gain pourra être toute valeur de R ∪ {±∞} .
Dans la suite, on appellera un tel jeu un jeu Min-Max, et les deux joueurs seront appelés Max et Min : le but
de Max est de maximiser son gain, le but de Min est de minimiser le gain du joueur Max.

II.2. Représentation par un arbre, algorithme Min-Max


Notion d’arbre.
Un tel jeu se représente sous forme arborescente. Formellement, un arbre est un graphe connexe acy-
clique, dont on choisit un nœud particulier (la racine), qui oriente l’arbre. En informatique, les arbres
≪ poussent ≫ de haut en bas : la racine est donc située en haut.

La profondeur d’un nœud est sa distance à la racine (la racine est donc l’unique nœud à profondeur zéro).
Pour la représentation graphique, tous les nœuds à une même profondeur sont représentés sur une même
ligne horizontale. Le fait que le graphe soit connexe et acyclique impose qu’il existe un unique chemin
simple (sans sommet en double) de la racine r à un nœud n quelconque de l’arbre. Le long de ce chemin il
y a une relation de parenté entre nœuds successifs : si p précède q , p est le père de q et q est unfils de p .
Un nœud sans fils est appelé une feuille.
Arbre pour un jeu Min-Max.
Un jeu Min-Max se représente comme un arbre, où les nœuds représentent des positions du jeu. Puisque les
joueurs jouent alternativement, les nœuds d’un même niveau sont alternativement contrôlés par un même
joueur. Par symétrie, on peut supposer que :
1. Aux jeux d’échecs, Deeper Blue bat le champion du monde en titre Garry Kasparov en 1997. Il a fallu attendre 2017 pour que le
champion du monde du jeu de Go Ke Jie soit battu par le programme informatique AlphaGo.

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 4/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

– la racine est contrôlée par le joueur Max ;


– les nœuds à profondeur 1 sont contrôlés par le joueur Min ;
– les nœuds à profondeur 2 sont contrôlés par le joueur Max ;
– etc...
Il est clair qu’il est impossible en général d’énumérer l’arbre complet du jeu, jusqu’à ce que l’on obtienne
une position gagnante. Pour cette raison, on utilisera une évaluation approchée des positions atteintes
à un certain niveau. On supposera donc construite une fonction h , appelée heuristique, à valeurs dans
R ∪ {±∞} , telle que, pour toute position p :
– plus h( p) est grand, meilleure est la position pour Max ; en particulier, si p est une position gagnante
pour Max, h( p) = +∞ .
– plus h( p) est petit, meilleure est la position pour Min ; en particulier, si p est une position gagnante
pour Min, h( p) = −∞ .
– si p correspond à une partie nulle, h( p) = 0 .
Toute la difficulté est de trouver une heuristique prenant en compte tous les aspects stratégiques du jeu. Par
exemple, pour un jeu d’échecs, on pourrait donner des valeurs aux pièces : 9 pour la dame, 5 pour une
tour, 3.25 pour un fou ou un cavalier, et 1 pour un pion. En faisant la différence entre les qualités des pièces
blanches et noires, on obtient une évaluation de la position, mais celle-ci est évidemment trop simpliste.
Un exemple
Voyons le principe de l’algorithme utilisé sur un exemple.
Ici, Max a 3 coups à jouer, pour lesquels la fonction h a donné les valeurs 1 à, 4 et −1 . Comme Max cherche
à maximiser son score, il jouera le coup conduisant au score 10 :

Max

10 4 -1

Mais Max peut aussi tenir compte du coup que va ensuite jouer Min, et donc calculer la valeur des positions
atteintes au deuxième niveau :

Max

  

-10 1 -5 0 -2 -4 -5 1 0

Ici, on voit que Max aura plutôt intérêt à jouer le deuxième coup : en effet, s’il joue le 1er, Min pourra obtenir
-10, s’il joue le second, Min pourra obtenir -4 et s’il joue le troisième, Min pourra obtenir -5.
(on rappelle que Max veut maximiser son score et Min le minimiser).
On peut réitérer le raisonnement, et tenir compte du coup suivant joué par Max :

Max

  

        

-5 -2 3 1 -2 9 0 -2 -4 2 -2 6 0 1 4 1 -2 2 2 1 3 0 7 8 0 -2 4

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 5/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

Maintenant, si Max joue le 1er coup, il peut espérer au maximum un score de 0 , s’il joue le 2ème, il peut
espérer un score de 2, et s’il joue le 3ème, un score de 3.
L’algorithme min-max
Pour calculer le meilleur coup de Max, il faut donc commencer par calculer la valeur de l’heuristique de
toutes les positions atteignables en n coups (les feuilles de l’arbre).
Si n est pair, Min aura joué le dernier coup : le père de chacune de ces feuilles se verra donc attribuer le
minimum des valeurs de ses fils.
À l’inverse, si n est impair, le père de chacune de ces feuilles se verra attribuer la valeur maximale de ses
fils (car Max aura joué en dernier). Ainsi, de proche en proche chaque nœud de l’arbre se verra attribuer
une valeur.
Voici comment sera rempli l’arbre précédent, selon que c’est Max ou Min qui commence :

Max

0 2 3

3 9 0 6 4 2 3 8 4

-5 -2 3 1 -2 9 0 -2 -4 2 -2 6 0 1 4 1 -2 2 2 1 3 0 7 8 0 -2 4

Min

-2 0 1

-5 -2 -4 -2 0 -2 1 0 -2

-5 -2 3 1 -2 9 0 -2 -4 2 -2 6 0 1 4 1 -2 2 2 1 3 0 7 8 0 -2 4

L’algorithme Min-Max est naturellement récursif, et peut s’écrire de la façon simplifiée suivante.

Algorithme 1 : Min-Max
Données : pos : une position du jeu
n : un entier naturel, la profondeur de la recherche
On suppose connues une fonction h (heuristique) et une fonction
successeurs donnant les positions jouables après pos
Résultat : valeur de la position pos
def MinMax(pos, n) :
si pos est une feuille (partie terminée) alors
retourner −∞ , +∞ ou 0
finsi
si n = 0 alors
retourner h( pos)
finsi
si Max joue alors n o
retourner max MinMax(suivante, n − 1) pour suivante ∈ successeurs( pos)
sinon n o
retourner min MinMax(suivante, n − 1) pour suivante ∈ successeurs( pos)
finsi
fin

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 6/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

Certains trouvent qu’il est plus lisible de ≪ découper ≫ cet algorithme en deux parties, l’une MaxMin lorsque
c’est à Max de jouer, et l’autre, MinMax, lorsque c’est au tour de Min.
Cela s’écrira alors de la façon suivante :

def minmax(pos, n) :
# Calcule la valeur du nœud en prenant le minimum des valeurs
# des nœuds fils, ces valeurs étant calculées par maxmin

if successeurs(pos) = []:
return # à compléter selon la nature de la feuille
if n == 0 :
return h(pos)
score = inf
for suivante in successeurs(pos) :
s = maxmin(suivante, n-1)
if s < score:
score = s
return score

def maxmin(pos, n) :
# Calcule la valeur du nœud en prenant le maximum des valeurs
# des nœuds fils, ces valeurs étant calculées par minmax

if successeurs(pos) = []:
return # à compléter selon la nature de la feuille
if n == 0 :
return h(pos)
score = - inf
for suivante in successeurs(pos) :
s = minmax(suivante, n-1)
if s > score:
score = s
return score

Pour connaı̂tre le coup à jouer, il suffira en plus de retenir le mouvement qui a permis de minimiser ou
maximiser la valeur à la première étape.

II.3. L’élagage alpha-beta


Prenons d’abord un exemple simple.

Max

1 

1 2 3 -1 ? ? ?

Ici, c’est à Max de jouer, et l’algorithme Min-Max a attribué au premier noeud la valeur 1.
On étudie alors le second noeud, où c’est à Min de jouer. Or le premier coup de Min a obtenu le score
-1 ; on peut donc déjà affirmer que la valeur du second nœud sera inférieure à -1, et il est donc inutile de
poursuivre l’étude des autres coups : Max ne jouera pas le second coup.
On a effectué ci-dessus une coupure alpha : lors de l’exploration du 1er coup, la variable α a reçu la valeur
1, et quand on examine les coups suivants, on arrête l’étude dès que l’on trouve une valeur inférieure à α .
On a une situation symétrique lorsque c’est à Min de jouer :

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 7/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

Min

3 

1 2 3 4 ? ? ?

Ici, le second nœud aura de toutes façons un score supérieur à 4, il est donc inutile de l’étudier en entier.
On a effectué ici une coupure beta : la variable β a reçu la valeur 3 lors de l’exploration du premier coup,
et on arrête l’étude dès que l’on trouve une valeur supérieure à β .
Mise en œuvre
L’algorithme Min-Max va donc être modifié en ajoutant deux variables alpha et beta, initialisées à −∞ et
+∞ respectivement, et telles que les valeurs de tous les nœuds examinés soient toujours comprises entre α
et β .

# avant le 1er appel, alpha = _inf et beta = inf

def minmax(alpha, beta, pos, n) :

if successeurs(pos) = []:
return # à compléter selon la nature de la feuille
if n == 0 :
return h(pos)
score = inf
for suivante in successeurs(pos) :
s = maxmin(alpha, beta, suivante, n-1)
if s < score:
score = s
if score <= alpha: # coupure alpha
return score
beta = min(beta, score)
return score

def maxmin(alpha, beta, pos, n) :

if successeurs(pos) = []:
return # à compléter selon la nature de la feuille
if n == 0 :
return h(pos)
score = - inf
for suivante in successeurs(pos) :
s = minmax(alpha, beta, suivante, n-1)
if s > score:
score = s
if score >= beta: # coupure beta
return score
alpha = max(alpha, score)
return score

III. Une application : le jeu Puissance 4 (TP)


Pour illustrer cette section, nous allons prendre l’exemple du jeu Puissance 4 : le but du jeu est d’aligner une
suite de quatre pions de même couleur sur une grille comptant six rangées et sept colonnes. Tour à tour,
les deux joueurs placent un pion dans la colonne de leur choix, le pion coulisse alors jusqu’à la position la
plus basse possible dans la dite colonne à la suite de quoi c’est à l’adversaire de jouer. Le vainqueur est le
joueur qui réalise le premier un alignement (horizontal, vertical ou diagonal) consécutif d’au moins quatre
pions de sa couleur. Si, alors que toutes les cases de la grille de jeu sont remplies, aucun des deux joueurs
n’a réalisé un tel alignement, la partie est déclarée nulle.

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 8/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

Figure 5 - Le jeu Puissance 4


Plusieurs heuristiques sont envisageables (et elles peuvent être combinées).
1. La première façon d’évaluer une position consiste à attribuer à chaque case une valeur, par exemple
le nombre d’alignements potentiels de quatre pions lorsqu’on place un pion à cet emplacement, puis
à sommer les cases occupées (positivement pour les pions de Max, négativement pour ceux de Min).

Figure 6 - Valeurs des cases


Par exemple, si on convient que les pions jaunes sont ceux de Max, la valeur de l’heuristique de la
position présentée figure 5 est égale à 4 + 5 + 7 + 6 + 8 + 13 + 11 - 3 - 5 - 4 - 8 - 10 - 11 - 11 = 2.
Évidemment, l’heuristique sera égale à +∞ pour une position gagnante pour Max, et à −∞ pour une
position gagnante pour Min.
2. Une deuxième façon d’évaluer une position consiste à compter le nombre d’alignements de 4 cases
comportant une cases vide et 3 autres occupées par des pions de la même couleur, donc qui donnent
une position gagnante si le joueur concerné joue à cet emplacement. Là encore, ce nombre sera compté
positivement ou négativement selon le joueur concerné.
On peut encore raffiner cette évaluation en tenant compte des alignements comportant 2 cases vides
plus 2 autres de la même couleur, mais en l’affectant d’un coefficient moindre.
3. Enfin la dernière heuristique, qui a donné des résultats très satisfaisants lors du programme de jeu
de Go, est la méthode de Monte-Carlo. Elle consiste simplement, à partir d’un position, à jouer un
grand nombre de parties aléatoires, et à dénombrer le nombre de parties gagnantes pour Max et Min.
Le résultat de cette fonction sera le nombre de parties gagnantes pour Max moins celui de parties
gagnantes pour Min.

Mise en œuvre en Python

• Structure des données


Pour pouvoir étendre le jeu à un plateau plus grand, et à des alignements de plus de quatre jetons, on uti-
lisera des variables globales nb lignes, nb colonnes et long ligne gagnante, initialisées respectivement
à 6, 7 et 4.
La grille de jeu est représentée par une liste de nb lignes listes de longueurs nb colonnes ; chaque case
grille[i][j] peur contenir trois valeurs vide, rouge et jaune, dont la valeur importe peu (j’ai choisi 0,
1 et 2, mais cela pourrait être None, True et False).
Une fonction d’affichage très simpliste de la grille est fournie.
La situation à un moment donnée est représentée par une liste position, dont les 4 éléments sont : le
joueur dont c’est le tour (rouge ou jaune), la grille, le nombre de coups (= nombre de jetons sur la grille),
et le score correspondant à l’heuristique 1 (celui-ci est mis à jour à chaque tour, pour ne pas avoir à le
recalculer à chaque fois).

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 9/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

• Les procédures utiles


Voir le programme ci-dessous à compléter et les explications en classe.
Le programme à compléter

# puissance 4
from copy import deepcopy
from math import inf as infini
import sys
import random

def affiche(grille): # affichage très sommaire


print()
for i in range(nb_lignes - 1, -1, -1):
print(i, end=' ')
for j in range(nb_colonnes):
print(symboles[grille[i][j]], end='')
print()
print()
print(' ', end='')
for j in range(nb_colonnes):
print(j,end=' ')
print()
print()

def adversaire(joueur):
return jaune if joueur == rouge else rouge

def humain_gagne():
print('Bravo, vous avez gagné!')
sys.exit()

def ordi_gagne():
print("J'ai gagné!")
sys.exit()

def partie_nulle():
print('Grille pleine, partie nulle')
sys.exit()

def coups_possibles(grille):
# liste des coups possibles (ligne, colonne)
pass

def jouer_en_place(coup, position):


# modifie la variable position, sans s'occuper du score ni du nombre de coups
# la case est supposée jouable
joueur, grille, _, _ = position
ligne, colonne = coup
position[0] = adversaire(joueur)
position[1][ligne][colonne] = joueur

def jouer(coup, position):


# joue et renvoie une nouvelle position
# en actualisant le nombre de coups et le score
# la case est supposée jouable
joueur, grille, nb_coups, score = position
ligne, colonne = coup
grille_nouv = deepcopy(grille)
grille_nouv[ligne][colonne] = joueur
if joueur == rouge:
score += valeurs_cases[ligne][colonne]

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 10/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

else:
score -= valeurs_cases[ligne][colonne]
return [adversaire(joueur), grille_nouv, nb_coups+1, score]

def poids_cases():
# calcule le poids des cases:
# c'est le nombre de lignes gagnantes auxquelles appartient la case
# et construit la liste de tous les alignements
# remplit les var. globales: valeurs_cases, liste_tous_alignements, liste_alignements_par_case
'''
Pour vérifier votre programme, si nb_lignes = 6 et nb_colonnes = 7 et long_ligne_gagnante = 4,
le poids des cases doit être :
[ [3, 4, 5, 7, 5, 4, 3],
[4, 6, 8, 10, 8, 6, 4],
[5, 8, 11, 13, 11, 8, 5],
[5, 8, 11, 13, 11, 8, 5],
[4, 6, 8, 10, 8, 6, 4],
[3, 4, 5, 7, 5, 4, 3] ]
'''
liste_tous_alignements = []
liste_alignements_par_case = {}
valeurs_cases = [ [0 for j in range(nb_colonnes)] for i in range(nb_lignes) ]

# alignements horizontaux

# alignements verticaux

# alignements diagonale droite vers le haut

# alignements diagonale gauche vers le haut

# valeurs_cases et liste_tous_alignements doivent maintenant être remplis


# On remplit finalement liste_alignements_par_case

return valeurs_cases, liste_tous_alignements, liste_alignements_par_case

def alignement_gagnant_apres_coup(coup, grille):


# cherche si après avoir joué en coup = (ligne, colonne) on a obtenu un alignement
# si oui renvoie la couleur concernée sinon None
pass

def etude_alignements(grille):
# si il y a un alignement gagnant, renvoie True et +/- infini selon le joueur concerné
# sinon renvoie False et une valeur selon le nombre de lignes gagnantes à 1, 2 ou 3 trous près
# par exemple 0111 1011 1101 1110 :coeff 5
# par exemple 0011 0101 1100 : coeff 1
# par exemple 0001 0010 0100 1000 : coeff 0.1
nb_trous = {rouge:0, jaune:0}

return False, nb_trous

def resultat_partie_aleatoire(position):
# joue une partie aléatoire à partir de position
# renvoie -1 ou 1 selon joueur gagnant ou 0 si nul
pos = deepcopy(position) # la position initiale n'est pas modifiée !

pass

def resultat_aleatoire(position):
nb_parties = 30 # à voir selon le temps

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 11/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

# attention si on change ce nombre le coeff correspondant de l'heuristique devra changer


# l'ordi joue nb_parties au hasard à partir de position
# et compte celles gagnantes/perdantes
result = 0
for _ in range(nb_parties):
result += resultat_partie_aleatoire(position)
return result

def heuristique(position):
# s'il y a une position gagnante renvoie +- infini
# on suppose que la position n'est pas une partie nulle (testé avant)
# pondère la position selon l'intérêt des cases
# puis ajoute un coeff selon le nombre de lignes à un seul joueur
# et un autre selon le nombre de parties aléatoires gagnantes
joueur, grille, nb_coups, poids = position
result = etude_alignements(grille)
if result[0]:
return result[1] # il y a un gagnant, on renvoie sa couleur

pass

def min_max(alpha, beta, position, coup, n):


v = alignement_gagnant_apres_coup(coup, position[1])
if v != None: # un des joueurs a gagné, on est sur une feuille
return infini if v == rouge else -infini

liste_coups = coups_possibles(position[1])
if liste_coups == []: # partie nulle
return 0
if n == 0: # profondeur atteinte, on évalue
return heuristique(position)

mini = infini
for coup in liste_coups:
pos = jouer(coup, position)
score = max_min(alpha, beta, pos, coup, n-1)
if score < mini:
mini = score
if mini <= alpha: # coupure alpha
return mini
beta = min(beta, mini)
return mini

def max_min(alpha, beta, position, coup, n):


v = alignement_gagnant_apres_coup(coup, position[1])
if v != None:
return infini if v == rouge else -infini

liste_coups = coups_possibles(position[1])
if liste_coups == []:
return 0
if n == 0:
return heuristique(position)

maxi = -infini
for coup in liste_coups:
pos = jouer(coup, position)
score = min_max(alpha, beta, pos, coup, n-1)
if score > maxi:
maxi = score
if maxi >= beta: # coupure beta

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 12/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

return maxi
alpha = max(alpha, maxi)
return maxi

def humain_joue(position):
joueur, grille, _, _ = position
liste_coups = coups_possibles(grille)
if liste_coups == []:
partie_nulle()

colonnes = [ coups[1] for coups in liste_coups ]


correct = False
while not correct:
while True:
try:
j = int(input("No de colonne où vous jouez ? "))
break
except ValueError:
print("Oups! Ce n'est pas un entier, recommencez!")
correct = j in colonnes
if not correct:
print('Numéro de colonne incorrect! Recommencez.')
numero = colonnes.index(j)
i = liste_coups[numero][0]
position = jouer( (i,j), position)
affiche( position[1] )
if alignement_gagnant_apres_coup( (i,j), position[1]) == joueur:
humain_gagne()
ordi_joue(position)

def ordi_joue(position):
joueur, grille, nb_coups, _ = position
liste_coups = coups_possibles(position[1])
if liste_coups == []:
partie_nulle()

n = profondeur_arbre
if nb_coups > 15: # à voir selon rapidité de l'ordi
n += 2

alpha = -infini
beta = infini
best = random.choice(liste_coups) # au cas où le pgm perd, coup au hasard
if joueur == rouge:
# on cherche à maximiser
maxi = -infini
for coup in liste_coups:
pos = jouer(coup, position)
score = min_max(alpha, beta, pos, coup, n-1)
if score > maxi:
maxi = score
best = coup
alpha = max(alpha, maxi)
else:
mini = infini
for coup in liste_coups:
pos = jouer(coup, position)
score = max_min(alpha, beta, pos, coup, n-1)
if score < mini:
mini = score
best = coup

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 13/14 23 mars 2023


Éléments de la théorie des jeux PSI* 22-23

beta = min(beta, mini)

print('-----> Je joue colonne ', best[1])


position = jouer(best, position)
affiche( position[1] )
if alignement_gagnant_apres_coup(best, position[1]) == joueur:
ordi_gagne()
humain_joue(position)

# constantes du jeu, variables globales


nb_lignes = 6
nb_colonnes = 7
long_ligne_gagnante = 4 # nombre de pions à aligner pour gagner
vide = 0
rouge = 1
jaune = 2
symboles = {vide: ' '+chr(183)+' ', rouge: ' R ', jaune: ' J '} # pour l'affichage
profondeur_arbre = 6 # modifier selon vitesse de l'ordi

# valeurs cases pour l'heuristique


# et liste + dictionnaire avec tous les alignements possibles
valeurs_cases, liste_tous_alignements, liste_alignements_par_case = poids_cases()

# initialisation de la grille
grille_vide = [ [vide for j in range(nb_colonnes)] for i in range(nb_lignes) ]

# initialisation du jeu
correct = False
while not correct:
ch = input('Voulez-vous les rouges ou les jaunes (R/J) ? ')
correct = ch in ['r', 'R', 'j', 'J']
if not correct:
print("Je n'ai pas compris, recommencez.")
if ch == 'R' or ch == 'r':
humain = rouge; ordi = jaune
else:
humain = jaune; ordi = rouge
correct = False
while not correct:
ch = input('Voulez-vous commencer (O/N) ? ')
correct = ch in ['o', 'O', 'n', 'N']
if not correct:
print("Je n'ai pas compris, recommencez.")
humain_commence = (ch == 'O' or ch == 'o')

# une position est une liste [joueur, grille, nbcoups, score]


affiche(grille_vide)
if humain_commence:
position = [humain, grille_vide, 0, 0]
humain_joue(position)
else:
position = [ordi, grille_vide, 0, 0]
ordi_joue(position)

Informatique PSI* – © T.LEGAY – Lycée d’Arsonval 14/14 23 mars 2023

Vous aimerez peut-être aussi