Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #66 from bilts/fix-21-game-state

Move card-specific game state into card definitions
  • Loading branch information...
commit e6f34563dedf8d99fea65d976a08dec85cae528e 2 parents 1943bba + 10135e8
@rspeer authored
Showing with 238 additions and 141 deletions.
  1. +2 −2 basicAI.coffee
  2. +141 −50 cards.coffee
  3. +95 −89 gameState.coffee
View
4 basicAI.coffee
@@ -301,7 +301,7 @@ class BasicAI
# 7: Let's insert here an overly simplistic idea of how to play Crossroads.
# Or if we don't have a Crossroads, play a Great Hall that we might otherwise
# have played in priority level 5.
- "Crossroads" unless my.crossroadsPlayed
+ "Crossroads" unless my.countInPlay(state.cardInfo.Crossroads) > 0
"Great Hall"
# 8: card-cycling that might improve the hand.
@@ -453,7 +453,7 @@ class BasicAI
"Mountebank"
"Witch" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Sea Hag" if my.actions > 0 and state.countInSupply("Curse") >= 2
- "Crossroads" if (not my.crossroadsPlayed) or (my.actions > 0)
+ "Crossroads" if my.actions > 0 or my.countInPlay(state.cardInfo.Crossroads) == 0
"Torturer" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Young Witch" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Scheme" if my.countInDeck("King's Court") >= 2
View
191 cards.coffee
@@ -80,11 +80,10 @@ basicCard = {
# coins and the cost in potions.
getCost: (state) ->
coins = this.costInCoins(state)
- coins -= state.bridges
- coins -= state.highways
- coins -= state.princesses * 2
- if this.isAction
- coins -= state.quarries * 2
+
+ for modifier in state.costModifiers
+ coins += modifier.modify(this)
+
if coins < 0
coins = 0
return [coins, this.costInPotions(state)]
@@ -129,6 +128,10 @@ basicCard = {
# 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) ->
# - What happens when the card is bought?
buyEffect: (state) ->
# - What happens when the card is gained?
@@ -149,14 +152,16 @@ basicCard = {
# - What happens when the card is shuffled into the draw deck?
shuffleEffect: (state) ->
# - What happens when this card is in hand and an opponent plays an attack?
- reactToAttack: (state, player) ->
+ reactToAttack: (state, player, attackEvent) ->
# - What happens when this card is in hand and its owner gains a card?
reactToGain: (state, player, card) ->
# - What happens when this card is in hand and someone else gains a card?
reactToOpponentGain: (state, player, opponent, card) ->
# - What happens when this card is discarded?
reactToDiscard: (state, player) ->
-
+ # - What happens when a card is gained, in general?
+ 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.
@@ -377,6 +382,10 @@ makeCard 'Island', c.Estate, {
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).")
@@ -470,7 +479,7 @@ makeCard "Fool's Gold", treasure, {
coins: 1
getCoins: (state) ->
- if state.current.foolsGoldInPlay
+ if state.current.countInPlay("Fool's Gold") > 1
4
else
1
@@ -554,7 +563,14 @@ makeCard "Philosopher's Stone", treasure, {
makeCard 'Quarry', treasure, {
cost: 4
coins: 1
- playEffect: (state) -> state.quarries += 1
+ playEffect: (state) =>
+ state.costModifiers.push
+ source: this
+ modify: (card) ->
+ if card.isAction
+ -2
+ else
+ 0
}
makeCard 'Royal Seal', treasure, {
@@ -617,20 +633,25 @@ 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.setAsideByHaven)
+ 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.setAsideByHaven.pop()
+ cardFromHaven = state.current.mats.haven.pop()
if cardFromHaven?
state.log("#{state.current.ai} picks up a #{cardFromHaven} from Haven.")
state.current.hand.unshift(cardFromHaven)
@@ -671,7 +692,11 @@ makeCard 'Lighthouse', duration, {
coins: +1
durationCoins: +1
- # The protecting effect is defined in gameState.
+ 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 the Lighthouse.")
+ attackEvent.blocked = true
}
makeCard 'Outpost', duration, {
@@ -686,12 +711,18 @@ makeCard 'Tactician', duration, {
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.current.tacticians++
+ state.cardState[this].activeTacticians++
discards = state.current.hand
state.current.discard = state.current.discard.concat(discards)
state.current.hand = []
@@ -700,8 +731,8 @@ makeCard 'Tactician', duration, {
# 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.current.tacticians > 0
- state.current.tacticians--
+ 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.duration, state.current.discard)
@@ -836,12 +867,13 @@ makeCard 'Followers', prize, {
# Since there is only one Princess card, and Princess's cost
# reduction effect has the clause "while this is in play",
-# state.princesses will never need to be greater than 1.
makeCard 'Princess', prize, {
buys: 1
playEffect:
(state) ->
- state.princesses = 1
+ state.costModifiers.push
+ source: this
+ modify: (card) -> -2
}
makeCard 'Trusty Steed', prize, {
@@ -971,8 +1003,6 @@ makeCard 'Ghost Ship', attack, {
transferCardToTop(putBack, opp.hand, opp.draw)
}
-# Goons: *see Militia*
-
makeCard 'Jester', attack, {
cost: 5
coins: +2
@@ -1018,9 +1048,9 @@ makeCard "Goons", c.Militia, {
cost: 6
buys: +1
- # The effect of Goons that causes you to gain VP on each buy is
- # defined in `State.doBuyPhase`. Other than that, Goons is a fancy
- # Militia.
+ buyInPlayEffect: (state, card) ->
+ state.log("...getting +1 ▼.")
+ state.current.chips += 1
}
makeCard "Minion", attack, {
@@ -1135,6 +1165,10 @@ makeCard 'Oracle', attack, {
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'
@@ -1299,11 +1333,33 @@ makeCard 'Witch', attack, {
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 state.bane in opp.hand
- state.log("#{opp.ai} is protected by the Bane card, #{state.bane}.")
+ if bane in opp.hand
+ state.log("#{opp.ai} is protected by the Bane card, #{bane}.")
else
state.gainCard(opp, c.Curse)
@@ -1429,9 +1485,10 @@ makeCard 'Bridge', action, {
cost: 4
coins: 1
buys: 1
- playEffect:
- (state) ->
- state.bridges += 1
+ playEffect: (state) ->
+ state.costModifiers.push
+ source: this
+ modify: (card) -> -1
}
makeCard 'Cartographer', action, {
@@ -1567,8 +1624,7 @@ makeCard 'Crossroads', action, {
cost: 2
playEffect: (state) ->
- if not state.current.crossroadsPlayed
- state.current.crossroadsPlayed = true
+ if state.current.countInPlay('Crossroads') == 1
state.current.actions += 3
# shortcut, because it doesn't particularly matter whether just the
@@ -1782,11 +1838,9 @@ makeCard "Highway", action, {
actions: +1
playEffect: (state) ->
- highways = 0
- for card in state.current.inPlay
- if card.name is "Highway"
- highways++
- state.highways = highways
+ state.costModifiers.push
+ source: this
+ modify: (card) -> -1
}
makeCard "Horse Traders", action, {
@@ -1807,7 +1861,7 @@ makeCard "Horse Traders", action, {
state.drawCards(state.current, 1)
reactToAttack:
- (state, player) ->
+ (state, player, attackEvent) ->
transferCard(c['Horse Traders'], player.hand, player.duration)
}
@@ -2066,7 +2120,8 @@ makeCard "Mining Village", c.Village, {
makeCard "Mint", action, {
cost: 5
buyEffect: (state) ->
- state.quarries = 0
+ # 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]
@@ -2089,11 +2144,12 @@ makeCard "Moat", action, {
cost: 2
cards: +2
isReaction: true
- # Revealing Moat sets a flag in the player's state, indicating
- # that the player is unaffected by the attack. In this code, Moat
- # is always revealed, without an AI decision.
- reactToAttack:
- (state, player) -> player.moatProtected = 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
}
makeCard 'Moneylender', action, {
@@ -2264,7 +2320,7 @@ makeCard "Secret Chamber", action, {
state.log("...getting +$#{discarded.length} from the Secret Chamber.")
state.current.coins += discarded.length
- reactToAttack: (state, player) ->
+ 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)
@@ -2337,6 +2393,19 @@ makeCard 'Throne Room', c["King's Court"], {
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.
@@ -2349,10 +2418,13 @@ makeCard 'Tournament', action, {
discardProvince = state.current.ai.choose('tournamentDiscard', state, [yes, no])
if discardProvince
state.doDiscard(state.current, c.Province)
- choices = state.prizes.slice(0)
+ 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
@@ -2364,8 +2436,19 @@ 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.tradeRouteValue
+ state.cardState[this].mat.length
}
makeCard "Trader", action, {
@@ -2435,15 +2518,23 @@ makeCard 'Treasure Map', action, {
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.current.mayReturnTreasury = no
-
+ state.cardState[this].mayReturnTreasury = no
+
cleanupEffect: (state) ->
- if state.current.mayReturnTreasury
+ if state.cardState[this].mayReturnTreasury
transferCardToTop(c.Treasury, state.current.discard, state.current.draw)
- state.log("#{state.current.ai} returns a Treasury to the top of the deck.")
+ state.log("#{state.current.ai} returns a Treasury to the top of the deck.")
}
makeCard 'Tribute', action, {
View
184 gameState.coffee
@@ -22,17 +22,16 @@ class PlayerState
@buys = 1
@coins = 0
@potions = 0
- @mats = {
- pirateShip: 0
- nativeVillage: []
- island: []
- }
- @setAsideByHaven = []
@multipliedDurations = []
@chips = 0
@hand = []
@discard = [c.Copper, c.Copper, c.Copper, c.Copper, c.Copper,
c.Copper, c.Copper, c.Estate, c.Estate, c.Estate]
+
+ # A mat is a place where cards can store inter-turn state for a player.
+ # It can correspond to a physical mat, like the Island or Pirate Ship
+ # Mat or just a place to set things aside for cards like Haven.
+ @mats = {}
# If you want to ask what's in a player's draw pile, be sure to only do
# it to a *hypothetical* PlayerState that you retrieve with
@@ -43,11 +42,6 @@ class PlayerState
@duration = []
@setAside = []
@gainedThisTurn = []
- @moatProtected = no
- @tacticians = 0 # number of Tacticians that will go to the duration area
- @crossroadsPlayed = 0
- @foolsGoldInPlay = no
- @mayReturnTreasury = yes
@turnsTaken = 0
# To stack various card effects, we'll have to keep track of the location
@@ -94,8 +88,14 @@ class PlayerState
# `getDeck()` returns all the cards in the player's deck, even those in
# strange places such as the Island mat.
getDeck: () ->
- @draw.concat @discard.concat @hand.concat @inPlay.concat @duration.concat @setAside.concat @mats.nativeVillage.concat @mats.island.concat @setAsideByHaven
-
+ result = [].concat(@draw, @discard, @hand, @inPlay, @duration, @setAside)
+
+ for own name, contents of @mats when contents?
+ # If contents is a card or an array containing cards, add it to the list
+ if contents.hasOwnProperty('playEffect') || contents[0]?.hasOwnProperty('playEffect')
+ result = result.concat(contents)
+ result
+
# `getCurrentAction()` returns the action being resolved that is on the
# top of the stack.
getCurrentAction: () ->
@@ -385,12 +385,15 @@ class PlayerState
other.buys = @buys
other.coins = @coins
other.potions = @potions
- other.setAsideByHaven = @setAsideByHaven.slice(0)
other.multipliedDurations = @multipliedDurations.slice(0)
+
+ # Clone mat contents, deep-copying arrays of cards
other.mats = {}
- other.mats.pirateShip = @mats.pirateShip
- other.mats.nativeVillage = @mats.nativeVillage.slice(0)
- other.mats.island = @mats.island.slice(0)
+ for own name, contents of @mats
+ if contents instanceof Array
+ contents = contents.concat()
+ other.mats[name] = contents
+
other.chips = @chips
other.hand = @hand.slice(0)
other.draw = @draw.slice(0)
@@ -398,16 +401,11 @@ class PlayerState
other.inPlay = @inPlay.slice(0)
other.duration = @duration.slice(0)
other.setAside = @setAside.slice(0)
- other.moatProtected = @moatProtected
other.gainedThisTurn = @gainedThisTurn.slice(0)
- other.foolsGoldInPlay = no
- other.mayReturnTreasury = @mayReturnTreasury
other.playLocation = @playLocation
other.gainLocation = @gainLocation
other.actionStack = @actionStack.slice(0)
other.actionsPlayed = @actionsPlayed
- other.tacticians = @tacticians
- other.crossroadsPlayed = @crossroadsPlayed
other.ai = @ai
other.logFunc = @logFunc
other.turnsTaken = @turnsTaken
@@ -450,31 +448,34 @@ class State
@nPlayers = @players.length
@current = @players[0]
@supply = this.makeSupply(tableau)
+ # Cards like Tournament or Black Market may put cards in a special supply
+ @specialSupply = {}
@trash = []
- @prizes = [c["Bag of Gold"], c.Diadem, c.Followers, c.Princess, c["Trusty Steed"]]
- @tradeRouteMat = []
- @tradeRouteValue = 0
-
- @bridges = 0
- @highways = 0
- @princesses = 0
- @quarries = 0
+
+ # A map of Card to state object that allows cards to define lasting state.
+ @cardState = {}
+
+ # A list of objects which have a "modify" method that takes a card and returns
+ # a modification to its cost. Objects must also have a "source" property that
+ # specifies which card caused the cost modification.
+ @costModifiers = []
+
@copperValue = 1
@phase = 'start'
@extraturn = false
@cache = {}
- if c["Young Witch"] in tableau
- @bane = tableau[10]
- else
- @bane = null
-
# The `depth` indicates how deep into hypothetical situations we are. A depth of 0
# indicates the state of the actual game.
@depth = 0
this.log("Tableau: #{tableau}")
+ # Let cards in the tableau know the game is starting so they can perform
+ # any necessary initialization
+ for card in tableau
+ card.startGameEffect(this)
+
# `totalCards` tracks the total number of cards that are in the game. If it changes,
# we screwed up.
@totalCards = this.countTotalCards()
@@ -514,11 +515,10 @@ class State
index = 0
moreCards = c.allCards.slice(0)
shuffle(moreCards)
- while (tableau.length < 10) or (c["Young Witch"] in tableau and tableau.length < 11)
+ while tableau.length < 10
card = c[moreCards[index]]
if not (card in tableau or card in this.basicSupply or card in this.extraSupply or card.isPrize)
- if not (tableau.length == 10 and (card.cost > 3 or card.costPotion > 0))
- tableau.push(card)
+ tableau.push(card)
index++
if options.colonies
@@ -660,8 +660,9 @@ class State
total += player.numCardsInDeck()
for card, count of @supply
total += count
+ for card, count of @specialSupply
+ total += count
total += @trash.length
- total += @prizes.length
total
#### Playing a turn
@@ -853,12 +854,6 @@ class State
# Talisman, Quarry, Border Village, and Mandarin.
if cardInPlay?
cardInPlay.buyInPlayEffect(this, choice)
-
- # Gain victory for each Goons in play.
- goonses = @current.countInPlay('Goons')
- if goonses > 0
- this.log("...gaining #{goonses} VP.")
- @current.chips += goonses
# Handle all the things that happen at the end of the turn.
doCleanupPhase: () ->
@@ -924,16 +919,10 @@ class State
@current.buys = 1
@current.coins = 0
@current.potions = 0
- @current.tacticians = 0
- @current.crossroadsPlayed = 0
@current.actionsPlayed = 0
- @current.foolsGoldInPlay = no
- @current.mayReturnTreasury = yes
@copperValue = 1
- @bridges = 0
- @highways = 0
- @princesses = 0
- @quarries = 0
+
+ @costModifiers = []
#Announce extra turn
if @extraturn
@@ -969,7 +958,7 @@ class State
# be one of the objects in the `@players` array.
gainCard: (player, card, gainLocation='discard', suppressMessage=false) ->
delete @cache.gainsToEndGame
- if card in @prizes or @supply[card] > 0
+ if @supply[card] > 0 or @specialSupply[card] > 0
for i in [player.hand.length-1...-1]
reactCard = player.hand[i]
if reactCard? and reactCard.isReaction and reactCard.reactReplacingGain?
@@ -989,27 +978,31 @@ class State
location = player[gainLocation]
location.unshift(card)
- # Remove the card from the supply or the prize list, as appropriate.
- if card in @prizes
- @prizes.remove(card)
- else
+ # Remove the card from the supply
+ if @supply[card] > 0
@supply[card] -= 1
-
+ gainSource = 'supply'
+ else
+ @specialSupply[card] -= 1
+ gainSource = 'specialSupply'
+
# Delegate to `handleGainCard` to deal with reactions.
- this.handleGainCard(player, card, gainLocation)
+ this.handleGainCard(player, card, gainLocation, gainSource)
else
this.log("There is no #{card} to gain.")
# `handleGainCard` deals with the reactions that result from gaining a card.
# A card effect such as Thief needs to call this explicitly after gaining a
# card from someplace that is not the supply or the prize list.
- handleGainCard: (player, card, gainLocation='discard') ->
+ handleGainCard: (player, card, gainLocation='discard', gainSource='supply') ->
# Remember where the card was gained, so that reactions can find it.
player.gainLocation = gainLocation
- if @supply["Trade Route"]? and card.isVictory and card not in @tradeRouteMat
- @tradeRouteMat.push(card)
- @tradeRouteValue += 1
+ for own supplyCard, quantity of @supply
+ c[supplyCard].globalGainEffect(this, player, card, gainSource)
+
+ for own supplyCard, quantity of @specialSupply
+ c[supplyCard].globalGainEffect(this, player, card, gainSource)
# Handle cards such as Royal Seal that respond to gains while they are
# in play.
@@ -1183,25 +1176,22 @@ class State
# `attackPlayer` does the work of attacking a particular player, including
# handling their reactions to attacks.
attackPlayer: (player, effect) ->
- # The most straightforward reaction is Moat, which cancels the attack.
- # Set a flag on the PlayerState that indicates that the player has not
- # yet revealed a Moat.
- player.moatProtected = no
-
- # Iterate backwards, because we might be removing things from the list.
- for i in [player.hand.length-1...-1]
- card = player.hand[i]
- if card.isReaction
- card.reactToAttack(this, player)
+ # attackEvent gets passed to each reactToAttack method. Any card
+ # may block the attack by setting attackEvent.blocked to true
+ attackEvent = {}
+
+ # Reaction cards in the hand can react to the attack
+ reactionCards = (card for card in player.hand when card.isReaction)
+
+ # Duration cards such as Lighthouse can also react
+ reactionCards = reactionCards.concat(player.duration)
+
+ for card in reactionCards
+ card.reactToAttack(this, player, attackEvent)
- # If the player has revealed a Moat, or has Lighthouse in the duration
- # area, the attack is averted. Otherwise, it happens.
- if player.moatProtected
- this.log("#{player.ai} is protected by a Moat.")
- else if c.Lighthouse in player.duration
- this.log("#{player.ai} is protected by the Lighthouse.")
- else
- effect(player)
+ # Apply the attack's effect unless it's been blocked by a card such as
+ # Moat or Lighthouse
+ effect(player) unless attackEvent.blocked
#### Bookkeeping
# `copy()` makes a copy of this state that can be safely mutated
@@ -1216,6 +1206,10 @@ class State
for key, value of @supply
newSupply[key] = value
+ newSpecialSupply = {}
+ for key, value of @specialSupply
+ newSpecialSupply[key] = value
+
newState = new State()
# If something overrode the log function, make sure that's preserved.
newState.logFunc = @logFunc
@@ -1225,22 +1219,34 @@ class State
playerCopy = player.copy()
playerCopy.logFunc = (obj) ->
newPlayers.push(playerCopy)
+
+ # Copy card-specific state
+ newCardState = {}
+ for card, state of @cardState
+ # If the card state has a copy method, call it, otherwise just shallow
+ # copy the state
+ if state.copy?
+ # Objects with a copy method
+ newCardState[card] = state.copy?()
+ else if typeof state == 'object'
+ # Objects with no copy method
+ newCardState[card] = copy = {}
+ copy[k] = v for k, v of state
+ else
+ # Simple types
+ newCardState[card] = state
newState.players = newPlayers
newState.supply = newSupply
+ newState.specialSupply = newSpecialSupply
+ newState.cardState = newCardState
newState.trash = @trash.slice(0)
newState.current = newPlayers[0]
newState.nPlayers = @nPlayers
- newState.tradeRouteMat = @tradeRouteMat.slice(0)
- newState.tradeRouteValue = @tradeRouteValue
- newState.bridges = @bridges
- newState.highways = @highways
- newState.princesses = @princesses
- newState.quarries = @quarries
+ newState.costModifiers = @costModifiers.concat()
newState.copperValue = @copperValue
newState.phase = @phase
newState.cache = {}
- newState.prizes = @prizes.slice(0)
newState
Please sign in to comment.
Something went wrong with that request. Please try again.