# --- §7.2 (m,n,k)-jeu : grille 5x5, aligner 4 pions (arbre minimax EXPLOSIF) ---
# Les compteurs du §7.1 étaient à profondeur illimitée (conçus pour le Tic-Tac-Toe
# complet). Ici l'arbre est inexplorable : versions à PROFONDEUR LIMITÉE (coupure à
# `prof`, heuristique neutre = utilité — le point est le COMPTE de nœuds, pas la valeur).
class JeuMNK(JeuSommeNulle):
"""(m,n,k)-jeu : grille m x n, aligner k pions. État = tuple hashable."""
def __init__(self, m=5, n=5, k=4):
self.m, self.n, self.k = m, n, k
def etat_initial(self):
return tuple([0] * (self.m * self.n))
def actions(self, etat):
return [i for i, v in enumerate(etat) if v == 0]
def resultat(self, etat, action):
joueur = 1 if etat.count(1) <= etat.count(2) else 2 # MAX (X) commence
return etat[:action] + (joueur,) + etat[action+1:]
def joueur(self, etat):
return 'MAX' if etat.count(1) <= etat.count(2) else 'MIN'
def _aligne(self, etat, pion):
m, n, k = self.m, self.n, self.k
for r in range(m):
for c in range(n):
if etat[r * n + c] != pion:
continue
for dr, dc in ((0, 1), (1, 0), (1, 1), (1, -1)):
rr, cc, longueur = r, c, 0
while 0 <= rr < m and 0 <= cc < n and etat[rr * n + cc] == pion:
longueur, rr, cc = longueur + 1, rr + dr, cc + dc
if longueur >= k:
return True
return False
def est_terminal(self, etat):
return self._aligne(etat, 1) or self._aligne(etat, 2) or 0 not in etat
def utilite(self, etat, joueur='MAX'):
gagne_max, gagne_min = self._aligne(etat, 1), self._aligne(etat, 2)
if joueur == 'MAX':
return 1 if gagne_max else (-1 if gagne_min else 0)
return 1 if gagne_min else (-1 if gagne_max else 0)
def afficher(self, etat):
sym = {0: '.', 1: 'X', 2: 'O'}
return '\n'.join(' '.join(sym[etat[r * self.n + c]] for c in range(self.n))
for r in range(self.m))
_cpt_prof = {"minimax": 0, "alpha_beta": 0, "transposition": 0}
def minimax_prof(jeu, etat, prof, joueur_max='MAX'):
_cpt_prof["minimax"] += 1
if jeu.est_terminal(etat) or prof == 0:
return jeu.utilite(etat, joueur_max)
actions = jeu.actions(etat)
if jeu.joueur(etat) == joueur_max:
return max(minimax_prof(jeu, jeu.resultat(etat, a), prof - 1, joueur_max) for a in actions)
return min(minimax_prof(jeu, jeu.resultat(etat, a), prof - 1, joueur_max) for a in actions)
def alpha_beta_prof(jeu, etat, prof, alpha, beta, joueur_max='MAX'):
_cpt_prof["alpha_beta"] += 1
if jeu.est_terminal(etat) or prof == 0:
return jeu.utilite(etat, joueur_max)
actions = jeu.actions(etat)
if jeu.joueur(etat) == joueur_max:
v = float('-inf')
for a in actions:
v = max(v, alpha_beta_prof(jeu, jeu.resultat(etat, a), prof - 1, alpha, beta, joueur_max))
alpha = max(alpha, v)
if beta <= alpha:
break
return v
v = float('+inf')
for a in actions:
v = min(v, alpha_beta_prof(jeu, jeu.resultat(etat, a), prof - 1, alpha, beta, joueur_max))
beta = min(beta, v)
if beta <= alpha:
break
return v
def alpha_beta_transpo_prof(jeu, etat, prof, alpha, beta, table, joueur_max='MAX'):
_cpt_prof["transposition"] += 1
if etat in table and table[etat][1] >= prof:
return table[etat][0]
if jeu.est_terminal(etat) or prof == 0:
v = jeu.utilite(etat, joueur_max); table[etat] = (v, prof); return v
actions = jeu.actions(etat)
if jeu.joueur(etat) == joueur_max:
v = float('-inf')
for a in actions:
v = max(v, alpha_beta_transpo_prof(jeu, jeu.resultat(etat, a), prof - 1, alpha, beta, table, joueur_max))
alpha = max(alpha, v)
if beta <= alpha:
break
else:
v = float('+inf')
for a in actions:
v = min(v, alpha_beta_transpo_prof(jeu, jeu.resultat(etat, a), prof - 1, alpha, beta, table, joueur_max))
beta = min(beta, v)
if beta <= alpha:
break
table[etat] = (v, prof)
return v
import time
jeu_large = JeuMNK(5, 5, 4)
print("Croissance des nœuds visités — jeu (5x5, aligner 4) selon la profondeur :")
print(f"{'prof':>5} | {'minimax':>14} | {'alpha-beta':>12} | {'+transposition':>15} | {'gain AB':>9} | {'gain +TT':>9}")
print("-" * 86)
for prof in [3, 4]:
for cle in _cpt_prof:
_cpt_prof[cle] = 0
minimax_prof(jeu_large, jeu_large.etat_initial(), prof)
n_mm = _cpt_prof["minimax"]
alpha_beta_prof(jeu_large, jeu_large.etat_initial(), prof, float('-inf'), float('+inf'))
n_ab = _cpt_prof["alpha_beta"]
alpha_beta_transpo_prof(jeu_large, jeu_large.etat_initial(), prof, float('-inf'), float('+inf'), {})
n_tt = _cpt_prof["transposition"]
print(f"{prof:>5} | {n_mm:>14,d} | {n_ab:>12,d} | {n_tt:>15,d} | x{n_mm / n_ab:>7.1f} | x{n_mm / n_tt:>7.1f}")
# Profondeur 5 : le minimax explose (~6,7 millions, plusieurs minutes) — on ne lance QUE l'élagage.
for cle in _cpt_prof:
_cpt_prof[cle] = 0
alpha_beta_prof(jeu_large, jeu_large.etat_initial(), 5, float('-inf'), float('+inf'))
n_ab5 = _cpt_prof["alpha_beta"]
alpha_beta_transpo_prof(jeu_large, jeu_large.etat_initial(), 5, float('-inf'), float('+inf'), {})
n_tt5 = _cpt_prof["transposition"]
N_MM5_MESURE = 6_693_626 # minimax exhaustif prof=5 mesuré hors-ligne (~minutes, non relancé en direct)
print(f"{'5':>5} | {'~' + format(N_MM5_MESURE, ',d') + ' (mesuré)':>14} | {n_ab5:>12,d} | {n_tt5:>15,d} | x{N_MM5_MESURE / n_ab5:>7.1f} | x{N_MM5_MESURE / n_tt5:>7.1f}")
print()
print("-> Le minimax est multiplié par ~22 à chaque niveau (14k -> 318k -> 6,7M) ;")
print(" l'alpha-beta+transposition ne l'est que par ~3-4 (672 -> 1,5k -> 8k).")
print(" L'écart EXPLOSE : c'est ici que l'élagage passe d'utile à INDISPENSABLE.")