Dilemma del prigioniero iterato rumoroso


34

In questa sfida, giocherai al rumoroso dilemma del prigioniero iterato.

Il dilemma del prigioniero è uno scenario nella teoria dei giochi in cui ci sono due giocatori, ognuno con due opzioni: cooperare o difetto. Ogni giocatore fa meglio per se stesso se difettano che se cooperano, ma entrambi i giocatori preferirebbero il risultato in cui entrambi i giocatori cooperano a quello in cui entrambi i giocatori difettano.

Il dilemma del prigioniero ripetuto è lo stesso gioco, tranne per il fatto che giochi ripetutamente contro lo stesso avversario e sai cosa ha giocato il tuo avversario in passato. Il tuo obiettivo è sempre quello di accumulare il punteggio più alto per te, indipendentemente da come lo fa il tuo avversario.

Il rumoroso dilemma del prigioniero ripetuto introduce un po 'di rumore nella comunicazione. La tua conoscenza di ciò che il tuo avversario ha giocato in passato avrà un po 'di rumore introdotto. Saprai anche quali mosse hai fatto in passato. La frequenza del rumore è costante per un round contro lo stesso avversario, ma diversa tra round diversi.

Sfida

In questa sfida, scriverai un programma Python 3 per giocare al rumoroso dilemma del prigioniero iterato.

Il tuo programma riceverà tre input:

  • Le tue mosse, senza capovolgere casualmente.

  • Le mosse del tuo avversario, con lanci casuali applicati.

  • Una variabile di stato, che inizia come un elenco vuoto ogni round e che è possibile modificare se lo si desidera. Puoi ignorarlo se non vuoi usarlo.

Il tuo programma dovrebbe produrre 'c'per cooperare o 'd'per difettoso.

Ad esempio, ecco un programma che coopera se l'avversario ha collaborato almeno il 60% delle volte in passato, dopo l'applicazione di lanci casuali, e per i primi 10 lanci:

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'

Se non conosci Python, scrivi la tua richiesta in pseudocodice e qualcuno (io o un altro membro del sito) può creare il programma Python corrispondente.

gameplay

Il corridore del torneo può essere trovato qui: gioco rumoroso . Corri noisy-game.pyper correre il torneo. Terrò quel repository aggiornato con nuovi invii. Programmi di esempio sono disponibili in basic.py.

Il punteggio complessivo di un programma è il totale del suo punteggio in 100 partite giocate.

Un gioco consiste in match-round-robin di ciascun giocatore contro ogni giocatore, incluso se stesso. Un matchup è composto da 100 round. Un round è composto da 300 mosse, ognuna delle quali prevede l'output 'c'o 'd'.

Il tuo invio giocherà un matchup contro ogni invio, incluso il tuo. Ogni incontro sarà composto da 100 round. Durante ogni round, una probabilità di inversione verrà scelta in modo uniforme in modo casuale [0, 0.5].

Ogni round sarà composto da 300 mosse. Ad ogni mossa, entrambi i programmi riceveranno tutte le riproduzioni precedenti che hanno tentato, e tutte le riproduzioni precedenti eseguite dall'altro programma, dopo l'applicazione dei flip, e una variabile di stato, che è un elenco mutabile che il programma può modificare se lo desidera. I programmi produrranno le loro mosse.

Le mosse vengono segnate come segue: se un programma gioca a 'c', il programma avversario ottiene 2 punti. Se un programma suona a 'd', quel programma ottiene 1 punto.

Quindi, ogni mossa viene lanciata indipendentemente con probabilità pari alla probabilità di ribaltamento e memorizzata per essere mostrata all'avversario.

Dopo che tutti i round sono stati giocati, sommiamo il numero di punti ottenuti da ciascun giocatore in ogni matchup. Quindi, utilizziamo il seguente sistema di punteggio per calcolare il punteggio di ciascun giocatore per il gioco. Questo punteggio viene eseguito dopo che tutti i matchup sono stati completati.

punteggio

Useremo il punteggio evolutivo. Ogni programma inizia con lo stesso peso. Quindi, i pesi vengono aggiornati come segue, per 100 iterazioni, utilizzando i totali dei punti del gioco:

Il nuovo peso di ciascun programma è proporzionale al prodotto del suo peso precedente e al suo punteggio medio totale, ponderato dai pesi dei suoi avversari.

Vengono applicati 100 di questi aggiornamenti e i pesi finali sono il punteggio di ciascun programma per quella corsa del gioco.

I punteggi complessivi saranno la somma di oltre 100 esecuzioni del gioco.

I giocatori avranno tutte le risposte valide a questa sfida, oltre a sei programmi di base per iniziare.

Avvertenze

Non modificare gli ingressi. Non tentare di influire sull'esecuzione di altri programmi, se non attraverso la cooperazione o il difetto. Non fare una sottomissione sacrificale che tenti di riconoscere un'altra sottomissione e avvantaggiare quell'avversario a proprie spese. Le scappatoie standard sono vietate.

EDIT: le presentazioni non possono duplicare esattamente nessuno dei programmi di base o precedenti.

Se avete domande non esitate a chiedere.

Risultati attuali

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

Risultati con sole risposte a questa domanda e programmi di base che ignorano il gioco dell'avversario:

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

vincente

Il concorso rimarrà aperto a tempo indeterminato, poiché verranno inviati nuovi invii. Tuttavia, dichiarerò un vincitore (accetto una risposta) in base ai risultati 1 mese dopo la pubblicazione della domanda.


In che modo tit_for_whoops ignora il gioco dell'avversario?
Lirico

@Lirico: Suppongo che la categoria si riferisca ai programmi di base forniti da Isaac che ignorano i loro avversari.
FryAmTheEggman

1
Capisco bene che puoi usare la variabile di stato per registrare tutte le tue mosse mentre le invii, e quindi conoscere sia le tue vere mosse che le mosse lanciate e stimare la probabilità di vibrazione?
xnor

1
@xnor Ti vengono sempre raccontate le tue vere mosse. Solo le mosse degli avversari possono essere lanciate.

1
@isaacg Ho provato a copiare exploit_threshold()più volte come exploit_threshold1(), ecc. e le ho aggiunte playersall'elenco. Perché ottengo risultati molto diversi per strategie identiche?
ngn,

Risposte:


4

Genug ist nicht genug

(potrebbe anche essere chiamato enough2o 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"

Ho imparato che il titolo originale per due tatuaggi ha aspettato due tatuaggi consecutivi come tit_for_whoopsfa, e in effetti sembra che dovremmo perdonare e dimenticare (beh, quasi ...) tatuaggi singoli precedenti. E molti giocatori difettano negli ultimi round. Preferisco ancora essere gentile quando tutto è andato bene finora, ma la barra di tolleranza del bot continua ad abbassarsi.


10

Tit-for-Ops

Ispirato da una strategia di 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'

Difetti solo se l'altro giocatore ha disertato due volte di seguito, per evitare equivoci.


grazie per la sua iscrizione! Tieni presente che poiché la probabilità di capovolgimento è in media 1/4, ci sarà un doppio capovolgimento ogni 16 mosse circa.
isaacg,

Ho aggiunto una variabile di stato, che puoi ignorare se non vuoi usarla.
isaacg,

9

Cambio di cuore

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

Ha parzialmente cambiato il cuore. Fa sorprendentemente bene.


Congratulazioni per aver preso il comando / secondo posto. Sono impressionato e sorpreso dal fatto che un avversario che ignora la strategia faccia così bene.
isaacg

9

Stealer di strategia

Ispirato da abbastanza, change_of_heart e tit-for-whoops. Dovrebbe essere un po 'più indulgente. Ho provato a modificare i numeri per i migliori risultati, ma non volevano cambiare molto.

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'

benvenuto in PPCG!
Giuseppe,

Congratulazioni per aver preso l'iniziativa!
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"

Se hai passato gran parte del tempo a farmi del male, ti farò solo del male. Probabilmente.


Bella presentazione! Attualmente sei al 1 ° posto senza i programmi di base consapevoli dell'avversario.
isaacg,

7

Diffidenza crescente

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'

Più l'avversario mi tradisce, meno posso fidarmi che fosse solo rumore.


Sì, la cosa senza stato è sfortunata, ma volevo che le domande fossero uniformi, quindi questa è la migliore che mi venga in mente. Hai idea di come aggiungere uno stato?
isaacg,

stateHai solo un argomento che di default è un elenco? Le liste sono mutabili, quindi lo stato sarebbe facilmente modificabile.
Lirico

Come mai? Non vedo come potrebbe essere.
Lirico

@Mnemonic Penso di sapere come implementarlo. Ci darò un vortice.
isaacg,

Ho aggiunto una variabile di stato, che è inizialmente un elenco vuoto e che è possibile modificare.
isaacg,

7

Jedi2Sith

Inizia tutto bello e altruistico, ma col tempo l'influenza del lato oscuro diventa costantemente più forte, fino al punto di non ritorno. Non si può fermare questa influenza, ma tutte le cose brutte che vede accadere contribuiscono solo al potere del lato oscuro ...

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'

Provalo online!


6

Slider

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'

Inizia con 'c' e scivola gradualmente verso o lontano da 'd'.


Ho fatto una riscrittura funzionalmente equivalente di questo per usare la variabile di stato, perché stava funzionando piuttosto lentamente. Non è necessario modificare nulla, tuttavia.
isaacg

6

Stumbler testardo

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'

Basato sulla tua strategia di soglia di exploit con solo giochi coerenti tenuti traccia del passaggio tra difetto e cooperare principalmente

AGGIORNAMENTO: Tiene traccia di due giochi consecutivi e tre consecutivi, punendo solo in condizioni più difficili e aggiungendo una scelta casuale quando non sei sicuro

AGGIORNAMENTO 2: condizione rimossa e funzione di distribuzione aggiunta


Contrattazioni sulla scrittura del primo programma per prendere il comando!
isaacg,

6

Noise Bot

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

Sono sicuramente cooperare bot. Questo è solo rumore.


6

Adesso basta

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"

Inizia come tit per due tatuaggi in cui i due tatuaggi non devono essere consecutivi (a differenza tit_for_whoops). Se deve giocare dtroppo spesso diventa d-totale.


Congratulazioni per aver preso l'iniziativa!
isaacg

6

Goldfish Bot

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

Un pesce rosso non perdona mai, ma dimentica rapidamente.


6

imbroglione (ripristinato di nuovo)

Sono conteggiate solo le ultime 10 giocate, ma divise in due blocchi di cinque, che sono mediati con ciascuno classificato come buono o cattivo.

Se l'avversario gioca in media "bello", il truffatore gioca sempre meno bene. Se i risultati sono ambigui, il truffatore gioca bene per attirare l'avversario in sicurezza. Se l'avversario sembra giocare "male", il truffatore si ritira.

L'idea è di raccogliere punti di tanto in tanto da giocatori ingenui, catturando presto inganni ingannevoli.

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: non ho mai pubblicato qui prima, se sto facendo qualcosa di sbagliato> per favore dimmelo e correggerò.


Benvenuti nel sito! Sfortunatamente, il tuo codice non funziona attualmente. Hai un elfo dopo un altro. Potresti risolverlo? Grazie
isaacg

Immagino che tutto, dall'elif in poi, dovrebbe essere rientrato di nuovo?
isaacg

Esatto, indenterò.
Hektor-Waartgard

@isaacg Ho aggiornato la mia risposta con un nuovo codice. Non ho abbastanza reputazione per dirtelo nei commenti alle domande. Non sono sicuro di utilizzare correttamente la variabile di stato, presumo sia un elenco vuoto che posso aggiungere qualunque cosa io voglia, giusto?
Hektor-Waartgard,

2
Non funzionerà Dopo ogni turno, viene deciso se la mossa corrente viene lanciata o meno (indipendentemente per i due giocatori). Tale decisione viene quindi fissata. Vedrai sempre la stessa prima mossa che può essere capovolta o meno, ma non cambierà.
Christian Sievers,

5

Memoria in decomposizione

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'

Pesa di più la storia recente. Dimentica lentamente il passato.


5

tangente

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"

Alcune idee vaghe ...


Congratulazioni per aver preso il comando nella versione in cui vengono rimossi gli incantesimi di base adattivi.
isaacg,

Grazie. Penso che sia sorprendente quanto siano diversi i due risultati!
Christian Sievers,

4

Non si ottiene davvero tutta la cosa "rumorosa"

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

Non perdona mai un traditore.


4

sounder:

modifica: aggiunta ritorsione in scenari probabilmente a basso rumore

fondamentalmente, se tutte e 4 le prime mosse cooperano, ciò significa che dovremmo aspettarci meno rumore del solito. ogni tanto un po 'di difetto per compensare il minor numero di punti che non avremmo mai ricevuto e avremmo potuto dare la colpa al rumore. vendiamo anche se difettano contro di noi

se il nostro avversario fa un sacco di difetti in quei turni (2 o più) ci limitiamo a difenderli. se fosse solo rumore, il rumore influenzerebbe comunque le nostre mosse.

in caso contrario, se solo 1 mossa fosse un difetto, faremo semplicemente una tit per il resto del gioco.

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

Alternato

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'

Seleziona in modo casuale al primo turno, quindi si alterna. Difetti sempre negli ultimi 10 round.


3

Aspetta 50

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

Dopo 50 difetti, lasciali avere!


Ho riparato il tuo pitone preservando le tue intenzioni.
isaacg,

Congratulazioni per essere passato al 3 ° posto.
isaacg,

2

Somehwat ingenuo

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'

Suppongo che se hai disertato meno della probabilità di capovolgere (approssimativamente) nell'ultimo n turni, era rumore e non che sei cattivo!

Non ho capito il migliore n , potrebbe approfondire questo.


2

Ogni tre

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"

Difetti ogni tre turni indipendentemente. Difetti anche negli ultimi 50 turni. Difetti anche se il suo avversario ha disertato 4 su 5 degli ultimi round.


2

secchi

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'

Gioca bene per iniziare. Guarda i loro ultimi 20, se <25% d, restituisce c,> 75% d, restituisce d, e nel mezzo sceglie casualmente lungo una funzione di probabilità lineare. Ultimi 50, difetti. Ho avuto questo alla fine 10 ma ho visto molti degli ultimi 50 difetti.

Prima volta qui quindi fammi sapere se qualcosa deve essere riparato (o come posso provarlo).


Se vuoi testare le cose localmente, puoi clonare il repository ed eseguire noisy-game.py. Ci vuole un po ', quindi potresti voler rimuovere alcuni degli avversari playersper veloci iterazioni.
isaacg,

Grazie Isaac - Dovrò giocarci e fare qualche armeggiamento.
brian_t

1

Tit-for-Stat

Difetti se l'avversario ha disertato per più della metà delle volte.

def tit_for_stat(m, t, s):
  if t.count('d') * 2 > len(m):
    return 'd'
  else:
    return 'c'
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.