Trilemme du prisonnier itéré

19

ÉTAT DU DÉFI: OUVERT

Commentez, ouvrez un PR ou criez-moi si je manque votre bot.


Le dilemme du prisonnier ... avec trois choix. Fou, hein?

Voici notre matrice de gains. Joueur A à gauche, B en haut

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

La matrice des gains est conçue de sorte qu'il est préférable que les deux joueurs coopèrent toujours, mais vous pouvez gagner (généralement) en choisissant Neutre ou Défection.

Voici quelques exemples de robots (concurrents).

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

Votre bot est une classe Python3. Une nouvelle instance est créée pour chaque partie, et round()est appelée à chaque tour, avec le choix de votre adversaire lors du dernier tour (ou Aucun, s'il s'agit du premier tour)

Il y a une prime de 50 représentants pour le gagnant en un mois environ.

Détails

  • Chaque bot joue tous les autres bots (1v1), y compris lui-même, dans les rounds [SUPPRIMÉ].
  • Failles standard interdites.
  • Ne jouez avec rien en dehors de votre classe ou d'autres manigances sournoises.
  • Vous pouvez soumettre jusqu'à cinq bots.
  • Oui, vous pouvez implémenter une poignée de main.
  • Toute réponse autre que C, Nou Dsera considérée comme silencieuse N.
  • Les points de chaque bot de chaque partie jouée seront totalisés et comparés.

Manette

Vérifier!

Autres langues

Je jetterai ensemble une API si quelqu'un en a besoin.

Notes: 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
la source
1
Comment les bots sont-ils opposés? D'après le Grudger, il y a toujours deux bots l'un contre l'autre et le dernier choix de l'ennemi est passé au bot. Combien de tours sont joués? Et pour un jeu: Est-ce que seul le résultat compte (c'est-à-dire qui a gagné) ou aussi les points?
Black Owl Kai
1
Vous obtiendriez plus d'entrées si vous rendiez cette langue indépendante de la langue, ou du moins plus large. Vous pouvez avoir une classe python wrapper qui génère un processus et lui envoie des commandes de texte pour récupérer les réponses de texte.
Sparr
1
Terminé. C'était sur le bac à sable pendant un mois!
SIGSTACKFAULT
2
Si vous envelopper la plupart de main.py while len(botlist) > 1:avec botlist.remove(lowest_scoring_bot)au bas de la boucle, vous obtenez un tournoi d'élimination avec des résultats intéressants.
Sparr
1
Une autre version de ce jour pourrait passer l'intégralité de l'historique des interactions plutôt que le dernier mouvement. Cela ne change pas grand-chose, bien qu'il simplifie légèrement le code utilisateur. Mais cela permettrait des extensions, telles que des canaux de communication bruyants qui clarifient au fil du temps: "Vraiment, un D, ​​même si j'ai dit C quatre fois de suite? Non, je n'ai pas dit D; que me prenez-vous Oh, désolé, pouvons-nous juste oublier ce tour? "
Scott Sauyet

Réponses:

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]

Gagne contre tous les bots précédemment soumis sauf (peut-être) le bot aléatoire (mais cela pourrait avoir un avantage, car il choisit D dans un tirage au sort et D devrait être optimal) et joue un tirage constant contre lui-même.

Chouette noire Kai
la source
Oui, bat tout.
SIGSTACKFAULT
Grattez cela, PatternFinder le bat un peu.
SIGSTACKFAULT
7

Équilibre de Nash

Ce bot a suivi un cours de théorie des jeux à l'université mais était paresseux et n'est pas allé à la classe où ils ont couvert les jeux itérés. Il ne joue donc qu'à un seul jeu d'équilibre mixte de Nash. Il s'avère que 1/5 2/5 2/5 est le NE mixte pour les gains.

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

Équilibre constant de Nash

Ce robot a pris une ou deux leçons de son frère paresseux. Le problème de son frère paresseux était qu'il ne profitait pas de stratégies fixes. Cette version vérifie si l'adversaire est un joueur constant ou titfortat et joue en conséquence, sinon il joue l'équilibre régulier de Nash.

Son seul inconvénient est qu'il se situe en moyenne à 2,2 points par tour en jouant contre lui-même.

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
la source
1
NashEquilibrium.round doit prendre des arguments même s'il ne les utilise pas, afin de s'adapter au prototype de fonction attendu.
Ray
Merci de l'avoir corrigé
Ofya
Un peu plus court: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"

Ce bot alternera la sélection de DNDN tandis que TitForTat alterne CDCD, pour un gain net moyen de 3 points par tour si j'ai lu correctement la matrice de paiement. Je pense que cela pourrait être optimal contre TitForTat. Évidemment, il pourrait être amélioré pour détecter un adversaire non-TFT et adopter d'autres stratégies, mais je visais juste la prime d'origine.

Sparr
la source
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

Ce bot recherche les occurrences précédentes de l'état de jeu récent pour voir comment l'adversaire a réagi à ces occurrences, avec une préférence pour les matchs de modèle plus longs et les matchs plus récents, puis joue le coup qui "battra" le coup prévu de l'adversaire. Il y a beaucoup de place pour qu'il soit plus intelligent avec toutes les données dont il assure le suivi, mais j'ai manqué de temps pour y travailler.

Sparr
la source
Quand vous avez le temps, vous voulez lui donner un laissez-passer d'optimisation? C'est facilement le plus grand puits de temps.
SIGSTACKFAULT
2
@Blacksilver Je viens de réduire la longueur maximale du motif de 100 à 10. Il devrait fonctionner presque instantanément maintenant si vous exécutez <200 tours
Sparr
1
Peut-être que l'utilisation d'un nombre hautement composite (c.-à-d. 12) donnerait de meilleurs résultats?
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'

Commence optimiste, mais devient progressivement plus amer lorsque l'adversaire refuse de coopérer. Beaucoup de constantes magiques qui pourraient probablement être modifiées, mais cela ne fera probablement pas assez bien pour justifier le temps.


la source
5

Ensemble

Cela exécute un ensemble de modèles connexes. Les modèles individuels prennent en compte différentes quantités d'historique et ont la possibilité de toujours choisir le mouvement qui optimisera la différence de paiement attendue, ou de sélectionner au hasard un mouvement proportionnel à la différence de paiement attendue.

Chaque membre de l'ensemble vote ensuite son choix préféré. Ils obtiennent un nombre de voix égal à ce qu'ils ont gagné de plus que l'adversaire (ce qui signifie que les modèles terribles obtiendront des votes négatifs). Quel que soit le coup gagnant, le vote est alors sélectionné.

(Ils devraient probablement répartir leurs votes entre les mouvements proportionnellement à leur préférence pour chacun, mais je m'en fiche assez de le faire maintenant.)

Il bat tout ce qui a été publié jusqu'à présent, sauf EvaluaterBot et PatternFinder. (Un contre un, il bat EvaluaterBot et perd contre 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"

Cadre de test

Au cas où quelqu'un d'autre le trouverait utile, voici un cadre de test pour examiner les correspondances individuelles. Python2. Mettez simplement tous les adversaires qui vous intéressent dans adversants.py et changez les références à Ensemble par les vôtres.

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
Rayon
la source
Le contrôleur est prêt, vous n'avez pas eu à faire tout cela ...
SIGSTACKFAULT
1
@Blacksilver Je l'ai réalisé au moment où j'allais soumettre. Mais celui-ci fonctionne dans les versions antérieures à 3.6 et donne des informations sur les correspondances individuelles qui peuvent aider à identifier les points faibles, donc ce n'était pas une perte de temps complète.
Ray
C'est suffisant; en cours d'exécution maintenant. J'ajouterai probablement des options à mon contrôleur pour faire des choses similaires.
SIGSTACKFAULT
"Il bat tout ce qui a été publié jusqu'à présent, sauf Ensemble et PatternFinder" Je suis honoré :)
Sparr
@Sparr Oups. C'était censé dire EvaluaterBot et PatternFinder. Mais c'est en comparant le score total avec l'ensemble du terrain. PatternFinder reste le seul qui bat cela dans un match direct.
Ray
4

OldTitForTat

Le joueur de la vieille école est trop paresseux pour mettre à jour les nouvelles règles.

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

NeverCOOP

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

Si le bot adverse est défectueux ou neutre, choisissez le défaut. Sinon, si c'est le premier tour ou que le bot adverse coopère, choisissez neutre. Je ne sais pas à quel point cela fonctionnera bien ...

glietz
la source
À quoi sert / sauf?
SIGSTACKFAULT
1
@Blacksilver Je suppose que cela sert le même but que if(last):dans votre bot Grudger, en détectant s'il y a eu un tour précédent.
ETHproductions
Ahh je vois. None in "ND"les erreurs.
SIGSTACKFAULT
Parce que if last and last in "ND":c'était trop compliqué?
user253751
3

LastOptimalBot

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

Suppose que le bot adverse jouera toujours le même coup à nouveau et choisit celui qui a le meilleur gain contre lui.

Moyennes:

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
la source
oof. Peut-être que T4T ferait mieux return last.
SIGSTACKFAULT
J'aimerais ça! Si TitForTat étaitreturn last , LOB passerait 18-9 sur 6 rounds plutôt que les 13-10 sur 5 rounds qu'il obtient actuellement. Je pense que c'est bien comme ça - ne vous inquiétez pas d'optimiser les robots d'exemple.
Spitemaster
return last serait un meilleur T4T pour ce défi, je pense
Sparr
Je viens d'essayer - if(last): return last; else: return "C"c'est pire.
SIGSTACKFAULT
D'accord, mais comme le disait @Sparr, cela pourrait être plus approprié. À vous, je suppose.
Spitemaster du
3

Imitateur

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

Copie le dernier coup de l'adversaire.
Je ne m'attends pas à ce que cela fonctionne bien, mais personne n'avait encore implémenté ce classique.

MannerPots
la source
2

Dés de Dirichlet améliorés

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    

Il s'agit d'une version améliorée de Dirichlet Dice. Au lieu de prendre la distribution multinomiale attendue de la distribution de Dirichlet, il tire une distribution multinomiale au hasard de la distribution de Dirichlet. Ensuite, au lieu de dessiner à partir du multinomial et de donner la réponse optimale à cela, il donne la réponse attendue optimale au multinomial donné en utilisant les points. Le caractère aléatoire a donc été essentiellement déplacé du tirage multinomial au tirage de Dirichlet. De plus, les prieurs sont plus plats maintenant, pour encourager l'exploration.

Il est "amélioré" car il rend compte du système de points en donnant la meilleure valeur attendue par rapport aux probabilités, tout en conservant son caractère aléatoire en dessinant les probabilités elles-mêmes. Avant, j'essayais simplement de faire le meilleur gain attendu à partir des probabilités attendues, mais cela a mal tourné car il est resté bloqué et n'a pas suffisamment exploré pour mettre à jour ses dés. Il était également plus prévisible et exploitable.


Soumission originale:

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

Fondamentalement, je suppose que la réponse de l'adversaire à ma dernière sortie est une variable multinomiale (dés pondérés), une pour chacune de mes sorties, donc il y a un dé pour "C", un pour "N" et un pour "D" . Donc, si mon dernier lancer était, par exemple, un "N" alors je lance les "N-dés" pour deviner quelle serait leur réponse à mon "N". Je commence par un Dirichlet avant qui suppose que mon adversaire est quelque peu "intelligent" (plus susceptible de jouer celui avec le meilleur gain contre mon dernier jet, le moins susceptible de jouer celui avec le pire gain). Je génère la distribution multinomiale "attendue" à partir du précédent Dirichlet approprié (il s'agit de la valeur attendue de la distribution de probabilité sur leurs poids de dés). Je lance les dés pondérés de ma dernière sortie,

À partir du troisième tour, je fais une mise à jour bayésienne du Dirichlet approprié avant la dernière réponse de mon adversaire à la chose que j'ai jouée il y a deux tours. J'essaie d'apprendre de manière itérative leur pondération en dés.

J'aurais pu aussi simplement choisir la réponse avec le meilleur résultat «attendu» une fois que les dés ont été générés, au lieu de simplement lancer les dés et de répondre au résultat. Cependant, je voulais garder l'aléatoire, afin que mon bot soit moins vulnérable à ceux qui tentent de prédire un modèle.

Brûleurs
la source
2

Kevin

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

Choisit le pire choix. Le pire robot créé.

Inutile

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

Il regarde les deux derniers coups effectués par l'adversaire et choisit le plus de choses non faites sinon il choisit quelque chose au hasard. Il existe probablement une meilleure façon de procéder.

Link1J
la source
2

Moyenne historique

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)

Regarde l'histoire et trouve l'action qui aurait été la meilleure en moyenne. Démarre la coopérative.

MegaTom
la source
Cela pourrait fonctionner plus rapidement s'il ne recalculait pas les moyennes à chaque tour.
Sparr
@Sparr true. Je l'ai édité donc il le fait maintenant.
MegaTom
1

Moyenne pondérée

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"

Le comportement de l'adversaire est modélisé comme un triangle rectangle avec des coins pour CND à 0,0 0,1 1,0 respectivement. Chaque mouvement de l'adversaire déplace le point dans ce triangle vers ce coin, et nous jouons pour battre le mouvement indiqué par le point (avec C étant donné une petite tranche ajustable du triangle). En théorie, je voulais que cela ait une mémoire plus longue avec plus de poids par rapport aux mouvements précédents, mais en pratique, la méta actuelle favorise les bots qui changent rapidement, donc cela se transforme en une approximation de LastOptimalBot contre la plupart des ennemis. Affichage pour la postérité; peut-être que quelqu'un sera inspiré.

Sparr
la source
1

Tétragramme

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

Essayez de trouver un schéma dans les mouvements de l'adversaire, en supposant qu'il regarde également notre dernier mouvement.


la source
1

Poignée de main

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]

Reconnaît quand il joue contre lui-même, puis coopère. Sinon, imite LastOptimalBot, ce qui semble être la meilleure stratégie sur une ligne. Fonctionne moins bien que LastOptimalBot, d'un montant inversement proportionnel au nombre de tours. Évidemment, ce serait mieux s'il y en avait plus sur le terrain * toux ** clin d'œil *.

Sparr
la source
Soumettez simplement quelques clones qui ont un comportement différent de la prise de contact.
SIGSTACKFAULT
Cela semble exploit-y. Je pourrais soumettre un tel clone pour chaque comportement simple représenté ici.
Sparr
J'ai ajouté une clause supplémentaire disant que vous ne pouvez soumettre que cinq bots maximum.
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

Ce bot utilise l'algorithme de LastOptimalBot tant qu'il gagne. Si l'autre bot commence à le prédire, cependant, il commencera à jouer le coup que son adversaire a joué en dernier (qui est le coup qui bat le coup qui battrait LastOptimalBot). Il parcourt les transpositions simples de ces algorithmes tant qu'il continue de perdre (ou lorsqu'il s'ennuie en dessinant beaucoup).

Honnêtement, je suis surpris que LastOptimalBot soit assis en 5ème position lorsque je poste ceci. Je suis assez certain que cela fera mieux, en supposant que j'ai écrit ce python correctement.

Spitemaster
la source
0

Poignée de main

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

Pourquoi le motif vous correspond-il? Poignée de main et coopérez.

Draco18s
la source
import PatternFindertriche dans mes livres.
SIGSTACKFAULT
@Blacksilver Cela se fait tout le temps dans KOTH. Ce n'est pas différent que de copier le code dans une réponse existante et de l'utiliser. Roulette de robot: Le jeu de robot à enjeux élevés a eu lieu partout au point que les bots détecteraient si leur code était appelé par un adversaire et saboteraient le retour.
Draco18s
Très bien alors. TIL.
SIGSTACKFAULT
Je ferai le crunch demain.
SIGSTACKFAULT
Voici un exemple parfait d'utilisation du code d'autres bots. Habituellement, cela se résume à "ce gars a fait des calculs difficiles, je veux ses résultats dans ces conditions." (Ma propre entrée a fait cela assez bien; UpYours était plus dispersé dans son approche).
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]

Joue simplement une séquence codée en dur de mouvements optimisés pour battre certains des meilleurs robots déterministes.

MegaTom
la source