<< GameTheory-06-EvolutionTrust-Python | Index GameTheory | GameTheory-07-ExtensiveForm-Python >>

GameTheory-6c (C#) : Jeux Répétés et Théorème Folk

Série : GameTheory / approfondissement c (jumeau .NET du notebook Python GameTheory-6c).

Durée estimée : ~1h

Prérequis : GameTheory-02-NormalForm-Python (forme normale, équilibre de Nash), GameTheory-04-NashEquilibrium-Python, notions de C# / .NET.

Pourquoi un jumeau C# ? Le socle répété est réimplémenté en C# avec la BCL : dilemme du prisonnier, série géométrique actualisée, stratégies grim trigger et tit-for-tat, théorème Folk et rendu ASCII de la région faisable. La section de statique comparative porte ensuite l’estimation SciPy du jumeau Python vers le vrai moteur .NET MathNet.Numerics.Optimization.NelderMeadSimplex. Les deux notebooks dérivent les mêmes seuils analytiques et confrontent séparément sympathie et engagement sur plusieurs jeux où seuls les gains d’autrui varient.

Objectifs d’apprentissage

  1. Formaliser un jeu repete comme la repetition d’un jeu étape (le dilemme du prisonnier) avec un facteur d’escompte delta.
  2. Comprendre pourquoi l’horizon fini detruit la cooperation (backward induction vers la defection perpetuelle), alors qu’un horizon infini la rend soutenable.
  3. Deriver la condition de credibilite d’une stratégie de punition (grim trigger) et la comparer a une punition plus legere (tit-for-tat).
  4. Illustrer le theoreme Folk : l’ensemble des paiements soutenables comme equilibres sous-game-parfaits coincide, pour delta assez proche de 1, avec la region faisable et individuellement rationnelle.

1. Le jeu étape : Dilemme du Prisonnier

On encode la matrice de gains du dilemme du prisonnier (convention d’Axelrod) : T (tentation), R (recompense de la cooperation), P (punition mutuelle), S (sucker). Les inegalites canoniques T > R > P > S et 2R > T+S garantissent que (D,D) est l’unique equilibre de Nash, bien que (C,C) soit Pareto-optimal.

// Matrice des gains du Dilemme du Prisonnier (convention d'Axelrod).
// Lignes = joueur 1 (Cooperer=0, Defecter=1), colonnes = joueur 2 (Cooperer=0, Defecter=1).
double T = 5.0, R = 3.0, P = 1.0, S = 0.0;

// A1[i,j] = gain du joueur 1 quand J1 joue i et J2 joue j
double[,] A1 = {
    { R, S },   // J1 coopere (ligne 0)
    { T, P }    // J1 defecte  (ligne 1)
};
// A2[i,j] = gain du joueur 2
double[,] A2 = {
    { R, T },   // J2 coopere (col 0)
    { S, P }    // J2 defecte (col 1)
};

Console.WriteLine("Gain joueur 1 (lignes=J1, colonnes=J2):");
Console.WriteLine($"  [[{A1[0,0]}, {A1[0,1]}]");
Console.WriteLine($"   [{A1[1,0]}, {A1[1,1]}]]");
Console.WriteLine();
Console.WriteLine($"Verif des inegalites du DP : T>R>P>S : {T > R && R > P && P > S}");
Console.WriteLine($"Verif 2R > T+S : {2*R > T+S} (2R={2*R}, T+S={T+S})");
Console.WriteLine($"Equilibre de Nash du jeu etape : (D,D) -> paiement P = {P}");
Console.WriteLine($"Pareto-optimum cooperatif : (C,C) -> paiement R = {R}");
Gain joueur 1 (lignes=J1, colonnes=J2):
  [[3, 0]
   [5, 1]]

Verif des inegalites du DP : T>R>P>S : True
Verif 2R > T+S : True (2R=6, T+S=5)
Equilibre de Nash du jeu etape : (D,D) -> paiement P = 1
Pareto-optimum cooperatif : (C,C) -> paiement R = 3

2. Horizon fini : l’effondrement inevitable

Sur un horizon fini de N tours (sans escompte, delta = 1), la cooperation est non credible. Par backward induction, le dernier tour est un jeu étape unique ou la defection domine ; sachant cela, l’avant-dernier tour aussi ; et ainsi de suite jusqu’au premier tour. L’unique equilibre sous-game-parfait (SPNE) est donc la defection perpetuelle, avec un paiement total P * N.

// Horizon fini : la defection perpetuelle est l'unique SPNE.
int N = 10;
double payoffDefectAlways = P * N;       // (D,D) chaque tour
double payoffCoopFictif   = R * N;       // (C,C) chaque tour, irrealiste en horizon fini

Console.WriteLine($"Horizon fini N = {N} tours (delta=1)");
Console.WriteLine($"  Paiement SPNE (D,D)x{N}   = {payoffDefectAlways:F1}");
Console.WriteLine($"  Paiement coop. fictif   = {payoffCoopFictif:F1} (NON credible : effondre par induction)");
Console.WriteLine();
Console.WriteLine($"Conclusion : en horizon fini, le SPNE = defaction, paiement total = P*N = {payoffDefectAlways:F1}");
Console.WriteLine($"La cooperation (R={R:F1} > P={P:F1}) exige un horizon INFINI.");
Horizon fini N = 10 tours (delta=1)
  Paiement SPNE (D,D)x10   = 10,0
  Paiement coop. fictif   = 30,0 (NON credible : effondre par induction)

Conclusion : en horizon fini, le SPNE = defaction, paiement total = P*N = 10,0
La cooperation (R=3,0 > P=1,0) exige un horizon INFINI.

3. Horizon infini et facteur d’escompte

Avec un horizon infini et un facteur d’escompte delta in (0,1), la valeur actualisee d’un flux constant g est la serie geometrique g / (1 - delta). On verifie numeriquement que la somme tronquee converge vers cette formule close, pour plusieurs valeurs de delta.

// Verif numerique de la serie geometrique g/(1-delta).
static double FluxGeom(double g, double delta, int terms = 2000)
{
    // Somme actualisee d'un flux constant g sur 'terms' tours.
    double s = 0.0;
    double pow = 1.0;  // delta^0
    for (int t = 0; t < terms; t++)
    {
        s += pow * g;
        pow *= delta;
    }
    return s;
}

double[] deltas = { 0.3, 0.5, 0.7, 0.9, 0.99 };
foreach (double d in deltas)
{
    double closed = R / (1 - d);
    double approx = FluxGeom(R, d);
    Console.WriteLine($"delta={d,-5:F2} : formule g/(1-d)={closed,10:F4} | somme tronquee={approx,10:F4}");
}
Console.WriteLine();
Console.WriteLine("Les deux colonnes coincident : la serie converge vers g/(1-delta).");
delta=0,30  : formule g/(1-d)=    4,2857 | somme tronquee=    4,2857
delta=0,50  : formule g/(1-d)=    6,0000 | somme tronquee=    6,0000
delta=0,70  : formule g/(1-d)=   10,0000 | somme tronquee=   10,0000
delta=0,90  : formule g/(1-d)=   30,0000 | somme tronquee=   30,0000
delta=0,99  : formule g/(1-d)=  300,0000 | somme tronquee=  300,0000

Les deux colonnes coincident : la serie converge vers g/(1-delta).

4. Le grim trigger et sa condition de credibilite

La stratégie grim trigger (déclenchement implacable) : cooperer tant que l’adversaire cooperate, mais le punir en defectant pour toujours des la première defection. Soit :

  • V_C = R / (1 - delta) : valeur de la cooperation perpetuelle.
  • V_D = T + delta * P / (1 - delta) : valeur d’une defection unique (gain T au tour de la trahison, puis punition P a jamais).

La cooperation est soutenable si V_C >= V_D, soit delta >= (T-R)/(T-P). C’est le seuil de credibilite.

// Derivation numerique de la condition de credibilite du grim trigger.
// Les constantes T,R,P,S (top-level) sont passees en parametres aux helpers statiques.
static double VC(double delta, double r) => r / (1 - delta);
static double VD(double delta, double t, double p) => t + delta * p / (1 - delta);

double deltaStar = (T - R) / (T - P);
Console.WriteLine($"Stage game : T={T} R={R} P={P} S={S}");
Console.WriteLine($"Seuil de credibilite delta* = (T-R)/(T-P) = ({T}-{R})/({T}-{P}) = {deltaStar:F4}");
Console.WriteLine();
Console.WriteLine($"{"delta",-8} {"V_C(coop)",-12} {"V_D(deviat)",-12} Coop. soutenable?");
foreach (double d in new[] { 0.30, 0.40, 0.50, 0.60, 0.70 })
{
    double vc = VC(d, R), vd = VD(d, T, P);
    string flag = vc >= vd ? "OUI" : "NON";
    Console.WriteLine($"{d,-8:F2} {vc,-12:F4} {vd,-12:F4} {flag}");
}
Console.WriteLine();
Console.WriteLine($"Verification au seuil delta*={deltaStar:F1} : V_C={VC(deltaStar, R):F4}, V_D={VD(deltaStar, T, P):F4} (indifference)");
Stage game : T=5 R=3 P=1 S=0
Seuil de credibilite delta* = (T-R)/(T-P) = (5-3)/(5-1) = 0,5000

delta    V_C(coop)    V_D(deviat)  Coop. soutenable?
0,30     4,2857       5,4286       NON
0,40     5,0000       5,6667       NON
0,50     6,0000       6,0000       OUI
0,60     7,5000       6,5000       OUI
0,70     10,0000      7,3333       OUI

Verification au seuil delta*=0,5 : V_C=6,0000, V_D=6,0000 (indifference)

5. Tit-for-tat vs grim trigger

Le tit-for-tat (TFT) punit plus legerement : il reprend la cooperation des que l’adversaire coopere de nouveau. Après une defection, le deviateur recoit T (gain de trahison), puis S au tour suivant (punition d’un tour), puis la cooperation reprend a R. La valeur de deviation est donc :

V_D_TFT = T + delta*S + delta^2 * R / (1 - delta)

Une punition plus legere exige un joueur plus patient (seuil delta plus eleve). Le grim trigger, punition la plus dure, soutient la cooperation pour le delta le plus faible — c’est l’intuition centrale du theoreme Folk.

// Comparaison des seuils : grim trigger (punition dure) vs tit-for-tat (punition legere).
static double VDgrim(double delta, double t, double p) => t + delta * p / (1 - delta);
static double VDtft(double delta, double t, double s, double r) => t + delta * s + (delta * delta) * r / (1 - delta);

double deltaGrim = (T - R) / (T - P);

// Seuil TFT : plus petit delta tel que R/(1-delta) >= VDtft(delta). Resolution numerique
// par balayage fin (equivalent du np.linspace + argmax du Python).
double deltaTft = double.NaN;
for (int k = 1; k < 10000; k++)
{
    double d = 0.01 + (0.99 - 0.01) * k / 10000.0;
    if (R / (1 - d) >= VDtft(d, T, S, R)) { deltaTft = d; break; }
}

Console.WriteLine($"Seuil grim trigger : delta* = {deltaGrim:F4}  (punition P a jamais)");
Console.WriteLine($"Seuil tit-for-tat  : delta* = {deltaTft:F4}  (punition legere, pardonne)");
Console.WriteLine();
Console.WriteLine($"Conclusion : grim trigger soutient la cooperation pour delta >= {deltaGrim:F3}");
Console.WriteLine($"             tit-for-tat exige  delta >= {deltaTft:F3} (PLUS patient requis)");
Console.WriteLine("Punition plus dure => cooperation plus facile a soutenir (folk theorem intuition).");
Seuil grim trigger : delta* = 0,5000  (punition P a jamais)
Seuil tit-for-tat  : delta* = 0,6667  (punition legere, pardonne)

Conclusion : grim trigger soutient la cooperation pour delta >= 0,500
             tit-for-tat exige  delta >= 0,667 (PLUS patient requis)
Punition plus dure => cooperation plus facile a soutenir (folk theorem intuition).

6. Le theoreme Folk

Le theoreme Folk (des annees 1950, “folk” car de provenance populaire non attribuee) etablit que tout paiement faisable et individuellement rationnel (IR) peut etre soutenu comme equilibre sous-game-parfait d’un jeu repete infiniment, des lors que le facteur d’escompte delta est assez proche de 1.

  • Faisable : dans l’enveloppe convexe des paiements d’issues du jeu étape. Pour le DP, les 4 issues sont (C,C)=(R,R), (C,D)=(S,T), (D,C)=(T,S), (D,D)=(P,P) — un quadrilatere.
  • Individuellement rationnel : chaque joueur recoit au moins son paiement de minimax (=P dans le DP symetrique).

La region faisable & IR est donc l’intersection du quadrilatere des issues avec le quart-dent {x >= P, y >= P}. C’est l’ensemble des paiements soutenables.

// Illustration ASCII : ensemble des paiements faisables et individuellement rationnels (IR).
// Les 4 sommets du quadrilatere des issues, ordonnes dans le sens direct (CCW).
double[][] issues = {
    new[] { R, R },   // (C,C)
    new[] { S, T },   // (C,D)
    new[] { T, S },   // (D,C)
    new[] { P, P }    // (D,D)
};
double minimax = P;  // DP symetrique

// Test point-in-convex-polygon pour un quadrilatere CCW.
static bool InsidePoly(double[][] poly, double x, double y)
{
    int n = poly.Length;
    for (int i = 0; i < n; i++)
    {
        // cross product (poly[(i+1)%n] - poly[i]) x (P - poly[i])
        double ax = poly[(i + 1) % n][0] - poly[i][0];
        double ay = poly[(i + 1) % n][1] - poly[i][1];
        double bx = x - poly[i][0];
        double by = y - poly[i][1];
        double cross = ax * by - ay * bx;
        if (cross < 0) return false;  // strictement en dehors (CCW)
    }
    return true;
}

// Reordonne les 4 sommets en CCW (angle croissant depuis le centroide).
double cx = (R + S + T + P) / 4.0, cy = (R + T + S + P) / 4.0;
var ordered = issues.OrderBy(p => Math.Atan2(p[1] - cy, p[0] - cx)).ToArray();

Console.WriteLine("Region faisable & IR du Dilemme du Prisonnier (ASCII):");
Console.WriteLine("  # = faisable & IR   . = hors region   * = sommet d'issue   : = seuil IR (minimax)");
Console.WriteLine();
Console.WriteLine("  y");
Console.WriteLine("  ^");
for (int y = 6; y >= -0; y--)
{
    Console.Write($"{y} | ");
    for (int x = 0; x <= 6; x++)
    {
        char ch = '.';
        bool isVertex = false;
        foreach (var v in ordered)
            if (Math.Abs(v[0] - x) < 1e-9 && Math.Abs(v[1] - y) < 1e-9) isVertex = true;

        bool feasible = InsidePoly(ordered, x, y);
        bool ir = x >= minimax - 1e-9 && y >= minimax - 1e-9;
        bool onIrLine = (x == (int)minimax || y == (int)minimax);

        if (isVertex) ch = '*';
        else if (feasible && ir) ch = '#';
        else if (onIrLine) ch = ':';
        Console.Write(ch + " ");
    }
    Console.WriteLine();
}
Console.WriteLine("  + " + new string('-', 14));
Console.WriteLine("    0 1 2 3 4 5 6  ->  x");
Console.WriteLine();
Console.WriteLine("Sommets : (C,C)=(3,3)  (C,D)=(0,5)  (D,C)=(5,0)  (D,D)=(1,1)");
Console.WriteLine($"La region '#' (faisable & IR, au-dessus et a droite du seuil minimax={minimax}) = ensemble des");
Console.WriteLine("paiements soutenables comme SPNE pour delta assez proche de 1 (theoreme Folk).");
Region faisable & IR du Dilemme du Prisonnier (ASCII):
  # = faisable & IR   . = hors region   * = sommet d'issue   : = seuil IR (minimax)

  y
  ^
6 | . : . . . . . 
5 | * : . . . . . 
4 | . # . . . . . 
3 | . # # * . . . 
2 | . # # # . . . 
1 | : * # # # : : 
0 | . : . . . * . 
  + --------------
    0 1 2 3 4 5 6  ->  x

Sommets : (C,C)=(3,3)  (C,D)=(0,5)  (D,C)=(5,0)  (D,D)=(1,1)
La region '#' (faisable & IR, au-dessus et a droite du seuil minimax=1) = ensemble des
paiements soutenables comme SPNE pour delta assez proche de 1 (theoreme Folk).

Interpretation : la cooperation comme cas particulier

Le paiement cooperatif (R,R) = (3,3) n’est qu’un point de la region faisable & IR. Le theoreme Folk dit que n’importe quel point de cette region — y compris des paiements asymetriques ou punitifs — peut emerger comme equilibre pour une stratégie de punition credible et un delta suffisamment eleve. La cooperation n’est donc pas un miracle : c’est un cas particulier d’un ensemble beaucoup plus large d’equilibres possibles, et c’est la durete de la punition (grim trigger) qui la soutient au seuil delta* le plus bas.

7. Statique comparative : mesurer la sympathie avec MathNet

Le classifieur du jumeau Python laisse alpha libre dans l’utilité transformée u = u_mien + alpha * u_autrui. Une trajectoire de coopération isolée ne permet donc pas de distinguer sympathie et engagement. La statique comparative fournit l’observation discriminante : faire varier uniquement le gain d’autrui à (C,C), en gardant tous les gains propres fixes, puis mesurer la réponse du taux de coopération.

  • sous sympathie, la coopération croît avec le gain d’autrui ;
  • sous engagement, la règle reste plate ;
  • pour un mélange, une MLE binomiale estime séparément la part plate q et la sensibilité alpha.

Le jumeau Python emploie scipy.optimize.minimize(method="Nelder-Mead"). Ici, le même problème non convexe est confié au moteur .NET MathNet.Numerics.Optimization.NelderMeadSimplex. Deux contrôles précèdent l’agent sous test : un alpha connu à retrouver et une pente nulle à ne pas surinterpréter.

// Section 7 : statique comparative et MLE avec le moteur MathNet.Numerics.
#r "nuget: MathNet.Numerics, 5.0.0"
using MathNet.Numerics.LinearAlgebra;
using MathNet.Numerics.Optimization;

const double DeltaSc = 0.6;
const double EpsEngagementSc = 0.05;
const int RoundsSc = 400;
double[] rAutreGridSc = { 0.0, 1.0, 2.0, 3.0, 4.0, 6.0 };
int[] seedsSc = { 0, 1, 7, 42 };

static double SigmoidSc(double value) => 1.0 / (1.0 + Math.Exp(-Math.Clamp(value, -30.0, 30.0)));

static double PCoopSympathieSc(double rAutre, double alpha, double beta,
                               double rPropre, double tPropre, double delta)
{
    double avantageCoop = (rPropre + alpha * rAutre - tPropre) / (1.0 - delta);
    return SigmoidSc(beta * avantageCoop);
}

static int[] SimulerComptesSc(double[] grid, int[] seeds, int rounds,
                              Func<double, double> probability)
{
    var counts = new int[grid.Length];
    foreach (int seed in seeds)
    {
        var rng = new Random(seed);
        for (int i = 0; i < grid.Length; i++)
            for (int round = 0; round < rounds; round++)
                if (rng.NextDouble() < probability(grid[i])) counts[i]++;
    }
    return counts;
}

static double BinomialNllSc(double[] grid, int[] counts, int n, Func<double, double> probability)
{
    double nll = 0.0;
    for (int i = 0; i < grid.Length; i++)
    {
        double p = Math.Clamp(probability(grid[i]), 1e-9, 1.0 - 1e-9);
        nll -= counts[i] * Math.Log(p) + (n - counts[i]) * Math.Log(1.0 - p);
    }
    return nll;
}

static (double B0, double B1, double SeB1) AjusterLogistiqueSc(
    double[] grid, int[] counts, int n)
{
    var objective = ObjectiveFunction.Value(x =>
    {
        if (Math.Abs(x[0]) > 30.0 || Math.Abs(x[1]) > 30.0) return 1e12;
        return BinomialNllSc(grid, counts, n, r => SigmoidSc(x[0] + x[1] * r));
    });
    var result = NelderMeadSimplex.Minimum(
        objective,
        Vector<double>.Build.DenseOfArray(new[] { 0.0, 0.0 }),
        Vector<double>.Build.DenseOfArray(new[] { 1.0, 0.25 }));
    double b0 = result.MinimizingPoint[0], b1 = result.MinimizingPoint[1];

    double h00 = 0.0, h01 = 0.0, h11 = 0.0;
    foreach (double r in grid)
    {
        double p = SigmoidSc(b0 + b1 * r);
        double w = n * p * (1.0 - p);
        h00 += w;
        h01 += w * r;
        h11 += w * r * r;
    }
    double det = h00 * h11 - h01 * h01;
    double seB1 = Math.Sqrt(h00 / det);
    return (b0, b1, seB1);
}

static (double Q, double Alpha, double Beta) AjusterMelangeSc(
    double[] grid, int[] counts, int n, double eps,
    double rPropre, double tPropre, double delta)
{
    (double Value, Vector<double> Point)? best = null;
    foreach (double q0 in new[] { 0.2, 0.5, 0.8 })
    foreach (double a0 in new[] { 0.3, 0.8, 1.5 })
    {
        var objective = ObjectiveFunction.Value(x =>
        {
            double q = x[0], alpha = x[1], beta = x[2];
            if (q < 0.0 || q > 1.0 || alpha < 0.0 || alpha > 4.0 || beta < 0.05 || beta > 10.0)
                return 1e12;
            return BinomialNllSc(grid, counts, n, r =>
                q * (1.0 - eps) + (1.0 - q) * PCoopSympathieSc(r, alpha, beta, rPropre, tPropre, delta));
        });
        var result = NelderMeadSimplex.Minimum(
            objective,
            Vector<double>.Build.DenseOfArray(new[] { q0, a0, 1.0 }),
            Vector<double>.Build.DenseOfArray(new[] { 0.08, 0.15, 0.2 }));
        if (best is null || result.FunctionInfoAtMinimum.Value < best.Value.Value)
            best = (result.FunctionInfoAtMinimum.Value, result.MinimizingPoint);
    }
    return (best!.Value.Point[0], best.Value.Point[1], best.Value.Point[2]);
}

int observationsParCelluleSc = seedsSc.Length * RoundsSc;
bool gainsPropresConstantsSc = rAutreGridSc.All(_ => R == 3.0 && T == 5.0 && P == 1.0 && S == 0.0);
bool gainsAutruiVarientSc = rAutreGridSc.Distinct().Count() == rAutreGridSc.Length;
Console.WriteLine($"Condition d'interpretabilite : gains propres constants = {gainsPropresConstantsSc} ; " +
                  $"R_autre prend {rAutreGridSc.Length} valeurs distinctes = {gainsAutruiVarientSc}");
Console.WriteLine($"Grille R_autre : [{string.Join(", ", rAutreGridSc.Select(x => x.ToString("F1")))}]");
Console.WriteLine();

// Contrôle 1 : sympathie pure, alpha connu.
int[] comptesSympathieSc = SimulerComptesSc(
    rAutreGridSc, seedsSc, RoundsSc,
    r => PCoopSympathieSc(r, 0.8, 1.0, R, T, DeltaSc));
var fitSympathieSc = AjusterLogistiqueSc(rAutreGridSc, comptesSympathieSc, observationsParCelluleSc);
double alphaHatSympathieSc = fitSympathieSc.B1 * (R - T) / fitSympathieSc.B0;
Console.WriteLine("--- CONTROLE 1 : sympathie pure (alpha vrai = 0.8) ---");
Console.WriteLine($"  taux : [{string.Join(", ", comptesSympathieSc.Select(k => ((double)k / observationsParCelluleSc).ToString("F3")))}]");
Console.WriteLine($"  MathNet MLE : alpha_hat = {alphaHatSympathieSc:F3}, pente b1 = {fitSympathieSc.B1:+0.000;-0.000}");
Console.WriteLine($"  ecart absolu a la valeur vraie = {Math.Abs(alphaHatSympathieSc - 0.8):F3}");
Console.WriteLine();

// Contrôle 2 : engagement pur, règle plate avec bruit indépendant de R_autre.
int[] comptesEngagementSc = SimulerComptesSc(
    rAutreGridSc, seedsSc, RoundsSc, _ => 1.0 - EpsEngagementSc);
var fitEngagementSc = AjusterLogistiqueSc(rAutreGridSc, comptesEngagementSc, observationsParCelluleSc);
double ciBasPenteSc = fitEngagementSc.B1 - 1.96 * fitEngagementSc.SeB1;
double ciHautPenteSc = fitEngagementSc.B1 + 1.96 * fitEngagementSc.SeB1;
Console.WriteLine("--- CONTROLE 2 : engagement pur (regle fixe, insensible) ---");
Console.WriteLine($"  taux : [{string.Join(", ", comptesEngagementSc.Select(k => ((double)k / observationsParCelluleSc).ToString("F3")))}]");
Console.WriteLine($"  pente b1 = {fitEngagementSc.B1:+0.0000;-0.0000}, CI95 Wald " +
                  $"[{ciBasPenteSc:+0.0000;-0.0000}, {ciHautPenteSc:+0.0000;-0.0000}] " +
                  $"-> couvre 0 : {ciBasPenteSc <= 0.0 && ciHautPenteSc >= 0.0}");
Console.WriteLine();

// Agent sous test : mélange 50 % engagement, 50 % sympathie alpha=0.6.
int[] comptesMelangeSc = SimulerComptesSc(
    rAutreGridSc, seedsSc, RoundsSc,
    r => 0.5 * (1.0 - EpsEngagementSc) + 0.5 * PCoopSympathieSc(r, 0.6, 1.0, R, T, DeltaSc));
var fitMelangeSc = AjusterMelangeSc(
    rAutreGridSc, comptesMelangeSc, observationsParCelluleSc,
    EpsEngagementSc, R, T, DeltaSc);
Console.WriteLine("--- AGENT SOUS TEST : melange (q vrai = 0.5, alpha vrai = 0.6) ---");
Console.WriteLine($"  taux : [{string.Join(", ", comptesMelangeSc.Select(k => ((double)k / observationsParCelluleSc).ToString("F3")))}]");
Console.WriteLine($"  MathNet NelderMead : q_hat = {fitMelangeSc.Q:F3}, alpha_hat = {fitMelangeSc.Alpha:F3}, beta_hat = {fitMelangeSc.Beta:F3}");
Console.WriteLine($"  ecarts absolus : |q_hat - 0.5| = {Math.Abs(fitMelangeSc.Q - 0.5):F3}, " +
                  $"|alpha_hat - 0.6| = {Math.Abs(fitMelangeSc.Alpha - 0.6):F3}");
Console.WriteLine("  verdict : masse plate et composante sensible separees par le moteur .NET.");
Installed Packages
  • MathNet.Numerics, 5.0.0
Condition d'interpretabilite : gains propres constants = True ; R_autre prend 6 valeurs distinctes = True
Grille R_autre : [0,0, 1,0, 2,0, 3,0, 4,0, 6,0]

--- CONTROLE 1 : sympathie pure (alpha vrai = 0.8) ---
  taux : [0,010, 0,040, 0,279, 0,736, 0,955, 0,998]
  MathNet MLE : alpha_hat = 0,804, pente b1 = +2,002
  ecart absolu a la valeur vraie = 0,004

--- CONTROLE 2 : engagement pur (regle fixe, insensible) ---
  taux : [0,951, 0,953, 0,963, 0,951, 0,951, 0,947]
  pente b1 = -0,0240, CI95 Wald [-0,0714, +0,0235] -> couvre 0 : True

--- AGENT SOUS TEST : melange (q vrai = 0.5, alpha vrai = 0.6) ---
  taux : [0,500, 0,478, 0,554, 0,671, 0,851, 0,958]
  MathNet NelderMead : q_hat = 0,506, alpha_hat = 0,608, beta_hat = 0,945
  ecarts absolus : |q_hat - 0.5| = 0,006, |alpha_hat - 0.6| = 0,008
  verdict : masse plate et composante sensible separees par le moteur .NET.

Lecture de la sortie : ce que la statique identifie

Les deux contrôles rendent l’interprétation falsifiable : l’estimateur doit retrouver un alpha connu pour un agent de sympathie pure, puis une pente compatible avec zéro pour une règle d’engagement insensible aux gains d’autrui. Le même moteur estime ensuite séparément la masse plate q et la sensibilité alpha d’un agent mélangeant les deux mécanismes.

alpha ne devient donc mesurable qu’en observant le même agent sur plusieurs jeux où seuls les gains d’autrui varient. Sur une trajectoire de coopération isolée, l’équivalence observationnelle de la section 7d du jumeau Python subsiste : l’engagement reste un candidat, jamais un verdict imposé par un paramètre choisi après coup.

Exercice 1 : Sensibilite au facteur d’escompte

Calculez le seuil de credibilite delta* = (T-R)/(T-P) pour un nouveau dilemme du prisonnier ou T=6, R=4, P=2, S=0. La cooperation est-elle plus ou moins facile a soutenir que dans le cas standard (5,3,1,0) ?

// Exercice 1 a completer
double Te = 6.0, Re = 4.0, Pe = 2.0, Se = 0.0;
double deltaStarE = 0.0;  // TODO etudiant : calculer le seuil (Te-Re)/(Te-Pe)
Console.WriteLine($"Seuil a determiner pour T={Te},R={Re},P={Pe},S={Se} : {deltaStarE} (a completer)");
Seuil a determiner pour T=6,R=4,P=2,S=0 : 0 (a completer)

Exercice 2 : Une punition limitee (N-periodes)

On remplace le grim trigger par une punition limitee a N periodes : après une defection, les deux joueurs reçoivent P pendant N tours, puis la cooperation reprend. Exprimez la valeur de deviation :

VD_limited = T + sum_{k=1}^{N} delta^k * P + delta^{N+1} * R / (1 - delta)

// Exercice 2 a completer
static double VDLimited(double delta, int N)
{
    double result = 0.0;  // TODO etudiant : T + sum_{k=1}^{N} delta^k * P + delta^{N+1} * R/(1-delta)
    return result;
}
Console.WriteLine($"A completer pour delta=0.5, N=2 : {VDLimited(0.5, 2)} (a completer)");
A completer pour delta=0.5, N=2 : 0 (a completer)

Exercice 3 : Au-dela du DP - jeu de la poule (Chicken)

Le jeu de la poule n’est pas un dilemme du prisonnier (l’inegalite T > R > P > S echoue). Identifiez les equilibres de Nash en stratégies pures de sa matrice. Combien y en a-t-il, et le theoreme Folk s’applique-t-il differemment ?

// Exercice 3 a completer
// Matrice du jeu de la poule (Chicken) : a analyser.
// A1_chicken = [[3, 1], [4, 0]] ; A2_chicken = [[3, 4], [1, 0]]
// TODO etudiant : identifier les equilibres de Nash purs (et eventuellement mixte).
int equilibresPursIdentifies = 0;  // a completer (attendu : 2 purs + 1 mixte)
Console.WriteLine($"Equilibres de Nash purs a identifier (0 = a completer) : {equilibresPursIdentifies}");
Equilibres de Nash purs a identifier (0 = a completer) : 0

Conclusion et perspectives

Ce jumeau C# réimplémente les objets mathématiques du notebook Python et confie l’estimation non convexe au moteur .NET MathNet :

  • Le jeu étape (dilemme du prisonnier) et l’effondrement de la coopération en horizon fini (backward induction).
  • La série géométrique actualisée g/(1-delta), vérifiée numériquement.
  • La condition de crédibilité du grim trigger delta >= (T-R)/(T-P) = 0.5, et la comparaison avec le tit-for-tat (seuil plus élevé, ~0.667) : une punition plus dure soutient la coopération plus facilement.
  • L’ensemble faisable & IR du théorème Folk, visualisé en ASCII.
  • La statique comparative des gains d’autrui, qui distingue une règle d’engagement plate d’une composante de sympathie sensible et estime leur mélange avec MathNet.Numerics.Optimization.NelderMeadSimplex.

Parité avec le notebook Python

Les seuils analytiques restent identiques au notebook Python. Les estimations stochastiques ne sont pas recopiées : chaque jumeau les mesure avec son propre générateur aléatoire et son moteur d’optimisation.

Quantité Python C#
delta* grim trigger (T-R)/(T-P) 0.5000 0.5000
delta* tit-for-tat (numérique) ~0.6668 ~0.6668
Série g/(1-d), d=0.9 30.0000 30.0000
Contrôle sympathie, alpha vrai = 0.8 alpha_hat = 0.801 (SciPy) alpha_hat = 0.804 (MathNet)
Contrôle engagement CI95 de la pente couvre 0 CI95 de la pente couvre 0
Mélange, (q, alpha) = (0.5, 0.6) (0.490, 0.606) (SciPy) (0.506, 0.608) (MathNet)

Les deux notebooks expriment la même intuition : la coopération émergente des jeux répétés n’est pas un miracle altruiste. La menace crédible et la patience peuvent la soutenir ; pour attribuer une coopération observée à la sympathie plutôt qu’à l’engagement, il faut en plus faire varier les gains d’autrui à gains propres constants.

Prochaines étapes


Part of #4956 (marathon parité .NET <-> Python).

Retour au sommet