ドミニオンでいつ勝利点購入に切り替えると有利か

1行まとめ:ドミニオンでコイン戦略をする場合、10ターン目で勝利点購入に切り替えるのが一番有利だった

今までの話

でわかったように、アクションカードを一切買わなくても12ターン目には平均コインが1.63になるので、1デッキが平均8コインを超えるようになる。つまりその頃には属州が買えるターンもあるってことだ。ここで問題になるのは「いつコイン購入から勝利点購入に切り替えるか」だ。焦って早く買ってはコイン濃度が薄まってしまい、後で伸び悩む。一方、コイン濃度を高めてから買いはじめれば勝利点の獲得速度は早いが、追いつく前にゲームが終わってしまう。

周りのプレイヤーがどう行動するかによって大きな影響を受けるだろうと思ったので、「nターン目手前までは金貨か銀貨を買う。nターン目以降は購入できる最も高い勝利点を買う。勝利点が買えない場合は金貨や銀貨を買う」という戦略でnを変えた4人のプレイを10000回ずつシミュレーションした。

10, 10, 10, 20のとき、ゲーム終了時に獲得した勝利点の平均と標準偏差

player 0
38.4287 4.46966624145
player 1
38.3893 4.52960765519
player 2
38.4571 4.57748398031
player 3
13.7286 7.82187586452

と10ターン目から3人が勝利点を買い始めているのに10ターンほど遅れると完全に出遅れて、平均で25点も差がついてしまう。

遅れが5ターンくらいだったらどうだろう?10, 15, 10, 10のとき

player 0
34.0452 3.95185487588
player 1
23.1996 6.612349041
player 2
33.7394 3.9206233739
player 3
33.8632 3.99002327813

それでも10点ほど出遅れてしまう。

2ターン差だったら?
10, 10, 12, 10の場合

player 0
31.7152 4.23264562183
player 1
31.8004 4.28403546204
player 2
29.1521 5.47075548622
player 3
31.6406 4.27626374771

平均ではやはり2〜3点出遅れているが、標準偏差が4〜5点あるのでこの程度の差であれば運次第で逆転しうる。

12ターン開始の人が少数派だからまけるということも考えられる?12, 10, 12, 10の時

player 0
28.8797 4.86765116971
player 1
32.0421 3.97332701775
player 2
28.9603 4.95125478137
player 3
31.9745 3.95330870917

やはり12ターン開始の人が出遅れている。

12, 10, 12, 12でも

player 0
28.7421 4.55216295732
player 1
32.1273 3.75575487885
player 2
28.9181 4.47363301915
player 3
28.8409 4.49593006952

「やはり12ターン開始と10ターン開始では10ターン開始の方が有利」ってのは何人が10点開始を選ぶかには無関係みたいだね。

じゃあもっと手前に開始するとどうなるのかな?8, 10, 8, 8のとき

player 0
31.69 5.29408160118
player 1
36.8734 5.83316144471
player 2
31.5036 5.26218462618
player 3
31.7262 5.27483019253

おやおや、逆転した。8ターン開始よりは10ターン開始の方が有利?

8, 10, 8, 10の時

player 0
30.8428 4.89545586846
player 1
34.7508 5.42166942556
player 2
30.8484 4.81404377213
player 3
34.7647 5.48778041744

10, 10, 8, 10の時

player 0
33.1002 5.02897205799
player 1
32.916 5.01730445558
player 2
30.5101 4.3520682428
player 3
33.1524 4.99991742332

ふむふむ、8ターン開始よりは10ターン開始の方が有利なのか。

もちろん戦略に関係なく無条件に10ターン開始が有利ってわけではあるまい。おそらく「コイン密度がある程度の量に達したら勝利点購入を始めるべき」ということなんだろう。あと当たり前だけども20枚中32コインの1.6と40枚中64コインの1.6では勝利点を買った時の薄まる速度が異なるので後者のほうが有利なわけなので、濃度だけで決まるもんでもないのだろう。

以下ソースコード

from random import shuffle, randint
from copy import copy
from math import sqrt

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 count_vp(cards):
    return cards.count("E") + cards.count("V3") * 3 + cards.count("V6") * 6

class CoinStrategy:
    def __init__(self, threshold):
        self.count = 0
        self.threshold = threshold

    def __call__(self, (deck, used, hand), game):
        self.count += 1
        money = count_money(hand)
        if self.count < self.threshold:
            # buy money
            if money >= 6:
                used.append("3")
            elif money >= 3:
                used.append("2")
        else:
            # buy victory point
            if money >= 8:
                used.append("V6")
                game.vp6 -= 1
            elif money >= 5 and game.vp3 > 0:
                used.append("V3")
                game.vp3 -= 1
            elif money >= 2 and game.vp1 > 0:
                used.append("E")
                game.vp1 -= 1
            elif money >= 6:
                used.append("3")
            elif money >= 3:
                used.append("2")
                
        return deck, used, hand

class Player:
    def __init__(self, strat, game):
        self.used = []
        self.deck = list("1111111EEE")
        shuffle(self.deck)
        self.strat = strat
        self.game = game
    
    def do_turn(self):
        self.deck, self.used, hand = self.strat(draw(self.deck, self.used, 5), self.game)
        self.used += hand
        
class GameManager:
    def __init__(self):
        self.vp6 = 12
        self.vp3 = 12
        self.vp1 = 12

    def is_finished(self):
        return self.vp6 == 0

def test(thresholds):
    NUM_PLAYER = len(thresholds)
    sum = [0.0] * NUM_PLAYER
    sumsq = [0.0] * NUM_PLAYER
    for _trial in range(NUM_TRIAL):
        game = GameManager()
        players = [Player(CoinStrategy(th), game) for th in thresholds]
        i = randint(0, NUM_PLAYER - 1)
        while not game.is_finished():
            p = players[i]
            p.do_turn()
            i = (i + 1) % NUM_PLAYER

        for i in range(NUM_PLAYER):
            p = players[i]
            p.deck += p.used
            money = count_money(p.deck)
            vp = count_vp(p.deck)
            #print "player", i
            #print money, vp
            sum[i] += vp
            sumsq[i] += vp * vp

    for i in range(NUM_PLAYER):
        print "player", i
        print sum[i] / NUM_TRIAL, sqrt(sumsq[i] / NUM_TRIAL - (sum[i] / NUM_TRIAL) ** 2)


test([12, 10, 12, 12])