GameTheory-03h — Deux espèces de flèches : quand une transformation est-elle aussi un morphisme ?
Objectifs de ce notebook :
Distinguer deux espèces de flèches qui coexistent sur l’espace des jeux 2x2 ordinaux : les flèches qui transforment la structure (les swaps de rangs de Robinson-Goforth) et les flèches qui la préservent (les morphismes de jeux au sens Tohmé-Viglizzo).
Rendre la question finie et décidable : sur les 576 jeux stricts et les 6 générateurs adjacents, la question « ce générateur est-il un morphisme pour ce jeu ? » est une énumération de 3456 paires — pas un slogan.
Exhiber un contre-exemple concret où un swap détruit un équilibre de Nash qu’un morphisme aurait préservé.
Énumérer l’intersection : combien de paires (jeu, générateur) le générateur préserve-t-il ? Le compte doit être reproductible.
Caractériser la préservation par une condition simple sur le jeu de départ — et la vérifier exhaustivement, pas seulement la conjecturer.
Ce notebook est la première expérience de la « Loi III — les deux espèces de flèches » sur un univers fini où tout se vérifie. Il prolonge GameTheory-03-Topology2x2-Python.ipynb, dont il réutilise la représentation ordinale.
1. Le problème : deux espèces de flèches sur le même espace
La théorie des jeux dispose de deux traditions qui font circuler des flèches entre les jeux, et elles ne font pas la même chose :
Espèce
Ce qu’elle fait
Source
Flèches qui PRÉSERVENT la structure
morphismes de jeux : ils transportent les équilibres d’un jeu vers un autre jeu qui « joue pareil »
catégories concrètes de jeux de Tohmé-Viglizzo (Gam_I, Gam)
Flèches qui TRANSFORMENT la structure
swaps de rangs : ils changent les préférences d’un joueur, donc le jeu lui-même
topologie des jeux 2x2 de Robinson-Goforth
Un swap de rangs échange deux valeurs dans la table de gains d’un joueur — par exemple, permuter les rangs 1 et 2 du joueur Ligne. Ce n’est pas un changement de croyances ni d’actions : c’est un changement des préférences elles-mêmes, donc du monde descriptif. Un morphisme de jeux, à l’inverse, garantit que ce qui est rationnel ici l’est encore là-bas : les équilibres se transportent.
La Loi III du chantier 4 s’énonce alors précisément :
Un swap change les préférences — il n’est donc pas automatiquement un morphisme. La question n’est pas « transformation ou morphisme ? » en général, mais « pour ce jeu précis, ce générateur précis, préserve-t-il les équilibres ? ».
Et cette question, sur un univers fini, a une réponse mécanique.
La question rendue finie
L’univers de GameTheory-3 est l’espace des jeux 2x2 stricts ordinaux : chaque joueur distribue les rangs 1-4 sur les quatre cases. Il y a \(24 \times 24 = 576\) tels jeux. La grammaire de Robinson-Goforth les fait bouger par six générateurs adjacents :
\(R_{12}, R_{23}, R_{34}\) : échanger les rangs adjacents \((1,2)\), \((2,3)\) ou \((3,4)\) dans les gains du joueur Ligne ;
\(C_{12}, C_{23}, C_{34}\) : la même chose pour le joueur Colonne.
Ces six swaps suffisent à engendrer tout le groupe \(S_4 \times S_4\) des permutations de rangs (les transpositions adjacentes engendrent \(S_4\)) : un jeu ne se décrit pas, il se rejoint depuis un autre.
Sur cet espace, la question « transformation ou morphisme ? » devient une propriété décidable par énumération :
Réduction honnête de la notion de morphisme : dans les catégories de Tohmé-Viglizzo, un morphisme de jeux transporte aussi les stratégies. Ici, un swap ne déplace aucune stratégie — seule l’étiquette des gains change — donc l’application induite sur les profils est l’identité. L’espèce préservante se teste alors opérationnellement :
Définition (finie). Le générateur \(g\) est un morphisme pour le jeu \(G\) si \(g\) préserve l’ensemble des équilibres de Nash purs : \(\text{Nash}(G) = \text{Nash}(g(G))\), la comparaison se faisant cellule à cellule (application identité).
C’est cette définition que tout le notebook met à l’épreuve.
from dataclasses import dataclassfrom itertools import permutationsfrom typing import Dict, List, Set, Tupleimport numpy as np@dataclassclass OrdinalGame:""" Jeu 2x2 en representation ordinale (convention GameTheory-3). Chaque matrice contient les rangs 1-4 pour les 4 cellules, numerotees : 0 | 1 ----- 2 | 3 """ row_payoffs: Tuple[int, int, int, int] col_payoffs: Tuple[int, int, int, int] name: str=""def __post_init__(self):assertsorted(self.row_payoffs) == [1, 2, 3, 4], "row_payoffs doit etre une permutation de 1-4"assertsorted(self.col_payoffs) == [1, 2, 3, 4], "col_payoffs doit etre une permutation de 1-4"def to_matrices(self) -> Tuple[np.ndarray, np.ndarray]: A = np.array([[self.row_payoffs[0], self.row_payoffs[1]], [self.row_payoffs[2], self.row_payoffs[3]]]) B = np.array([[self.col_payoffs[0], self.col_payoffs[1]], [self.col_payoffs[2], self.col_payoffs[3]]])return A, Bdef display(self): A, B =self.to_matrices()print(f"\n{self.name ifself.name else'Jeu'}")print("="*32)print(f" C0 C1")print(f"R0 ({A[0,0]},{B[0,0]}) ({A[0,1]},{B[0,1]})")print(f"R1 ({A[1,0]},{B[1,0]}) ({A[1,1]},{B[1,1]})")def__hash__(self):returnhash((self.row_payoffs, self.col_payoffs))def__eq__(self, other):return (self.row_payoffs == other.row_payoffsandself.col_payoffs == other.col_payoffs)# Encodages ordinaux des jeux classiques (identiques a GameTheory-3)CLASSIC_GAMES = {"Prisoner's Dilemma": OrdinalGame((3, 1, 4, 2), (3, 4, 1, 2), "Prisoner's Dilemma"),"Stag Hunt": OrdinalGame((4, 1, 3, 2), (4, 3, 1, 2), "Stag Hunt"),"Chicken": OrdinalGame((3, 2, 4, 1), (3, 4, 2, 1), "Chicken (Hawk-Dove)"),"Matching Pennies": OrdinalGame((4, 1, 2, 3), (1, 4, 3, 2), "Matching Pennies"),"Deadlock": OrdinalGame((2, 1, 4, 3), (2, 4, 1, 3), "Deadlock"),"Harmony": OrdinalGame((4, 3, 2, 1), (4, 2, 3, 1), "Harmony"),}for name, game in CLASSIC_GAMES.items(): game.display()
Chaque case affiche (gain Ligne, gain Colonne) en rangs ordinaux — le rang 4 est le meilleur pour le joueur qui le reçoit. Deux observations qui serviront tout le long :
Lecture par colonne : dans la colonne \(C_j\), le joueur Ligne compare ses deux gains ; le plus grand désigne sa meilleure réponse dans cette colonne. Symétriquement, le joueur Colonne se lit par ligne.
Stricteté : chaque joueur place exactement une fois chaque rang 1-4 ; il n’y a aucune égalité, donc chaque colonne (resp. ligne) a une meilleure réponse unique de chaque côté. C’est ce qui rend l’espace fini et propre — les jeux à égalité (les « murs » du grain D2, issue #12213) sont un autre objet.
Le Dilemme du Prisonnier se relit ainsi : pour Ligne, la colonne \(C_0\) compare 3 et 4 (il préfère \(R_1\) : déserter), la colonne \(C_1\) compare 1 et 2 (il préfère encore \(R_1\)) — \(R_1\) est une stratégie dominante, et l’équilibre unique \((R_1, C_1)\) est le pire outcome collectif.
def find_pure_nash(game: OrdinalGame) -> List[Tuple[int, int]]:"""Equilibres de Nash purs : cases ou chaque strategie est meilleure reponse a l'autre.""" A, B = game.to_matrices() equilibria = []for i inrange(2):for j inrange(2): row_br = A[i, j] >= A[1- i, j] col_br = B[i, j] >= B[i, 1- j]if row_br and col_br: equilibria.append((i, j))return equilibriadef describe_nash(profiles: List[Tuple[int, int]]) ->str:return", ".join(f"(R{i},C{j})"for i, j in profiles) if profiles else"aucun"print("Structure de Nash des jeux classiques :")print("="*52)for name, game in CLASSIC_GAMES.items(): eq = find_pure_nash(game)print(f" {name:22s}{len(eq)} equilibre(s) : {describe_nash(eq)}")
Interprétation : la structure de Nash, signature du jeu
L’ensemble des équilibres de Nash purs est la signature stratégique d’un jeu — c’est précisément ce qu’un morphisme de jeux s’engage à transporter. La collection classique couvre les quatre situations : un unique équilibre à dominance (Dilemme, Deadlock, Harmony), deux équilibres de coordination ou d’anti-coordination (Stag Hunt, Chicken), et aucun équilibre pur (Matching Pennies, où l’instabilité est le jeu).
Ce qui va nous intéresser n’est pas la classification elle-même (c’est le sujet de GameTheory-3, §5-6) mais une question plus fine : cette signature survit-elle au passage d’un générateur ? Un jeu sans équilibre peut en acquérir un, un jeu à équilibre unique peut le voir déménager — ou tout perdre. Chacun de ces événements est une destruction au sens morphique : la flèche existe (le swap est toujours applicable), mais elle ne préserve pas.
def swap_payoffs(payoffs: Tuple[int, int, int, int], rank1: int, rank2: int) -> Tuple[int, int, int, int]:"""Echange deux valeurs de rang dans une table de gains (convention GameTheory-3).""" result =list(payoffs)for i inrange(4):if result[i] == rank1: result[i] = rank2elif result[i] == rank2: result[i] = rank1returntuple(result)def apply_generator(game: OrdinalGame, gen: str) -> OrdinalGame:"""Applique un des six generateurs adjacents R12/R23/R34/C12/C23/C34.""" player, r1, r2 = gen[0], int(gen[1]), int(gen[2])if player =="R":return OrdinalGame(swap_payoffs(game.row_payoffs, r1, r2), game.col_payoffs)return OrdinalGame(game.row_payoffs, swap_payoffs(game.col_payoffs, r1, r2))GENERATORS = ["R12", "R23", "R34", "C12", "C23", "C34"]# Demonstration : le Dilemme du Prisonnier perd son rang 1<->2 cote Lignepd = CLASSIC_GAMES["Prisoner's Dilemma"]pd_r12 = apply_generator(pd, "R12")print("Jeu de depart :")pd.display()print(f" Nash : {describe_nash(find_pure_nash(pd))}")print("\nApres le generateur R12 (rangs 1 et 2 echanges pour Ligne) :")pd_r12.display()print(f" Nash : {describe_nash(find_pure_nash(pd_r12))}")
Jeu de depart :
Prisoner's Dilemma
================================
C0 C1
R0 (3,3) (1,4)
R1 (4,1) (2,2)
Nash : (R1,C1)
Apres le generateur R12 (rangs 1 et 2 echanges pour Ligne) :
Jeu
================================
C0 C1
R0 (3,3) (2,4)
R1 (4,1) (1,2)
Nash : (R0,C1)
Interprétation : la grammaire en action — et le soupçon
Le générateur \(R_{12}\) a échangé les rangs 1 et 2 du joueur Ligne : la case \((R_0, C_1)\) est passée de 1 à 2 et la case \((R_1, C_1)\) de 2 à 1. Les stratégies n’ont pas bougé, les préférences ont changé — et la sortie mesure déjà le soupçon fondateur de la Loi III : dans la colonne \(C_1\), Ligne comparait 1 et 2 (il préférait \(R_1\)) ; il compare maintenant 2 et 1 (il préfère \(R_0\)). Sa meilleure réponse dans cette colonne s’est retournée, et avec elle l’équilibre.
Ce seul exemple montre les deux espèces à l’œuvre : la flèche \(R_{12}\)existe toujours (c’est une transformation valide de l’espace) mais, pour ce jeu précis, elle ne préserve pas la signature de Nash. Reste à savoir si c’est un accident du Dilemme ou une structure de l’espace entier — c’est l’objet des sections suivantes. La réponse tiendra en une condition d’une ligne sur la table de gains du jeu de départ.
2. L’espèce préservante : tester le morphisme, flèche par flèche
La définition finie de la section 1 se implémente en une fonction : \(g\) est un morphisme pour \(G\) si et seulement si les ensembles de Nash coïncident avant et après l’application de \(g\). Notons ce que cette comparaison ne demande pas : elle ne demande pas que le jeu soit « pareil » (les gains diffèrent en général), ni que les équilibres soient aux mêmes cases pour toujours — elle demande l’égalité des deux ensembles pour cette paire \((G, g)\).
C’est bien une relation locale : le même générateur peut être morphisme pour un jeu et pas pour son voisin. L’espace des flèches se partitionne donc en
flèches préservantes — celles qui, pour ce jeu, transportent la signature de Nash intacte (l’identité sur les profils est alors compatible avec la structure de jeu) ;
flèches transformatrices — celles qui cassent, déplacent ou créent des équilibres.
Et parce que l’univers est fini, chacune des 3456 paires tombe d’un côté ou de l’autre sans zone grise.
def est_morphisme_pour(game: OrdinalGame, gen: str) ->bool:""" Le generateur gen preserve-t-il l'ensemble des equilibres de Nash purs de game ? Reduction finite : un swap ne deplace aucune strategie, donc la comparaison se fait sous l'application identite sur les profils. """returnset(find_pure_nash(game)) ==set(find_pure_nash(apply_generator(game, gen)))print("Trancher quelques paires a la main :")print("="*56)for game_name, gen in [ ("Deadlock", "R12"), ("Deadlock", "R34"), ("Prisoner's Dilemma", "R12"), ("Prisoner's Dilemma", "C12"), ("Harmony", "C34"), ("Matching Pennies", "R23"),]: g = CLASSIC_GAMES[game_name] g2 = apply_generator(g, gen) verdict = est_morphisme_pour(g, gen)print(f" {game_name:22s} + {gen} : "f"Nash {describe_nash(find_pure_nash(g)):12s} -> {describe_nash(find_pure_nash(g2)):12s} "f"=> {'PRESERVE'if verdict else'TRANSFORME'}")
Les six verdicts mesurés racontent déjà la Loi III :
Deadlock + \(R_{12}\) : PRÉSERVÉ. Pour Ligne, les colonnes comparent (2,4) et (1,3) — échanger 1 et 2 ne change aucun ordre interne de colonne : les meilleures réponses ne bougent pas, l’équilibre \((R_1, C_1)\) reste seul. La flèche transforme la table mais transporte la structure : elle est des deux espèces à la fois.
Prisoner’s Dilemma + \(R_{12}\) : TRANSFORMÉ. La colonne \(C_1\) comparait (1,2) ; après le swap elle compare (2,1) — la meilleure réponse de Ligne s’y retourne, et l’équilibre unique déménage de \((R_1, C_1)\) vers \((R_0, C_1)\).
Matching Pennies + \(R_{23}\) : PRÉSERVÉ… trivialement. L’ensemble vide est égal à lui-même : le jeu n’a pas d’équilibre pur avant ni après. La préservation ici ne dit rien d’intéressant — un cas limite qu’il faudra garder en tête en lisant les comptes de la section 4.
Le point structurant : le verdict n’appartient ni au générateur ni au jeu seul, mais à la paire. C’est exactement pourquoi la question « un swap est-il un morphisme ? » est mal posée sans son jeu — et pourquoi l’énumération qui suit n’est pas un exercice de style.
3. Exercice 1 : implémenter les deux espèces et produire un contre-exemple
À vous de jouer : la fonction contre_exemple doit trouver, dans les jeux classiques, une paire (jeu, générateur) où le swap détruit un équilibre — c’est-à-dire où un équilibre de Nash du jeu de départ n’est plus équilibre dans le jeu d’arrivée. Un contre-exemple, pas un principe.
def contre_exemple(games: Dict[str, OrdinalGame]) -> List[dict]:# TODO etudiant : trouver les paires (jeu, generateur) ou le swap DETRUIT un equilibre# Etape 1 : pour chaque jeu et chaque generateur des 6, calculer Nash avant et Nash apres# Etape 2 : garder les paires ou un equilibre du depart disparait (destruction stricte :# un profil de Nash(G) absent de Nash(g(G)) -- pas seulement un deplacement)# Etape 3 : retourner une liste de dicts {jeu, generateur, nash_avant, nash_apres, detruit}# Indice : est_morphisme_pour dit si les ENSEMBLES sont egaux ; ici il faut regarder# profil par profil -- un deplacement (ensemble different) n'est pas une destruction# Verifiez votre resultat : au moins un classique doit apparaitre avec une destruction strictereturn [] # TODO etudiant : remplacer# resultats_ex1 = contre_exemple(CLASSIC_GAMES)# for r in resultats_ex1:# print(r["jeu"], r["generateur"], ":", r["detruit"])print("Exercice a completer : contre-exemple de destruction d'equilibre par un swap")
Exercice a completer : contre-exemple de destruction d'equilibre par un swap
Exemple résolu : un équilibre détruit, un équilibre déplacé
La démonstration guidée ci-dessous tranche la question sur le Dilemme du Prisonnier avec les deux générateurs qui l’attaquent le plus naturellement : \(R_{12}\) (Ligne) et \(C_{12}\) (Colonne). On regarde les profils un par un — c’est la distinction destruction/déplacement qui compte :
déplacement : l’équilibre de départ n’est plus là, mais un autre est apparu (la cardinalité est conservée) ;
destruction stricte : un profil de Nash(\(G\)) n’est plus Nash dans \(g(G)\), sans compensation nécessaire — la signature perd un élément.
def differencial_nash(game: OrdinalGame, gen: str) ->dict:"""Compare Nash avant/apres, profil par profil.""" avant =set(find_pure_nash(game)) apres =set(find_pure_nash(apply_generator(game, gen)))return {"generateur": gen,"avant": avant,"apres": apres,"detruits": avant - apres,"creees": apres - avant, }pd = CLASSIC_GAMES["Prisoner's Dilemma"]print(f"Jeu : Prisoner's Dilemma Nash initial : {describe_nash(find_pure_nash(pd))}")print("="*62)for gen in ["R12", "C12", "R34"]: d = differencial_nash(pd, gen)if d["detruits"] and d["creees"]: nature ="DEPLACEMENT : "+ describe_nash(list(d["detruits"])) +" -> "+ describe_nash(list(d["creees"]))elif d["detruits"]: nature ="DESTRUCTION stricte : "+ describe_nash(list(d["detruits"]))else: nature ="preservation"+ (" (deplacement : "+ describe_nash(list(d["creees"])) +")"if d["creees"] else"")print(f" {gen} : Nash apres {describe_nash(list(d['apres'])):12s} | {nature}")# Le contre-exemple attendu par l'exercice 1 existe-t-il dans les classiques ?print()print("Recherche d'une DESTRUCTION stricte dans les jeux classiques :")trouve =Falsefor name, game in CLASSIC_GAMES.items():for gen in GENERATORS: d = differencial_nash(game, gen)if d["detruits"] andnot d["creees"]: trouve =Trueprint(f" {name} + {gen} : perd {describe_nash(list(d['detruits']))} sans compensation")ifnot trouve:print(" (aucune dans cet echantillon -- elargir l'espace)")
Jeu : Prisoner's Dilemma Nash initial : (R1,C1)
==============================================================
R12 : Nash apres (R0,C1) | DEPLACEMENT : (R1,C1) -> (R0,C1)
C12 : Nash apres (R1,C0) | DEPLACEMENT : (R1,C1) -> (R1,C0)
R34 : Nash apres (R1,C1) | preservation
Recherche d'une DESTRUCTION stricte dans les jeux classiques :
Stag Hunt + R12 : perd (R1,C1) sans compensation
Stag Hunt + R34 : perd (R0,C0) sans compensation
Stag Hunt + C12 : perd (R1,C1) sans compensation
Stag Hunt + C34 : perd (R0,C0) sans compensation
Chicken + R12 : perd (R0,C1) sans compensation
Chicken + R34 : perd (R1,C0) sans compensation
Chicken + C12 : perd (R1,C0) sans compensation
Chicken + C34 : perd (R0,C1) sans compensation
Interprétation : trois régimes mesurés sur un seul jeu — et le mur habité
Le Dilemme exhibe d’un coup les trois régimes possibles d’une flèche :
\(R_{12}\) et \(C_{12}\) : déplacement. L’équilibre unique déménage (\((R_1,C_1) \to (R_0,C_1)\) pour le premier, \(\to (R_1,C_0)\) pour le second). L’ensemble change — pas morphisme — mais le jeu d’arrivée a toujours un équilibre : la richesse stratégique est transportée, pas amputée.
\(R_{34}\) : préservation. La colonne \(C_0\) du Dilemme contient bien le couple adjacent \((3,4)\) pour Ligne — la flèche traverse un mur — et pourtant la signature de Nash ne bouge pas. Premier indice que la condition naïve « couple adjacent ⟹ transformation » est fausse : il faut comprendre pourquoi ce passage est invisible.
Sur les autres classiques, Stag Hunt et Chicken subissent des destructions strictes : chacun de leurs deux équilibres peut être supprimé sans compensation par le bon générateur. C’est le contre-exemple dur que l’exercice 1 demande — une coordination qui perd une de ses sorties.
Le vocabulaire géométrique du chantier 4 s’impose : dans la lecture Bruns-Kimmich (grain D2, issue #12213), les jeux stricts sont des chambres et les swaps adjacents traversent des murs. Mais le régime \(R_{34}\) du Dilemme montre la subtilité : un mur ne change la chambre Nash que s’il est habité — si aucune meilleure réponse de Colonne ne pointe vers la colonne traversée, aucun équilibre ne vivait contre ce mur, et le passage est stratégiquement invisible. C’est cette intuition que la section 6 transforme en théorème vérifié.
4. Énumération exhaustive : l’intersection des deux espèces
L’échantillon des classiques ne suffit pas : la Loi III demande le compte sur l’espace entier. L’énumération ci-dessous génère les 576 jeux stricts (toutes paires de permutations de 1-4), applique chacun des 6 générateurs à chacun des jeux, et tranche les 3456 paires une par une. Trois mesures en sortent :
le compte par générateur : combien de jeux chaque générateur préserve ;
la distribution par jeu : combien de générateurs transforment chaque jeu (de 0 à 6) ;
les points fixes de la grammaire : les jeux que les six générateurs préservent — les jeux « au cœur de leur chambre », inatteignables par un seul pas de la grammaire.
def generate_all_ordinal_games() -> Set[OrdinalGame]:"""Les 576 jeux 2x2 stricts : 24 permutations de rangs par joueur."""return {OrdinalGame(r, c)for r in permutations([1, 2, 3, 4])for c in permutations([1, 2, 3, 4])}all_games = generate_all_ordinal_games()print(f"Jeux stricts generes : {len(all_games)} (24 x 24)")print()# 1. Compte par generateurprint("Preservation par generateur (sur 576 jeux) :")print("="*46)total_preserve =0for gen in GENERATORS: n_pres =sum(est_morphisme_pour(g, gen) for g in all_games) total_preserve += n_presprint(f" {gen} : {n_pres:3d} preserves / {576- n_pres:3d} transformes "f"({100* n_pres /576:.1f} %)")print(f" {'TOTAL':>4s} : {total_preserve} paires preservantes sur {576*6} "f"({100* total_preserve / (576*6):.1f} %)")# 2. Distribution par jeu : combien de generateurs transforment ce jeuprint()print("Distribution du nombre de generateurs transformateurs par jeu :")par_jeu = {}for g in all_games: n_trans =sum(not est_morphisme_pour(g, gen) for gen in GENERATORS) par_jeu[n_trans] = par_jeu.get(n_trans, 0) +1for k insorted(par_jeu): barre ="#"* (par_jeu[k] //6)print(f" {k} generateur(s) transformateur(s) : {par_jeu[k]:3d} jeux {barre}")# 3. Points fixes de la grammaire : 0 generateur transformateurfixes = [g for g in all_gamesifall(est_morphisme_pour(g, gen) for gen in GENERATORS)]print(f"\nPoints fixes (6/6 preserves) : {len(fixes)} jeux")
Interprétation : la séparation nette existe — et elle est massive
Les comptes mesurés établissent la Loi III sur l’univers entier :
Chaque générateur préserve 432 jeux sur 576 (75 %) et en transforme 144 (25 %). Un quart de l’espace change de signature de Nash à chaque pas de la grammaire — la séparation n’est ni marginale ni décorative. Et aucun générateur n’est « globalement » morphisme : l’espèce d’une flèche est une propriété de la paire.
La distribution par jeu est unanime : chaque jeu est transformé par au moins un des six générateurs (aucun jeu à 0 n’échapperait… en fait si : 100 jeux sont préservés par les six — les « points fixes de la grammaire »), et 16 jeux sont transformés par quatre générateurs sur six — les plus exposés de l’espace. La masse (460 jeux) se concentre entre un et trois générateurs transformateurs.
2592 paires préservantes sur 3456 — dont les préservations triviales (les jeux sans équilibre pur, dont l’ensemble vide se préserve de lui-même) et les préservations invisibles découvertes en section 3 (\(R_{34}\) sur le Dilemme) : le compte honnête les inclut, la lecture doit s’en souvenir.
Le verdict du grain D5 s’écrit : la séparation nette apparaît — certaines flèches préservent, d’autres transforment, et le partage n’est ni uniforme ni arbitraire. La distribution a une forme ; une forme annonce une cause. La section suivante commence par se tromper dessus — c’est voulu.
5. Exercice 2 : cartographier l’intersection par structure de Nash
Le compte global dit combien ; à vous de dire quoi. La fonction cartographier_intersection doit croiser l’énumération avec la structure de Nash des jeux de départ (nombre d’équilibres purs) pour répondre : les jeux préservés par un générateur sont-ils surtout des jeux sans équilibre (préservation triviale) ou surtout des jeux à équilibre unique ? La réponse mesure à quel point la préservation mesurée en section 4 est substantielle.
def cartographier_intersection(games: Set[OrdinalGame]) -> Dict[str, Dict[str, int]]:# TODO etudiant : croiser preservation x structure de Nash des jeux de depart# Etape 1 : pour chaque jeu, calculer son nombre d'equilibres purs (0 a 4)# Etape 2 : pour chaque paire (jeu, generateur), enregistrer preserve/transforme# Etape 3 : aggreger en dict {nb_equilibres: {"preserves": X, "transformes": Y}}# Indice : find_pure_nash et est_morphisme_pour font tout le travail ;# la question interessante est le RATIO preserves/transformes par strate# Verifiez : les jeux a 0 equilibre sont preserves a 100 % (ensemble vide), les autres nonreturn {} # TODO etudiant : remplacer# carte = cartographier_intersection(all_games)# for nb_eq, compteurs in sorted(carte.items()):# print(nb_eq, compteurs)print("Exercice a completer : cartographie preservation x structure de Nash")
Exercice a completer : cartographie preservation x structure de Nash
6. Caractérisation : quand un générateur préserve-t-il ?
L’énumération a une forme ; cherchons la cause. Premier pas sûr — la mécanique des meilleures réponses :
Un swap \(R_{(a,b)}\) ne peut retourner une meilleure réponse que dans une colonne qui contient les deux rangs échangés. Dans une colonne donnée, la meilleure réponse de Ligne ne dépend que de l’ordre de ses deux gains ; échanger les valeurs \(a\) et \(b\) ne change aucun ordre… sauf dans une colonne dont les deux gains sont précisément \(a\) et \(b\) (l’ordre s’y inverse).
On serait tenté de conclure : « couple \(\{a,b\}\) présent dans une colonne ⟹ transformation. » C’est la conjecture naïve — et elle est fausse : le régime \(R_{34}\) du Dilemme (section 3) la contredit déjà. La cellule suivante la teste sur tout l’espace, montre ses désaccords, puis construit la bonne condition en regardant pourquoi certains retournements sont invisibles :
Un retournement n’est visible dans l’ensemble de Nash que si la colonne traversée est aussi une colonne-maison pour l’autre joueur. Le profil \((i, C_j)\) n’est équilibre que si \(C_j\) est aussi la meilleure réponse de Colonne en ligne \(i\). Si les meilleures réponses de Colonne (dans les deux lignes) pointent toutes ailleurs que vers \(C_j\), aucun équilibre ne vit dans la colonne \(C_j\) — ni avant ni après le retournement de Ligne : le mur est traversé, mais personne n’habitait contre.
D’où la caractérisation attendue : \(R_{(a,b)}\) transforme \(G\) si et seulement si une colonne des gains de Ligne contient \(\{a, b\}\)et cette colonne est la meilleure réponse de Colonne dans au moins une ligne (symétrique pour \(C_{(a,b)}\) : la ligne contenant \(\{a,b\}\) doit être la meilleure réponse de Ligne dans au moins une colonne). L’univers étant fini, tout cela se vérifie.
def colonne_contenant(game: OrdinalGame, r1: int, r2: int):"""Colonne (0 ou 1) dont les gains Ligne contiennent {r1, r2}, sinon None."""if {game.row_payoffs[0], game.row_payoffs[2]} == {r1, r2}:return0if {game.row_payoffs[1], game.row_payoffs[3]} == {r1, r2}:return1returnNonedef condition_naive(game: OrdinalGame, gen: str) ->bool:"""Conjecture naive : preserve ssi le joueur vise n'a pas {a,b} dans une colonne/ligne.""" player, r1, r2 = gen[0], int(gen[1]), int(gen[2])if player =="R":return colonne_contenant(game, r1, r2) isNoneif {game.col_payoffs[0], game.col_payoffs[1]} == {r1, r2}:returnFalseif {game.col_payoffs[2], game.col_payoffs[3]} == {r1, r2}:returnFalsereturnTruedef br_colonne(game: OrdinalGame) -> List[int]:"""Meilleure reponse de Colonne dans chaque ligne : [beta(0), beta(1)].""" _, B = game.to_matrices()return [0if B[0, 0] > B[0, 1] else1, 0if B[1, 0] > B[1, 1] else1]def br_ligne(game: OrdinalGame) -> List[int]:"""Meilleure reponse de Ligne dans chaque colonne : [rho(0), rho(1)].""" A, _ = game.to_matrices()return [0if A[0, 0] > A[1, 0] else1, 0if A[0, 1] > A[1, 1] else1]def condition_verifiee(game: OrdinalGame, gen: str) ->bool:""" Caracterisation etablie : gen preserve game ssi le joueur vise n'a PAS {a,b} dans une colonne/ligne, OU cette colonne/ligne n'est la meilleure reponse de l'AUTRE joueur dans AUCUNE ligne/colonne (le mur traverse n'est habite par personne). """ player, r1, r2 = gen[0], int(gen[1]), int(gen[2])if player =="R": j = colonne_contenant(game, r1, r2)return j isNoneor j notin br_colonne(game) i =Noneif {game.col_payoffs[0], game.col_payoffs[1]} == {r1, r2}: i =0elif {game.col_payoffs[2], game.col_payoffs[3]} == {r1, r2}: i =1return i isNoneor i notin br_ligne(game)# Etape 1 : la conjecture naive echoue -- mesurons son echecdesaccords_naif = [(g, gen) for g in all_games for gen in GENERATORSif est_morphisme_pour(g, gen) != condition_naive(g, gen)]print(f"Conjecture naive testee sur {576*6} paires : {len(desaccords_naif)} desaccords")g_ex, gen_ex = desaccords_naif[0]print(f" contre-exemple : R={g_ex.row_payoffs} C={g_ex.col_payoffs} + {gen_ex}")print(f" Nash avant {sorted(find_pure_nash(g_ex))} / apres "f"{sorted(find_pure_nash(apply_generator(g_ex, gen_ex)))}"f" -> mur traverse mais equilibres intacts")# Etape 2 : la condition verifiee predit-elle exactement les verdicts ?accords = desaccords =0for g in all_games:for gen in GENERATORS:if est_morphisme_pour(g, gen) == condition_verifiee(g, gen): accords +=1else: desaccords +=1print(f" DESACCORD : {g.row_payoffs}/{g.col_payoffs} + {gen}")print(f"\nCondition verifiee sur {576*6} paires : accords = {accords}, desaccords = {desaccords}")assert desaccords ==0, "la caracterisation echoue sur au moins une paire"print("La condition predit exactement les 3456 verdicts : theoreme etabli.")# Etape 3 : le compte se DERIVE au lieu d'etre constate# 16 permutations Ligne sur 24 ne placent pas {1,2} dans une meme colonne# (les partitions de {1,2,3,4} en deux paires de colonnes : {1,2}|{3,4} garde# la paire ensemble, {1,3}|{2,4} et {1,4}|{2,3} la separent -- 2 sur 3) -># preservation certaine. Pour les 8 autres, il faut que les deux meilleures# reponses de Colonne pointent hors de la colonne j* : 6 arrangements sur 24.from itertools import permutations as _permsans_paire = [p for p in _perm([1, 2, 3, 4])if {p[0], p[2]} != {1, 2} and {p[1], p[3]} != {1, 2}]print(f"\nDerivation pour R12 : {len(sans_paire)} permutations sans paire (sur 24)")print(f" -> {len(sans_paire)} x 24 = {len(sans_paire) *24} jeux preserves sans mur")print(f" + 8 permutations avec paire x 6 arrangements Colonne ou le mur est inhabite")print(f" = {len(sans_paire) *24} + {8*6} = {len(sans_paire) *24+8*6} jeux preserves attendus")mesure =sum(est_morphisme_pour(g, "R12") for g in all_games)print(f" mesure section 4 : {mesure}")
Conjecture naive testee sur 3456 paires : 288 desaccords
contre-exemple : R=(1, 3, 2, 4) C=(2, 4, 1, 3) + R12
Nash avant [(1, 1)] / apres [(1, 1)] -> mur traverse mais equilibres intacts
Condition verifiee sur 3456 paires : accords = 3456, desaccords = 0
La condition predit exactement les 3456 verdicts : theoreme etabli.
Derivation pour R12 : 16 permutations sans paire (sur 24)
-> 16 x 24 = 384 jeux preserves sans mur
+ 8 permutations avec paire x 6 arrangements Colonne ou le mur est inhabite
= 384 + 48 = 432 jeux preserves attendus
mesure section 4 : 432
Interprétation : le théorème vérifié — verdict explicite du grain D5
La scène s’est jouée en deux actes, et c’est elle-même un enseignement :
Acte 1 — la conjecture naïve meurt mesurée. « Couple adjacent en colonne ⟹ transformation » échoue sur 288 paires : tous les cas de murs inhabités. Sur un univers fini, une conjecture ne se discute pas, elle se compte.
Acte 2 — la condition vraie prédit les 3456 verdicts sans exception.\(R_{(a,b)}\) transforme \(G\) ssi la colonne qui contient \(\{a,b\}\) dans les gains de Ligne est aussi une colonne de meilleure réponse de Colonne. Et le compte s’en dérive : \(16 \times 24\) jeux sans mur + \(8 \times 6\) jeux au mur inhabité \(= 384 + 48 = 432\) — exactement la mesure de la section 4. Le compte n’est plus un constat, c’est une conséquence.
Verdict de l’exercice 3 de l’issue (explicité) : oui, il existe une condition simple sur le jeu de départ qui prédit la préservation. Elle n’est pas purement une condition sur le joueur visé (comme le suggérait l’intuition) : elle couple les deux tables — la géométrie des rangs d’un côté, la direction des meilleures réponses de l’autre. C’est plus riche que prévu, et c’est vérifié exhaustivement.
Relue en langage géométrique : traverser un mur ne change de chambre Nash que si le mur est habité — si aucune meilleure réponse de l’autre joueur ne pointe vers la colonne traversée, aucun équilibre ne vivait contre ce mur et le passage est stratégiquement invisible. La grammaire (swaps de rangs) et la géométrie (chambres/murs du grain D2) racontent la même histoire : les transitions se jouent là où les préférences des deux joueurs se rencontrent.
7. Exercice 3 : la condition survit-elle aux swaps non adjacents ?
Les six générateurs adjacents engendrent tout \(S_4 \times S_4\), mais la caractérisation de la section 6 a été établie et vérifiée sur eux seuls. La tester sur les swaps non adjacents\((1,3)\), \((1,4)\), \((2,4)\) est le vrai test de généralité : l’argument (un retournement n’a lieu que dans une colonne contenant les deux rangs échangés, visible seulement si le mur est habité) ne mentionne jamais l’adjacence — donc la même condition devrait tenir telle quelle : \(R_{(a,b)}\) transforme \(G\) ssi une colonne contient \(\{a,b\}\) et cette colonne est une meilleure réponse de Colonne. À vous de le mesurer — et de vérifier le compte attendu par la même dérivée combinatoire.
def preservations_swap_general(game: OrdinalGame, player: str, a: int, b: int) ->bool:# TODO etudiant : generaliser la preservation aux swaps NON adjacents# Etape 1 : ecrire le swap (a, b) quelconque cote player (R ou C)# Etape 2 : mesurer la preservation comme en section 2# Etape 3 : comparer avec la condition generalisee "aucune colonne/ligne ne contient {a,b}"# Indice : swap_payoffs accepte deja n'importe quelle paire de rangs ;# testez au minimum (1,3), (1,4), (2,4) -- les trois paires non adjacentes# Question bonus : chaque swap quelconque preserve-t-il aussi 384 jeux sur 576 ?# (la derivation combinatoire de la section 6 ne depend pas de l'adjacence)returnNone# TODO etudiant : remplacer# for paire in [(1, 3), (1, 4), (2, 4)]:# print(paire, preservations_swap_general(CLASSIC_GAMES["Stag Hunt"], "R", *paire))print("Exercice a completer : generalisation aux swaps non adjacents")
Exercice a completer : generalisation aux swaps non adjacents
8. Synthèse : la Loi III sur un univers fini
Ce que l’univers fini a permis de trancher, ce que le slogan seul ne pouvait pas :
La séparation est nette et massive. Sur 3456 paires (jeu, générateur), 2592 flèches préservent la signature de Nash et 864 la transforment. Aucun générateur n’est morphisme pour tout l’espace : l’espèce d’une flèche est une propriété de la paire, jamais du générateur seul ni du jeu seul.
La cause couple les deux tables. Un swap \(R_{(a,b)}\) transforme \(G\) si et seulement si une colonne des gains de Ligne contient \(\{a,b\}\)et cette colonne est une colonne de meilleure réponse de Colonne. La conjecture naïve (condition sur le seul joueur visé) est fausse — 288 paires de murs inhabités la réfutent, et l’argument réfuté puis corrigé est resté dans le notebook à dessein.
La géométrie et la grammaire coïncident. Les 100 points fixes de la grammaire (jeux préservés par les six générateurs) et les 16 jeux les plus exposés (quatre générateurs transformateurs) dessinent la topologie de l’espace ; les murs se traversent partout, mais seuls les murs habités changent la chambre Nash. La lecture Bruns-Kimmich (grain D2) devient computationnelle.
Prudence triviale. Les jeux sans équilibre pur préservent trivialement leur ensemble vide — la Loi III est substantielle sur les signatures non vides, et l’exercice 2 demande de quantifier cette réserve.
La strate 7 du voyage ICT (« certaines transformations changent le monde descriptif lui-même ») gagne ici son premier terrain calibré : changer les préférences n’est pas changer d’avis, et l’écart entre les deux se mesure — un quart de l’espace par pas de grammaire, seulement quand le mur traversé est habité.
9. Résumé
Deux espèces de flèches coexistent sur l’espace des 576 jeux 2x2 stricts : les swaps de rangs (Robinson-Goforth), qui transforment les préférences, et les morphismes de jeux (Tohmé-Viglizzo), qui transportent les équilibres. Un swap n’est morphisme que par paire.
Définition finie : \(g\) est morphisme pour \(G\) si \(\text{Nash}(G) = \text{Nash}(g(G))\) (identité sur les profils). Décidable par énumération : 3456 paires.
Comptes mesurés : chaque générateur adjacent préserve 432/576 jeux (75 %) et en transforme 144 ; 100 jeux sont des points fixes de la grammaire, 16 sont exposés à quatre générateurs.
Théorème établi : \(R_{(a,b)}\) transforme \(G\) ssi une colonne des gains de Ligne contient \(\{a, b\}\)et est meilleure réponse de Colonne dans au moins une ligne (symétrique pour \(C\)). Vérifié sur les 3456 paires ; le compte 432 s’en dérive (\(16 \times 24\) sans mur \(+ 8 \times 6\) au mur inhabité).
Contre-exemples exposés : Stag Hunt et Chicken perdent strictement un équilibre (destruction sans compensation) ; le Dilemme ne fait que déplacer le sien ; son \(R_{34}\) traverse un mur inhabité sans rien changer.
Lien aval : la lecture chambres/murs (grain D2, #12213) est la même structure vue géométriquement ; le chemin minimal certifié (grain D3, #12205 en cible Lean) circulera dans ce graphe dont on sait maintenant quels arcs changent la chambre Nash.
A retenir :la question « transformation ou morphisme ? » n’a de sens que flèche par flèche — et sur un univers fini, chaque flèche se tranche. Le mur ne compte que s’il est habité.
Sources, attribution et dettes de vérification
Robinson, D. & Goforth, D. (2005), The Topology of the 2×2 Games — l’espace des jeux ordinaux stricts et ses swaps élémentaires. La représentation informatique et les 576 jeux viennent de GameTheory-03-Topology2x2-Python.ipynb.
Tohmé, F. & Viglizzo, I. — catégories concrètes de jeux (Gam_I, Gam) pour l’espèce préservante. Cadrage cité depuis le chantier 4 (#12207) ; le présent notebook réduit la question morphique au test d’égalité des ensembles de Nash sous l’identité sur les profils — une instanciation, pas la théorie catégorique générale.
Bruns, B. — transmutations et lecture chambres/murs, développées dans le grain D2 (#12213).
Dettes de vérification héritées du chantier 4 (statut RAPPORTÉ, non établi ici) : le quotient 576 → 144 et le tore à 37 trous sont cités par la source sans dérivation ; ce notebook travaille sur les 576 jeux complets (sur ensemble du quotient, donc sans le supposer) et le compte par générateur en est indépendant. Les trois références arXiv du chantier n’ont pas été ouvertes firsthand.
See #12237 · See #12207 (chantier 4) · See #12213 (grain D2, chambres et murs)