読者です 読者をやめる 読者になる 読者になる

ドミニオンのシミュレーション改善

id:nishiohirokazu 氏がpythonドミニオンのシュミレーションという琴線に触れる事をやっていた。
ドミニオンのシミュレーション - 西尾泰和のはてなダイアリー
これは、

ドミニオンのコインを買う戦略をする際に、初手で礼拝堂を買うべきかどうかについて。
初手終了後10ターンの間「礼拝堂が出たら屋敷を捨てる。買えるなら銀貨や金貨を買う。」という戦略をとった場合に、デッキ1枚あたりのコイン価値がどうなるかを1万回シミュレーションした。結果、「礼拝堂+銀貨」は「銀貨+銀貨」、「礼拝堂+礼拝堂」、「銀貨+堀」のいずれよりも優れていることがわかった。

という、ある程度ドミニオンをやっている人であれば当然体感的に理解しているであろう事の裏付けを取ったものであるが

なお、この実験では礼拝堂を屋敷を捨てるためだけに使っているが実戦では銅貨を捨てることに使ってもよいので「礼拝堂+銀貨」と「礼拝堂+礼拝堂」はより高い値になりうる。

という記述にあるように、銅貨を捨てることがシミュレーションに盛り込まれていなかったので、実際によく行うように、銀貨/金貨を買うのに余った銅貨を捨てる remove_copper を追加してみた。

結果、初手で礼拝堂と銀貨を買った場合、平均1.87、SD 0.14。礼拝堂2枚を買った場合、平均1.83, SD 0.15とより高い結果が得られた。

from random import shuffle
from copy import copy

NUM_TRIAL = 10000

def draw(deck, used, n):
    if len(deck) < n:
        shuffle(used)
        deck.extend(used)
        used = []
    hand = deck[:n]
    deck = deck[n:]
    return deck, used, hand

def count_money(cards):
    return cards.count("1") + cards.count("2") * 2 + cards.count("3") * 3

def remove_copper(cards):
    coppers = cards.count("1")
    others  = cards.count("2") * 2 + cards.count("3") * 3

    if others >= 6:
        while "1" in cards:
            cards.remove("1")
    elif others + coppers >= 6:
        removable = others + coppers - 6
        i = 0
        while "1" in cards:
            if removable <= i:
                break
            cards.remove("1")
            i += 1
    elif others + coppers >= 3:
        removable = others + coppers - 3
        i = 0
        while "1" in cards:
            if removable <= i:
                break
            cards.remove("1")
            i += 1

def test(initial_deck):
    sum = 0
    sumsq = 0.0

    for triel in range(NUM_TRIAL):
        deck = copy(initial_deck)
       shuffle(deck)
        used = []
        for i in range(10):
            deck, used, hand = draw(deck, used, 5)
            if "C" in hand:
                while "E" in hand:
                    hand.remove("E")
                remove_copper(hand)
            elif "M" in hand:
                deck, used, got = draw(deck, used, 2)
                hand += got

            money = count_money(hand)
            if money >= 6:
                used.append("3")
            elif money >= 3:
                used.append("2")

            used += hand

        deck += used
        money = count_money(deck)
        x = float(money) / len(deck)
        sum += x
        sumsq += x * x

    from math import sqrt
    print sum / NUM_TRIAL, sqrt(sumsq / NUM_TRIAL - (sum / NUM_TRIAL) ** 2)


print "a silver only"
test(list("1111111EEE2"))
print "silver and chapel"
test(list("1111111EEE2C"))
print "two silvers"
test(list("1111111EEE22"))
print "two chapels"
test(list("1111111EEECC"))
print "silver and moat"
test(list("1111111EEE2M"))