Iterated Trilemma Więźnia

19

STAN WYZWANIA: OTWARTY

Komentuj, otwieraj PR lub w inny sposób krzycz na mnie, jeśli brakuje mi twojego bota.


Dylemat więźnia ... z trzema wyborami. Szalony, co?

Oto nasza matryca wypłat. Gracz A po lewej, B na górze

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

Matryca wypłat jest zaprojektowana tak, aby obaj gracze zawsze współpracowali, ale możesz (zwykle) zyskać, wybierając Neutralny lub Usterkę.

Oto niektóre (konkurujące) przykładowe boty.

# 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"

Twój bot jest klasą Python3. Nowa instancja jest tworzona dla każdej gry i round()jest wywoływana w każdej rundzie, z wyborem przeciwnika z ostatniej rundy (lub Brak, jeśli jest to pierwsza runda)

Zwycięzca otrzyma nagrodę w wysokości 50 powtórzeń za miesiąc.

Specyfika

  • Każdy bot zagrywa co drugiego bota (1 na 1), w tym siebie, w rundach [ZMIENIONO].
  • Standardowe luki zabronione.
  • Bez bałaganu z czymkolwiek poza klasą lub innymi podstępnymi shenanigains.
  • Możesz przesłać do pięciu botów.
  • Tak, możesz wprowadzić uzgadnianie.
  • Wszelkie odpowiedzi inne niż C, Nlub Dbędą po cichu traktowane jako N.
  • Punkty każdego bota z każdej gry będą sumowane i porównywane.

Kontroler

Czek!

Inne języki

Wyrzucę razem API, jeśli ktoś tego potrzebuje.

Wyniki: 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
SIGSTACKFAULT
źródło
1
W jaki sposób boty są stawiane przeciwko sobie? Czuję od Grudgera, że ​​zawsze są dwa boty przeciwko sobie i ostatni wybór wroga należy do bota. Ile rund rozegranych? A w grze: czy liczy się tylko wynik (kto wygrał), czy też punkty?
Black Owl Kai,
1
Otrzymasz więcej wpisów, jeśli uczynisz ten język niezależnym, lub przynajmniej szerszym. Możesz mieć opakowującą klasę python, która odradza proces i wysyła mu komendy tekstowe w celu uzyskania odpowiedzi tekstowych.
Sparr,
1
Gotowy. To było na piaskownicy przez około miesiąc!
SIGSTACKFAULT,
2
Jeśli owinąć w większości main.py while len(botlist) > 1:ze botlist.remove(lowest_scoring_bot)w dolnej części pętli, masz turnieju eliminacji z interesujących wyników.
Sparr
1
Inna wersja tego dnia może przekazać całą historię interakcji, a nie tylko ostatni ruch. Nie zmienia się wiele, choć nieco upraszcza kod użytkownika. Pozwoliłoby to jednak na rozszerzenia, takie jak głośne kanały komunikacyjne, które wyjaśniają się w czasie: „Naprawdę, D, mimo że powiedziałem C cztery razy z rzędu? Nie, nie powiedziałem D; co bierzesz bo? Przepraszam, czy możemy zapomnieć o tej rundzie? ”
Scott Sauyet

Odpowiedzi:

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]

Wygrywa ze wszystkimi poprzednio przesłanymi botami, z wyjątkiem (być może) losowego bota (ale może mieć przewagę, ponieważ wybiera D w losowaniu, a D powinien być optymalny) i gra przeciwko sobie.

Czarna sowa Kai
źródło
Tak, wszystko bije.
SIGSTACKFAULT
Zarysuj to, PatternFinder bije go trochę.
SIGSTACKFAULT
7

NashEquilibrium

Ten bot wziął udział w zajęciach z teorii gier na studiach, ale był leniwy i nie poszedł na zajęcia, na których omawiał iterowane gry. Gra więc tylko w jedną grę mieszaną równowagą Nasha. Okazuje się, że 1/5 2/5 2/5 to mieszany NE dla wypłat.

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

Stałe nadużywanie równowagi Nasha

Ten bot odebrał lekcję od dwóch leniwych braci. Problem jego leniwego brata polegał na tym, że nie korzystał z ustalonych strategii. Ta wersja sprawdza, czy przeciwnik jest stałym graczem lub titfortat i gra odpowiednio, w przeciwnym razie gra normalną równowagę nasha.

Jedynym minusem jest to, że gra średnio o 2,2 punktu na turę.

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
Ofya
źródło
1
NashEquilibrium.round musi pobierać argumenty, nawet jeśli ich nie używa, aby dopasować oczekiwany prototyp funkcji.
Ray
Dziękuję, że to naprawiłeś
Ofya
Nieco krócej: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"

Ten bot naprzemiennie wybiera DNDN, a TitForTat zmienia CDCD, uzyskując średni zysk netto w wysokości 3 punktów na rundę, jeśli poprawnie odczytałem macierz wypłat. Myślę, że może to być optymalne w stosunku do TitForTat. Oczywiście można poprawić wykrywanie przeciwnika nieobjętego TFT i przyjmowanie innych strategii, ale ja po prostu dążyłem do oryginalnej nagrody.

Sparr
źródło
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

Ten bot szuka poprzednich wystąpień ostatniego stanu gry, aby zobaczyć, jak przeciwnik zareagował na te zdarzenia, preferując dłuższe mecze według wzorca i nowsze mecze, a następnie odtwarza ruch, który „pokona” przewidywany ruch przeciwnika. Jest dużo miejsca na to, aby był mądrzejszy ze wszystkimi śledzonymi danymi, ale zabrakło mi czasu na pracę nad tym.

Sparr
źródło
Kiedy będziesz miał czas, czy mógłbyś podać jej przepustkę optymalizacyjną? Jest to z pewnością największy zlew czasu.
SIGSTACKFAULT,
2
@Blacksilver Właśnie zmniejszyłem maksymalną długość wzoru ze 100 do 10. Powinien działać prawie natychmiast, jeśli masz
Sparr
1
Może użycie wysoce złożonej liczby (tj. 12) dałoby lepszy wynik?
SIGSTACKFAULT
5

Jadeit

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'

Zaczyna się optymistycznie, ale staje się coraz bardziej zgorzkniały, gdy przeciwnik odmawia współpracy. Wiele magicznych stałych, które prawdopodobnie można by poprawić, ale prawdopodobnie nie będzie to wystarczająco dobre, aby uzasadnić czas.


źródło
5

Ensemble

To uruchamia zestaw powiązanych modeli. Poszczególne modele uwzględniają różne ilości historii i mają opcję albo wyboru ruchu, który zoptymalizuje oczekiwaną różnicę wypłat, albo losowego wyboru ruchu proporcjonalnego do oczekiwanej różnicy wypłat.

Każdy członek zespołu głosuje następnie na preferowany ruch. Otrzymują liczbę głosów równą temu, ile więcej wygrywają niż przeciwnik (co oznacza, że ​​okropne modele otrzymają głosy negatywne). Którykolwiek ruch wygrywa, głosowanie jest następnie wybierane.

(Prawdopodobnie powinni podzielić swoje głosy między ruchy proporcjonalnie do tego, na ile faworyzują każdy, ale teraz nie dbam o to wystarczająco dużo).

Pokonuje wszystko, co do tej pory opublikowano, z wyjątkiem EvaluaterBot i PatternFinder. (Jeden na jednego, pokonuje EvaluaterBot i przegrywa z 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"

Framework testowy

W przypadku, gdy ktokolwiek uzna to za przydatne, oto struktura testowa do wyszukiwania pojedynczych pojedynków. Python2. Po prostu umieść wszystkich przeciwników, których jesteś zainteresowany, w opponents.py i zmień odniesienia do Ensemble na własne.

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
Promień
źródło
Kontroler jest gotowy, nie
musiałeś
1
@Blacksilver Zdałem sobie sprawę, że właśnie miałem się poddać. Ale ten działa w wersjach wcześniejszych niż 3.6 i dostarcza informacji o pojedynczych pojedynkach, które mogą pomóc zidentyfikować słabe punkty, więc nie była to całkowita strata czasu.
Ray
Słusznie; biegnie teraz. Prawdopodobnie dodam opcje do mojego kontrolera, aby robić podobne rzeczy.
SIGSTACKFAULT
„Pokonuje wszystko, co do tej pory opublikowano oprócz Ensemble i PatternFinder”. Jestem zaszczycony :)
Sparr,
@Sparr Ups. To miało powiedzieć: AssessaterBot i PatternFinder. Ale to przy porównywaniu całkowitego wyniku z całym polem. PatternFinder pozostaje jedynym, który pokonuje to w bezpośrednim pojedynku.
Ray
4

OldTitForTat

Gracz ze starej szkoły jest zbyt leniwy, aby aktualizować nowe zasady.

class OldTitForTat:
    def round(self, last):
        if(last == None)
            return "C"
        if(last == "C"):
            return "C"
        return "D"
Jozuego
źródło
3

NeverCOOP

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

Jeśli przeciwny bot ma wady lub jest neutralny, wybierz defekt. W przeciwnym razie, jeśli jest to pierwsza tura lub przeciwny bot współpracuje, wybierz neutralny. Nie jestem pewien, jak dobrze to zadziała ...

Glietz
źródło
Jaka jest próba / z wyjątkiem?
SIGSTACKFAULT,
1
@Blacksilver Zakładam, że służy temu samemu celowi, co if(last):w bocie Grudger, wykrywając, czy była poprzednia runda.
ETHprodukcje
Ahh rozumiem. None in "ND"błędy.
SIGSTACKFAULT,
Ponieważ if last and last in "ND":było to zbyt skomplikowane?
user253751,
3

LastOptimalBot

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

Zakłada, że ​​przeciwny bot zawsze będzie ponownie wykonywał ten sam ruch i wybiera ten, który ma najlepszą wypłatę przeciwko niemu.

Średnie:

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
Mistrz Spitemaster
źródło
oof. Może T4T zrobiłby lepiej jako return last.
SIGSTACKFAULT,
Chciałbym, aby! Gdyby TitForTat był return last, LOB poszedłby 18-9 w ciągu 6 rund zamiast 13-10 w ciągu 5 rund, które obecnie otrzymuje. Myślę, że jest w porządku - nie martw się o optymalizację przykładowych botów.
Spitemaster,
return lastbyłby lepszy T4T do tego wyzwania, myślę
Sparr
Właśnie próbowałem - if(last): return last; else: return "C"jest gorzej.
SIGSTACKFAULT,
Racja, ale jak mówił @Sparr, może być bardziej odpowiednie. Do ciebie, jak sądzę.
Spitemaster,
3

CopyCat

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

Kopiuje ostatni ruch przeciwnika.
Nie spodziewam się, że będzie dobrze, ale nikt jeszcze nie wdrożył tego klasycznego.

MannerPots
źródło
2

Ulepszone kości Dirichleta

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    

To ulepszona wersja kości Dirichlet. Zamiast brać oczekiwany rozkład wielomianowy z rozkładu Dirichleta, losuje rozkład wielomianowy losowo z rozkładu Dirichleta. Następnie zamiast rysować z wielomianu i dawać optymalną odpowiedź na to, daje optymalną oczekiwaną odpowiedź na dany wielomian za pomocą punktów. Tak więc losowość została zasadniczo przesunięta z losowania wielomianowego na losowanie Dirichleta. Ponadto priory są teraz bardziej płaskie, aby zachęcić do eksploracji.

Jest „ulepszony”, ponieważ teraz uwzględnia system punktowy, podając najlepszą oczekiwaną wartość względem prawdopodobieństw, przy jednoczesnym zachowaniu jego losowości poprzez losowanie samych prawdopodobieństw. Wcześniej próbowałem po prostu uzyskać najlepszą oczekiwaną wypłatę z oczekiwanych prawdopodobieństw, ale zrobiło to źle, ponieważ utknęło i nie zbadałem wystarczająco dużo, aby zaktualizować swoje kości. Było to również bardziej przewidywalne i możliwe do wykorzystania.


Oryginalne zgłoszenie:

Kości Dirichleta

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

Zasadniczo zakładam, że odpowiedź przeciwnika na mój ostatni wynik jest zmienną wielomianową (kostki ważone), po jednej dla każdego z moich wyników, więc istnieją kości dla „C”, jeden dla „N” i jeden dla „D” . Więc jeśli mój ostatni rzut był na przykład „N”, to rzucam „N-kostkami”, aby zgadnąć, jaka byłaby ich odpowiedź na moje „N”. Zaczynam od przeornika Dirichleta, który zakłada, że ​​mój przeciwnik jest w pewnym sensie „inteligentny” (bardziej prawdopodobne, że zagra w tę z najlepszą wypłatą w stosunku do mojego ostatniego rzutu, najmniej prawdopodobne, że zagra w tę z najgorszą wypłatą). Generuję „oczekiwany” rozkład wielomianowy z odpowiedniego Dirichleta wcześniej (jest to oczekiwana wartość rozkładu prawdopodobieństwa na podstawie ich masy kości). Rzucam ważonymi kostkami mojego ostatniego wyniku,

Zaczynając od trzeciej rundy, wykonuję Bayesowską aktualizację odpowiedniego Dirichleta przed ostatnią odpowiedzią mojego przeciwnika na rzecz, w którą grałem dwie rundy temu. Próbuję iteracyjnie nauczyć się ich ważenia kości.

Mogłem też po prostu wybrać odpowiedź z najlepszym „oczekiwanym” wynikiem po wygenerowaniu kości, zamiast po prostu rzucać kostką i reagować na wynik. Chciałem jednak zachować losowość, aby mój bot był mniej wrażliwy na tych, którzy próbują przewidzieć wzór.

Spalacze mostkowe
źródło
2

Kevin

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

Wybiera najgorszy wybór. Najgorszy zrobiony bot.

Nieprzydatny

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")

Patrzy na dwa ostatnie ruchy wykonane przez przeciwnika i wybiera najbardziej niewykonane, inaczej wybiera coś losowego. Jest prawdopodobnie lepszy sposób na zrobienie tego.

Link1J
źródło
2

Średnia historyczna

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)

Spogląda na historię i znajduje działanie, które byłoby średnio najlepsze. Zaczyna współpracować.

MegaTom
źródło
Mogłoby to działać szybciej, gdyby nie przeliczało średnich w każdej rundzie.
Sparr
@Sparr true. Zredagowałem to, więc teraz.
MegaTom,
1

Średnia ważona

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"

Zachowanie przeciwnika jest modelowane jako trójkąt prostokątny z narożnikami dla CND odpowiednio 0,0 0,1 1,0. Każdy ruch przeciwnika przesuwa punkt w obrębie tego trójkąta w kierunku tego rogu, a my gramy, aby pokonać ruch wskazany przez punkt (przy czym C otrzymuje odpowiednio mały kawałek trójkąta). Teoretycznie chciałem mieć dłuższą pamięć z większą wagą do poprzednich ruchów, ale w praktyce obecne meta faworyzuje boty, które zmieniają się szybko, więc przekształca się to w przybliżenie LastOptimalBot przeciwko większości wrogów. Delegowanie dla potomności; może ktoś będzie inspirowany.

Sparr
źródło
1

Tetragram

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

Postaraj się znaleźć wzór w ruchach przeciwnika, zakładając, że obserwują również nasz ostatni ruch.


źródło
1

Uścisk dłoni

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]

Rozpoznaje, kiedy gra przeciwko sobie, a następnie współpracuje. W przeciwnym razie naśladuje LastOptimalBot, który wydaje się najlepszą strategią jednowierszową. Działa gorzej niż LastOptimalBot, w ilości odwrotnie proporcjonalnej do liczby rund. Oczywiście lepiej by było, gdyby było więcej kopii w terenie * kaszel ** mrugnięcie *.

Sparr
źródło
Po prostu prześlij kilka klonów, które zachowują się inaczej niż podczas uzgadniania.
SIGSTACKFAULT
To wydaje się wykorzystywane. Mógłbym złożyć jeden taki klon dla każdego prostego zachowania przedstawionego tutaj.
Sparr
Dodałem dodatkową klauzulę, że możesz przesłać maksymalnie pięć botów.
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

Ten bot używa algorytmu LastOptimalBot, dopóki wygrywa. Jeśli jednak inny bot zacznie to przewidywać, zacznie grać w ruch, który wykonał przeciwnik jako ostatni (czyli ruch, który pokonuje ruch, który pokonałby LastOptimalBot). Cyklicznie przechodzi przez proste transpozycje tych algorytmów, o ile nadal traci (lub gdy się nudzi rysując dużo).

Szczerze mówiąc, jestem zaskoczony, że LastOptimalBot siedzi na 5. miejscu, pisząc to. Jestem całkiem pewien, że zrobi to lepiej, zakładając, że poprawnie napisałem ten python.

Mistrz Spitemaster
źródło
0

HandshakePatternMatch

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

Dlaczego wzór pasuje do ciebie? Uścisk dłoni i współpraca.

Draco18s
źródło
import PatternFinderoszukuje w moich książkach.
SIGSTACKFAULT,
@Blacksilver Robi się to cały czas w KOTH. Nie różni się niczym od skopiowania kodu w istniejącej odpowiedzi i użycia go. Robot Roulette: gry hazardowe na wysokich stawkach miały miejsce w takim miejscu, że boty wykryłyby, gdyby ich kod został wywołany przez przeciwnika i sabotowały powrót.
Draco18,
W porządku. TIL
SIGSTACKFAULT,
Jutro zrobię chrupanie.
SIGSTACKFAULT,
Oto doskonały przykład użycia kodu innych botów. Zwykle sprowadza się to do „tego faceta, który opracował skomplikowaną matematykę, chcę jego wyniki w tych warunkach”. (Mój własny wpis zrobił to z całkiem dobrym skutkiem; UpYours był bardziej rozproszony w swoim podejściu).
Draco18,
0

Mocno zakodowane

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]

Po prostu odtwarza zakodowaną sekwencję ruchów zoptymalizowaną pod kątem pokonania niektórych najlepszych deterministycznych botów.

MegaTom
źródło