Le notebook Python modelise les jeux sous forme extensive (arbre de jeu) avec networkx (graphe), affiche les arbres avec matplotlib, et invoque OpenSpiel (pyspiel) pour Kuhn Poker. Ce twin deroule tout a la main (BCL .NET 9, 0 NuGet) : on construit la structure de données de l’arbre de jeu, on modelise les ensembles d’information (infosets) et les noeuds de nature (chance), et on code la conversion forme extensive -> forme normale. La visualisation de l’arbre se fait en ASCII (equivalent du plot matplotlib).
Distinct du twin GT-9-C# : GT-9 resout les jeux extensifs par induction arriere (un algorithme de resolution). Ce notebook GT-7 est sur la representation et la modelisation (comment encoder un arbre de jeu, des infosets, des noeuds de hasard, et convertir vers la forme normale). Les deux sont complementaires : modeliser (GT-7) puis resoudre (GT-9).
1. De la forme normale a la forme extensive
La forme normale (GT-2) represente un jeu par sa matrice de gains. La forme extensive represente le deroulement temporel : qui joue quand, quelles actions sont disponibles, et ce que chaque joueur sait a chaque decision (les ensembles d’information). C’est la structure naturelle pour les jeux dynamiques (chacun joue a son tour) et les jeux a information incomplete (chance, informations cachees).
Les trois types de noeuds : - Noeud de decision (player >= 1) : un joueur choisit une action parmi actions. - Noeud terminal (player == -1) : la partie finit, payoffs donne le gain de chaque joueur. - Noeud de nature / chance (player == 0) : la Nature tire une action selon chanceProbs (carte, de, etc.).
// Modele de jeu sous forme extensive (BCL .NET, 0 NuGet)using System.Globalization;publicclass GameNode{publicstring Id {get;set;}publicint Player {get;set;}// -1 = terminal, 0 = nature, >=1 = joueurpublic List<string> Actions {get;set;}=new();public Dictionary<string, GameNode> Children {get;set;}=new();publicdouble[] Payoffs {get;set;}// noeud terminal uniquementpublicstring Infoset {get;set;}// ensemble d'informationpublic Dictionary<string,double> ChanceProbs {get;set;}// noeud de naturepublicbool IsTerminal => Player ==-1;publicbool IsChance => Player ==0;}publicclass ExtensiveFormGame{publicstring Name {get;set;}publicint NumPlayers {get;set;}public GameNode Root {get;set;}public Dictionary<string, GameNode> Nodes {get;set;}=new();public Dictionary<string, List<string>> Infosets {get;set;}=new();publicExtensiveFormGame(string name,int numPlayers){ Name = name; NumPlayers = numPlayers;}public GameNode AddNode(GameNode n){ Nodes[n.Id]= n;if(n.Player>=1&& n.Infoset==null) n.Infoset= n.Id;if(n.Infoset!=null){if(!Infosets.ContainsKey(n.Infoset)) Infosets[n.Infoset]=new(); Infosets[n.Infoset].Add(n.Id);}return n;}publicvoidSetRoot(GameNode r){ Root = r;AddNode(r);}publicvoidAddChild(GameNode parent,string action, GameNode child){if(!parent.Actions.Contains(action)) parent.Actions.Add(action); parent.Children[action]= child;AddNode(child);}}// Helpers (la culture FR ne persiste pas entre cellules .NET Interactive ; -0.0 supprime)staticstringFI(double x,string fmt ="F2"){double z = Math.Abs(x)<1e-12?0.0: x;returnstring.Format(CultureInfo.InvariantCulture,"{0:"+ fmt +"}", z);}staticvoidShow(object s){display(s?.ToString()??"(null)");}display("Modele ExtensiveFormGame pret : GameNode (decision/terminal/chance + infoset) + arbre + infosets index.");
The below script needs to be able to find the current output cell; this is an easy method to get it.
Le jeu d’entree canonique : un entrant (J1) decide d’entrer (In) ou de rester dehors (Out). S’il entre, l’incumbent (J2) decide de Fight (guerre des prix) ou Accommodate (coexistence). C’est un jeu parfait information (chacun voit tout).
L’exemple est canonique en économie industrielle : il condense le dilemme de la menace crédible dans cinq nœuds. En lisant la sortie de la cellule suivante, repérer les trois ingrédients qui feront tout le restant du notebook — la chronologie (J1 décide d’abord), l’arbre (chaque branche mène à un sous-jeu), et les payoffs terminaux qui disent si la menace de l’incumbent tient. C’est sur ce même jeu que la conversion en forme normale (section 7) et l’exercice 3 (nœud de nature) reviendront.
// Jeu d'entree sur le marche (information parfaite)var entry =newExtensiveFormGame("Entry Game",2);var root =new GameNode { Id ="entrant", Player =1, Actions =new(){"In","Out"}};entry.SetRoot(root);var incumbent =new GameNode { Id ="incumbent", Player =2, Actions =new(){"Fight","Accommodate"}};entry.AddChild(root,"In", incumbent);entry.AddChild(root,"Out",new GameNode { Id ="out", Player =-1, Payoffs =new[]{0.0,2.0}});entry.AddChild(incumbent,"Fight",new GameNode { Id ="fight", Player =-1, Payoffs =new[]{-1.0,-1.0}});entry.AddChild(incumbent,"Accommodate",new GameNode { Id ="accommodate", Player =-1, Payoffs =new[]{1.0,1.0}});Show(entry.Name+" : "+ entry.Nodes.Count+" noeuds, information parfaite (1 infoset par decision).");
Entry Game : 5 noeuds, information parfaite (1 infoset par decision).
Rendu ASCII de l’arbre
Le Python utilise matplotlib + networkx pour dessiner l’arbre. Nous le rendons en ASCII : un parcours récursif avec indentation par profondeur, qui marque les noeuds de nature (N), les infosets (~), et les payoffs terminaux.
Ce rendu textuel a un avantage pédagogique sur le dessin : il est déterministe et diffable. Deux exécutions produisent exactement les mêmes caractères, ce qui permet de comparer deux modèles nœud par nœud — le test de non-régression du jumeau s’appuie sur cette propriété. La profondeur d’indentation code le niveau dans l’arbre, le marqueur ~ les infosets, et l’étoile * les feuilles : la grammaire du rendu est celle du modèle.
// Rendu ASCII de l'arbre de jeu (equivalent du plot matplotlib)staticstringRenderTree(ExtensiveFormGame g){var sb =new System.Text.StringBuilder();voidRec(GameNode n,string lastAction,int depth){string ind =newstring(' ', depth *2);string branch = depth ==0?"":(lastAction.Length>0?"["+ lastAction +"] ":"");if(n.IsTerminal) sb.AppendLine(ind + branch +"* (J1="+FI(n.Payoffs[0])+", J2="+FI(n.Payoffs[1])+")");elseif(n.IsChance) sb.AppendLine(ind + branch +"NATURE"+(n.Infoset!=null?" ~"+ n.Infoset:""));else sb.AppendLine(ind + branch +"J"+ n.Player+(n.Infoset!=null?" ~"+ n.Infoset:""));foreach(var a in n.Actions)Rec(n.Children[a], a, depth +1);} sb.AppendLine(g.Name); sb.AppendLine(newstring('-', g.Name.Length));Rec(g.Root,"",0);return sb.ToString();}Show(RenderTree(entry));
L’arbre montre la chronologie : J1 (entrant) choisit In/Out ; s’il entre, J2 (incumbent) choisit Fight/Accommodate. Les payoffs terminaux revelent la structure : Fight (-1,-1) est un equilibre non-credible (la menace de guerre n’est pas credible une fois l’entree effective), Accommodate (1,1) est l’issue rationnelle. C’est précisément ce que l’induction arriere (GT-9) formalise.
3. Stratégies dans les jeux extensifs
Une stratégie pure en forme extensive est un plan d’action complet : pour chaque ensemble d’information du joueur, quelle action jouer. Ce n’est pas “une action”, c’est un plan contingents (une action PAR infoset). Le nombre de stratégies pures d’un joueur = produit du nombre d’actions sur chacun de ses infosets.
La sortie confirme le compte : 2 stratégies pour chaque joueur ici, parce que chacun n’a qu’un seul infoset à deux actions. La règle générale est un produit : un joueur avec deux infosets de 2 et 3 actions a 2×3 = 6 plans purs. C’est le premier indice de l’explosion combinatoire — l’exercice 2 (Card Game) fait passer J1 de 1 à 2 infosets, et sa forme normale quadrille déjà ; le paradoxe de Chain-Store (exercice 1) pousse jusqu’à la séquence d’entrants où la conversion exhaustive devient déraisonnable.
// Enumeration des strategies pures d'un joueur = plans contingents (produit cartesien sur ses infosets)static List<Dictionary<string,string>>PureStrategies(ExtensiveFormGame g,int player){// Infosets de ce joueur, conservant l'ordre d'action de chaque noeudvar playerInfosets = g.Infosets.Where(kv => g.Nodes[kv.Value[0]].Player== player).Select(kv =>(id: kv.Key, actions: g.Nodes[kv.Value[0]].Actions.ToList())).ToList();var plans =new List<Dictionary<string,string>>();voidRec(int idx, Dictionary<string,string> cur){if(idx == playerInfosets.Count){ plans.Add(new Dictionary<string,string>(cur));return;}var(iid, acts)= playerInfosets[idx];foreach(var a in acts){ cur[iid]= a;Rec(idx +1, cur);}}Rec(0,new Dictionary<string,string>());return plans;}// Entry game : J1 a 1 infoset (entrant: In/Out = 2 strategies) ; J2 a 1 infoset (incumbent: Fight/Accommodate = 2 strategies)var s1 =PureStrategies(entry,1);var s2 =PureStrategies(entry,2);Show("J1 strategies ("+ s1.Count+") : "+string.Join(" ", s1.Select(p =>"{"+string.Join(",", p.Values)+"}")));Show("J2 strategies ("+ s2.Count+") : "+string.Join(" ", s2.Select(p =>"{"+string.Join(",", p.Values)+"}")));
J1 strategies (2) : {In} {Out}
J2 strategies (2) : {Fight} {Accommodate}
4. Information parfaite vs imparfaite
La différence cruciale : en information parfaite, chaque infoset est un singleton (le joueur sait exactement ou il est). En information imparfaite, un infoset regroupe plusieurs noeuds : le joueur sait qu’il est a l’un d’entre eux mais ne sait pas lequel.
Le jeu mouvement simultane (Matching Pennies en forme extensive) : J1 joue Pile/Face, puis J2 joue Pile/Face sans voir le coup de J1. Les deux noeuds de decision de J2 sont dans le même infosetj2_choice.
Le test opérationnel est mécanique : compter les nœuds de chaque infoset. Tout infoset singleton = information parfaite ; tout infoset de cardinal > 1 encode une ignorance. La sortie de la cellule suivante imprimera le compte — 2 infosets dont un de cardinal 2 — et c’est cette asymétrie de cardinal entre j1 (1) et j2_choice (2) qui distingue structurellement ce jeu d’un jeu séquentiel à information parfaite comme celui de la section 2.
// Mouvement simultane = Matching Pennies en forme extensive (information imparfaite)var sim =newExtensiveFormGame("Simultaneous Move (Matching Pennies)",2);var r =new GameNode { Id ="j1", Player =1, Actions =new(){"Pile","Face"}};sim.SetRoot(r);// J2 ne voit pas le coup de J1 -> les deux noeuds J2 partagent l'infoset "j2_choice"var j2pile =new GameNode { Id ="j2_after_pile", Player =2, Actions =new(){"Pile","Face"}, Infoset ="j2_choice"};var j2face =new GameNode { Id ="j2_after_face", Player =2, Actions =new(){"Pile","Face"}, Infoset ="j2_choice"};sim.AddChild(r,"Pile", j2pile);sim.AddChild(r,"Face", j2face);// Payoffs (Matching Pennies : J1 gagne si meme face, J2 sinon)sim.AddChild(j2pile,"Pile",new GameNode { Id ="pp", Player =-1, Payoffs =new[]{1.0,-1.0}});sim.AddChild(j2pile,"Face",new GameNode { Id ="pf", Player =-1, Payoffs =new[]{-1.0,1.0}});sim.AddChild(j2face,"Pile",new GameNode { Id ="fp", Player =-1, Payoffs =new[]{-1.0,1.0}});sim.AddChild(j2face,"Face",new GameNode { Id ="ff", Player =-1, Payoffs =new[]{1.0,-1.0}});Show(RenderTree(sim));Show("Infosets : "+ sim.Infosets.Count+" (j1 singleton, j2_choice regroupe 2 noeuds = J2 ignore le coup de J1).");Show("=> Le marqueur ~j2_choice sur 2 noeuds = c'est l'ENCODAGE de l'information imparfaite.");
Infosets : 2 (j1 singleton, j2_choice regroupe 2 noeuds = J2 ignore le coup de J1).
=> Le marqueur ~j2_choice sur 2 noeuds = c'est l'ENCODAGE de l'information imparfaite.
Lecture du Matching Pennies : l’infoset comme encodage
La sortie compte 2 infosets — j1 singleton, j2_choice de cardinal 2 — et c’est tout le jeu : la simultanéité n’est pas une propriété du calendrier mais de la structure d’information. En forme normale (GT-2), Matching Pennies se jouait « en même temps » par hypothèse ; ici la simultanéité est encodée dans l’arbre séquentiel par la fusion des deux nœuds de J2 en un seul infoset — J2 sait qu’il doit choisir, mais pas où il est dans l’arbre. Les payoffs reproduisent la matrice originale : diagonale (1,-1) et (-1,1), anti-diagonale inversée — jeu à somme nulle sans équilibre pur, exactement le Matching Pennies de la GT-2. Leçon de structure : deux représentations du même jeu, et le pont formel entre elles sera vérifié par la conversion de la section 7.
5. Jeux avec hasard : noeuds de nature
Quand le hasard est implique (tirage de carte, de), on ajoute des noeuds de nature (player == 0) avec des probabilites sur chaque branche. Le jeu de carte simple : la Nature tire High/Low (p=0.5), J1 voit la carte et Bet/Check, puis si Bet J2 Call/Fold sans voir la carte (infoset partage).
Le nœud de nature est le seul du modèle avec Player = 0 : il ne décide rien, il tire. La sortie suivante montre la conséquence structurelle — l’arbre gagne un étage sous la racine, et les payoffs se lisent désormais en espérance : traverser le nœud de nature pondère chaque branche par sa probabilité. Ce mécanisme est exactement celui qu’exploite ExpectedPayoff (section 7) lors de la conversion, et celui de l’annexe où OpenSpiel tire les cartes de Kuhn Poker.
// Jeu de carte simple avec noeud de nature + infoset (information incomplete)var card =newExtensiveFormGame("Simple Card Game",2);var nature =new GameNode { Id ="nature", Player =0, Actions =new(){"High","Low"}, ChanceProbs =new(){{"High",0.5},{"Low",0.5}}};card.SetRoot(nature);// J1 voit la carte (infosets distincts High/Low car J1 connait sa carte)var j1h =new GameNode { Id ="j1_high", Player =1, Actions =new(){"Bet","Check"}, Infoset ="j1_H"};var j1l =new GameNode { Id ="j1_low", Player =1, Actions =new(){"Bet","Check"}, Infoset ="j1_L"};card.AddChild(nature,"High", j1h);card.AddChild(nature,"Low", j1l);// J2 apres Bet ne sait pas High/Low -> MEME infoset "j2_bet"var j2hb =new GameNode { Id ="j2_h_bet", Player =2, Actions =new(){"Call","Fold"}, Infoset ="j2_bet"};var j2lb =new GameNode { Id ="j2_l_bet", Player =2, Actions =new(){"Call","Fold"}, Infoset ="j2_bet"};card.AddChild(j1h,"Bet", j2hb); card.AddChild(j1h,"Check",new GameNode { Id ="h_check", Player =-1, Payoffs =new[]{1.0,1.0}});card.AddChild(j1l,"Bet", j2lb); card.AddChild(j1l,"Check",new GameNode { Id ="l_check", Player =-1, Payoffs =new[]{-1.0,-1.0}});// Payoffs Bet+Fold=(1,-1) ; Bet+Call depend High(2,-2)/Low(-2,2)card.AddChild(j2hb,"Call",new GameNode { Id ="h_call", Player =-1, Payoffs =new[]{2.0,-2.0}});card.AddChild(j2hb,"Fold",new GameNode { Id ="h_fold", Player =-1, Payoffs =new[]{1.0,-1.0}});card.AddChild(j2lb,"Call",new GameNode { Id ="l_call", Player =-1, Payoffs =new[]{-2.0,2.0}});card.AddChild(j2lb,"Fold",new GameNode { Id ="l_fold", Player =-1, Payoffs =new[]{1.0,-1.0}});Show(RenderTree(card));Show("Infosets : "+string.Join(", ", card.Infosets.Keys)+" | j2_bet regroupe J2_h_bet + J2_l_bet (J2 ignore la carte).");
Lecture du Card Game : information incomplète vs imparfaite
La sortie fait coexister les trois natures de nœuds du modèle — NATURE (tirage High/Low à p=0,5 chacune), nœuds de décision, feuilles * — et le résumé des infosets porte la distinction conceptuelle de la section : J1 a deux infosets séparés (j1_H, j1_L) parce qu’il voit sa carte, J2 en a un seul (j2_bet) qui regroupe ses deux nœuds parce qu’il ne la voit pas. C’est l’information incomplète au sens de Harsanyi : l’asymétrie de savoir sur l’état tiré par la Nature. Les payoffs le reflètent — si J1 a High et mise, Call perd 2 pour J2 ; s’il a Low, Call gagne 2 — de sorte que la décision de J2 doit se faire en espérance sur la carte. Ce jeu est la brique exacte du Kuhn Poker de l’annexe, qui généralise à trois cartes.
Le notebook Python invoque OpenSpiel (pyspiel) pour charger Kuhn Poker, un jeu classique a information incomplete. OpenSpiel n’a pas d’equivalent .NET (pas de binding .NET officiel) — c’est un verdict INTRINSIC pour cette section spécifique (#3801) : on ne peut pas re-invoquer OpenSpiel en C#.
Ce twin documente donc la structure de Kuhn Poker (3 cartes, infosets par carte + historique d’encheres) et renvoie au twin Python pour l’exécution OpenSpiel. Le coeur du notebook (modelisation + conversion, sections 1-5+7) est from-scratch SOTA-OK : ce que networkx/OpenSpiel cachent (l’arbre, les infosets, le hasard), ce twin le rend explicite.
Kuhn Poker (structure) : 2 joueurs, 3 cartes (J,Q,K) distribuees par la Nature, chaque joueur voit sa carte (infoset par carte), mises Pass/Bet. Information incomplete (la carte de l’adversaire est cachee). C’est le banc d’essai canonique du CFR (GT-13) et des solveurs de poker modernes.
7. Conversion forme extensive -> forme normale
Tout jeu extensif peut etre converti en jeu normal (matrice de gains) : on enumere les stratégies pures de chaque joueur (plans contingents, section 3), puis pour chaque couple (s1, s2) on calcule le payoff espere en traversant l’arbre (les noeuds de nature etant ponderes par leurs probabilites). La matrice résultat est exactement le jeu normal equivalent.
L’équivalence a un prix : la taille. La matrice croît comme le produit des stratégies pures des deux joueurs — quadratique sur les petits jeux de ce notebook, ingérable sur un jeu comme le Chain-Store multi-entrants. C’est ce qui motive les deux représentations : la forme extensive pour calculer (l’induction arrière ne visite chaque nœud qu’une fois), la forme normale pour raisonner (outils de la GT-2 : dominances, équilibres mixtes). La cellule suivante exécute la conversion sur les deux premiers jeux du notebook.
// Conversion extensive -> normale : matrice de gains sur les strategies puresstaticdouble[]ExpectedPayoff(ExtensiveFormGame g, Dictionary<int, Dictionary<string,string>> stratByPlayer){// Traversee recursive : a chaque noeud, choisir l'action du joueur (via son infoset) ou ponderer la naturedouble[]Rec(GameNode n){if(n.IsTerminal)return n.Payoffs;if(n.IsChance){double[] acc =newdouble[g.NumPlayers];foreach(var(a, p)in n.ChanceProbs){var sub =Rec(n.Children[a]);for(int k =0; k < g.NumPlayers; k++) acc[k]+= p * sub[k];}return acc;}// Noeud de decision : l'action = strategie du joueur pour l'infoset de ce noeudstring action = stratByPlayer[n.Player][n.Infoset];returnRec(n.Children[action]);}returnRec(g.Root);}staticstringConvertToNormal(ExtensiveFormGame g){var s1 =PureStrategies(g,1);var s2 =PureStrategies(g,2);var sb =new System.Text.StringBuilder(); sb.AppendLine("Forme normale equivalente : "+ g.Name); sb.AppendLine(newstring('-',50)); sb.AppendLine("J1 \\ J2 "+string.Join(" ", s2.Select(p =>"{"+string.Join(",", p.Values)+"}")));foreach(var a in s1){var strat =new Dictionary<int, Dictionary<string,string>>{{1, a }};var cells =new List<string>();foreach(var b in s2){ strat[2]= b;var pay =ExpectedPayoff(g, strat); cells.Add("("+FI(pay[0])+","+FI(pay[1])+")");} sb.AppendLine("{"+string.Join(",", a.Values)+"} "+string.Join(" ", cells));}return sb.ToString();}Show("=== Entry Game -> forme normale ===");Show(ConvertToNormal(entry));Show("=== Simultaneous Move -> forme normale (= Matching Pennies) ===");Show(ConvertToNormal(sim));
=== Entry Game -> forme normale ===
Forme normale equivalente : Entry Game
--------------------------------------------------
J1 \ J2 {Fight} {Accommodate}
{In} (-1.00,-1.00) (1.00,1.00)
{Out} (0.00,2.00) (0.00,2.00)
=== Simultaneous Move -> forme normale (= Matching Pennies) ===
La conversion de l’Entry Game donne une matrice 2x2 dont l’equilibre est (In, Accommodate) — la même conclusion que l’induction arriere (GT-9), mais obtenue par la théorie normale. La conversion du Simultaneous Move redonne exactement la matrice de Matching Pennies (pas de point-selle, stratégies mixtes uniformes). C’est la preuve que les deux formes sont equivalentes en information complete : tout jeu extensif se reduit a une matrice de gains.
8. Resume
Concept
Python (twin)
Twin C# (ici)
Arbre de jeu
networkx (graphe)
structure GameNode récursive from-scratch
Visualisation
matplotlib
rendu ASCII (parcours + indentation)
OpenSpiel / Kuhn Poker
pyspiel (exécution)
section conceptuelle (INTRINSIC, pas de binding .NET)
Conversion -> normale
itertools.product
enumeration récursive des plans contingents
Ce que la lib cache et que ce twin rend explicite : la structure récursive du noeud (decision/terminal/chance + infoset), le codage de l’information imparfaite par infoset partage (le coeur conceptuel), le ponderation des noeuds de nature dans le calcul de payoff espere, et le produit cartesien des plans contingents qui définit une stratégie pure en forme extensive.
Lien avec GT-9 : ce notebook modelise la forme extensive (representation). GT-9-C# la resout par induction arriere. Les deux se completent : modeliser puis resoudre.
Lien avec la formalisation Lean : les jeux extensifs et les equilibres (SPE) sont formalises dans game_theory_lean. Ce notebook en est le versant computationnel.
9. Exercices
Convention : stubs a completer (null + // TODO etudiant), jamais d’erreur volontaire — le notebook s’execute de bout en bout même non complete.
Les trois exercices sont ordonnés par difficulté croissante sur le même socle : l’exercice 1 (Chain-Store) entraîne la construction d’arbre avec observation séquentielle, l’exercice 2 (conversion du Card Game) réutilise le mécanisme de la section 7 sur un jeu à information incomplète, l’exercice 3 (nœud de nature) modifie le jeu le plus simple pour y injecter du hasard caché. Les squelettes sont auto-exécutables — incomplets, ils affichent la consigne sans interrompre la chaîne.
Exercice 1 : Chain-Store a 2 entrants (paradoxe de Selten)
Contexte : Le paradoxe du Chain-Store (Selten, 1978) met en scene un incumbent (monopoleur etabli) face a une sequence d’entrants potentiels. A chaque tour, l’entrant choisit In/Out, puis l’incumbent choisit Fight/Accommodate. Le 2e entrant observe l’issue du 1er : c’est le coeur du paradoxe, car la reputation de l’incumbent se construit sur l’histoire du jeu.
Objectif : Construisez cet arbre a 2 entrants avec AddChild et les infosets, puis affichez-le avec RenderTree. La question cle : le 2e entrant a-t-il un infoset distinct selon que l’incumbent a Fight ou Accommodate le 1er entrant ?
Indices : - Reutilisez le modele GameNode (decision/terminal) et ExtensiveFormGame.AddChild de la section 2. - Le 2e entrant a deux noeuds de decision (un apres Fight, un apres Accommodate) : sont-ils dans le meme infoset (information imparfaite) ou dans des infosets separes (information parfaite : il observe l’issue) ? - Payoffs : Fight est couteux pour l’incumbent mais vise a dissuader ; Accommodate partage le marche.
// Exercice 1 : Construire un arbre de jeu (Chain-Store paradox simplifie)// Etape 1 : 1 entrant, incumbent Fight/Accommodate. Etape 2 : ajouter un 2e entrant qui observe l'issue du 1er.// Indice : reutiliser AddChild + infosets. Le 2e entrant a-t-il un infoset distinct apres Fight vs Accommodate ?var ex1 =(ExtensiveFormGame)null;// votre Chain-Store a 2 entrantsShow("Exercice 1 a completer : construisez un Chain-Store a 2 entrants, affichez RenderTree.");
Exercice 1 a completer : construisez un Chain-Store a 2 entrants, affichez RenderTree.
Exercice 2 : Convertir le Card Game en forme normale
Contexte : Le jeu de cartes de la section 5 (noeud de nature + infoset d’information incomplete) est donne en forme extensive. Le convertir en forme normale revele la matrice de gains sur les strategies pures (plans contingents), ce qui permet ensuite de chercher un equilibre de Nash mixte.
Objectif : Appelez ConvertToNormal(card) et observez (a) la matrice de gains et (b) le nombre de strategies pures de chaque joueur. Verifiez que la matrice reflete l’information incomplete : J1 a deux infosets (selon sa carte), J2 un seul (il ne voit pas la carte).
Indices : - J1 a deux infosets (j1_H carte haute, j1_L carte basse) ; son plan contingent est le produit cartesien des actions par infoset -> combien de strategies pures ? - J2 a un seul infoset (j2_bet) -> combien de strategies pures ? - La conversion extensive -> normale est deja implementee (section 7) ; cet exercice est une lecture du resultat, pas une reimplementation.
// Exercice 2 : Convertir le Card Game (section 5) en forme normale// Indice : appelez ConvertToNormal(card). Combien de strategies pures pour J1 (infosets j1_H, j1_L) ?// Pour J2 (infoset unique j2_bet) ? Verifiez que la matrice reflete l'information incomplete.var ex2 =(string)null;// = ConvertToNormal(card)Show("Exercice 2 a completer : ConvertToNormal(card) -> observer la matrice et le compte de strategies.");
Exercice 2 a completer : ConvertToNormal(card) -> observer la matrice et le compte de strategies.
Exercice 3 : Ajouter un noeud de nature (cout d’entree aleatoire)
Contexte : Le jeu d’entree de la section 2 est en information parfaite et deterministe. On complique le modele : au moment d’entrer, l’entrant tire un cout d’entreeHigh (p=0.3) ou Low (p=0.7) qui modifie ses payoffs s’il entre. L’incumbent ne connait pas le cout tire.
Objectif : Ajoutez ce noeud de nature au jeu d’entree, puis discutez l’infoset de l’incumbent : comment doit-il raisonner quand il ignore le cout tire ?
Indices : - Un noeud de nature (chance node) se modelise avec un GameNode de type chance + des probabilites sur les enfants (cf. section 5, Card Game). - Le payoff espere au noeud In devient la moyenne ponderee : 0.3 * payoff(High) + 0.7 * payoff(Low). - L’incumbent, ne connaissant pas le cout, joue dans un infoset regroupant les deux issues du noeud de nature : c’est le passage de l’information parfaite a l’information imparfaite. - Lien avec GT-9 : cet exercice prepare la resolution par induction arriere avec hasard (noeuds de nature ponderes).
// Exercice 3 : Ajouter un noeud de nature au jeu d'entree (entrant a un cout aleatoire)// Indice : au noeud 'entrant', le cout d'entree est High(p=0.3) ou Low(p=0.7), modifiant les payoffs 'In'.// Question : comment l'incumbent doit-il raisonner sans connaitre le cout (nouvel infoset) ?var ex3 =(ExtensiveFormGame)null;// entry game avec cout aleatoireShow("Exercice 3 a completer : ajoutez un noeud de nature cout, discutez l'infoset de l'incumbent.");
Exercice 3 a completer : ajoutez un noeud de nature cout, discutez l'infoset de l'incumbent.
Conclusion
Ce twin a deroule les cinq couches de la forme extensive : 1. Modèle : GameNode (decision/terminal/chance + infoset) et ExtensiveFormGame. 2. Arbre : construction récursive + rendu ASCII (equivalent matplotlib). 3. Stratégies : enumeration des plans contingents (produit cartesien sur les infosets). 4. Information imparfaite : encodage par infoset partage (Matching Pennies, Card Game). 5. Conversion -> normale : traversee récursive avec ponderation des noeuds de nature.
La ou networkx/OpenSpiel fournissent des structures opaques, ce notebook montre le noeud récursif, le codage de l’information imparfaite, le hasard equilibre et le produit cartesien des plans. C’est toute la mecanique de modelisation, rendue explicite — l’objectif du marathon #4956.
See #4956 (marathon parite .NET/Python). Refs #3801 (axe-2 SOTA).
Genere par myia-po-2023, cycle 57, marathon #4956.
Annexe — Pont .NET vers OpenSpiel (moteur de référence, #10459)
La section 6 présentait OpenSpiel de manière conceptuelle (« OpenSpiel fournit de nombreux jeux déjà implémentés ») sans appeler le moteur. L’audit #10382 classait open_spiel/pyspiel en bucket 3 (cœur C++, aucun binding .NET) avec un verdict INTRINSIC implicite. C’était le même angle mort taxonomique que celui corrigé pour GT-13/GT-17/Search-7 (#10459) : l’axe PythonNet (.NET → Python.Runtime → CPython → lib Python appelée par import) n’avait pas été envisagé. Or pyspiel est précisément une lib Python invoquée par import — le cas que PythonNet résout.
Cette annexe branche donc le moteur de référence OpenSpiel (Kuhn 1950, jeux extensifs pré-implémentés) depuis .NET : chargement de kuhn_poker, simulation d’une partie, exploration de l’arbre avec infosets, et — en bonus par rapport au jumeau Python — l’équilibre calculé par Counterfactual Regret Minimization (CFR, 30 itérations), dont la valeur converge vers la valeur théorique du jeu -1/18 pour le joueur 0 (désavantage structurel du joueur qui agit en premier). Elle reclasse GT-7 en RECOVERABLE-LOCAL. Le pont est EN PLUS, jamais À LA PLACE (mandat #10382) : les couches from-scratch des sections 1-7 (modèle GameNode/ExtensiveFormGame, backward induction, conversion extensive→normale) restent la contrepartie pédagogique principale.
Prérequis : pip install open_spiel (pyspiel 2.0.1 natif Windows, env CPython 3.11.9) + pythonnet 3.0.5 (NuGet). Variable d’env optionnelle PYTHONNET_PYDLL pour pointer vers une DLL CPython alternative. Sans elle, et hors du CPython du poste de la flotte, la cellule interroge le premier interpréteur (python3, puis python) qui importe pyspiel, et en reprend la bibliothèque et le sys.path : sous Linux et macOS, pip install open_spiel suffit, environnement virtuel compris.
#r "nuget: pythonnet,3.0.5"using System;using Python.Runtime;// Pont .NET -> CPython 3.11 -> pyspiel (annexe forme extensive, reclassification #10459).// OpenSpiel (Kuhn 1950, jeux extensifs pre-implementes) = moteur de reference section 6.// Pont via pythonnet (NuGet, Python.Runtime) -> CPython 3.11.9 ou pyspiel 2.0.1 est installe (pip).// Meme axe de pontage que GT-13/GT-17 (pyspiel CFR/rollout), Search-7 (pyspiel MCTS), GT-6 (axelrod).// Repli portable (Linux, macOS, ou Windows sans le CPython ci-dessus) : le premier// interpreteur (python3, puis python) qui importe le module fournit sa bibliotheque// partagee, son prefixe et son sys.path, environnement virtuel compris.staticvoidInitPythonFromInterpreter(string module){conststring probe ="import importlib, json, os, sys, sysconfig\n"+"importlib.import_module(sys.argv[1])\n"+"v = sysconfig.get_config_var; lib = v('LIBDIR') or ''; mm = sys.version_info[:2]\n"+"c = [os.path.join(lib, n) for n in (v('INSTSONAME'), v('LDLIBRARY')) if n]\n"+"c += [os.path.join(d, 'libpython%d.%d.dylib' % mm) for d in (lib, os.path.join(sys.base_prefix, 'lib'))]\n"+"c.append(os.path.join(sys.base_prefix, 'python%d%d.dll' % mm))\n"+"dll = next((p for p in c if os.path.isfile(p)), '')\n"+"print(json.dumps({'dll': dll, 'home': sys.base_prefix, 'path': [p for p in sys.path if p]}))\n";foreach(var exe innew[]{"python3","python"}){try{var psi =new System.Diagnostics.ProcessStartInfo(exe){ RedirectStandardOutput =true, RedirectStandardError =true, UseShellExecute =false};foreach(var arg innew[]{"-c", probe, module }) psi.ArgumentList.Add(arg);usingvar p = System.Diagnostics.Process.Start(psi);var stderr = p.StandardError.ReadToEndAsync();string json = p.StandardOutput.ReadToEnd(); p.WaitForExit();if(p.ExitCode!=0)continue;var info = System.Text.Json.JsonDocument.Parse(json).RootElement;string dll = info.GetProperty("dll").GetString();if(string.IsNullOrEmpty(dll))continue; Runtime.PythonDLL= dll; PythonEngine.PythonHome= info.GetProperty("home").GetString(); PythonEngine.Initialize();using(Py.GIL()){var path = info.GetProperty("path").EnumerateArray().Select(e =>(PyObject)newPyString(e.GetString())).ToArray(); Py.Import("sys").SetAttr("path",newPyList(path));}return;}catch(System.ComponentModel.Win32Exception){}// interpreteur absent du PATH}thrownew System.IO.FileNotFoundException( $"Aucun CPython n'importe {module} : l'installer (pip install), ou definir PYTHONNET_PYDLL.");}var pyDll = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL")?? @"C:\Users\jsboi\AppData\Local\Programs\Python\Python311\python311.dll";if(System.IO.File.Exists(pyDll)){ Runtime.PythonDLL= pyDll; PythonEngine.Initialize();}elseInitPythonFromInterpreter("pyspiel");Console.WriteLine($"Runtime Python : {PythonEngine.Version}");using(Py.GIL()){ dynamic scope = Py.CreateScope(); scope.Exec(@"import pyspielfrom open_spiel.python.algorithms import cfr, exploitabilitykuhn = pyspiel.load_game('kuhn_poker')lines =[f'pyspiel {getattr(pyspiel,""__version__"",""N/A"")}: Kuhn Poker(OpenSpiel)']lines.append('='*50)lines.append(f'Nombre de joueurs :{kuhn.num_players()}')lines.append(f'Dynamique :{kuhn.get_type().dynamics}')lines.append(f'Information :{kuhn.get_type().information}')lines.append(f'Utilite :{kuhn.get_type().utility}')# 1) Partie deterministe : premiere action legale a chaque noeud(nature incluse)state = kuhn.new_initial_state()lines.append('\nSimulation deterministe d\'une partie(premiere action legale a chaque noeud):')while not state.is_terminal():if state.is_chance_node(): a, p = state.chance_outcomes()[0] lines.append(f' Nature tire : action {a}(p={p:.2f})')else: pl = state.current_player() a = state.legal_actions()[0] lines.append(f' Joueur {pl} joue :{state.action_to_string(pl, a)}') state.apply_action(a)lines.append(f'Resultat(gains P0, P1):{state.returns()}')# 2) Exploration de l'arbre avec infosets(DFS borne, miroir du jumeau Python)def explore(s, depth, budget):if budget[0]<=0:return budget[0]-=1 indent = ' ' * depthif s.is_terminal(): lines.append(f'{indent}[Terminal] Gains:{s.returns()}')returnif s.is_chance_node(): lines.append(f'{indent}[Nature] Outcomes:{len(s.chance_outcomes())}')for a, p in s.chance_outcomes()[:2]: lines.append(f'{indent}-> action {a}(p={p:.2f})')explore(s.child(a), depth +2, budget)else: pl = s.current_player() inf = s.information_state_string(pl) lines.append(f'{indent}[J{pl}] Infoset:{inf[:24]}...')for a in s.legal_actions()[:2]: lines.append(f'{indent}->{s.action_to_string(pl, a)}')explore(s.child(a), depth +2, budget)lines.append('\nExploration de l\'arbre de kuhn_poker(infosets, budget 14 noeuds):')lines.append('='*50)explore(kuhn.new_initial_state(),0,[14])# 3) Equilibre par Counterfactual Regret Minimization(CFR,30 iterations)solver = cfr.CFRSolver(kuhn)for _ inrange(30): solver.evaluate_and_update_policy()avg = solver.average_policy()def compute_value(s, pol): # Valeur pour P0 sous la politique moyenne(somme sur l'arbre, Kuhn =58 etats)if s.is_terminal():return s.player_return(0)if s.is_chance_node():returnsum(p *compute_value(s.child(a), pol)for a, p in s.chance_outcomes()) pl = s.current_player() probs =dict(pol[s.information_state_string(pl)])returnsum(pa *compute_value(s.child(a), pol)for a, pa in probs.items())val =compute_value(kuhn.new_initial_state(), avg.to_dict())conv = exploitability.exploitability(kuhn, avg)lines.append('\nEquilibre par Counterfactual Regret Minimization(CFR,30 iterations):')lines.append(f' NashConv(politique moyenne):{conv:.4f}(0= equilibre parfait)')lines.append(f' Valeur du jeu pour P0 :{val:.4f}')lines.append(f' Theorie Kuhn(1950): valeur =-1/18={-1/18:.4f}(P0 agit en premier, desavantage structurel)')report =chr(10).join(lines)"); Console.WriteLine((string)scope.report);}Console.WriteLine("PONT .NET -> pyspiel : OK -- le moteur de reference OpenSpiel confirme la structure extensive (infosets) du from-scratch et converge vers la valeur theorique de Kuhn (-1/18) via CFR");
Installed Packages
pythonnet, 3.0.5
Runtime Python : 3.11.15 (main, Mar 3 2026, 09:26:23) [GCC 13.3.0]
pyspiel 2.0.1 : Kuhn Poker (OpenSpiel)
==================================================
Nombre de joueurs : 2
Dynamique : Dynamics.SEQUENTIAL
Information : Information.IMPERFECT_INFORMATION
Utilite : Utility.ZERO_SUM
Simulation deterministe d'une partie (premiere action legale a chaque noeud) :
Nature tire : action 0 (p=0.33)
Nature tire : action 1 (p=0.50)
Joueur 0 joue : Pass
Joueur 1 joue : Pass
Resultat (gains P0, P1) : [-1.0, 1.0]
Exploration de l'arbre de kuhn_poker (infosets, budget 14 noeuds) :
==================================================
[Nature] Outcomes: 3
-> action 0 (p=0.33)
[Nature] Outcomes: 2
-> action 1 (p=0.50)
[J0] Infoset: 0...
-> Pass
[J1] Infoset: 1p...
-> Pass
[Terminal] Gains: [-1.0, 1.0]
-> Bet
[J0] Infoset: 0pb...
-> Pass
[Terminal] Gains: [-1.0, 1.0]
-> Bet
[Terminal] Gains: [-2.0, 2.0]
-> Bet
[J1] Infoset: 1b...
-> Pass
[Terminal] Gains: [1.0, -1.0]
-> Bet
[Terminal] Gains: [-2.0, 2.0]
-> action 2 (p=0.50)
[J0] Infoset: 0...
-> Pass
[J1] Infoset: 2p...
-> Pass
[Terminal] Gains: [-1.0, 1.0]
-> Bet
-> Bet
-> action 1 (p=0.33)
Equilibre par Counterfactual Regret Minimization (CFR, 30 iterations) :
NashConv (politique moyenne) : 0.0230 (0 = equilibre parfait)
Valeur du jeu pour P0 : -0.0571
Theorie Kuhn (1950) : valeur = -1/18 = -0.0556 (P0 agit en premier, desavantage structurel)
PONT .NET -> pyspiel : OK -- le moteur de reference OpenSpiel confirme la structure extensive (infosets) du from-scratch et converge vers la valeur theorique de Kuhn (-1/18) via CFR
Lecture du pont OpenSpiel : CFR converge vers la valeur de Kuhn
La sortie du pont est une contre-épreuve chiffrée du modèle from-scratch : OpenSpiel confirme les attributs structurels (Dynamics.SEQUENTIAL, Information.IMPERFECT_INFORMATION, Utility.ZERO_SUM — les trois propriétés construites à la main dans les sections 2 à 6), puis le solveur parle. Après 30 itérations de Counterfactual Regret Minimization, la mesure NashConv tombe à 0,0230 — l’écart exploitables à l’équilibre, proche de zéro — et la valeur du jeu pour P0 vaut -0,0571, à comparer à la valeur théorique de Kuhn (1950) : -1/18 ≈ -0,0556. L’écart résiduel (~0,0015) est le coût des 30 itérations : CFR converge vers l’équilibre sans l’atteindre en temps fini. Le signe négatif est structurel — le premier joueur de Kuhn Poker est désavantagé, et c’est le même désavantage de premier-mouvant que le jeu d’entree inversait en faveur de l’entrant informé. Le pont PythonNet fait donc deux travaux : il invalide le verdict INTRINSIC hérité (l’axe 5 de #10459) et il ancre le modèle pédagogique sur un moteur de référence.