Dilema ruidoso do prisioneiro iterado

35

Neste desafio, você jogará o dilema do prisioneiro ruidoso e iterado.

O dilema do prisioneiro é um cenário na teoria dos jogos em que existem dois jogadores, cada um com duas opções: cooperar ou defeito. Cada jogador se sai melhor se desertar do que cooperar, mas ambos preferem o resultado em que os dois cooperam com aquele em que os dois desertam.

O dilema do prisioneiro iterado é o mesmo jogo, exceto que você joga contra o mesmo oponente repetidamente e sabe o que o seu oponente jogou no passado. Seu objetivo é sempre acumular a maior pontuação para si mesmo, independentemente de como o seu oponente o faz.

O dilema ruidoso do prisioneiro iterado introduz algum ruído na comunicação. Seu conhecimento do que seu oponente jogou no passado terá algum ruído introduzido. Você também saberá quais movimentos você fez no passado. A taxa de ruído é constante durante uma rodada contra o mesmo oponente, mas diferente entre rodadas diferentes.

Desafio

Neste desafio, você escreverá um programa Python 3 para reproduzir o dilema ruidoso do prisioneiro iterado.

Seu programa receberá três entradas:

  • Seus próprios movimentos, sem movimentos aleatórios aplicados.

  • Os movimentos do seu oponente, com movimentos aleatórios aplicados.

  • Uma variável de estado, que começa como uma lista vazia a cada rodada e que você pode modificar se desejar. Você pode ignorar isso se não quiser usá-lo.

Seu programa deve produzir 'c'para cooperar ou 'd'desertar.

Por exemplo, aqui está um programa que coopera se o oponente cooperou pelo menos 60% do tempo no passado, após a aplicação de movimentos aleatórios e nos 10 primeiros movimentos:

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 você não conhece o Python, escreva sua submissão em pseudocódigo, e alguém (eu ou outro membro do site) pode criar o programa Python correspondente.

Jogabilidade

O corredor do torneio pode ser encontrado aqui: jogo barulhento . Corra noisy-game.pypara executar o torneio. Manterei esse repositório atualizado com novos envios. Programas de exemplo podem ser encontrados em basic.py.

A pontuação geral de um programa é o total de sua pontuação em mais de 100 jogadas.

Um jogo consiste em confrontos round-robin de cada jogador contra cada jogador, incluindo ele próprio. Uma partida consiste em 100 rodadas. Uma rodada consiste em 300 jogadas, cada uma das quais envolve saída 'c'ou 'd'.

Seu envio fará uma comparação com todos os envios, incluindo o seu. Cada confronto será composto de 100 rodadas. Durante cada rodada, uma probabilidade de inversão será escolhida uniformemente aleatoriamente [0, 0.5].

Cada rodada consistirá em 300 jogadas. Em cada movimento, os dois programas receberão todas as reproduções anteriores que tentaram e todas as reproduções anteriores que o outro programa fez, após a aplicação dos lançamentos, e uma variável de estado, que é uma lista mutável que o programa pode modificar, se desejar. Os programas produzirão seus movimentos.

Os movimentos são pontuados da seguinte forma: Se um programa toca a 'c', o programa adversário recebe 2 pontos. Se um programa toca a 'd', esse programa recebe 1 ponto.

Então, cada movimento é invertido independentemente, com probabilidade igual à probabilidade de inverter, e armazenado para exibição ao oponente.

Depois que todas as rodadas foram disputadas, somamos o número de pontos que cada jogador obteve em cada partida. Em seguida, usamos o seguinte sistema de pontuação para calcular a pontuação de cada jogador para o jogo. Essa pontuação é realizada após a conclusão de todos os confrontos.

Pontuação

Usaremos a pontuação evolutiva. Cada programa começa com o mesmo peso. Em seguida, os pesos são atualizados da seguinte forma, para 100 iterações, usando os totais de pontos do jogo:

O novo peso de cada programa é proporcional ao produto do seu peso anterior e ao seu total médio de pontos, ponderado pelos pesos de seus oponentes.

São aplicadas 100 atualizações desse tipo e os pesos finais são a pontuação de cada programa para a execução do jogo.

A pontuação geral será a soma de mais de 100 corridas do jogo.

Os jogadores terão todas as respostas válidas para esse desafio, além de seis programas básicos para começar.

Ressalvas

Não modifique as entradas. Não tente afetar a execução de nenhum outro programa, exceto através da cooperação ou defeito. Não faça uma submissão sacrificial que tente reconhecer outra submissão e beneficiar esse oponente às suas próprias custas. As brechas padrão são proibidas.

EDIT: Os envios não podem duplicar exatamente nenhum dos programas básicos ou envios anteriores.

Se você tiver alguma dúvida não hesite em perguntar.

Resultados atuais

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

Resultados com apenas respostas a esta pergunta e programas básicos que ignoram a jogada do oponente:

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

Ganhando

A competição permanecerá aberta indefinidamente, à medida que novos envios forem publicados. No entanto, declararei um vencedor (aceite uma resposta) com base nos resultados 1 mês após a postagem desta pergunta.

isaacg
fonte
Como tit_for_whoops ignora a jogada do oponente?
LyricLy
@LyricLy Suponho que a categoria se refira aos programas básicos fornecidos pelo Isaac que ignoram seus oponentes.
FryAmTheEggman
11
Entendo direito que você pode usar a variável state para registrar todos os seus movimentos à medida que os envia e, portanto, conhecer seus movimentos verdadeiros e movimentos invertidos e estimar a probabilidade de inversão?
xnor
11
@xnor Você sempre é informado de seus verdadeiros movimentos. Apenas os movimentos dos oponentes podem ser invertidos.
Mnemonic
11
@isaacg Tentei copiar exploit_threshold()várias vezes como exploit_threshold1(), etc e os adicionei à playerslista. Por que obtenho resultados muito diferentes para estratégias idênticas?
NGN

Respostas:

4

Genug ist nicht genug

(também pode ser chamado 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"

Aprendi que o tit original para duas tatuagens esperava duas tatuagens consecutivas, como tit_for_whoopsparece, e de fato parece que devemos perdoar e esquecer (bem, quase ...) as tatuagens únicas anteriores. E muitos jogadores desertam nas últimas rodadas. Eu ainda prefiro ser legal quando tudo está bem até agora, mas a barra de tolerância do bot continua diminuindo.

Peneiradores cristãos
fonte
11

Chapim-para-Ops

Inspirado por uma estratégia do 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'

Defeitos somente se o outro jogador desertou duas vezes seguidas, para evitar mal-entendidos.

LyricLy
fonte
Obrigado por se inscrever! Lembre-se de que, como a probabilidade de virar a média é de 1/4, haverá um salto duplo uma vez a cada 16 movimentos.
Isaacg
Eu adicionei uma variável de estado, que você pode ignorar se não quiser usá-la.
Isaacg 5/05
9

Mudança de Coração

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

Tem uma mudança de coração no meio. Surpreendentemente bem.

qwewqa
fonte
Parabéns por assumir a liderança / segundo lugar. Estou impressionado e surpreso que um oponente que ignora a estratégia se sai tão bem.
Isaacg
9

Ladrão de estratégia

Inspirado pelo suficiente, change_of_heart e tit-for-whoops. Deveria ser um pouco mais perdoador. Tentei ajustar os números para obter melhores resultados, mas eles não queriam mudar muito.

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'
adebunk
fonte
bem-vindo ao PPCG!
Giuseppe
Parabéns por assumir a liderança!
Isaacg
8

Peitos-Por-Tempo

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 você passou a maior parte do tempo me machucando, eu vou te machucar de volta. Provavelmente.

Azul
fonte
Boa apresentação! Você está atualmente no 1º lugar sem os programas básicos que reconhecem o oponente.
Isaacg 5/05
7

Desconfiança 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'

Quanto mais o oponente me trai, menos posso confiar que era apenas barulho.

Mnemônico
fonte
Sim, a coisa sem estado é lamentável, mas eu queria que as submissões fossem uniformes, então é o melhor que posso pensar. Você tem alguma idéia de como adicionar um estado?
Isaacg 5/05
Basta ter um stateargumento de que, por padrão, é uma lista? As listas são mutáveis, portanto o estado pode ser modificado facilmente.
LyricLy
Como assim? Não vejo como isso poderia ser.
LyricLy
@Mnemonic Acho que sei como implementar isso. Vou dar uma volta.
Isaacg 5/05
Eu adicionei uma variável de estado, que é inicialmente uma lista vazia e que você pode modificar.
Isaacg 5/05
7

Jedi2Sith

Começa de maneira agradável e altruísta, mas com o tempo a influência do lado sombrio aumenta cada vez mais, até o ponto sem retorno. Não há como parar essa influência, mas todas as coisas ruins que vê acontecendo contribuem para o poder do lado sombrio ...

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'

Experimente online!

Leo
fonte
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'

Começa com 'c' e desliza gradualmente na direção ou fora de 'd'.

milhas
fonte
Fiz uma reescrita funcionalmente equivalente para usar a variável state, porque ela estava sendo executada bem devagar. Você não precisa alterar nada, no entanto.
Isaacg
6

Tropeço Teimoso

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'

Com base na sua estratégia de limite de exploração, com apenas jogadas consistentes, acompanhadas para alternar entre defeito e principalmente cooperar

UPDATE: Mantém o controle de duas jogadas consecutivas e três consecutivas, punindo apenas sob condições mais severas e adicionando uma escolha aleatória quando não tiver certeza

ATUALIZAÇÃO 2: condição removida e função de distribuição adicionada

JMigst
fonte
Contratações para escrever o primeiro programa a assumir a liderança!
Isaacg
6

Noise Bot

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

Estou definitivamente cooperar bot. Isso é apenas barulho.

Chris
fonte
6

Já é suficiente

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"

Começa como tit para duas tatuagens em que as duas tatuagens não precisam ser consecutivas (ao contrário tit_for_whoops). Se tiver que jogar com dmuita frequência, ele será dtotal.

Peneiradores cristãos
fonte
Parabéns por assumir a liderança!
Isaacg
6

Goldfish Bot

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

Um peixe dourado nunca perdoa, mas esquece rapidamente.

Chris
fonte
6

malandro (restabelecido novamente)

Apenas as últimas 10 jogadas são contabilizadas, mas divididas em dois blocos de cinco, que são calculados em média com cada um classificado como bom ou ruim.

Se o oponente joga em média "bom", o trapaceiro joga cada vez menos. Se os resultados forem ambíguos, o malandro joga bem para atrair o oponente para a segurança. Se o oponente parece estar jogando "mal", o trapaceiro retalia.

A idéia é reunir pontos de vez em quando de jogadores ingênuos, ao mesmo tempo em que pega cedo os enganosos.

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'

Isenção de responsabilidade: eu nunca postei aqui antes, se estiver fazendo algo errado> informe-me e eu corrijo.

Hektor-Waartgard
fonte
Bem vindo ao site! Infelizmente, seu código não funciona atualmente. Você tem um elif após outro. Você poderia consertar isso? Obrigado
isaacg
Estou supondo que tudo, desde o elif em diante, deva ser recuado mais uma vez?
Isaacg
Correto, eu vou recuar.
Hektor-Waartgard
@isaacg Atualizei minha resposta com um novo código. Não tenho reputação suficiente para lhe contar nos comentários das perguntas. Não tenho certeza de que estou usando a variável state corretamente, presumo que seja uma lista vazia que eu possa acrescentar o que eu quiser, correto?
Hektor-Waartgard
2
Isso não vai funcionar. Após cada turno, é decidido se o movimento atual é invertido ou não (independentemente para os dois jogadores). Essa decisão é então fixada. Você sempre verá o mesmo primeiro movimento que pode ser invertido ou não, mas não será alterado.
Christian Sievers
5

Memória deteriorada

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 mais a história recente. Lentamente esquece o passado.

qwewqa
fonte
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"

Algumas idéias vagas ...

Peneiradores cristãos
fonte
Parabéns por assumir a liderança na versão em que os feitiços básicos adaptativos são removidos.
Isaacg
Obrigado. Eu acho incrível como os dois resultados são diferentes!
Christian Sievers
4

Realmente não recebe toda a coisa do "ruído"

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

Nunca perdoa um traidor.

Chris
fonte
4

sirene:

editar: retaliação adicionada em cenários provavelmente com pouco ruído

basicamente, se todos os quatro primeiros movimentos forem cooperados, isso significa que devemos esperar menos ruído do que o habitual. desertar um pouco de vez em quando para compensar os menos pontos que obteríamos por nunca desertar, e poder ser responsabilizado pelo ruído. também retaliamos se eles desertarem contra nós

se o nosso oponente faz muita deserção nesses turnos (2 ou mais), nós apenas desaprovamos. se fosse apenas barulho, o barulho afetaria nossos movimentos de qualquer maneira.

caso contrário, se apenas um movimento tiver sido defeituoso, nós apenas fazemos uma análise simples pelo resto do jogo.

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]
Limão destrutível
fonte
3

Alternar

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'

Escolhe aleatoriamente no primeiro turno e depois alterna. Sempre defeitos nas últimas 10 rodadas.

Chris
fonte
3

Aguarde 50

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

Após 50 defeitos, vamos tê-lo!

MegaTom
fonte
Corrigi seu python, preservando sua intenção.
Isaacg
Parabéns por passar para o 3º lugar.
Isaacg
2

Somehwat ingênuo

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'

Eu assumirei que, se você desertou menos do que a probabilidade de virar (aproximadamente) nas últimas n voltas, era ruído e não que você seja malvado!

Ainda não descobriu o melhor n , pode se aprofundar nisso.

JeroendeK
fonte
2

A cada três

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"

Defeitos a cada três turnos, independentemente. Também falha nos últimos 50 turnos. Também desertará se o seu oponente tiver desertado em 4 dos 5 dos últimos rounds.

MegaTom
fonte
2

Baldes

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'

É bom começar. Observa os últimos 20, se <25% d, retorna c,> 75% d, retorna d e entre escolhe aleatoriamente ao longo de uma função de probabilidade linear. Últimos 50, defeitos. Teve isso no último 10, mas viu muitos dos últimos 50 defeitos.

Primeira vez aqui, então deixe-me saber se algo precisa ser corrigido (ou como posso testar isso).

brian_t
fonte
Se você quiser testar as coisas localmente, poderá clonar o repositório e executar noisy-game.py. Demora um pouco, então você pode remover alguns dos adversários playerspara fazer iterações rápidas.
Isaacg
Obrigado Isaac - vou ter que brincar com ele e fazer alguns ajustes.
Brian_t 14/0518
1

Tit-For-Stat

Defeitos se o oponente desertou mais da metade do tempo.

def tit_for_stat(m, t, s):
  if t.count('d') * 2 > len(m):
    return 'd'
  else:
    return 'c'
RamenChef
fonte