Jumeau C# / .NET du notebook Python Search-06-AdversarialSearch. Partie de l’EPIC #4956 — parité .NET ⇄ Python des séries de notebooks. Cette partie couvre la recherche sur arbre de jeu : Minimax, élagage Alpha-Beta, heuristiques d’évaluation, recherche à profondeur limitée, iterative deepening et tables de transposition. Le fil rouge est le Tic-Tac-Toe (3×3), banc d’essai historique de la théorie des jeux (von Neumann 1928, Nash 1950) où l’arbre de jeu est assez petit pour être exploré complètement.
Contrairement à la recherche sur graphe (Partie 1, A*/IDA*) où un seul agent cherche un chemin, la recherche adversariale modélise deux agents en conflit : MAX cherche à maximiser l’utilité, MIN à la minimiser. L’optimalité de Minimax repose sur l’hypothèse d’adversaire parfait — chaque joueur joue le meilleur coup disponible — ce qui produit la valeur du jeu (von Neumann) : pour le Tic-Tac-Toe, cette valeur est 0 (nulle avec jeu optimal des deux côtés).
Fidélité du port (#3801 — SOTA-OK) : les algorithmes sont des implémentations fidèles de Minimax (von Neumann) et Alpha-Beta (Knuth & Moore 1975), pas des workarounds dégradés. Les exercices (Connect Four, ordonnancement, negamax, tournoi) sont laissés en stub // TODO etudiant conformément à la règle C.1 — le notebook s’exécute de bout en bout même non complété.
Interface : jeu à somme nulle à information parfaite
On abstrait la structure d’un jeu à somme nulle par une interface générique paramétrée par le type d’état TEtat. Sept méthodes suffisent à exprimer n’importe quel jeu de cette classe (échecs, dames, Tic-Tac-Toe, Puissance 4…) — la logique de recherche (Minimax, Alpha-Beta) s’écrit une seule fois contre cette interface et s’applique à toutes les instances.
// Interface pour un jeu a somme nulle a information parfaite.using System;using System.Collections.Generic;using System.Linq;using System.Diagnostics;publicinterface IJeuSommeNulle<TEtat>{ TEtat EtatInitial();// etat de departstringJoueur(TEtat etat);// "MAX" ou "MIN" : a qui le tour List<int>Actions(TEtat etat);// coups legaux (indices) TEtat Resultat(TEtat etat,int action);// nouvel etat apres le coupboolEstTerminal(TEtat etat);// partie finie (gain ou nul)doubleUtilite(TEtat etat,string joueur);// +1 victoire, -1 defaite, 0 nulstringAfficher(TEtat etat);// representation textuelle}Console.WriteLine("Interface IJeuSommeNulle<TEtat> definie (7 methodes, jeux a somme nulle).");
Interface IJeuSommeNulle<TEtat> definie (7 methodes, jeux a somme nulle).
Instance : Tic-Tac-Toe
Le Tic-Tac-Toe (morpion 3×3) est le banc d’essai canonique : son arbre de jeu compte ~5 478 états légaux, explorables en quelques millisecondes. X joue le rôle de MAX (cherche à gagner, utilité +1), O joue le rôle de MIN (cherche à faire perdre X, utilité −1 du point de vue de MAX). L’état est un couple (grille, joueur) où la grille est une chaîne de 9 caractères (’ ‘, ’X’ ou ‘O’).
Minimax (von Neumann, 1928) calcule la valeur du jeu en explorant tout l’arbre jusqu’aux états terminaux. À chaque nœud :
si c’est le tour de MAX, on choisit l’action qui maximise la valeur ;
si c’est le tour de MIN, on choisit l’action qui minimise la valeur.
L’hypothèse est celle de l’adversaire parfait : MIN jouera le coup qui nous fait le plus de mal. Pour le Tic-Tac-Toe, Minimax confirme la valeur 0 (partie nulle avec jeu optimal des deux côtés) — aucun des deux joueurs ne peut forcer la victoire.
// Minimax recursif : retourne (valeur du jeu, meilleure action) du point de vue de joueurMax.publicstatic(double Valeur,int? Action) Minimax<TEtat>( IJeuSommeNulle<TEtat> jeu, TEtat etat,string joueurMax ="MAX"){if(jeu.EstTerminal(etat))return(jeu.Utilite(etat, joueurMax),null);var actions = jeu.Actions(etat);if(jeu.Joueur(etat)== joueurMax){double meilleure =double.NegativeInfinity;int? meilleureAction =null;foreach(var a in actions){var(v, _)=Minimax(jeu, jeu.Resultat(etat, a), joueurMax);if(v > meilleure){ meilleure = v; meilleureAction = a;}}return(meilleure, meilleureAction);}else{double meilleure =double.PositiveInfinity;int? meilleureAction =null;foreach(var a in actions){var(v, _)=Minimax(jeu, jeu.Resultat(etat, a), joueurMax);if(v < meilleure){ meilleure = v; meilleureAction = a;}}return(meilleure, meilleureAction);}}var jeu2 =newTicTacToe();var(valeur, action)=Minimax(jeu2, jeu2.EtatInitial());Console.WriteLine($"Valeur Minimax depuis l'etat initial : {valeur}");Console.WriteLine($"Meilleure action (case 0-8) : {action}");Console.WriteLine("-> Valeur 0 = partie nulle garantie avec jeu optimal des deux cotes.");
Valeur Minimax depuis l'etat initial : 0
Meilleure action (case 0-8) : 0
-> Valeur 0 = partie nulle garantie avec jeu optimal des deux cotes.
Lire le verdict Minimax : la valeur 0 n’est pas un échec, c’est un théorème
La sortie affiche Valeur Minimax depuis l'etat initial : 0, Meilleure action (case 0-8) : 0, puis le commentaire imprimé -> Valeur 0 = partie nulle garantie avec jeu optimal des deux cotes. La lecture importante est celle du 0 : ce n’est pas « l’algorithme n’a rien trouvé », c’est la valeur du jeu — au Tic-Tac-Toe, deux joueurs parfaits font nulle, et le minimax qui explore l’arbre complet le constate numériquement. La « meilleure action 0 » (le coin) ne gagne rien de plus qu’une autre case : toutes les actions mènent à la valeur 0, l’algorithme rend la première. Retenir le geste : la recherche adversariale ne « résout » pas le jeu en trouvant la victoire — elle mesure ce que chaque camp peut garantir.
Élagage Alpha-Beta
Minimax explore tout l’arbre. L’élagage Alpha-Beta (Knuth & Moore, 1975) coupe des branches dont on sait qu’elles ne peuvent pas influencer la décision : si MAX a déjà trouvé un coup valant alpha, toute branche de MIN garantissant ≤ alpha est inutile à explorer (MIN ne permettra jamais mieux). On maintient deux bornes — alpha (meilleur pour MAX), beta (meilleur pour MIN) — et on élague dès que beta ≤ alpha.
La valeur retournée est identique à Minimax (Alpha-Beta est exact) ; seul le nombre de nœuds explorés diminue. Sur Tic-Tac-Toe, le speedup est mesurable mais modeste (l’arbre est petit) ; sur des jeux plus profonds (échecs, Puissance 4), il devient décisif.
// Alpha-Beta : meme resultat que Minimax, moins de nœuds explores.publicstatic(double Valeur,int? Action) AlphaBeta<TEtat>( IJeuSommeNulle<TEtat> jeu, TEtat etat,double alpha,double beta,string joueurMax ="MAX"){if(jeu.EstTerminal(etat))return(jeu.Utilite(etat, joueurMax),null);var actions = jeu.Actions(etat);if(jeu.Joueur(etat)== joueurMax){double meilleure =double.NegativeInfinity;int? meilleureAction =null;foreach(var a in actions){var(v, _)=AlphaBeta(jeu, jeu.Resultat(etat, a), alpha, beta, joueurMax);if(v > meilleure){ meilleure = v; meilleureAction = a;} alpha = Math.Max(alpha, meilleure);if(beta <= alpha)break;// elagage : MIN ne permettra jamais mieux}return(meilleure, meilleureAction);}else{double meilleure =double.PositiveInfinity;int? meilleureAction =null;foreach(var a in actions){var(v, _)=AlphaBeta(jeu, jeu.Resultat(etat, a), alpha, beta, joueurMax);if(v < meilleure){ meilleure = v; meilleureAction = a;} beta = Math.Min(beta, meilleure);if(beta <= alpha)break;// elagage : MAX ne permettra jamais pire}return(meilleure, meilleureAction);}}// Benchmark : Minimax vs Alpha-Beta sur l'etat initial.var jeu3 =newTicTacToe();var init3 = jeu3.EtatInitial();var sw = Stopwatch.StartNew();var(v1, a1)=Minimax(jeu3, init3);sw.Stop();double t1 = sw.Elapsed.TotalSeconds;sw.Restart();var(v2, a2)=AlphaBeta(jeu3, init3,double.NegativeInfinity,double.PositiveInfinity);sw.Stop();double t2 = sw.Elapsed.TotalSeconds;Console.WriteLine($"Minimax : valeur={v1}, temps={t1:F4}s");Console.WriteLine($"Alpha-Beta : valeur={v2}, temps={t2:F4}s");Console.WriteLine($"Speedup : {(t2 > 0 ? t1 / t2 : double.PositiveInfinity):F1}x (meme valeur -> decision identique)");
La sortie aligne les deux moteurs : Minimax : valeur=0, temps=0,2095s puis Alpha-Beta : valeur=0, temps=0,0061s, et conclut Speedup : 34,1x (meme valeur -> decision identique). Deux lectures. Le speedup fait la leçon, pas les temps absolus : 0,2095 s et 0,0061 s dépendent de la machine (convention du registre : seuls les rapports et l’ordre comptent) — mais le facteur ~34 mesure ce que l’élagage épargne : l’alpha-beta visite une fraction de l’arbre sans jamais changer la racine. La parenthèse est la garantie de correction : « meme valeur -> decision identique » — un élagage plus rapide qui donnait une autre décision serait un bug, pas une optimisation ; la sortie atteste que la valeur racine (0) survit à l’élagage.
Heuristique, profondeur limitée et iterative deepening
Sur des jeux trop profonds pour explorer entièrement (échecs, Go), on borne la profondeur et on évalue les états non terminaux par une fonction d’évaluation heuristique. Pour le Tic-Tac-Toe, une heuristique naturelle compte les lignes « ouvertes » (sans pion adverse) et pondère par le nombre de pions alignés. L’iterative deepening enchaîne les profondeurs 1, 2, 3… jusqu’à une limite de temps, en conservant la meilleure action trouvée — il permet d’interrompre la recherche à tout moment avec un coup raisonnable.
// Fonction d'evaluation heuristique pour le Tic-Tac-Toe.publicstaticdoubleEvaluationHeuristique(TttEtat e,string joueur){char moi = joueur =="MAX"?'X':'O';char adv = joueur =="MAX"?'O':'X';int[][] lignes ={new[]{0,1,2},new[]{3,4,5},new[]{6,7,8},new[]{0,3,6},new[]{1,4,7},new[]{2,5,8},new[]{0,4,8},new[]{2,4,6}};double score =0;foreach(var l in lignes){int ma = l.Count(i => e.Grille[i]== moi);int adverse = l.Count(i => e.Grille[i]== adv);if(adverse ==0) score += ma * ma;// ligne ouverte pour moiif(ma ==0) score -= adverse * adverse;// ligne ouverte pour l'adversaire}return score /9.0;}// Alpha-Beta a profondeur limitee : s'arrete a profondeur 0 sur l'heuristique.publicstatic(double Valeur,int? Action) AlphaBetaLimite<TEtat>( IJeuSommeNulle<TEtat> jeu, TEtat etat,int profondeur,double alpha,double beta,string joueurMax ="MAX"){if(jeu.EstTerminal(etat))return(jeu.Utilite(etat, joueurMax),null);if(profondeur ==0)return(EvaluationHeuristique((TttEtat)(object)etat!, joueurMax),null);var actions = jeu.Actions(etat);if(jeu.Joueur(etat)== joueurMax){double bestV =double.NegativeInfinity;int? bestA =null;foreach(var a in actions){var(v, _)=AlphaBetaLimite(jeu, jeu.Resultat(etat, a), profondeur -1, alpha, beta, joueurMax);if(v > bestV){ bestV = v; bestA = a;} alpha = Math.Max(alpha, bestV);if(beta <= alpha)break;}return(bestV, bestA);}else{double bestV =double.PositiveInfinity;int? bestA =null;foreach(var a in actions){var(v, _)=AlphaBetaLimite(jeu, jeu.Resultat(etat, a), profondeur -1, alpha, beta, joueurMax);if(v < bestV){ bestV = v; bestA = a;} beta = Math.Min(beta, bestV);if(beta <= alpha)break;}return(bestV, bestA);}}// Iterative deepening : profondeurs 1, 2, 3... jusqu'a temps_max ou profondeur max// (sur Tic-Tac-Toe, l'arbre complet fait 9 plis : inutile de chercher plus profond).publicstatic(double Valeur,int? Action,int Profondeur)IterativeDeepening( TicTacToe jeu, TttEtat etat,double tempsMax =0.5,int profondeurMax =9){var sw = Stopwatch.StartNew();double bestV =0;int? bestA =null;int depth;for(depth =1; depth <= profondeurMax && sw.Elapsed.TotalSeconds< tempsMax; depth++){var(v, a)=AlphaBetaLimite(jeu, etat, depth,double.NegativeInfinity,double.PositiveInfinity); bestV = v; bestA = a;if(Math.Abs(v)>=1)break;// victoire certaine, inutile d'aller plus profond}int atteinte = Math.Min(depth, profondeurMax);return(bestV, bestA, atteinte);}var jeu4 =newTicTacToe();var(vi, ai, di)=IterativeDeepening(jeu4, jeu4.EtatInitial(), tempsMax:0.3, profondeurMax:9);Console.WriteLine($"Iterative Deepening : valeur={vi:F2}, action={ai}, profondeur atteinte={di}");Console.WriteLine("-> Profondeur 9 = arbre complet du Tic-Tac-Toe (9 cases) ; valeur 0 = nul avec jeu optimal.");
Iterative Deepening : valeur=0,00, action=0, profondeur atteinte=9
-> Profondeur 9 = arbre complet du Tic-Tac-Toe (9 cases) ; valeur 0 = nul avec jeu optimal.
Lire l’iterative deepening : la même valeur obtenue par progression
La sortie imprime Iterative Deepening : valeur=0,00, action=0, profondeur atteinte=9 puis -> Profondeur 9 = arbre complet du Tic-Tac-Toe (9 cases) ; valeur 0 = nul avec jeu optimal. Deux détails à lire. La profondeur atteinte 9 : la progression par profondeur croissante a fini par couvrir l’arbre entier — au Tic-Tac-Toe l’horizon est fini, l’iterative deepening converge donc vers le minimax exact. La valeur 0,00 : c’est la valeur heuristique à profondeur limitée qui, à chaque palier, se rapproche de la valeur exacte — ici elle vaut exactement 0 dès que la profondeur suffit. Le geste algorithmique : dans un jeu trop profond pour l’arbre complet, la même boucle rendrait la meilleure valeur atteignable dans le budget, avec la garantie de toujours avoir une réponse.
Table de transposition
Une table de transposition (Zobrist, 1970 ; clé de hachage de l’état) met en cache les résultats déjà calculés : si l’on retombe sur un état déjà vu (différents ordres de coups mènent au même état), on renvoie la valeur cachée au lieu de recalculer. Au Tic-Tac-Toe, de nombreuses permutations de coups convergent vers le même plateau — le cache évite ce recalcul redondant. On stocke pour chaque clé (valeur, profondeur, drapeau) afin de ne réutiliser le cache que si l’entrée est au moins aussi profonde que la requête courante.
Alpha-Beta + Transposition : valeur=0, temps=0,0091s
Cache : 1565 hits, 3010 misses
-> Valeur identique a Minimax pur (le cache ne change pas la decision, seulement la vitesse).
Lire la table de transposition : le cache ne change pas la décision
La sortie affiche Alpha-Beta + Transposition : valeur=0, temps=0,0091s, Cache : 1565 hits, 3010 misses, et le commentaire -> Valeur identique a Minimax pur (le cache ne change pas la decision, seulement la vitesse). La statistique centrale est le couple 1565 hits / 3010 misses : un peu plus d’un tiers des recherches d’état tombent sur une position déjà calculée — le Tic-Tac-Toe contient des transpositions (jouer case 1 puis case 3 mène au même état que 3 puis 1), et la table capture cette redondance. Le temps (0,0091 s) est du même ordre que l’alpha-beta pur : sur ce petit jeu, le cache ne paie pas — c’est exactement la lecture honnête, la table de transposition est un investissement qui rapporte sur les arbres profonds (échecs), pas sur 9 cases. Et la ligne finale est le contrat d’invariance : valeur identique = le cache est transparent pour la décision.
Exercices
Les exercices suivants sont laissés en stub // TODO etudiant (règle C.1) : le notebook s’exécute de bout en bout même non complété. Chaque exercice est précédé de son énoncé et d’indices. Les classifier par contenu (règle de labeling) : ce sont des exercices (à compléter), à distinguer des exemples guidés résolus ci-dessus.
Exercice 1 — Connect Four (Puissance 4)
Implémentez la classe ConnectFour héritant de IJeuSommeNulle<C4Etat> : - grille 6 lignes × 7 colonnes ; - 4 pions alignés (horizontal, vertical, deux diagonales) pour gagner ; - les coups se jouent en choisissant une colonne (le pion tombe sur la case libre la plus basse).
Indice : représentez la grille par une string de 42 caractères (comme le Tic-Tac-Toe), et parcourez les 4 directions pour détecter l’alignement de 4.
// Exercice 1 : Connect Four// TODO etudiant : implementer ConnectFour : IJeuSommeNulle<C4Etat>// - EtatInitial : grille 6x7 vide, joueur 'X'// - Actions : colonnes (0-6) non pleines// - Resultat : faire tomber le pion dans la colonne// - EstTerminal : 4 alignes (4 directions) OU grille pleine// - Utilite : +1 si joueur gagne, -1 s'il perd, 0 nulpublic record C4Etat(string Grille,char Joueur);// grille 42 chars, ligne 0 en hautpublicclass ConnectFour : IJeuSommeNulle<C4Etat>{public C4Etat EtatInitial()=>null!;// TODO etudiantpublicstringJoueur(C4Etat e)=>null!;// TODO etudiantpublic List<int>Actions(C4Etat e)=>null!;// TODO etudiantpublic C4Etat Resultat(C4Etat e,int action)=>null!;// TODO etudiantpublicboolEstTerminal(C4Etat e)=>false;// TODO etudiantpublicdoubleUtilite(C4Etat e,string joueur)=>0.0;// TODO etudiantpublicstringAfficher(C4Etat e)=>"(grille Connect Four a afficher)";// TODO etudiant}Console.WriteLine("Exercice 1 a completer : Connect Four (voir indices ci-dessus).");
Exercice 1 a completer : Connect Four (voir indices ci-dessus).
Exercice 2 — Ordonnancement des coups
L’élagage Alpha-Beta est d’autant plus efficace que l’on explore d’abord les bons coups (alors l’élagage intervient tôt). Modifiez AlphaBeta pour trier les actions par proximité au centre (colonnes 3, 2, 4, 1, 5, 0, 6) avant de les parcourir.
Indice : écrivez OrdonnerActions(List<int>, int centre = 3) qui trie par distance croissante au centre, et appez-la sur jeu.Actions(etat) avant la boucle.
// Exercice 2 : Ordonnancement des coups// TODO etudiant : ordonner les actions par proximite au centre avant Alpha-Betapublicstatic List<int>OrdonnerActions(List<int> actions,int centre =3){return actions;// TODO etudiant : trier par distance croissante au centre}publicstatic(double Valeur,int? Action) AlphaBetaOrdonne<TEtat>( IJeuSommeNulle<TEtat> jeu, TEtat etat,int profondeur,double alpha,double beta,string joueurMax ="MAX"){return(0.0,null);// TODO etudiant : Alpha-Beta avec tri des coups (cf. AlphaBetaLimite)}Console.WriteLine("Exercice 2 a completer : ordonnancement des coups (voir indices ci-dessus).");
Exercice 2 a completer : ordonnancement des coups (voir indices ci-dessus).
Exercice 3 — Negamax
Dans Minimax, MAX et MIN font exactement la même chose, sauf que MIN minimise. Or minimiser pour soi = maximiser le négatif. Negamax unifie les deux cas : on maximise toujours, mais on négatie la valeur retournée par le fils (qui est du point de vue de l’adversaire).
Relation fondamentale : negamax(etat) = max(−negamax(fils)) pour chaque fils. L’utilité est calculée du point de vue du joueur courant puis multipliée par signe (+1 si tour de MAX, −1 si tour de MIN).
Indice : la borne Alpha-Beta s’inverse dans l’appel récursif : −negamax_ab(fils, −beta, −alpha, −signe).
// Exercice 3 : Negamax (unification de MAX/MIN)// TODO etudiant : implementer negamax (max de -negamax(fils) pour chaque fils)publicstatic(double Valeur,int? Action) Negamax<TEtat>( IJeuSommeNulle<TEtat> jeu, TEtat etat,int signe =1,string joueurMax ="MAX"){return(0.0,null);// TODO etudiant}publicstatic(double Valeur,int? Action) NegamaxAlphaBeta<TEtat>( IJeuSommeNulle<TEtat> jeu, TEtat etat,double alpha,double beta,int signe =1,string joueurMax ="MAX"){return(0.0,null);// TODO etudiant : negamax avec elagage (bornes inversées dans l'appel recursif)}Console.WriteLine("Exercice 3 a completer : negamax (voir indices ci-dessus).");
Exercice 3 a completer : negamax (voir indices ci-dessus).
Exercice 4 — Tournoi algorithmique
Construisez un cadre de tournoi automatique entre stratégies : joueur aléatoire, Minimax, Alpha-Beta jouent les uns contre les autres, en alternant qui commence. Collectez : victoires, nulles, temps moyen par coup.
Indice : écrivez JouerPartie(jeu, fnMax, fnMin) qui joue une partie complète en appelant fnMax / fnMin à chaque tour pour choisir l’action, et renvoie le résultat (+1/0/−1).
// Exercice 4 : Tournoi algorithmique// TODO etudiant : framework de tournoi entre strategiespublicstaticint? JoueurAleatoire<TEtat>(IJeuSommeNulle<TEtat> jeu, TEtat etat, Random rng){returnnull;// TODO etudiant : choisir un coup au hasard parmi jeu.Actions(etat)}publicstaticint? JouerPartie<TEtat>( IJeuSommeNulle<TEtat> jeu, Func<IJeuSommeNulle<TEtat>, TEtat,int?> fnMax, Func<IJeuSommeNulle<TEtat>, TEtat,int?> fnMin){returnnull;// TODO etudiant : jouer la partie, retourner +1 (gain MAX), 0 (nul), -1 (gain MIN)}Console.WriteLine("Exercice 4 a completer : tournoi algorithmique (voir indices ci-dessus).");
Exercice 4 a completer : tournoi algorithmique (voir indices ci-dessus).
Lire les quatre exercices : l’échelle des extensions
Les cellules d’exercice impriment la même invite (Exercice N a completer) — le contrat C.1 : le notebook s’exécute d’un bout à l’autre, la validation est l’étudiant. L’échelle se lit dans les énoncés : Exercice 1 transpose tout ce notebook vers Connect Four (grille 7×6, victory line de 4) — le même minimimax sur un arbre qui ne tient plus en entier ; Exercice 2 attaque l’ordonnancement des coups (move ordering) — l’ordre dans lequel alpha-beta visite les enfants détermine l’élagage, la question est de le mesurer ; Exercice 3 unifie MAX et MIN en une seule récursion negamax — la reformulation standard des échecs ; Exercice 4 organise le tournoi où ces variantes s’affrontent. Quatre niveaux : transférer, optimiser, reformuler, confronter.
Conclusion
Ce jumeau .NET a porté fidèlement les cinq pierres angulaires de la recherche adversariale :
Minimax (von Neumann, 1928) — la valeur du jeu sous hypothèse d’adversaire parfait ;
Alpha-Beta (Knuth & Moore, 1975) — même décision, moins de nœuds (élagage par bornes) ;
Heuristique + profondeur limitée — pour les jeux trop profonds à explorer entièrement ;
Iterative deepening — recherche interrompible, meilleur coup disponible à tout instant ;
Table de transposition — cache des états déjà calculés (et non du code métier).
La leçon générale, cohérente avec le reste de la série Search : l’optimalité a un coût (Minimax explore tout l’arbre), et l’art consiste à préserver la décision optimale en réduisant le travail — Alpha-Beta, l’ordonnancement des coups, les tables de transposition sont autant de leviers qui rapprochent le coût effectif du coût incompressible. Pour les jeux où même cela ne suffit pas (échecs, Go), on relâche l’optimalité via une heuristique d’évaluation — exactement le même contraste qu’entre A* (optimal, Partie 1) et les métaheuristiques (approché, Partie 4).
Ponts
Notebook Python source : Search-06-AdversarialSearch — même contenu algorithmique, noyau python3 (avec numpy/matplotlib pour la visualisation) ;