# Exemple resolu 7 : Analyse de convergence MCTS
# Mesure comment MCTS converge vers l'action Minimax optimale avec le nombre
# d'iterations.
#
# On utilise une position ou le signal est tres fort : X a deux alignements
# sur la ligne du haut (cases 0 et 1), O au centre. X doit JOUER LA CASE 2
# pour gagner immediatement ; toute autre action rate la victoire. Le signal
# est maximal car les rollouts depuis la case 2 retournent +1 systematiquement
# (victoire triviale), alors que les autres donnent un melange.
def _minimax_memo(jeu, etat, joueur_max='MAX', _cache={}):
"""Minimax avec memoisation (cle : (etat, joueur_max)). Permet d'enumerer
rapidement les actions optimales de n'importe quelle position."""
key = (etat, joueur_max)
if key in _cache:
return _cache[key]
if jeu.est_terminal(etat):
_cache[key] = (jeu.utilite(etat, joueur_max), None)
return _cache[key]
actions = jeu.actions(etat)
if jeu.joueur(etat) == joueur_max:
best = (float('-inf'), None)
for a in actions:
v, _ = _minimax_memo(jeu, jeu.resultat(etat, a), joueur_max)
if v > best[0]:
best = (v, a)
else:
best = (float('+inf'), None)
for a in actions:
v, _ = _minimax_memo(jeu, jeu.resultat(etat, a), joueur_max)
if v < best[0]:
best = (v, a)
_cache[key] = best
return best
def analyser_convergence(jeu, etat, iterations_list, repetitions=30, seed=0):
"""Pour chaque taille d'iterations, repete MCTS et collecte :
- taux_optimal_% : % de fois ou MCTS choisit une action Minimax-optimale
- valeur_moyenne : valeur MCTS moyenne (devrait converger vers Minimax)
- temps_moyen_s : temps moyen d'une recherche
Chaque repetition utilise une graine RNG distincte derivee de `seed` pour
rendre l'analyse reproductible tout en exposant la variance naturelle de MCTS.
"""
v_opt, _ = _minimax_memo(jeu, etat)
actions_optimales = set()
for a in jeu.actions(etat):
v_a, _ = _minimax_memo(jeu, jeu.resultat(etat, a))
if v_a == v_opt:
actions_optimales.add(a)
donnees = []
for n_iter in iterations_list:
succes = 0
valeurs, temps = [], []
for rep in range(repetitions):
random.seed(seed * 10_000 + rep)
mcts = MCTS(jeu)
t0 = time.time()
action, valeur = mcts.recherche(etat, iterations=n_iter)
temps.append(time.time() - t0)
valeurs.append(valeur)
if action in actions_optimales:
succes += 1
donnees.append({
'iterations': n_iter,
'taux_optimal_%': round(100.0 * succes / repetitions, 1),
'valeur_moyenne': round(float(np.mean(valeurs)), 4),
'temps_moyen_s': round(float(np.mean(temps)), 4),
})
return pd.DataFrame(donnees), v_opt, actions_optimales
def tracer_convergence(df, v_ref, actions_opt, n_actions_total,
titre="TicTacToe (X a 2-en-ligne top, doit jouer la case 2)"):
"""Trois sous-graphiques : taux d'action optimale, valeur moyenne, temps."""
fig, axes = plt.subplots(1, 3, figsize=(15, 4.2))
axes[0].plot(df['iterations'], df['taux_optimal_%'], 'o-',
color='tab:blue', linewidth=2, markersize=7)
axes[0].axhline(100.0, color='grey', linestyle='--', alpha=0.5)
taux_random = 100.0 * len(actions_opt) / n_actions_total
axes[0].axhline(taux_random, color='tab:red', linestyle=':', alpha=0.7,
label=f'Choix uniforme ({taux_random:.0f}%)')
axes[0].set_xlabel('Iterations MCTS (log)')
axes[0].set_ylabel("Taux d'action optimale (%)")
axes[0].set_xscale('log')
axes[0].set_ylim(-5, 105)
axes[0].set_title(f"Actions optimales : {sorted(actions_opt)}")
axes[0].grid(alpha=0.3, which='both')
axes[0].legend(loc='lower right')
axes[1].plot(df['iterations'], df['valeur_moyenne'], 's-',
color='tab:orange', linewidth=2, markersize=7)
axes[1].axhline(v_ref, color='tab:red', linestyle='--', alpha=0.7,
label=f'Minimax = {v_ref}')
axes[1].set_xlabel('Iterations MCTS (log)')
axes[1].set_ylabel("Valeur MCTS estimee")
axes[1].set_xscale('log')
axes[1].set_title("Valeur estimee vs reference")
axes[1].grid(alpha=0.3, which='both')
axes[1].legend()
axes[2].plot(df['iterations'], df['temps_moyen_s'], '^-',
color='tab:green', linewidth=2, markersize=7)
axes[2].set_xlabel('Iterations MCTS (log)')
axes[2].set_ylabel("Temps moyen (s, log)")
axes[2].set_xscale('log')
axes[2].set_yscale('log')
axes[2].set_title("Temps de calcul (echelle log-log)")
axes[2].grid(alpha=0.3, which='both')
plt.suptitle(f'Convergence de MCTS sur {titre}', y=1.02, fontsize=13)
plt.tight_layout()
plt.show()
# --- Execution : X a deux alignements sur la ligne du haut, doit jouer la case 2 ---
jeu = TicTacToe()
# Grille 3x3 : X | X | .
# . | O | .
# . | . | .
# X a jouer. L'action gagnante est la case 2 (complete la ligne 0).
grille = ('X', 'X', ' ',
' ', 'O', ' ',
' ', ' ', ' ')
etat_depart = (grille, 'X')
iterations_list = [10, 30, 100, 300, 1000]
df_conv, v_ref, actions_opt = analyser_convergence(
jeu, etat_depart, iterations_list, repetitions=30, seed=42)
print("Etat de depart : X a deux cases alignees en haut (0 et 1), O au centre.")
print(f"Actions possibles pour X : {sorted(jeu.actions(etat_depart))}")
print(f"Valeur Minimax optimale : {v_ref} (X peut gagner immediatement)")
print(f"Action(s) optimale(s) : {sorted(actions_opt)} <- completer la ligne 0")
print(f"Baseline aleatoire : {100.0 / len(jeu.actions(etat_depart)):.0f}%")
print()
display(df_conv)
tracer_convergence(df_conv, v_ref, actions_opt,
n_actions_total=len(jeu.actions(etat_depart)))
Constat
Un stash ancien de la machine ai-01, recycle dans le cadre de jsboige/roo-extensions#3827 (ancre
refs/stash-archive/ai01/coursia/22-2026-04-21, commit57bdd86c39, 21/04/2026), porte deux contenus pedagogiques absents demainpourSearch/Part1-Foundations/Search-07-MCTS-And-Beyond.ipynb:mainporte la variante normale (« Le joueur qui prend la derniere allumette GAGNE ») ; la misere n'y apparait pas (grep -ci misere= 0 sur le notebook). Le stash l'accompagne d'une interpretation : les positions perdantes basculent den % 4 == 0an % 4 == 1, et MCTS redecouvre la solution d'un jeu resolu sans la surpasser._minimax_memo) qui enumere les actions optimales d'une position, utilise par l'analyse de convergence de l'exemple 7. Absent demain(grep -c _minimax_memo= 0).Le reste de l'ancre est deja sur
mainsous une autre forme : le correctif de signe MCTS (#7876, convention negamax) et les exemples 6 et 7 (#8994, #9202).L'ancre n'existe que dans le depot local d'ai-01 (aucune ref
refs/stash-archive/*surorigin). Les trois cellules utiles sont donc recopiees ci-dessous, telles qu'elles sont dans l'ancre, pour qu'une lane d'une autre machine puisse trancher.Ce qui est demande
Une decision de la lane qui porte la serie Search, ecrite ici :
Grain:), en adaptant les cellules au code actuel du notebook (le moteur MCTS demaina change de convention de signe depuis le stash) ;Les deux sorties ferment cette issue. Reporter la decision en une ligne sur jsboige/roo-extensions#3827, qui attend ce verdict pour son recompte final.
Contenu de l'ancre (a lire, pas a copier tel quel)
Cellule de code n° 25 de l'ancre
Cellule de code n° 28 de l'ancre
Cellule markdown n° 29 de l'ancre
Critere de sortie
See jsboige/roo-extensions#3827