Dilemme du prisonnier itéré bruyant


34

Dans ce défi, vous allez jouer au dilemme du prisonnier itéré bruyant.

Le dilemme du prisonnier est un scénario de la théorie des jeux où il y a deux joueurs, chacun avec deux options: coopérer ou faire défaut. Chaque joueur fait mieux pour lui-même s’il fait défaut que s'il coopère, mais les deux joueurs préfèrent le résultat où les deux joueurs coopèrent à celui où les deux joueurs font défaut.

Le dilemme du prisonnier itéré est le même jeu, sauf que vous jouez contre le même adversaire à plusieurs reprises et que vous savez ce que votre adversaire a joué dans le passé. Votre objectif est toujours d'accumuler le score le plus élevé pour vous-même, quel que soit le résultat de vos adversaires.

Le dilemme du prisonnier itéré bruyant introduit du bruit dans la communication. Du bruit sera introduit dans votre connaissance de ce que votre adversaire a joué dans le passé. Vous saurez également ce que vous avez fait dans le passé. Le taux de bruit est constant sur un tour contre le même adversaire, mais différent entre les tours.

Défi

Dans ce défi, vous allez écrire un programme Python 3 pour jouer le dilemme bruyant du prisonnier itéré.

Votre programme recevra trois entrées:

  • Vos propres mouvements, sans inversion aléatoire appliquée.

  • Les mouvements de votre adversaire, avec des retournements aléatoires appliqués.

  • Une variable d'état, qui commence par une liste vide à chaque tour et que vous pouvez modifier si vous le souhaitez. Vous pouvez l'ignorer si vous ne voulez pas l'utiliser.

Votre programme devrait sortir 'c'coopérer ou 'd'défecter.

Par exemple, voici un programme qui coopère si l’opposant a coopéré au moins 60% du temps dans le passé, après l’application de lancers aléatoires et pour les 10 premiers lancers:

def threshold(my_plays, their_flipped_plays, state):
    if len(their_flipped_plays) < 10:
        return 'c'
    opp_c_freq = their_flipped_plays.count('c')/len(their_flipped_plays)
    if opp_c_freq > 0.6:
        return 'c'
    else:
        return 'd'

Si vous ne connaissez pas Python, écrivez votre soumission en pseudocode et une personne (moi-même ou un autre membre du site) peut créer le programme Python correspondant.

Gameplay

Le coureur du tournoi peut être trouvé ici: noisy-game . Courez noisy-game.pypour lancer le tournoi. Je garderai ce référentiel à jour avec les nouvelles soumissions. Des exemples de programmes peuvent être trouvés dans basic.py.

Le score global d'un programme est le total de ses scores sur 100 parties du jeu.

Une partie consiste en des confrontations à tour de rôle de chaque joueur contre chaque joueur, y compris le sien. Un match consiste en 100 rounds. Un tour consiste en 300 mouvements, chacun impliquant la sortie 'c'ou'd' .

Votre soumission jouera un match contre toutes les soumissions, y compris la vôtre. Chaque match comprendra 100 rounds. Au cours de chaque tour, une probabilité de retournement sera choisie uniformément de manière aléatoire [0, 0.5].

Chaque tour consistera en 300 mouvements. À chaque déplacement, les deux programmes recevront toutes les lectures précédentes qu'ils ont tentées et toutes les lectures précédentes que l'autre programme a effectuées après l'application des flips, ainsi qu'une variable d'état, qui est une liste modifiable que le programme peut modifier s'il le souhaite. Les programmes vont sortir leurs mouvements.

Les coups sont marqués comme suit: Si un programme joue un 'c', le programme adverse obtient 2 points. Si un programme joue une'd' , ce programme gagne 1 point.

Ensuite, chaque coup est retourné indépendamment avec une probabilité égale à la probabilité de retournement et stocké pour être montré à l'adversaire.

Une fois que tous les tours ont été joués, nous additionnons le nombre de points que chaque joueur a obtenus lors de chaque match. Ensuite, nous utilisons le système de notation suivant pour calculer le score de chaque joueur pour la partie. Cette notation est effectuée une fois que toutes les confrontations sont terminées.

Notation

Nous allons utiliser la notation évolutive. Chaque programme commence avec un poids égal. Ensuite, les poids sont mis à jour comme suit, pour 100 itérations, en utilisant les totaux de points du jeu:

Le nouveau poids de chaque programme est proportionnel au produit de son poids précédent et de son total de points moyen pondéré par le poids de ses adversaires.

100 mises à jour sont appliquées et les poids finaux sont les scores de chaque programme pour cette phase du jeu.

Les scores globaux seront la somme sur 100 manches du match.

Les joueurs seront tous des réponses valables à ce défi, ainsi que six programmes de base pour nous aider à démarrer.

Mises en garde

Ne modifiez pas les entrées. Ne tentez pas d’affecter l’exécution d’un autre programme, sauf en coopérant ou en échouant. Ne faites pas de soumission sacrificielle qui tente de reconnaître une autre soumission et de bénéficier cet adversaire à ses propres frais. Les échappatoires standard sont interdites.

EDIT: Les soumissions peuvent ne pas dupliquer exactement les programmes de base ou les soumissions antérieures.

Si vous avez des questions, n'hésitez pas à les poser.

Résultats actuels

nicht_genug: 40.6311
stealer: 37.1416
enough: 14.4443
wait_for_50: 6.947
threshold: 0.406784
buckets: 0.202875
change_of_heart: 0.0996783
exploit_threshold: 0.0670485
kickback: 0.0313357
tit_for_stat: 0.0141368
decaying_memory: 0.00907645
tit_for_whoops: 0.00211803
slider: 0.00167053
trickster: 0.000654875
sounder: 0.000427348
tit_for_tat: 9.12471e-05
stubborn_stumbler: 6.92879e-05
tit_for_time: 2.82541e-05
jedi2sith: 2.0768e-05
cooperate: 1.86291e-05
everyThree: 1.04843e-05
somewhat_naive: 4.46701e-06
just_noise: 1.41564e-06
growing_distrust: 5.32521e-08
goldfish: 4.28982e-09
vengeful: 2.74267e-09
defect: 3.71295e-10
alternate: 2.09372e-20
random_player: 6.74361e-21

Résultats avec uniquement des réponses à cette question et programmes de base ignorant le jeu de l'adversaire:

nicht_genug: 39.3907
stealer: 33.7864
enough: 20.9032
wait_for_50: 5.60007
buckets: 0.174457
kickback: 0.0686975
change_of_heart: 0.027396
tit_for_stat: 0.024522
decaying_memory: 0.0193272
tit_for_whoops: 0.00284842
slider: 0.00153227
sounder: 0.000472289
trickster: 0.000297515
stubborn_stumbler: 3.76073e-05
cooperate: 3.46865e-05
tit_for_time: 2.42263e-05
everyThree: 2.06095e-05
jedi2sith: 1.62591e-05
somewhat_naive: 4.20785e-06
just_noise: 1.18372e-06
growing_distrust: 6.17619e-08
vengeful: 3.61213e-09
goldfish: 3.5746e-09
defect: 4.92581e-10
alternate: 6.96497e-20
random_player: 1.49879e-20

Gagnant

Le concours restera ouvert indéfiniment, au fur et à mesure que de nouvelles soumissions seront publiées. Cependant, je vais déclarer un gagnant (accepter une réponse) sur la base des résultats 1 mois après la publication de cette question.


Comment tit_for_whoops ignore-t-il le jeu de l'adversaire?
LyricLy

@ LyyLy Je suppose que la catégorie fait référence aux programmes de base fournis par Isaac qui ignorent leurs adversaires.
FryAmTheEggman

1
Dois-je comprendre que vous pouvez utiliser la variable d'état pour enregistrer tous vos mouvements au fur et à mesure que vous les soumettez, et donc connaître à la fois vos mouvements réels et retournés, et estimer la probabilité de retournement?
xnor

1
@xnor On vous dit toujours vos vrais gestes. Seuls les mouvements de l'adversaire peuvent être inversés.

1
@isaacg J'ai essayé de copier exploit_threshold()plusieurs fois en tant que exploit_threshold1(), etc. et les ai ajoutés à la playersliste. Pourquoi ai-je des résultats très différents pour des stratégies identiques?
ngn

Réponses:


4

Genug n'est pas genug

(pourrait aussi être appelé enough2ou stealback)

def nicht_genug(m,t,s):
    if not s:
        s.append("c")
        return "c"
    if s[0]=="t":
        return "d"
    if m[-42:].count("d")>10 or len(t)+t.count("d")>300:
        s[0]="t"
        return "d"
    if t[-1]=="d":
        if s[0]=="d":
            s[0]="c"
            return "d"
        else:
            s[0]="d"
            return "c"
    else:
        if t[-3:].count("d")==0:
            s[0]="c"
        return "c"

J'ai appris que la mésange originale de deux tatouages attendait deux tats consécutifs comme des biches tit_for_whoops, et en effet, il semble que nous devrions pardonner et oublier (enfin, presque ...) des tats simples plus tôt. Et beaucoup de joueurs font défaut dans les derniers tours. Je préfère toujours être sympa quand tout va bien jusqu'à présent, mais la barre de tolérance du bot ne cesse de baisser.


10

Tit-For-Whoops

Inspiré par une stratégie de ncase.me/trust

def tit_for_whoops(m, t, s):
    if len(t) < 2:
        return 'c'
    else:
        return 'd' if all([x == 'd' for x in t[-2:]]) else 'c'

Défauts seulement si l'autre joueur a fait défection deux fois de suite, pour éviter les malentendus.


Merci pour votre soumission! Gardez à l'esprit que puisque la probabilité de retournement est en moyenne de 1/4, il y aura un double retournement une fois tous les 16 mouvements environ.
isaacg

J'ai ajouté une variable d'état que vous pouvez ignorer si vous ne voulez pas l'utiliser.
isaacg

9

Changement de cœur

def change_of_heart(m, t, s):
    return 'c' if len(t) < 180 else 'd'

A un changement de coeur en cours de route. Fait étonnamment bien.


Félicitations pour avoir pris la tête / deuxième place. Je suis impressionné et surpris qu'un adversaire qui ignore la stratégie réussisse si bien.
isaacg

9

Stealer de stratégie

Inspiré par Assez, change_sur_ce_cœur et se moque de qui. Devrait être un peu plus tolérant. J'ai essayé de modifier les chiffres pour obtenir les meilleurs résultats, mais ils ne voulaient pas beaucoup changer.

def stealer(mine, theirs, state):
    if len(mine) == 0:
        state.append('c')
        return 'c'
    elif len(mine) > 250:
        return "d"
    elif state[0] == 't':
        return 'd'
    elif mine[-40:].count('d') > 10:
        state[0] = 't'
        return 'd'
    elif theirs[-1] == 'd':
        if state[0] == 'd':
            state[0] = 'c'
            return 'd'
        else:
            state[0] = 'd'
            return 'c'
    elif all([x == 'c' for x in theirs[-3:]]):
        state[0] = 'c'
        return 'c'
    else:
        return 'c'

Bienvenue chez PPCG!
Giuseppe

Félicitations pour avoir pris les devants!
isaacg

8

Tit-For-Time

def tit_for_time(mine, theirs, state):
    theirs = theirs[-30:]
    no_rounds = len(theirs)
    return "c" if no_rounds < 5 or random.random() > theirs.count("d") / no_rounds else "d"

Si vous avez passé le plus clair de mon temps à me faire mal, je vais juste vous faire du mal en retour. Probablement.


Belle soumission! Vous êtes actuellement à la 1re place sans les programmes de base destinés aux adversaires.
isaacg

7

Méfiance croissante

import random

def growing_distrust(mine, theirs, state):
    # Start with trust.
    if len(mine) == 0:
        state.append(dict(betrayals=0, trust=True))
        return 'c'

    state_info = state[0]

    # If we're trusting and we get betrayed, trust less.
    if state_info['trust'] and theirs[-1] == 'd':
        state_info['trust'] = False
        state_info['betrayals'] += 1

    # Forgive, but don't forget.
    if random.random() < 0.5 ** state_info['betrayals']:
        state_info['trust'] = True

    return 'c' if state_info['trust'] else 'd'

Plus l'adversaire me trahissait, moins je pouvais croire que c'était juste du bruit.


Ouais, le fait qu'il n'y ait pas d'état est regrettable, mais je voulais que les soumissions soient uniformes, c'est donc le meilleur que je puisse imaginer. Avez-vous une idée sur la façon d'ajouter un état?
isaacg

Avoir juste un stateargument qui par défaut est une liste? Les listes sont modifiables, de sorte que l'état serait facilement modifiable.
LyricLy

Comment? Je ne vois pas comment cela pourrait être.
LyricLy

@Mememonic Je pense savoir comment implémenter ceci. Je vais faire un tourbillon.
isaacg

J'ai ajouté une variable d'état, qui est initialement une liste vide et que vous pouvez modifier.
isaacg

7

Jedi2Sith

Tout commence bien et altruiste, mais avec le temps l'influence du côté obscur devient de plus en plus forte, jusqu'au point de non retour. Il n’ya pas moyen d’arrêter cette influence, mais toutes les mauvaises choses qu’elle voit se produisent ne font que contribuer au pouvoir du côté obscur ...

def jedi2sith(me, them, the_force):
  time=len(them)
  bad_things=them.count('d')
  dark_side=(time+bad_things)/300
  if dark_side>random.random():
    return 'd'
  else:
    return 'c'

Essayez-le en ligne!


6

Curseur

def slider(m, t, s):
    z = [[2, 1], [0, 1], [2, 3], [2, 1]]
    x = 0
    for y in t:
      x = z[x][y == 'c']
    return 'c' if x < 2 else 'd'

Commence par «c» et glisse progressivement vers ou en dehors de «d».


J'ai fait une réécriture fonctionnellement équivalente de cela pour utiliser la variable d'état, car elle fonctionnait très lentement. Vous n'avez pas besoin de changer quoi que ce soit, cependant.
isaacg

6

Stumbler Stumbler

def stubborn_stumbler(m, t, s):
    if not t:
        s.append(dict(last_2=[], last_3=[]))
    if len(t) < 5:
        return 'c'
    else:
        # Records history to state depending if the last two and three
        # plays were equal
        s = s[0]
        if t[-2:].count(t[-1]) == 2:
            s['last_2'].append(t[-1])
        if t[-3:].count(t[-1]) == 3:
            s['last_3'].append(t[-1])
    c_freq = t.count('c')/len(t)
    # Checks if you've consistently defected against me
    opp_def_3 = s['last_3'].count('d') > s['last_3'].count('c')
    opp_def_2 = s['last_2'].count('d') > s['last_2'].count('c')
    # dist func from 0 to 1
    dist = lambda x: 1/(1+math.exp(-5*(x-0.5)))
    # You've wronged me too much
    if opp_def_3 and opp_def_2:
        return 'd'
    # Otherwise, if you're consistently co-operating, co-operate more
    # the less naive you are
    else:
        return 'c' if random.random() > dist(c_freq) - 0.5 else 'd'

Basé sur votre stratégie de seuil d’exploitation avec seulement des parties cohérentes gardées en trace pour permuter entre défaut et surtout coopérer

UPDATE: garde la trace de deux jeux consécutifs et de trois jeux consécutifs, punissant uniquement dans des conditions plus difficiles et ajoutant un choix aléatoire en cas de doute.

UPDATE 2: Condition supprimée et fonction de distribution ajoutée


Contratsulations sur l'écriture du premier programme à prendre les devants!
Isaac

6

Bruit Bot

def just_noise(m,t,s):
    return 'c' if random.random() > .2 else 'd'

Je suis certainement coopérer bot. C'est juste du bruit.


6

Trop c'est trop

def enough(m,t,s):
    if not s:
        s.append("c")
        return "c"
    if s[0]=="t":
        return "d"
    if m[-42:].count("d")>10:
        s[0]="t"
        return "d"
    if t[-1]=="d":
        if s[0]=="d":
            s[0]="c"
            return "d"
        else:
            s[0]="d"
            return "c"
    else:
        return "c"

Commence comme tit pour deux tat où les deux tat ne doivent pas être consécutifs (contrairement à tit_for_whoops). S'il doit jouer dtrop souvent, il passe au dtotal.


Félicitations pour avoir pris les devants!
isaacg

6

Poisson rouge

def goldfish(m,t,s):
    return 'd' if 'd' in t[-3:] else 'c'

Un poisson rouge ne pardonne jamais, mais il oublie vite.


6

filou (réintégré à nouveau)

Seuls les 10 derniers jeux sont comptabilisés, mais divisés en deux blocs de cinq, dont la moyenne est classée comme bonne ou mauvaise.

Si l'adversaire joue en moyenne "sympa", le filou joue de moins en moins sympa. Si les résultats sont ambigus, le filou joue bien pour attirer l'adversaire en sécurité. Si l'adversaire semble jouer "mal", le filou se venge.

L'idée est de collecter de temps en temps des points sur des joueurs naïfs, tout en attrapant tôt des joueurs trompeurs.

import random
def trickster(player,opponent,state):
    pBad = 0.75
    pNice = 0.8
    pReallyBad =0.1
    decay = 0.98
    r = random.random()
    if len(player)<20: #start off nice
        return 'c' 
    else: #now the trickery begins
        last5 = opponent[-5:].count('c')/5.0 > 0.5
        last5old = opponent[-10:-5].count('c')/5.0  > 0.5
        if last5 and last5old: #she is naive, punish her
            pBad = pBad*decay #Increase punishment
            if r<pBad:
                return 'c'
            else:
                return 'd'
        elif last5 ^ last5old: #she is changing her mind, be nice!
            if r<pNice:
                return 'c'
            else:
                return 'd'
        else: #she's ratting you out, retaliate
            pReallyBad = pReallyBad*decay #Retaliate harder
            if r<pReallyBad:
                return 'c'
            else:
                return 'd'

Disclaimer: Je n'ai jamais posté ici auparavant, si je fais quelque chose de mal> s'il vous plaît dites-moi et je corrigerai.


Welcome to the site! Unfortunately, your code doesn't work currently. You have an elif after an else. Could you fix it? Thanks
isaacg

I'm guessing everything from the elif onward should be indented once more?
isaacg

Correct, I'll indent.
Hektor-Waartgard

@isaacg I have updated my answer with a new code. I do not have reputation enough to tell it to you in the question-comments. I am not certain that I am using the state variable correctly, I assume it is an empty list which I can append whatever I want to, correct?
Hektor-Waartgard

2
That won't work. After each turn, it is decided whether the current move is flipped or not (independently for the two players). That decission is then fixed. You will always see the same first move which may be flipped or not, but it won't change.
Christian Sievers

5

Decaying Memory

def decaying_memory(me, them, state):
    m = 0.95
    lt = len(them)

    if not lt:
        state.append(0.0)
        return 'c'

    # If it's the last round, there is no reason not to defect
    if lt >= 299: return 'd'

    state[0] = state[0] * m + (1.0 if them[-1] == 'c' else -1.0)

    # Use a gaussian distribution to reduce variance when opponent is more consistent
    return 'c' if lt < 5 or random.gauss(0, 0.4) < state[0] / ((1-m**lt)/(1-m)) else 'd'

Weighs recent history more. Slowly forgets the past.


5

Kickback

def kickback(m, t, s):
  if len(m) < 10:
    return "c"
  td = t.count("d")
  md = m.count("d")
  f = td/(len(t)+1)
  if f < 0.3:
    return "d" if td > md and random.random() < 0.1 else "c"
  return "c" if random.random() > f+2*f*f else "d"

Some vague ideas...


Congratulations on taking the lead in the version where adaptive basic spells are removed.
isaacg

Thanks. I think it's amazing how different the two results are!
Christian Sievers

4

Doesn't Really Get The Whole "Noise" Thing

def vengeful(m,t,s):
    return 'd' if 'd' in t else 'c'

Never forgives a traitor.


4

sounder:

edit: added retaliation in probably low noise scenarios

basically, if all 4 first moves are cooperate, that means we should expect less noise than usual. defect a little bit every so often to make up for the less points we would get from never defecting, and have it be able to be blamed on noise. we also retaliate if they defect against us

if our opponent does a lot of defecting in those turns (2 or more) we just defect back at them. if it was just noise, the noise would affect our moves anyway.

otherwise, if only 1 move was defect, we just do simple tit for tat the rest of the game.

def sounder(my, their, state):
    if len(my)<4:
        if their.count("d")>1:
            return "d"
        return "c"
    elif len(my) == 4:
        if all(i == "c" for i in their):
            state.append(0)
            return "d"
        elif their.count("c") == 3:
            state.append(1)
            return "c"
        else:
            state.append(2)
    if state[0] == 2:
        return "d"
    if state[0] == 0:
        if not "d" in my[-4:]:
            return "d"
        return their[-1]
    else:
        return their[-1]

3

Alternate

def alternate(m, t, s):
    if(len(m)==0):
        return 'c' if random.random()>.5 else 'd'
    elif(len(m)>290):
        return 'd'
    else:
        return 'd' if m[-1]=='c' else 'c'

Picks randomly in the first round, then alternates. Always defects in the last 10 rounds.


3

Wait for 50

def wait_for_50(m, t, s):
  return 'c' if t.count('d') < 50 else 'd'

After 50 defects, let 'em have it!


I fixed your python while preserving your intent.
isaacg

Congratulations on moving into 3rd place.
isaacg

2

Somehwat naive

def somewhat_naive(m, t, s):
    p_flip = 0.25
    n = 10
    if len(t) < n:
        return 'c' if random.random() > p_flip else 'd'
    d_freq = t[-n:].count('d')/n
    return 'c' if d_freq < p_flip else 'd'

I'll just assume that if you've defected less than the flip probability (roughly) in the last n turns, it was noise and not that you're mean!

Haven't figures out the best n, might look further into that.


2

Every Three

def everyThree(me,him,s):
    if len(me) % 3 == 2:
        return "d"
    if len(me) > 250:
        return "d"
    if him[-5:].count("d")>3:
        return "d"
    else:
        return "c"

Defects every three turns regardless. Also defects the last 50 turns. Also defects if his opponent defected 4 out of 5 of the last rounds.


2

Buckets

def buckets(m, t, s):
    if len(m) <= 5:
        return 'c'
    if len(m) >= 250:
        return 'd'
    d_pct = t[-20:].count('d')/len(t[-20:])
    if random.random() > (2 * d_pct - 0.5):
        return 'c'
    else:
        return 'd'

Plays nice to begin. Looks at their last 20, if < 25% d, returns c, > 75% d, returns d, and in between chooses randomly along a linear probability function. Last 50, defects. Had this at last 10 but saw lots of last 50 defects.

First time here so let me know if something needs to be fixed (or how I can test this).


If you want to test things locally, you can clone the repository, and run noisy-game.py. It takes a while, so you might want to remove some of the opponents in players for quick iterations.
isaacg

Thanks Isaac - I'll have to play with it and do some tinkering.
brian_t

1

Tit-For-Stat

Defects if the opponent has defected more than half the time.

def tit_for_stat(m, t, s):
  if t.count('d') * 2 > len(m):
    return 'd'
  else:
    return 'c'
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.