GameTheory-6 : Évolution et Confiance — Twin C# (tournoi d’Axelrod from-scratch)

Twin C# (.NET Interactive) de GameTheory-06-EvolutionTrust-Python — marathon #4956 (parité .NET ⇄ Python), volet GameTheory théorie évolutive, axe-2 SOTA #3801 Prong B.

Le notebook Python invoque la librairie axelrod (boîte noire regroupant 200+ stratégies de Dilemme du Prisonnier Itéré) et numpy/matplotlib pour les tournois et la dynamique du réplicateur. Le cœur pédagogique de ce twin déroule tout from-scratch (BCL .NET 9, sans NuGet) : moteur IPD, stratégies classiques, tournoi round-robin d’Axelrod, matrice de gains et dynamique du réplicateur (intégrale d’Euler). L’annexe invoque ensuite le vrai moteur axelrod via PythonNet pour vérifier que la référence SOTA reste atteignable depuis .NET.

Objectifs d’apprentissage

  1. Modéliser le Dilemme du Prisonnier Itéré (IPD) : matrice T>R>P>S, condition 2R > T+S.
  2. Implémenter des stratégies classiques (Tit-for-Tat, Grudger, Pavlov, AlwaysC/D, Random).
  3. Organiser un tournoi round-robin (toutes paires, score moyen par match) — l’expérience d’Axelrod (1980).
  4. Étudier la dynamique du réplicateur : comment les fréquences de stratégies évoluent sous sélection (fitness = gain moyen contre la population).
  5. Comprendre pourquoi Tit-for-Tat (gagnant d’Axelrod) est robuste : ni exploitable, ni exploiteur.

Introduction

Le Dilemme du Prisonnier Itéré

Deux joueurs choisissent simultanément Coopérer (C) ou Déserter (D). La matrice de gain (convention Axelrod) :

C D
C R, R S, T
D T, S P, P

avec T (Temptation) > R (Reward) > P (Punishment) > S (Sucker), et la condition 2R > T + S (la coopération mutuelle est préférable à l’alternance). Joué une fois, la défection domine (D est strictement meilleure). Mais itéré indéfiniment, la coopération peut émerger : un joueur peut punir la défection future.

Le tournoi d’Axelrod (1980)

Robert Axelrod a invité des théoriciens à soumettre des stratégies IPD et les a fait s’affronter en tournoi round-robin. Le gagnant, Tit-for-Tat (Anatol Rapoport) — coopère au premier tour, puis imite le dernier coup de l’adversaire — est la stratégie la plus simple et la plus robuste : elle est nice (ne désert jamais la première), retaliatory (punie la défection), forgiving (revient à la coopération), clear (compréhensible).

Dynamique du réplicateur

Dans une population où chaque stratégie a une fréquence \(x_i\), la fitness de \(i\) = son gain moyen contre la population : \(f_i = (M x)_i\) où \(M\) est la matrice de gains. La sélection fait croître les stratégies au-dessus de la moyenne :

\[\dot{x}_i = x_i (f_i - \bar{f}), \quad \bar{f} = x^\top M x\]

Intégrée par Euler (\(x \leftarrow x + dt \cdot \dot{x}\)), cette ODE révèle quels attracteurs (stratégies ou mélanges stables) la sélection atteint.

Complémentarité (#3801 Prong B)

Aspect Python (twin) Twin C# (ici)
Stratégies axelrod (200+ précodées) 6 stratégies from-scratch
Tournoi axelrod.Tournament round-robin manuel
Replicator axelrod/numpy Euler ODE from-scratch
Visualisation matplotlib + animation tables console + ASCII
Dépendances axelrod, numpy, matplotlib cœur BCL sans NuGet ; annexe PythonNet

1. Moteur du Dilemme du Prisonnier Itéré

On définit la matrice de gains (T=5, R=3, P=1, S=0) et la fonction Payoff(a1, a2) qui retourne les gains des deux joueurs. On vérifie les deux conditions structurelles d’un dilemme.

// Cellule 1 — Moteur IPD (persistant cross-cell kernel .net-csharp)
using System;
using System.Collections.Generic;
using System.Linq;

public enum Act { C, D }   // Cooperate, Defect

public static class IPD
{
    public const int T = 5, R = 3, P = 1, S = 0;   // convention Axelrod

    public static (int p1, int p2) Payoff(Act a1, Act a2) => (a1, a2) switch
    {
        (Act.C, Act.C) => (R, R),
        (Act.C, Act.D) => (S, T),
        (Act.D, Act.C) => (T, S),
        _              => (P, P),
    };
}

// Verifications structurelles d'un dilemme
$"Matrice : T={IPD.T}, R={IPD.R}, P={IPD.P}, S={IPD.S}".Display();
$"Condition T > R > P > S : {IPD.T > IPD.R && IPD.R > IPD.P && IPD.P > IPD.S}".Display();
$"Condition 2R > T + S    : {2*IPD.R > IPD.T + IPD.S} ({2*IPD.R} > {IPD.T + IPD.S})".Display();
"Si les deux conditions tiennent, la cooperation mutuelle (R,R) est Pareto-optimale et stable a l'itere.".Display();
Matrice : T=5, R=3, P=1, S=0
Condition T > R > P > S : True
Condition 2R > T + S    : True (6 > 5)
Si les deux conditions tiennent, la cooperation mutuelle (R,R) est Pareto-optimale et stable a l'itere.

Lecture ancrée : La matrice de gains T=5, R=3, P=1, S=0 satisfait les conditions 2R > T+S et T > R > P > S, validant que la coopération mutuelle (R,R) est à la fois Pareto-optimale et stable itérativement.

2. Stratégies classiques

Une stratégie IPD = un état interne (historique des coups) + une règle Choose(). On implémente les 6 stratégies fondatrices du tournoi d’Axelrod :

Stratégie Règle
AlwaysCooperate (Colombe) Toujours C
AlwaysDefect (Faucon) Toujours D
TitForTat (gagnante Axelrod) C au 1er tour, puis imite le dernier coup adverse
SuspiciousTFT D au 1er tour, puis imite
Grudger (Rancunier) C jusqu’à la 1ère défection adverse, puis D pour toujours
Pavlov (Win-Stay-Lose-Shift) Garde son coup s’il a bien payé, change sinon
Random C ou D équiprobable
// Cellule 2 — Stratégies IPD (classe abstraite + 7 implémentations)
public abstract class Strategy
{
    public string Name { get; }
    protected List<Act> Mine = new();
    protected List<Act> Foe  = new();
    protected Strategy(string name) { Name = name; }
    public virtual void Reset() { Mine.Clear(); Foe.Clear(); }
    public void Update(Act mine, Act foe) { Mine.Add(mine); Foe.Add(foe); }
    public abstract Act Choose();
    public abstract Func<Strategy> Factory { get; }   // cree une instance fraiche (etat vierge)
    public override string ToString() => Name;
}

public class AlwaysCooperate : Strategy { public AlwaysCooperate() : base("AlwaysCooperate") {}
    public override Act Choose() => Act.C;
    public override Func<Strategy> Factory => () => new AlwaysCooperate(); }

public class AlwaysDefect : Strategy { public AlwaysDefect() : base("AlwaysDefect") {}
    public override Act Choose() => Act.D;
    public override Func<Strategy> Factory => () => new AlwaysDefect(); }

public class TitForTat : Strategy { public TitForTat() : base("TitForTat") {}
    public override Act Choose() => Foe.Count == 0 ? Act.C : Foe[^1];
    public override Func<Strategy> Factory => () => new TitForTat(); }

public class SuspiciousTFT : Strategy { public SuspiciousTFT() : base("SuspiciousTFT") {}
    public override Act Choose() => Foe.Count == 0 ? Act.D : Foe[^1];
    public override Func<Strategy> Factory => () => new SuspiciousTFT(); }

public class Grudger : Strategy { public Grudger() : base("Grudger") {}
    public override Act Choose() => Foe.Any(a => a == Act.D) ? Act.D : Act.C;
    public override Func<Strategy> Factory => () => new Grudger(); }

public class Pavlov : Strategy {   // Win-Stay-Lose-Shift
    public Pavlov() : base("Pavlov") {}
    public override Act Choose() {
        if (Mine.Count == 0) return Act.C;
        var (my, _) = IPD.Payoff(Mine[^1], Foe[^1]);
        return my >= IPD.R ? Mine[^1] : (Mine[^1] == Act.C ? Act.D : Act.C);   // garde si bien paye, sinon change
    }
    public override Func<Strategy> Factory => () => new Pavlov();
}

public class RandomStrat : Strategy {
    private readonly Random _rng;
    public RandomStrat(int seed = 42) : base("Random") { _rng = new Random(seed); }
    public override Act Choose() => _rng.Next(2) == 0 ? Act.C : Act.D;
    public override Func<Strategy> Factory => () => new RandomStrat(42);   // seed fixe pour reproductibilite
}

display("7 strategies IPD definies : AlwaysCooperate, AlwaysDefect, TitForTat, SuspiciousTFT, Grudger, Pavlov, Random.");
7 strategies IPD definies : AlwaysCooperate, AlwaysDefect, TitForTat, SuspiciousTFT, Grudger, Pavlov, Random.

Lecture ancrée : Les 7 stratégies implémentées (AlwaysCooperate, AlwaysDefect, TitForTat, SuspiciousTFT, Grudger, Pavlov, Random) couvrent les comportements canoniques du dilemme du prisonnier itéré.

3. Simulation d’un match

Un match = N tours. À chaque tour : les deux stratégies Choose() simultanément, on calcule les gains via Payoff, puis chaque stratégie Update() son historique avec le coup adverse (le bruit optionnel inverse une action avec probabilité noise). Le score cumulé détermine le gagnant — plus le score est haut, meilleur c’est (convention Axelrod : on maximise le gain total). La déception exploite les coopérateurs sur un match (T=5) mais, face à un retaliator (TitForTat, Grudger), stagne à P=1 sans atteindre R=3 — d’où le paradoxe où la coopération l’emporte au score cumulé (cf. §10).

// Cellule 3 — play_match (avec bruit optionnel)
public static (int s1, int s2, List<(Act a1,Act a2)> trace) PlayMatch(Strategy s1, Strategy s2, int rounds=200, double noise=0.0, int seed=0)
{
    s1.Reset(); s2.Reset();
    var rng = noise > 0 ? new Random(seed) : null;
    int score1 = 0, score2 = 0;
    var trace = new List<(Act, Act)>();
    for (int t = 0; t < rounds; t++)
    {
        Act a1 = s1.Choose(), a2 = s2.Choose();
        if (rng != null)
        {
            if (rng.NextDouble() < noise) a1 = a1 == Act.C ? Act.D : Act.C;
            if (rng.NextDouble() < noise) a2 = a2 == Act.C ? Act.D : Act.C;
        }
        var (p1, p2) = IPD.Payoff(a1, a2);
        score1 += p1; score2 += p2;
        s1.Update(a1, a2); s2.Update(a2, a1);
        trace.Add((a1, a2));
    }
    return (score1, score2, trace);
}

// Demo : TitForTat vs AlwaysDefect (TFT se fait exploiter au 1er tour, puis puni)
var (sa, sb, _) = PlayMatch(new TitForTat(), new AlwaysDefect(), rounds: 200);
$"TitForTat vs AlwaysDefect (200 tours) : TFT={sa}, AllD={sb}".Display();
$"Gain moyen/tour : TFT={sa/200.0:F2}, AllD={sb/200.0:F2}".Display();
$"-> AllD gagne le match (score plus haut), mais TFT limite les degats grace a la retaliation.".Display();
TitForTat vs AlwaysDefect (200 tours) : TFT=199, AllD=204
Gain moyen/tour : TFT=0,99, AllD=1,02
-> AllD gagne le match (score plus haut), mais TFT limite les degats grace a la retaliation.

Lecture ancrée : Le match TitForTat vs AlwaysDefect montre que bien qu’AlwaysDefect gagne le match individuel (204 vs 199), TitForTat limite les dégâts avec un gain moyen de 0,99 par tour contre 1,02, illustrant la résilience de la réciprocité.

4. Trace d’un match (visualisation ASCII)

Le twin Python trace les coups en matplotlib (step plot). En C# BCL, on affiche le trace textuel tour par tour : la séquence des actions des deux stratégies + le score cumulé. On observe le pattern caractéristique TitForTat vs SuspiciousTFT (décalage d’un tour → cascade de défections).

// Cellule 4 — Trace ASCII d'un match
public static string MatchTrace(Strategy s1, Strategy s2, int rounds=20)
{
    var (sc1, sc2, trace) = PlayMatch(s1, s2, rounds);
    var sb = new System.Text.StringBuilder();
    sb.AppendLine($" Tour | {s1.Name,-16} | {s2.Name,-16} | Gain cumule ({s1.Name}/{s2.Name})");
    sb.AppendLine(new string('-', 66));
    int c1 = 0, c2 = 0;
    for (int t = 0; t < trace.Count; t++)
    {
        var (a1, a2) = trace[t];
        var (p1, p2) = IPD.Payoff(a1, a2);
        c1 += p1; c2 += p2;
        sb.AppendLine($" {t+1,4} | {(a1==Act.C?"C":"D"),-16} | {(a2==Act.C?"C":"D"),-16} | {c1,3} / {c2,3}");
    }
    sb.AppendLine(new string('-', 66));
    sb.AppendLine($" TOTAL {s1.Name}={sc1} (moy {sc1/(double)rounds:F2}/tour)  |  {s2.Name}={sc2} (moy {sc2/(double)rounds:F2}/tour)");
    return sb.ToString();
}

display("Trace TitForTat vs SuspiciousTFT (20 tours) :");
MatchTrace(new TitForTat(), new SuspiciousTFT(), rounds: 20).Display();
"-> SuspiciousTFT commence par D, TFT repond D au tour 2, et les deux s'enferment en defection mutuelle (P,P).".Display();
Trace TitForTat vs SuspiciousTFT (20 tours) :
 Tour | TitForTat        | SuspiciousTFT    | Gain cumule (TitForTat/SuspiciousTFT)
------------------------------------------------------------------
    1 | C                | D                |   0 /   5
    2 | D                | C                |   5 /   5
    3 | C                | D                |   5 /  10
    4 | D                | C                |  10 /  10
    5 | C                | D                |  10 /  15
    6 | D                | C                |  15 /  15
    7 | C                | D                |  15 /  20
    8 | D                | C                |  20 /  20
    9 | C                | D                |  20 /  25
   10 | D                | C                |  25 /  25
   11 | C                | D                |  25 /  30
   12 | D                | C                |  30 /  30
   13 | C                | D                |  30 /  35
   14 | D                | C                |  35 /  35
   15 | C                | D                |  35 /  40
   16 | D                | C                |  40 /  40
   17 | C                | D                |  40 /  45
   18 | D                | C                |  45 /  45
   19 | C                | D                |  45 /  50
   20 | D                | C                |  50 /  50
------------------------------------------------------------------
 TOTAL TitForTat=50 (moy 2,50/tour)  |  SuspiciousTFT=50 (moy 2,50/tour)
-> SuspiciousTFT commence par D, TFT repond D au tour 2, et les deux s'enferment en defection mutuelle (P,P).

Lecture ancrée : La trace ASCII révèle qu’une seule défection initiale (SuspiciousTFT commence par D) peut déclencher une cascade de défection mutuelle, démontrant la fragilité de la coopération face aux erreurs.

5. Tournoi round-robin d’Axelrod

Le tournoi fait s’affronter toutes les paires (y compris soi-vs-soi) sur N tours. Le score d’une stratégie = moyenne par match. Le classement révèle quelles stratégies réussissent collectivement — pas en exploitant, mais en atteignant la coopération mutuelle (R,R) souvent. C’est pourquoi TitForTat gagne : elle coopère avec les cooperateurs (R,R) et limite les degats contre les défecteurs.

// Cellule 5 — Tournoi round-robin
public static Dictionary<string,double> RunTournament(List<Strategy> strats, int rounds=200, double noise=0.0, int repetitions=1)
{
    var totals = new Dictionary<string,double>();
    var counts = new Dictionary<string,int>();
    foreach (var s in strats) { totals[s.Name]=0; counts[s.Name]=0; }
    var rngSeed = 1;
    for (int rep = 0; rep < repetitions; rep++)
    {
        for (int i = 0; i < strats.Count; i++)
        for (int j = 0; j < strats.Count; j++)
        {
            // instances fraiches (etat interne reinitialise) via la fabrique de chaque strategie
            var s1 = strats[i].Factory();
            var s2 = strats[j].Factory();
            var (sc, _, _) = PlayMatch(s1, s2, rounds, noise, rngSeed++);
            totals[strats[i].Name] += sc;
            counts[strats[i].Name] += 1;
        }
    }
    // score moyen par match
    var avg = new Dictionary<string,double>();
    foreach (var k in totals.Keys) avg[k] = totals[k] / counts[k];
    return avg;
}

public static string DisplayTournament(Dictionary<string,double> results, string title)
{
    var ranked = results.OrderByDescending(x => x.Value).ToList();   // score le plus HAUT = meilleur (convention Axelrod : maximiser le gain)
    var sb = new System.Text.StringBuilder();
    sb.AppendLine($"=== {title} ===");
    sb.AppendLine($" Rang | Strategie            | Score moyen/match  (plus HAUT = meilleur, convention Axelrod)");
    sb.AppendLine(new string('-', 64));
    int rank = 1;
    foreach (var kv in ranked)
        sb.AppendLine($" {rank++,3}  | {kv.Key,-20} | {kv.Value,10:F1}");
    return sb.ToString();
}

var tournamentStrats = new List<Strategy> {
    new AlwaysCooperate(), new AlwaysDefect(), new TitForTat(),
    new SuspiciousTFT(), new Grudger(), new Pavlov(), new RandomStrat()
};

var results = RunTournament(tournamentStrats, rounds: 200);
DisplayTournament(results, "Tournoi sans bruit (200 tours/match)").Display();
"Note : au IPD le score le plus HAUT gagne (convention Axelrod). TitForTat gagne en atteignant souvent la cooperation mutuelle (R=3,R=3), meme si AlwaysDefect la bat sur le match individuel (T=5 > S=0).".Display();
=== Tournoi sans bruit (200 tours/match) ===
 Rang | Strategie            | Score moyen/match  (plus HAUT = meilleur, convention Axelrod)
----------------------------------------------------------------
   1  | TitForTat            |      508,6
   2  | Grudger              |      489,3
   3  | Pavlov               |      482,6
   4  | AlwaysCooperate      |      473,6
   5  | AlwaysDefect         |      433,1
   6  | Random               |      393,1
   7  | SuspiciousTFT        |      366,9
Note : au IPD le score le plus HAUT gagne (convention Axelrod). TitForTat gagne en atteignant souvent la cooperation mutuelle (R=3,R=3), meme si AlwaysDefect la bat sur le match individuel (T=5 > S=0).

Lecture ancrée : Le tournoi confirme que TitForTat domine en atteignant la coopération mutuelle (R=3) plus fréquemment, bien qu’AlwaysDefect la batte sur un match direct, validant le résultat historique d’Axelrod.

Tournoi avec bruit

Avec du bruit (5% d’erreurs de transmission), une coopération peut être perçue comme une défection, déclenchant des cascades de représailles. Résultat contre-intuitif mais documenté : le bruit pénalise davantage les stratégies “nice + retaliatory” (TitForTat, AlwaysCooperate) — les erreurs les entraînent dans des cycles de punition — tandis que les stratégies intrinsèquement défectives (AlwaysDefect, Grudger) s’en sortent relativement mieux, leurs actions étant peu sensibles au retournement. Ce phénomène motive les variantes clémentes comme GenerousTitForTat (exercice 2).

// Cellule 6 — Tournoi avec bruit (5% d'erreurs)
var resultsNoisy = RunTournament(tournamentStrats, rounds: 200, noise: 0.05, repetitions: 5);
DisplayTournament(resultsNoisy, "Tournoi avec bruit (5% erreurs, moyenne sur 5 repetitions)").Display();

display("Comparaison sans bruit vs bruite :");
var sb6 = new System.Text.StringBuilder();
sb6.AppendLine($" Strategie            | sans bruit | avec bruit | variation");
sb6.AppendLine(new string('-', 56));
foreach (var s in tournamentStrats)
{
    double a = results[s.Name], b = resultsNoisy[s.Name];
    sb6.AppendLine($" {s.Name,-20} | {a,10:F1} | {b,10:F1} | {(b-a),+9:F1}");
}
sb6.ToString().Display();
"-> Les ecarts (variation) confirment : TitForTat (-97,5) et AlwaysCooperate (-110,6) chutent fort sous le bruit (cascades de punition), alors qu'AlwaysDefect (+13,1) beneficie du chaos. Motive GenerousTitForTat (exercice 2).".Display();
=== Tournoi avec bruit (5% erreurs, moyenne sur 5 repetitions) ===
 Rang | Strategie            | Score moyen/match  (plus HAUT = meilleur, convention Axelrod)
----------------------------------------------------------------
   1  | Grudger              |      452,4
   2  | AlwaysDefect         |      446,2
   3  | Pavlov               |      426,0
   4  | TitForTat            |      411,1
   5  | SuspiciousTFT        |      408,6
   6  | Random               |      399,5
   7  | AlwaysCooperate      |      363,0
Comparaison sans bruit vs bruite :
 Strategie            | sans bruit | avec bruit | variation
--------------------------------------------------------
 AlwaysCooperate      |      473,6 |      363,0 |    -110,6
 AlwaysDefect         |      433,1 |      446,2 |      13,1
 TitForTat            |      508,6 |      411,1 |     -97,5
 SuspiciousTFT        |      366,9 |      408,6 |      41,8
 Grudger              |      489,3 |      452,4 |     -36,9
 Pavlov               |      482,6 |      426,0 |     -56,5
 Random               |      393,1 |      399,5 |       6,4
-> Les ecarts (variation) confirment : TitForTat (-97,5) et AlwaysCooperate (-110,6) chutent fort sous le bruit (cascades de punition), alors qu'AlwaysDefect (+13,1) beneficie du chaos. Motive GenerousTitForTat (exercice 2).

6. Matrice de gains

\(M[i,j]\) = gain moyen par tour de la stratégie \(i\) contre la stratégie \(j\). Cette matrice est l’ingrédient clé de la dynamique du réplicateur : elle donne la fitness de chaque stratégie face à n’importe quelle population.

// Cellule 7 — Matrice de gains M[i,j]
public static double[,] PayoffMatrix(List<Strategy> strats, int rounds=200)
{
    int n = strats.Count;
    var M = new double[n,n];
    for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++)
    {
        var s1 = strats[i].Factory();
        var s2 = strats[j].Factory();
        var (sc, _, _) = PlayMatch(s1, s2, rounds);
        M[i,j] = sc / (double)rounds;   // gain moyen par tour
    }
    return M;
}

var M = PayoffMatrix(tournamentStrats, rounds: 200);
var sb7 = new System.Text.StringBuilder();
sb7.Append($"               ");
foreach (var s in tournamentStrats) sb7.Append($"{s.Name.Substring(0,Math.Min(7,s.Name.Length)),8} ");
sb7.AppendLine();
for (int i = 0; i < tournamentStrats.Count; i++)
{
    sb7.Append($"{tournamentStrats[i].Name.Substring(0,Math.Min(13,tournamentStrats[i].Name.Length)),-14} ");
    for (int j = 0; j < tournamentStrats.Count; j++)
        sb7.Append($"{M[i,j],8:F2} ");
    sb7.AppendLine();
}
"Matrice de gains M[i,j] (gain moyen/tour de i contre j) :".Display();
sb7.ToString().Display();
Matrice de gains M[i,j] (gain moyen/tour de i contre j) :
                AlwaysC  AlwaysD  TitForT  Suspici  Grudger   Pavlov   Random 
AlwaysCoopera      3,00     0,00     3,00     2,98     3,00     3,00     1,59 
AlwaysDefect       5,00     1,00     1,02     1,00     1,02     3,00     3,12 
TitForTat          3,00     0,99     3,00     2,50     3,00     3,00     2,31 
SuspiciousTFT      3,01     1,00     2,50     1,00     1,01     2,00     2,31 
Grudger            3,00     0,99     3,00     1,01     3,00     3,00     3,12 
Pavlov             3,00     0,50     3,00     2,00     3,00     3,00     2,38 
Random             3,94     0,47     2,33     2,31     0,49     2,16     2,06 

Lecture ancrée : La matrice M[i,j] montre que TitForTat obtient des gains élevés contre les stratégies coopératives (AlwaysCooperate, Pavlov) tout en limitant les pertes contre les stratégies agressives.

7. Dynamique du réplicateur

Dans une population mixte de stratégies (fréquences \(x_i\)), la fitness de \(i\) = son gain moyen contre la population courante : \(f_i = (M x)_i\). La sélection favorise les stratégies au-dessus de la moyenne \(\bar{f} = x^\top M x\). L’ODE du réplicateur :

\[\dot{x}_i = x_i (f_i - \bar{f})\]

On l’intègre par Euler (\(x \leftarrow x + dt \cdot \dot{x}\), renormalisé pour rester une distribution). On observe quels attracteurs la sélection atteint : une stratégie envahit-elle (fixation) ou un mélange stable émerge-t-il ?

// Cellule 8 — Dynamique du replicateur (Euler ODE)
public static List<double[]> ReplicatorDynamics(double[,] M, double[] x0, int steps=300, double dt=0.05)
{
    int n = x0.Length;
    var traj = new List<double[]> { (double[])x0.Clone() };
    var x = (double[])x0.Clone();
    for (int s = 0; s < steps; s++)
    {
        // fitness f_i = sum_j M[i,j] * x_j
        var f = new double[n];
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                f[i] += M[i,j] * x[j];
        // fitness moyenne bar_f = sum_i x_i * f_i
        double avg = 0;
        for (int i = 0; i < n; i++) avg += x[i] * f[i];
        // dx_i = x_i * (f_i - avg) * dt
        for (int i = 0; i < n; i++) x[i] += x[i] * (f[i] - avg) * dt;
        // renormalisation (rester une distribution)
        double sum = x.Sum();
        if (sum <= 0) break;
        for (int i = 0; i < n; i++) x[i] /= sum;
        traj.Add((double[])x.Clone());
    }
    return traj;
}

// Population initiale : equipartition des 7 strategies
var x0 = Enumerable.Repeat(1.0/7, 7).ToArray();
var traj = ReplicatorDynamics(M, x0, steps: 400, dt: 0.05);

$"Evolution des frequences (population initiale equipartie 1/7) :".Display();
var sb8 = new System.Text.StringBuilder();
sb8.Append($" Etape | ");
foreach (var s in tournamentStrats) sb8.Append($"{s.Name.Substring(0,Math.Min(6,s.Name.Length)),7} ");
sb8.AppendLine();
sb8.AppendLine(new string('-', 16 + 7*7));
foreach (var step in new[] { 0, 50, 100, 200, 400 })
{
    if (step >= traj.Count) continue;
    var x = traj[step];
    sb8.Append($"{step,5}  | ");
    for (int i = 0; i < x.Length; i++) sb8.Append($"{x[i],7:F2} ");
    sb8.AppendLine();
}
sb8.ToString().Display();

var final = traj[^1];
var dominant = tournamentStrats[Array.IndexOf(final, final.Max())].Name;
$"-> Strategie dominante a l'equilibre : {dominant} (frequence {final.Max():F2})".Display();
Evolution des frequences (population initiale equipartie 1/7) :
 Etape |  Always  Always  TitFor  Suspic  Grudge  Pavlov  Random 
-----------------------------------------------------------------
    0  |    0,14    0,14    0,14    0,14    0,14    0,14    0,14 
   50  |    0,18    0,08    0,25    0,04    0,22    0,20    0,05 
  100  |    0,19    0,02    0,30    0,01    0,26    0,22    0,01 
  200  |    0,19    0,00    0,31    0,00    0,27    0,23    0,00 
  400  |    0,19    0,00    0,31    0,00    0,27    0,23    0,00 
-> Strategie dominante a l'equilibre : TitForTat (frequence 0,31)

Lecture ancrée : La dynamique du replicateur s’arrête sur TitForTat à la fréquence 0,31 dans une population initialement équipartie — non parce qu’elle domine, mais parce qu’elle appartient à la clique neutre où TitForTat, Grudger et Pavlov font exactement le même score. C’est un point de repos sur une face neutre, pas une victoire.

7bis. La stabilité testée : ESS, NSS et point de repos (#12472)

La cellule précédente observe la dynamique et nomme une « stratégie dominante » — mais un état final n’est pas une preuve de stabilité. La théorie des jeux évolutionnaires sépare trois notions que la trajectoire seule ne distingue pas :

  • ESS (Evolutionarily Stable Strategy, Maynard Smith & Price 1973) : la stratégie \(i\) résiste à tout mutant \(j\) — soit \(M[i,i] > M[j,i]\) (le mutant fait strictement moins bien que la résidente contre sa propre population), soit, à l’égalité \(M[i,i] = M[j,i]\), \(M[i,j] > M[j,j]\) (la résidente se défend mieux contre le mutant que le mutant contre lui-même) ;
  • NSS (Neutrally Stable Strategy) : égalité parfaite sur les deux axes — le mutant ne perd ni ne gagne, il coexiste ;
  • Point de repos du réplicateur : la trajectoire s’arrête (\(\dot{x} = 0\)). Toute NSS en est un, mais un point de repos peut aussi n’être que l’endroit où le budget de simulation s’est arrêté.

Le critère se code : c’est le test qui manquait à la série (ESS n’apparaissait qu’en prose, jamais en exécution — mesure de l’issue #12472 : is_ess absent, code=0).

// Cellule 8a — Critere ESS de Maynard Smith (#12472) : le test qui manquait a la serie.
// i est ESS ssi pour TOUT mutant j != i :
//   (1) M[i,i] > M[j,i]                          (mutant strictement domine contre la residente)
//   (2) ou M[i,i] == M[j,i] et M[i,j] > M[j,j]   (egalite, mais i bat le mutant en duel)
// Trois cas de sortie, a ne pas confondre :
//   - M[j,i] > M[i,i]                 : envahisseur strict (meilleur contre la population residente)
//   - egalite residente + duel perdu  : envahisseur AUSSI -- en population mixte (1-e)i + e*j,
//     fitness_j - fitness_i = e * (M[j,j] - M[i,j]) > 0 pour tout e > 0
//   - egalite PARFAITE sur les deux axes : mutant NEUTRE -> NSS, pas ESS
public static (bool isEss, List<string> neutral, List<string> invaders) IsEss(
    double[,] M, string[] names, int i, double tol = 1e-9)
{
    int n = names.Length;
    var neutral = new List<string>();
    var invaders = new List<string>();
    for (int j = 0; j < n; j++)
    {
        if (j == i) continue;
        if (M[j,i] > M[i,i] + tol) { invaders.Add(names[j]); continue; }   // envahisseur strict
        if (Math.Abs(M[i,i] - M[j,i]) >= tol) continue;                    // cas (1) : mutant domine
        if (M[i,j] > M[j,j] + tol) continue;                               // cas (2) : duel gagne
        if (Math.Abs(M[i,j] - M[j,j]) < tol) neutral.Add(names[j]);        // egalite parfaite : neutre
        else invaders.Add(names[j]);                                       // egalite residente, duel perdu
    }
    return (invaders.Count == 0 && neutral.Count == 0, neutral, invaders);
}

var names7 = tournamentStrats.Select(s => s.Name).ToArray();
var sbEss = new System.Text.StringBuilder();
sbEss.AppendLine(" Strategie           | ESS ? | detail du test");
sbEss.AppendLine(new string('-', 84));
foreach (var (s, i) in tournamentStrats.Select((s, i) => (s, i)))
{
    var (ess, neutral, invaders) = IsEss(M, names7, i);
    string detail = ess
        ? "resiste a tout mutant"
        : "envahi par : " + (invaders.Count > 0 ? string.Join(", ", invaders) : "-")
          + (neutral.Count > 0 ? " ; mutant(s) neutre(s) : " + string.Join(", ", neutral) : "");
    sbEss.AppendLine($" {s.Name,-19} | {(ess ? "OUI" : "NON"),4} | {detail}");
}
"Verdict ESS (Maynard Smith) sur la matrice M de la cellule 7 :".Display();
sbEss.ToString().Display();
Verdict ESS (Maynard Smith) sur la matrice M de la cellule 7 :
 Strategie           | ESS ? | detail du test
------------------------------------------------------------------------------------
 AlwaysCooperate     |  NON | envahi par : AlwaysDefect, SuspiciousTFT, Random ; mutant(s) neutre(s) : TitForTat, Grudger, Pavlov
 AlwaysDefect        |  NON | envahi par : - ; mutant(s) neutre(s) : SuspiciousTFT
 TitForTat           |  NON | envahi par : - ; mutant(s) neutre(s) : AlwaysCooperate, Grudger, Pavlov
 SuspiciousTFT       |  NON | envahi par : AlwaysCooperate, TitForTat, Grudger, Pavlov, Random ; mutant(s) neutre(s) : AlwaysDefect
 Grudger             |  NON | envahi par : - ; mutant(s) neutre(s) : AlwaysCooperate, TitForTat, Pavlov
 Pavlov              |  NON | envahi par : AlwaysDefect ; mutant(s) neutre(s) : AlwaysCooperate, TitForTat, Grudger
 Random              |  NON | envahi par : AlwaysDefect, TitForTat, SuspiciousTFT, Grudger, Pavlov

Lecture du verdict — aucune ESS sur ce pool, et c’est la clique neutre qui survit

Deux enseignements dans ce tableau :

  1. Aucune des 7 stratégies n’est ESS sur ce pool. AlwaysCooperate est strictement envahie (AlwaysDefect tire 5.00 contre elle, Random 3.94, SuspiciousTFT 3.01) ; TitForTat, Grudger et Pavlov échouent chacune sur des mutants neutres — elles font toutes exactement 3.00 entre elles et contre AlwaysCooperate, égalité parfaite sur les deux axes du critère : NSS, pas ESS. Le cas Pavlov illustre la troisième branche du test : AlwaysDefect fait égalité contre sa population (3.00 partout) mais gagne le duel (\(u(AD,AD) = 1.00 > u(Pav,AD) = 0.50\)) — en population mixte, la différence de fitness vaut \(\varepsilon \cdot (1.00 - 0.50) > 0\) pour tout \(\varepsilon\) : c’est un envahisseur, pas un neutre.
  2. Les 4 survivantes du réplicateur (cellule 8 : AlwaysCooperate 0.19, TitForTat 0.31, Grudger 0.27, Pavlov 0.23) sont exactement cette clique neutre. Ce n’est pas un hasard : entre stratégies mutuellement neutres, \(f_i = f_j\) sur la face, donc \(\dot{x} = 0\) partout dessus — la dynamique s’arrête où elle arrive, sans préférence.

Noter aussi le contraste avec le twin Python (3 stratégies) : là-bas, AlwaysDefect est une ESS stricte au sens pair-à-pair (\(u(AD,AD) = 1.00 > u(TFT,AD) = 0.99\)). Ici, SuspiciousTFT fait exactement 1.00 contre AlwaysDefect (elle déserte la première rencontre puis rend coup pour coup) — égalité parfaite, et le verdict d’AlwaysDefect tombe de ESS à NSS. Le statut ESS n’est pas une propriété intrinsèque d’une stratégie : il est relatif à l’ensemble des mutants disponibles. C’est précisément ce que le test rend visible et que la prose seule masquait.

// Cellule 8b — Dependence a x0 : sur une face neutre, l'etat final depend du point de depart.
// Memes M, meme dynamique, deux populations initiales differentes.
double[] x0a = Enumerable.Repeat(1.0 / 7, 7).ToArray();                       // equipartition (cellule 8)
double[] x0bRaw = { 1.0, 1.0, 1.0, 1.0, 4.0, 1.0, 1.0 };                      // Grudger sursampling x4
double s0 = x0bRaw.Sum();
double[] x0b = x0bRaw.Select(v => v / s0).ToArray();
var trajA = ReplicatorDynamics(M, x0a, steps: 400, dt: 0.05);
var trajB = ReplicatorDynamics(M, x0b, steps: 400, dt: 0.05);
var finalA = trajA[^1];
var finalB = trajB[^1];

var sbX0 = new System.Text.StringBuilder();
sbX0.AppendLine(" Survivantes (face neutre) | x0 equipartie 1/7 | x0 Grudger x4 (4/11)");
sbX0.AppendLine(new string('-', 62));
for (int i = 0; i < 7; i++)
{
    if (finalA[i] < 0.005) continue;   // strategies eliminees dans les deux runs
    sbX0.AppendLine($" {names7[i],-25} | {finalA[i],17:F2} | {finalB[i],14:F2}");
}
"Deux etats finaux, meme dynamique (part de depart Grudger 0.091 -> 0.364) :".Display();
sbX0.ToString().Display();
Deux etats finaux, meme dynamique (part de depart Grudger 0.091 -> 0.364) :
 Survivantes (face neutre) | x0 equipartie 1/7 | x0 Grudger x4 (4/11)
--------------------------------------------------------------
 AlwaysCooperate           |              0,19 |           0,13
 TitForTat                 |              0,31 |           0,15
 Grudger                   |              0,27 |           0,57
 Pavlov                    |              0,23 |           0,14

Lecture — un point de repos, pas un attracteur

Les deux runs éliminent pareillement AlwaysDefect, SuspiciousTFT et Random (fitness strictement inférieure sur la face), mais répartissent autrement les 4 survivantes : donner à Grudger un poids initial 4 fois plus grand change substantiellement les proportions finales. La face TFT–Grudger–Pavlov–AC est un continuum de points de repos : la ligne « Strategie dominante a l’equilibre : TitForTat (frequence 0,31) » de la cellule 8 décrit où la trajectoire partie de l’équipartition s’est arrêtée, pas un attracteur. La persistance résiduelle d’AlwaysCooperate à ~19 % n’est pas une anomalie : c’est le symptôme visible de cette non-stricte stabilité.

À l’inverse, quand une ESS stricte existe, l’état final est indépendant de \(x_0\) — c’est le contrôle positif de la cellule suivante.

// Cellule 8c — Controle positif : Faucon-Colombe (Maynard Smith & Price, 1973).
// Sans un cas ou une ESS stricte EXISTE et est detectee, le verdict "NON partout"
// du pool 7x7 ne serait pas credible (un test qui ne dit jamais OUI ne teste rien).
// u(F,F) = (V-C)/2, u(F,C) = V, u(C,F) = 0, u(C,C) = V/2.
public static double[,] HawkDove(double V, double C)
    => new double[,] { { (V - C) / 2.0, V }, { 0.0, V / 2.0 } };

var sbHD = new System.Text.StringBuilder();
foreach (var (V, C) in new[] { (50.0, 25.0), (25.0, 50.0) })
{
    var HD = HawkDove(V, C);
    string[] hdNames = { "Faucon", "Colombe" };
    var (fEss, fNeutral, fInv) = IsEss(HD, hdNames, 0);
    var (cEss, cNeutral, cInv) = IsEss(HD, hdNames, 1);
    var trajHD = ReplicatorDynamics(HD, new[] { 0.5, 0.5 }, steps: 400, dt: 0.05);
    var fHD = trajHD[^1];
    sbHD.AppendLine($" Regime V={V:F0}, C={C:F0} {(V > C ? "(guerre peu couteuse)" : "(guerre ruineuse)")}");
    sbHD.AppendLine($"   Faucon   : ESS = {(fEss ? "OUI (stricte : (V-C)/2 > 0)" : "NON, envahi par " + string.Join(", ", fInv))}");
    sbHD.AppendLine($"   Colombe  : ESS = {(cEss ? "OUI" : "NON, envahi par " + string.Join(", ", cInv))}");
    sbHD.AppendLine($"   Replicateur depuis (0.5, 0.5) -> Faucon={fHD[0]:F2}, Colombe={fHD[1]:F2}");
    sbHD.AppendLine();
}
"Controle positif Faucon-Colombe (critere et dynamique doivent s'accorder) :".Display();
sbHD.ToString().Display();
Controle positif Faucon-Colombe (critere et dynamique doivent s'accorder) :
 Regime V=50, C=25 (guerre peu couteuse)
   Faucon   : ESS = OUI (stricte : (V-C)/2 > 0)
   Colombe  : ESS = NON, envahi par Faucon
   Replicateur depuis (0.5, 0.5) -> Faucon=1,00, Colombe=0,00

 Regime V=25, C=50 (guerre ruineuse)
   Faucon   : ESS = NON, envahi par Colombe
   Colombe  : ESS = NON, envahi par Faucon
   Replicateur depuis (0.5, 0.5) -> Faucon=0,50, Colombe=0,50

Lecture — le critère et la dynamique s’accordent

Dans le régime \(V > C\) (conflit peu coûteux), Faucon est une ESS stricte — \(u(F,F) = (V-C)/2 = 12.5 > u(C,F) = 0\) — et le réplicateur converge vers sa fixation (1.00 / 0.00) : ESS stricte ⇒ attracteur, quel que soit \(x_0\). Dans le régime \(V < C\), aucune stratégie pure n’est ESS (Faucon est envahie par Colombe, Colombe par Faucon) et la dynamique converge vers le mélange \(x_{Faucon} = V/C = 0.5\) — exactement le mélange asymétrique prédit par la théorie. Le test IsEss dit OUI quand il faut, NON quand il faut : c’est ce qui crédite son verdict « NON, mutants neutres » sur le pool IPD 7×7 de ce notebook.

Le pont avec l’exercice 3 : la cellule d’invasion (fitness d’un mutant à faible fréquence \(\varepsilon\)) teste une invasion à la fois, empiriquement ; IsEss quantifie la résistance pour tout mutant simultanément, par le critère. Les deux faces du même concept — l’une se simule, l’autre se prouve.

8. Pourquoi Tit-for-Tat gagne

TitForTat réunit les 4 propriétés identifiées par Axelrod comme clés du succès en IPD : 1. Nice (nice) : ne désert jamais la première → n’amorce pas de guerre de représailles. 2. Retaliatory (provookable) : punit immédiatement toute défection → non exploitable. 3. Forgiving (forgiving) : revient à la coopération dès que l’adversaire coopère → ne reste pas bloqué en punition mutuelle. 4. Clear (comprehensible) : l’adversaire comprend la règle → peut s’y adapter.

On vérifie ces propriétés en mesurant les scores de TitForTat contre chaque adversaire vs AlwaysDefect contre les mêmes.

// Cellule 9 — Robustesse de TitForTat vs AlwaysDefect
// On lit le PROPRE score de la strategie (element [0] du tuple retourne par PlayMatch).
var tftScores = new List<(string foe, double tft, double alld)>();
foreach (var s in tournamentStrats)
{
    if (s is TitForTat) continue;
    var (tftOwn, _, _) = PlayMatch(new TitForTat(), s.Factory(), 200);
    var (alldOwn, _, _) = PlayMatch(new AlwaysDefect(), s.Factory(), 200);
    tftScores.Add((s.Name, tftOwn/200.0, alldOwn/200.0));
}

var sb9 = new System.Text.StringBuilder();
sb9.AppendLine($" Adversaire          | score TFT | score AllD | qui extrait le plus ?");
sb9.AppendLine(new string('-', 62));
foreach (var (foe, tft, alld) in tftScores)
    sb9.AppendLine($" {foe,-20} | {tft,10:F2} | {alld,10:F2} | {(alld > tft ? "AllD (exploite)" : (tft > alld ? "TFT (cooperer)" : "egalite"))}");
sb9.ToString().Display();

double tftTotal = tftScores.Sum(x => x.tft);
double alldTotal = tftScores.Sum(x => x.alld);
$"Score total (moyen/tour, 6 adversaires) : TFT={tftTotal:F2}  |  AllD={alldTotal:F2}".Display();
display(tftTotal > alldTotal
    ? "-> TitForTat a un score total plus HAUT (=meilleur, convention Axelrod) qu'AlwaysDefect : sur l'ensemble du pool, la cooperation robuste bat l'exploitation brute. C'est le resultat historique d'Axelrod (1980)."
    : "-> AllD extrait plus sur ce pool (population trop cooperative/naive).");
 Adversaire          | score TFT | score AllD | qui extrait le plus ?
--------------------------------------------------------------
 AlwaysCooperate      |       3,00 |       5,00 | AllD (exploite)
 AlwaysDefect         |       0,99 |       1,00 | AllD (exploite)
 SuspiciousTFT        |       2,50 |       1,00 | TFT (cooperer)
 Grudger              |       3,00 |       1,02 | TFT (cooperer)
 Pavlov               |       3,00 |       3,00 | egalite
 Random               |       2,31 |       3,12 | AllD (exploite)
Score total (moyen/tour, 6 adversaires) : TFT=14,80  |  AllD=14,14
-> TitForTat a un score total plus HAUT (=meilleur, convention Axelrod) qu'AlwaysDefect : sur l'ensemble du pool, la cooperation robuste bat l'exploitation brute. C'est le resultat historique d'Axelrod (1980).

Lecture ancrée : Sur l’ensemble du pool de 6 adversaires, TitForTat (score total 14,80) bat AlwaysDefect (14,14), confirmant que la coopération robuste surclasse l’exploitation brute selon la convention Axelrod.

9. Exercices

Trois extensions à compléter (stubs C.1). Le notebook s’exécute end-to-end même non complété.

Exercice 1 — Exploitabilité d’une stratégie

L’exploitabilité mesure la vulnérabilité : l’écart entre le pire et le meilleur gain obtenu contre une galerie d’adversaires. Une stratégie robuste a une faible exploitabilité.

Indice : pour chaque adversaire, calculer le gain via PlayMatch, identifier min/max, exploitabilité = max − min.

// Cellule 10 — Exercice 1 (a completer) : exploitabilite
// TODO etudiant : implementer ComputeExploitability(strategy, opponents)
// Etape 1 : faire jouer la strategie contre chaque adversaire (PlayMatch, 200 tours).
// Etape 2 : collecter les gains moyens par tour.
// Etape 3 : retourner (worst, best, exploitability = best - worst).
public (string worstFoe, double worst, double best, double exploitability) ComputeExploitability(
    Strategy strat, List<Strategy> opponents)
{
    // TODO etudiant
    return ("", 0, 0, 0);   // stub
}

display("Exercice 1 (exploitabilite) : stub a completer.");
Exercice 1 (exploitabilite) : stub a completer.

Exercice 2 — Stratégie GenerousTitForTat (GTFT)

GenerousTitForTat = TitForTat avec pardon : après une défection adverse, elle coopère avec probabilité \(p\) (au lieu de punir systématiquement). Implémenter GTFT et montrer qu’elle résiste mieux au bruit.

Indice : sous-classe de Strategy, Choose() imite TFT mais après une défection adverse, tire Random.NextDouble() < p.

// Cellule 11 — Exercice 2 (a completer) : GenerousTitForTat
// TODO etudiant : implementer GTFT (TFT + pardon probabiliste p apres une defection adverse)
// Etape 1 : sous-classer Strategy, parametre p (ex 0.1).
// Etape 2 : Choose() : si dernier coup adverse = D, cooperer avec proba p, sinon imiter.
// Etape 3 : mesurer son score au tournoi bruite vs TFT standard.
public class GenerousTitForTat : Strategy
{
    // TODO etudiant
    public GenerousTitForTat(double p = 0.1) : base("GTFT") { }
    public override Act Choose() => Act.C;   // stub
    public override Func<Strategy> Factory => () => new GenerousTitForTat();   // fourni (requis par la classe de base)
}

display("Exercice 2 (GenerousTitForTat) : stub a completer.");
Exercice 2 (GenerousTitForTat) : stub a completer.

Exercice 3 — Stratégie évolutive envahissante

Une stratégie \(S\) envahit une population résidente \(R\) si, introduite à faible fréquence \(\varepsilon\), sa fitness contre la population (majoritairement \(R\)) est supérieure à celle de \(R\). Tester si TitForTat envahit AlwaysDefect (et inversement).

Indice : fitness de \(S\) rare = \(M[S, R]\) (gain contre \(R\) quasi seul) ; fitness de \(R\) = \(M[R, R]\). Envahit ssi \(M[S,R] > M[R,R]\).

// Cellule 12 — Exercice 3 (a completer) : invasion
// TODO etudiant : tester si une strategie S envahit une population residente R
// Etape 1 : calculer M[S,R] et M[R,R] via PlayMatch.
// Etape 2 : S envahit R ssi M[S,R] > M[R,R] (fitness rare > fitness residente).
public bool CanInvade(Strategy invader, Strategy resident, int rounds=200)
{
    // TODO etudiant
    return false;   // stub
}

display("Exercice 3 (invasion evolutive) : stub a completer.");
Exercice 3 (invasion evolutive) : stub a completer.

10. Résumé

Ce twin C# a reconstruit from-scratch (BCL .NET 9) toute la mécanique de la théorie des jeux évolutive :

  1. Moteur IPD : matrice T>R>P>S, fonction Payoff, conditions du dilemme (2R > T+S).
  2. 7 stratégies : AlwaysC, AlwaysD, TitForTat, SuspiciousTFT, Grudger, Pavlov (Win-Stay-Lose-Shift), Random.
  3. Match avec bruit optionnel (erreurs de transmission) + trace ASCII.
  4. Tournoi round-robin d’Axelrod : toutes paires, score moyen par match.
  5. Matrice de gains \(M[i,j]\).
  6. Dynamique du réplicateur : ODE \(\dot{x}_i = x_i(f_i - \bar{f})\) intégrée par Euler.
  7. Critère ESS (IsEss) : la stabilité évolutionne se teste — sur ce pool de 7, aucune stratégie n’est ESS (la clique TFT/Grudger/Pavlov/AlwaysCooperate est mutuellement neutre = NSS), et l’état final du réplicateur est un point de repos qui dépend de \(x_0\) (#12472).
  8. Analyse de robustesse : pourquoi TitForTat (nice/retaliatory/forgiving/clear) bat l’exploitation brute.

Leçons clés

  • La coopération émerge à l’itéré : un Dilemme joué une fois → défection dominante ; itéré indéfiniment → coopération robuste (TitForTat).
  • Le tournoi récompense la clémence, pas l’exploitation : AlwaysDefect gagne beaucoup de matchs individuels (elle exploite les cooperateurs à T=5) mais perd le tournoi au score cumulé — elle stagne à P=1 contre les retaliators (TitForTat, Grudger) sans jamais atteindre R=3. TitForTat, sans exploiter personne, accumule R=3 et gagne : score le plus HAUT = vainqueur (convention Axelrod).
  • Le bruit pénalise les “nice + retaliatory” : TitForTat et AlwaysCooperate chutent fortement sous 5% d’erreurs (cascades de représailles déclenchées par des coopérations perçues comme des défections). Les stratégies clémentes (GenerousTitForTat, exercice 2) y gagnent en robustesse.

Référence SOTA

Le twin Python GameTheory-06-EvolutionTrust-Python utilise la librairie axelrod (200+ stratégies, tournoi, analyse) + numpy/matplotlib (animation de la dynamique). Cette lib est l’outil SOTA de recherche en IPD. Verdict SOTA-OK (#10459) : l’annexe ci-dessous invoque réellement axelrod depuis .NET via PythonNet (Python.Runtime → CPython → axelrod), tandis que les sections principales conservent l’implémentation from-scratch comme contrepartie pédagogique. Le pont est en plus, jamais à la place : en production, utiliser axelrod ; en pédagogie, comprendre chaque brique.


Marathon #4956 — twin C# .NET ⇄ Python. Suite de GameTheory-03-Topology2x2-CSharp. Voir #4956, #3801. Revue souhaitée : ai-01, po-2024 (GameTheory/.NET owner), po-2025.

Annexe — Pont .NET vers axelrod (moteur de référence, #10459)

Le verdict INTRINSIC initial (PR #10406) concluait « pont Python->.NET pas viable » car axelrod est une lib Python pure utilisée exclusivement via import. 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) n’avait pas été envisagé. Or axelrod, exactement comme pyspiel/nashpy/ortools, est une lib Python invoquée par import — précisément le cas que PythonNet résout.

Cette annexe démontre le pont .NET → CPython 3.10+ → axelrod (Knight, Glynatsi et al., 200+ stratégies IPD, Tournament, MoranProcess) et le reclasse donc INTRINSIC → RECOVERABLE-LOCAL. Le pont est EN PLUS, jamais À LA PLACE (mandat #10382) : l’implémentation from-scratch des sections 3-9 reste la contrepartie pédagogique principale ; le pont prouve seulement que le moteur SOTA de référence est atteignable depuis .NET.

Prérequis : pip install axelrod (Python pure, léger) + pythonnet 3.1.0 (NuGet) + CPython 3.10+. Le notebook résout automatiquement la bibliothèque CPython. Sous Windows, il cherche python3xx.dll et ajoute les dépendances natives d’un environnement Conda. Ailleurs, ou si cette recherche échoue, il interroge le premier interpréteur (python3, puis python) qui importe axelrod, et en reprend la bibliothèque (libpython3.x.so sous Linux, libpython3.x.dylib sous macOS) et le sys.path, environnement virtuel compris. PYTHONNET_PYDLL reste disponible pour une installation non standard.

#r "nuget: pythonnet,3.1.0"
using System;
using Python.Runtime;

// Pont .NET -> CPython 3.10+ -> axelrod (annexe évolution IPD, reclassification #10459).
// axelrod (Knight, Glynatsi et al.) = lib Python pure, 200+ stratégies IPD, Tournament, MoranProcess.
// Même axe de pontage que GT-13/GT-17 (pyspiel CFR/rollout) et Search-7 (pyspiel MCTS).
static string PreparePythonDll(string candidate)
{
    if (!OperatingSystem.IsWindows()) return candidate;

    var pythonRoot = System.IO.Path.GetDirectoryName(candidate)!;
    var dependencyDirectories = new[] {
        pythonRoot,
        System.IO.Path.Combine(pythonRoot, "Library", "bin"),
        System.IO.Path.Combine(pythonRoot, "DLLs")
    }.Where(System.IO.Directory.Exists);
    var currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
    Environment.SetEnvironmentVariable(
        "PATH",
        string.Join(System.IO.Path.PathSeparator, dependencyDirectories.Append(currentPath)));
    return candidate;
}

static string ResolvePythonDll()
{
    var configured = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL");
    if (!string.IsNullOrWhiteSpace(configured) && System.IO.File.Exists(configured))
        return PreparePythonDll(configured);

    if (OperatingSystem.IsWindows())
    {
        var candidates = new List<string>();
        var localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA");
        if (!string.IsNullOrWhiteSpace(localAppData))
        {
            var pythonRoot = System.IO.Path.Combine(localAppData, "Programs", "Python");
            if (System.IO.Directory.Exists(pythonRoot))
                candidates.AddRange(
                    System.IO.Directory.GetDirectories(pythonRoot, "Python3*")
                        .OrderByDescending(path => path)
                        .SelectMany(path => System.IO.Directory.GetFiles(path, "python3*.dll")));
        }

        var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        foreach (var root in new[] {
                     System.IO.Path.Combine(home, "miniconda3"),
                     System.IO.Path.Combine(home, "anaconda3"),
                     @"C:\ProgramData\miniconda3",
                     @"C:\ProgramData\anaconda3" })
        {
            if (!System.IO.Directory.Exists(root)) continue;
            var versioned = System.IO.Directory.GetFiles(root, "python3??.dll");
            candidates.AddRange(versioned.OrderByDescending(path => path));
        }

        foreach (var candidate in candidates)
            if (System.IO.File.Exists(candidate)) return PreparePythonDll(candidate);
    }

    return null;   // aucun CPython désigné ni trouvé : repli sur l'interpréteur, ci-dessous
}

// 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.
static void InitPythonFromInterpreter(string module)
{
    const string 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 in new[] { "python3", "python" })
    {
        try
        {
            var psi = new System.Diagnostics.ProcessStartInfo(exe)
            { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
            foreach (var arg in new[] { "-c", probe, module }) psi.ArgumentList.Add(arg);
            using var 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)new PyString(e.GetString())).ToArray();
                Py.Import("sys").SetAttr("path", new PyList(path));
            }
            return;
        }
        catch (System.ComponentModel.Win32Exception) { }   // interpreteur absent du PATH
    }
    throw new System.IO.FileNotFoundException(
        $"Aucun CPython n'importe {module} : l'installer (pip install), ou definir PYTHONNET_PYDLL.");
}

var pyDll = ResolvePythonDll();
if (pyDll != null) { Runtime.PythonDLL = pyDll; PythonEngine.Initialize(); }
else InitPythonFromInterpreter("axelrod");
Console.WriteLine($"Runtime Python : {PythonEngine.Version}");

using (Py.GIL())
{
    dynamic scope = Py.CreateScope();
    scope.Exec(@"
import axelrod as axl, io, contextlib
players = [axl.Cooperator(), axl.Defector(), axl.TitForTat(), axl.Grudger()]
tour = axl.Tournament(players, turns=20, repetitions=3, seed=42)
with contextlib.redirect_stderr(io.StringIO()):
    results = tour.play()
lines = [f'axelrod {axl.__version__} : Tournament(Cooperator, Defector, TitForTat, Grudger), 20 tours x 3 reps']
lines.append('Classement IPD de cette expérience finie :')
for rank, player_index in enumerate(results.ranking, start=1):
    name = players[player_index].name
    scores = list(results.scores[player_index])
    lines.append(f'  {rank}. {name:16s} scores={scores}')
m = axl.Match((axl.Cooperator(), axl.Defector()), turns=10)
m.play()
ms = list(m.final_score_per_turn())
lines.append(f'Match Cooperator vs Defector (10 tours) score/tour : Coop={ms[0]}, Def={ms[1]}')
report = chr(10).join(lines)
");
    Console.WriteLine((string)scope.report);
}
Console.WriteLine("PONT .NET -> axelrod : OK -- sur ce petit tournoi fini, Defector gagne ; le tournoi historique plus large d'Axelrod couronne Tit-for-Tat.");
Installed Packages
  • pythonnet, 3.1.0
Runtime Python : 3.11.15 (main, Mar  3 2026, 09:26:23) [GCC 13.3.0]
axelrod 4.14.0 : Tournament(Cooperator, Defector, TitForTat, Grudger), 20 tours x 3 reps
Classement IPD de cette expérience finie :
  1. Defector         scores=[148, 148, 148]
  2. Tit For Tat      scores=[139, 139, 139]
  3. Grudger          scores=[139, 139, 139]
  4. Cooperator       scores=[120, 120, 120]
Match Cooperator vs Defector (10 tours) score/tour : Coop=0.0, Def=5.0
PONT .NET -> axelrod : OK -- sur ce petit tournoi fini, Defector gagne ; le tournoi historique plus large d'Axelrod couronne Tit-for-Tat.
Retour au sommet