Permalink
Browse files

Merge branch 'master' into aiplayvalue

Conflicts:
	basicAI.coffee
	cards.coffee
  • Loading branch information...
2 parents 5228fd4 + 1d01a03 commit 9662d85709e7c685bbe57ceaa4d79b56ef19fa0e @rspeer committed May 27, 2012
Showing with 504 additions and 220 deletions.
  1. +1 −1 Makefile
  2. +6 −0 README.md
  3. +27 −8 basicAI.coffee
  4. +4 −4 card_list.txt
  5. +192 −53 cards.coffee
  6. +210 −96 gameState.coffee
  7. +1 −1 gh-pages
  8. +2 −2 play.coffee
  9. +0 −27 playWeb.coffee
  10. +1 −1 strategies/RoyalBigMoney.coffee
  11. +15 −0 strategies/SchemeWitch.coffee
  12. +8 −0 testSimulation.coffee
  13. +15 −1 web/multiLog.coffee
  14. +22 −26 web/play.html
View
2 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
View
6 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:
View
35 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
@@ -274,7 +276,9 @@ class BasicAI
"Conspirator" if my.inPlay.length >= 2 or multiplier > 1
"Familiar"
"Highway"
+ "Scheme"
"Wishing Well"
+ "Golem" # seems to be reasonable to expect +1 action from Golem
"Great Hall" if state.cardInfo.Crossroads not in my.hand
"Spice Merchant" if state.cardInfo.Copper in my.hand
"Stables" if this.choose('stablesDiscard', state, my.hand.concat([null]))
@@ -302,7 +306,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. (500-599)
- "Crossroads" unless my.crossroadsPlayed
+ "Crossroads" unless my.countInPlay(state.cardInfo.Crossroads) > 0
"Great Hall"
# 8: card-cycling that might improve the hand. (400-499)
@@ -391,6 +395,7 @@ class BasicAI
"Trader" if wantsToTrash >= multiplier
"Trade Route" if wantsToTrash >= multiplier
"Mint" if my.ai.choose('mint', state, my.hand) # 140
+ "Secret Chamber"
"Pirate Ship"
"Noble Brigand"
"Thief"
@@ -436,7 +441,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.
@@ -453,9 +458,10 @@ 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
"Scrying Pool"
"Wharf" if my.actions > 0
"Bridge" if my.actions > 0
@@ -632,7 +638,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)
@@ -882,6 +888,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) ->
@@ -1101,7 +1120,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)
View
8 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)
View
245 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,18 @@ 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 the duration pile and an opponent plays an attack?
+ durationReactToAttack: (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.
@@ -386,6 +393,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).")
@@ -480,7 +491,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
@@ -564,7 +575,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, {
@@ -627,20 +645,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)
@@ -686,7 +709,11 @@ makeCard 'Lighthouse', duration, {
durationCoins: +1
aiPlayValue: (state, my) -> 715
- # The protecting effect is defined in gameState.
+ durationReactToAttack: (state, player, attackEvent) ->
+ # Don't bother blocking the attack if it's already blocked (avoid log spam)
+ unless attackEvent.blocked
+ state.log("#{player.ai} is protected by the Lighthouse.")
+ attackEvent.blocked = true
}
makeCard 'Outpost', duration, {
@@ -701,12 +728,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 = []
@@ -715,11 +748,11 @@ 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)
+ transferCard(c.Tactician, state.current.inPlay, state.current.discard)
state.handleDiscards(state.current, [c.Tactician])
}
@@ -853,12 +886,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, {
@@ -991,8 +1025,6 @@ makeCard 'Ghost Ship', attack, {
transferCardToTop(putBack, opp.hand, opp.draw)
}
-# Goons: *see Militia*
-
makeCard 'Jester', attack, {
cost: 5
coins: +2
@@ -1041,9 +1073,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, {
@@ -1065,6 +1097,7 @@ makeCard "Minion", attack, {
state.attackOpponents (opp) ->
c['Minion'].discardAndDraw4(state, opp)
else
+ state.attackOpponents (opp) -> null
player.coins += 2
aiPlayValue: (state, my) ->
@@ -1160,9 +1193,14 @@ 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'
+ state.attackOpponents (opp) -> null
state.current.coins += state.current.mats.pirateShip
state.log("...getting +$#{state.current.mats.pirateShip}.")
else if choice is 'attack'
@@ -1331,11 +1369,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)
@@ -1369,7 +1429,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)
aiPlayValue: (state, my) -> 785
}
@@ -1407,7 +1467,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)
@@ -1467,9 +1527,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, {
@@ -1615,8 +1676,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
@@ -1833,11 +1893,10 @@ 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
+
aiPlayValue: (state, my) -> 750
}
@@ -1859,7 +1918,7 @@ makeCard "Horse Traders", action, {
state.drawCards(state.current, 1)
reactToAttack:
- (state, player) ->
+ (state, player, attackEvent) ->
transferCard(c['Horse Traders'], player.hand, player.duration)
}
@@ -2155,7 +2214,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]
@@ -2178,11 +2238,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, {
@@ -2302,6 +2363,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
@@ -2328,6 +2402,35 @@ makeCard 'Scout', action, {
}
+# Secret Chamber
+# Initial code by Jorbles
+# This is far from optimal, but I believe it does what the card
+# is supposed to do without breaking any rules. I may have to come
+# back to this when my coffee skills are stronger. And I have a
+# greater understanding of how discards are decided. Ideally, the
+# code for discards should be different depending on the type of
+# attack and the total money already in hand.
+
+makeCard "Secret Chamber", action, {
+ cost: 2
+ isReaction: true
+
+ playEffect: (state) ->
+ discarded = state.allowDiscard(state.current, Infinity)
+ state.log("...getting +$#{discarded.length} from the Secret Chamber.")
+ state.current.coins += discarded.length
+
+ reactToAttack: (state, player, attackEvent) ->
+ state.log("#{player.ai.name} reveals a Secret Chamber.")
+ state.drawCards(player, 2)
+ card = player.ai.choose('putOnDeck', state, player.hand)
+ if card isnt null
+ state.doPutOnDeck(player, card)
+ card = player.ai.choose('putOnDeck', state, player.hand)
+ if card isnt null
+ state.doPutOnDeck(player, card)
+}
+
makeCard 'Shanty Town', action, {
cost: 3
actions: +2
@@ -2404,6 +2507,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.
@@ -2416,10 +2532,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
@@ -2435,12 +2554,24 @@ 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, {
cost: 4
+ isReaction: true
playEffect: (state) ->
trashed = state.requireTrash(state.current, 1)[0]
if trashed?
@@ -2506,15 +2637,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
- transferCardToTop(c.Treasury, state.current.discard, state.current.draw)
- state.log("#{state.current.ai} returns a Treasury to the top of the deck.")
+ if state.cardState[this].mayReturnTreasury
+ transferCardToTop(c.Treasury, state.current.inPlay, state.current.draw)
+ state.log("#{state.current.ai} returns a Treasury to the top of the deck.")
aiPlayValue: (state, my) -> 765
}
View
306 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: () ->
@@ -293,7 +293,7 @@ class PlayerState
drawCards: (nCards) ->
drawn = this.getCardsFromDeck(nCards)
- @hand = @hand.concat(drawn)
+ Array::push.apply @hand, drawn
this.log("#{@ai} draws #{drawn.length} cards: #{drawn}.")
return drawn
@@ -385,29 +385,27 @@ 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)
other.discard = @discard.slice(0)
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()
@@ -494,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
@@ -514,11 +518,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,9 +663,69 @@ 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
+
+ buyCausesToLose: (player, state, card) ->
+ if not card? || @supply[card] > 1 || state.gainsToEndGame() > 1
+ return false
+
+ # 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)
+ else
+ return false
+
+ # 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("Buying #{card} will cause #{player.ai} to lose the game")
+ return true
+
#### Playing a turn
#
@@ -818,7 +881,13 @@ 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
+ #
+ # 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)
@@ -853,12 +922,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: () ->
@@ -897,6 +960,15 @@ 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().reverse()
+ for i in [cardsToCleanup.length-1...-1]
+ card = cardsToCleanup[i]
+ card.onCleanup(this)
+
# Clean up cards in play.
while @current.inPlay.length > 0
card = @current.inPlay[0]
@@ -907,9 +979,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)
@@ -920,16 +989,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
@@ -965,7 +1028,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?
@@ -985,27 +1048,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.
@@ -1179,25 +1246,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
+ # 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)
+
+ for card in reactionCards
+ card.reactToAttack(this, player, 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)
+ for card in player.duration
+ card.durationReactToAttack(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
@@ -1212,6 +1276,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
@@ -1221,22 +1289,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
@@ -1370,9 +1450,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
# -------
2 gh-pages
@@ -1 +1 @@
-Subproject commit 265878b69c4dc5011e3c8e081738c4a34843f3e3
+Subproject commit be6bafc32b555a26891d1c40818b1df7471b4afc
View
4 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'
@@ -26,7 +26,7 @@ playGame = (filenames) ->
colonies: false
randomizeOrder: true
log: console.log
- require: ['Tunnel', 'Venture']
+ require: []
})
until st.gameIsOver()
st.doPlay()
View
27 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()
View
2 strategies/RoyalBigMoney.coffee
@@ -1,6 +1,6 @@
{
name: 'Royal Big Money'
- requires: ['Big Money']
+ requires: ['Royal Seal']
gainPriority: (state, my) ->
if state.supply.Colony?
[
View
15 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"
+ ]
+}
View
8 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')
View
16 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])
@@ -39,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 = "<li class='#{this.prevButtonClass()}'><a href='#'>&larr; Previous</a></li>"
next = "<li class='#{this.nextButtonClass()}'><a href='#'>Next &rarr;</a></li>"
items = [prev]
- for pageNum in this.pagesShown()
+ for pageNum in pages
if pageNum == @currentPage
item = "<li class='active page'><a href='#'>#{pageNum}</a></li>"
else
@@ -51,6 +63,8 @@ class MultiLog
items.push(next)
@paginatorElt.html('<ul>' + items.join('') + '</ul>')
this.updateEvents()
+ @pagesRendered = pages
+ @renderedPage = @currentPage
updateEvents: ->
$('.page').click (event) =>
View
48 web/play.html
@@ -19,7 +19,7 @@ <h2 class="tagline">A simulator for Dominion strategies.</h2>
</div>
<div id="news">
- <b>News</b>: Only 13 cards left to implement! The remaining cards are now listed on our
+ <b>News</b>: Scheme and the no-suicide rule are in. 11 cards left! The remaining cards are now listed on our
<a href="https://github.com/rspeer/dominiate/issues">bug tracker</a>.
</div>
@@ -91,13 +91,6 @@ <h2 class="tagline">A simulator for Dominion strategies.</h2>
<span>Include Colony and Platinum</span>
</label>
</li>
- <li>
- <label>
- <input type="checkbox" name="optionsCheckboxes" value="fast"
- id="fast" />
- <span>Play fast (without much output)</span>
- </label>
- </li>
</ul>
<a id="playButton" href="#" class="btn success">
Start playing
@@ -203,22 +196,27 @@ <h2 class="tagline">A simulator for Dominion strategies.</h2>
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)
+ 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 = ->
@@ -231,7 +229,6 @@ <h2 class="tagline">A simulator for Dominion strategies.</h2>
log: log
colonies: $('#colonies').is(':checked')
randomizeOrder: $('#randomize').is(':checked')
- fast: $('#fast').is(':checked')
tracker: tracker
grapher: grapher
}
@@ -241,6 +238,7 @@ <h2 class="tagline">A simulator for Dominion strategies.</h2>
playUntil = (condition, count) ->
+ startTime = new Date();
scripts = [leftEd.getValue(), rightEd.getValue()]
compiled = compileStrategies(scripts, errorHandler)
return if not compiled
@@ -249,32 +247,30 @@ <h2 class="tagline">A simulator for Dominion strategies.</h2>
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()
+ 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) ->

0 comments on commit 9662d85

Please sign in to comment.