Ітерована трилема в'язня


19

СТАТУС ВИПУСКУ: ВІДКРИТИ

Прокоментуйте, відкрийте PR або інакше кричите на мене, якщо я пропускаю вашого бота.


Дилема в'язня ... з трьома варіантами. Божевільний, так?

Ось наша матриця виплат. Гравець A зліва, B вгорі

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

Матриця виплат розроблена так, що обидва гравці найкраще завжди співпрацювати, але ви можете отримати (як правило) вибір, вибравши нейтральний або дефектний.

Ось кілька (конкуруючих) прикладів ботів.

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

Ваш бот - клас Python3. Для кожної гри створюється новий екземпляр, round()який називається кожен раунд, за вибором опонента з останнього раунду (або Жодного, якщо це перший раунд)

Там, як місяць, виграш 50 переможців для переможця.

Особливості

  • Кожен бот грає у кожного іншого бота (1v1), включаючи самого себе, у [РЕДАКТОВАНІ] раунди.
  • Стандартні лазівки заборонені.
  • Не возитися ні з чим поза вашим класом чи іншими неприхованими шенагінгами.
  • Ви можете подати до п'яти ботів.
  • Так, ви можете реалізувати рукостискання.
  • Будь-яка відповідь, крім C, Nабо Dбуде мовчки сприйнята як N.
  • Очки кожного бота за кожну гру, яку вони грають, підсумовуються та порівнюються.

Контролер

Перевір!

Інші мови

Я зберу API, якщо комусь це потрібно.

Оцінки: 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

1
Як боти ставляться один проти одного? Я отримую від Grudger, що завжди є два боти проти / один з одним, і останній вибір противника передається боту. Скільки зіграно раундів? А для гри: Чи враховується лише результат (тобто хто виграв) чи також очки?
Чорна сова Кай

1
Ви отримаєте більше записів, якби ви зробили цю мову-агностик або, принаймні, ширше. У вас може бути клас обгорткового python, який породжує процес і надсилає йому текстові команди, щоб отримати відповіді на текст.
Спарр

1
Зроблено. Це було на пісочниці приблизно місяць!
SIGSTACKFAULT

2
Якщо ви загорнете більшість main.py while len(botlist) > 1:з botlist.remove(lowest_scoring_bot)у нижній частині циклу, ви отримаєте елімінаційний турнір із цікавими результатами.
Спарр

1
Інша версія цього дня може пройти всю історію взаємодії, а не лише останній крок. Він не сильно змінюється, хоча трохи спрощує код користувача. Але це дозволило б розширити, наприклад, галасливі канали зв'язку, які з часом з’ясовуються: "Дійсно, D, хоча я говорив C чотири рази поспіль? Ні, я не сказав D; що ти береш мене бо? О, вибачте, чи можемо ми просто забути цей тур? "
Скотт Саует

Відповіді:


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]

Виграє проти всіх раніше представлених ботів, за винятком (можливо) випадкового бота (але це може мати перевагу, оскільки він вибирає D в нічию, а D повинен бути оптимальним) і грає постійну нічию проти них самих.


Так, б'є все.
SIGSTACKFAULT

Подрапуйте це, PatternFinder трохи його б'є.
SIGSTACKFAULT

7

Наш рівновага

Цей бот взяв клас теорії ігор у коледжі, але був лінивий і не ходив до класу, де вони висвітлювали ітераційні ігри. Тож він грає лише в одиночну гру змішаної рівноваги наш. Виходить 1/5 2/5 2/5 - змішаний NE для виплат.

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

Постійне зловживання рівновагою Неша

Цей бот узяв урок-два у свого ледачого брата. Проблема його ледачого брата полягала в тому, що він не скористався фіксованими стратегіями. Ця версія перевіряє, чи опонент є постійним гравцем або титфортатом і грає відповідно, інакше він грає звичайну рівновагу наш.

Недоліком є ​​лише те, що він в середньому становить 2,2 бали за оборот, граючи проти себе.

class NashEquilibrium2:

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

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

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

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

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

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

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

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


        self.mylast = self.myret
        return self.myret

1
NashEquilibrium.round повинен приймати аргументи, навіть якщо він не використовує їх, щоб відповідати очікуваному прототипу функції.
Рей

Дякую, що ви виправили це
Оф’я

Трохи коротше: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
Роберт Грант

7

TatForTit

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

Цей бот буде чергувати вибір DNDN, поки TitForTat чергує CDCD, для середнього чистого виграшу в 3 бали за раунд, якщо я правильно прочитав матрицю виплат. Я думаю, що це може бути оптимальним щодо TitForTat. Очевидно, що можна було б покращити виявлення противника, який не є TFT, та прийняття інших стратегій, але я просто мав на меті оригінальний виграш.


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

Цей бот шукає попередні явища недавнього ігрового стану, щоб побачити, як опонент реагував на ці події, з перевагою на більш довгі поєдинки з малюнком та новітні матчі, а потім грає в хід, який "переможе" передбачуваний хід противника. Існує багато місця, щоб воно було розумнішим з усіма даними, які він відслідковує, але мені не вистачило часу на роботу над ним.


Коли ви знайдете час, розум подає їй пропуск на оптимізацію? Це легко найбільша раковина часу.
SIGSTACKFAULT

2
@Blacksilver Я щойно скоротив максимальну довжину шаблону зі 100 до 10. Він повинен працювати майже миттєво зараз, якщо ти працюєш <200 раундів
Sparr

1
Може, використання сильно складеного числа (тобто 12) набрало б кращого результату?
SIGSTACKFAULT

5

Нефрит

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'

Починає оптимістично, але стає прогресивно гіршим, оскільки опонент відмовляється співпрацювати. Багато магічних констант, які, ймовірно, можуть бути налаштовані, але це, ймовірно, не буде робити досить добре, щоб виправдати час.


5

Ансамбль

Тут працює ансамбль споріднених моделей. Окремі моделі враховують різні обсяги історії та мають можливість або завжди обирати хід, який оптимізує очікувану різницю виплат, або випадковим чином вибрати хід пропорційно очікуваній різниці виплат.

Кожен учасник ансамблю потім голосує за свій бажаний хід. Вони отримують кількість голосів, рівну тому, наскільки більше виграли, ніж опонент (це означає, що жахливі моделі отримають негативні голоси). Незалежно від того, який хід виграє, голос вибирається.

(Вони, мабуть, повинні розділити свої голоси між ходами пропорційно тому, наскільки вони віддають перевагу кожному, але мені зараз недостатньо все одно, щоб зробити це.)

Він б'є все, що розміщено дотепер, крім EvaluaterBot та PatternFinder. (Один на один, він б'є EvaluaterBot і програє 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"

Тестова рамка

Якщо хтось вважає це корисним, ось тестова рамка для перегляду окремих поєдинків. Python2. Просто введіть усіх опонентів, які вас цікавлять, і замініть посилання на ансамбль на власні.

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

Контролер готовий, вам не потрібно було робити все це ...
SIGSTACKFAULT

1
@Blacksilver Я зрозумів, що так само, як я збирався подати заяву. Але ця версія працює у версіях до 3.6 та дає інформацію про окремі поєдинки, які можуть допомогти визначити слабкі місця, тому це не було повною тратою часу.
Рей

Справедливо; працює зараз. Я, мабуть, додаю варіанти до свого контролера, щоб робити подібні речі.
SIGSTACKFAULT

"Він перемагає все, що розміщено поки що, окрім ансамблю та PatternFinder" Мені честь :)
Sparr

@Sparr На жаль. Це повинно було сказати EvaluaterBot і PatternFinder. Але це коли порівнювати загальний бал проти всього поля. PatternFinder залишається єдиним, хто перемагає це в прямому поєднанні.
Рей

4

OldTitForTat

Гравець старої школи занадто лінивий, щоб оновити нові правила.

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

3

NeverCOOP

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

Якщо дефект протиборчого бота або нейтральний, виберіть дефект. В іншому випадку, якщо це перша черга або протилежний бот співпрацює, виберіть нейтральний. Я не впевнений, наскільки добре це буде працювати ...


Що таке спробу / окрім?
SIGSTACKFAULT

1
@Blacksilver Я припускаю, що він служить тій самій цілі, що і if(last):у вашого бота Grudger, виявляючи, чи був попередній раунд.
ETHproductions

ах, я бачу. None in "ND"помилки.
SIGSTACKFAULT

Тому що if last and last in "ND":було занадто складно?
користувач253751

3

LastOptimalBot

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

Передбачає, що протиборчий бот завжди буде знову грати в один і той самий хід і вибирає той, який має найкращий виграш проти нього.

Середні показники:

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

уф. Можливо, T4T зробить краще як return last.
SIGSTACKFAULT

Мені б це сподобалося! Якби TitForTat був return last, LOB пішов би 18-9 за 6 раундів, а не 13-10 за 5 раундів, які він отримує в даний час. Я думаю, що це нормально, як є - не хвилюйтеся щодо оптимізації прикладів ботів.
Шпітемастер

return lastбув би кращим T4T для цього виклику, я думаю
Спарр

Щойно спробував - тим if(last): return last; else: return "C"гірше.
SIGSTACKFAULT

Правильно, але, як казав @Sparr, це може бути більш доречним. Думаю, до вас.
Шпітемастер

3

CopyCat

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

Копіює останній хід суперника.
Я не очікую, що це буде добре, але ніхто ще не втілив цю класику.


2

Поліпшені кістки Діріхле

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    

Це вдосконалена версія Dichichlet 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

В основному я припускаю, що відповідь опонента на мій останній результат - це мультиноміальна змінна (зважена кістка), по одній для кожного мого виходу, тому є кістки для "C", одна для "N" і одна для "D" . Отже, якщо в моєму останньому ролику було, наприклад, "N", то я перекидаю "N-кубики", щоб здогадатися, якою буде їх відповідь на "N". Я починаю з Діріхле перед тим, що передбачає, що мій опонент дещо "розумний" (швидше за все, гратимуть із найкращим виграшем проти мого останнього рулону, найменш імовірно, що він зіграє той, що має найгірший окуп). Я генерую "очікуваний" мультиноміальний розподіл з відповідного попереднього Діріхле (це очікуване значення розподілу ймовірностей за вагою їх кісток). Я кочую зважені кістки мого останнього результату,

Починаючи з третього раунду, я оновлюю байесівський відповідний Діріхле до останньої відповіді мого опонента на те, що я грав у два тури тому. Я намагаюся ітеративно вивчити їхні кістки.

Я міг би також просто вибрати відповідь з найкращим "очікуваним" результатом, щойно генерує кістки, замість того, щоб просто катати кістки та відповідати на результат. Однак я хотів утримати випадковість, щоб мій бот був менш вразливим до тих, хто намагається передбачити схему.


2

Кевін

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

Вибирає найгірший вибір. Найгірший бот зробив.

Марно

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

Він переглядає два останніх кроки, зроблені опонентом, і вибирає найбільш не зроблені інші, він вибирає щось випадкове. Напевно, є кращий спосіб зробити це.


2

Історичний середній

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)

Переглядає історію та знаходить дію, яка була б найкраща в середньому. Заводить кооператив.


Це може працювати швидше, якби не перераховувати середні значення в кожному раунді.
Спарр

@Sparr правда. Я відредагував це так, як зараз.
MegaTom

1

Середнє зважене

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"

Поведінка противника моделюється як правильний трикутник з кутами для CND на 0,0 0,1 1,0 відповідно. Кожен хід супротивника зміщує точку в цьому трикутнику в бік цього кута, і ми граємо, щоб бити по ходу, зазначеному точкою (при цьому C надається регульовано невеликим відрізком трикутника). Теоретично я хотів, щоб ця пам'ять була довшою з більшою вагою до попередніх кроків, але на практиці нинішня мета надає перевагу ботам, які швидко змінюються, тому це переходить у наближення LastOptimalBot до більшості ворогів. Публікація для нащадків; можливо, хтось надихнеться.


1

Тетраграма

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

Постарайтеся знайти зразок у ходах суперника, припускаючи, що вони також спостерігають за нашим останнім кроком.


1

Рукостискання

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]

Визнає, коли грає проти себе, потім співпрацює. Інакше імітує LastOptimalBot, що здається найкращою однолінійною стратегією. Діє гірше, ніж LastOptimalBot, на величину, обернено пропорційну кількості раундів. Очевидно було б краще, якби в полі було більше його копій * кашель ** підморгує *.


Просто подайте кілька клонів, які мають різну поведінку без рукостискань.
SIGSTACKFAULT

Це здається експлоататором. Я міг би подати один такий клон за кожну просту поведінку, представлену тут.
Спарр

Я додав додатковий пункт про те, що ви можете подати максимум п'ять ботів.
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

Цей бот використовує алгоритм LastOptimalBot до тих пір, поки він виграє. Якщо інший бот почне прогнозувати це, проте він почне грати незалежно від того, в якому русі його противник зіграв би останнім (це той хід, який б'є ходу, який би переміг LastOptimalBot). Він проходить цикли простими перекладами цих алгоритмів до тих пір, поки він продовжує втрачати (або коли йому нудно, багато малюючи).

Чесно кажучи, я здивований, що LastOptimalBot сидить в п'ятій частині, коли я публікую це. Я впевнений, що це буде краще, припускаючи, що я написав цей пітон правильно.


0

РукостисканняPatternMatch

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

Чому візерунок відповідає собі? Рукостискання та співпраця.


import PatternFinderобман у моїх книгах.
SIGSTACKFAULT

@Blacksilver Це робиться весь час у КОТІ. Це не відрізняється від копіювання коду в існуючу відповідь та використання його. Роботна рулетка: Робота з азартними іграми з високими ставками, якби це відбувалося повсюдно, до того, що боти виявлять, якби їх код викликав противник і саботував повернення.
Draco18s

Тоді добре. TIL.
SIGSTACKFAULT

Я завтра зроблю хрускіт.
SIGSTACKFAULT

Ось ідеальний приклад використання коду інших ботів. Зазвичай зводиться до того, що "хлопець розробив якусь хитру математику, я хочу, щоб його результати були в цих умовах". (Мій власний запис зробив це досить добре; UpYours був більш розсіяний у своєму підході).
Draco18s

0

Жорстко кодований

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]

Просто відтворює твердо кодовану послідовність рухів, оптимізовану для перемоги над деякими найкращими детермінованими ботами.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.