{c} = require './cards' if exports?
{c} = require './cards' if exports?
This class defines a rule-based AI of the kind that is popular for evaluating Dominion strategies. It can be subclassed — or simply have its methods overwritten on an instance — to play new strategies.
Every time the player needs to make a meaningful decision, a method called
chooseX
(for some X) will be called on the AI, which can examine the game
state and make a decision accordingly.
In any case that is not a simple yes/no decision, the method will be
given a list of choices. It will first check a method called xPriority
,
which takes in the state and returns an ordered list of choices.
The player will make the first valid choice in that list. Choices are
skipped when they have an “if” clause that fails.
If the priority list doesn’t choose anything, or if there is no priority
function, it will consult the xValue
method instead, which takes in
a specific choice and assigns it a numerical value.
Priority functions are usually easier to define than value functions, but value functions can easily cover every possible case.
The BasicAI has a default decision function for every decision, so every AI that derives from it will have some way to decide what to do in any situation. However, when defining an AI, you will often want to override some of these decision functions.
class BasicAI
name: 'Basic AI'
author: 'rspeer'
Referring to state.current
to find information about one’s own state is
not always safe! Some of these decisions may be made during other players’
turns. In those cases, what we want is this.myPlayer(state)
.
This is passed in as an argument my
to the decision functions, because
it’s convenient and it creates nice idioms such as my.hand
.
myPlayer: (state) ->
for player in state.players
if player.ai is this
return player
throw new Error("#{this} is being asked to make a decision, but isn't playing the game...?")
Make the AI’s preferred choice, first by checking its explicit priority list. If no valid choices are on the list, ask the value function instead.
The priority function returns an ordered list of choices it will want to make when they are available. If ‘null’ is on the priority list, that represents an explicit preference to choose “none of the above” when it’s an option.
The value list assigns a numerical value to every possible choice. ‘null’ automatically has a value of 0. Here you can represent actions you will only take when forced to, by giving them negative values.
If a choice should be made entirely using the value function, make the priority function return an empty list.
choose: (type, state, choices) ->
my = this.myPlayer(state)
Are there no choices? We follow the rule that makes the null choice available in that situation, and choose it.
if choices.length == 0
return null
First, try the priority function. If the priority function reaches the end of its list, it is treated as “none of the above”.
priorityfunc = this[type+'Priority']
if priorityfunc?
Construct an object with the choices as keys, so we can look them up quickly.
choiceSet = {}
for choice in choices
choiceSet[choice] = choice
nullable = null in choices
Get the priority list.
priority = priorityfunc.call(this, state, my)
Now look up all the preferences in that list. The moment we encounter a valid choice, we can return it.
for preference in priority
if preference is null and nullable
return null
if choiceSet[preference]?
return choiceSet[preference]
The priority list doesn’t want any of these choices (perhaps because it doesn’t exist). Now try the value function.
bestChoice = null
bestValue = -Infinity
for choice in choices
value = this.getChoiceValue(type, state, choice, my)
if value > bestValue
bestValue = value
bestChoice = choice
If we got a valid choice, return it.
if bestChoice in choices
return bestChoice
If we get here, the AI probably wants to choose none of the above.
if null in choices
return null
throw new Error("#{this} somehow failed to make a choice")
getChoiceValue: (type, state, choice, my) ->
if choice is null or choice is no
return 0
specificValueFunc = this[type+'Value']
if specificValueFunc?
result = specificValueFunc.call(this, state, choice, my)
if result is undefined
throw new Error("#{this} has an undefined #{type} value for #{choice}")
if result isnt null
return result
defaultValueFunc = choice['ai_'+type+'Value']
if defaultValueFunc?
result = defaultValueFunc.call(choice, state, my)
if result is undefined
throw new Error("#{this} has an undefined #{type} value for #{choice}")
if result isnt null
return result
state.warn("#{this} doesn't know how to make a #{type} decision for #{choice}")
return -1000
Sometimes we need to compare choices in a strictly numeric way. This takes a particular choice for a particular choice type, and gets its numeric value. If the value comes from a priority list, it will be 100 * (distance from end of list).
So, for example, the default choiceToValue of discarding a Colony is 999, while the choiceToValue of discarding an extra terminal is 1.
choiceToValue: (type, state, choice) ->
return 0 if choice is null or choice is no
my = this.myPlayer(state)
priorityfunc = this[type+'Priority']
if priorityfunc?
priority = priorityfunc.bind(this)(state, my)
else
priority = []
index = priority.indexOf(stringify(choice))
if index != -1
return (priority.length - index) * 100
else
return this.getChoiceValue(type, state, choice, my)
Originally implemented in the Rebuild.coffee
strategy, this method gets
the difference in score if the game were to end now.
getScoreDifference: (state, my) ->
for status in state.getFinalStatus()
[name, score, turns] = status
if name == my.ai.toString()
myScore = score
else
opponentScore = score
return myScore - opponentScore
More utilities from the Rebuild strategy.
countNotInHand: (my, card) ->
return my.countInDeck(card) - my.countInHand(card)
countInDraw: (my, card) ->
return my.countInDeck(card) - my.countInHand(card) - my.countInDiscard(card)
To avoid having to rewrite all the code at once, we support these functions
that pass chooseAction
onto choose('action')
, and so on.
chooseAction: (state, choices) -> this.choose('play', state, choices)
chooseTreasure: (state, choices) -> this.choose('play', state, choices)
chooseGain: (state, choices) -> this.choose('gain', state, choices)
chooseDiscard: (state, choices) -> this.choose('discard', state, choices)
chooseTrash: (state, choices) -> this.choose('trash', state, choices)
The default buying strategy is a form of Big Money that has, by now, been beaten by the newer one in BigMoney.coffee.
gainPriority: (state, my) -> [
"Colony" if my.countInDeck("Platinum") > 0
"Province" if state.countInSupply("Colony") <= 6
"Duchy" if 0 < state.gainsToEndGame() <= 5
"Estate" if 0 < state.gainsToEndGame() <= 2
"Platinum"
"Gold"
"Silver"
"Copper" if state.gainsToEndGame() <= 3
]
gainValue covers cases where a strategy has to gain a card that isn’t in its priority list. The default is to favor more expensive cards, particularly action and treasure cards.
It is important for all these values to be negative, to avoid giving defined strategies cards they don’t actually want.
gainValue: (state, card, my) ->
card.cost + 2*card.costPotion + card.isTreasure + card.isAction - 20
This used to be the default action-playing priority. Now the value of playing a card is defined on the “ai_playValue” function of each card.
old_actionPriority: (state, my, skipMultipliers = false) ->
wantsToTrash = this.wantsToTrash(state)
countInHandCopper = my.countInHand("Copper")
currentAction = my.getCurrentAction()
multiplier = 1
if currentAction?.isMultiplier
multiplier = currentAction.multiplier
wantsToPlayMultiplier = false
okayToPlayMultiplier = false
unless skipMultipliers
mults = (card for card in my.hand when card.isMultiplier)
if mults.length > 0
We’ve got a multiplier in hand. Figure out if we want to play it.
mult = mults[0]
choices = my.hand.slice(0)
choices.remove(mult)
choices.push(null)
Determine if it’s better than nothing.
choice1 = this.choose('multipliedAction', state, choices)
if choice1 isnt null
okayToPlayMultiplier = true
Now add the “wait” option and see if we want to multiply an action right now.
if choices.length > 1
choices.push("wait")
choice = this.choose('multipliedAction', state, choices)
if choice != "wait"
wantsToPlayMultiplier = true
Priority 1: cards that succeed if we play them now, and might not if we play them later. (950-999)
["Menagerie" if my.menagerieDraws() == 3
"Shanty Town" if my.shantyTownDraws(true) == 2
"Tournament" if my.countInHand("Province") > 0
"Library" if my.hand.length <= 3 and my.actions > 1
2: Multipliers that do something sufficiently cool. (900-949)
"Throne Room" if wantsToPlayMultiplier
"King's Court" if wantsToPlayMultiplier
3: cards that stack the deck. (850-899)
"Lookout" if state.gainsToEndGame() >= 5 or state.cardInfo.Curse in my.draw
"Cartographer"
"Bag of Gold"
"Apothecary"
"Scout"
"Scrying Pool"
"Spy"
4: cards that give +2 actions. (800-849)
"Trusty Steed"
"Festival"
"University"
"Farming Village"
"Bazaar"
"Worker's Village"
"City"
"Walled Village"
"Fishing Village"
"Village"
"Border Village"
"Mining Village"
5: cards that give +1 action and are almost always good. (700-800)
"Grand Market"
"Hunting Party"
"Alchemist"
"Laboratory"
"Caravan"
"Market"
"Peddler"
"Treasury"
"Conspirator" if my.inPlay.length >= 2 or multiplier > 1
"Familiar"
"Highway"
"Scheme"
"Wishing Well"
"Golem" # seems to be reasonable to expect +1 action from Golem
"Great Hall" if state.cardInfo.Crossroads not in my.hand
"Spice Merchant" if state.cardInfo.Copper in my.hand
"Stables" if this.choose('stablesDiscard', state, my.hand.concat([null]))
"Apprentice"
"Pearl Diver"
"Hamlet"
"Lighthouse"
"Haven"
"Minion"
6: terminal card-drawers, if we have actions to spare. (600-699)
"Library" if my.actions > 1 and my.hand.length <= 4 # 695
"Torturer" if my.actions > 1
"Margrave" if my.actions > 1
"Rabble" if my.actions > 1
"Witch" if my.actions > 1
"Ghost Ship" if my.actions > 1
"Smithy" if my.actions > 1
"Embassy" if my.actions > 1
"Watchtower" if my.actions > 1 and my.hand.length <= 4
"Library" if my.actions > 1 and my.hand.length <= 5 # 620
"Council Room" if my.actions > 1
"Courtyard" if my.actions > 1 and (my.discard.length + my.draw.length) <= 3
"Oracle" if my.actions > 1
7: Let’s insert here an overly simplistic idea of how to play Crossroads. Or if we don’t have a Crossroads, play a Great Hall that we might otherwise have played in priority level 5. (500-599)
"Crossroads" unless my.countInPlay(state.cardInfo.Crossroads) > 0
"Great Hall"
8: card-cycling that might improve the hand. (400-499)
"Upgrade" if wantsToTrash >= multiplier
"Oasis"
"Pawn"
"Warehouse"
"Cellar"
"Library" if my.actions > 1 and my.hand.length <= 6
"Spice Merchant" if this.choose('spiceMerchantTrash', state, my.hand.concat([null]))
9: non-terminal cards that don’t succeed but at least give us something. (300-399)
"King's Court"
"Throne Room" if okayToPlayMultiplier
"Tournament"
"Menagerie"
"Shanty Town" if my.actions < 2
10: terminals. Of course, Nobles might be a non-terminal if we decide we need the actions more than the cards. (100-299)
"Crossroads"
"Nobles"
"Treasure Map" if my.countInHand("Treasure Map") >= 2
"Followers"
"Mountebank" # 290
"Witch"
"Sea Hag"
"Torturer"
"Young Witch"
"Tribute" # after Cursers but before other terminals, there is probably a better spot for it
"Margrave" # 280
"Goons"
"Wharf"
Tactician needs a play condition, but I don’t know what it would be.
"Tactician"
"Masquerade" # 270
"Vault"
"Ghost Ship"
"Princess"
"Explorer" if my.countInHand("Province") >= 1
"Library" if my.hand.length <= 3 # 260
"Jester"
"Militia"
"Cutpurse" # 250
"Bridge"
"Bishop"
"Horse Traders" # 240
"Jack of All Trades"
"Steward"
"Moneylender" if countInHandCopper >= 1 # 230
"Expand"
"Remodel"
"Salvager" # 220
"Mine"
"Coppersmith" if countInHandCopper >= 3
"Library" if my.hand.length <= 4 # 210
"Rabble"
"Envoy"
"Smithy" # 200
"Embassy"
"Watchtower" if my.hand.length <= 3
"Council Room"
"Library" if my.hand.length <= 5
"Watchtower" if my.hand.length <= 4 # 190
"Courtyard" if (my.discard.length + my.draw.length) > 0
"Merchant Ship"
"Baron" if my.countInHand("Estate") >= 1
"Monument"
"Oracle" # 180
"Remake" if wantsToTrash >= multiplier * 2 # has a low priority so it'll mostly be played early in the game
"Adventurer"
"Harvest"
"Haggler" # probably needs to make sure the gained card will be wanted; 170
"Mandarin"
"Explorer"
"Woodcutter"
"Nomad Camp"
"Chancellor" # 160
"Counting House"
"Coppersmith" if countInHandCopper >= 2
"Outpost" if state.extraturn == false
"Ambassador" if wantsToTrash # 150
"Trading Post" if wantsToTrash + my.countInHand("Silver") >= 2 * multiplier
"Chapel" if wantsToTrash
"Trader" if wantsToTrash >= multiplier
"Trade Route" if wantsToTrash >= multiplier
"Mint" if my.ai.choose('mint', state, my.hand) # 140
"Secret Chamber"
"Pirate Ship"
"Noble Brigand"
"Thief"
"Island" # could be moved
"Fortune Teller" # 130
"Bureaucrat"
"Navigator"
"Conspirator" if my.actions < 2
"Herbalist"
"Moat" # 120
"Library" if my.hand.length <= 6
"Ironworks" # should have higher priority if condition can see it will gain an Action card
"Workshop"
"Smugglers" if state.smugglerChoices().length > 1 # 110
"Feast"
"Transmute" if wantsToTrash >= multiplier
"Coppersmith"
"Saboteur"
"Poor House"
"Duchess"
"Library" if my.hand.length <= 7
"Thief" # 100
11: cards that have become useless. Maybe they’ll decrease the cost of Peddler, trigger Conspirator, or something. (20-99)
"Treasure Map" if my.countInDeck("Gold") >= 4 and state.current.countInDeck("Treasure Map") == 1
"Spice Merchant"
"Shanty Town"
"Stables" # 50
"Chapel"
"Library"
12: Conspirator when +actions remain. (10)
"Conspirator"
“Baron”
At this point, we take no action if that choice is available.
null
Nope, something is forcing us to take an action.
Last priority: cards that are actively harmful to play at this point, in order of increasing badness.
"Baron"
"Mint"
"Watchtower"
"Outpost"
"Ambassador" # -20
"Trader"
"Transmute"
"Trade Route"
"Upgrade" # -30
"Remake"
"Trading Post"
"Treasure Map" # -40
"Throne Room"
]
multipliedActionPriority
is similar to actionPriority
, but is used when
we have played a Throne Room or King’s Court.
This list emphasizes cards that are really good when multiplied, especially terminals when there are +actions left. At the end, it falls back on the usual actionPriority list.
old_multipliedActionPriority: (state, my) ->
[
"King's Court" # 2000
"Throne Room" # 1900
"Followers" if my.actions > 0
"Grand Market"
"Mountebank" if my.actions > 0
"Witch" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Sea Hag" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Torturer" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Young Witch" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Crossroads" if my.actions > 0 or my.countInPlay(state.cardInfo.Crossroads) == 0 # 1800
"Scheme" if my.countInDeck("King's Court") >= 2
Scrying Pool was here once, but I think you’d rather use it to draw actions for your KC
"Wharf" if my.actions > 0
"Bridge" if my.actions > 0
"Minion" # 1700
"Ghost Ship" if my.actions > 0
"Jester" if my.actions > 0
"Horse Traders" if my.actions > 0
"Mandarin" if my.actions > 0
"Rabble" if my.actions > 0 # 1600
"Council Room" if my.actions > 0
"Margrave" if my.actions > 0
"Smithy" if my.actions > 0
"Embassy" if my.actions > 0
"Merchant Ship" if my.actions > 0 # 1500
"Pirate Ship" if my.actions > 0
"Saboteur" if my.actions > 0
"Noble Brigand" if my.actions > 0
"Thief" if my.actions > 0
"Monument" if my.actions > 0 # 1400
"Feast" if my.actions > 0
"Conspirator"
"Nobles"
"Tribute"
"Steward" if my.actions > 0 # 1300
"Goons" if my.actions > 0
"Mine" if my.actions > 0
"Masquerade" if my.actions > 0
"Vault" if my.actions > 0
"Oracle" if my.actions > 0 # 1200
"Cutpurse" if my.actions > 0
"Coppersmith" if my.actions > 0 and my.countInHand("Copper") >= 2
"Ambassador" if my.actions > 0 and this.wantsToTrash(state) # 1100
"wait"
We could add here some more cards that would be nice to play with a multiplier. Nicer than Lookout, let’s say, which appears pretty high on the regular action priority list.
But at this point, just fall back on that priority list.
].concat(this.old_actionPriority(state, my, skipMultipliers=true))
treasurePriority
determines what order to play treasures in.
Most of the order has no effect on gameplay. The
important part is that Bank and Horn of Plenty are last.
treasurePriority: (state, my) -> [
"Platinum"
"Diadem"
"Philosopher's Stone"
"Gold"
"Cache"
"Hoard"
"Royal Seal"
"Harem"
"Silver"
"Fool's Gold"
"Quarry"
"Talisman"
"Copper"
"Masterpiece"
"Potion" # 100 from here up
"Loan" # 90
"Venture" # 80
"Ill-Gotten Gains"
"Bank"
"Horn of Plenty" if my.numUniqueCardsInPlay() >= 2
"Spoils" if this.wantsToPlaySpoils(state)
null
]
The default discardPriority
is tuned for Big Money where the decisions
are obvious. But many strategies would probably prefer a different
priority list, especially one that knows about action cards.
It doesn’t understand discarding cards to make Shanty Town or Menagerie work, for example.
discardPriority: (state, my) -> [
"Tunnel"
"Vineyard"
"Colony"
"Duke"
"Duchy"
"Fairgrounds"
"Gardens"
"Province" # Provinces are occasionally useful in hand
"Curse"
"Estate"
]
discardValue: (state, card, my) ->
If we can discard excess actions, do so. Otherwise, discard the cheapest cards. Victory cards would already have been discarded by discardPriority, but if Tunnel fell through somehow we discard it here.
First, check to see if it’s our turn. That changes whether we want to discard actions.
myTurn = (state.current == my)
if card.name == 'Tunnel'
25
else if card.isAction and myTurn and \
((card.actions == 0 and my.actionBalance() <= 0) or (my.actions == 0))
20 - card.cost
else
0 - card.cost
trashPriority: (state, my) -> [
"Curse"
"Estate" if state.gainsToEndGame() > 4
"Copper" if my.getTotalMoney() > 4
"Potion" if my.turnsTaken >= 10
"Estate" if state.gainsToEndGame() > 2
]
If we have to trash a card we don’t want to, assign a value to each card. By default, we want to trash the card with the lowest (cost + VP).
trashValue: (state, card, my) ->
0 - card.vp - card.cost
developPriority: (state, my) => trashPriority(state, my)
developValue: (state, card, my) => this.trashValue(state, card, my)
Some cards give you a choice to discard an opponent’s deck. These are
evaluated with discardFromOpponentDeckValue
.
discardFromOpponentDeckValue: (state, card, my) ->
if card.name == 'Tunnel'
return -2000
else if not (card.isAction) and not (card.isTreasure)
return -10
else
return card.coins + card.cost + 2*card.isAttack
discardHandValue
decides whether to discard an entire hand of cards.
discardHandValue: (state, hand, my, nCards = 5) ->
return 0 if hand is null
deck = my.discard.concat(my.draw)
total = 0
for i in [0...5]
shuffle(deck)
randomHand = deck[0...nCards]
If a random hand from this deck is better, discard this hand.
total += my.ai.compareByDiscarding(state, randomHand, hand)
return total
Prefer to gain action and treasure cards on the deck, assuming we want
them at all. Give other cards a value of -1 so that null
is a better
choice.
gainOnDeckValue: (state, card, my) ->
if (card.isAction or card.isTreasure)
this.getChoiceValue('gain', state, card, my)
else
-1
Changed Priorities for putting cards back on deck. Only works well for putting back 1 card, and for 1 buy.
putOnDeckPriority: (state, my) ->
Make a priority order of:
1) Actions
actions = (card for card in my.hand when card.isAction)
getChoiceValue = this.getChoiceValue
byPlayValue = (x, y) ->
getChoiceValue('play', state, y, my) - getChoiceValue('play', state, x, my)
actions.sort(byPlayValue)
putBack = actions[my.countPlayableTerminals(state) ...]
2) Put back as much money as you can
if putBack.length == 0
Get a list of all distinct treasures in hand, in order.
treasures = []
for card in my.hand
if (card.isTreasure) and (not (card in treasures))
treasures.push card
treasures.sort( (x, y) -> y.coins - x.coins )
Get the margin of how much money we’re willing to discard.
margin = my.ai.coinLossMargin(state)
Find the treasure cards worth less than that.
for card in treasures
if my.ai.coinsDueToCard(state, card) <= margin
putBack.push(card)
Don’t put back last Potion if Alchemists are in play
if my.countInPlay(state.cardInfo["Alchemist"])>0
if "Potion" in putBack
putBack.remove(state.cardInfo["Potion"])
3) Put back the worst card (take priority for discard)
if putBack.length == 0
putBack = [my.ai.choose('discard', state, my.hand)]
putBack
putOnDeckValue: (state, card, my) =>
this.discardValue(state, card, my)
How much does the AI want to discard its deck right now (for Chancellor)? Here, we decide to reshuffle (returning a reshuffleValue over 0) when most of the non-Action, non-Treasure cards are in the draw pile, or when there are no such cards in the deck.
reshuffleValue: (state, choice, my) ->
junkToDraw = 0
totalJunk = 0
for card in my.draw
if not (card.isAction or card.isTreasure)
junkToDraw++
for card in my.getDeck()
if not (card.isAction or card.isTreasure)
totalJunk++
return 1 if (totalJunk == 0)
proportion = junkToDraw/totalJunk
return (proportion - 0.5)
Choose opponent treasure to trash; go by the card’s base cost. Diadems are comparable to the cost-5 treasures.
trashOppTreasureValue: (state, card, my) =>
if card is 'Diadem'
return 5
return card.cost
ambassadorPriority
chooses a card to Ambassador and how many of it to
return.
These choices may look odd: remember that choices are evaluated as strings. So if we return lists, they won’t match any of the choices. We need to return their joined string versions.
This is a moderately acceptable way to deal with the fact that, in JavaScript, lists are never “equal” to other lists anyway.
ambassadorPriority: (state, my) ->
[
"[Curse, 2]"
"[Curse, 1]"
"[Curse, 0]"
Handle a silly case:
"[Ambassador, 2]"
"[Estate, 2]"
"[Estate, 1]"
Make sure we have at least $5 in the deck, including if we buy a Silver.
"[Copper, 2]" if my.getTreasureInHand() < 3 and my.getTotalMoney() >= 5
"[Copper, 2]" if my.getTreasureInHand() >= 5
"[Copper, 2]" if my.getTreasureInHand() == 3 and my.getTotalMoney() >= 7
"[Copper, 1]" if my.getTreasureInHand() < 3 and my.getTotalMoney() >= 4
"[Copper, 1]" if my.getTreasureInHand() >= 4
"[Estate, 0]"
"[Copper, 0]"
"[Potion, 2]"
"[Potion, 1]"
null
].concat ("[#{card}, 1]" for card in my.ai.trashPriority(state, my) when card?)\
.concat ("[#{card}, 0]" for card in my.hand)
apprenticeTrashPriority: (state, my) ->
"Border Village"
"Mandarin"
"Ill-Gotten Gains" if this.coinLossMargin(state) > 0
"Feodum"
"Estate"
"Curse"
"Apprentice"
apprenticeTrashValue: (state, card, my) ->
vp = card.getVP(my)
[coins, potions] = card.getCost(state)
drawn = Math.min(my.draw.length + my.discard.length, coins+2*potions)
return this.choiceToValue('trash', state, card) + 2*drawn - vp
The question here is: do you want to discard an Estate using a Baron? And the answer is yes.
baronDiscardPriority: (state, my) -> [yes]
bishopTrashPriority
lists cards that are especially good to trash.
bishopTrashPriority: (state, my) -> [
"Farmland"
"Duchy" if this.goingGreen(state) < 3
"Border Village"
"Mandarin"
"Feodum"
"Bishop"
"Ill-Gotten Gains" if this.coinLossMargin(state) > 0
"Curse"
]
bishopTrashValue: (state, card, my) ->
[coins, potions] = card.getCost(state)
value = Math.floor(coins/2) - card.getVP(my)
if we’re going for victory points, that’s all we care about.
if this.goingGreen(state) >= 3
return value
otherwise, focus on what we want to trash
else
if card in this.trashPriority(state, my)
value += 1
if card.isAction and ((card.actions == 0 and my.actionBalance() <= 0) or (my.actions == 0))
value += 1
if card.isTreasure and card.coins > (this.coinLossMargin(state) + 1)
value -= 10
return value
envoyValue: (state, card, my) ->
Choose a card to discard from your opponent’s hand when it’s their turn.
opp = state.current
if card.name == 'Tunnel'
return -25
else if not (card.isAction) and not (card.isTreasure)
return -10
else if opp.actions == 0 and card.isAction
return -5
else if opp.actions >= 2
return card.cards + card.coins + card.cost + 2*card.isAttack
else
return card.coins + card.cost + 2*card.isAttack
foolsGoldTrashPriority
will trash a Fool’s Gold for a real Gold if
it’s nearing the endgame (5 gains or less), there is one FG in hand,
and losing it will not change its buy.
foolsGoldTrashPriority: (state, my) ->
if my.countInHand(state.cardInfo["Fool's Gold"]) == 1 and my.ai.coinLossMargin(state) >= 1
[yes]
else
[no]
Do you want to gain a copper from Ill-Gotten Gains? Yes, we want if that improves our buy
gainCopperPriority: (state, my) ->
if my.ai.coinGainMargin(state) <= my.countInHand("Ill-Gotten Gains")+1
[yes]
else
[no]
The herbalist
decision puts a treasure card back on the deck. It sounds
the same as putOnDeck
, but it’s for a different
situation — the card is coming from in play, not from your hand. So
actually we use the mintValue
by default.
herbalistValue: (state, card, my) =>
this.mintValue(state, card, my)
huntingGroundsGainPriority: (state, my) -> [
"Duchy"
"Estates"
]
islandPriority chooses which card to set aside with Island. At present this list is incomplete, but covers just about everything that we would want to set aside with an Island.
islandPriority: (state, my) ->
[
"Colony"
"Province"
"Fairgrounds"
"Duchy"
"Duke"
"Gardens"
"Vineyard"
"Estate"
"Copper"
"Curse"
"Island"
"Tunnel"
]
islandValue: (state, card, my) -> this.discardValue(state, card, my)
librarySetAsideValue: (state, card, my) -> [
if my.actions == 0 and card.isAction
1
else
-1
]
miningVillageTrashValue: (state, choice, my) ->
if this.goingGreen(state) and this.coinGainMargin(state) <= 2
1
else
-1
minionDiscardValue: (state, choice, my) ->
if choice == yes
Find out how valuable it would be to discard these cards and draw 4.
value = this.discardHandValue(state, my.hand, my, 4)
opponent = state.players[state.players.length - 1]
If the attack would decrease an opponent’s hand size, it’s more valuable.
if opponent.hand.length > 4
value += 2
return value
else
return 0
Mint anything but Copper and Diadem. Otherwise, go mostly by the card’s base cost. There is only 1 Diadem, never any available to gain, so never Mint it.
mintValue: (state, card, my) ->
return card.cost - 1
Choose whether we want these cards or two random cards.
oracleDiscardValue: (state, cards, my) ->
deck = my.discard.concat(my.draw)
shuffle(deck)
randomCards = deck[0...cards.length]
return my.ai.compareByDiscarding(state, my.hand.concat(randomCards), my.hand.concat(cards))
Choose to attack or use available coins when playing Pirate Ship. Current strategy is basically Geronimoo’s attackUntil5Coins play strategy, but only with Provinces—or technically, cards costing 8 or more.
pirateShipPriority: (state, my) -> [
'coins' if state.current.mats.pirateShip >= 5 and state.current.getAvailableMoney()+state.current.mats.pirateShip >= 8
'attack'
]
might want to think about something more clever, but for first, just discard Coppers
plazaDiscardPriority: (state, my) -> [
"Copper"
null
]
rogueGainValue: (state, card, my) ->
[coins, potions] = card.getCost(state)
return coins
rogueTrashValue: (state, card, my) ->
[coins, potions] = card.getCost(state)
return coins
salvagerTrashPriority: (state, card, my) -> [
"Border Village"
"Mandarin"
"Ill-Gotten Gains" if this.coinLossMargin(state) > 0
"Feodum"
"Salvager"
]
To calculate the salvagerTrashValue, we simulate trashing each card, determine the best card we would buy as a result, and evaluate it as if we were upgrading the trashed card into the bought one.
salvagerTrashValue: (state, card, my) ->
[hypothesis, hypothetically_my] = state.hypothetical(this)
hypothetically_my.hand.remove(card)
[coins, potions] = card.getCost(hypothesis)
hypothetically_my.coins += coins
hypothetically_my.buys += 1
buyState = this.fastForwardToBuy(hypothesis, hypothetically_my)
gained = buyState.getSingleBuyDecision()
return this.upgradeValue(state, [card, gained], my)
Scheme uses the same priority function as multiplied actions. Good actions to multiply this turn are typically good actions to have around next turn.
schemeValue: (state, card, my) ->
Project a little of what the state will look like at the beginning of the next turn. This keeps multipliedActionPriority from evaluating a card as though it will be used in the current (finished) turn.
myNext = {}
myNext[key] = value for key, value of my
myNext.actions = 1
myNext.buys = 1
myNext.coins = 0
return this.getChoiceValue('multiplied', state, card, myNext)
scryingPoolDiscardValue
is like discardValue
, except it strongly
prefers to discard non-actions.
scryingPoolDiscardValue: (state, card, my) ->
if not card.isAction
2000
else
this.choiceToValue('discard', state, card)
spiceMerchantTrashPriority: (state, my) -> [
"Copper",
"Potion",
"Loan",
"Ill-Gotten Gains",
"Fool's Gold" if my.countInDeck("Fool's Gold") == 1,
"Silver" if my.getTotalMoney() >= 8,
null,
"Silver",
"Venture",
"Cache",
"Gold",
"Harem",
"Platinum"
]
Which treasure, if any, should be discarded to feed Stables? Defaults to a list of generally crappy treasures. Doesn’t include $1 Fool’s Gold because you presumably have another one you’re trying to draw.
stablesDiscardPriority: (state, my) -> [
"Copper"
"Potion" if my.countInPlay(state.cardInfo["Alchemist"]) == 0
"Ill-Gotten Gains"
"Silver"
"Horn of Plenty"
null
"Potion"
"Venture"
"Cache"
"Gold"
"Platinum"
]
Do you want to discard a Province to win a Tournament? The answer is very yes.
tournamentDiscardPriority: (state, my) -> [yes]
transmuteValue: (state, card, my) ->
if card.isAction and this.goingGreen(state)
return 10
else if card.isAction and card.isVictory and card.cost <= 4
return 1000
else
return this.choiceToValue('trash', state, card)
wishValue
prefers to wish for the card its draw pile contains
the most of.
The fact that this doesn’t make a hypothetical copy is a shortcut. We are technically “peeking” at the deck, but we don’t use any information except the count of each card, which would be the same in any hypothetical version.
wishValue: (state, card, my) ->
pile = my.draw
if pile.length == 0
pile = my.discard
return countInList(pile, card)
Choose to discard or to gain a curse when attacked by Torturer.
torturerPriority: (state, my) -> [
'curse' if state.countInSupply("Curse") == 0
'discard' if my.ai.wantsToDiscard(state) >= 2
'discard' if my.hand.length <= 1
'curse' if my.trashingInHand() > 0
'curse' if my.hand.length <= 3
'discard'
'curse'
]
Taking into account gain priorities, gain values, trash priorities, and trash values, how much do we like having this card in our deck overall?
cardInDeckValue: (state, card, my) ->
endgamePower = 1
Are we in the late game? If so, we care much more about getting cards at the top of our priority order.
if state.gainsToEndGame() <= 5
endgamePower = 3
return -(this.choiceToValue('trash', state, card)) + \
Math.pow(this.choiceToValue('gain', state, card), endgamePower)
upgradeValue measures the benefit of choices on Remodel, Upgrade, and so on, where you exchange one card for a better one.
So here’s a really basic thing that might work.
upgradeValue: (state, choice, my) ->
[oldCard, newCard] = choice
return my.ai.cardInDeckValue(state, newCard, my) - \
my.ai.cardInDeckValue(state, oldCard, my)
developValue measures the benefit of choices Develop, where you exchange one card for two.
So here’s a really basic thing that might work.
developValue: (state, choice, my) ->
[oldCard, [newCard1, newCard2]] = choice
return my.ai.cardInDeckValue(state, newCard1, my) + \
my.ai.cardInDeckValue(state, newCard2, my) - \
my.ai.cardInDeckValue(state, oldCard, my)
chooseOrderOnDeck
handles situations where multiple cards are returned
to the deck, such as Scout and Apothecary.
This decision doesn’t fit into the xPriority / xValue framework, as there are a number of mostly indistinguishable choices. Instead of listing all the permutations of cards as choices, we just list the cards to arrange.
The default decision is to put the cards with the lowest discard value on top.
chooseOrderOnDeck: (state, cards, my) ->
sorter = (card1, card2) ->
my.ai.choiceToValue('discard', state, card1)\
- my.ai.choiceToValue('discard', state, card2)
choice = cards.slice(0)
return choice.sort(sorter)
How much do we want to overpay for Masterpiece? If we care to buy it probably as much as possible
chooseOverpayMasterpiece: (state, maxAmount) ->
return maxAmount
How many Coin Tokens do we want to spend? Try to buy the ‘best’ card you can afford, and spend as less as possible for this
spendCoinTokens: (state, my) ->
cardsBoughtOld = []
ct = my.coinTokens
loop
[hypState, hypMy] = state.hypothetical(this)
hypMy.coins += ct
hypMy.coinTokensSpendThisTurn = ct
cardsBought = []
while hypMy.buys > 0
cardBought = hypState.getSingleBuyDecision()
if cardBought?
[coinCost, potionCost] = cardBought.getCost(hypState)
hypMy.coins -= coinCost
hypMy.potions -= potionCost
cardsBought.push cardBought
hypMy.buys -= 1
if ((ct < my.coinTokens) and not (arrayEqual(cardsBought, cardsBoughtOld)))
ct += 1
break
if ct == 0
break
ct -= 1
cardsBoughtOld = cardsBought
return ct
When presented with a card with simple but variable benefits, such as Nobles, this is the default way for an AI to decide which benefit it wants. This function should actually handle a number of common situations.
benefitValue: (state, choice, my) ->
buyValue = 1
cardValue = 2
coinValue = 3
trashValue = 4 # if there are cards we want to trash
actionValue = 10 # if we need more actions
actionBalance = my.actionBalance()
usableActions = Math.max(0, -actionBalance)
if actionBalance >= 1
cardValue += actionBalance
if my.ai.wantsToTrash(state) < (choice.trash ? 0)
trashValue = -4
value = cardValue * (choice.cards ? 0)
value += coinValue * (choice.coins ? 0)
value += buyValue * (choice.buys ? 0)
value += trashValue * (choice.trash ? 0)
value += actionValue * Math.min((choice.actions ? 0), usableActions)
value
wantsToTrash
returns the number of cards in hand that we would trash
for no benefit.
wantsToTrash: (state) ->
my = this.myPlayer(state)
trashableCards = 0
for card in my.hand
if this.chooseTrash(state, [card, null]) is card
trashableCards += 1
return trashableCards
wantsToPlayRats
is like wantsToTrash
except that the answer is no.
Come on, it’s a first-order approximation of good strategy. If you’ve got a better idea, put it in a strategy file.
wantsToPlayRats: (state) -> no
wantsToDiscard
returns the number of cards in hand that we would
freely discard.
wantsToDiscard: (state) ->
my = this.myPlayer(state)
discardableCards = 0
for card in my.hand
if this.chooseDiscard(state, [card, null]) is card
discardableCards += 1
return discardableCards
multiplierChoices: (state) ->
my = this.myPlayer(state)
mults = (card for card in my.hand when card.isMultiplier)
if mults.length > 0
mult = mults[0]
choices = (card for card in my.hand when card.isAction)
choices.remove(mult)
choices.push(null)
return choices
else
return []
okayToPlayMultiplier: (state) ->
choices = this.multiplierChoices(state)
if this.choose('multiplied', state, choices)?
return true
else
return false
wantsToPlayMultiplier: (state) ->
my = this.myPlayer(state)
choices = this.multiplierChoices(state)
if choices.length > 1
choice = this.choose('multiplied', state, choices)
multipliedValue = this.getChoiceValue('multiplied', state, choice, my)
if choice? and choice.isMultiplier
prevent infinite loops
unmultipliedValue = 0
else
unmultipliedValue = this.getChoiceValue('play', state, choice, my)
return (multipliedValue > unmultipliedValue)
return false
play Spoils if it changes your buys this turn. Or if in hypothetical state to solve recursion
wantsToPlaySpoils: (state) ->
if state.depth > 0
return true
else
cardsGainedWithout = this.pessimisticCardsGained(state)
[hypState, hypMy] = state.hypothetical(this)
hypState.current.hand.remove(c["Spoils"])
cardsGainedWith = this.pessimisticCardsGained(hypState)
if arrayEqual(cardsGainedWithout, cardsGainedWith)
return false
else
return true
wantsToRebuild and rebuildPriority: taken from the Rebuild strategy
wantsToRebuild: (state, my) ->
if my.countInHand("Rebuild") >= state.countInSupply("Province") \
and my.ai.getScoreDifference(state, my) > 0
answer = 1
else if state.countInSupply("Province") == 1 \
and my.ai.getScoreDifference(state, my) < -4
answer = 0
else if state.countInSupply("Duchy") == 0 \
and my.ai.countNotInHand(my, "Duchy") == 0\
and my.ai.getScoreDifference(state, my) < 0
answer = 0
else if my.getTreasureInHand() > 7 and state.countInSupply("Province") == 1
answer = 0
else
answer = state.countInSupply("Province") > 0
return answer
rebuildPriority: (state, my) -> [
"Province"
"Duchy"
"Estate"
]
nameVPPriority: (state, my) -> [
"Colony" if my.countInDeck("Colony") > 0
"Province"
]
Assume we always want to play Journeyman
wantsToJM: (state, my) ->
true
wantsToDiscardBeggar: (state) ->
return true
goingGreen
: determine when we’re playing for victory points. By default,
it’s if there are any Colonies, Provinces, or Duchies in the deck.
The bigger the number, the greener the deck, but a true (greater than 0) value is a good indication in itself that we want victory cards.
goingGreen: (state) ->
my = this.myPlayer(state)
bigGreen = my.countInDeck("Colony") + my.countInDeck("Province") + my.countInDeck("Duchy")
return bigGreen
pessimisticMoneyInHand
establishes a minimum on how much money the
player will be able to spend in this game state. It assumes the player
will draw no money from the deck.
pessimisticMoneyInHand: (state) ->
Don’t recurse more than once. If we’re already in a hypothetical situation, use the stupid version instead.
if state.depth > 0
return this.myPlayer(state).getAvailableMoney()
buyPhase = this.pessimisticBuyPhase(state)
return buyPhase.current.coins
Look ahead to the buy phase, assuming we draw no money from the deck.
TODO: when we can handle known cards on top of the deck, take them into account.
pessimisticBuyPhase: (state) ->
if state.depth > 0
A last-ditch effort to avoid recursion, by simply fast-forwarding to the next phase.
if state.phase == 'action'
state.phase = 'treasure'
else if state.phase == 'treasure'
state.phase = 'buy'
[hypothesis, hypothetically_my] = state.hypothetical(this)
return this.fastForwardToBuy(hypothesis, hypothetically_my)
fastForwardToBuy: (state, my) ->
if state.depth == 0
throw new Error("Can only fast-forward in a hypothetical state")
We need to save draw and discard before emptying and restore them before buyPhase, to be able to choose the right buys in actionPriority(state)
oldDraws = my.draw.slice(0)
oldDiscard = my.discard.slice(0)
my.draw = []
my.discard = []
while state.phase != 'buy'
state.doPlay()
my.draw = oldDraws
my.discard = oldDiscard
return state
pessimisticCardsGained: (state) ->
newState = this.pessimisticBuyPhase(state)
newState.doPlay()
return newState.current.gainedThisTurn
coinLossMargin determines how much treasure the player can lose “for free” (because it won’t change their buy decision). Intended to be more efficient than calling pessimisticCardsGained on a number of different states.
TODO: do we need an equivalent for potions?
coinLossMargin: (state) ->
newState = this.pessimisticBuyPhase(state)
coins = newState.current.coins
cardToBuy = newState.getSingleBuyDecision()
return 0 if cardToBuy is null
[coinsCost, potionsCost] = cardToBuy.getCost(newState)
return coins - coinsCost
coinGainMargin determines how much treasure the player wants to gain, in order to get a better card. Tries up to +$8, then returns Infinity if nothing changes.
coinGainMargin: (state) ->
newState = this.pessimisticBuyPhase(state)
coins = newState.current.coins
baseCard = newState.getSingleBuyDecision()
for increment in [1, 2, 3, 4, 5, 6, 7, 8]
newState.current.coins = coins+increment
cardToBuy = newState.getSingleBuyDecision()
if cardToBuy != baseCard
return increment
return Infinity
Estimate the number of coins we’d lose by discarding/trashing/putting back a card.
coinsDueToCard: (state, card) ->
c = state.cardInfo
value = card.getCoins(state)
if card.isTreasure
banks = state.current.countInHand(state.cardInfo.Bank)
value += banks
if card is state.cardInfo.Bank
nonbanks = (aCard for aCard in state.current.hand when aCard.isTreasure).length
value += nonbanks
value
Figure out whether hand1 or hand2 is better by discarding their cards in priority order, until one of them gets to 2 or fewer cards.
Returns a -1 or 1 that can be used in sorting; it’s positive if the first hand is better.
compareByDiscarding: (state, hand1, hand2) ->
Guard against accidental mutation; we’re going to be messing with these lists.
hand1 = hand1.slice(0)
hand2 = hand2.slice(0)
Preserve our number of actions.
savedActions = state.current.actions
state.current.actions = 1
state.log(“hand1 = #{hand1}”) state.log(“hand2 = #{hand2}”)
counter = 0
loop
counter++
if counter >= 100
throw new Error("got stuck in a loop")
Figure out whether we’d rather discard from hand1 or hand2.
discard1 = this.choose('discard', state, hand1)
value1 = this.choiceToValue('discard', state, discard1)
discard2 = this.choose('discard', state, hand2)
value2 = this.choiceToValue('discard', state, discard2)
if value1 > value2
hand1.remove(discard1)
else if value2 > value1
hand2.remove(discard2)
else
hand1.remove(discard1)
hand2.remove(discard2)
if hand1.length <= 2 and hand2.length <= 2
state.current.actions = savedActions
return 0
if hand1.length <= 2
state.current.actions = savedActions
return -1
if hand2.length <= 2
state.current.actions = savedActions
return 1
copy
makes a copy of the AI. It will have the same behavior but a
different name, and will not be equal to this AI.
copy: () =>
ai = new BasicAI()
for key, value of this
ai[key] = value
ai.name = this.name+'*'
ai
Some functions need to check the actionPriority a lot. This pair of methods will save a cached value so you don’t need to run such an expensive function over and over.
cachedActionPriority: (state, my) ->
my.ai.cachedAP
cacheActionPriority: (state, my) ->
@cachedAP = my.ai.actionPriority(state, my)
toString: () -> this.name
this.BasicAI = BasicAI
count
counts the number of times elt
appears in list
.
countInList = (list, elt) ->
count = 0
for member in list
if member == elt
count++
count
stringify
turns an object into a string, while handling null
safely.
stringify = (obj) ->
if obj is null
return null
else
return obj.toString()
General function to randomly shuffle a list.
shuffle = (v) ->
i = v.length
while i
j = parseInt(Math.random() * i)
i -= 1
temp = v[i]
v[i] = v[j]
v[j] = temp
v
compare Arrays
arrayEqual = (a, b) ->
a.length is b.length and a.every (elem, i) -> elem is b[i]