Trilema Iterado do Prisioneiro


19

ESTADO DO DESAFIO: ABERTO

Comente, abra um PR ou grite comigo se estiver sentindo falta do seu bot.


O dilema do prisioneiro ... com três opções. Louco, hein?

Aqui está a nossa matriz de pagamento. Jogador A à esquerda, B na parte superior

A,B| C | N | D
---|---|---|---
 C |3,3|4,1|0,5
 N |1,4|2,2|3,2
 D |5,0|2,3|1,1

A matriz de pagamento é projetada para que seja melhor os dois jogadores sempre cooperarem, mas você pode ganhar (geralmente) escolhendo Neutro ou Deserção.

Aqui estão alguns exemplos de bots (concorrentes).

# turns out if you don't actually have to implement __init__(). TIL!

class AllC:
    def round(self, _): return "C"
class AllN:
    def round(self, _): return "N"
class AllD:
    def round(self, _): return "D"
class RandomBot:
    def round(self, _): return random.choice(["C", "N", "D"])

# Actually using an identically-behaving "FastGrudger".
class Grudger:
    def __init__(self):
        self.history = []
    def round(self, last):
        if(last):
            self.history.append(last)
            if(self.history.count("D") > 0):
                return "D"
        return "C"

class TitForTat:
    def round(self, last):
        if(last == "D"):
            return "D"
        return "C"

Seu bot é uma classe Python3. Uma nova instância é criada para cada jogo e round()é chamada a cada rodada, com a escolha do seu oponente da última rodada (ou Nenhuma, se for a primeira rodada)

Há uma recompensa de 50 representantes para o vencedor em um mês.

Específicos

  • Todo bot joga todos os outros bot (1v1), inclusive ele mesmo, nas rodadas [REDACTED].
  • Falhas padrão não permitidas.
  • Não mexa com nada fora da sua classe ou outras desvantagens ocultas.
  • Você pode enviar até cinco bots.
  • Sim, você pode implementar o aperto de mão.
  • Qualquer resposta que não C, Nou Dserá silenciosamente tomado como N.
  • Os pontos de cada bot de cada jogo que eles jogam serão totalizados e comparados.

Controlador

Verifica!

Outras línguas

Vou montar uma API, se alguém precisar.

Pontuações: 27/11/2018

27 bots, 729 games.

name            | avg. score/round
----------------|-------------------
PatternFinder   | 3.152
DirichletDice2  | 3.019
EvaluaterBot    | 2.971
Ensemble        | 2.800
DirichletDice   | 2.763
Shifting        | 2.737
FastGrudger     | 2.632
Nash2           | 2.574
HistoricAverage | 2.552
LastOptimalBot  | 2.532
Number6         | 2.531
HandshakeBot    | 2.458
OldTitForTat    | 2.411
WeightedAverage | 2.403
TitForTat       | 2.328
AllD            | 2.272
Tetragram       | 2.256
Nash            | 2.193
Jade            | 2.186
Useless         | 2.140
RandomBot       | 2.018
CopyCat         | 1.902
TatForTit       | 1.891
NeverCOOP       | 1.710
AllC            | 1.565
AllN            | 1.446
Kevin           | 1.322

1
Como os robôs são colocados um contra o outro? Recebo do Grudger que sempre existem dois bots um contra o outro e a última escolha do inimigo é passada para o bot. Quantas rodadas são jogadas? E para um jogo: apenas o resultado conta (ou seja, quem ganhou) ou também os pontos?
Coruja preta Kai

1
Você obteria mais entradas se tornasse esse idioma independente de idioma ou, pelo menos, mais amplo. Você pode ter uma classe python de wrapper que gera um processo e envia comandos de texto para receber respostas de texto.
Sparr

1
Feito. Isso ficou na caixa de areia por um mês!
SIGSTACKFAULT

2
Se você enrolar mais de main.py em while len(botlist) > 1:com botlist.remove(lowest_scoring_bot)a parte inferior do loop, você começa um torneio de eliminação com resultados interessantes.
Sparr

1
Outra versão disso algum dia poderá passar por todo o histórico de interação, e não apenas pela última jogada. Não muda muito, embora simplifique levemente o código do usuário. Mas isso permitiria extensões, como canais de comunicação ruidosos que esclarecem com o tempo: "Realmente, um D, mesmo que eu tenha dito C quatro vezes seguidas? Não, eu não disse D; o que você me leva? para? Oh, desculpe, podemos simplesmente esquecer essa rodada? "
21818 Scott Sauyet

Respostas:


10

EvaluaterBot

class EvaluaterBot:
    def __init__(self):
        self.c2i = {"C":0, "N":1, "D":2}
        self.i2c = {0:"C", 1:"N", 2:"D"}
        self.history = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
        self.last = [None, None]

    def round(self, last):
        if self.last[0] == None:
            ret = 2
        else:
            # Input the latest enemy action (the reaction to my action 2 rounds ago)
            # into the history
            self.history[self.last[0]][self.c2i[last]] += 1
            # The enemy will react to the last action I did
            prediction,_ = max(enumerate(self.history[self.last[1]]), key=lambda l:l[1])
            ret = (prediction - 1) % 3
        self.last = [self.last[1], ret]
        return self.i2c[ret]

Ganha contra todos os bots enviados anteriormente, exceto (talvez) o bot aleatório (mas pode ter uma vantagem, porque escolhe D em um empate e D deve ser ideal) e joga um empate constante contra si mesmo.


Sim, bate tudo.
SIGSTACKFAULT

Risque isso, PatternFinder bate um pouco.
SIGSTACKFAULT

7

Equilíbrio de Nash

Esse bot participou de uma aula de teoria dos jogos na faculdade, mas era preguiçoso e não foi para a classe em que abordava jogos iterados. Então, ele joga apenas um jogo de equilíbrio nash misto. Acontece que 1/5 2/5 2/5 é o NE misto para os pagamentos.

class NashEquilibrium:
    def round(self, _):
        a = random.random()
        if a <= 0.2:
            return "C"
        elif a <= 0.6:
            return "N"
        else:
            return "D" 

Equilíbrio de Nash com abuso constante

Esse bot pegou uma ou duas lições do irmão preguiçoso. O problema de seu irmão preguiçoso era que ele não se aproveitava de estratégias fixas. Esta versão verifica se o oponente é um jogador constante ou um titfortat e joga de acordo, caso contrário, ele joga o equilíbrio nash regular.

A única desvantagem é que ele calcula a média de 2,2 pontos por turno contra si mesmo.

class NashEquilibrium2:

    def __init__(self):
        self.opphistory = [None, None, None]
        self.titfortatcounter = 0
        self.titfortatflag = 0
        self.mylast = "C"
        self.constantflag = 0
        self.myret = "C"

    def round(self, last):
        self.opphistory.pop(0)
        self.opphistory.append(last)

        # check if its a constant bot, if so exploit
        if self.opphistory.count(self.opphistory[0]) == 3:
            self.constantflag = 1
            if last == "C":
                 self.myret = "D"
            elif last == "N":
                 self.myret = "C"
            elif last == "D":
                 self.myret = "N"

        # check if its a titfortat bot, if so exploit
        # give it 2 chances to see if its titfortat as it might happen randomly
        if self.mylast == "D" and last == "D":
            self.titfortatcounter = self.titfortatcounter + 1

        if self.mylast == "D" and last!= "D":
            self.titfortatcounter = 0

        if self.titfortatcounter >= 3:
            self.titfortatflag = 1

        if self.titfortatflag == 1:
            if last == "C":
                 self.myret = "D"
            elif last == "D":
                 self.myret = "N"    
            elif last == "N":
                # tit for tat doesn't return N, we made a mistake somewhere
                 self.titfortatflag = 0
                 self.titfortatcounter = 0

        # else play the single game nash equilibrium
        if self.constantflag == 0 and self.titfortatflag == 0:
            a = random.random()
            if a <= 0.2:
                self.myret = "C"
            elif a <= 0.6:
                self.myret = "N"
            else:
                self.myret = "D"


        self.mylast = self.myret
        return self.myret

1
O NashEquilibrium.round precisa receber argumentos, mesmo que não os utilize, para ajustar o protótipo de função esperado.
Raio

Obrigado fixa-lo
Ofya

Um pouco mais curto:class NashEquilibrium: def round(self, _): a = random.random() for k, v in [(0.2, "C"), (0.6, "N"), (1, "D")]: if a <= k: return v
Robert Grant

7

TatForTit

class TatForTit:
    def round(self, last):
        if(last == "C"):
            return "N"
        return "D"

Esse bot alternará a escolha do DNDN enquanto o TitForTat alterna o CDCD, para um ganho líquido médio de 3 pontos por rodada, se eu li a matriz de pagamento corretamente. Eu acho que isso pode ser ideal contra TitForTat. Obviamente, poderia ser melhorado detectar um oponente não-TFT e adotar outras estratégias, mas eu estava apenas buscando a recompensa original.


6

PatternFinder

class PatternFinder:
    def __init__(self):
        import collections
        self.size = 10
        self.moves = [None]
        self.other = []
        self.patterns = collections.defaultdict(list)
        self.counter_moves = {"C":"D", "N":"C", "D":"N"}
        self.initial_move = "D"
        self.pattern_length_exponent = 1
        self.pattern_age_exponent = 1
        self.debug = False
    def round(self, last):
        self.other.append(last)
        best_pattern_match = None
        best_pattern_score = None
        best_pattern_response = None
        self.debug and print("match so far:",tuple(zip(self.moves,self.other)))
        for turn in range(max(0,len(self.moves)-self.size),len(self.moves)):
            # record patterns ending with the move that just happened
            pattern_full = tuple(zip(self.moves[turn:],self.other[turn:]))
            if len(pattern_full) > 1:
                pattern_trunc = pattern_full[:-1]
                pattern_trunc_result = pattern_full[-1][1]
                self.patterns[pattern_trunc].append([pattern_trunc_result,len(self.moves)-1])
            if pattern_full in self.patterns:
                # we've seen this pattern at least once before
                self.debug and print("I've seen",pattern_full,"before:",self.patterns[pattern_full])
                for [response,turn_num] in self.patterns[pattern_full]:
                    score = len(pattern_full) ** self.pattern_length_exponent / (len(self.moves) - turn_num) ** self.pattern_age_exponent
                    if best_pattern_score == None or score > best_pattern_score:
                        best_pattern_match = pattern_full
                        best_pattern_score = score
                        best_pattern_response = response
                    # this could be much smarter about aggregating previous responses
        if best_pattern_response:
            move = self.counter_moves[best_pattern_response]
        else:
            # fall back to playing nice
            move = "C"
        self.moves.append(move)
        self.debug and print("I choose",move)
        return move

Esse bot procura ocorrências anteriores do estado do jogo recente para ver como o oponente respondeu a essas ocorrências, com uma preferência por partidas com padrões mais longas e partidas mais recentes, e depois executa a jogada que "superará" a jogada prevista do oponente. Há muito espaço para que seja mais inteligente com todos os dados que acompanha, mas fiquei sem tempo para trabalhar nisso.


Quando você tiver tempo, lembre-se de dar a ela um passe de otimização? É facilmente o maior tempo gasto.
SIGSTACKFAULT

2
@Blacksilver eu só reduziu o comprimento máximo padrão de 100 para 10. Ele deve ser executado quase que instantaneamente agora se você estiver executando <200 rodadas
Sparr

1
Talvez o uso de um número altamente composto (ou seja, 12) tenha uma pontuação melhor?
SIGSTACKFAULT 13/11

5

Jade

class Jade:
    def __init__(self):
        self.dRate = 0.001
        self.nRate = 0.003

    def round(self, last):
        if last == 'D':
            self.dRate *= 1.1
            self.nRate *= 1.2
        elif last == 'N':
            self.dRate *= 1.03
            self.nRate *= 1.05
        self.dRate = min(self.dRate, 1)
        self.nRate = min(self.nRate, 1)

        x = random.random()
        if x > (1 - self.dRate):
            return 'D'
        elif x > (1 - self.nRate):
            return 'N'
        else:
            return 'C'

Começa otimista, mas fica progressivamente mais amargo à medida que o oponente se recusa a cooperar. Muitas constantes mágicas que provavelmente poderiam ser aprimoradas, mas isso provavelmente não será suficiente para justificar o tempo.


5

Conjunto

Isso executa um conjunto de modelos relacionados. Os modelos individuais consideram diferentes quantidades do histórico e têm a opção de escolher sempre a jogada que otimizará a diferença de pagamento esperada ou selecionará aleatoriamente uma jogada na proporção da diferença de pagamento esperada.

Cada membro do grupo vota em sua jogada preferida. Eles recebem um número de votos igual ao quanto mais ganharam do que o oponente (o que significa que modelos terríveis receberão votos negativos). Qualquer movimento que vencer, o voto será selecionado.

(Eles provavelmente devem dividir seus votos entre os movimentos na proporção de quanto eles favorecem cada um, mas eu não me importo o suficiente para fazer isso agora.)

Ele supera tudo publicado até agora, exceto EvaluaterBot e PatternFinder. (Um a um, ele vence o EvaluaterBot e perde para o PatternFinder).

from collections import defaultdict
import random
class Number6:
    class Choices:
        def __init__(self, C = 0, N = 0, D = 0):
            self.C = C
            self.N = N
            self.D = D

    def __init__(self, strategy = "maxExpected", markov_order = 3):
        self.MARKOV_ORDER = markov_order;
        self.my_choices = "" 
        self.opponent = defaultdict(lambda: self.Choices())
        self.choice = None # previous choice
        self.payoff = {
            "C": { "C": 3-3, "N": 4-1, "D": 0-5 },
            "N": { "C": 1-4, "N": 2-2, "D": 3-2 },
            "D": { "C": 5-0, "N": 2-3, "D": 1-1 },
        }
        self.total_payoff = 0

        # if random, will choose in proportion to payoff.
        # otherwise, will always choose argmax
        self.strategy = strategy
        # maxExpected: maximize expected relative payoff
        # random: like maxExpected, but it chooses in proportion to E[payoff]
        # argmax: always choose the option that is optimal for expected opponent choice

    def update_opponent_model(self, last):
        for i in range(0, self.MARKOV_ORDER):
            hist = self.my_choices[i:]
            self.opponent[hist].C += ("C" == last)
            self.opponent[hist].N += ("N" == last)
            self.opponent[hist].D += ("D" == last)

    def normalize(self, counts):
        sum = float(counts.C + counts.N + counts.D)
        if 0 == sum:
            return self.Choices(1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0)
        return self.Choices(
            counts.C / sum, counts.N / sum, counts.D / sum)

    def get_distribution(self):
        for i in range(0, self.MARKOV_ORDER):
            hist = self.my_choices[i:]
            #print "check hist = " + hist
            if hist in self.opponent:
                return self.normalize(self.opponent[hist])

        return self.Choices(1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0)

    def choose(self, dist):
        payoff = self.Choices()
        # We're interested in *beating the opponent*, not
        # maximizing our score, so we optimize the difference
        payoff.C = (3-3) * dist.C + (4-1) * dist.N + (0-5) * dist.D
        payoff.N = (1-4) * dist.C + (2-2) * dist.N + (3-2) * dist.D
        payoff.D = (5-0) * dist.C + (2-3) * dist.N + (1-1) * dist.D

        # D has slightly better payoff on uniform opponent,
        # so we select it on ties
        if self.strategy == "maxExpected":
            if payoff.C > payoff.N:
                return "C" if payoff.C > payoff.D else "D"
            return "N" if payoff.N > payoff.D else "D"
        elif self.strategy == "randomize":
            payoff = self.normalize(payoff)
            r = random.uniform(0.0, 1.0)
            if (r < payoff.C): return "C"
            return "N" if (r < payoff.N) else "D"
        elif self.strategy == "argMax":
            if dist.C > dist.N:
                return "D" if dist.C > dist.D else "N"
            return "C" if dist.N > dist.D else "N"

        assert(0) #, "I am not a number! I am a free man!")

    def update_history(self):
        self.my_choices += self.choice
        if len(self.my_choices) > self.MARKOV_ORDER:
            assert(len(self.my_choices) == self.MARKOV_ORDER + 1)
            self.my_choices = self.my_choices[1:]

    def round(self, last):
        if last: self.update_opponent_model(last)

        dist = self.get_distribution()
        self.choice = self.choose(dist)
        self.update_history()
        return self.choice

class Ensemble:
    def __init__(self):
        self.models = []
        self.votes = []
        self.prev_choice = []
        for order in range(0, 6):
            self.models.append(Number6("maxExpected", order))
            self.models.append(Number6("randomize", order))
            #self.models.append(Number6("argMax", order))
        for i in range(0, len(self.models)):
            self.votes.append(0)
            self.prev_choice.append("D")

        self.payoff = {
            "C": { "C": 3-3, "N": 4-1, "D": 0-5 },
            "N": { "C": 1-4, "N": 2-2, "D": 3-2 },
            "D": { "C": 5-0, "N": 2-3, "D": 1-1 },
        }

    def round(self, last):
        if last:
            for i in range(0, len(self.models)):
                self.votes[i] += self.payoff[self.prev_choice[i]][last]

        # vote. Sufficiently terrible models get negative votes
        C = 0
        N = 0
        D = 0
        for i in range(0, len(self.models)):
            choice = self.models[i].round(last)
            if "C" == choice: C += self.votes[i]
            if "N" == choice: N += self.votes[i]
            if "D" == choice: D += self.votes[i]
            self.prev_choice[i] = choice

        if C > D and C > N: return "C"
        elif N > D: return "N"
        else: return "D"

Estrutura de Teste

Caso alguém ache útil, aqui está uma estrutura de teste para analisar correspondências individuais. Python2. Basta colocar todos os oponentes nos quais você está interessado em oponentes.py e alterar as referências ao Ensemble às suas.

import sys, inspect
import opponents
from ensemble import Ensemble

def count_payoff(label, them):
    if None == them: return
    me = choices[label]
    payoff = {
        "C": { "C": 3-3, "N": 4-1, "D": 0-5 },
        "N": { "C": 1-4, "N": 2-2, "D": 3-2 },
        "D": { "C": 5-0, "N": 2-3, "D": 1-1 },
    }
    if label not in total_payoff: total_payoff[label] = 0
    total_payoff[label] += payoff[me][them]

def update_hist(label, choice):
    choices[label] = choice

opponents = [ x[1] for x 
    in inspect.getmembers(sys.modules['opponents'], inspect.isclass)]

for k in opponents:
    total_payoff = {}

    for j in range(0, 100):
        A = Ensemble()
        B = k()
        choices = {}

        aChoice = None
        bChoice = None
        for i in range(0, 100):
            count_payoff(A.__class__.__name__, bChoice)
            a = A.round(bChoice)
            update_hist(A.__class__.__name__, a)

            count_payoff(B.__class__.__name__, aChoice)
            b = B.round(aChoice)
            update_hist(B.__class__.__name__, b)

            aChoice = a
            bChoice = b
    print total_payoff

O controlador está pronto, você não tem que fazer tudo o que ...
SIGSTACKFAULT

1
@Blacksilver percebi que apenas como eu estava prestes a enviar. Mas este funciona nas versões anteriores à 3.6 e fornece informações sobre combinações individuais que podem ajudar a identificar pontos fracos, por isso não foi uma completa perda de tempo.
Raio

Justo; correndo agora. Provavelmente adicionarei opções ao meu controlador para fazer coisas semelhantes.
SIGSTACKFAULT

"É melhor do que tudo postado até agora, exceto Ensemble e PatternFinder" Estou honrado :)
Sparr

@Sparr Oops. Isso deveria dizer EvaluaterBot e PatternFinder. Mas é quando se compara a pontuação total com todo o campo. O PatternFinder continua sendo o único que supera isso em uma correspondência direta.
Raio

4

OldTitForTat

O jogador da velha escola está com preguiça de atualizar para as novas regras.

class OldTitForTat:
    def round(self, last):
        if(last == None)
            return "C"
        if(last == "C"):
            return "C"
        return "D"

3

NeverCOOP

class NeverCOOP:
    def round(self, last):
        try:
            if last in "ND":
                return "D"
            else:
                return "N"
        except:
            return "N"

Se o bot adversário falhar ou for neutro, escolha defeito. Caso contrário, se for o primeiro turno ou o bot oposto cooperar, escolha neutro. Não sei ao certo como isso funcionará ...


Qual é a tentativa / exceto?
SIGSTACKFAULT

1
@Blacksilver Eu suponho que serve para o mesmo propósito que if(last):no seu bot Grudger, detectando se houve uma rodada anterior.
ETHproductions

Ahh, entendi. None in "ND"erros.
SIGSTACKFAULT

Porque if last and last in "ND":era muito complicado?
usar o seguinte comando

3

LastOptimalBot

class LastOptimalBot:
    def round(self, last):
        return "N" if last == "D" else ("D" if last == "C" else "C")

Supõe que o bot oponente sempre jogará o mesmo movimento novamente e escolhe aquele que tem a melhor recompensa contra ele.

Médias:

Me   Opp
2.6  2    vs TitForTat
5    0    vs AllC
4    1    vs AllN
3    2    vs AllD
3.5  3.5  vs Random
3    2    vs Grudger
2    2    vs LastOptimalBot
1    3.5  vs TatForTit
4    1    vs NeverCOOP
1    4    vs EvaluaterBot
2.28 2.24 vs NashEquilibrium

2.91 average overall

oof. Talvez T4T faria melhor como return last.
SIGSTACKFAULT

Gostaria disso! Se o TitForTat fosse return last, o LOB passaria 18-9 ao longo de 6 rodadas, em vez das 13-10 ao longo de 5 rodadas atualmente. Eu acho que está tudo bem como está - não se preocupe em otimizar os bots de exemplo.
Spitemaster

return lastseria uma melhor T4T para este desafio, eu acho
Sparr

Apenas tentei - o if(last): return last; else: return "C"é pior.
SIGSTACKFAULT

Certo, mas como o @Sparr estava dizendo, poderia ser mais apropriado. Depende de você, suponho.
Spitemaster

3

Imitador

class CopyCat:
    def round(self, last):
        if last:
            return last
        return "C"

Copia a última jogada do oponente.
Não espero que isso dê certo, mas ninguém implementou esse clássico ainda.


2

Dados aprimorados de Dirichlet

import random

class DirichletDice2:
    def __init__(self):

        self.alpha = dict(
                C = {'C' : 1, 'N' : 1, 'D' : 1},
                N = {'C' : 1, 'N' : 1, 'D' : 1},
                D = {'C' : 1, 'N' : 1, 'D' : 1}
        )
        self.myLast = [None, None]
        self.payoff = dict(
                C = { "C": 0, "N": 3, "D": -5 },
                N = { "C": -3, "N": 0, "D": 1 },
                D = { "C": 5, "N": -1, "D": 0 }
        )

    def DirichletDraw(self, key):
        alpha = self.alpha[key].values()
        mu = [random.gammavariate(a,1) for a in alpha]
        mu = [m / sum(mu) for m in mu]
        return mu

    def ExpectedPayoff(self, probs):
        expectedPayoff = {}
        for val in ['C','N','D']:
            payoff = sum([p * v for p,v in zip(probs, self.payoff[val].values())])
            expectedPayoff[val] = payoff
        return expectedPayoff

    def round(self, last):
        if last is None:
            self.myLast[0] = 'D'
            return 'D'

        #update dice corresponding to opponent's last response to my
        #outcome two turns ago
        if self.myLast[1] is not None:
            self.alpha[self.myLast[1]][last] += 1

        #draw probs for my opponent's roll from Dirichlet distribution and then return the optimal response
        mu = self.DirichletDraw(self.myLast[0])
        expectedPayoff = self.ExpectedPayoff(mu)
        res = max(expectedPayoff, key=expectedPayoff.get)

        #update myLast
        self.myLast[1] = self.myLast[0]
        self.myLast[0] = res

        return res    

Esta é uma versão aprimorada do Dirichlet Dice. Em vez de obter a distribuição multinomial esperada da distribuição Dirichlet, ela extrai uma distribuição Multinomial aleatoriamente da distribuição Dirichlet. Então, em vez de extrair do Multinomial e fornecer a resposta ideal para isso, ele fornece a resposta ideal esperada para o Multinomial fornecido usando os pontos. Portanto, a aleatoriedade foi essencialmente deslocada do sorteio Multinomial para o sorteio Dirichlet. Além disso, os priores estão mais planos agora, para incentivar a exploração.

Ele é "aprimorado" porque agora é responsável pelo sistema de pontos, fornecendo o melhor valor esperado contra as probabilidades, enquanto mantém sua aleatoriedade ao desenhar as próprias probabilidades. Antes, eu tentei simplesmente fazer o melhor retorno esperado das probabilidades esperadas, mas isso foi muito ruim porque ficou travado e não explorou o suficiente para atualizar seus dados. Também era mais previsível e explorável.


Submissão original:

Dirichlet Dice

import random

class DirichletDice:
    def __init__(self):

        self.alpha = dict(
                C = {'C' : 2, 'N' : 3, 'D' : 1},
                N = {'C' : 1, 'N' : 2, 'D' : 3},
                D = {'C' : 3, 'N' : 1, 'D' : 2}
        )

        self.Response = {'C' : 'D', 'N' : 'C', 'D' : 'N'}
        self.myLast = [None, None]

    #expected value of the dirichlet distribution given by Alpha
    def MultinomialDraw(self, key):
        alpha = list(self.alpha[key].values())
        probs = [x / sum(alpha) for x in alpha]
        outcome = random.choices(['C','N','D'], weights=probs)[0]
        return outcome

    def round(self, last):
        if last is None:
            self.myLast[0] = 'D'
            return 'D'

        #update dice corresponding to opponent's last response to my
        #outcome two turns ago
        if self.myLast[1] is not None:
            self.alpha[self.myLast[1]][last] += 1

        #predict opponent's move based on my last move
        predict = self.MultinomialDraw(self.myLast[0])
        res = self.Response[predict]

        #update myLast
        self.myLast[1] = self.myLast[0]
        self.myLast[0] = res

        return res

Basicamente, estou assumindo que a resposta do oponente à minha última saída é uma variável multinomial (dado ponderado), uma para cada uma das minhas saídas, então há um dado para "C", um para "N" e um para "D" . Portanto, se meu último lançamento foi, por exemplo, um "N", lancei o "N-dice" para adivinhar qual seria a resposta deles ao meu "N". Começo com um Dirichlet anterior, que pressupõe que meu oponente é um pouco "inteligente" (é mais provável jogar aquele com o melhor retorno contra o meu último lançamento, menos propenso a jogar aquele com o pior retorno). Eu gero a distribuição Multinomial "esperada" a partir do Dirichlet apropriado anterior (este é o valor esperado da distribuição de probabilidade sobre o peso de seus dados). Lancei os dados ponderados da minha última saída,

Começando na terceira rodada, faço uma atualização bayesiana do Dirichlet apropriado antes da última resposta do meu oponente à coisa que joguei duas rodadas atrás. Estou tentando aprender iterativamente o peso dos dados.

Eu também poderia simplesmente escolher a resposta com o melhor resultado "esperado" depois de gerar os dados, em vez de simplesmente rolar os dados e responder ao resultado. No entanto, eu queria manter a aleatoriedade, para que meu bot fique menos vulnerável aos que tentam prever um padrão.


2

Kevin

class Kevin:
    def round(self, last):      
        return {"C":"N","N":"D","D":"C",None:"N"} [last]

Escolhe a pior escolha. O pior bot feito.

Sem utilidade

import random

class Useless:
    def __init__(self):
        self.lastLast = None

    def round(self, last):
        tempLastLast = self.lastLast
        self.lastLast = last

        if(last == "D" and tempLastLast == "N"):
            return "C"
        if(last == "D" and tempLastLast == "C"):
            return "N"

        if(last == "N" and tempLastLast == "D"):
            return "C"
        if(last == "N" and tempLastLast == "C"):
            return "D"

        if(last == "C" and tempLastLast == "D"):
            return "N"
        if(last == "C" and tempLastLast == "N"):
            return "D"

        return random.choice("CND")

Ele analisa os dois últimos movimentos realizados pelo oponente e escolhe o máximo que não foi feito, caso contrário, escolhe algo aleatório. Provavelmente existe uma maneira melhor de fazer isso.


2

Média histórica

class HistoricAverage:
    PAYOFFS = {
        "C":{"C":3,"N":1,"D":5},
        "N":{"C":4,"N":2,"D":2},
        "D":{"C":0,"N":3,"D":1}}
    def __init__(self):
        self.payoffsum = {"C":0, "N":0, "D":0}
    def round(this, last):
        if(last != None):
            for x in this.payoffsum:
               this.payoffsum[x] += HistoricAverage.PAYOFFS[last][x]
        return max(this.payoffsum, key=this.payoffsum.get)

Analisa a história e encontra a ação que teria sido melhor em média. Inicia cooperativa.


Isso poderia correr mais rápido se não recalculasse as médias a cada rodada.
Sparr

@Sparr true. Eu o editei agora.
MegaTom 14/11

1

Média ponderada

class WeightedAverageBot:
  def __init__(self):
    self.C_bias = 1/4
    self.N = self.C_bias
    self.D = self.C_bias
    self.prev_weight = 1/2
  def round(self, last):
    if last:
      if last == "C" or last == "N":
        self.D *= self.prev_weight
      if last == "C" or last == "D":
        self.N *= self.prev_weight
      if last == "N":
        self.N = 1 - ((1 - self.N) * self.prev_weight)
      if last == "D":
        self.D = 1 - ((1 - self.D) * self.prev_weight)
    if self.N <= self.C_bias and self.D <= self.C_bias:
      return "D"
    if self.N > self.D:
      return "C"
    return "N"

O comportamento do oponente é modelado como um triângulo retângulo com cantos para CND a 0,0 0,1 1,0, respectivamente. Cada movimento do oponente muda o ponto dentro desse triângulo em direção àquele canto, e jogamos para vencer o movimento indicado pelo ponto (com C recebendo uma fatia ajustável do triângulo). Em teoria, eu queria que isso tivesse uma memória mais longa com mais peso dos movimentos anteriores, mas, na prática, a meta atual favorece os bots que mudam rapidamente, e isso se transforma em uma aproximação do LastOptimalBot contra a maioria dos inimigos. Postagem para posteridade; talvez alguém seja inspirado.


1

Tetragrama

import itertools

class Tetragram:
    def __init__(self):
        self.history = {x: ['C'] for x in itertools.product('CND', repeat=4)}
        self.theirs = []
        self.previous = None

    def round(self, last):
        if self.previous is not None and len(self.previous) == 4:
            self.history[self.previous].append(last)
        if last is not None:
            self.theirs = (self.theirs + [last])[-3:]

        if self.previous is not None and len(self.previous) == 4:
            expected = random.choice(self.history[self.previous])
            if expected == 'C':
                choice = 'C'
            elif expected == 'N':
                choice = 'C'
            else:
                choice = 'N'
        else:
            choice = 'C'

        self.previous = tuple(self.theirs + [choice])
        return choice

Tente encontrar um padrão nos movimentos do oponente, supondo que eles também estejam assistindo nosso último movimento.


1

Aperto de mão

class HandshakeBot:
  def __init__(self):
    self.handshake_length = 4
    self.handshake = ["N","N","C","D"]
    while len(self.handshake) < self.handshake_length:
      self.handshake *= 2
    self.handshake = self.handshake[:self.handshake_length]
    self.opp_hand = []
    self.friendly = None
  def round(self, last):
    if last:
      if self.friendly == None:
        # still trying to handshake
        self.opp_hand.append(last)
        if self.opp_hand[-1] != self.handshake[len(self.opp_hand)-1]:
          self.friendly = False
          return "D"
        if len(self.opp_hand) == len(self.handshake):
          self.friendly = True
          return "C"
        return self.handshake[len(self.opp_hand)]
      elif self.friendly == True:
        # successful handshake and continued cooperation
        if last == "C":
          return "C"
        self.friendly = False
        return "D"
      else:
        # failed handshake or abandoned cooperation
        return "N" if last == "D" else ("D" if last == "C" else "C")
    return self.handshake[0]

Reconhece quando está jogando contra si mesmo e depois coopera. Caso contrário, imita LastOptimalBot, que parece ser a melhor estratégia de uma linha. Executa pior que LastOptimalBot, em uma quantidade inversamente proporcional ao número de rodadas. Obviamente, seria melhor se houvesse mais cópias no campo * tosse ** piscadela *.


Basta enviar alguns clones que tenham um comportamento diferente do não-handshake.
SIGSTACKFAULT

Isso parece explorador. Eu poderia enviar um desses clones para cada comportamento simples representado aqui.
Sparr

Adicionei uma cláusula extra dizendo que você só pode enviar no máximo cinco bots.
SIGSTACKFAULT

1

ShiftingOptimalBot

class ShiftingOptimalBot:
    def __init__(self):
        # wins, draws, losses
        self.history = [0,0,0]
        self.lastMove = None
        self.state = 0
    def round(self, last):
        if last == None:
            self.lastMove = "C"
            return self.lastMove
        if last == self.lastMove:
            self.history[1] += 1
        elif (last == "C" and self.lastMove == "D") or (last == "D" and self.lastMove == "N") or (last == "N" and self.lastMove == "C"):
            self.history[0] += 1
        else:
            self.history[2] += 1

        if self.history[0] + 1 < self.history[2] or self.history[2] > 5:
            self.state = (self.state + 1) % 3
            self.history = [0,0,0]
        if self.history[1] > self.history[0] + self.history[2] + 2:
            self.state = (self.state + 2) % 3
            self.history = [0,0,0]

        if self.state == 0:
            self.lastMove = "N" if last == "D" else ("D" if last == "C" else "C")
        elif self.state == 1:
            self.lastMove = last
        else:
            self.lastMove = "C" if last == "D" else ("N" if last == "C" else "D")
        return self.lastMove

Este bot usa o algoritmo de LastOptimalBot, desde que esteja ganhando. Se o outro bot começar a prever, no entanto, ele começará a jogar o que o oponente tiver jogado por último (que é o movimento que supera o movimento que venceria LastOptimalBot). Ele alterna entre transposições simples desses algoritmos, desde que continue a perder (ou quando fica entediado por desenhar muito).

Sinceramente, estou surpreso que LastOptimalBot esteja em 5º lugar quando eu postar isso. Estou bastante certo de que isso será melhor, assumindo que eu escrevi esse python corretamente.


0

Aperto de mãoPadrão

from .patternfinder import PatternFinder
import collections

class HandshakePatternMatch:
    def __init__(self):
        self.moves = [None]
        self.other = []
        self.handshake = [None,"N","C","C","D","N"]
        self.friendly = None
        self.pattern = PatternFinder()
    def round(self, last):
        self.other.append(last)
        if last:
            if len(self.other) < len(self.handshake):
                # still trying to handshake
                if self.friendly == False or self.other[-1] != self.handshake[-1]:
                    self.friendly = False
                else:
                    self.friendly = True
                move = self.handshake[len(self.other)]
                self.pattern.round(last)
            elif self.friendly == True:
                # successful handshake and continued cooperation
                move = self.pattern.round(last)
                if last == "C":
                    move = "C"
                elif last == self.handshake[-1] and self.moves[-1] == self.handshake[-1]:
                    move = "C"
                else:
                    self.friendly = False
            else:
                # failed handshake or abandoned cooperation
                move = self.pattern.round(last)
        else:
            move = self.handshake[1]
            self.pattern.round(last)
        self.moves.append(move)
        return move

Por que o padrão combina com você? Aperto de mão e cooperar.


import PatternFinderestá trapaceando nos meus livros.
SIGSTACKFAULT

@Blacksilver É feito o tempo todo em KOTH. Não é diferente de copiar o código em uma resposta existente e usá-lo. Roleta de Robôs: O jogo de robôs de alto risco fazia com que isso acontecesse em todo o lugar, a ponto de os bots detectarem se seu código estava sendo chamado por um oponente e sabotar o retorno.
Draco18s

Tudo bem então. TIL.
SIGSTACKFAULT

Eu vou fazer a trituração amanhã.
SIGSTACKFAULT

Aqui está um exemplo perfeito do uso de código de outros bots. Normalmente, tudo se resume a "esse cara elaborou algumas contas complicadas, quero os resultados dele nessas condições". (Minhas própria entrada fez isso com um bom efeito; o UpYours foi mais disperso em sua abordagem).
Draco18s

0

Codificado

class Hardcoded:
    sequence = "DNCNNDDCNDDDCCDNNNNDDCNNDDCDCNNNDNDDCNNDDNDDCDNCCNNDNNDDCNNDDCDCNNNDNCDNDNDDNCNDDCDNNDCNNDDCDCNNDNNDDCDNDDCCNNNDNNDDCNNDDNDCDNCNDDCDNNDDCCNDNNDDCNNNDCDNDDCNNNNDNDDCDNCDCNNDNNDDCDNDDCCNNNDNDDCNNNDNDCDCDNNDCNNDNDDCDNCNNDDCNDNNDDCDNNDCDNDNCDDCNNNDNDNCNDDCDNDDCCNNNNDNDDCNNDDCNNDDCDCNNDNNDDCDNDDCCNDNNDDCNNNDCDNNDNDDCCNNNDNDDNCDCDNNDCNNDNDDCNNDDCDNCNNDDCDNNDCDNDNCDDCNDNNDDCNNNDDCDNCNNDNNDDCNNDDNNDCDNCNDDCNNDCDNNDDCNNDDNCDCNNDNDNDDCDNCDCNNNDNDDCDCNNDNNDDCDNDDCCNNNDNNDDCNDNDNCDDCDCNNNNDNDDCDNCNDDCDNNDDCNNNDNDDCDNCNNDCNNDNDDNCDCDNNNDDCNNDDCNNDDNNDCDNCNDDCNNDDNDCDNNDNDDCCNCDNNDCNNDDNDDCNCDNNDCDNNNDDCNNDDCDCDNNDDCNDNCNNDNNDNDNDDCDNCDCNNNDNDDCDNCNNDDCDNNDCNNDDCNNDDCDCDNNDDCNDNCNNNDDCDNNDCDNDNCNNDNDDNNDNDCDDCCNNNDDCNDNDNCDDCDCNNNDNNDDCNDCDNDDCNNNNDNDDCCNDNNDDCDCNNNDNDDNDDCDNCCNNDNNDDCNNDDCDCNNDNNDDCNNDDNCNDDNNDCDNCNDDCNNDDNDCDNNDNDDCCNCDNNDCNNDNDDCNNDDNCDCDNNDCNNDNDDCDCDNNNNDDCNNDDNDCCNNDDNDDCNCDNNDCNNDDNDDCDNCNDDCNNNNDCDNNDDCNDNDDCDNCNNDCDNNDCNNDNDDNCDCNNDNDDCDNDDCCNNNNDNDDCNNDDCDCNNDNNDDCDCDNNDDC"
    def __init__(self):
        self.round_num = -1
    def round(self,_):
        self.round_num += 1
        return Hardcoded.sequence[self.round_num % 1000]

Apenas reproduz uma sequência codificada de movimentos otimizada para vencer alguns dos principais bots determinísticos.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.