From 36684e5de2d32a7d92eea93d04c3e4f03380885c Mon Sep 17 00:00:00 2001 From: Jorbles Date: Thu, 15 Dec 2011 15:19:26 -0800 Subject: [PATCH 01/38] @rspeer I've never written coffee code before so please go over this, especially how i implemented the card drawing for the reaction to being attacked. I've gotta get going for now, but I'll give this some more thought once I've got time to pour over where the ai.choose code is implemented. --- cards.coffee | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cards.coffee b/cards.coffee index 9c1301b..f5759c9 100644 --- a/cards.coffee +++ b/cards.coffee @@ -2231,6 +2231,33 @@ makeCard 'Scout', action, { state.current.setAside = [] } +# 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) -> + state.drawCards(player, 2) #is this correct? + #there is probably a better way to write this, but i couldn't find how AIs decide what to discard from Militia, Goons, etc. + card = state.current.ai.choose('putOnDeck', state, state.current.hand) + state.doPutOnDeck(state.current, card) + card = state.current.ai.choose('putOnDeck', state, state.current.hand) + state.doPutOnDeck(state.current, card) +} + makeCard 'Shanty Town', action, { cost: 3 actions: +2 From 0a30ed7d022adbbe4e582fbd1051522e7ddf3722 Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Mon, 19 Dec 2011 06:18:35 -0500 Subject: [PATCH 02/38] No longer plays Apprentice as if it were Salvager. Still testing Secret Chamber. --- basicAI.coffee | 1 + cards.coffee | 21 ++++++++++----------- gh-pages | 2 +- play.coffee | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/basicAI.coffee b/basicAI.coffee index 8938c8b..2127661 100644 --- a/basicAI.coffee +++ b/basicAI.coffee @@ -389,6 +389,7 @@ class BasicAI "Trader" if wantsToTrash >= multiplier "Trade Route" if wantsToTrash >= multiplier "Mint" if my.ai.choose('mint', state, my.hand) + "Secret Chamber" "Pirate Ship" "Noble Brigand" "Thief" diff --git a/cards.coffee b/cards.coffee index f5759c9..0753e90 100644 --- a/cards.coffee +++ b/cards.coffee @@ -2242,20 +2242,19 @@ makeCard 'Scout', action, { 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) -> - state.drawCards(player, 2) #is this correct? - #there is probably a better way to write this, but i couldn't find how AIs decide what to discard from Militia, Goons, etc. + 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) -> + state.drawCards(player, 2) card = state.current.ai.choose('putOnDeck', state, state.current.hand) state.doPutOnDeck(state.current, card) card = state.current.ai.choose('putOnDeck', state, state.current.hand) - state.doPutOnDeck(state.current, card) + state.doPutOnDeck(state.current, card) } makeCard 'Shanty Town', action, { diff --git a/gh-pages b/gh-pages index 2867341..265878b 160000 --- a/gh-pages +++ b/gh-pages @@ -1 +1 @@ -Subproject commit 28673413767ad7f1d399d8b6e48766ba2bffff3a +Subproject commit 265878b69c4dc5011e3c8e081738c4a34843f3e3 diff --git a/play.coffee b/play.coffee index a53a1cb..2631e84 100755 --- a/play.coffee +++ b/play.coffee @@ -26,7 +26,7 @@ playGame = (filenames) -> colonies: false randomizeOrder: true log: console.log - require: ['Tunnel', 'Venture'] + require: ['Secret Chamber', 'Apprentice'] }) until st.gameIsOver() st.doPlay() From 2f14dd3141e950b0debbc50948c7e4f960b07b08 Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Mon, 19 Dec 2011 06:40:39 -0500 Subject: [PATCH 03/38] fix Secret Chamber bugs. Also, make sure that the non-attacky sides of Pirate Ship and Minion still count as attacks so that opponents can react. --- cards.coffee | 15 ++++++++++----- play.coffee | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cards.coffee b/cards.coffee index 0753e90..a6cf078 100644 --- a/cards.coffee +++ b/cards.coffee @@ -1042,6 +1042,7 @@ makeCard "Minion", attack, { state.attackOpponents (opp) -> c['Minion'].discardAndDraw4(state, opp) else + state.attackOpponents (opp) -> null player.coins += 2 } @@ -1137,6 +1138,7 @@ makeCard 'Pirate Ship', attack, { 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' @@ -1369,7 +1371,7 @@ makeCard 'Apprentice', action, { actions: +1 playEffect: (state) -> - toTrash = state.current.ai.choose('salvagerTrash', state, state.current.hand) + toTrash = state.current.ai.choose('apprenticeTrash', state, state.current.hand) if toTrash? [coins, potions] = toTrash.getCost(state) state.doTrash(state.current, toTrash) @@ -2250,11 +2252,14 @@ makeCard "Secret Chamber", action, { state.current.coins += discarded.length reactToAttack: (state, player) -> + state.log("#{player.ai.name} reveals a Secret Chamber.") state.drawCards(player, 2) - card = state.current.ai.choose('putOnDeck', state, state.current.hand) - state.doPutOnDeck(state.current, card) - card = state.current.ai.choose('putOnDeck', state, state.current.hand) - state.doPutOnDeck(state.current, card) + 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) } makeCard 'Shanty Town', action, { diff --git a/play.coffee b/play.coffee index 2631e84..5e52366 100755 --- a/play.coffee +++ b/play.coffee @@ -26,7 +26,7 @@ playGame = (filenames) -> colonies: false randomizeOrder: true log: console.log - require: ['Secret Chamber', 'Apprentice'] + require: [] }) until st.gameIsOver() st.doPlay() From b26f8963a905ae8630bfe6b937b215fb567e3e53 Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Mon, 19 Dec 2011 06:43:29 -0500 Subject: [PATCH 04/38] mark Secret Chamber as done -- fixes #36 --- card_list.txt | 8 ++++---- gh-pages | 2 +- web/play.html | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/card_list.txt b/card_list.txt index f1af32e..407f531 100644 --- a/card_list.txt +++ b/card_list.txt @@ -1,4 +1,4 @@ -Cards that are done (157) +Cards that are done (158) ========================= Adventurer Alchemist @@ -117,6 +117,7 @@ Salvager Scout Scrying Pool Sea Hag +Secret Chamber Shanty Town Silk Road Silver @@ -158,7 +159,7 @@ Worker's Village Workshop Young Witch -Cards that still need to be coded (13) +Cards that still need to be coded (12) ====================================== Possibly easy cards @@ -178,7 +179,7 @@ to write. Black Market (requires a new setup step) Develop (the hard part is making the list of choices) Embargo (the trick is guessing what your opponent wants) -Forge +Forge (exponentially increasing number of choices!) Inn Native Village @@ -187,6 +188,5 @@ Difficult cards In estimated order of difficulty: Scheme (adds a new sub-phase to cleanup) -Secret Chamber (avoid infinite loops) Stash (requires a decision mid-shuffle) Possession (will require entirely new kinds of AI decisions) diff --git a/gh-pages b/gh-pages index 265878b..5922f99 160000 --- a/gh-pages +++ b/gh-pages @@ -1 +1 @@ -Subproject commit 265878b69c4dc5011e3c8e081738c4a34843f3e3 +Subproject commit 5922f9938ea42e41e36ce811c6f4f7393e6023fe diff --git a/web/play.html b/web/play.html index bce19bb..463bb41 100644 --- a/web/play.html +++ b/web/play.html @@ -19,7 +19,7 @@

A simulator for Dominion strategies.

- News: Only 13 cards left to implement! The remaining cards are now listed on our + News: Only 12 cards left to implement! The remaining cards are now listed on our bug tracker.
From 6c28a23b2cf66f4d55940818781b20b9cb054f37 Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Fri, 30 Dec 2011 06:55:49 -0500 Subject: [PATCH 05/38] add a comment explaining how to run testSimulation.coffee --- gh-pages | 2 +- testSimulation.coffee | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/gh-pages b/gh-pages index 5922f99..082829a 160000 --- a/gh-pages +++ b/gh-pages @@ -1 +1 @@ -Subproject commit 5922f9938ea42e41e36ce811c6f4f7393e6023fe +Subproject commit 082829a8aa6c50b7f3c8b69d5ae030d0d39533ae diff --git a/testSimulation.coffee b/testSimulation.coffee index 20a03df..44cd262 100644 --- a/testSimulation.coffee +++ b/testSimulation.coffee @@ -1,3 +1,11 @@ +# This isn't a standalone file that can be run with the "coffee" interpreter. +# It's a unit test file. +# +# You can run it through "nodeunit" (`npm install nodeunit`), which now has +# built-in support for CoffeeScript: +# +# nodeunit testSimulation.coffee + c = require('./cards') gameState = require('./gameState') basicAI = require('./basicAI') From 88f1112f7dbbc97acdabeab69fdd981704b24513 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 8 Jan 2012 13:07:52 +0100 Subject: [PATCH 06/38] FIrst attempt for changed starting conditions --- basicAI.coffee | 3 +++ gameState.coffee | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/basicAI.coffee b/basicAI.coffee index 2127661..95c5f9b 100644 --- a/basicAI.coffee +++ b/basicAI.coffee @@ -34,6 +34,9 @@ class BasicAI # use cachedActionPriority(state, my) to obtain cache @cachedAP = [] + startingDraw: () -> [] + startingDiscard: () -> ["Copper", "Copper", "Copper", "Copper", "Copper", "Copper", "Copper", "Estate", "Estate", "Estate"] + # Referring to `state.current` to find information about one's own state is # not always safe! Some of these decisions may be made during other players' # turns. In those cases, what we want is `this.myPlayer(state)`. diff --git a/gameState.coffee b/gameState.coffee index fbefe39..4b56d8a 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -31,14 +31,13 @@ class PlayerState @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] + @discard = (c[card] for card in ai.startingDiscard()) # 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 # `state.hypothetical(ai)`. Then the draw pile will contain a random # guess, as opposed to the actual hidden information. - @draw = [] + @draw = (c[card] for card in ai.startingDraw()) @inPlay = [] @duration = [] @setAside = [] @@ -293,7 +292,8 @@ class PlayerState drawCards: (nCards) -> drawn = this.getCardsFromDeck(nCards) - @hand = @hand.concat(drawn) + Array::push.apply @hand, drawn + # @hand = @hand.concat(drawn) this.log("#{@ai} draws #{drawn.length} cards: #{drawn}.") return drawn From e8f051ba38c399557c9b9b3d6b023e70f025ed2f Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 8 Jan 2012 16:05:11 +0100 Subject: [PATCH 07/38] Don't kill yourself v0.7 --- gameState.coffee | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/gameState.coffee b/gameState.coffee index 4b56d8a..da93d59 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -663,6 +663,54 @@ class State total += @trash.length total += @prizes.length total + + buyCausesToLose: (player, state, card) -> + if (state.gainsToEndGame() > 1) + return false + if (not card?) + return false + + # One level of recursion is enough for first + [hypState, hypMy] = [this, state.current] + if (this.depth==0) + [hypState, hypMy] = state.hypothetical(player.ai) + + # try to buy this card + # C&P from below + # + [coinCost, potionCost] = card.getCost(this) + hypMy.coins -= coinCost + hypMy.potions -= potionCost + hypMy.buys -= 1 + + hypState.gainCard(hypMy, card, 'discard', true) + card.onBuy(hypState) + + + for i in [hypMy.inPlay.length-1...-1] + cardInPlay = hypMy.inPlay[i] + if cardInPlay? + cardInPlay.buyInPlayEffect(hypState, card) + + goonses = hypMy.countInPlay('Goons') + if goonses > 0 + this.log("...gaining #{goonses} VP.") + hypMy.chips += goonses + # + # C&P until here + + #finish buyPhase + hypState.doBuyPhase() + + # find out if game ended and who if we have won it + hypState.phase = 'start' + if not hypState.gameIsOver() + return false + if ( hypMy.ai.toString() in hypState.getWinners() ) + return false + state.log("Forbid buying #{card}!") + return true + #### Playing a turn # @@ -818,7 +866,16 @@ class State [coinCost, potionCost] = card.getCost(this) if coinCost <= @current.coins and potionCost <= @current.potions buyable.push(card) + + # Don't allow cards that will lose us the game + # + # Not completely optimal because further buys could still cause us winning + # But Geronimoo also does not do better, so for first we should do it like that + # + # Also note that this just cares for the buyPhase, gains by other means (Workshop) are not covered + buyable = (card for card in buyable when (not this.buyCausesToLose(@current, this, card)) ) + # Ask the AI for its choice. this.log("Coins: #{@current.coins}, Potions: #{@current.potions}, Buys: #{@current.buys}") choice = @current.ai.chooseGain(this, buyable) From 3a41c2907bd09510b497a0a2d9a552432f265804 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 8 Jan 2012 16:10:34 +0100 Subject: [PATCH 08/38] Corrected comment --- gameState.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gameState.coffee b/gameState.coffee index da93d59..3d0ef7d 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -869,10 +869,7 @@ class State # Don't allow cards that will lose us the game # - # Not completely optimal because further buys could still cause us winning - # But Geronimoo also does not do better, so for first we should do it like that - # - # Also note that this just cares for the buyPhase, gains by other means (Workshop) are not covered + # Note that this just cares for the buyPhase, gains by other means (Workshop) are not covered buyable = (card for card in buyable when (not this.buyCausesToLose(@current, this, card)) ) From 04bb12da207a0b86a99ddf88c52a67412d446139 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Wed, 11 Jan 2012 02:00:08 -0500 Subject: [PATCH 09/38] Implement Scheme --- basicAI.coffee | 15 +++++++++++++++ cards.coffee | 13 +++++++++++++ gameState.coffee | 10 +++++++--- strategies/SchemeWitch.coffee | 15 +++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 strategies/SchemeWitch.coffee diff --git a/basicAI.coffee b/basicAI.coffee index 2127661..46b3cb7 100644 --- a/basicAI.coffee +++ b/basicAI.coffee @@ -272,6 +272,7 @@ class BasicAI "Conspirator" if my.inPlay.length >= 2 or multiplier > 1 "Familiar" "Highway" + "Scheme" "Wishing Well" "Great Hall" if state.cardInfo.Crossroads not in my.hand "Spice Merchant" if state.cardInfo.Copper in my.hand @@ -455,6 +456,7 @@ class BasicAI "Crossroads" if (not my.crossroadsPlayed) or (my.actions > 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 "Scrying Pool" "Wharf" if my.actions > 0 "Bridge" if my.actions > 0 @@ -881,6 +883,19 @@ class BasicAI return this.upgradeValue(state, [card, gained], my) + # Scheme uses the same priority function as multiplied actions. Good actions + # to multiply this turn are typically good actions to have around next turn. + schemePriority: (state, my) -> + # Project a little of what the state will look like at the beginning of the + # next turn. This keeps multipliedActionPriority from evaluating a card + # as though it will be used in the current (finished) turn. + myNext = {} + myNext[key] = value for key, value of my + myNext.actions = 1 + myNext.buys = 1 + myNext.coins = 0 + this.multipliedActionPriority(state, myNext) + # `scryingPoolDiscardValue` is like `discardValue`, except it strongly # prefers to discard non-actions. scryingPoolDiscardValue: (state, card, my) -> diff --git a/cards.coffee b/cards.coffee index a6cf078..bfeeec1 100644 --- a/cards.coffee +++ b/cards.coffee @@ -2210,6 +2210,19 @@ makeCard 'Salvager', action, { state.current.coins += coins } +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) +} + makeCard 'Scout', action, { cost: 4 actions: +1 diff --git a/gameState.coffee b/gameState.coffee index fbefe39..6f0e937 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -897,6 +897,13 @@ class State @current.duration.push(card) @current.multipliedDurations.splice(i, 1) + + # Handle effects of cleaning up the card, which may involve moving it + # somewhere else. We do this before removing cards from play because + # cards such as Scheme and Herbalist need to consider cards in play. + cardsToCleanup = @current.inPlay.concat() + card.onCleanup(this) for card in cardsToCleanup + # Clean up cards in play. while @current.inPlay.length > 0 card = @current.inPlay[0] @@ -907,9 +914,6 @@ class State @current.duration.push(card) else @current.discard.push(card) - # Handle effects of cleaning up the card, which may involve moving it - # somewhere else. - card.onCleanup(this) # Discard the remaining cards in hand. @current.discard = @current.discard.concat(@current.hand) diff --git a/strategies/SchemeWitch.coffee b/strategies/SchemeWitch.coffee new file mode 100644 index 0000000..9562c2e --- /dev/null +++ b/strategies/SchemeWitch.coffee @@ -0,0 +1,15 @@ +{ + name: 'SchemeWitch' + requires: ['Witch', 'Scheme'] + gainPriority: (state, my) -> [ + "Colony" if my.countInDeck("Platinum") > 0 + "Province" if state.countInSupply("Colony") <= 6 and my.countInDeck("Gold") > 0 + "Witch" if my.countInDeck("Witch") == 0 + "Duchy" if 0 < state.gainsToEndGame() <= 5 + "Estate" if 0 < state.gainsToEndGame() <= 2 + "Platinum" + "Gold" + "Scheme" if my.countInDeck('Scheme') < 2 and my.countInDeck('Silver') > 0 + "Silver" + ] +} From bf2d086dcc2bd6539099b5e1a023459cf1c6e34e Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Fri, 30 Dec 2011 12:07:27 -0500 Subject: [PATCH 10/38] Fix CoffeeScript 1.2.0 incompatibility issues: improper indentation and require order in play.coffee. Issue #56 --- basicAI.coffee | 2 +- play.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basicAI.coffee b/basicAI.coffee index 2127661..2d1b7c4 100644 --- a/basicAI.coffee +++ b/basicAI.coffee @@ -435,7 +435,7 @@ class BasicAI "Treasure Map" "Ambassador" "Throne Room" - ] + ] # `multipliedActionPriority` is similar to `actionPriority`, but is used when # we have played a Throne Room or King's Court. diff --git a/play.coffee b/play.coffee index 5e52366..1267db8 100755 --- a/play.coffee +++ b/play.coffee @@ -3,8 +3,8 @@ # This is the script that you can run at the command line to see how # strategies play against each other. -{State,tableaux} = require './gameState' {BasicAI} = require './basicAI' +{State,tableaux} = require './gameState' fs = require 'fs' coffee = require 'coffee-script' From 4fcc86587e940017f3e5ba27d4b84572b07370b4 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Sat, 14 Jan 2012 01:32:17 -0500 Subject: [PATCH 11/38] Avoid refreshing turn logs when they're not visible Repeatedly running games takes about 1/3 less time when we use this optimization step. See Issue #23. --- web/multiLog.coffee | 6 ++++++ web/play.html | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/web/multiLog.coffee b/web/multiLog.coffee index 83dd0aa..fe39bee 100644 --- a/web/multiLog.coffee +++ b/web/multiLog.coffee @@ -21,6 +21,12 @@ class MultiLog getCurrent: -> @pages[@currentPage-1] + getCurrentPage: -> + @currentPage + + getLastPage: -> + @pages.length + updateOutput: -> if @pages[@currentPage-1]? @outputElt.html(@pages[@currentPage-1]) diff --git a/web/play.html b/web/play.html index 463bb41..33f4080 100644 --- a/web/play.html +++ b/web/play.html @@ -203,7 +203,8 @@

Game logs

singleGameResult = (state) => result = state.getWinners().toString() + " wins!" multiLog.addLineToEnd(result) - multiLog.updateOutput() + # Avoid refreshing the output if it's not visible + multiLog.updateOutput() if multiLog.getCurrentPage() == multiLog.getLastPage() tracker.recordGame(state) errorHandler = (error) => alert(error.message) From e03bd75983a3a929fea8e64702d6b5dd368e2eca Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Sun, 15 Jan 2012 00:29:56 -0500 Subject: [PATCH 12/38] Fix Courtyard's choice of card to put back on deck There were two problems. First, coinLossMargin was using State#coins rather than PlayerState#coins. The former is undefined, so coinLossMargin was incorrectly returning 0. Second, the treasure sorting in putOnDeckPriority was reversed, so the AI would put back the lowest valued treasure it could, rather than the highest. These are now fixed. Fixes #53. --- basicAI.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basicAI.coffee b/basicAI.coffee index 2127661..396b0f4 100644 --- a/basicAI.coffee +++ b/basicAI.coffee @@ -631,7 +631,7 @@ class BasicAI for card in my.hand if (card.isTreasure) and (not (card in treasures)) treasures.push card - treasures.sort( (x, y) -> x.coins - y.coins) + treasures.sort( (x, y) -> y.coins - x.coins) # Get the margin of how much money we're willing to discard. margin = my.ai.coinLossMargin(state) @@ -1100,7 +1100,7 @@ class BasicAI # TODO: do we need an equivalent for potions? coinLossMargin: (state) -> newState = this.pessimisticBuyPhase(state) - coins = newState.coins + coins = newState.current.coins cardToBuy = newState.getSingleBuyDecision() return 0 if cardToBuy is null [coinsCost, potionsCost] = cardToBuy.getCost(newState) From 3b569d710487f1444a52de2c3d38cacaace5386f Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 00:55:41 -0500 Subject: [PATCH 13/38] Move Goons VP logic into its card definition See #21 --- cards.coffee | 8 +++----- gameState.coffee | 6 ------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/cards.coffee b/cards.coffee index a6cf078..9e4bc6e 100644 --- a/cards.coffee +++ b/cards.coffee @@ -971,8 +971,6 @@ makeCard 'Ghost Ship', attack, { transferCardToTop(putBack, opp.hand, opp.draw) } -# Goons: *see Militia* - makeCard 'Jester', attack, { cost: 5 coins: +2 @@ -1018,9 +1016,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, { diff --git a/gameState.coffee b/gameState.coffee index fbefe39..87ca4f7 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -853,12 +853,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: () -> From 2c07bac0ab1c5fd3beff57e1c02e7e54b6ea8252 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 01:13:53 -0500 Subject: [PATCH 14/38] Move Crossroads +action logic into its card definition See #21 --- basicAI.coffee | 4 ++-- cards.coffee | 3 +-- gameState.coffee | 3 --- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/basicAI.coffee b/basicAI.coffee index 2127661..4074396 100644 --- a/basicAI.coffee +++ b/basicAI.coffee @@ -300,7 +300,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. @@ -452,7 +452,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 "Scrying Pool" diff --git a/cards.coffee b/cards.coffee index 9e4bc6e..25fd836 100644 --- a/cards.coffee +++ b/cards.coffee @@ -1565,8 +1565,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 diff --git a/gameState.coffee b/gameState.coffee index 87ca4f7..097fc62 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -45,7 +45,6 @@ class PlayerState @gainedThisTurn = [] @moatProtected = no @tacticians = 0 # number of Tacticians that will go to the duration area - @crossroadsPlayed = 0 @foolsGoldInPlay = no @mayReturnTreasury = yes @turnsTaken = 0 @@ -407,7 +406,6 @@ class PlayerState other.actionStack = @actionStack.slice(0) other.actionsPlayed = @actionsPlayed other.tacticians = @tacticians - other.crossroadsPlayed = @crossroadsPlayed other.ai = @ai other.logFunc = @logFunc other.turnsTaken = @turnsTaken @@ -915,7 +913,6 @@ class State @current.coins = 0 @current.potions = 0 @current.tacticians = 0 - @current.crossroadsPlayed = 0 @current.actionsPlayed = 0 @current.foolsGoldInPlay = no @current.mayReturnTreasury = yes From 43b923f71921d111ca0299ff68297dbf4cf426d2 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 01:28:59 -0500 Subject: [PATCH 15/38] Move Moat protection logic into its card definition See #21 --- cards.coffee | 17 +++++++++-------- gameState.coffee | 24 +++++++++--------------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/cards.coffee b/cards.coffee index 25fd836..342de7c 100644 --- a/cards.coffee +++ b/cards.coffee @@ -149,7 +149,7 @@ 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? @@ -1804,7 +1804,7 @@ makeCard "Horse Traders", action, { state.drawCards(state.current, 1) reactToAttack: - (state, player) -> + (state, player, attackEvent) -> transferCard(c['Horse Traders'], player.hand, player.duration) } @@ -2086,11 +2086,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, { @@ -2248,7 +2249,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) diff --git a/gameState.coffee b/gameState.coffee index 097fc62..a906a8d 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -43,7 +43,6 @@ class PlayerState @duration = [] @setAside = [] @gainedThisTurn = [] - @moatProtected = no @tacticians = 0 # number of Tacticians that will go to the duration area @foolsGoldInPlay = no @mayReturnTreasury = yes @@ -397,7 +396,6 @@ 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 @@ -1170,25 +1168,21 @@ 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 + attackEvent = {} # 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) + 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 + unless attackEvent.blocked + if c.Lighthouse in player.duration + this.log("#{player.ai} is protected by the Lighthouse.") + else + effect(player) #### Bookkeeping # `copy()` makes a copy of this state that can be safely mutated From d386de0b69856816266e7d8950746053319755c2 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 01:42:37 -0500 Subject: [PATCH 16/38] Move Lighthouse protection logic into its card definition See #21 --- cards.coffee | 6 +++++- gameState.coffee | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cards.coffee b/cards.coffee index 342de7c..fe77fec 100644 --- a/cards.coffee +++ b/cards.coffee @@ -671,7 +671,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, { diff --git a/gameState.coffee b/gameState.coffee index a906a8d..5991610 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -1168,21 +1168,22 @@ class State # `attackPlayer` does the work of attacking a particular player, including # handling their reactions to attacks. attackPlayer: (player, effect) -> + # attackEvent gets passed to each reactToAttack method. Any card + # may block the attack by setting attackEvent.blocked to true attackEvent = {} - - # 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) + + # 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) # Apply the attack's effect unless it's been blocked by a card such as # Moat or Lighthouse - unless attackEvent.blocked - if c.Lighthouse in player.duration - this.log("#{player.ai} is protected by the Lighthouse.") - else - effect(player) + effect(player) unless attackEvent.blocked #### Bookkeeping # `copy()` makes a copy of this state that can be safely mutated From 64baaeeb56cab8deca5194f858946c7c516925bb Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 12:19:08 -0500 Subject: [PATCH 17/38] Move Fool's Gold play state into its card definition See #21 --- cards.coffee | 2 +- gameState.coffee | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/cards.coffee b/cards.coffee index fe77fec..0e88335 100644 --- a/cards.coffee +++ b/cards.coffee @@ -470,7 +470,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 diff --git a/gameState.coffee b/gameState.coffee index 5991610..3803625 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -44,7 +44,6 @@ class PlayerState @setAside = [] @gainedThisTurn = [] @tacticians = 0 # number of Tacticians that will go to the duration area - @foolsGoldInPlay = no @mayReturnTreasury = yes @turnsTaken = 0 @@ -397,7 +396,6 @@ class PlayerState other.duration = @duration.slice(0) other.setAside = @setAside.slice(0) other.gainedThisTurn = @gainedThisTurn.slice(0) - other.foolsGoldInPlay = no other.mayReturnTreasury = @mayReturnTreasury other.playLocation = @playLocation other.gainLocation = @gainLocation @@ -912,7 +910,6 @@ class State @current.potions = 0 @current.tacticians = 0 @current.actionsPlayed = 0 - @current.foolsGoldInPlay = no @current.mayReturnTreasury = yes @copperValue = 1 @bridges = 0 From a40c279520335cbd91074afba976c3b02afc1047 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 12:29:53 -0500 Subject: [PATCH 18/38] Move Tactician active/inactive state into its card definition See #21 --- cards.coffee | 9 ++++++--- gameState.coffee | 3 --- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cards.coffee b/cards.coffee index 0e88335..e5a8232 100644 --- a/cards.coffee +++ b/cards.coffee @@ -690,12 +690,15 @@ makeCard 'Tactician', duration, { durationCards: +5 playEffect: (state) -> + # If this is the first time we've played Tactician this turn, reset our count + @activeTacticians = 0 if state.current.countInPlay('Tactician') == 1 + 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++ + @activeTacticians++ discards = state.current.hand state.current.discard = state.current.discard.concat(discards) state.current.hand = [] @@ -704,8 +707,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 @activeTacticians > 0 + @activeTacticians-- else state.log("#{state.current.ai} discards an inactive Tactician.") transferCard(c.Tactician, state.current.duration, state.current.discard) diff --git a/gameState.coffee b/gameState.coffee index 3803625..d49da9c 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -43,7 +43,6 @@ class PlayerState @duration = [] @setAside = [] @gainedThisTurn = [] - @tacticians = 0 # number of Tacticians that will go to the duration area @mayReturnTreasury = yes @turnsTaken = 0 @@ -401,7 +400,6 @@ class PlayerState other.gainLocation = @gainLocation other.actionStack = @actionStack.slice(0) other.actionsPlayed = @actionsPlayed - other.tacticians = @tacticians other.ai = @ai other.logFunc = @logFunc other.turnsTaken = @turnsTaken @@ -908,7 +906,6 @@ class State @current.buys = 1 @current.coins = 0 @current.potions = 0 - @current.tacticians = 0 @current.actionsPlayed = 0 @current.mayReturnTreasury = yes @copperValue = 1 From 15a092e0d9de4a361e848ff4002a1b10246b03b4 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 12:39:40 -0500 Subject: [PATCH 19/38] Move logic for returning Treasury to the deck into its card definition See #21 --- cards.coffee | 15 +++++++++++---- gameState.coffee | 3 --- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cards.coffee b/cards.coffee index e5a8232..0a51b0f 100644 --- a/cards.coffee +++ b/cards.coffee @@ -2427,15 +2427,22 @@ makeCard 'Treasure Map', action, { makeCard 'Treasury', c.Market, { buys: 0 + + playEffect: (state) -> + @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 - + @mayReturnTreasury = no + cleanupEffect: (state) -> - if state.current.mayReturnTreasury + if @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, { diff --git a/gameState.coffee b/gameState.coffee index d49da9c..86f9710 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -43,7 +43,6 @@ class PlayerState @duration = [] @setAside = [] @gainedThisTurn = [] - @mayReturnTreasury = yes @turnsTaken = 0 # To stack various card effects, we'll have to keep track of the location @@ -395,7 +394,6 @@ class PlayerState other.duration = @duration.slice(0) other.setAside = @setAside.slice(0) other.gainedThisTurn = @gainedThisTurn.slice(0) - other.mayReturnTreasury = @mayReturnTreasury other.playLocation = @playLocation other.gainLocation = @gainLocation other.actionStack = @actionStack.slice(0) @@ -907,7 +905,6 @@ class State @current.coins = 0 @current.potions = 0 @current.actionsPlayed = 0 - @current.mayReturnTreasury = yes @copperValue = 1 @bridges = 0 @highways = 0 From b972d8e1538afd4a84369931a32f959216e83a85 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 16:04:08 -0500 Subject: [PATCH 20/38] Make mat implementation more generic Mats are now set up by the card implementations rather than coded into the game state. Haven and Island logic has been updated accordingly and those cards no longer have logic in the game state. See #21 --- cards.coffee | 25 +++++++++++++++++++++---- gameState.coffee | 37 +++++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/cards.coffee b/cards.coffee index 0a51b0f..65b6bc2 100644 --- a/cards.coffee +++ b/cards.coffee @@ -129,6 +129,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? @@ -377,6 +381,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).") @@ -617,20 +625,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) @@ -1140,6 +1153,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' diff --git a/gameState.coffee b/gameState.coffee index 86f9710..31d4d44 100644 --- a/gameState.coffee +++ b/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 @@ -89,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: () -> @@ -380,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) @@ -469,6 +477,11 @@ class State # we screwed up. @totalCards = this.countTotalCards() + # Let cards in the tableau know the game is starting so they can perform + # any necessary initialization + for card in tableau + card.startGameEffect(this) + return this # `setUpWithOptions` is the function I'd like to use as the primary way of setting up From 1b7f9c674c88e4bfb3613ae0de0bb1a3a8e30b8e Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 19:58:29 -0500 Subject: [PATCH 21/38] Add concepts of card-specific state and special supply, clean up Tournament Cards can now specify their own state without the game state needing to have knowledge. Tactician and Treasury needed to be updated to use this because their state could get messed up if the AI player was making hypothetical plays. Tournament no longer needs code in the game state. See #21 --- cards.coffee | 37 +++++++++++++++++++++++++-------- gameState.coffee | 53 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/cards.coffee b/cards.coffee index 65b6bc2..edb14f4 100644 --- a/cards.coffee +++ b/cards.coffee @@ -703,15 +703,18 @@ makeCard 'Tactician', duration, { durationCards: +5 playEffect: (state) -> - # If this is the first time we've played Tactician this turn, reset our count - @activeTacticians = 0 if state.current.countInPlay('Tactician') == 1 + # 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.") - @activeTacticians++ + state.cardState[this].activeTacticians++ discards = state.current.hand state.current.discard = state.current.discard.concat(discards) state.current.hand = [] @@ -720,8 +723,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 @activeTacticians > 0 - @activeTacticians-- + 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) @@ -2346,6 +2349,18 @@ 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] = + prizes: prizes + playEffect: (state) -> # All Provinces are automatically revealed. @@ -2358,10 +2373,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 @@ -2446,7 +2464,8 @@ makeCard 'Treasury', c.Market, { buys: 0 playEffect: (state) -> - @mayReturnTreasury = yes + state.cardState[this] = + mayReturnTreasury: yes buyInPlayEffect: (state, card) -> # FIXME: This is incorrect in one highly unlikely edge case - if you buy @@ -2454,10 +2473,10 @@ makeCard 'Treasury', c.Market, { # 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 - @mayReturnTreasury = no + state.cardState[this].mayReturnTreasury = no cleanupEffect: (state) -> - if @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.") } diff --git a/gameState.coffee b/gameState.coffee index 31d4d44..f4950a1 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -448,8 +448,13 @@ 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"]] + + # A map of Card to state object that allows cards to define lasting state. + @cardState = {} + @tradeRouteMat = [] @tradeRouteValue = 0 @@ -473,15 +478,15 @@ class State @depth = 0 this.log("Tableau: #{tableau}") - # `totalCards` tracks the total number of cards that are in the game. If it changes, - # we screwed up. - @totalCards = this.countTotalCards() - # 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() + return this # `setUpWithOptions` is the function I'd like to use as the primary way of setting up @@ -663,8 +668,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 @@ -958,7 +964,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? @@ -978,12 +984,12 @@ 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 - + else + @specialSupply[card] -= 1 + # Delegate to `handleGainCard` to deal with reactions. this.handleGainCard(player, card, gainLocation) else @@ -1202,6 +1208,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 @@ -1211,9 +1221,27 @@ 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 @@ -1226,7 +1254,6 @@ class State newState.copperValue = @copperValue newState.phase = @phase newState.cache = {} - newState.prizes = @prizes.slice(0) newState From ca88f666f4ae3742d15d51de065a2e0890376dcc Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 16 Jan 2012 20:40:44 -0500 Subject: [PATCH 22/38] Make logic for Highway, Bridge, etc cost reduction more generic See #21 --- cards.coffee | 41 ++++++++++++++++++++++++----------------- gameState.coffee | 20 ++++++++------------ 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/cards.coffee b/cards.coffee index edb14f4..a1347ff 100644 --- a/cards.coffee +++ b/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)] @@ -562,7 +561,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, { @@ -859,12 +865,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, { @@ -1454,9 +1461,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, { @@ -1806,11 +1814,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, { @@ -2090,7 +2096,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] diff --git a/gameState.coffee b/gameState.coffee index f4950a1..8d7d130 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -458,10 +458,11 @@ class State @tradeRouteMat = [] @tradeRouteValue = 0 - @bridges = 0 - @highways = 0 - @princesses = 0 - @quarries = 0 + # 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 @@ -925,10 +926,8 @@ class State @current.potions = 0 @current.actionsPlayed = 0 @copperValue = 1 - @bridges = 0 - @highways = 0 - @princesses = 0 - @quarries = 0 + + @costModifiers = [] #Announce extra turn if @extraturn @@ -1247,10 +1246,7 @@ class State 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 = {} From 47119783dae99039006520eb35d2f0f51be79485 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Wed, 18 Jan 2012 15:36:03 -0500 Subject: [PATCH 23/38] Move logic for Trade Route mat into its card definition See #21 --- cards.coffee | 18 ++++++++++++++++-- gameState.coffee | 19 +++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/cards.coffee b/cards.coffee index a1347ff..a823471 100644 --- a/cards.coffee +++ b/cards.coffee @@ -159,7 +159,9 @@ basicCard = { 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. @@ -2366,6 +2368,7 @@ makeCard 'Tournament', action, { state.specialSupply[prize] = 1 state.cardState[this] = + copy: -> prizes: @prizes.concat() prizes: prizes playEffect: @@ -2398,8 +2401,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, { diff --git a/gameState.coffee b/gameState.coffee index 8d7d130..5e8000e 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -455,9 +455,6 @@ class State # A map of Card to state object that allows cards to define lasting state. @cardState = {} - @tradeRouteMat = [] - @tradeRouteValue = 0 - # 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. @@ -986,24 +983,28 @@ class State # 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. @@ -1244,8 +1245,6 @@ class State newState.trash = @trash.slice(0) newState.current = newPlayers[0] newState.nPlayers = @nPlayers - newState.tradeRouteMat = @tradeRouteMat.slice(0) - newState.tradeRouteValue = @tradeRouteValue newState.costModifiers = @costModifiers.concat() newState.copperValue = @copperValue newState.phase = @phase From 5f0180ba573fcfa70e5ddbf573bf817c481e11ef Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Wed, 18 Jan 2012 16:17:18 -0500 Subject: [PATCH 24/38] Move logic for Young Witch bane card into its card definition See #21 --- cards.coffee | 24 ++++++++++++++++++++++-- gameState.coffee | 10 ++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/cards.coffee b/cards.coffee index a823471..4561bee 100644 --- a/cards.coffee +++ b/cards.coffee @@ -1333,11 +1333,31 @@ 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) + 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) diff --git a/gameState.coffee b/gameState.coffee index 5e8000e..9b6045c 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -466,11 +466,6 @@ class State @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 @@ -520,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 From b456d46dce01a32e19ce13cd9ab489de16ca6afb Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Thu, 19 Jan 2012 19:04:55 -0500 Subject: [PATCH 25/38] Avoid some unnecessary overhead in AI choice function. --- basicAI.coffee | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basicAI.coffee b/basicAI.coffee index 2127661..a0bb0cf 100644 --- a/basicAI.coffee +++ b/basicAI.coffee @@ -82,13 +82,15 @@ class BasicAI choiceSet = {} for choice in choices choiceSet[choice] = choice + + nullable = null in choices # Get the priority list. - priority = priorityfunc.bind(this)(state, my) + priority = priorityfunc.call(this, state, my) # Now look up all the preferences in that list. The moment we encounter # a valid choice, we can return it. for preference in priority - if preference is null and null in choices + if preference is null and nullable return null if choiceSet[preference]? return choiceSet[preference] @@ -103,7 +105,7 @@ class BasicAI if (choice is null) or (choice is no) value = 0 else - value = valuefunc.bind(this)(state, choice, my) + value = valuefunc.call(this, state, choice, my) if value > bestValue bestValue = value bestChoice = choice From 10135e802019cb767d6f0060f2ada080c8407bb0 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Thu, 19 Jan 2012 19:55:51 -0500 Subject: [PATCH 26/38] Notify Young Witch bane card that the game is starting when the card is added --- cards.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cards.coffee b/cards.coffee index 4561bee..a9aad9c 100644 --- a/cards.coffee +++ b/cards.coffee @@ -1350,6 +1350,8 @@ makeCard 'Young Witch', attack, { # 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) -> From a3e7855816ee65f8c0bc980e8e3b1c4c960c9284 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Thu, 19 Jan 2012 20:16:47 -0500 Subject: [PATCH 27/38] Log the time spent playing --- web/play.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/play.html b/web/play.html index 33f4080..163e3bd 100644 --- a/web/play.html +++ b/web/play.html @@ -209,17 +209,21 @@

Game logs

errorHandler = (error) => alert(error.message) + startTime = null + begunPlaying = -> window.playing = true $('#playButton').removeClass('success').addClass('danger') $('#playButton').text('Stop playing') + startTime = new Date() - donePlaying = -> + donePlaying = (count) -> window.playing = false grapher.updateGraphs() tracker.updateScoresOnPage() $('#playButton').removeClass('danger').addClass('success') $('#playButton').text('Start playing') + console?.log?("Played #{count} games in #{(new Date() - startTime) / 1000.0}s"); window.donePlaying = donePlaying playOneGame = -> @@ -242,6 +246,7 @@

Game logs

playUntil = (condition, count) -> + startTime = new Date(); scripts = [leftEd.getValue(), rightEd.getValue()] compiled = compileStrategies(scripts, errorHandler) return if not compiled @@ -259,7 +264,7 @@

Game logs

playLoop = (result) -> singleGameResult(result) if condition() or (playCount >= count) or (not window.playing) - donePlaying() + donePlaying(playCount) else if (not options.fast) then multiLog.addPageQuietly('') nextIteration = -> From b51bf28122c68fa5d759e2a9e15f6defabedf151 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Thu, 19 Jan 2012 22:46:28 -0500 Subject: [PATCH 28/38] Turn slow mode into fast mode This fixes several major UI inefficiencies: * playWeb.coffee triggered output updates that were being suppressed by play.html I removed these. The output updates less frequently and is completely controlled by play.html. Eventually, all of the CoffeeScript in play.html should be merged with playWeb.coffee, but that's a different issue. * We were calling zeroTimeout very frequently. zeroTimeout is fast, but not free. We now call it approximately 4 times a second, enough to keep the browser from timing out, but not enough to cause noticeable performance overhead. * We had released control back to the browser based on the number of games played. We also updated the graphs based on the number of games played. Doing this way, we needed to be pessimistic about AI and browser performance. We now release control when a game is over and we've had control for more than .25s. * We were updating the DOM whenever we added a page to the log, even when we updated "quietly" because we updated the paginator. We now detect if the pagination has changed before updating it. Combined, these changes allow the "slow mode" simulations to run nearly as fast as the fast mode. In Firefox, the UI updates in slow mode cause about 10% runtime overhead. Because of this, I removed the fast mode checkbox. Fixes #23 --- playWeb.coffee | 27 --------------------------- web/multiLog.coffee | 10 +++++++++- web/play.html | 34 ++++++++++++---------------------- 3 files changed, 21 insertions(+), 50 deletions(-) diff --git a/playWeb.coffee b/playWeb.coffee index a04d984..5561a43 100644 --- a/playWeb.coffee +++ b/playWeb.coffee @@ -24,10 +24,6 @@ makeStrategy = (changes) -> ai[key] = value ai -# Setting `fast` to true will takesome shortcuts to play the game -# really quickly. These include -# producing no output, and not returning control to the browser between -# game steps. playGame = (strategies, options, ret) -> ais = (makeStrategy(item) for item in strategies) @@ -38,30 +34,7 @@ playGame = (strategies, options, ret) -> state = new State().setUpWithOptions(ais, options) ret ?= options.log - if options.fast - options.log = () -> - playFast(state, options, ret) - else - window.setZeroTimeout -> playStep(state, options, ret) -playStep = (state, options, ret) -> - if state.gameIsOver() - ret(state) - else - try - state.doPlay() - if state.phase == 'buy' and (not state.extraturn) and options.grapher? - options.grapher.recordMoney(state.current.ai.name, state.current.turnsTaken, state.current.coins) - if state.phase == 'cleanup' and (not state.extraturn) and options.grapher? - options.grapher.recordVP(state.current.ai.name, state.current.turnsTaken, state.current.getVP(state)) - window.setZeroTimeout -> playStep(state, options, ret) - catch err - errorHandler = options.errorHandler ? (alert ? console.log) - errorHandler(err.message) - window.donePlaying() - throw err - -playFast = (state, options, ret) -> until state.gameIsOver() try state.doPlay() diff --git a/web/multiLog.coffee b/web/multiLog.coffee index fe39bee..b8ea414 100644 --- a/web/multiLog.coffee +++ b/web/multiLog.coffee @@ -45,10 +45,16 @@ class MultiLog "next page" updatePaginator: -> + # Avoid updating the DOM if the new pages are the same as the old + pages = @pagesShown() + oldPages = @pagesRendered + if oldPages? and oldPages.length == pages.length and pages[0] == oldPages[0] + return if @currentPage == @renderedPage + prev = "
  • ← Previous
  • " next = "
  • Next →
  • " items = [prev] - for pageNum in this.pagesShown() + for pageNum in pages if pageNum == @currentPage item = "
  • #{pageNum}
  • " else @@ -57,6 +63,8 @@ class MultiLog items.push(next) @paginatorElt.html('
      ' + items.join('') + '
    ') this.updateEvents() + @pagesRendered = pages + @renderedPage = @currentPage updateEvents: -> $('.page').click (event) => diff --git a/web/play.html b/web/play.html index 163e3bd..0384504 100644 --- a/web/play.html +++ b/web/play.html @@ -91,13 +91,6 @@

    Player 2

    Include Colony and Platinum -
  • - -
  • Start playing @@ -236,7 +229,6 @@

    Game logs

    log: log colonies: $('#colonies').is(':checked') randomizeOrder: $('#randomize').is(':checked') - fast: $('#fast').is(':checked') tracker: tracker grapher: grapher } @@ -255,32 +247,30 @@

    Game logs

    log: log colonies: $('#colonies').is(':checked') randomizeOrder: $('#randomize').is(':checked') - fast: $('#fast').is(':checked') tracker: tracker grapher: grapher } playCount = 0 begunPlaying() + timeControlStarted = new Date() playLoop = (result) -> singleGameResult(result) if condition() or (playCount >= count) or (not window.playing) donePlaying(playCount) else - if (not options.fast) then multiLog.addPageQuietly('') - nextIteration = -> - playCount++ - playGame(compiled, options, playLoop) - if (not options.fast) and (playCount % 10 == 0) - grapher.updateGraphs() - if (not options.fast) or (playCount % 20 == 0) - tracker.updateScoresOnPage() - if (not options.fast) or (playCount % 10) == 0 - window.setZeroTimeout(nextIteration) + multiLog.addPageQuietly('') + msWithControl = new Date() - timeControlStarted + playCount++ + if msWithControl > 250 + tracker.updateScoresOnPage() + grapher.updateGraphs() + window.setZeroTimeout -> + timeControlStarted = new Date() + playGame(compiled, options, playLoop) else - # no time to stop and give control to the Web browser! - nextIteration() + playGame(compiled, options, playLoop) - if (not options.fast) then multiLog.addPageQuietly('') + multiLog.addPageQuietly('') playGame(compiled, options, playLoop) $('#playButton').click (event) -> From 5023ded04222c1f47bbed2ba344267dcb9642923 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Fri, 20 Jan 2012 15:50:26 -0500 Subject: [PATCH 29/38] Prevent Array.prototype.toString from interfering with other modules The CoffeeScript compiler depends on the output from Array::toString being formatted in a specific way. Our changes, which wrapped the output in square brackets, were causing the CoffeeScript compiler to double-wrap some arrays (particularly the ones returned by player strategies). This commit makes it so that the change to Array::toString is applied when the program enters Dominiate's top-level API methods and is reverted when program control leaves those methods. Fixes #56 Fixes #59 --- gameState.coffee | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/gameState.coffee b/gameState.coffee index 63a3646..25c3704 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -1380,9 +1380,43 @@ countStr = (list, elt) -> numericSort = (array) -> array.sort( (a, b) -> (a-b) ) -# Make JavaScript's lists not suck. -Array.prototype.toString = -> - '[' + this.join(', ') + ']' +# When modifying built-in methods of core types, we need to play nice with +# other libraries. For instance, our Array#toString method modifies the +# behavior in a way that breaks the CoffeeScript compiler. + +# Modifies built-in methods of core Javascript types in a way that's reversible +modifyCoreTypes = -> + # Make Array#toString output more readable + Array::_originalToString ||= Array::toString + Array::toString = -> + '[' + this.join(', ') + ']' + +# Reverses modifications to core Javascript types +restoreCoreTypes = -> + Array::toString = Array::_originalToString if Array::_originalToString? + delete Array::_originalToString + +# useCoreTypeMods takes an object and the name of a method. It then wraps +# that method so that it correctly uses and restores our core type +# modifications. The modifications are visible within the method body and +# any child method calls, but they are cleaned up when leaving the method +useCoreTypeMods = (object, method) -> + originalMethod = "_original_#{method}" + unless object[originalMethod]? + object[originalMethod] = object[method] + object[method] = -> + try + modifyCoreTypes() + this[originalMethod](arguments...) + finally + restoreCoreTypes() + +# Use our core type modifications within the State object. These three +# methods are the ones called by external functions to set up and play +# a game. +useCoreTypeMods(State::, 'setUpWithOptions') +useCoreTypeMods(State::, 'gameIsOver') +useCoreTypeMods(State::, 'doPlay') # Exports # ------- From 900964911e05de716446aadc7cecf0481dccc7cb Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Jan 2012 20:04:33 +0100 Subject: [PATCH 30/38] Changed Output --- gameState.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameState.coffee b/gameState.coffee index 3d0ef7d..7b27fc6 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -708,7 +708,7 @@ class State return false if ( hypMy.ai.toString() in hypState.getWinners() ) return false - state.log("Forbid buying #{card}!") + state.log("Buying #{card} will cause #{player.ai} to lose the game") return true From 769e5bc341f37a644cac4562c0bed59e2235e544 Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Thu, 26 Jan 2012 02:31:44 -0500 Subject: [PATCH 31/38] Fixing cleanup effects, because the relevant cards are still in play now --- cards.coffee | 6 +++--- gameState.coffee | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cards.coffee b/cards.coffee index 16ab252..80fbd43 100644 --- a/cards.coffee +++ b/cards.coffee @@ -735,7 +735,7 @@ makeCard 'Tactician', duration, { state.cardState[this].activeTacticians-- else state.log("#{state.current.ai} discards an inactive Tactician.") - transferCard(c.Tactician, state.current.duration, state.current.discard) + transferCard(c.Tactician, state.current.inPlay, state.current.discard) state.handleDiscards(state.current, [c.Tactician]) } @@ -1393,7 +1393,7 @@ makeCard 'Alchemist', action, { cleanupEffect: (state) -> if c.Potion in state.current.inPlay - transferCardToTop(c.Alchemist, state.current.discard, state.current.draw) + transferCardToTop(c.Alchemist, state.current.inPlay, state.current.draw) } makeCard 'Apothecary', action, { @@ -2533,7 +2533,7 @@ makeCard 'Treasury', c.Market, { cleanupEffect: (state) -> if state.cardState[this].mayReturnTreasury - transferCardToTop(c.Treasury, state.current.discard, state.current.draw) + transferCardToTop(c.Treasury, state.current.inPlay, state.current.draw) state.log("#{state.current.ai} returns a Treasury to the top of the deck.") } diff --git a/gameState.coffee b/gameState.coffee index 25c3704..2630d39 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -495,7 +495,10 @@ class State # left undefined. # - `log setUpWithOptions: (ais, options) -> - tableau = options.require ? [] + tableau = [] + if options.require? + for card in options.require + tableau.push(c[card]) for ai in ais if ai.requires? for card in ai.requires @@ -896,8 +899,10 @@ class State # Handle effects of cleaning up the card, which may involve moving it # somewhere else. We do this before removing cards from play because # cards such as Scheme and Herbalist need to consider cards in play. - cardsToCleanup = @current.inPlay.concat() - card.onCleanup(this) for card in cardsToCleanup + cardsToCleanup = @current.inPlay.concat().reverse() + for i in [cardsToCleanup.length-1...-1] + card = cardsToCleanup[i] + card.onCleanup(this) # Clean up cards in play. while @current.inPlay.length > 0 From 3edc9a16e215d66782e6fd07673fe7a65aae705f Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Thu, 26 Jan 2012 02:57:07 -0500 Subject: [PATCH 32/38] remove test setup, update the news --- play.coffee | 2 +- web/play.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/play.coffee b/play.coffee index 0761af7..1267db8 100755 --- a/play.coffee +++ b/play.coffee @@ -26,7 +26,7 @@ playGame = (filenames) -> colonies: false randomizeOrder: true log: console.log - require: ['Horse Traders'] + require: [] }) until st.gameIsOver() st.doPlay() diff --git a/web/play.html b/web/play.html index 0384504..636d08e 100644 --- a/web/play.html +++ b/web/play.html @@ -19,7 +19,7 @@

    A simulator for Dominion strategies.

    From c6dc60fca7282cba27d6a6228857c104810eabe7 Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Thu, 26 Jan 2012 03:26:49 -0500 Subject: [PATCH 33/38] link to the evolving branch of Dominiate --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3942bcd..477f9a8 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,12 @@ downloaded from the Internet, you of course run this at your own risk.) Running `windows/compile.bat` should do the Right Thing, but I haven't tested it. See `windows/README` for more information. +Genetic algorithm version +------------------------- +Dr. Mitchell Morris is working on an exciting version of Dominiate that can evolve +new strategies using genetic algorithms. You can find this version at +https://github.com/Narmical/dominiate. + Roadmap ------- Short-term planned features include: From df8cc95d95bc979e53a62b2e5a4db8324d7c2fd2 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 13 Feb 2012 19:32:16 -0500 Subject: [PATCH 34/38] Mark Trader as a reaction card --- cards.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/cards.coffee b/cards.coffee index 81bcd54..d48f287 100644 --- a/cards.coffee +++ b/cards.coffee @@ -2455,6 +2455,7 @@ makeCard "Trade Route", action, { makeCard "Trader", action, { cost: 4 + isReaction: true playEffect: (state) -> trashed = state.requireTrash(state.current, 1)[0] if trashed? From 797a171ab517f7f8c3a1beae44ee81b7c4121051 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Mon, 13 Feb 2012 23:18:57 -0500 Subject: [PATCH 35/38] Improve performance of checking whether a buy causes a loss Before cloning the game state, we first check to see if buying the card in question would cause a pile to empty, and whether the current player would be in the lead after gaining it. This cuts the total Dominiate execution time nearly in half. --- gameState.coffee | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/gameState.coffee b/gameState.coffee index f3ec91b..15aafba 100644 --- a/gameState.coffee +++ b/gameState.coffee @@ -669,11 +669,21 @@ class State total buyCausesToLose: (player, state, card) -> - if (state.gainsToEndGame() > 1) + if not card? || @supply[card] > 1 || state.gainsToEndGame() > 1 return false - if (not card?) + + # Check to see if the player would be in the lead after buying this card + maxOpponentScore = -Infinity + for status in this.getFinalStatus() + [name, score, turns] = status + if name == player.ai.toString() + myScore = score + card.getVP(player) + else if score > maxOpponentScore + maxOpponentScore = score + + if myScore > maxOpponentScore return false - + # One level of recursion is enough for first if (this.depth==0) [hypState, hypMy] = state.hypothetical(player.ai) From 7ab4c62d636600ebe28a5ae2d13a3b1cec54b216 Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Sun, 19 Feb 2012 15:49:40 -0500 Subject: [PATCH 36/38] add Golem to play priority order --- basicAI.coffee | 1 + gh-pages | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/basicAI.coffee b/basicAI.coffee index 13a39e5..b8b0f27 100644 --- a/basicAI.coffee +++ b/basicAI.coffee @@ -276,6 +276,7 @@ class BasicAI "Highway" "Scheme" "Wishing Well" + "Golem" # seems to be reasonable to expect +1 action from Golem "Great Hall" if state.cardInfo.Crossroads not in my.hand "Spice Merchant" if state.cardInfo.Copper in my.hand "Stables" if this.choose('stablesDiscard', state, my.hand.concat([null])) diff --git a/gh-pages b/gh-pages index 082829a..db7cd23 160000 --- a/gh-pages +++ b/gh-pages @@ -1 +1 @@ -Subproject commit 082829a8aa6c50b7f3c8b69d5ae030d0d39533ae +Subproject commit db7cd23d714e44337d15caff03356fe82863ce1d From a27287f573a31e51cc2269d0fa365e2d71a73820 Mon Sep 17 00:00:00 2001 From: strayjohno Date: Thu, 5 Apr 2012 13:09:40 +0200 Subject: [PATCH 37/38] Typo in required cards --- strategies/RoyalBigMoney.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strategies/RoyalBigMoney.coffee b/strategies/RoyalBigMoney.coffee index a813359..2fd3cac 100644 --- a/strategies/RoyalBigMoney.coffee +++ b/strategies/RoyalBigMoney.coffee @@ -1,6 +1,6 @@ { name: 'Royal Big Money' - requires: ['Big Money'] + requires: ['Royal Seal'] gainPriority: (state, my) -> if state.supply.Colony? [ From 5a9c20cfbce33bebfd777073a40bfefdfb406485 Mon Sep 17 00:00:00 2001 From: Rob Speer Date: Sun, 27 May 2012 09:37:18 +0300 Subject: [PATCH 38/38] use local copy of lessc --- Makefile | 2 +- gh-pages | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e3bb4b9..0f24367 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ web-strategies: coffee compileStrategies.coffee css: - lessc web/dominiate.less web/dominiate.css + node_modules/.bin/lessc web/dominiate.less web/dominiate.css doc: docco *.coffee diff --git a/gh-pages b/gh-pages index 082829a..be6bafc 160000 --- a/gh-pages +++ b/gh-pages @@ -1 +1 @@ -Subproject commit 082829a8aa6c50b7f3c8b69d5ae030d0d39533ae +Subproject commit be6bafc32b555a26891d1c40818b1df7471b4afc