Iteriertes Gefangenentrilemma

19

HERAUSFORDERUNGSSTATUS: OFFEN

Kommentar, PR eröffnen oder mich anderweitig anschreien, wenn ich deinen Bot vermisse.


Gefangenendilemma ... mit drei Möglichkeiten. Verrückt, nicht wahr?

Hier ist unsere Auszahlungsmatrix. Spieler A links, B oben

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

Die Auszahlungsmatrix ist so konstruiert, dass beide Spieler immer zusammenarbeiten können. Sie können jedoch (normalerweise) gewinnen, indem Sie Neutral oder Defection wählen.

Hier sind einige (konkurrierende) Beispiel-Bots.

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

Ihr Bot ist eine Python3-Klasse. Für jedes Spiel wird eine neue Instanz erstellt, round()die in jeder Runde mit der Auswahl des Gegners aus der letzten Runde aufgerufen wird (oder Keine, wenn es die erste Runde ist).

In nur einem Monat gibt es eine Prämie von 50 Wiederholungen für den Gewinner.

Besonderheiten

  • Jeder Bot spielt in [ANONYMISIERT] Runden jeden anderen Bot (1vs1), einschließlich sich selbst.
  • Standardlücken sind nicht erlaubt.
  • Keine Unordnung mit irgendetwas außerhalb Ihrer Klasse oder anderen hinterhältigen Spielereien.
  • Sie können bis zu fünf Bots einreichen.
  • Ja, Sie können Handshake implementieren.
  • Jede andere Antwort als C, Noder Dwird stillschweigend als N.
  • Die Punkte jedes Bots aus jedem Spiel, das er spielt, werden aufsummiert und verglichen.

Regler

Prüfen!

Andere Sprachen

Ich werde eine API zusammenstellen, falls jemand sie benötigt.

Scores: 2018-11-27

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
quelle
1
Wie werden Bots gegeneinander ausgespielt? Ich bekomme vom Grudger, dass es immer zwei Bots gegeneinander gibt und die letzte Wahl des Feindes an den Bot weitergereicht wird. Wie viele Runden werden gespielt? Und für ein Spiel: Zählt nur das Ergebnis (dh wer hat gewonnen) oder auch die Punkte?
Black Owl Kai
1
Sie würden mehr Einträge erhalten, wenn Sie diese sprachunabhängig oder zumindest allgemeiner machen würden. Sie könnten eine Wrapper-Python-Klasse haben, die einen Prozess erzeugt und ihm Textbefehle sendet, um Rückantworten zu erhalten.
Sparr
1
Getan. Dies war für wie einen Monat auf dem Sandkasten!
SIGSTACKFAULT
2
Wenn Sie den Großteil von main.py am Ende der Schleife while len(botlist) > 1:mit einschließen botlist.remove(lowest_scoring_bot), erhalten Sie ein Ausscheidungsturnier mit interessanten Ergebnissen.
Sparr
1
Eine andere Version davon könnte eines Tages den gesamten Interaktionsverlauf durchlaufen und nicht nur den letzten Zug. Es ändert sich nicht viel, obwohl es den Benutzercode ein wenig vereinfacht. Aber es würde Erweiterungen wie laute Kommunikationskanäle zulassen, die im Laufe der Zeit klarstellen: "Wirklich, ein D, obwohl ich viermal hintereinander C gesagt habe? Nein, ich habe nicht D gesagt; was nimmst du mit mir? Oh, Entschuldigung, können wir diese Runde einfach vergessen? "
Scott Sauyet

Antworten:

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]

Gewinne gegen alle zuvor eingereichten Bots mit Ausnahme (vielleicht) des zufälligen Bots (aber es könnte einen Vorteil haben, weil es D bei einem Unentschieden auswählt und D optimal sein sollte) und ein konstantes Unentschieden gegen sich selbst spielt.

Schwarze Eule Kai
quelle
Ja, schlägt alles.
SIGSTACKFAULT
Vergiss das nicht, PatternFinder schlägt es ein bisschen.
SIGSTACKFAULT
7

NashEquilibrium

Dieser Bot hat eine Spieltheorie-Klasse im College besucht, war aber faul und ging nicht in die Klasse, in der er iterierte Spiele behandelte. Er spielt also nur Single Game Mixed Nash Equilibrium. Es stellt sich heraus, dass 1/5 2/5 2/5 das gemischte NE für die Auszahlungen ist.

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

Ständiger Missbrauch des Nash-Gleichgewichts

Dieser Bot hat eine oder zwei Lektionen von seinem faulen Bruder erhalten. Das Problem seines faulen Bruders war, dass er keine festen Strategien ausnutzte. Diese Version prüft, ob der Gegner ein konstanter Spieler oder ein Titfortat ist und spielt dementsprechend, ansonsten spielt sie das reguläre Nash-Gleichgewicht.

Der einzige Nachteil ist, dass es durchschnittlich 2,2 Punkte pro Spielzug gibt, wenn man gegen sich selbst spielt.

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
quelle
1
NashEquilibrium.round muss Argumente verwenden, auch wenn sie nicht verwendet werden, um dem erwarteten Funktionsprototyp zu entsprechen.
Ray
Vielen Dank, dass Sie es behoben haben
Ofya
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"

Dieser Bot wählt abwechselnd DNDN aus, während TitForTat CDCD abwechselt, was einen durchschnittlichen Nettogewinn von 3 Punkten pro Runde ergibt, wenn ich die Auszahlungsmatrix richtig gelesen habe. Ich denke, das könnte gegen TitForTat optimal sein. Natürlich könnte es verbessert werden, einen Nicht-TFT-Gegner zu erkennen und andere Strategien zu verfolgen, aber ich habe nur das ursprüngliche Kopfgeld angestrebt.

Sparr
quelle
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

Dieser Bot sucht nach früheren Ereignissen des letzten Spielzustands, um zu sehen, wie der Gegner auf diese Ereignisse reagiert hat. Dabei werden längere Musterübereinstimmungen und neuere Übereinstimmungen bevorzugt. Anschließend wird der Zug ausgeführt, der den vorhergesagten Zug des Gegners "schlägt". Es gibt viel Raum, damit es mit all den Daten, die es verfolgt, intelligenter wird, aber ich hatte keine Zeit mehr, daran zu arbeiten.

Sparr
quelle
Hast du etwas dagegen, ihr einen Optimierungspass zu geben, wenn du Zeit hast? Es ist leicht die größte Zeitsenke.
SIGSTACKFAULT
2
@Blacksilver Ich habe gerade die maximale Musterlänge von 100 auf 10 reduziert. Es sollte jetzt fast sofort laufen, wenn Sie <200 Runden laufen
Sparr
1
Vielleicht würde die Verwendung einer hochkompositen Zahl (dh 12) besser abschneiden?
SIGSTACKFAULT
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'

Beginnt optimistisch, wird aber zunehmend bitterer, da der Gegner sich weigert zu kooperieren. Viele magische Konstanten, die wahrscheinlich angepasst werden könnten, aber dies wird wahrscheinlich nicht gut genug sein, um die Zeit zu rechtfertigen.


quelle
5

Ensemble

Dies lässt ein Ensemble verwandter Modelle laufen. Die einzelnen Modelle berücksichtigen unterschiedliche historische Beträge und haben die Option, entweder immer den Zug zu wählen, der die erwartete Auszahlungsdifferenz optimiert, oder zufällig einen Zug im Verhältnis zur erwarteten Auszahlungsdifferenz auszuwählen.

Jedes Ensemblemitglied stimmt dann über seinen bevorzugten Zug ab. Sie erhalten eine Stimmenzahl, die der Anzahl der Stimmen entspricht, die sie gewonnen haben, als die des Gegners (was bedeutet, dass schreckliche Models negative Stimmen erhalten). Welcher Zug gewinnt die Abstimmung wird dann ausgewählt.

(Wahrscheinlich sollten sie ihre Stimmen auf die einzelnen Schritte aufteilen, je nachdem, wie sehr sie sie bevorzugen, aber mir ist es im Moment nicht wichtig genug, dies zu tun.)

Es schlägt alles, was bisher gepostet wurde, mit Ausnahme von EvaluaterBot und PatternFinder. (Eins-zu-eins schlägt es EvaluaterBot und verliert gegen 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"

Test Framework

Für den Fall, dass es jemand anderes nützlich findet, finden Sie hier ein Test-Framework zum Betrachten einzelner Übereinstimmungen. Python2. Setzen Sie einfach alle Gegner, an denen Sie interessiert sind, in opponents.py ein und ändern Sie die Verweise auf Ensemble in Ihre eigenen.

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
Strahl
quelle
Der Controller ist bereit, Sie mussten nicht alles tun ...
SIGSTACKFAULT
1
@Blacksilver Mir wurde klar, dass ich mich gerade unterwerfen wollte. Diese Version funktioniert jedoch in Versionen vor 3.6 und enthält Informationen zu einzelnen Übereinstimmungen, mit deren Hilfe Schwachstellen identifiziert werden können. Es war also keine reine Zeitverschwendung.
Ray
Meinetwegen; Laufen jetzt. Ich werde wahrscheinlich meinem Controller Optionen hinzufügen, um ähnliche Dinge zu tun.
SIGSTACKFAULT
"Es schlägt alles, was bisher gepostet wurde, außer Ensemble und PatternFinder" Ich fühle mich geehrt :)
Sparr
@Sparr Ups. Das sollte EvaluaterBot und PatternFinder heißen. Aber das ist, wenn man die Gesamtpunktzahl mit dem gesamten Feld vergleicht. PatternFinder ist nach wie vor der einzige, der dies in einem direkten Match übertrifft.
Ray
4

OldTitForTat

Old School-Spieler sind zu faul, um die neuen Regeln zu aktualisieren.

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

NeverCOOP

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

Wenn der gegnerische Bot fehlerhaft oder neutral ist, wählen Sie einen Fehler. Andernfalls, wenn dies die erste Runde ist oder der gegnerische Bot kooperiert, wähle neutral. Ich bin mir nicht sicher, wie gut das funktionieren wird ...

glietz
quelle
Was ist der Versuch / außer für?
SIGSTACKFAULT
1
@Blacksilver Ich nehme an, es dient dem gleichen Zweck wie der if(last):in Ihrem Grudger-Bot, um festzustellen, ob es eine vorherige Runde gab.
ETHproductions
ahh, ich verstehe. None in "ND"fehler.
SIGSTACKFAULT
Weil if last and last in "ND":es zu kompliziert war?
user253751
3

LastOptimalBot

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

Nimmt an, dass der gegnerische Bot immer wieder den gleichen Zug spielt und wählt denjenigen, der die beste Auszahlung hat.

Durchschnittswerte:

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
Spitemaster
quelle
oof. Vielleicht würde T4T besser als return last.
SIGSTACKFAULT
Das würde mir gefallen! Wenn TitForTat return lastwäre, würde LOB über 6 Runden 18-9 gehen und nicht über 5 Runden 13-10 . Ich denke, es ist in Ordnung so wie es ist - mach dir keine Sorgen, die Beispiel-Bots zu optimieren.
Spitemaster
return lastwäre ein besserer T4T für diese Herausforderung, denke ich
Sparr
Nur ausprobiert - das if(last): return last; else: return "C"ist schlimmer.
SIGSTACKFAULT
Richtig, aber wie @Sparr sagte, könnte es angemessener sein. Bis zu dir, nehme ich an.
Spitemaster
3

CopyCat

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

Kopiert den letzten Zug des Gegners.
Ich erwarte nicht, dass das gut geht, aber noch hatte niemand diesen Klassiker implementiert.

MannerPots
quelle
2

Verbesserte Dirichletwürfel

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    

Dies ist eine verbesserte Version von Dirichlet Dice. Anstatt die erwartete Multinomialverteilung aus der Dirichlet-Verteilung zu ziehen, wird eine Multinomialverteilung zufällig aus der Dirichlet-Verteilung gezogen. Anstatt vom Multinomial zu zeichnen und die optimale Antwort darauf zu geben, gibt es die optimale erwartete Antwort auf das gegebene Multinomial unter Verwendung der Punkte. Die Zufälligkeit wurde also im Wesentlichen vom Multinomial Draw zum Dirichlet Draw verschoben. Außerdem sind die Priors jetzt flacher, um die Erkundung anzuregen.

Es ist "verbessert", weil es jetzt das Punktesystem berücksichtigt, indem es den besten erwarteten Wert für die Wahrscheinlichkeiten angibt, während es seine Zufälligkeit beibehält, indem es die Wahrscheinlichkeiten selbst zeichnet. Früher habe ich einfach versucht, die bestmögliche Auszahlung aus den erwarteten Wahrscheinlichkeiten zu erzielen, aber das war schlecht, weil es einfach hängen geblieben ist und nicht genug erforscht hat, um seine Würfel zu aktualisieren. Es war auch vorhersehbarer und ausnutzbarer.


Ursprüngliche Einreichung:

Dirichlet Würfel

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

Grundsätzlich gehe ich davon aus, dass die Antwort des Gegners auf meine letzte Ausgabe eine multinomiale Variable (gewichtete Würfel) ist, einer für jede meiner Ausgaben, also gibt es einen Würfel für "C", einen für "N" und einen für "D". . Wenn mein letzter Wurf zum Beispiel ein "N" wäre, würfle ich mit den "N-Würfeln", um zu erraten, wie sie auf mein "N" reagieren würden. Ich beginne mit einem Dirichlet, das voraussetzt, dass mein Gegner etwas "schlau" ist (eher derjenige mit der besten Auszahlung gegen meinen letzten Wurf, weniger derjenige mit der schlechtesten Auszahlung). Ich generiere die "erwartete" Multinomialverteilung aus dem entsprechenden Dirichlet vor (dies ist der erwartete Wert der Wahrscheinlichkeitsverteilung über ihre Würfelgewichte). Ich würfle mit dem Gewicht meiner letzten Ausgabe.

Ab der dritten Runde aktualisiere ich das entsprechende Dirichlet nach Bayes, bevor mein Gegner das letzte Mal auf das Spiel reagiert, das ich vor zwei Runden gespielt habe. Ich versuche iterativ ihre Würfelgewichte zu lernen.

Ich hätte auch einfach die Antwort mit dem besten "erwarteten" Ergebnis auswählen können, nachdem ich die Würfel erzeugt hatte, anstatt einfach die Würfel zu würfeln und auf das Ergebnis zu reagieren. Ich wollte jedoch die Zufälligkeit beibehalten, damit mein Bot weniger anfällig für diejenigen ist, die versuchen, ein Muster vorherzusagen.

Brückenbrenner
quelle
2

Kevin

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

Picks die schlechteste Wahl. Der schlimmste Bot gemacht.

Nutzlos

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

Es betrachtet die letzten beiden Züge des Gegners und wählt die am meisten nicht ausgeführten aus, ansonsten wählt es etwas Zufälliges aus. Es gibt wahrscheinlich einen besseren Weg, dies zu tun.

Link1J
quelle
2

Historischer Durchschnitt

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)

Schaut sich die Geschichte an und findet die Aktion, die im Durchschnitt am besten gewesen wäre. Startet die Zusammenarbeit.

MegaTom
quelle
Dies könnte schneller laufen, wenn die Durchschnittswerte nicht bei jeder Runde neu berechnet würden.
Sparr
@Sparr wahr. Ich habe es so bearbeitet, wie es jetzt ist.
MegaTom
1

Gewichteter Durchschnitt

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"

Das Verhalten des Gegners wird als rechtwinkliges Dreieck mit CND-Ecken von 0,0 0,1 1,0 modelliert. Jede Bewegung des Gegners verschiebt den Punkt innerhalb dieses Dreiecks in Richtung dieser Ecke, und wir spielen, um den durch den Punkt angegebenen Zug zu schlagen (wobei C eine einstellbare kleine Scheibe des Dreiecks erhält). Theoretisch wollte ich, dass dies ein längeres Gedächtnis mit mehr Gewicht gegenüber früheren Zügen hat, aber in der Praxis bevorzugt das aktuelle Meta Bots, die sich schnell ändern, so dass sich dies in einer Annäherung an LastOptimalBot gegen die meisten Feinde entwickelt. Posting für die Nachwelt; Vielleicht lässt sich jemand inspirieren.

Sparr
quelle
1

Tetragramm

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

Versuchen Sie, ein Muster in den Zügen des Gegners zu finden, vorausgesetzt, er beobachtet auch unseren letzten Zug.


quelle
1

Handschlag

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]

Erkennt, wenn es gegen sich selbst spielt, und kooperiert dann. Andernfalls wird LastOptimalBot nachgebildet, was nach der besten Einzeilenstrategie aussieht. Führt eine schlechtere Leistung als LastOptimalBot aus, und zwar um einen Betrag, der umgekehrt proportional zur Anzahl der Runden ist. Wäre natürlich besser, wenn es mehr Exemplare auf dem Feld gäbe * husten ** zwinker *.

Sparr
quelle
Übermitteln Sie einfach ein paar Klone, die ein anderes Verhalten ohne Handshake aufweisen.
SIGSTACKFAULT
Das scheint ausbeuterisch. Ich könnte einen solchen Klon für jedes hier dargestellte einfache Verhalten einreichen.
Sparr
Ich habe eine zusätzliche Klausel hinzugefügt, die besagt, dass Sie nur maximal fünf Bots einreichen können.
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

Dieser Bot verwendet den Algorithmus von LastOptimalBot, solange er gewinnt. Wenn der andere Bot dies jedoch vorhersagt, beginnt er zu spielen, welcher Zug auch immer sein Gegner zuletzt gespielt hat (das ist der Zug, der den Zug schlägt, der LastOptimalBot schlagen würde). Es durchläuft einfache Transpositionen dieser Algorithmen, solange sie verlieren (oder wenn es langweilig wird, wenn viel gezeichnet wird).

Ehrlich gesagt, ich bin überrascht, dass LastOptimalBot auf dem fünften Platz sitzt, als ich dies poste. Ich bin mir ziemlich sicher, dass dies besser gelingen wird, vorausgesetzt, ich habe diese Python korrekt geschrieben.

Spitemaster
quelle
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

Warum stimmen Sie mit Ihrem Muster überein? Händedruck und kooperieren weg.

Draco18s
quelle
import PatternFinderbetrügt in meinen Büchern.
SIGSTACKFAULT
@Blacksilver Es wird die ganze Zeit in KOTH gemacht. Es ist nichts anderes, als den Code in eine vorhandene Antwort zu kopieren und zu verwenden. Robot Roulette: Bei Robotern mit hohen Einsätzen passierte es überall bis zu einem Punkt, an dem Bots erkennen würden, ob ihr Code von einem Gegner aufgerufen wurde, und die Rückkehr sabotieren würden.
Draco18s
Alles klar dann. Bis.
SIGSTACKFAULT
Ich mache morgen das Knirschen.
SIGSTACKFAULT
Hier ist ein perfektes Beispiel für die Verwendung des Codes anderer Bots. Normalerweise kommt es darauf an, "dass dieser Kerl einige knifflige Mathe ausarbeitete, ich will seine Ergebnisse unter diesen Bedingungen." (Mein eigener Eintrag hat das ziemlich gut gemacht; UpYours war in seiner Herangehensweise eher streuend).
Draco18s
0

Hardcoded

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]

Spielt einfach eine hartkodierte Folge von Zügen ab, die optimiert wurden, um einige der besten deterministischen Bots zu schlagen.

MegaTom
quelle