The strength of the Money deck in Dominion

Last time my friends and I played Dominion, we discussed how the Money Deck is the fastest and therefore the best deck. I instantly found this disturbing as it would remove any choices the player has. This makes the game trivial. The luckier starts first and wins by playing one turn faster than the other players.

Let’s dissect this statement. Dominion is a deck-building card game in which the players start each with 7 Copper and 3 Estates. I assume the reader knows about how Dominion is played, if not the official rule book can be found online. To understand that the Money Deck is the fastest deck, we need to understand the concept of speed in Dominion.

Speed is the time, in turns, it takes to redraw the exact same card one has in his hand. For example, if I buy a Silver on my first turn, I would draw that card possibly on my third or fourth turn and surely on my fifth. After my second turn, my discard pile gets reshuffled and the chances for this Silver to be in the top 5 cards is 5/11 if I don’t buy another card on my second turn. If I do not get the Silver on my third turn, I have another 5/6 chance to draw it on my fourth turn. If still haven’t drawn my Silver, I will get it on my fifth turn. On average, I would need (2 * 5/11 + 3 * 6/11 * 5/6 + 4 * 6/11 * 1/6) ≈ 2.64 turns to get this card. The same applies to the cards I used to buy the Silver. By buying cards, the player is making the deck slower.

Another possibility to measure the speed of a deck is the number of turns needed to work through the whole deck. At the start, we only need 2 turns but after immediately after our first purchase, we need 3 turns. At 16 cards in our deck, we then need 4 turns. We can calculate this metric more easily than the redraw time: Deck Size / Hand Size = Speed. So, purchasing the Silver slows the deck by 1/5 of a turn. However, buying the Silver raises my deck’s overall purchasing power.

By purchasing power, I mean average coin value of my hand. At the beginning of the game, each player has 5 * 7 / 10 = 3.5 coins on average in his drawn hand. With this Silver, the average coin value on hand raises to 5 * 9 / 11 ≈ 4.1, an increase of 0.6 coins. One should note, that this is only the average, the overall card distribution may vary wildly. Each Silver bought raises the average coin value to the point where we have so many cards that we almost every time draw 5 silvers and the average coin value is 10. The goal is to achieve an average coin value above 8 to buy a Province on each turn. If the player can do this more often than the competition, then he wins the game. Here is a python code snippet to calculate the moment we reach this point.

1
2
3
4
5
6
7
8
9
10
11
def average_coin_value_with_silvers(silvers):
    total_coin_value = 7 + silvers * 2
    total_cards_in_deck = 10 + silvers
    return 5 * total_coin_value / total_cards_in_deck

for x in range(100):
    value =average_coin_value_with_silvers(x)
    print("Current average coin value: %s " % value)
    if value > 8:
        print("ACV > 8 fulfilled on turn %s." % x)
        break

OUTPUT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Current average coin value: 3.5
Current average coin value: 4.090909090909091
Current average coin value: 4.583333333333333
Current average coin value: 5.0
Current average coin value: 5.357142857142857
Current average coin value: 5.666666666666667
Current average coin value: 5.9375
Current average coin value: 6.176470588235294
Current average coin value: 6.388888888888889
Current average coin value: 6.578947368421052
Current average coin value: 6.75
Current average coin value: 6.904761904761905
Current average coin value: 7.045454545454546
Current average coin value: 7.173913043478261
Current average coin value: 7.291666666666667
Current average coin value: 7.4
Current average coin value: 7.5
Current average coin value: 7.592592592592593
Current average coin value: 7.678571428571429
Current average coin value: 7.758620689655173
Current average coin value: 7.833333333333333
Current average coin value: 7.903225806451613
Current average coin value: 7.96875
Current average coin value: 8.030303030303031
ACV > 8 fulfilled on turn 23.

However, each Province bought slows our deck and lowers our average coin value on hand. Here is a code snippet which simulates the point where we start buying Provinces and Silvers to get back to the magic threshold of 8. The game ends when the player buys 7 provinces. This results does not allow for another player to win. I would even go so far as to say, if one player owns 7 provinces, he will always win.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Dominion(object):
    def __init__(self):
        self.deck = [1 for x in range(7)] # Add Coppers to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        random.shuffle(self.deck) # Shuffle deck
        self.discard_pile = [] # Empty Discard Pile
        print(self.deck)
        self.won = False
        self.buy_log = []

    # Draw a card
    def draw_card(self):
        if not self.deck:
            if self.discard_pile:
                self.deck = self.discard_pile
                random.shuffle(self.deck)
                self.discard_pile = []
            else:
                return None

        return self.deck.pop()


    # Play the turn by drawing 5 cards and buying Silvers or Provinces.
    def play_turn(self):
        starting_five = [self.draw_card() for x in range(5)] # Draw possibly 5 cards
        starting_five = [x for x in starting_five if x] # Remove None elements from list
        drawn_cards = [x for x in starting_five if x not in ("E", "P")] # Get only cards with coin value.
        coin_value = sum(drawn_cards)
        bought = self.buy_silvers_and_provinces(coin_value)
        for card in starting_five:
            self.discard_pile.append(card)
        print("Had cards %s, with value %s and bought %s." % (starting_five, coin_value, bought))


    def buy_silvers_and_provinces(self, coin_value):
        if coin_value >= 8:
            self.discard_pile.append("P")
            self.buy_log.append("P")
            return "Province"
        elif coin_value >= 3:
            self.discard_pile.append(2)
            self.buy_log.append(2)
            return "Silver"
        else:
            self.buy_log.append("Nothing"
                                "")
            return "Nothing"

    def check_for_win(self):
        domain = self.deck + self.discard_pile
        domain = [x for x in domain if x == "P"]
        self.won = len(domain) > 6

    def play_until_win(self):
        turns = 0
        while not self.won:
            turns += 1
            print("Turn %s" % turns)
            self.play_turn()
            self.check_for_win()
        print("Won on turn %s with domain %s" % (turns, self.deck + self.discard_pile))
        print("Buy order was %s." % self.buy_log)
        return turns

if __name__ == '__main__':
    g = Dominion()
    g.play_until_win()

OUTPUT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
['E', 1, 1, 'E', 1, 'E', 1, 1, 1, 1]
Turn 1
Had cards [1, 1, 1, 1, 'E'], with value 4 and bought Silver.
Turn 2
Had cards [1, 'E', 1, 1, 'E'], with value 3 and bought Silver.
Turn 3
Had cards ['E', 1, 2, 1, 'E'], with value 4 and bought Silver.
Turn 4
Had cards ['E', 1, 1, 1, 1], with value 4 and bought Silver.
Turn 5
Had cards [2, 1, 1, 1, 1], with value 6 and bought Silver.
Turn 6
Had cards [1, 1, 'E', 2, 'E'], with value 4 and bought Silver.
Turn 7
Had cards [2, 'E', 1, 2, 1], with value 6 and bought Silver.
Turn 8
Had cards ['E', 'E', 1, 2, 1], with value 4 and bought Silver.
Turn 9
Had cards [2, 1, 1, 2, 2], with value 8 and bought Province.
Turn 10
Had cards [1, 1, 'E', 1, 'E'], with value 3 and bought Silver.
Turn 11
Had cards ['E', 2, 2, 'P', 1], with value 5 and bought Silver.
Turn 12
Had cards [2, 1, 2, 2, 1], with value 8 and bought Province.
Turn 13
Had cards [2, 2, 1, 2, 1], with value 8 and bought Province.
Turn 14
Had cards [1, 'P', 1, 'P', 2], with value 4 and bought Silver.
Turn 15
Had cards [2, 1, 1, 'E', 'E'], with value 4 and bought Silver.
Turn 16
Had cards [2, 2, 1, 2, 2], with value 9 and bought Province.
Turn 17
Had cards ['E', 2, 'E', 1, 'E'], with value 3 and bought Silver.
Turn 18
Had cards [1, 1, 2, 2, 2], with value 8 and bought Province.
Turn 19
Had cards [2, 1, 1, 1, 'P'], with value 5 and bought Silver.
Turn 20
Had cards [2, 'P', 1, 2, 'P'], with value 5 and bought Silver.
Turn 21
Had cards ['P', 2, 2, 2, 2], with value 8 and bought Province.
Turn 22
Had cards [2, 1, 2, 'P', 'E'], with value 5 and bought Silver.
Turn 23
Had cards [1, 1, 2, 1, 'P'], with value 5 and bought Silver.
Turn 24
Had cards [2, 2, 2, 2, 'P'], with value 8 and bought Province.
Won on turn 24 with domain [1, 2, 2, 2, 1, 'P', 2, 'E', 'P', 1, 2, 'E', 'P', 2, 2, 2, 2, 2, 1, 2, 'P', 'E', 2, 1, 1, 2, 1, 'P', 'P', 2, 2, 2, 2, 'P']
Buy order was [2, 2, 2, 2, 2, 2, 2, 2, 'P', 2, 2, 'P', 'P', 2, 2, 'P', 2, 'P', 2, 2, 'P', 2, 2, 'P'].

Now lets calculate the average turn on which this strategy wins (with 7 or more provinces). Heres the adjusted code snippet (and without the print statements in the Dominion class) with one million runs:

1
2
3
if __name__ == '__main__':
    results = [ Dominion().play_until_win() for x in range(1000000)]
    print("Average turns to win: %s" % (sum(results)/len(results),))

OUTPUT:

Average turns to win: 29.982415

30 turns to win on average on a simple strategy. Now to make this playstyle viable and interesting, let’s add Gold to the mix. We buy Provinces whenever possible, then Gold, then Silver. Here is the code snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Dominion(object):
    def __init__(self):
        self.deck = [1 for x in range(7)] # Add Coppers to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        random.shuffle(self.deck) # Shuffle deck
        self.discard_pile = [] # Empty Discard Pile
        # print(self.deck)
        self.won = False
        self.buy_log = []

    # Draw a card
    def draw_card(self):
        if not self.deck:
            if self.discard_pile:
                self.deck = self.discard_pile
                random.shuffle(self.deck)
                self.discard_pile = []
            else:
                return None

        return self.deck.pop()


    # Play the turn by drawing 5 cards and buying Silvers or Provinces.
    def play_turn(self):
        starting_five = [self.draw_card() for x in range(5)] # Draw possibly 5 cards
        starting_five = [x for x in starting_five if x] # Remove None elements from list
        drawn_cards = [x for x in starting_five if x not in ("E", "P")] # Get only cards with coin value.
        coin_value = sum(drawn_cards)
        bought = self.buy_silver_gold_provinces(coin_value)
        for card in starting_five:
            self.discard_pile.append(card)
        print("Had cards %s, with value %s and bought %s." % (starting_five, coin_value, bought))


    def buy_silver_gold_provinces(self, coin_value):
        if coin_value >= 8:
            self.discard_pile.append("P")
            self.buy_log.append("P")
            return "Province"
        elif coin_value >= 6:
            self.discard_pile.append(3)
            self.buy_log.append(3)
            return "Gold"
        elif coin_value >= 3:
            self.discard_pile.append(2)
            self.buy_log.append(2)
            return "Silver"
        else:
            self.buy_log.append("Nothing")
            return "Nothing"

    def check_for_win(self):
        domain = self.deck + self.discard_pile
        domain = [x for x in domain if x == "P"]
        self.won = len(domain) > 6

    def play_until_win(self):
        turns = 0
        while not self.won:
            turns += 1
            print("Turn %s" % turns)
            self.play_turn()
            self.check_for_win()
        print("Won on turn %s with domain %s" % (turns, self.deck + self.discard_pile))
        print("Buy order was %s." % self.buy_log)
        return turns

if __name__ == '__main__':
    g = Dominion()
    g.play_until_win()

OUTPUT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Turn 1
Had cards [1, 'E', 'E', 1, 1], with value 3 and bought Silver.
Turn 2
Had cards ['E', 1, 1, 1, 1], with value 4 and bought Silver.
Turn 3
Had cards ['E', 'E', 1, 1, 1], with value 3 and bought Silver.
Turn 4
Had cards [1, 1, 1, 1, 2], with value 6 and bought Gold.
Turn 5
Had cards ['E', 2, 1, 1, 2], with value 6 and bought Gold.
Turn 6
Had cards [1, 1, 1, 'E', 1], with value 4 and bought Silver.
Turn 7
Had cards [3, 1, 'E', 2, 1], with value 7 and bought Gold.
Turn 8
Had cards [1, 2, 'E', 3, 1], with value 7 and bought Gold.
Turn 9
Had cards [2, 1, 1, 'E', 1], with value 5 and bought Silver.
Turn 10
Had cards [2, 1, 'E', 1, 2], with value 6 and bought Gold.
Turn 11
Had cards [1, 'E', 2, 3, 3], with value 9 and bought Province.
Turn 12
Had cards [3, 3, 1, 1, 1], with value 9 and bought Province.
Turn 13
Had cards ['E', 2, 1, 2, 1], with value 6 and bought Gold.
Turn 14
Had cards [2, 'E', 3, 3, 2], with value 10 and bought Province.
Turn 15
Had cards [1, 'P', 2, 'E', 'P'], with value 3 and bought Silver.
Turn 16
Had cards [1, 1, 3, 1, 3], with value 9 and bought Province.
Turn 17
Had cards [1, 3, 3, 2, 'P'], with value 9 and bought Province.
Turn 18
Had cards [1, 1, 2, 'E', 'P'], with value 4 and bought Silver.
Turn 19
Had cards [2, 1, 1, 3, 'E'], with value 7 and bought Gold.
Turn 20
Had cards [2, 2, 1, 3, 'E'], with value 8 and bought Province.
Turn 21
Had cards ['P', 'P', 3, 1, 2], with value 6 and bought Gold.
Turn 22
Had cards [3, 'P', 'E', 1, 3], with value 7 and bought Gold.
Turn 23
Had cards [3, 1, 2, 'P', 2], with value 8 and bought Province.
Won on turn 23 with domain [2, 3, 1, 1, 2, 'P', 2, 'P', 1, 1, 3, 'E', 3, 'E', 2, 'P', 3, 1, 2, 3, 'P', 3, 3, 'P', 'E', 1, 3, 'P', 3, 1, 2, 'P', 2]
Buy order was [2, 2, 2, 3, 3, 2, 3, 3, 2, 3, 'P', 'P', 3, 'P', 2, 'P', 'P', 2, 3, 'P', 3, 3, 'P'].

The average turns to win with the buy gold and silver strategy simulated with one million runs is:

Average turns to win: 23.726031

24 turns to win with this better strategy, an improvement of 6 turns. The Money Deck, in its core, is this strategy.

How is this strategy faster than other strategies?

By purchasing anything other than Silver or Gold, you are slowing down your deck and diminishing your average coin value. Since the actual distribution of the cards in the deck can vary wildly this is not a problem when the player buys only a couple of other cards. This is actually a viable strategy in the Money Deck, so you can use your free Action per turn. But if the player continues to buy cards which do not provide coin value nor speed up the deck, by drawing more cards per turn, then you are getting outpaced by the much leaner and more stable Money Deck. The Money deck can sped up even more by trashing cards, first the 3 Estates and then all your Coppers with trasher cards like Chapel or Steward from the first two editions. I have to point out this is only true for the first two expansions. In later editions of the game there are cards which favor high card count decks like Gardens.

Lastly, we will calculate how the money deck plays against other 2 money decks. By comparing the average win percentage, we get a feeling for the deck.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import random
from multiprocessing import Pool


class DominionGame(object):
    def __init__(self, player_count):
        self.player_count = player_count
        self.player_won = None
        self.provinces = 8 if player_count <= 2 else 12
        self.provinces_at_start = self.provinces
        self.players = [DominionPlayer(self) for x in range(player_count)]
        self.current_player = None
        self.pli = 0  # One turn = pli / player count. Each player gets one pli per turn.

    def player_buys_province(self, player):
        self.provinces -= 1

    def get_winner(self):
        victory_points = [player.get_victory_points() for player in self.players]
        # print(victory_points)
        self.won = victory_points.index(max(victory_points))
        if max(victory_points) == 3 + (self.provinces_at_start / self.player_count) * 6:
            winner = -1
        else:
            winner = self.won
        return (winner, self.pli // self.player_count)

    def current_player_plays(self):
        current = self.pli % self.player_count
        self.current_player = self.players[current]
        self.current_player.play_turn()
        self.pli += 1

    def play_until_over(self):
        while self.provinces:
            self.current_player_plays()

        self.won = self.get_winner()
        # print("Player %s won with %s Provinces" % (self.won[0] + 1, self.players[self.won[0]].get_provinces()))
        return self.won


class DominionPlayer(object):
    def __init__(self, game):
        self.game = game
        self.deck = [1 for x in range(7)]  # Add Coppers to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        self.deck.append("E")  # Add Estate to the deck.
        random.shuffle(self.deck)  # Shuffle deck
        self.discard_pile = []  # Empty Discard Pile
        # print(self.deck)
        self.won = False
        self.buy_log = []

    # Draw a card
    def draw_card(self):
        if not self.deck:
            if self.discard_pile:
                self.deck = self.discard_pile
                random.shuffle(self.deck)
                self.discard_pile = []
            else:
                return None

        return self.deck.pop()

    # Play the turn by drawing 5 cards and buying Silvers or Provinces.
    def play_turn(self):
        starting_five = [self.draw_card() for x in range(5)]  # Draw possibly 5 cards
        starting_five = [x for x in starting_five if x]  # Remove None elements from list
        drawn_cards = [x for x in starting_five if x not in ("E", "P")]  # Get only cards with coin value.
        coin_value = sum(drawn_cards)
        bought = self.buy_silver_gold_provinces(coin_value)
        for card in starting_five:
            self.discard_pile.append(card)
            # print("Had cards %s, with value %s and bought %s." % (starting_five, coin_value, bought))

    def buy_silver_gold_provinces(self, coin_value):
        if coin_value >= 8:
            self.discard_pile.append("P")
            self.buy_log.append("P")
            self.game.player_buys_province(self)
            return "Province"
        elif coin_value >= 6:
            self.discard_pile.append(3)
            self.buy_log.append(3)
            return "Gold"
        elif coin_value >= 3:
            self.discard_pile.append(2)
            self.buy_log.append(2)
            return "Silver"
        else:
            self.buy_log.append("Nothing")
            return "Nothing"

    def check_for_win(self):
        domain = self.deck + self.discard_pile
        domain = [x for x in domain if x == "P"]
        self.won = len(domain) > 6

    def play_until_win(self):
        turns = 0
        while not self.won:
            turns += 1
            # print("Turn %s" % turns)
            self.play_turn()
            self.check_for_win()
        # print("Won on turn %s with domain %s" % (turns, self.deck + self.discard_pile))
        # print("Buy order was %s." % self.buy_log)
        return turns

    def get_victory_points(self):
        result_deck = self.deck + self.discard_pile
        provinces = [6 for x in result_deck if x == "P"]
        estates = [1 for x in result_deck if x == "E"]
        return sum(provinces) + sum(estates)

    def get_provinces(self):
        return len([6 for x in self.deck + self.discard_pile if x == "P"])


def get_game(x):
    return DominionGame(3).play_until_over()


if __name__ == '__main__':
    # g = Dominion()
    # g.play_until_win()
    pool = Pool(8)

    results = pool.map(get_game, range(1000000))  # Calculate on 8 cores.
    game_count = len(results)
    player_one = 0
    player_two = 0
    player_three = 0
    tie = 0
    for result in results:
        # print(result)
        if result[0] == 0:
            player_one += 1
        elif result[0] == 1:
            player_two += 1
        elif result[0] == 2:
            player_three += 1
        elif result[0] == -1:
            tie += 1
        else:
            print("Error")

    print("Average turns to win: %s" % (sum([result[1] for result in results]) / game_count,))
    print("Player one won %s out of %s games, quote %s" % (player_one, game_count, player_one / game_count))
    print("Player two won %s out of %s games, quote %s" % (player_two, game_count, player_two / game_count))
    print("Player three won %s out of %s games, quote %s" % (player_three, game_count, player_three / game_count))
    print("No player won (tie) %s out of %s games, quote %s" % (tie, game_count, tie / game_count))

OUTPUT:

Average turns to win: 16.871805
Player one won 295190 out of 1000000 games, quote 0.29519
Player two won 212251 out of 1000000 games, quote 0.212251
Player three won 153289 out of 1000000 games, quote 0.153289
No player won (tie) 339270 out of 1000000 games, quote 0.33927

While no player will win most of the time, with a 34% chance to happen, player one has still the statistical edge with 30% chance to win. Player three is most likely to lose with a 85% lose chance. My claim that the player one will win most likely stands true, atleast in the 3 player only money deck variant. Nonetheless, take this result with a grain of salt, as I simplified the tie rule.

While only owning the first edition of the game and playing the second edition which a friend of mine owns, I’m still new to the strategy side of the game. The money deck is my go-to benchmark for other combo decks. I hope that other editions open the gameplay to new viable strategies. If you know any counters to the Money deck using only the first two editions let me know in the comments.

Update: I played too much Intrigue, so I forgot which cards are actually in the base game. Gardens is available in the first edition and I was completely oblivious to its strength. This blog is then simply a conclusion on how “fast” an only Treasure cards deck is. As stated, I’m still new to the strategy side of the game.

Update 2: After reading the Big Money article on wiki.dominionstrategy.com I am relieved that my conclusions are in line with the article.

About me

Leave a Reply

Your email address will not be published. Required fields are marked *