# E1 — La famille 2x2 dans le plan
# Classification stricte des 4 archetypes Robinson-Goforth parmi les 78 profils distincts.
from itertools import permutations
def rgs_class(profile):
"""Classification Robinson-Goforth stricte : 4 archetypes par inequalities strictes.
profile = ((J1_00, J1_01), (J1_10, J1_11)) en ordinaux 1..4.
Renvoie : 'PD' | 'SH' | 'SD' | 'H' | 'Other'.
T = J1_10 (defecter vs cooperateur)
R = J1_00 (cooperer vs cooperateur)
P = J1_11 (defecter vs defecter)
S = J1_01 (cooperer vs defecter)
"""
T, R, P, S = profile[1][0], profile[0][0], profile[1][1], profile[0][1]
if T > R > P > S:
return 'PD'
if R > T and R > P and P > S:
return 'SH' # cooperation ET defection sont NE
if T > R > S > P:
return 'SD' # Snowdrift : cooperation faiblement dominante
if R > T and R > S > P:
return 'H' # Harmony : cooperation strictement dominante
return 'Other'
# Les 4 archetypes canoniques (reference : Robinson-Goforth 2005, p. 47-52)
PD_canon = ((3, 0), (4, 1)) # R=3, S=0, T=4, P=1 -> T=4>R=3>P=1>S=0 (strict)
SH_canon = ((4, 1), (3, 2)) # R=4, S=1, T=3, P=2 -> R=4>T=3>P=2>S=1 (strict, 4 valeurs distinctes)
SD_canon = ((2, 1), (3, 0)) # R=2, S=1, T=3, P=0 -> T=3>R=2>S=1>P=0 (Hawk-Dove)
H_canon = ((3, 2), (1, 0)) # R=3, S=2, T=1, P=0 -> R=3>T=1, R=3>S=2>P=0 (strict)
# Verification des 4 archetypes canoniques
print("Verification des 4 archetypes canoniques :")
for name, prof in [('PD', PD_canon), ('SH', SH_canon), ('SD', SD_canon), ('H', H_canon)]:
cls = rgs_class(prof)
status = 'OK' if cls == name else f'BUG (classe comme {cls})'
print(f" {name} {prof} -> {cls} [{status}]")
# Enumere tous les jeux ordinaux stricts : chaque joueur range strictement
# les 4 cases -> 4! = 24 rangements par joueur -> 24 x 24 = 576 jeux bruts.
# Renommer une strategie ou echanger les roles ne change pas le jeu : on
# reduit chaque jeu a sa forme normale (representant minimal de son orbite
# sous ces symetries) -> le compte canonique Rapoport-Guyer (1966).
def _swap_rows(m):
return (m[2], m[3], m[0], m[1]) # renomme les strategies du joueur ligne
def _swap_cols(m):
return (m[1], m[0], m[3], m[2]) # renomme les strategies du joueur colonne
def _transpose(m):
return (m[0], m[2], m[1], m[3])
def orbite(jeu):
"""Fermeture du jeu sous les renommages et l'echange des roles."""
vus, pile = {jeu}, [jeu]
while pile:
a, b = pile.pop()
for voisin in ((_swap_rows(a), _swap_rows(b)),
(_swap_cols(a), _swap_cols(b)),
(_transpose(b), _transpose(a))):
if voisin not in vus:
vus.add(voisin)
pile.append(voisin)
return vus
def archetypes_de_l_orbite(orb):
"""Archeotypes existentiels : un archeotype appartient a l'orbite s'il
existe un etiquetage du jeu ou les DEUX joueurs vivent sa signature
(T, R, P, S dans le bon ordre). La lecture du joueur colonne se fait
sur la transposee de sa matrice : son T est la case ou lui-meme defectionne."""
trouves = set()
for (a, b) in orb:
c_ligne = rgs_class((a[:2], a[2:]))
t = _transpose(b)
c_colonne = rgs_class((t[:2], t[2:]))
if c_ligne == c_colonne and c_ligne != "Other":
trouves.add(c_ligne)
return trouves
# Emboitement ordinal a trois niveaux (issue #13617) :
# Niveau 1 : permutations strictes des 4 issues -> 4! = 24 rangements par joueur.
# Niveau 2 : produit croise des deux espaces -> 24 x 24 = 576 jeux bruts.
# Niveau 3 : quotient par orbite (renommages + echanges de roles) -> 78 formes
# normales distinctes (compte canonique Rapoport-Guyer 1966).
_n_rangements = len(list(permutations([1, 2, 3, 4])))
print(f"Niveau 1 : {_n_rangements} rangements stricts par joueur (permutations 4 elements)")
print(f"Niveau 2 : produit croise = {_n_rangements} x {_n_rangements} = {_n_rangements * _n_rangements} jeux bruts")
print(f"Niveau 3 : quotient orbite = 78 formes normales distinctes (Rapoport-Guyer 1966)")
bruts = 0
orbites = {}
for J1_row in permutations([1, 2, 3, 4]):
for J2_row in permutations([1, 2, 3, 4]):
bruts += 1
o = orbite((J1_row, J2_row))
orbites[min(o)] = o
comptes = {"PD": 0, "SH": 0, "SD": 0, "H": 0, "Other": 0}
for norme, o in orbites.items():
trouves = archetypes_de_l_orbite(o)
if trouves:
for cls in trouves:
comptes[cls] += 1
else:
comptes["Other"] += 1
# Invariant : au plus UN archetype existentiel strict par orbite — si une
# orbite en realisait deux, le total depasserait len(orbites) sans erreur.
assert sum(comptes.values()) == len(orbites)
print(f"Enumeration brute : {bruts} jeux ordinaux (24 x 24 rangements stricts)")
print(f"Formes normales distinctes : {len(orbites)} jeux "
f"— compte canonique Rapoport-Guyer (1966)")
total = sum(comptes.values())
print(f"\nDistribution par archétype sur les {total} jeux canoniques :")
for cls in ["PD", "SH", "SD", "H", "Other"]:
n = comptes[cls]
pct = 100 * n / total
print(f" {cls:>5} : {n:>3} ({pct:>5.1f}%)")