Pr. EL HADIQ Zouhair
Jeux d'accessibilité à deux joueurs sur un graphe biparti : positions gagnantes, attracteurs, stratégies gagnantes et algorithme Minimax.
La théorie des jeux étudie les situations où plusieurs joueurs prennent des décisions qui s'influencent mutuellement, chacun cherchant à atteindre son propre objectif. On se limite ici à une famille particulièrement liée à la théorie des graphes : les jeux d'accessibilité (reachability games) à deux joueurs.
L'idée clé : la situation du jeu à un instant donné est une position (un sommet d'un graphe), et un coup consiste à se déplacer le long d'un arc vers une nouvelle position. Le déroulement d'une partie est donc un chemin dans un graphe orienté. Résoudre le jeu revient à étudier ce graphe.
On considère deux joueurs : le Joueur 0 (appelons-le Atteignant) et le Joueur 1 (Adversaire). Le Joueur 0 cherche à atteindre un ensemble de positions cibles ; le Joueur 1 cherche à l'en empêcher (éviter indéfiniment ces positions).
Les ingrédients du jeu :
| Notion (jeu) | Définition |
|---|---|
| Position (état) | Un sommet du graphe. C'est l'état courant du jeu. |
| État initial | La position v0 où commence la partie. |
| États gagnants (cible) | L'ensemble F des positions à atteindre. Si la partie y arrive, le Joueur 0 gagne. |
| Coup | Un arc (u → v). Le joueur qui contrôle u choisit le successeur v. |
| Partie | Une suite v0, v1, v2, … de positions reliées par des arcs (un chemin). |
| Stratégie | Une règle indiquant, pour chaque position contrôlée par un joueur, le coup à jouer. |
Comme les deux joueurs jouent à tour de rôle, on partitionne les positions selon le joueur qui doit jouer. On obtient une arène (graphe du jeu) :
G = (V0 ∪ V1, E) avec V0 ∩ V1 = ∅
Lorsque les joueurs alternent strictement (chaque arc va de V0 vers V1 ou de V1 vers V0), le graphe est biparti : aucun arc ne relie deux positions du même joueur. C'est le cadre étudié dans ce chapitre.
Une position est gagnante pour le Joueur 0 si, en partant de cette position, le Joueur 0 possède une manière de jouer qui force l'arrivée dans F, quoi que fasse le Joueur 1. L'ensemble de ces positions est l'attracteur de F pour le Joueur 0, noté Attr0(F).
On construit l'attracteur de proche en proche. On note Attrk l'ensemble des positions à partir desquelles le Joueur 0 peut forcer l'arrivée dans F en au plus k coups.
Initialisation : Attr0 = F (on y est déjà).
Récurrence : on ajoute une position u à la couche suivante si :
| u appartient à… | Condition pour ajouter u |
|---|---|
| V0 (le Joueur 0 choisit) | il existe un successeur déjà dans Attrk. (∃) |
| V1 (le Joueur 1 choisit) | tous les successeurs sont déjà dans Attrk. (∀) |
Formellement :
Attrk+1 = Attrk ∪ { u ∈ V0 : ∃ (u→v) ∈ E, v ∈ Attrk } ∪ { u ∈ V1 : ∀ (u→v) ∈ E, v ∈ Attrk }
La suite (Attrk) est croissante et bornée par V : elle se stabilise. La limite est l'attracteur :
Attr0(F) = ⋃k ≥ 0 Attrk
On parcourt le graphe « à l'envers » depuis F. On maintient pour chaque position du Joueur 1 un compteur de successeurs restant à valider.
def attracteur(V0, V1, succ, pred, F):
"""V0, V1 : ensembles de positions ; succ[u], pred[u] : listes ;
F : ensemble cible. Renvoie l'attracteur du Joueur 0."""
attr = set(F) # Attr^0 = F
file = list(F) # positions à traiter
# nb de successeurs encore hors de attr, pour les positions du Joueur 1
reste = {u: len(succ[u]) for u in V1}
while file:
v = file.pop() # v est déjà gagnant
for u in pred[v]: # on remonte vers les prédécesseurs
if u in attr:
continue
if u in V0: # Joueur 0 : UN successeur gagnant suffit
attr.add(u); file.append(u)
else: # Joueur 1 : il faut TOUS les successeurs
reste[u] -= 1
if reste[u] == 0:
attr.add(u); file.append(u)
return attr
Les positions hors de Attr0(F) sont exactement les positions gagnantes pour le Joueur 1 : à partir de là, le Joueur 1 peut éviter F pour toujours. C'est la détermination du jeu : chaque position est gagnante pour exactement l'un des deux joueurs.
Calculer Attr0(F) ne dit pas seulement qui gagne : il fournit aussi comment gagner. On lit la stratégie directement sur les couches de l'attracteur.
Notons rang(u) le plus petit k tel que u ∈ Attrk (le nombre minimal de coups pour forcer F). Sur chaque position u ∈ V0 ∩ Attr0(F), la stratégie consiste à jouer vers un successeur de rang strictement inférieur :
σ0(u) = un successeur v tel que rang(v) = rang(u) − 1
À chaque coup du Joueur 0, le rang diminue d'au moins 1 ; le Joueur 1, depuis une position de l'attracteur, ne peut que rester dans l'attracteur (tous ses successeurs y sont). Le rang, entier positif, décroît : la partie atteint F en au plus rang(v0) coups. La stratégie est sans mémoire (elle ne dépend que de la position courante).
Sur chaque position u ∈ V1 hors de l'attracteur, le Joueur 1 dispose forcément d'au moins un successeur lui aussi hors de l'attracteur (sinon u y serait entré par la règle du ∀). Sa stratégie : toujours jouer vers une position hors de Attr0(F). La partie ne touche alors jamais F.
Le résultat fondamental des jeux d'accessibilité (cas fini) est le théorème de détermination :
Pour savoir qui gagne la partie : on regarde simplement si l'état initial v0 appartient à Attr0(F).
Le calcul exact de l'attracteur suppose qu'on peut explorer tout le graphe du jeu. Pour des jeux réels (échecs, dames, Puissance 4…), ce graphe est gigantesque : on ne peut pas le construire. On utilise alors l'algorithme Minimax, qui explore l'arbre des coups sur une profondeur limitée et évalue les positions atteintes par une heuristique.
On déroule l'arbre des parties depuis la position courante. Les deux joueurs sont supposés jouer au mieux de leur intérêt, mesuré par une valeur :
Les valeurs sont calculées au fond de l'arbre :
def minimax(position, profondeur, joueurMax):
if profondeur == 0 or terminale(position):
return heuristique(position) # estimation ou score exact
if joueurMax: # le joueur MAX maximise
valeur = -infini
for coup in coups(position):
valeur = max(valeur, minimax(jouer(position, coup),
profondeur - 1, False))
return valeur
else: # le joueur MIN minimise
valeur = +infini
for coup in coups(position):
valeur = min(valeur, minimax(jouer(position, coup),
profondeur - 1, True))
return valeur
Le coup effectivement joué par MAX à la racine est celui qui réalise le max.
L'heuristique h estime la valeur d'une position sans explorer la suite du jeu. Elle privilégie certaines positions plutôt que d'autres. Exemples : aux échecs, la différence de matériel (somme des pièces pondérées) ; au morpion, le nombre d'alignements encore possibles. Une bonne heuristique permet de jouer correctement avec une profondeur faible, donc peu de calcul.
L'élagage alpha-bêta est une amélioration de Minimax qui coupe les branches dont on sait déjà qu'elles ne changeront pas la décision. Il renvoie la même valeur que Minimax mais explore beaucoup moins de positions, ce qui permet d'augmenter la profondeur à temps de calcul égal.
Informatique CPGE MP/PSI · Introduction à la théorie des jeux