c = {}
this.c = c
c.allCards = []
Dominion cards and their effects are defined in this file. Each card is a singleton, immutable object.
We begin by creating the c
object, an exported object with which one can
look up any card by its name.
c = {}
this.c = c
c.allCards = []
Many cards are defined in terms of other cards using a pattern similar to inheritance, except without the classes. There is no need for classes because there are no separate instances. Each Copper is a reference to the same single Copper object, for example.
The makeCard
function will define a new card and add it to the card list.
makeCard
works by copying an existing card object and applying a few new
properties to it.
name
is the name of the card, which will be the card’s string
representation and the key that you look it up in the card list c
by.
To define a card independently of any existing card, let origCard
be the
abstract card called basicCard
. To define a card in terms of another card,
let origCard
be that card object (probably a member of c
, such as
c.Estate
).
props
are the properties of the card that differ from its parent.
fake
is true when this should be an abstract card, not a card in the
supply. Fake cards are simply returned, not added to c
.
makeCard = (name, origCard, props, fake) ->
newCard = {}
for key, value of origCard
newCard[key] = value
newCard.name = name
for key, value of props
newCard[key] = value
newCard.parent = origCard.name # for debugging
if not fake
c[name] = newCard
c.allCards.push(name)
newCard
basicCard
contains all the things that are true by default
about a card, plus many useful methods that will be available on all cards.
All other cards should have basicCard
as an ancestor. Many of the
properties and methods of basicCard
are meant to be overridden in
real cards.
basicCard = {
This set of boolean values defines a card’s types. Cards may have any number of types.
isAction: false
isTreasure: false
isVictory: false
isAttack: false
isReaction: false
isDuration: false
isPrize: false
isMultiplier: false
The base cost of a card is defined here. To find out what a card actually costs, use the getCost() method.
cost: 0
costPotion: 0
These methods may be overridden by cards whose costs vary on their own, particularly Peddler.
costInCoins: (state) -> this.cost
costInPotions: (state) -> this.costPotion
Card costs can change according to things external to the card, such as
bridges and quarries in play. Therefore, any code that wants to know the
actual cost of a card in a state should call card.getCost(state)
.
This method returns a list of two elements, which are the cost in coins and the cost in potions.
getCost: (state) ->
coins = this.costInCoins(state)
for modifier in state.costModifiers
coins += modifier.modify(this)
if coins < 0
coins = 0
return [coins, this.costInPotions(state)]
These properties define simple, non-variable effects of playing a card. They may only have constant numeric values.
actions: 0
cards: 0
coins: 0
coinTokens: 0
buys: 0
vp: 0
trash: 0 # if the card requires trashing for no further effect
If a card has simple effects that vary based on the state, define them by overriding these methods, which do take the state as a parameter. The constant properties above will be ignored in that case, but you could fill them in with reasonable guesses for the benefit of AI methods that don’t want to examine the state.
getActions: (state) -> this.actions
getCards: (state) -> this.cards
getCoins: (state) -> this.coins
getCoinTokens: (state) -> this.coinTokens
getBuys: (state) -> this.buys
getTrash: (state) -> this.trash
getVP: (player) -> this.vp
getMultiplier: () ->
if this.isMultiplier then this.multiplier
else 1
getPotion says whether the card provides a potion. There is only one card for which this is true, which is Potion.
getPotion: (state) -> 0
Some cards (Grand Market) may not be bought in certain situations.
Use cards.mayBeBought(state)
to define when. By default, a card may be
bought whenever it is in the supply.
mayBeBought: (state) -> true
card.startingSupply(state)
is called once for each card in the supply
at the start of the game, to determine how many of them go into the supply.
This is 10 by default, but some types of cards override it.
startingSupply: (state) -> 10
More complex effects of a card can be defined using arbitrary functions
that modify the state. These functions are no-ops in basicCard
, and
may be overridden by cards that need them:
Card initialization that happens at the start of the game, for instance Black Market might set up the Black Market Deck, or Island might set up the Island Mat
startGameEffect: (state) ->
buyEffect: (state) ->
gainEffect: (state, player) ->
playEffect: (state) ->
trashEffect: (state, player) ->
gainInPlayEffect: (state, card) ->
buyInPlayEffect: (state, card) ->
cleanupEffect: (state) ->
durationEffect: (state) ->
shuffleEffect: (state) ->
reactToAttack: (state, player, attackEvent) ->
durationReactToAttack: (state, player, attackEvent) ->
reactToGain: (state, player, card) ->
reactToOpponentGain: (state, player, opponent, card) ->
reactToDiscard: (state, player) ->
globalGainEffect: (state, player, card, source) ->
This defines everything that happens when a card is played, including
basic effects and complex effects defined in playEffect
. Cards
should not override onPlay
; they should override playEffect
instead.
onPlay: (state) ->
state.current.actions += this.getActions(state)
state.current.coins += this.getCoins(state)
state.current.potions += this.getPotion(state)
state.current.coinTokens += this.getCoinTokens(state)
state.current.buys += this.getBuys(state)
cardsToDraw = this.getCards(state)
cardsToTrash = this.getTrash(state)
if cardsToDraw > 0
state.drawCards(state.current, cardsToDraw)
if cardsToTrash > 0
state.requireTrash(state.current, cardsToTrash)
if (ct = this.getCoinTokens(state)) > 0
state.log("#{state.current.ai} gains #{ct} Coin Token#{if ct > 1 then "s" else ""}")
this.playEffect(state)
Similarly, these are other ways for the game state to interact
with the card. Cards should override the Effect
methods, not these.
onDuration: (state) ->
this.durationEffect(state)
onCleanup: (state) ->
this.cleanupEffect(state)
onBuy: (state) ->
this.buyEffect(state)
onGain: (state, player) ->
this.gainEffect(state, player)
onTrash: (state, player) ->
this.trashEffect(state, player)
A card’s string representation is its name.
If you have a value called
card
that may be a string or a card object, you can ensure that it is
a card object by looking up c[card]
.
toString: () -> this.name
ai_
methods define the default AI preferences for this card. A prominent
example is ai_playValue, which tells the AI how much to prefer playing this
card (and of course changes with the state of the game). The higher the
ai_playValue, the more it prefers playing it before other cards.
ai_multipliedValue
is similar, but it can be higher when it’s playing an
action with a Throne Room or King’s Court.
ai_multipliedValue: (state, my) ->
unless this.ai_playValue?
throw new Error("no ai_playValue for #{this}")
result = this.ai_playValue(state, my)
return result
}
These are the cards that are not Kingdom cards. Most of them appear in every game; Potion, Platinum, and Colony appear in only some games.
makeCard 'Curse', basicCard, {
Curse is the only card with no type.
cost: 0
vp: -1
startingSupply: (state) ->
switch state.nPlayers
when 1, 2 then 10
when 3 then 20
when 4 then 30
when 5 then 40
else 50
}
To define victory cards, we define Estate and then derive other cards from it.
makeCard 'Estate', basicCard, {
cost: 2
isVictory: true
vp: 1
startingSupply: (state) ->
switch state.nPlayers
when 1, 2 then 8
else 12
}
makeCard 'Duchy', c.Estate, {
cost: 5, vp: 3,
If Duchess is in the game, the player has the option of gaining it.
gainEffect: (state, player) ->
if state.supply['Duchess']?
state.gainOneOf(player, [c.Duchess, null])
}
makeCard 'Province', c.Estate, {
cost: 8
vp: 6
startingSupply: (state) ->
switch state.nPlayers
when 1, 2 then 8
when 3, 4 then 12
when 5 then 15
else 18
}
makeCard 'Colony', c.Estate, {cost: 11, vp: 10}
Now we define the basic treasure cards. Our prototypical card here is Silver.
makeCard 'Silver', basicCard, {
cost: 3
isTreasure: true
coins: 2
startingSupply: (state) -> 40
ai_playValue: (state, my) -> 100
}
Copper is actually more complex than Silver: its value can vary when modified by Coppersmith.
makeCard 'Copper', c.Silver, {
cost: 0
coins: 1
getCoins: (state) -> state.copperValue ? 1
startingSupply: (state) -> 60
}
makeCard 'Gold', c.Silver, {
cost: 6
coins: 3
startingSupply: (state) -> 30
}
makeCard 'Platinum', c.Silver, {
cost: 9,
coins: 5,
startingSupply: (state) -> 12
}
makeCard 'Potion', c.Silver, {
cost: 4
coins: 0
getPotion: (state) -> 1
startingSupply: (state) -> 16
}
These cards have effects that involve no decisions, and are expressed entirely in +actions, +cards, +coins, +buys, and VP.
Action cards may derive from the virtual card called action
.
action = makeCard 'action', basicCard, {isAction: true}, true
makeCard 'Village', action, {
cost: 3, actions: 2, cards: 1
ai_playValue: (state, my) -> 820
}
makeCard "Worker's Village", action, {
cost: 4
actions: 2
cards: 1
buys: 1
ai_playValue: (state, my) -> 832
}
makeCard 'Laboratory', action, {
cost: 5, actions: 1, cards: 2
ai_playValue: (state, my) -> 782
}
makeCard 'Smithy', action, {
cost: 4
cards: 3
ai_playValue: (state, my) ->
if my.actions > 1 then 665 else 200
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1540 else -1
}
makeCard 'Festival', action, {
cost: 5, actions: 2, coins: 2, buys: 1
ai_playValue: (state, my) -> 845
}
makeCard 'Woodcutter', action, {
cost: 3, coins: 2, buys: 1
ai_playValue: (state, my) -> 164
}
makeCard 'Market', action, {
cost: 5, actions: 1, cards: 1, coins: 1, buys: 1
ai_playValue: (state, my) -> 775
}
makeCard 'Bazaar', action, {
cost: 5, actions: 2, cards: 1, coins: 1
ai_playValue: (state, my) -> 835
}
makeCard 'Candlestick Maker', action, {
cost: 2, actions: 1, coinTokens: 1, buys: 1
ai_playValue: (state, my) -> 734
}
These cards are all derived from Estate to insure their starting supply amount is correct. This goes for multi-type Victory cards too—deriving Great Hall from action instead of Estate results in 10 Great Halls in the supply instead of 8 for a 2-player game or 12 for more players.
makeCard 'Duke', c.Estate, {
cost: 5
getVP: (player) -> player.countInDeck('Duchy')
}
makeCard 'Fairgrounds', c.Estate, {
cost: 6
getVP: (player) ->
unique = []
deck = player.getDeck()
for card in deck
if card not in unique
unique.push(card)
2 * Math.floor(unique.length / 5)
}
makeCard 'Farmland', c.Estate, {
cost: 6
vp: 2
upgradeFilter: (state, oldCard, newCard) ->
[coins1, potions1] = oldCard.getCost(state)
[coins2, potions2] = newCard.getCost(state)
return (potions1 == potions2) and (coins1 + 2 == coins2)
buyEffect: (state) ->
choices = upgradeChoices(state, state.current.hand, this.upgradeFilter)
choice = state.current.ai.choose('upgrade', state, choices)
if choice isnt null
[oldCard, newCard] = choice
state.doTrash(state.current, oldCard)
state.gainCard(state.current, newCard)
}
makeCard 'Feodum', c.Estate, {
cost: 4
getVP: (player) -> Math.floor(player.countInDeck('Silver') / 3)
trashEffect: (state, player) ->
state.gainCard(player, c.Silver)
state.gainCard(player, c.Silver)
state.gainCard(player, c.Silver)
}
makeCard 'Gardens', c.Estate, {
cost: 4
getVP: (player) -> Math.floor(player.getDeck().length / 10)
}
makeCard 'Great Hall', c.Estate, {
isAction: true
cost: 3
cards: +1
actions: +1
ai_playValue: (state, my) ->
if c.Crossroads in my.hand
520
else
742
}
makeCard 'Harem', c.Estate, {
isTreasure: true
cost: 6
coins: 2
vp: 2
startingSupply: (state) -> 8
ai_playValue: (state, my) -> 100
}
makeCard 'Island', c.Estate, {
isAction: true
cost: 4
vp: 2
startGameEffect: (state) ->
for player in state.players
player.mats.island = []
playEffect: (state) ->
if state.current.hand.length == 0 # handle a weird edge case
state.log("…setting aside the Island (no other cards in hand).")
else
card = state.current.ai.choose('island', state, state.current.hand)
state.log("…setting aside the Island and a #{card}.")
state.current.hand.remove(card)
state.current.mats.island.push(card)
removing the Island from play is conditional so it won’t break with Throne Room and King’s Court
if this in state.current.inPlay
state.current.inPlay.remove(this)
state.current.mats.island.push(this)
ai_playValue: (state, my) -> 132
}
makeCard 'Nobles', c.Estate, {
isAction: true
cost: 6
vp: 2
Nobles is an example of a card that allows a choice from multiple
simple effects. We implement this using the choose('benefit')
AI method,
which is passed a list of benefit objects, one of which it will choose
to apply to the state.
playEffect: (state) ->
benefit = state.current.ai.choose('benefit', state, [
{actions: 2},
{cards: 3}
])
applyBenefit(state, benefit)
ai_playValue: (state, my) -> 296
ai_multipliedValue: (state, my) -> 1340
}
makeCard 'Silk Road', c.Estate, {
cost: 4
getVP: (player) -> Math.floor(player.countCardTypeInDeck('Victory') / 4)
}
Revealing Tunnel for Gold as it is discarded is automatic. TODO: make this into a decision.
makeCard 'Tunnel', c.Estate, {
isReaction: true
cost: 3
vp: 2
reactToDiscard: (state, player) ->
if state.phase isnt 'cleanup'
state.log("#{player.ai} gains a Gold for discarding the Tunnel.")
state.gainCard(player, c.Gold)
}
makeCard 'Vineyard', c.Estate, {
cost: 0
costPotion: 1
getVP: (player) -> Math.floor(player.numActionCardsInDeck() / 3)
}
Kingdom cards that are also treasure cards derive from treasure, which derives from Silver, but with a changed startingSupply.
treasure = makeCard 'treasure', c.Silver, {startingSupply: (state) -> 10}, true
makeCard 'Bank', treasure, {
cost: 7
getCoins: (state) ->
coins = 0
for card in state.current.inPlay
if card.isTreasure
coins += 1
coins
playEffect: (state) ->
state.log("...which is worth #{this.getCoins(state)}.")
ai_playValue: (state, my) -> 20
}
makeCard 'Cache', treasure, {
cost: 5
coins: 3
gainEffect: (state, player) ->
state.gainCard(player, c.Copper)
state.gainCard(player, c.Copper)
}
makeCard "Fool's Gold", treasure, {
isReaction: true
cost: 2
coins: 1
getCoins: (state) ->
if state.current.countInPlay("Fool's Gold") > 1
4
else
1
playEffect: (state) ->
state.current.foolsGoldInPlay = true
reactToOpponentGain: (state, player, opp, card) ->
if card is c.Province
if player.ai.choose('foolsGoldTrash', state, [yes, no])
state.doTrash(player, this)
state.gainCard(player, c.Gold, 'draw')
state.log("...putting the Gold on top of the draw pile.")
}
makeCard "Hoard", treasure, {
cost: 6
buyInPlayEffect: (state, card) ->
if card.isVictory
state.gainCard(state.current, c.Gold, 'discard', true)
state.log("...gaining a Gold.")
}
makeCard "Horn of Plenty", treasure, {
cost: 5
coins: 0
playEffect: (state) ->
limit = state.current.numUniqueCardsInPlay()
choices = []
for cardName of state.supply
card = c[cardName]
[coins, potions] = card.getCost(state)
if state.supply[cardName] > 0 and potions == 0 and coins <= limit
choices.push(card)
choice = state.gainOneOf(state.current, choices)
if choice.isVictory
transferCard(this, state.current.inPlay, state.trash)
state.log("...#{state.current.ai} trashes the Horn of Plenty.")
aiPlayValue: (state, my) ->
if my.numUniqueCardsInPlay() >= 2
10
else
-10
}
makeCard 'Ill-Gotten Gains', treasure, {
cost: 5
coins: 1
playEffect: (state) ->
if state.current.ai.choose('gainCopper', state, [yes, no])
state.gainCard(state.current, c.Copper, 'hand')
gainEffect: (state, player) ->
For each player but the gainer: gain a curse.
for i in [0...state.nPlayers]
if state.players[i] != player
state.gainCard(state.players[i], c.Curse)
}
makeCard 'Loan', treasure, {
coins: 1
playEffect: (state) ->
drawn = state.current.dig(state,
(state, card) -> card.isTreasure
)
if drawn.length > 0
treasure = drawn[0]
trash = state.current.ai.choose('trash', state, [treasure, null])
if trash?
state.log("...trashing the #{treasure}.")
transferCard(treasure, drawn, state.trash)
else
state.log("...discarding the #{treasure}.")
state.current.discard.push(treasure)
state.handleDiscards(state.current, [treasure])
ai_playValue: (state, my) -> 70
}
makeCard "Philosopher's Stone", treasure, {
cost: 3
costPotion: 1
getCoins: (state) ->
Math.floor((state.current.draw.length + state.current.discard.length) / 5)
playEffect: (state) ->
state.log("...which is worth #{this.getCoins(state)}.")
}
makeCard 'Quarry', treasure, {
cost: 4
coins: 1
playEffect: (state) =>
state.costModifiers.push
source: this
modify: (card) ->
if card.isAction
-2
else
0
}
makeCard 'Royal Seal', treasure, {
cost: 5
gainInPlayEffect: (state, card) ->
player = state.current
return if player.gainLocation == 'trash'
source = player[player.gainLocation]
if player.ai.choose('gainOnDeck', state, [card, null])
state.log("...putting the #{card} on top of the deck.")
player.gainLocation = 'draw'
transferCardToTop(card, source, player.draw)
}
makeCard 'Spoils', treasure, {
cost: 0
coins: 3
mayBeBought: (state) -> false
startingSupply: (state) -> 0
playEffect: (state) ->
state.current.inPlay.remove(this)
state.specialSupply['Spoils'] += 1
state.log("#{state.specialSupply['Spoils']} Spoils in the supply")
ai_playValue: (state, my) ->
if my.ai.wantsToPlaySpoils(state)
81
else
null
}
makeCard 'Talisman', treasure, {
cost: 4
coins: 1
buyInPlayEffect: (state, card) ->
if card.getCost(state)[0] <= 4 and not card.isVictory
state.gainCard(state.current, card, 'discard', true)
state.log("...gaining a #{card}.")
}
makeCard 'Venture', treasure, {
cost: 5
coins: 1
playEffect: (state) ->
drawn = state.current.dig(state,
(state, card) -> card.isTreasure
)
if drawn.length > 0
treasure = drawn[0]
state.log("...playing #{treasure}.")
state.current.inPlay.push(treasure)
treasure.onPlay(state)
ai_playValue: (state, my) -> 80
}
These cards have additional properties, such as durationActions
, defining
constant effects that happen when the card is resolved as a duration card.
The virtual card duration
specifies how to process these effects.
duration = makeCard 'duration', action, {
durationActions: 0
durationBuys: 0
durationCoins: 0
durationCards: 0
isDuration: true
durationEffect:
(state) ->
state.current.actions += this.durationActions
state.current.buys += this.durationBuys
state.current.coins += this.durationCoins
if this.durationCards > 0
state.drawCards(state.current, this.durationCards)
}, true
makeCard 'Haven', duration, {
cost: 2
cards: +1
actions: +1
startGameEffect: (state) ->
for player in state.players
We put Haven and the cards it sets aside on a “mat”
player.mats.haven = []
playEffect: (state) ->
cardInHaven = state.current.ai.choose('putOnDeck', state, state.current.hand)
if cardInHaven?
state.log("#{state.current.ai} sets aside a #{cardInHaven} with Haven.")
transferCard(cardInHaven, state.current.hand, state.current.mats.haven)
else
if state.current.hand.length==0
state.log("#{state.current.ai} has no cards to set aside.")
else
state.warn("hand not empty but no card set aside")
durationEffect: (state) ->
cardFromHaven = state.current.mats.haven.pop()
if cardFromHaven?
state.log("#{state.current.ai} picks up a #{cardFromHaven} from Haven.")
state.current.hand.unshift(cardFromHaven)
ai_playValue: (state, my) -> 710
}
makeCard 'Caravan', duration, {
cost: 4
cards: +1
actions: +1
durationCards: +1
ai_playValue: (state, my) -> 780
}
makeCard 'Fishing Village', duration, {
cost: 3
coins: +1
actions: +2
durationActions: +1
durationCoins: +1
ai_playValue: (state, my) -> 823
}
makeCard 'Wharf', duration, {
cost: 5
cards: +2
buys: +1
durationCards: +2
durationBuys: +1
ai_playValue: (state, my) -> 275
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1740 else -1
}
makeCard 'Merchant Ship', duration, {
cost: 5
coins: +2
durationCoins: +2
ai_playValue: (state, my) -> 186
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1500 else -1
}
makeCard 'Lighthouse', duration, {
cost: 2
actions: +1
coins: +1
durationCoins: +1
ai_playValue: (state, my) -> 715
durationReactToAttack: (state, player, attackEvent) ->
Don’t bother blocking the attack if it’s already blocked (avoid log spam)
unless attackEvent.blocked
state.log("#{player.ai} is protected by the Lighthouse.")
attackEvent.blocked = true
}
makeCard 'Outpost', duration, {
cost: 5
effect implemented by gameState
ai_playValue: (state, my) ->
if state.extraTurn
-15
else
154
}
makeCard 'Tactician', duration, {
cost: 5
durationActions: +1
durationBuys: +1
durationCards: +5
playEffect: (state) ->
If this is the first time we’ve played Tactician this turn, reset the count of active Tacticians.
if state.current.countInPlay('Tactician') == 1
state.cardState[this] =
activeTacticians: 0
cardsInHand = state.current.hand.length
If any cards can be discarded…
if cardsInHand > 0
Discard the hand and activate the tactician.
state.log("...discarding the whole hand.")
state.cardState[this].activeTacticians++
discards = state.current.hand
state.current.discard = state.current.discard.concat(discards)
state.current.hand = []
state.handleDiscards(state.current, discards)
The cleanupEffect of a dead Tactician is to discard it instead of putting it in the duration area. It’s not a duration card in this case.
cleanupEffect: (state) ->
if state.cardState[this].activeTacticians > 0
state.cardState[this].activeTacticians--
else
state.log("#{state.current.ai} discards an inactive Tactician.")
transferCard(c.Tactician, state.current.inPlay, state.current.discard)
state.handleDiscards(state.current, [c.Tactician])
ai_playValue: (state, my) ->
FIXME: playing Tactician is extremely situational and this doesn’t take it into account.
272
}
This section describes the actions where you trash one card to gain another. I refer to this in general as “upgrading”, which is not meant to be specific to the card Upgrade.
The prototype on which we base these cards is Remodel. Most of the other cards are variants that simply change the filter for which upgrades are possible.
makeCard 'Remodel', action, {
cost: 4
exactCostUpgrade: false
costFunction: (coins) -> coins + 2
upgradeFilter: (state, oldCard, newCard) ->
Given two cards, return whether upgrading from oldCard to newCard is allowed.
[coins1, potions1] = oldCard.getCost(state)
[coins2, potions2] = newCard.getCost(state)
We’ll leave the cost check in this.costFunction
, so we can reuse this code
for many upgrading cards with different cost requirements.
if this.exactCostUpgrade
return (potions1 == potions2) and (this.costFunction(coins1) == coins2)
else
return (potions1 >= potions2) and (this.costFunction(coins1) >= coins2)
playEffect: (state) ->
Find the pairs of cards we’re allowed to upgrade from and to.
choices = upgradeChoices(state, state.current.hand, this.upgradeFilter.bind(this))
if this.exactCostUpgrade
If the card requires upgrading to a card with an exact cost, then we’ll likely have the option to upgrade a card to nothing. Add in those choices.
choices2 = nullUpgradeChoices(state, state.current.hand, this.costFunction.bind(this))
choices = choices.concat(choices2)
choice = state.current.ai.choose('upgrade', state, choices)
if choice isnt null
[oldCard, newCard] = choice
state.doTrash(state.current, oldCard)
if newCard isnt null
state.gainCard(state.current, newCard)
ai_playValue: (state, my) -> 223
}
makeCard 'Expand', c.Remodel, {
cost: 7
costFunction: (coins) -> coins + 3
ai_playValue: (state, my) -> 226
}
New in Dark Ages.
makeCard 'Graverobber', c.Remodel, {
cost: 5
upgradeFilter: (state, oldCard, newCard) ->
[coins1, potions1] = oldCard.getCost(state)
[coins2, potions2] = newCard.getCost(state)
return oldCard.isAction and (potions1 >= potions2) and (coins1 + 3 >= coins2)
I’ll suppose this card is a bit better to play than Remodel and worse than Expand, but I really don’t know.
ai_playValue: (state, my) -> 225
playEffect: (state) ->
Find the pairs of cards we’re allowed to upgrade from and to.
choices = upgradeChoices(state, state.current.hand, this.upgradeFilter.bind(this))
We can instead choose to gain cards costing 3 to 6 from the trash onto the deck. Consider those as “upgrades” from nothing to that card, so we can compare them to our upgrade choices.
FIXME: This doesn’t take into account the benefit (or drawback) of gaining a card on the deck.
for card in state.trash
[coins, potions] = card.getCost(state)
if 3 <= coins <= 6 and potions == 0
choices.push [null, card]
choice = state.current.ai.choose('upgrade', state, choices)
if choice isnt null
[oldCard, newCard] = choice
if oldCard isnt null
state.doTrash(state.current, oldCard)
if newCard isnt null
if oldCard is null
state.log("...gaining #{newCard} from the trash and putting it on top of the deck.")
state.supply[newCard] += 1
state.trash.remove(newCard)
state.gainCard(state.current, newCard, 'draw', true)
else
state.gainCard(state.current, newCard, 'discard')
}
makeCard 'Upgrade', c.Remodel, {
cost: 5
actions: +1
cards: +1
exactCostUpgrade: true
costFunction: (coins) -> coins + 1
ai_playValue: (state, my) ->
multiplier = my.getMultiplier()
wantsToTrash = my.ai.wantsToTrash(state)
if wantsToTrash >= multiplier
490
else
-30
}
makeCard 'Remake', c.Remodel, {
exactCostUpgrade: true
costFunction: (coins) -> coins + 1
playEffect: (state) ->
for i in [1..2]
choices = upgradeChoices(state, state.current.hand, this.upgradeFilter.bind(this))
choices2 = nullUpgradeChoices(state, state.current.hand, this.costFunction.bind(this))
choice = state.current.ai.choose('upgrade', state, choices.concat(choices2))
if choice isnt null
[oldCard, newCard] = choice
state.doTrash(state.current, oldCard)
if newCard isnt null
state.gainCard(state.current, newCard)
ai_playValue: (state, my) ->
multiplier = my.getMultiplier()
wantsToTrash = my.ai.wantsToTrash(state)
if wantsToTrash >= multiplier*2
178
else
-35
}
makeCard 'Mine', c.Remodel, {
cost: 5
upgradeFilter: (state, oldCard, newCard) ->
[coins1, potions1] = oldCard.getCost(state)
[coins2, potions2] = newCard.getCost(state)
return (potions1 >= potions2) and (coins1 + 3 >= coins2) \
and oldCard.isTreasure and newCard.isTreasure
Modify the Remodel playEffect so that it gains the card in hand.
playEffect: (state) ->
choices = upgradeChoices(state, state.current.hand, this.upgradeFilter.bind(this))
choice = state.current.ai.choose('upgrade', state, choices)
if choice isnt null
[oldCard, newCard] = choice
state.doTrash(state.current, oldCard)
state.gainCard(state.current, newCard, 'hand')
ai_playValue: (state, my) -> 217
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1260 else -1
}
Because Prize cards can only be gained through Tournament, and all have cost = 0, startingSupply -> 0, and mayBeBought -> false it is useful to have a prototype prize. The prototype has isAction: true since 4 of the 5 prizes are action cards.
prize = makeCard 'prize', basicCard, {
cost: 0
isPrize: true
isAction: true
mayBeBought: (state) -> false
startingSupply: (state) -> 0
}, true
makeCard 'Bag of Gold', prize, {
actions: +1
playEffect: (state) ->
state.gainCard(state.current, c.Gold, 'draw')
state.log("...putting the Gold on top of the deck.")
ai_playValue: (state, my) -> 885
}
makeCard 'Diadem', prize, {
isAction: false
isTreasure: true
getCoins: (state) -> 2 + state.current.actions
}
makeCard 'Followers', prize, {
cards: +2
isAttack: true
playEffect: (state) ->
state.gainCard(state.current, c.Estate)
state.attackOpponents (opp) ->
state.gainCard(opp, c.Curse)
if opp.hand.length > 3
state.requireDiscard(opp, opp.hand.length - 3)
ai_playValue: (state, my) -> 292
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1890 else -1
}
Since there is only one Princess card, and Princess’s cost reduction effect has the clause “while this is in play”,
makeCard 'Princess', prize, {
buys: 1
playEffect:
(state) ->
state.costModifiers.push
source: this
modify: (card) -> -2
ai_playValue: (state, my) -> 264
}
makeCard 'Trusty Steed', prize, {
playEffect: (state) ->
benefit = state.current.ai.choose('benefit', state, [
{cards: 2, actions: 2},
{cards: 2, coins: 2},
{actions: 2, coins: 2},
{cards: 2, horseEffect: yes},
{actions: 2, horseEffect: yes},
{coins: 2, horseEffect: yes}
])
applyBenefit(state, benefit)
ai_playValue: (state, my) -> 848
}
Cards with the type Attack; their prototype is just used so isAttack: true doesn’t need to be rewritten every time.
attack = makeCard 'attack', action, {isAttack: true}, true
makeCard 'Ambassador', attack, {
cost: 3
playEffect: (state) ->
Determine the cards and quantities that can be ambassadored
counts = {}
for card in state.current.hand
counts[card] ?= 0
counts[card] += 1
choices = []
for card, count of counts
if count >= 2
choices.push [card, 2]
if count >= 1
choices.push [card, 1]
choices.push [card, 0]
choice = state.current.ai.choose('ambassador', state, choices)
if choice isnt null
[cardName, quantity] = choice
card = c[cardName]
state.log("...choosing to return #{quantity} #{cardName}.")
if state.supply[card]?
for i in [0...quantity]
state.current.hand.remove(card)
Return it to the supply, if it had a slot in the supply to begin with
state.supply[card] += quantity
state.attackOpponents (opp) ->
state.gainCard(opp, card)
else
state.log("...but #{cardName} is not in the Supply.")
ai_playValue: (state, my) ->
wantsToTrash = my.ai.wantsToTrash(state)
if wantsToTrash > 0
150
else
-20
ai_multipliedValue: (state, my) ->
wantsToTrash = my.ai.wantsToTrash(state)
if my.actions > 0 and wantsToTrash > 0
1100
else
-1
}
makeCard 'Bureaucrat', attack, {
cost: 4
playEffect: (state) ->
state.gainCard(state.current, c.Silver, 'draw')
state.attackOpponents (opp) ->
victory = []
for card in opp.hand
if card.isVictory
victory.push(card)
if victory.length == 0
state.revealHand(opp)
state.log("#{opp.ai} reveals a hand with no Victory cards.")
else
choice = opp.ai.choose('putOnDeck', state, victory)
transferCardToTop(choice, opp.hand, opp.draw)
state.log("#{opp.ai} returns #{choice} to the top of the deck.")
ai_playValue: (state, my) -> 128
}
makeCard 'Cutpurse', attack, {
cost: 4
coins: +2
playEffect: (state) ->
state.attackOpponents (opp) ->
if c.Copper in opp.hand
state.doDiscard(opp, c.Copper)
else
state.log("#{opp.ai} has no Copper in hand.")
state.revealHand(opp)
ai_playValue: (state, my) -> 250
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1180 else -1
}
makeCard 'Familiar', attack, {
cost: 3
costPotion: 1
cards: +1
actions: +1
playEffect: (state) ->
state.attackOpponents (opp) ->
state.gainCard(opp, c.Curse)
ai_playValue: (state, my) -> 755
}
makeCard 'Fortune Teller', attack, {
cost: 3
coins: +2
playEffect: (state) ->
state.attackOpponents (opp) ->
drawn = opp.dig(state,
(state, card) -> card.isVictory or card is c.Curse
)
if drawn.length > 0
card = drawn[0]
transferCardToTop(card, drawn, opp.draw)
state.log("...#{opp.ai} puts #{card} on top of the deck.")
ai_playValue: (state, my) -> 130
}
makeCard 'Ghost Ship', attack, {
cost: 5
cards: +2
playEffect: (state) ->
state.attackOpponents (opp) ->
while opp.hand.length > 3
Choosing cards one at a time does not necessarily lead to the best decision. However, it leads to a reasonable, quick decision when there could be a very large number of nearly-identical options to evaluate, which is good for a simulator.
choices = opp.hand
putBack = opp.ai.choose('putOnDeck', state, choices)
state.log("...#{opp.ai} puts #{putBack} on top of the deck.")
transferCardToTop(putBack, opp.hand, opp.draw)
ai_playValue: (state, my) ->
if my.actions > 1 then 670 else 266
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1680 else -1
}
makeCard 'Jester', attack, {
cost: 5
coins: +2
playEffect: (state) ->
state.attackOpponents (opp) ->
card = state.discardFromDeck(opp, 1)[0]
if card?
if card.isVictory
state.gainCard(opp, c.Curse)
else if state.current.ai.chooseGain(state, [card, null])
state.gainCard(state.current, card)
else
state.gainCard(opp, card)
ai_playValue: (state, my) -> 258
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1660 else -1
}
makeCard 'Margrave', attack, {
cost: 5
cards: +3
buys: +1
playEffect: (state) ->
state.attackOpponents (opp) ->
state.drawCards(opp, 1)
if opp.hand.length > 3
state.requireDiscard(opp, opp.hand.length - 3)
ai_playValue: (state, my) ->
if my.actions > 1 then 685 else 280
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1560 else -1
}
makeCard 'Masterpiece', treasure, {
cost: 3
coins: 1
buyEffect: (state) ->
amountOverpayed = state.current.ai.chooseOverpayMasterpiece(state, state.current.coins)
state.log("overpaying for #{amountOverpayed} and gaining #{amountOverpayed} Silvers")
for i in [1 .. amountOverpayed]
state.gainCard(state.current, c['Silver'], 'discard', true)
}
makeCard "Militia", attack, {
cost: 4
coins: +2
Militia is a straightforward example of an attack card.
All attack effects are wrapped in the state.attackOpponents
method, to give opponents a chance to play reaction cards.
playEffect: (state) ->
state.attackOpponents (opp) ->
if opp.hand.length > 3
state.requireDiscard(opp, opp.hand.length - 3)
ai_playValue: (state, my) -> 254
}
makeCard "Goons", c.Militia, {
cost: 6
buys: +1
buyInPlayEffect: (state, card) ->
state.log("...getting +1 ▼.")
state.current.chips += 1
ai_playValue: (state, my) -> 278
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1280 else -1
}
makeCard "Minion", attack, {
cost: 5
actions: +1
discardAndDraw4: (state, player) ->
state.log("#{player.ai} discards the hand.")
discarded = player.hand
Array::push.apply(player.discard, discarded)
player.hand = []
state.handleDiscards(player, discarded)
return state.drawCards(player, 4)
playEffect: (state) ->
player = state.current
if player.ai.choose('minionDiscard', state, [yes, no])
c['Minion'].discardAndDraw4(state, player)
state.attackOpponents (opp) ->
c['Minion'].discardAndDraw4(state, opp)
else
state.attackOpponents (opp) -> null
player.coins += 2
ai_playValue: (state, my) ->
705
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1700 else -1
}
makeCard "Mountebank", attack, {
cost: 5
coins: +2
playEffect: (state) ->
state.attackOpponents (opp) ->
if c.Curse in opp.hand
Discarding a Curse against Mountebank is automatic.
state.doDiscard(opp, c.Curse)
else
state.gainCard(opp, c.Copper)
state.gainCard(opp, c.Curse)
ai_playValue: (state, my) -> 290
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1870 else -1
}
Because attacking on buy does not count as playing an Attack, Noble Brigand’s buyEffect and playEffect cannot directly borrow from each other: the buyEffect should not be blockable by Moat, so it cannot just call the playEffect, and stat.attackOpponents needs an opp parameter, but buyEffect does not have an opp parameter. So a third method is defined which takes both the state and opp as parameters, and is accessed by both the buyEffect and the playEffect.
makeCard 'Noble Brigand', attack, {
cost: 4
coins: +1
buyEffect: (state) ->
for opp in state.players[1..]
c['Noble Brigand'].robTheRich(state, opp)
playEffect: (state) ->
state.attackOpponents (opp) ->
c['Noble Brigand'].robTheRich(state, opp)
robTheRich: (state, opp) ->
drawn = opp.getCardsFromDeck(2)
state.log("...#{opp.ai} reveals #{drawn}.")
silversAndGolds = []
gainCopper = true
for card in drawn
if card.isTreasure
gainCopper = false
if card is c.Gold or card is c.Silver
silversAndGolds.push(card)
treasureToTrash = state.current.ai.choose('trashOppTreasure', state, silversAndGolds)
if treasureToTrash
state.log("...#{state.current.ai} trashes #{opp.ai}'s #{treasureToTrash}.")
transferCard(treasureToTrash, drawn, state.trash)
transferCard(treasureToTrash, state.trash, state.current.discard)
state.handleGainCard(state.current, treasureToTrash, 'discard')
state.log("...#{state.current.ai} gains the trashed #{treasureToTrash}.")
if gainCopper
state.gainCard(opp, c.Copper)
opp.discard = opp.discard.concat(drawn)
state.handleDiscards(opp, [drawn])
state.log("...#{opp.ai} discards #{drawn}.")
ai_playValue: (state, my) -> 134
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1440 else -1
}
makeCard 'Oracle', attack, {
cost: 3
playEffect: (state) ->
player = state.current
myCards = state.getCardsFromDeck(player, 2)
if player.ai.oracleDiscardValue(state, myCards, player) > 0
state.log("...discarding #{myCards}.")
Array::push.apply(player.discard, myCards)
else
state.log("...keeping #{myCards} on top of the deck.")
Array::unshift.apply(player.draw, myCards)
state.attackOpponents (opp) ->
cards = state.getCardsFromDeck(opp, 2)
Can’t use oracleDiscardValue because it’s a different situation, and we don’t know what’s in the opponent’s hand.
value = 0
for card in cards
value += player.ai.choiceToValue('discardFromOpponentDeck', state, card)
if value > 0
state.log("#{player.ai} discards #{cards} from #{opp.ai}'s deck.")
Array::push.apply(opp.discard, cards)
else
state.log("#{player.ai} leaves #{cards} on #{opp.ai}'s deck.")
Array::unshift.apply(opp.draw, cards)
state.drawCards(player, 2)
ai_playValue: (state, my) ->
if my.actions > 1 then 610 else 180
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1200 else -1
}
makeCard 'Pirate Ship', attack, {
cost: 4
startGameEffect: (state) ->
for player in state.players
player.mats.pirateShip = 0
playEffect: (state) ->
choice = state.current.ai.choose('pirateShip', state, ['coins','attack'])
if choice is 'coins'
state.attackOpponents (opp) -> null
state.current.coins += state.current.mats.pirateShip
state.log("...getting +$#{state.current.mats.pirateShip}.")
else if choice is 'attack'
state.log("...attacking the other players.")
attackSuccess = false
state.attackOpponents (opp) ->
drawn = opp.getCardsFromDeck(2)
state.log("...#{opp.ai} reveals #{drawn}.")
drawnTreasures = []
for card in drawn
if card.isTreasure
drawnTreasures.push(card)
treasureToTrash = state.current.ai.choose('trashOppTreasure', state, drawnTreasures)
if treasureToTrash
attackSuccess = true
transferCard(treasureToTrash, drawn, state.trash)
state.log("...#{state.current.ai} trashes #{opp.ai}'s #{treasureToTrash}.")
opp.discard = opp.discard.concat(drawn)
state.handleDiscards(opp, drawn)
state.log("...#{opp.ai} discards #{drawn}.")
if attackSuccess
state.current.mats.pirateShip += 1
state.log("...#{state.current.ai} takes a Coin token (#{state.current.mats.pirateShip} on the mat).")
ai_playValue: (state, my) -> 136
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1480 else -1
}
makeCard 'Rabble', attack, {
cost: 5
cards: +3
playEffect: (state) ->
state.attackOpponents (opp) ->
drawn = opp.getCardsFromDeck(3)
state.log("#{opp.ai} draws #{drawn}.")
for card in drawn
if card.isTreasure or card.isAction
opp.discard.push(card)
state.log("...discarding #{card}.")
state.handleDiscards(opp, [card])
else
opp.setAside.push(card)
if opp.setAside.length > 0
order = opp.ai.chooseOrderOnDeck(state, opp.setAside, opp)
state.log("...putting #{order} back on the deck.")
opp.draw = order.concat(opp.draw)
opp.setAside = []
ai_playValue: (state, my) ->
if my.actions > 1 then 680 else 206
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1600 else -1
}
makeCard 'Rogue', attack, {
cost: 5
coins: +2
playEffect: (state) ->
my = state.current
gainables = []
for card in state.trash
[coins, potions] = card.getCost(state)
if coins >= 3 and coins <= 6 and potions == 0
gainables.push(card)
if gainables.length > 0
cardToGain = my.ai.choose('rogueGain', state, gainables)
state.supply[cardToGain] += 1
state.trash.remove(cardToGain)
state.gainCard(state.current, cardToGain, 'discard', true)
state.log("...#{my.ai} gains #{cardToGain} from trash.")
else
state.attackOpponents (opp) ->
drawn = opp.getCardsFromDeck(2)
state.log("...#{opp.ai} reveals #{drawn}.")
drawnTrashables = []
for card in drawn
[coins, potions] = card.getCost(state)
if coins >= 3 and coins <= 6
drawnTrashables.push(card)
cardToTrash = opp.ai.choose('rogueTrash', state, drawnTrashables)
if cardToTrash
transferCard(cardToTrash, drawn, state.trash)
state.log("...#{state.current.ai} trashes #{opp.ai}'s #{cardToTrash}.")
opp.discard = opp.discard.concat(drawn)
state.handleDiscards(opp, drawn)
state.log("...#{opp.ai} discards #{drawn}.")
ai_playValue: (state, my) -> 136
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1480 else -1
}
makeCard 'Saboteur', attack, {
cost: 5
upgradeFilter: (state, oldCard, newCard) ->
[coins1, potions1] = oldCard.getCost(state)
[coins2, potions2] = newCard.getCost(state)
return (potions1 >= potions2) and (coins1-2 >= coins2)
playEffect: (state) ->
state.attackOpponents (opp) ->
drawn = opp.dig(state,
(state, card) -> card.getCost(state)[0] >= 3
)
if drawn.length > 0
cardToTrash = drawn[0]
state.log("...#{state.current.ai} trashes #{opp.ai}'s #{cardToTrash}.")
state.trash.push(drawn[0])
drawn[0].trashEffect(state, state.current)
choices = upgradeChoices(state, drawn, c.Saboteur.upgradeFilter)
choices.push([cardToTrash,null])
choice = opp.ai.choose('upgrade', state, choices)
newCard = choice[1]
if newCard?
state.gainCard(opp, newCard, 'discard', true)
state.log("...#{opp.ai} gains #{newCard}.")
else
state.log("...#{opp.ai} gains nothing.")
ai_playValue: (state, my) -> 104
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1460 else -1
}
makeCard 'Scrying Pool', attack, {
cost: 2
costPotion: 1
actions: +1
playEffect: (state) ->
spyDecision(state.current, state.current, state, 'scryingPoolDiscard')
state.attackOpponents (opp) ->
spyDecision(state.current, opp, state, 'discardFromOpponentDeck')
loop
drawn = state.drawCards(state.current, 1)[0]
break if (not drawn?) or (not drawn.isAction)
ai_playValue: (state, my) -> 870
}
makeCard 'Sea Hag', attack, {
cost: 4
playEffect: (state) ->
state.attackOpponents (opp) ->
state.discardFromDeck(opp, 1)
state.gainCard(opp, c.Curse, 'draw')
state.log("...putting the Curse on top of the deck.")
ai_playValue: (state, my) -> 286
ai_multipliedValue: (state, my) ->
if my.actions > 0 and state.countInSupply('Curse') >= 2
1850
else
-1
}
makeCard 'Spy', attack, {
cost: 4
cards: +1
actions: +1
playEffect: (state) ->
spyDecision(state.current, state.current, state, 'discard')
state.attackOpponents (opp) ->
spyDecision(state.current, opp, state, 'discardFromOpponentDeck')
ai_playValue: (state, my) -> 860
}
makeCard 'Thief', attack, {
cost: 4
playEffect: (state) ->
state.attackOpponents (opp) ->
drawn = opp.getCardsFromDeck(2)
state.log("...#{opp.ai} reveals #{drawn}.")
drawnTreasures = []
for card in drawn
if card.isTreasure
drawnTreasures.push(card)
treasureToTrash = state.current.ai.choose('trashOppTreasure', state, drawnTreasures)
if treasureToTrash
state.log("...#{state.current.ai} trashes #{opp.ai}'s #{treasureToTrash}.")
transferCard(treasureToTrash, drawn, state.trash)
cardToGain = state.current.ai.chooseGain(state, [treasureToTrash, null])
if cardToGain
transferCard(cardToGain, state.trash, state.current.discard)
state.handleGainCard(state.current, cardToGain, 'discard')
state.log("...#{state.current.ai} gains the trashed #{treasureToTrash}.")
opp.discard = opp.discard.concat(drawn)
state.handleDiscards(opp, [drawn])
state.log("...#{opp.ai} discards #{drawn}.")
ai_playValue: (state, my) -> 100
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1420 else -1
}
makeCard "Torturer", attack, {
cost: 5
cards: +3
playEffect: (state) ->
state.attackOpponents (opp) ->
if opp.ai.choose('torturer', state, ['curse', 'discard']) == 'curse'
state.gainCard(opp, c.Curse, 'hand')
else
state.requireDiscard(opp, 2)
ai_playValue: (state, my) ->
if my.actions > 1 then 690 else 284
ai_multipliedValue: (state, my) ->
if my.actions > 0 and state.countInSupply('Curse') >= 2
1840
else
-1
}
makeCard 'Witch', attack, {
cost: 5
cards: +2
playEffect: (state) ->
state.attackOpponents (opp) ->
state.gainCard(opp, c.Curse)
ai_playValue: (state, my) ->
if my.actions > 1 then 675 else 288
ai_multipliedValue: (state, my) ->
if my.actions > 0 and state.countInSupply("Curse") >= 2
1860
else
-1
}
makeCard 'Young Witch', attack, {
cost: 4
cards: +2
startGameEffect: (state) ->
state.cardState[this] = cardState = {}
cards = c.allCards
nCards = cards.length
bane = null
Try random cards until we find a suitable bane
until cardState.bane?
bane = c[cards[Math.floor(Math.random() * nCards)]]
if (bane.cost == 2 or bane.cost == 3) and bane.costPotion == 0
unless state.supply[bane]
cardState.bane = bane
Add the bane to the supply
state.supply[bane] = bane.startingSupply(state)
Notify the new card that the game is starting
bane.startGameEffect(state)
state.log("Young Witch Bane card is #{bane}")
playEffect: (state) ->
bane = state.cardState.bane
state.requireDiscard(state.current, 2)
state.attackOpponents (opp) ->
if bane in opp.hand
state.log("#{opp.ai} is protected by the Bane card, #{bane}.")
else
state.gainCard(opp, c.Curse)
ai_playValue: (state, my) -> 282
ai_multipliedValue: (state, my) ->
if my.actions > 0 and state.countInSupply('Curse') >= 2
1830
else
-1
}
All of these cards have effects beyond what can be expressed with a
simple formula, which are generally defined by overriding the complex
methods such as playEffect
.
makeCard 'Advisor', action, {
cost: 4
actions: +1
playEffect: (state) ->
drawn = state.current.getCardsFromDeck(3)
state.log("#{state.current.ai} draws #{drawn}.")
Have the left-hand neighbor (or the AI itself in solitaire) choose a card to discard. Borrow the Envoy AI code for this. It’s not quite right b/c Envoy is terminal, however.
neighbor = state.players[1] ? state.players[0]
choice = neighbor.ai.choose('envoy', state, drawn)
if choice?
state.log("#{neighbor.ai} chooses for #{state.current.ai} to discard #{choice}.")
transferCard(choice, drawn, state.current.discard)
Array::push.apply state.current.hand, drawn
ai_playValue: (state, my) -> 1000
}
makeCard 'Adventurer', action, {
cost: 6
playEffect: (state) ->
drawn = state.current.dig(state,
(state, card) -> card.isTreasure,
2
)
if drawn.length > 0
treasures = drawn
state.current.hand = state.current.hand.concat(treasures)
state.log("...#{state.current.ai} draws #{treasures}.")
ai_playValue: (state, my) -> 176
}
makeCard 'Alchemist', action, {
cost: 3
costPotion: 1
actions: +1
cards: +2
cleanupEffect:
(state) ->
if c.Potion in state.current.inPlay and c.Alchemist in state.current.inPlay
transferCardToTop(c.Alchemist, state.current.inPlay, state.current.draw)
ai_playValue: (state, my) -> 785
}
makeCard 'Apothecary', action, {
cost: 2
costPotion: 1
cards: +1
actions: +1
playEffect: (state) ->
drawn = state.getCardsFromDeck(state.current, 4)
Sort the cards into coppers and potions, which go to the hand, and others, which go temporarily to the setAside pile.
state.log("...drawing #{drawn}.")
for card in drawn
if card is c.Copper or card is c.Potion
state.current.hand.push(card)
state.log("...putting #{card} in the hand.")
else
state.current.setAside.push(card)
if state.current.setAside.length > 0
order = state.current.ai.chooseOrderOnDeck(state, state.current.setAside, state.current)
state.log("...putting #{order} back on the deck.")
state.current.draw = order.concat(state.current.draw)
state.current.setAside = []
ai_playValue: (state, my) -> 880
}
makeCard 'Apprentice', action, {
cost: 5
actions: +1
playEffect: (state) ->
toTrash = state.current.ai.choose('apprenticeTrash', state, state.current.hand)
if toTrash?
[coins, potions] = toTrash.getCost(state)
state.doTrash(state.current, toTrash)
state.drawCards(state.current, coins+2*potions)
ai_playValue: (state, my) -> 730
}
makeCard 'Baker', action, {
cost: 5
actions: 1
cards: 1
coinTokens: 1
startGameEffect: (state) ->
for player in state.players
player.coinTokens += 1
ai_playValue: (state, my) -> 774
}
makeCard 'Bandit Camp', c.Village, {
cost: 5
playEffect: (state) ->
state.gainCard(state.current, c.Spoils)
startGameEffect: (state) ->
state.specialSupply['Spoils'] = 15
ai_playValue: (state, my) -> 821
}
makeCard 'Baron', action, {
cost: 4
buys: +1
playEffect: (state) ->
discardEstate = no
if c.Estate in state.current.hand
discardEstate = state.current.ai.choose('baronDiscard', state, [yes, no])
if discardEstate
state.doDiscard(state.current, c.Estate)
state.current.coins += 4
else
state.gainCard(state.current, c.Estate)
ai_playValue: (state, my) ->
if c.Estate in my.hand
184
else
if my.ai.cardInDeckValue(state, c.Estate, my) > 0
5
else
-5
}
makeCard 'Beggar', action, {
cost: 2
isReaction: true
playEffect: (state) ->
state.gainCard(state.current, c.Copper, 'hand')
state.gainCard(state.current, c.Copper, 'hand')
state.gainCard(state.current, c.Copper, 'hand')
reactToAttack: (state, player, attackEvent) ->
if player.ai.wantsToDiscardBeggar(state, player)
state.doDiscard(player, c.Beggar)
state.gainCard(player, c.Silver, 'draw')
state.gainCard(player, c.Silver, 'draw')
ai_playValue: (state, my) -> 243
}
makeCard 'Bishop', action, {
cost: 4
coins: +1
playEffect: (state) ->
toTrash = state.current.ai.choose('bishopTrash', state, state.current.hand)
state.current.chips += 1
state.log("...gaining 1 VP.")
if toTrash?
state.doTrash(state.current, toTrash)
[coins, potions] = toTrash.getCost(state)
vp = Math.floor(coins/2)
state.log("...gaining #{vp} VP.")
state.current.chips += vp
for opp in state.players[1...]
state.allowTrash(opp, 1)
ai_playValue: (state, my) -> 243
}
makeCard 'Border Village', c.Village, {
cost: 6
gainEffect: (state, player) ->
choices = []
[myCoins, myPotions] = c['Border Village'].getCost(state)
for card of state.supply
if state.supply[card] > 0
[coins, potions] = c[card].getCost(state)
if potions <= myPotions and coins < myCoins
choices.push(c[card])
state.gainOneOf(player, choices)
ai_playValue: (state, my) -> 817
}
makeCard 'Bridge', action, {
cost: 4
coins: 1
buys: 1
playEffect: (state) ->
state.costModifiers.push
source: this
modify: (card) -> -1
ai_playValue: (state, my) -> 246
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1720 else -1
}
makeCard 'Cartographer', action, {
cost: 5
cards: +1
actions: +1
playEffect: (state) ->
player = state.current
revealed = player.getCardsFromDeck(4)
kept = []
state.log("#{player.ai} reveals #{revealed} from the deck.")
while revealed.length
card = revealed.pop()
if player.ai.choose('discard', state, [card, null])
state.log("#{player.ai} discards #{card}.")
player.discard.push(card)
state.handleDiscards(player, [card])
else
kept.push(card)
order = player.ai.chooseOrderOnDeck(state, kept, player)
state.log("#{player.ai} puts #{order} back on the deck.")
player.draw = order.concat(player.draw)
ai_playValue: (state, my) -> 890
}
makeCard 'Cellar', action, {
cost: 2
actions: 1
playEffect: (state) ->
startingCards = state.current.hand.length
state.allowDiscard(state.current, Infinity)
numDiscarded = startingCards - state.current.hand.length
state.drawCards(state.current, numDiscarded)
ai_playValue: (state, my) -> 450
}
makeCard 'Chancellor', action, {
cost: 3
coins: +2
playEffect: (state) ->
player = state.current
The AI has the option of reshuffling. Ask directly if it’ll take it.
if player.ai.choose('reshuffle', state, [yes, no])
state.log("...putting the draw pile into the discard pile.")
draw = player.draw.slice(0)
player.draw = []
player.discard = player.discard.concat(draw)
state.handleDiscards(state.current, draw)
ai_playValue: (state, my) -> 160
}
makeCard 'Chapel', action, {
cost: 2
playEffect:
(state) ->
state.allowTrash(state.current, 4)
ai_playValue: (state, my) ->
wantsToTrash = my.ai.wantsToTrash(state)
if wantsToTrash > 0
146
else
30
}
makeCard 'City', action, {
cost: 5
actions: +2
cards: +1
getCards: (state) ->
if state.numEmptyPiles() >= 1
2
else
1
getBuys: (state) ->
if state.numEmptyPiles() >= 2
1
else
0
getCoins: (state) ->
if state.numEmptyPiles() >= 2
1
else
0
ai_playValue: (state, my) -> 829
}
makeCard 'Conspirator', action, {
cost: 4
coins: 2
don’t count Duration cards because they’re not “played this turn”
getActions: (state) ->
if state.current.actionsPlayed >= 3
1
else
0
getCards: (state) ->
if state.current.actionsPlayed >= 3
1
else
0
ai_playValue: (state, my) ->
if my.inPlay.length >= 2 or my.getCurrentAction()?.isMultiplier
760
else if my.actions < 2
124
else
10
ai_multipliedValue: (state, my) -> 1380
}
makeCard 'Coppersmith', action, {
cost: 4
playEffect:
(state) ->
state.copperValue += 1
ai_playValue: (state, my) ->
switch my.countInHand("Copper")
when 0, 1 then 105
when 2 then 156
else 213
ai_multipliedValue: (state, my) ->
if my.actions > 0 and my.countInHand('Copper') >= 2
1140
else
-1
}
makeCard 'Council Room', action, {
cost: 5
cards: 4
buys: 1
playEffect: (state) ->
for opp in state.players[1...]
state.drawCards(opp, 1)
ai_playValue: (state, my) ->
if my.actions > 0 then 619 else 194
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1580 else -1
}
makeCard 'Counting House', action, {
cost: 5
playEffect: (state) ->
coppersFromDiscard = (card for card in state.current.discard when card==c.Copper)
state.current.discard = (card for card in state.current.discard when card!=c.Copper)
Array::push.apply state.current.hand, coppersFromDiscard
state.log("#{state.current.ai} puts " + coppersFromDiscard.length + " Coppers into his hand.")
ai_playValue: (state, my) -> 158
}
makeCard 'Courtyard', action, {
cost: 2
cards: 3
playEffect: (state) ->
if state.current.hand.length > 0
card = state.current.ai.choose('putOnDeck', state, state.current.hand)
state.doPutOnDeck(state.current, card)
ai_playValue: (state, my) ->
if my.actions > 1 and (my.discard.length + my.draw.length) <= 3
return 615
else
return 188
}
makeCard 'Crossroads', action, {
cost: 2
playEffect: (state) ->
if state.current.countInPlay('Crossroads') == 1
state.current.actions += 3
shortcut, because it doesn’t particularly matter whether just the victory cards are revealed
state.revealHand(state.current)
nVictory = (card for card in state.current.hand when card.isVictory).length
state.drawCards(state.current, nVictory)
ai_playValue: (state, my) ->
FIXME: This represents a particularly dumb strategy. It doesn’t even take into account whether it has any victory cards, or whether it could draw more.
if my.countInPlay(state.cardInfo.Crossroads) > 0
return 298
else
return 580
ai_multipliedValue: (state, my) ->
if my.actions > 0 or my.countInPlay(c.Crossroads) == 0
1800
else
-1
}
makeCard 'Duchess', action, {
cost: 2
coins: +2
playEffect: (state) ->
for pl in state.players
drawn = state.getCardsFromDeck(pl, 1)[0]
state.log("#{pl.ai} reveals #{drawn}.")
if drawn?
discarded = pl.ai.choose('discard', state, [drawn, null])
if discarded?
state.log("...choosing to discard it.")
pl.discard.push(drawn)
else
state.log("...choosing to put it back.")
pl.draw.unshift(drawn)
ai_playValue: (state, my) -> 102
}
makeCard 'Embassy', action, {
cost: 5
cards: +5
playEffect: (state) ->
state.requireDiscard(state.current, 3)
gainEffect: (state, player) ->
for pl in state.players
if pl isnt player
state.gainCard(pl, c.Silver)
ai_playValue: (state, my) ->
if my.actions > 1 then 660 else 198
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1520 else -1
}
makeCard 'Envoy', action, {
cost: 4
playEffect: (state) ->
drawn = state.current.getCardsFromDeck(5)
state.log("#{state.current.ai} draws #{drawn}.")
Have the left-hand neighbor (or the AI itself in solitaire) choose a card to discard.
neighbor = state.players[1] ? state.players[0]
choice = neighbor.ai.choose('envoy', state, drawn)
if choice?
state.log("#{neighbor.ai} chooses for #{state.current.ai} to discard #{choice}.")
transferCard(choice, drawn, state.current.discard)
Array::push.apply state.current.hand, drawn
ai_playValue: (state, my) -> 203
}
makeCard 'Explorer', action, {
cost: 5
playEffect: (state) ->
cardToGain = c.Silver
if c.Province in state.current.hand
state.log("…revealing a Province.")
cardToGain = c.Gold
if state.countInSupply(cardToGain) > 0
state.gainCard(state.current, cardToGain, 'hand', true)
state.log("…and gaining a #{cardToGain}, putting it in the hand.")
else
state.log("…but there are no #{cardToGain}s available to gain.")
ai_playValue: (state, my) ->
if my.countInHand("Province") > 1
282
else
166
}
makeCard 'Farming Village', action, {
cost: 4
actions: +2
playEffect: (state) ->
drawn = state.current.dig(state,
(state, card) -> card.isAction or card.isTreasure
)
if drawn.length > 0
card = drawn[0]
state.log("...#{state.current.ai} draws #{card}.")
state.current.hand.push(card)
ai_playValue: (state, my) -> 838
}
makeCard "Feast", action, {
cost: 4
playEffect: (state) ->
Trash the Feast, unless it’s already been trashed.
if state.current.playLocation != 'trash'
transferCard(c.Feast, state.current[state.current.playLocation], state.trash)
state.current.playLocation = 'trash'
state.log("...trashing the Feast.")
Gain a card costing up to $5.
choices = []
for cardName of state.supply
card = c[cardName]
[coins, potions] = card.getCost(state)
if potions == 0 and coins <= 5
choices.push(card)
state.gainOneOf(state.current, choices)
ai_playValue: (state, my) -> 108
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1390 else -1
}
makeCard 'Golem', action, {
cost: 4
costPotion: 1
playEffect: (state) ->
drawn = state.current.dig(state,
(state, card) -> card.isAction and card.name isnt 'Golem',
2
)
if drawn.length > 0
firstAction = state.current.ai.choose('play', state, drawn)
drawn.remove(firstAction)
secondAction = drawn[0]
actions = [firstAction, secondAction]
for card in actions
if card?
state.log("...#{state.current.ai} plays #{card}.")
state.current.inPlay.push(card)
state.current.playLocation = 'inPlay'
state.resolveAction(card)
ai_playValue: (state, my) -> 743
}
makeCard "Grand Market", c.Market, {
cost: 6
coins: 2
actions: 1
cards: 1
buys: 1
Grand Market is the only card with a non-constant mayBeBought value.
mayBeBought: (state) ->
not(c.Copper in state.current.inPlay)
ai_playValue: (state, my) -> 795
ai_multipliedValue: (state, my) -> 880
}
makeCard 'Haggler', action, {
cost: 5
coins: +2
buyInPlayEffect: (state, card1) ->
[coins1, potions1] = card1.getCost(state)
choices = []
for cardName of state.supply
card2 = c[cardName]
[coins2, potions2] = card2.getCost(state)
if (potions2 <= potions1) and (coins2 < coins1) and not card2.isVictory
choices.push(card2)
else if (potions2 < potions1) and (coins2 == coins1) and not card2.isVictory
choices.push(card2)
state.gainOneOf(state.current, choices)
ai_playValue: (state, my) -> 170
}
makeCard "Hamlet", action, {
cost: 2
cards: +1
actions: +1
playEffect: (state) ->
player = state.current
We take a bit of a shortcut for now: we discard up to two cards, then if only one was discarded, decide whether to use it for +action or +buy.
discarded = state.allowDiscard(player, 2)
if discarded.length == 2
state.log("#{player.ai} gets +1 action and +1 buy.")
player.actions++
player.buys++
else if discarded.length == 1
benefit = player.ai.choose('benefit', state, [
{actions: 1},
{cards: 1}
])
applyBenefit(state, benefit)
ai_playValue: (state, my) -> 720
}
makeCard "Harvest", action, {
cost: 5
playEffect: (state) ->
unique = []
cards = state.discardFromDeck(state.current, 4)
for card in cards
if card not in unique
unique.push(card)
state.current.coins += unique.length
state.log("...gaining +$#{unique.length}.")
ai_playValue: (state, my) -> 174
}
makeCard "Herbalist", action, {
cost: 2
buys: +1
coins: +1
cleanupEffect: (state) ->
choices = []
for card in state.current.inPlay
if card.isTreasure
choices.push(card)
choices.push(null)
choice = state.current.ai.choose('herbalist', state, choices)
if choice isnt null
state.log("#{state.current.ai} uses Herbalist to put #{choice} back on the deck.")
transferCardToTop(choice, state.current.inPlay, state.current.draw)
ai_playValue: (state, my) -> 122
}
makeCard "Highway", action, {
cost: 5
cards: +1
actions: +1
playEffect: (state) ->
state.costModifiers.push
source: this
modify: (card) -> -1
ai_playValue: (state, my) -> 750
}
makeCard "Horse Traders", action, {
cost: 4
buys: +1
coins: +3
isReaction: true
playEffect:
(state) -> state.requireDiscard(state.current, 2)
Horse Traders is not actually a duration card, but it resolves like one when it is set aside. There seems to be no harm in simplifying by putting it in the duration area.
durationEffect:
(state) ->
Pick up Horse Traders and draw another card.
transferCard(c['Horse Traders'], state.current.duration, state.current.hand)
state.drawCards(state.current, 1)
reactToAttack:
(state, player, attackEvent) ->
if c['Horse Traders'] in player.hand
transferCard(c['Horse Traders'], player.hand, player.duration)
ai_playValue: (state, my) -> 240
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1640 else -1
}
So far Hunting Party is the only card that digs for something dependent on the game state.
makeCard 'Hunting Party', action, {
cost: 5
cards: +1
actions: +1
playEffect: (state) ->
state.revealHand(state.current)
drawn = state.current.dig(state,
(state, card) -> card not in state.current.hand
)
if drawn.length > 0
card = drawn[0]
state.log("...#{state.current.ai} draws #{card}.")
state.current.hand.push(card)
ai_playValue: (state, my) -> 790
}
makeCard 'Hunting Grounds', action, {
cost: 6
cards: 4
trashEffect: (state, player) ->
choice = player.ai.choose('huntingGroundsGain', state, ["Estates", "Duchy"])
if choice == "Estates"
state.gainCard(player, c.Estate)
state.gainCard(player, c.Estate)
state.gainCard(player, c.Estate)
else if choice == "Duchy"
state.gainCard(player, c.Duchy)
else
state.log("Invalid choice for HuntingGroundsGain: #{choice}!")
state.gainCard(player, c.Duchy)
ai_playValue: (state, my) ->
if my.actions > 1 then 666 else 201
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1542 else -1
}
makeCard 'Ironworks', action, {
cost: 4
playEffect: (state) ->
choices = []
for cardName, count of state.supply
card = c[cardName]
[coins, potions] = card.getCost(state)
if potions == 0 and coins <= 4 and count > 0
choices.push(card)
gained = state.gainOneOf(state.current, choices)
if gained isnt null
if gained.isAction
state.current.actions += 1
if gained.isTreasure
state.current.coins += 1
if gained.isVictory
state.current.drawCards(1)
FIXME: The current ai_playValue assumes that Ironworks is a terminal. If it wants to gain an action, it should have a higher value.
ai_playValue: (state, my) -> 115
}
Jack of All Trades is a complex card made up of steps that are simple to code:
makeCard 'Jack of All Trades', action, {
cost: 4
playEffect: (state) ->
Gain a silver.
state.gainCard(state.current, c.Silver)
Look at the top card of your deck…
card = state.current.getCardsFromDeck(1)[0]
discard it or put it back.
if card?
if state.current.ai.choose('discard', state, [card, null])
state.log("#{state.current.ai} reveals and discards #{card}.")
state.current.discard.push(card)
else
state.log("#{state.current.ai} reveals #{card} and puts it back.")
state.current.draw.unshift(card)
Draw until you have 5 cards in hand.
if state.current.hand.length < 5
state.drawCards(state.current, 5 - state.current.hand.length)
You may trash a card from your hand that is not a Treasure.
choices = (card for card in state.current.hand when not card.isTreasure)
choices.push(null)
choice = state.current.ai.choose('trash', state, choices)
if choice?
state.doTrash(state.current, choice)
ai_playValue: (state, my) -> 236
}
makeCard 'Journeyman', action, {
cost: 5
playEffect: (state) ->
unique = []
deck = state.current.getDeck()
for card in deck
if card not in unique
unique.push(card)
choices = unique
choice = state.current.ai.choose('skip', state, choices)
my = state.current
drawn = state.current.dig(state,
(state, card) -> return card != choice,
3
)
if drawn.length > 0
newcards = drawn
state.current.hand = state.current.hand.concat(newcards)
state.log("...#{state.current.ai} draws #{newcards}.")
ai_playValue: (state, my) ->
wantsToJM = my.ai.wantsToJM(state, my)
if wantsToJM > 0
146
else
0
}
makeCard "King's Court", action, {
cost: 7
isMultiplier: true
multiplier: 3
optional: true
playEffect: (state) ->
choices = (card for card in state.current.hand when card.isAction)
if choices.length == 0
state.log("...but has no action to play with the #{this}.")
else
choices.push(null) if @optional
chosenAction = state.current.ai.choose('multiplied', state, choices)
if chosenAction is null
state.log("...choosing not to play an action.")
else
transferCard(chosenAction, state.current.hand, state.current.inPlay)
for i in [0...@multiplier]
return if chosenAction is null
state.log("...playing #{chosenAction} (#{i+1} of #{@multiplier}).")
state.resolveAction(chosenAction)
Determine whether this multiplier is going to go to the duration area during the cleanup phase.
putInDuration = false
neverPutInDuration = false
If we’ve already marked a multiplier to be put in the Duration area, don’t mark this one. It’s either already marked or it’s not needed.
md = state.current.multipliedDurations
if md.length > 0 and md[md.length - 1].isMultiplier
neverPutInDuration = true
unless neverPutInDuration
if chosenAction.isMultiplier
Mark the multiplier as if it were a multiplied Duration, which is a flag to not clean it up (as if it were a Duration) later.
if md.length > 0 and not (md[md.length - 1].isMultiplier)
putInDuration = true
if chosenAction.isDuration and chosenAction.name != 'Tactician'
putInDuration = true
Store virtual copies of a multiplied duration card in multipliedDurations
.
for i in [0...@multiplier-1]
md.push(chosenAction)
if putInDuration
Mark it by putting it in multipliedDurations. This also signals that all multiplied duration cards previous to it are accounted for.
md.push(this)
durationEffect: (state) ->
TR and KC don’t actually have a duration effect. The multiplication of of the Duration card has already happened, possibly more than once, and the number of times it happens is not strictly related to the number of multipliers in the duration area. It took a very long BGG thread to figure this out.
ai_playValue: (state, my) ->
if my.ai.wantsToPlayMultiplier(state) then 910 else 390
ai_multipliedValue: (state, my) -> 2000
}
makeCard "Library", action, {
cost: 5
playEffect: (state) ->
player = state.current
while player.hand.length < 7
drawn = player.getCardsFromDeck(1)
If nothing was drawn, the deck and discard pile are empty.
if drawn.length == 0
state.log("...stopping because there are no cards to draw.")
break
card = drawn[0]
if card.isAction
Assume the times the AI wants to set the card aside are the times it is on the discard priority list or has a positive discard value.
if player.ai.choose('discard', state, [card, null])
state.log("#{player.ai} sets aside a #{card}.")
player.setAside.push(card)
else
state.log("#{player.ai} draws a #{card} and chooses to keep it.")
player.hand.push(card)
else
state.log("#{player.ai} draws a #{card}.")
player.hand.push(card)
Discard the set-aside cards.
discards = player.setAside
player.discard = player.discard.concat(discards)
player.setAside = []
state.handleDiscards(state.current, discards)
ai_playValue: (state, my) ->
if my.actions > 1
switch my.hand.length
when 0, 1, 2, 3 then 955
when 4 then 695
when 5 then 620
when 6 then 420
when 7 then 101
else 20
else
switch my.hand.length
when 0, 1, 2, 3 then 260
when 4 then 210
when 5 then 192
when 6 then 118
when 7 then 101
else 20
}
makeCard "Lookout", action, {
cost: 3
actions: +1
playEffect: (state) ->
drawn = state.getCardsFromDeck(state.current, 3)
state.log("...drawing #{drawn}.")
state.current.setAside = drawn
trash = state.current.ai.choose('trash', state, drawn)
if trash isnt null
Trash the card, with the side effect of removing it from the choice list.
state.log("...trashing #{trash}.")
transferCard(trash, state.current.setAside, state.trash)
discard = state.current.ai.choose('discard', state, drawn)
if discard isnt null
transferCard(discard, state.current.setAside, state.current.discard)
state.log("...discarding #{discard}.")
state.handleDiscards(state.current, [discard])
Put the remaining card back on the deck.
state.log("...putting #{drawn} back on the deck.")
state.current.draw = state.current.setAside.concat(state.current.draw)
state.current.setAside = []
ai_playValue: (state, my) ->
if state.gainsToEndGame >= 5 or state.cardInfo.Curse in my.draw
895
else
-5
}
makeCard "Mandarin", action, {
cost: 5
coins: +3
playEffect: (state) ->
if state.current.hand.length > 0
putBack = state.current.ai.choose('putOnDeck', state, state.current.hand)
state.doPutOnDeck(state.current, putBack)
gainEffect: (state, player) ->
treasures = (card for card in player.inPlay when card.isTreasure)
if treasures.length > 0
for treasure in treasures
player.inPlay.remove(treasure)
order = player.ai.chooseOrderOnDeck(state, treasures, state.current)
state.log("...putting #{order} back on the deck.")
player.draw = order.concat(player.draw)
ai_playValue: (state, my) -> 168
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1620 else -1
}
makeCard "Masquerade", action, {
cost: 3
cards: +2
playEffect: (state) ->
Get everyone’s choice of cards to pass.
passed = []
for player in state.players
cardToPass = player.ai.choose('trash', state, player.hand)
passed.push(cardToPass)
Pass the cards.
for i in [0...state.nPlayers]
player = state.players[i]
nextPlayer = state.players[(i + 1) % state.nPlayers]
cardToPass = passed[i]
state.log("#{player.ai} passes #{cardToPass}.")
if cardToPass isnt null
transferCard(cardToPass, player.hand, nextPlayer.hand)
Allow the Masquerade player to trash a card.
state.allowTrash(state.current, 1)
ai_playValue: (state, my) -> 270
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1240 else -1
}
makeCard "Menagerie", action, {
cost: 3
actions: +1
playEffect: (state) ->
state.revealHand(state.current)
state.drawCards(state.current, state.current.menagerieDraws())
ai_playValue: (state, my) ->
if my.menagerieDraws() == 3 then 980 else 340
}
makeCard "Merchant Guild", action, {
cost: 5
buys: 1
coins: 1
buyInPlayEffect: (state, card) ->
state.current.coinTokens += 1
state.log("#{state.current.ai} gains 1 Coin Token")
ai_playValue: (state, my) ->
269
}
makeCard "Mining Village", c.Village, {
cost: 4
playEffect: (state) ->
if state.current.ai.choose('miningVillageTrash', state, [yes, no])
if state.current.playLocation != 'trash'
transferCard(c['Mining Village'], state.current[state.current.playLocation], state.trash)
state.current.playLocation = 'trash'
state.log("...trashing the Mining Village for +$2.")
state.current.coins += 2
ai_playValue: (state, my) -> 814
}
makeCard "Mint", action, {
cost: 5
buyEffect: (state) ->
Remove cost modifiers that were created by treasure (e.g. Quarry)
state.costModifiers = (m for m in state.costModifiers when !m.source.isTreasure)
state.potions = 0
inPlay = state.current.inPlay
for i in [inPlay.length-1...-1]
if inPlay[i].isTreasure
state.log("...trashing a #{inPlay[i]}.")
state.trash.push(inPlay[i])
inPlay.splice(i, 1)
playEffect: (state) ->
treasures = []
for card in state.current.hand
if card.isTreasure
treasures.push(card)
choice = state.current.ai.choose('mint', state, treasures)
if choice isnt null
state.gainCard(state.current, choice)
ai_playValue: (state, my) ->
multiplier = my.getMultiplier()
wantsToTrash = my.ai.wantsToTrash(state)
if my.ai.choose('mint', state, my.hand)
140
else
-7
}
makeCard "Moat", action, {
cost: 2
cards: +2
isReaction: true
reactToAttack: (state, player, attackEvent) ->
Don’t bother blocking the attack if it’s already blocked (avoid log spam)
unless attackEvent.blocked
state.log("#{player.ai} is protected by a Moat.")
attackEvent.blocked = true
ai_playValue: (state, my) -> 120
}
makeCard 'Moneylender', action, {
cost: 4
playEffect: (state) ->
if c.Copper in state.current.hand
state.doTrash(state.current, c.Copper)
state.current.coins += 3
ai_playValue: (state, my) -> 230
}
makeCard "Monument", action, {
cost: 4
coins: 2
playEffect:
(state) ->
state.current.chips += 1
ai_playValue: (state, my) -> 182
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1400 else -1
}
makeCard 'Nomad Camp', c.Woodcutter, {
cost: 4
gainEffect: (state, player) ->
if player.gainLocation != 'trash'
transferCardToTop(c['Nomad Camp'], player[player.gainLocation], player.draw)
player.gainLocation = 'draw'
state.log("...putting the Nomad Camp on top of the deck.")
ai_playValue: (state, my) -> 162
}
makeCard 'Navigator', action, {
cost: 4
coins: +2
playEffect: (state) ->
drawn = state.getCardsFromDeck(state.current, 5)
if state.current.ai.choose('discardHand', state, [drawn, null]) is null
state.log("...choosing to keep #{drawn}.")
order = state.current.ai.chooseOrderOnDeck(state, drawn, state.current)
state.log("...putting #{order} back on the deck.")
state.current.draw = order.concat(state.current.draw)
else
state.log("...discarding #{drawn}.")
Array::push.apply state.current.discard, drawn
state.handleDiscards(state.current, drawn)
ai_playValue: (state, my) -> 126
}
makeCard 'Oasis', action, {
cost: 3
cards: +1
actions: +1
coins: +1
playEffect: (state) ->
state.requireDiscard(state.current, 1)
ai_playValue: (state, my) -> 480
}
makeCard 'Pawn', action, {
cost: 2
playEffect:
(state) ->
benefit = state.current.ai.choose('benefit', state, [
{cards: 1, actions: 1},
{cards: 1, buys: 1},
{cards: 1, coins: 1},
{actions: 1, buys: 1},
{actions: 1, coins: 1},
{buys: 1, coins: 1}
])
applyBenefit(state, benefit)
ai_playValue: (state, my) -> 470
}
makeCard 'Pearl Diver', action, {
cost: 2
cards: +1
actions: +1
playEffect: (state) ->
player = state.current
bottomCard = player.draw.pop()
if bottomCard?
doNotWant = player.ai.choose('discard', state, [bottomCard, null])
if doNotWant
state.log("...choosing to leave #{bottomCard} at the bottom of the deck.")
player.draw.push(bottomCard)
else
state.log("...moving #{bottomCard} from the bottom to the top of the deck.")
player.draw.unshift(bottomCard)
else
state.log("...but the draw pile is empty.")
ai_playValue: (state, my) -> 725
}
makeCard 'Peddler', action, {
cost: 8
actions: 1
cards: 1
coins: 1
costInCoins: (state) ->
cost = 8
if state.phase is 'buy'
cost -= 2 * state.current.actionsPlayed
if cost < 0
cost = 0
cost
ai_playValue: (state, my) -> 770
}
makeCard 'Plaza', c.Village, {
cost: 4
playEffect: (state) ->
numStartingCards = state.current.hand.length
possibleDiscards = (card for card in state.current.hand when card.isTreasure)
possibleDiscards.push(null)
choice = state.current.ai.choose('plazaDiscard', state, possibleDiscards)
if choice?
if choice in possibleDiscards
state.requireDiscard(state.current, 1, (card) -> card == choice)
state.current.coinTokens += 1
state.log("#{state.current.ai} discards a #{choice}")
state.log("... gaining a Coin Token")
}
New in Dark Ages.
makeCard 'Poor House', action, {
cost: 1
coins: +4
playEffect: (state) ->
my = state.current
state.revealHand(my)
for card in my.hand
if card.isTreasure
my.coins -= 1
if my.coins < 0
my.coins = 0
ai_playValue: (state, my) -> 103
}
makeCard 'Rats', action, {
cost: 4
actions: +1
cards: +1
playEffect: (state) ->
my = state.current
trashables = []
for card in my.hand
if card.name != 'Rats'
trashables.push(card)
toTrash = state.current.ai.choose('trash', state, trashables)
if toTrash?
state.doTrash(my, toTrash)
ai_playValue: (state, my) ->
if my.ai.wantsToPlayRats(state, my)
486
else
-1
}
makeCard 'Rebuild', action, {
cost: 5
actions: +1
playEffect: (state) ->
my = state.current
choices = []
for cardname in ["Estate", "Duchy", "Duke", "Province", "Colony"]
card = c[cardname]
choices.push(cardname)
for card in my.getDeck()
if card not in choices and card.isVictory
choices.push(card)
choices.push(c.Copper)
namedcard = my.ai.choose('nameVP', state, choices)
state.log("...#{my.ai} names #{namedcard}.")
drawn = my.dig(state,
(state, card) ->
return card.isVictory and card != namedcard
)
if drawn isnt null and drawn.length > 0
cardToTrash = drawn[0]
state.log("...#{state.current.ai} trashes #{state.current.ai}'s #{cardToTrash}.")
state.trash.push(drawn[0])
vpChoices = []
for cardname in ["Estate", "Duchy", "Duke", "Province", "Colony"]
card = c[cardname]
if state.supply[card] > 0
[coins1, potions1] = cardToTrash.getCost(state)
[coins2, potions2] = card.getCost(state)
if coins2 <= coins1 + 3
vpChoices.push(card)
newCard = my.ai.choose('rebuild', state, vpChoices)
if newCard isnt null
state.gainCard(my, newCard, 'discard', true)
state.log("...#{state.current.ai} gains #{newCard}.")
else
state.log("...#{state.current.ai} gains nothing.")
ai_playValue: (state, my) ->
if my.ai.wantsToRebuild(state, my)
return 1000
else
return -1
}
Also new in Dark Ages.
makeCard 'Sage', action, {
cost: 3
actions: +1
playEffect: (state) ->
my = state.current
drawn = state.current.dig(state,
(state, card) ->
[coins, potions] = card.getCost(state)
return coins >= 3
)
if drawn.length > 0
card = drawn[0]
state.log("...#{state.current.ai} draws #{card}.")
state.current.hand.push(card)
ai_playValue: (state, my) -> 746
}
makeCard 'Salvager', action, {
cost: 4
buys: +1
playEffect: (state) ->
toTrash = state.current.ai.choose('salvagerTrash', state, state.current.hand)
if toTrash?
[coins, potions] = toTrash.getCost(state)
state.doTrash(state.current, toTrash)
state.current.coins += coins
ai_playValue: (state, my) -> 220
}
makeCard 'Scheme', action, {
cost: 3
actions: 1
cards: 1
cleanupEffect: (state) ->
choices = (card for card in state.current.inPlay when card.isAction)
choices.push(null)
choice = state.current.ai.choose('scheme', state, choices)
if choice isnt null
state.log("#{state.current.ai} uses Scheme to put #{choice} back on the deck.")
transferCardToTop(choice, state.current.inPlay, state.current.draw)
ai_playValue: (state, my) -> 745
ai_multipliedValue: (state, my) ->
if my.countInDeck("King's Court") > 2 then 1780 else -1
}
makeCard 'Scout', action, {
cost: 4
actions: +1
playEffect: (state) ->
drawn = state.getCardsFromDeck(state.current, 4)
state.log("...drawing #{drawn}.")
Implemented approximately the same way as Apothecary.
for card in drawn
if card.isVictory
state.current.hand.push(card)
state.log("...putting #{card} in the hand.")
else
state.current.setAside.push(card)
if state.current.setAside.length > 0
order = state.current.ai.chooseOrderOnDeck(state, state.current.setAside, state.current)
state.log("...putting #{order} back on the deck.")
state.current.draw = order.concat(state.current.draw)
state.current.setAside = []
ai_playValue: (state, my) -> 875
}
Secret Chamber — Initial code by Jorbles
This is far from optimal, but I believe it does what the card is supposed to do without breaking any rules. I may have to come back to this when my coffee skills are stronger. And I have a greater understanding of how discards are decided. Ideally, the code for discards should be different depending on the type of attack and the total money already in hand.
makeCard "Secret Chamber", action, {
cost: 2
isReaction: true
playEffect: (state) ->
discarded = state.allowDiscard(state.current, Infinity)
state.log("...getting +$#{discarded.length} from the Secret Chamber.")
state.current.coins += discarded.length
reactToAttack: (state, player, attackEvent) ->
state.log("#{player.ai.name} reveals a Secret Chamber.")
state.drawCards(player, 2)
card = player.ai.choose('putOnDeck', state, player.hand)
if card isnt null
state.doPutOnDeck(player, card)
card = player.ai.choose('putOnDeck', state, player.hand)
if card isnt null
state.doPutOnDeck(player, card)
ai_playValue: (state, my) -> 138
}
makeCard 'Shanty Town', action, {
cost: 3
actions: +2
playEffect: (state) ->
state.revealHand(state.current)
state.drawCards(state.current, state.current.shantyTownDraws())
ai_playValue: (state, my) ->
if my.shantyTownDraws(true) == 2
970
else if my.actions < 2
340
else
70
}
makeCard 'Smugglers', action, {
cost: 3
playEffect: (state) ->
state.gainOneOf(state.current, state.smugglerChoices())
ai_playValue: (state, my) -> 110
}
makeCard 'Spice Merchant', action, {
cost: 4
playEffect: (state) ->
trashChoices = (card for card in state.current.hand when card.isTreasure)
trashChoices.push(null)
trashed = state.current.ai.choose('spiceMerchantTrash', state, trashChoices)
if trashed?
state.doTrash(state.current, trashed)
benefit = state.current.ai.choose('benefit', state, [
{cards: 2, actions: 1},
{coins: 2, buys: 1}
])
applyBenefit(state, benefit)
ai_playValue: (state, my) ->
if c.Copper in my.hand
740
else
trashChoices = (card for card in state.current.hand when card.isTreasure)
trashChoices.push(null)
if my.ai.choose('spiceMerchantTrash', state, trashChoices)
410
else
80
}
makeCard 'Stables', action, {
cost: 5
playEffect: (state) ->
discardChoices = (card for card in state.current.hand when card.isTreasure)
discardChoices.push(null)
discarded = state.current.ai.choose('stablesDiscard', state, discardChoices)
if discarded?
state.doDiscard(state.current, discarded)
state.drawCards(state.current, 3)
state.current.actions += 1
ai_playValue: (state, my) ->
discardChoices = (card for card in state.current.hand when card.isTreasure)
discardChoices.push(null)
if my.ai.choose('stablesDiscard', state, discardChoices)
735
else
50
}
makeCard 'Steward', action, {
cost: 3
playEffect:
(state) ->
benefit = state.current.ai.choose('benefit', state, [
{cards: 2},
{coins: 2},
{trash: 2}
])
applyBenefit(state, benefit)
ai_playValue: (state, my) -> 233
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1300 else -1
}
makeCard 'Throne Room', c["King's Court"], {
cost: 4
multiplier: 2
optional: false
ai_playValue: (state, my) ->
if my.ai.wantsToPlayMultiplier(state)
920
else if my.ai.okayToPlayMultiplier(state)
380
else
-50
ai_multipliedValue: (state, my) -> 1900
}
makeCard 'Tournament', action, {
cost: 4
actions: +1
startGameEffect: (state) ->
Add Tournament prizes to the game state’s special supply
prizeNames = ['Bag of Gold', 'Diadem', 'Followers', 'Princess', 'Trusty Steed']
prizes = (c[name] for name in prizeNames)
for prize in prizes
state.specialSupply[prize] = 1
state.cardState[this] =
copy: -> prizes: @prizes.concat()
prizes: prizes
playEffect:
(state) ->
All Provinces are automatically revealed.
opposingProvince = false
for opp in state.players[1...]
if c.Province in opp.hand
state.log("#{opp.ai} reveals a Province.")
opposingProvince = true
if c.Province in state.current.hand
discardProvince = state.current.ai.choose('tournamentDiscard', state, [yes, no])
if discardProvince
state.doDiscard(state.current, c.Province)
prizes = state.cardState[this].prizes
choices = (prize for prize in prizes when state.specialSupply[prize] > 0)
if state.supply[c.Duchy] > 0
choices.push(c.Duchy)
choice = state.gainOneOf(state.current, choices, 'draw')
if choice isnt null
state.log("...putting the #{choice} on top of the deck.")
if not opposingProvince
state.current.coins += 1
state.current.drawCards(1)
ai_playValue: (state, my) ->
if my.countInHand('Province') == 3 then 960 else 360
}
makeCard "Trade Route", action, {
cost: 3
buys: 1
trash: 1
startGameEffect: (state) ->
state.cardState[this] =
copy: -> mat: @mat.concat()
mat: []
globalGainEffect: (state, player, card, source) ->
mat = state.cardState[this].mat
if card.isVictory and source == 'supply' and card not in mat
mat.push(card)
getCoins: (state) ->
state.cardState[this].mat.length
ai_playValue: (state, my) ->
multiplier = my.getMultiplier()
wantsToTrash = my.ai.wantsToTrash(state)
if wantsToTrash >= multiplier
160
else
-25
}
makeCard "Trader", action, {
cost: 4
isReaction: true
playEffect: (state) ->
trashed = state.requireTrash(state.current, 1)[0]
if trashed?
[coins, potions] = trashed.getCost(state)
for i in [0...coins]
state.gainCard(state.current, c.Silver)
reactReplacingGain
triggers before reactToGain
, and lets you replace
the card with a different one.
reactReplacingGain: (state, player, card) ->
card = player.ai.choose('gain', state, [c.Silver, card])
return c[card]
ai_playValue: (state, my) ->
multiplier = my.getMultiplier()
wantsToTrash = my.ai.wantsToTrash(state)
if wantsToTrash >= multiplier
142
else
-22
}
makeCard "Trading Post", action, {
cost: 5
playEffect: (state) ->
state.requireTrash(state.current, 2)
state.gainCard(state.current, c.Silver, 'hand')
state.log("...gaining a Silver in hand.")
ai_playValue: (state, my) ->
multiplier = my.getMultiplier()
wantsToTrash = my.ai.wantsToTrash(state)
if wantsToTrash >= multiplier*2
148
else
-38
}
makeCard "Transmute", action, {
cost: 0
costPotion: 1
playEffect: (state) ->
player = state.current
trashed = player.ai.choose('transmute', state, player.hand)
if trashed?
state.doTrash(player, trashed)
if trashed.isAction
state.gainCard(state.current, c.Duchy)
if trashed.isTreasure
state.gainCard(state.current, c.Transmute)
if trashed.isVictory
state.gainCard(state.current, c.Gold)
ai_playValue: (state, my) ->
multiplier = my.getMultiplier()
wantsToTrash = my.ai.wantsToTrash(state)
if my.ai.choose('mint', state, my.hand)
106
else
-27
}
makeCard 'Treasure Map', action, {
cost: 4
playEffect: (state) ->
trashedMaps = 0
if c['Treasure Map'] in state.current.inPlay
state.log("...trashing the Treasure Map.")
transferCard(c['Treasure Map'], state.current.inPlay, state.trash)
trashedMaps += 1
if c['Treasure Map'] in state.current.hand
state.doTrash(state.current, c['Treasure Map'])
state.log("...and trashing another Treasure Map.")
trashedMaps += 1
if trashedMaps == 2
numGolds = 0
for num in [1..4]
if state.countInSupply(c.Gold) > 0
state.gainCard(state.current, c.Gold, 'draw')
numGolds += 1
state.log("…gaining #{numGolds} Golds, putting them on top of the deck.")
ai_playValue: (state, my) ->
if my.countInHand("Treasure Map") >= 2
294
else if my.countInDeck("Gold") >= 4 and state.current.countInDeck("Treasure Map") == 1
90
else
-40
}
makeCard 'Treasury', c.Market, {
buys: 0
playEffect: (state) ->
state.cardState[this] =
mayReturnTreasury: yes
buyInPlayEffect: (state, card) ->
FIXME: This is incorrect in one highly unlikely edge case - if you buy a victory card from the Black Market, then you play a Treasury, you are not allowed to return the treasury to the top of the deck even though the treasury wasn’t in play when you bought the card.
if card.isVictory
state.cardState[this].mayReturnTreasury = no
cleanupEffect: (state) ->
if state.cardState[this].mayReturnTreasury and c.Treasury in state.current.inPlay
transferCardToTop(c.Treasury, state.current.inPlay, state.current.draw)
state.log("#{state.current.ai} returns a Treasury to the top of the deck.")
ai_playValue: (state, my) -> 765
}
makeCard 'Tribute', action, {
cost: 5
playEffect: (state) ->
revealedCards = state.discardFromDeck(state.players[1], 2)
unique = []
for card in revealedCards
if card not in unique
unique.push(card)
for card in unique
if card.isAction
state.current.actions += 2
if card.isTreasure
state.current.coins += 2
if card.isVictory
state.current.drawCards(2)
ai_playValue: (state, my) ->
after Cursers but before other terminals; there is probably a better spot for it
281
ai_multipliedValue: (state, my) -> 1320
}
makeCard 'University', action, {
cost: 2
costPotion: 1
actions: 2
playEffect: (state) ->
choices = []
for cardName of state.supply
card = c[cardName]
[coins, potions] = card.getCost(state)
if potions == 0 and coins <= 5 and card.isAction
choices.push(card)
state.gainOneOf(state.current, choices)
ai_playValue: (state, my) -> 842
}
makeCard 'Vault', action, {
cost: 5
cards: +2
playEffect: (state) ->
discarded = state.allowDiscard(state.current, Infinity)
state.log("...getting +$#{discarded.length} from the Vault.")
state.current.coins += discarded.length
for opp in state.players[1...]
if opp.ai.wantsToDiscard(state) >= 2
discarded = state.requireDiscard(opp, 2)
if discarded.length == 2
state.drawCards(opp, 1)
ai_playValue: (state, my) -> 268
ai_multipliedValue: (state, my) ->
if my.actions > 0 then 1220 else -1
}
makeCard 'Walled Village', c.Village, {
cost: 4
ai_playValue: (state, my) -> 826
Clean up effect defined in State.doCleanupPhase
}
makeCard 'Warehouse', action, {
cost: 3
actions: +1
playEffect: (state) ->
state.drawCards(state.current, 3)
state.requireDiscard(state.current, 3)
ai_playValue: (state, my) -> 460
}
makeCard 'Watchtower', action, {
cost: 3
isReaction: true
playEffect: (state) ->
handLength = state.current.hand.length
if handLength < 6
state.drawCards(state.current, 6 - handLength)
reactToGain: (state, player, card) ->
return if player.gainLocation == 'trash'
source = player[player.gainLocation]
Determine if the player wants to trash the card. If so, use the Watchtower to do so.
if player.ai.chooseTrash(state, [card, null]) is card
trash the card
state.log("#{player.ai} reveals a Watchtower and trashes the #{card}.")
transferCard(card, source, state.trash)
Note that the gained card now has no valid location; it’s in the trash.
player.gainLocation = 'trash'
else if player.ai.choose('gainOnDeck', state, [card, null])
state.log("#{player.ai} reveals a Watchtower and puts the #{card} on the deck.")
player.gainLocation = 'draw'
transferCardToTop(card, source, player.draw)
ai_playValue: (state, my) ->
if my.actions > 1
switch my.hand.length
when 0, 1, 2, 3, 4 then 650
else -1
else
switch my.hand.length
when 0, 1, 2, 3 then 196
when 4 then 190
else -1
}
makeCard 'Wishing Well', action, {
cost: 3
cards: 1
actions: 1
playEffect: (state) ->
choices = []
for cardName in c.allCards
choices.push(c[cardName])
wish = state.current.ai.choose('wish', state, choices)
state.log("...wishing for a #{wish}.")
drawn = state.current.getCardsFromDeck(1)
if drawn.length > 0
card = drawn[0]
if card is wish
state.log("...revealing a #{card} and keeping it.")
state.current.hand.push(card)
else
state.log("...revealing a #{card} and putting it back.")
state.current.draw.unshift(card)
else
state.log("...drawing nothing.")
ai_playValue: (state, my) -> 745
}
makeCard 'Workshop', action, {
cost: 3
playEffect: (state) ->
choices = []
for cardName of state.supply
card = c[cardName]
[coins, potions] = card.getCost(state)
if potions == 0 and coins <= 4 and state.supply[cardName] > 0
choices.push(card)
state.gainOneOf(state.current, choices)
ai_playValue: (state, my) -> 112
}
transferCard
will move a card from one list to the end of another.
If you are doing something to each card in a list which might result in that card being moved somewhere else, you must iterate over the list backwards. Otherwise you’ll run off the end of the list.
transferCard = (card, fromList, toList) ->
if card not in fromList
throw new Error("#{fromList} does not contain #{card}")
fromList.remove(card)
toList.push(card)
transferCardToTop
will move a card from one list to the front of another.
This is used to put a card on top of the deck, for example.
transferCardToTop = (card, fromList, toList) ->
if card not in fromList
throw new Error("#{fromList} does not contain #{card}")
fromList.remove(card)
toList.unshift(card)
Array::unique
returns the unique keys from a given array
Array::unique = ->
output = {}
output[@[key]] = @[key] for key in [0...@length]
value for key, value of output
Some cards give you a constant benefit, such as +cards or +actions,
every time you play them; these benefits are defined directly on the card
object. Other cards give you such a benefit only under certain conditions,
and if the benefits are straightforward, we may use applyBenefit
to make
them happen. This takes in an object that describes the benefit, and
applies it to the game state.
The actions that can be performed through applyBenefit
currently are:
{cards: n}
: draw n cards{actions: n}
: get +n actions{buys: n}
: get +n buys{coins: n}
: get +n coins{trash: n}
: trash n cards{horseEffect: yes}
: gain 4 Silvers and discard your draw pileThe AI has no rule in it that chooses horseEffect
.
applyBenefit = (state, benefit) ->
state.log("#{state.current.ai} gets #{JSON.stringify(benefit)}.")
if benefit.cards?
state.drawCards(state.current, benefit.cards)
if benefit.actions?
state.current.actions += benefit.actions
if benefit.buys?
state.current.buys += benefit.buys
if benefit.coins?
state.current.coins += benefit.coins
if benefit.trash?
state.requireTrash(state.current, benefit.trash)
if benefit.horseEffect
for i in [0...4]
state.gainCard(state.current, c.Silver)
discards = state.current.draw
state.current.discard = state.current.discard.concat(discards)
state.current.draw = []
state.handleDiscards(state.current, discards)
upgradeChoices
is a helper function to get a list of choices for
Remodel and similar “upgrading” cards. In addition to the game state, it
takes in:
cards
: a list of cards that may be improved, which is usually the cards
in hand; duplicates are fine.filter
: a function of (oldCard, newCard) that describes whether the
improvement is allowed.upgradeChoices = (state, cards, filter) ->
used = []
choices = []
for card in cards
if card not in used
used.push(card)
for cardname2 of state.supply
card2 = c[cardname2]
if filter(state, card, card2) and state.supply[card2] > 0
choices.push([card, card2])
return choices
Find options where you can upgrade a card into nothing, because you’re required to gain a card at a cost where there isn’t anything.
nullUpgradeChoices = (state, cards, costFunction) ->
costs = []
for cardname of state.supply
if state.supply[cardname] > 0
card = c[cardname]
cost = ""+card.getCost(state) # make it a string so it's searchable
if cost not in costs
costs.push(cost)
used = []
choices = []
for card in cards
if card not in used
used.push(card)
[coins, potions] = card.getCost(state)
coins2 = costFunction(coins)
costStr = ""+[coins2, potions]
if costStr not in costs
choices.push([card, null])
return choices
The player
makes a single spying decision on target
‘s deck, using
the decision named decision
to decide whether to keep the card. For
example, if the player is choosing to discard from its own deck, the
decision name is discard
; if it’s an opponent’s deck, the decision
name is discardFromOpponentDeck
.
spyDecision = (player, target, state, decision) ->
drawn = state.getCardsFromDeck(target, 1)[0]
if drawn?
state.log("#{target.ai} reveals #{drawn}.")
discarded = player.ai.choose(decision, state, [drawn, null])
if discarded?
state.log("#{player.ai} chooses to discard it.")
target.discard.push(drawn)
else
state.log("#{player.ai} chooses to put it back on the draw pile.")
target.draw.unshift(drawn)
else
state.log("#{target.ai} has no card to reveal.")
Export functions that are needed elsewhere.
this.transferCard = transferCard
this.transferCardToTop = transferCardToTop