A program which plays Dominion. Start the GUI (dominion_qt) and play away!
Please contribute! I implemented some cards, but far from all of them. I hope that the code is now in a state where adding more cards is not super complicated; at least for the common things happening, there is very often a similar card already. Implementing a card is usually between 20 and 80 lines of code for the card, plus another 5-10 for integrating it into the language explained below (see src/gamelogic/actors/genericplay.cpp).
See bottom of this file for a super quick introduction.
The syntax of the strategy files looks something like this:
buylists
list if AnyOf(SupplyHasLess(Province, 2), SupplyEmptyPilesGreater(1))
Province, Duchy, Estate
end
list if SupplyHasLess(Province, 4)
Province, Duchy
end
list
Province if Has(IllGottenGains, 5),
Smithy [1],
IllGottenGains,
Silver [2]
end
end
strategies
strategy
playorder
Smithy,
:Default
end
options for IllGottenGains
choose option
end
end
end
Pretty much all sections are required; the minimum file is similar to this:
buylists
list
Estate
end
end
strategies
strategy
playorder
:Default
end
end
end
Here's a quick primer on how this all works. I'm not actually sure how complex the language is for somebody who has not designed it, so it's entirely possible you don't need to read any of this and can just look at the numerous included examples.
First of all, newlines and extra spaces are always ignored.
- The
buylists
section describes what cards are bought in the Buy phase of the turn. It contains multiple lists. - Each list has a condition (which is optional), and contains a sequence of card names, separated by commas ,
- The lists are processed in order, top to bottom, until a list with a fulfilled condition and at least one applicable entry is found.
- The first applicable card, top to bottom, is selected from the list.
- A card is applicable if its condition is fulfilled and you have enough money to buy the card.
- If buys remain, processing then restarts from the top.
- Conditions can be specified on a whole list (the whole list is skipped if the condition is not fulfilled), and on individual entries. The idea is to use different lists for different phases of the game, and conditions on the individual cards in a list for control how many to buy and in what order.
- There is a short form for "if not Has(Foo, N)" which is [N]. So, Smithy [2] will buy a Smithy unless your deck already contains 2 or more.
- A card can occur more than once in a list.
- The strategies section describes in what order cards are played, and which choices are made if the cards offer them.
- It consists of at least one
strategy
section; these sections can be made conditional, like for buylists: usestrategy if ...
. - For each action to perform, Strategies are processed top to bottom, until one with a matching condition is found. That strategy is used for that action. If another action is performed, search starts from the top again.
- Strategies have three sub-entries:
playorder
, which configures in what order cards are played;options for CardName
, which configures how to play CardName; andreactions for EventName
, which configures how to handle certain events. - The
playorder
section must come first and there must be exactly one for each strategy.
A playorder section simply lists which card(s) to play, in order. As soon as one card is played, processing stops and completely re-starts from the top of the strategies section. There are two special entries available, starting with a colon:
:ChainDraw
plays option-less cards giving plus-actions until no more are available, then plays at most one option-less card which draws more cards, and loops until it cannot do anything any more.:Default
first executes:ChainDraw
, then plays the most expensive card from your hand it knows how to play.
options for
is the most powerful tool of this application. It allows to configure how cards behave, and even modify this
behaviour depending on other circumstances. Its body differs depending on the card, but always has one of the following forms:
- Card list:
Copper, Estate, Curse
(used for e.g. Chapel) - Card association list:
Copper -> Estate, Estate -> Silver, Gold -> Province
(used for e.g. Remodel) - Yes/No decision:
choose option
orignore option
(e.g. Ill-Gotten Gains) - Choice:
choose option 1
, options are numbered starting from 1 in the order they appear on the card. - Choice with options:
choose option 1, discard (Curse, Estate, Copper)
; the "options" are any of the first three forms (e.g. Torturer reaction) - Choice list:
choose option 1, choose option 3
(e.g. Pawn) - Annotated fragments:
discard (Curse, Estate), ...
; these are used by some cards to process multiple options, and are optional for others to improve readability. For example, for Chapel,trash (Curse, Copper)
and justCurse, Copper
means the same.
In addition, a condition can be specified after the card name, e.g.
options for Chapel if Has(Silver, 2)
trash (Estate, Copper)
end
options for Chapel
trash (Estate)
end
can be used to trash all Estates and Coppers if your deck contains 2 silvers, but only Estates otherwise.
reactions for
works the same, except it sometimes mentions an event name instead of a card name. Some cards (e.g. Trader)
can have both an options for
and a reactions for
section.
For example,
reactions for TorturerAttack if HasInHand(Trader, 1)
choose option 2
end
reactions for Trader
Curse, Copper
end
options for Trader
Estate, Curse, Copper
end
can be used to choose gaining a curse when attacked by a Torturer while having a Trader in hand, and telling the Trader to react (by gaining a Silver instead) on gaining a Curse or Copper. In addition, the Trader will be played as an action if it can be used on an Estate, Curse or Copper, in that order.
To keep the complexity manageable, conditions have a relatively limited syntax. They start with either "if" or "if not", and then there is exactly one (possibly nested) function call-like expression. Negations, And and Or operations can be implemented using the Not(), AllOf(), and AnyOf() expressions, the latter two taking any number of arguments. Conditions can always be omitted, in which case they evalute to True.
The following expressions are available:
AllOf(expr, expr, ...)
-- true if all of its arguments are true.AnyOf(expr, expr, ...)
-- true if any of its arguments is true.Not(expr)
-- true if its one argument is not true.Has1InHand(Card1, Card2, ...)
-- true if you have 1 of the provided cards in your handHas2InHand(Card1, Card2, ...)
-- same for 2 cardsHas3InHand(Card1, Card2, ...)
-- same for 3 cardsHasInHand(CardName, N)
-- true if there are at least N instances of CardName in your handHasMoney(N)
-- true if at the current point in time, you have at least N virtual money. This does not include cards in your hand!HasMoneyInHand(N)
-- true if the (estimated) combined value of treasure cards in your hand is at least N.Has(CardName, N)
-- true if you have at least N of CardName in your deck in totalHasExact(CardName, N)
-- true if you have exactly N of CardName in your deck in totalSupplyHasLess(CardName, N)
-- true if the supply pile of CardName has less than N cardsSupplyEmptyPilesGreater(N)
-- true if there are more then N empty supply pilesHasInPlay(CardName, N)
-- true if at the current time, you have at least N instances of CardName in play (useful for e.g. Bridge)
Note that conditions not applicable to Reactions (e.g. HasMoney) will silently evalute to False in that context.
The following cards are implemented:
- Baron
- Bridge
- Cartographer
- Cellar
- Chapel
- Conspirator
- Copper
- CouncilRoom
- Courtyard
- Crossroads
- Curse
- Duchy
- Duke
- Embassy
- Estate
- Festival
- FoolsGold
- Gold
- Haggler
- Harem
- Highway
- IllGottenGains
- Ironworks
- Laboratory
- Mandarin
- Margrave
- Market
- Masquerade
- Militia
- Mine
- MiningVillage
- Minion
- Moat
- Moneylender
- Nobles
- Oasis
- Pawn
- Province
- Rebuild
- Remodel
- ShantyTown
- Shepherd
- Silver
- Smithy
- Steward
- Swindler
- ThroneRoom
- Torturer
- Torturer
- Trader
- TradingPost
- Upgrade
- Village
- Witch
- Workshop
The stuff in cards/ should not make any assumptions about how the cards are played, I'd like that to be a completely generic Dominion game logic library.
- Add the card's name to the CardId enum in cardid.h.
- Add the card's name to cardName() in the same file.
- Create a file named myamazingcard.h in gamelogic/cards/.../myamazingcard.h.
- Think about a card that is similar in how it works to your card. For a very simple action card, Smithy; if the card offers choices, Cellar or Workshop; for Attacks, Militia (enemy has a choice) or Witch (enemy has no choice); for a Treasure card, Ill-Gotten Gains.
- Copy that card's code to your file as a starting point. Adjust the name, card type, flags (if your card provides a choice, you must set the Choice flag), and cost.
- Implement playAction() or playTreasure(), or both. Override victoryPoints() if your card gives victory points.
- If your card provides a choice, provide an option struct. If your card is an attack, provide a react option struct. See the appropriate examples on how to do that.
- To make your card supported in the language, in genericplay.cpp, add a case to the switch handling all the cards in genericPlay(). Again, copy code from a similar card and adjust it accordingly.
- If you want, you can do the same in defaultplay.cpp to offer an option for how to play the card by default, when the user does not provide options for it.
- Add your card to the supply by adding it to the factory in supply.cpp, and creating a pile for it in Supply::Supply.
- If you like, add syntax highlighting by adding the card's name to buylist.xml.