rspeer / dominiate Goto Github PK
View Code? Open in Web Editor NEWA simulator for Dominion card game strategies
Home Page: http://rspeer.github.com/dominiate
License: MIT License
A simulator for Dominion card game strategies
Home Page: http://rspeer.github.com/dominiate
License: MIT License
Well, I fixed the blatant bug in Remake, but now I don't understand the decisions it's making. A Remake strategy, for example, will remake Silvers into arbitrary $4 cards.
Doing Apprentice well would probably require looking ahead in the game state and planning more of the turn. But that takes time, so let's try to do Apprentice acceptably instead.
There should be a way to put information after the # in the URL that pre-populates the boxes with specific strategies and settings.
This would go particularly well with a way to save strategies (#18).
In the early game, the decisions are ordinary trashing, but trashing Duchies is useful, possibly even Provinces in the mid-game, and expensive non-victory cards should also be trashed in the late game.
We support them from the command line. We should have an interface for 3+-player games on the Web. This will probably require making the ACE edit box appear differently.
in makeCard "Lookout",
ai_playValue: (state, my) ->
if state.gainsToEndGame >= 5 or state.cardInfo.Curse in my.draw
895
else
-5
It should say
if state.gainsToEndGame() >= 5 or state.cardInfo.Curse in my.draw
since state.gainsToEndGame is a function. Source
The onGain of Nomad Camp is triggered while it's not in the discard pile. Maybe it's triggering for the wrong player?
The hard part of this is that it adds a decision during shuffling, which can happen in the middle of another card resolving, and can result from code that's currently oblivious to the overall game state.
It's possible that we can get a satisfactory result from a simple decision that only knows about the player state.
Each strategy should have an attribute called "require", containing a list of (names of) cards. The strategy is saying that it can't run unless those cards are in the supply.
Then, we can make actual 10-card kingdoms for our simulated games, ensuring that they contain all the cards that the players require. With meaningful kingdoms, we can then create a Bane pile for Young Witch, and a Black Market deck.
The main reason one would want to run multiple games in slow mode is if you're not on Chrome, and therefore running lots of computation in JavaScript without returning control makes the whole browser sad.
We could avoid a lot of unnecessary DOM updates while still giving the diagnostic output of slow mode: update the score tracker less often, update the grapher less often, and stop logging games after the first 10.
At that point, clicking "Fast Mode" means you're okay with intense JavaScript operations that don't return control very often. The label would be "Fast Mode (makes some browsers sluggish)".
This just needs a function for guessing what the opponent wants to buy.
Clearly, Dominiate would be more pleasant to use if you could save strategies to the Internet, retrieve them, and link to them.
I rather like the fact that Dominiate runs without a server (besides GitHub's web server). To be able to save strategies, then, we need an external place we can save to.
My current thought is to use our Wiki on GitHub to store strategies, which requires only a GitHub account (no particular permissions). I've figured out how to automatically generate Wiki posts where the user just needs to click "save". This would require people to have GitHub accounts to save things, though, which may be a mental barrier to entry.
gist.github.com allows anonymous posts, but then the URL you get is an arbitrary number, so you'd have to hold onto that number to save the strategy. I just had a wacky idea involving sending the resulting URL to TinyURL and giving it a name again. One problem with that is that you couldn't revise a strategy under the same name.
And I suppose a system that let you alter existing strategies, completely anonymously, would be vulnerable to griefing.
Any other ideas?
I've been profiling the simulator. Web stuff aside, a very large amount of time is being spent in chooseByPriorityAndValue, particularly when it calls actionPriority.
actionPriority ends up evaluating the relative merits of every action in the game for the current game state every time it is called. This is very wasteful.
In most cases, simple AIs will have a very simple decision, e.g. do I play my Smithy? In many cases, the answer is always yes. As things stand, though, that AI is stuck considering how it'd rank its Smithy against a Spice Merchant, even if Spice Merchant isn't available in the game.
I'm not sure exactly how to fix this without really messing with how AIs are written, but it has a big impact on performance (around 20% of the sim run time, likely more for allocation/GC).
The SillyAI will often play Ambassador because there is a card it wants to trash, such as Potion, but then its ambassadorPriority does not have a way of choosing that card.
This can probably be fixed simply by completing the priority list with a modified version of the trashPriority list.
I'm trying to run my version of Dominate (which I just forked a few hours ago) on my local machine, but I am getting the following error:
$ ./play.coffee strategies/BigMoney.coffee strategies/ChapelWitch.coffee
SyntaxError: In /path/to/dominiate/cards.coffee, multiple object literal properties named "ai_playValue"
at SyntaxError (unknown source)
at Obj.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:1187:19)
at Obj.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Value.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:701:24)
at Value.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:920:29
at Call.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:923:8)
at Call.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Block.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:307:23)
at Block.compileWithDeclarations (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:391:19)
I get the same error when I run make:
$ make
node_modules/.bin/lessc web/dominiate.less web/dominiate.css
coffee -c -j web/playWeb.js playWeb.coffee basicAI.coffee cards.coffee gameState.coffee
SyntaxError: In web/playWeb.js, multiple object literal properties named "ai_playValue"
at SyntaxError (unknown source)
at Obj.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:1187:19)
at Obj.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Value.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:701:24)
at Value.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:920:29
at Call.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:923:8)
at Call.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Block.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:307:23)
at Block.compileWithDeclarations (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:391:19)
make: *** [web-coffee] Error 1
Version info:
$ coffee -v
CoffeeScript version 1.3.3
$ node -v
v0.6.18
The simulator should be able to avoid gaining a card that will cause it to lose the game. It should also try to gain/buy cards to win the game when possible (although some of the more interesting ways to do this are things it could only do by the relatively slow process of searching the game tree).
The interesting part is guessing what your opponent wants.
I don't want to use Geronimoo's trick of peeking into the opponent's mind.
Strategy1 here is buying Bishops as soon as possible, and here it's trashing Silver instead of Estate:
== Strategy1's turn 1 ==
Strategy1 plays Copper.
Strategy1 plays Copper.
Strategy1 plays Copper.
Strategy1 plays Copper.
Coins: 4, Potions: 0, Buys: 1
Strategy1 buys Bishop.
Strategy1 draws 5 cards: [Copper, Estate, Copper, Copper, Estate].
...
== Strategy1's turn 2 ==
Strategy1 plays Copper.
Strategy1 plays Copper.
Strategy1 plays Copper.
Coins: 3, Potions: 0, Buys: 1
Strategy1 buys Silver.
(Strategy1 shuffles.)
Strategy1 draws 5 cards: [Estate, Bishop, Copper, Copper, Silver].
...
== Strategy1's turn 3 ==
Strategy1 plays Bishop.
...gaining 1 VP.
Strategy1 trashes Silver.
...gaining 1 VP.
Strategy2 trashes Estate.
Strategy1 plays Copper.
Strategy1 plays Copper.
Coins: 3, Potions: 0, Buys: 1
Strategy1 buys Silver.
Strategy1 draws 5 cards: [Estate, Estate, Copper, Copper, Copper].
Scheme needs its own decision at the beginning of the cleanup phase.
If a player is being forced to gain a Card (ie they played Haggler), but the player doesn't want to gain any of the available choices (ie they bought a Silver with the Haggler in play, but don't want a Copper or Curse), the player gains a Curse. Might not always be Curse, but that's what showed up in my testing of Haggler. (maybe because Curse is the first card created in cards.coffee, so it's at the front of the card list?)
Obviously for Haggler this can be avoided by more sophisticated play rules, but that would not get rid of this underlying issue.
Turns out adding (state.supply[card] ? 0) > 0 into upgradeChoices, instead of my current if not card2.isPrize band-aid will work. It looks like when the card object is created, it is added to state.supply--there are 10 of each of the prizes in the supply, and 30 of Diadem (since it is derived from Silver which is set to start at 30 in the supply), so anything that checks the supply will always find cards there (like the proposed fix for upgradeChoices). This problem also affects Ambassador; the Ambassador player can return a Prize (or ostensibly a card gained from the Black Market) and cause the other players to gain it.
For example:
Set a DoubleAmbassador player against a TournamentPlayer, have them each want to gain exactly 1 Remodel. Have the Ambassador player want to gain a Prize, like Bag of Gold, but not want to gain any Tournaments. Put "Bag of Gold,0" at the top of ambassadorPriority and make sure Ambassador is higher in actionPriority than Bag of Gold.
With those settings, you will see DoubleAmbassador Remodeling Coppers and Estates into Bag of Gold, and returning Bag of Gold when possible. TournamentPlayer will Remodel Coppers and Estates into Followers.
I'm working on it when I can. In practice it looks like the greatest danger will be a Tournament player using upgrading type cards--Tournament/Ambassador players probably won't be wanting to return Prizes anyway.
Example:
ChapelPoorHouseThroneRoom draws 5 cards: [Poor House, Throne Room, Throne Room, Poor House, Chapel].
...
== ChapelPoorHouseThroneRoom's turn 11 ==
ChapelPoorHouseThroneRoom plays Throne Room.
...playing Throne Room (1 of 2).
...playing Poor House (1 of 2).
ChapelPoorHouseThroneRoom reveals the hand ([Poor House, Chapel]).
...playing Poor House (2 of 2).
ChapelPoorHouseThroneRoom reveals the hand ([Poor House, Chapel]).
...playing Poor House (2 of 2).
ChapelPoorHouseThroneRoom reveals the hand ([Poor House, Chapel]).
Coins: 12, Potions: 0, Buys: 1
ChapelPoorHouseThroneRoom buys Province.
(ChapelPoorHouseThroneRoom shuffles.)
ChapelPoorHouseThroneRoom draws 5 cards: [Province, Poor House, Province, Chapel, Throne Room].
When the Throne Room is played on the Throne Room, the second play of the nested TR becomes a play of Poor House instead. The root of this issue is the line action = state.current.ai.choose('multiplied', state, choices)
in the King's Court implementation. It is interpreted as an assignment to a global variable.
It seems that when "state.current" is called inside of a cards getVP()-method 'current' always points to the first bot (the bot that finishes the game first).
This happens when getVP is called from within getFinalStatus().
The result is that wrong Points are given for example if the second Player uses gardens since state.current.getDeck().length will return the amount of cards of the first bot instead of the second bot.
I guess this affects all cards which use state.current inside the getVP method.
The OBM Courtyard strategy plays far from optimally. If it has $7 in hand, for example, it doesn't seem to return a copper to its deck, even though the comments in the putOnDeckPriority say that it should.
The play effect is straightforward, but there can be lots of possible choices for the on-gain decision.
See forum post here: http://forum.dominionstrategy.com/index.php?topic=619.msg20870#msg20870
Running play.coffee against CoffeeScript v1.1.0 works fine. Running against v1.2.0 producs a few exceptions:
Trashing is now a specific action. If an effect trashes a card without using state.doTrash, it should be sure to append to the state.trash list itself. Otherwise, in a very realistic sense, it's dropping the card on the floor.
With trashing handled more consistently, we can now set the total number of cards in the game as an invariant and check it every turn. If the number decreases, cards are being dropped on the floor (and if it increases, cards are being duplicated).
While refactoring trashing, I caught a couple of cases where cards were being dropped (mostly by attempts to mutate a list with .concat, which does nothing). There are almost certainly other cases. We should create and check this invariant, and close this bug when we can get through many thousands of games while guaranteeing that no cards are lost.
The player should be able to draw a reaction card using Secret Chamber and subsequently react with that card. We don't currently handle this. Simultaneously, the player needs to be able to react with, say, Moat, then react with Secret Chamber, placing Moat on top of the deck.
Cards should also be able to react to the same event twice. I can react with Secret Chamber, draw a Moat, react with Moat, then react with Secret Chamber again, placing Moat back on deck.
The AI should be more careful about which treasures it chooses to play when buying Mint:
== Mint AI's turn 4 ==
Mint AI plays Silver.
Mint AI plays Quarry.
Mint AI plays Copper.
Mint AI plays Copper.
Mint AI plays Copper.
Coins: 6, Potions: 0, Buys: 1
Mint AI buys Mint.
...trashing a Copper.
...trashing a Copper.
...trashing a Copper.
...trashing a Quarry.
...trashing a Silver.
(Mint AI shuffles.)
Mint AI draws 5 cards: [Copper, Copper, Mint, Copper, Estate].
One way that Dominiate can really shine is allowing people to playtest new cards they propose (for example, on the Variants forum of forum.dominionstrategy.com).
People can run arbitrary CoffeeScript code on top of Dominiate already; that's almost everything they need to define new cards. We should have a system that supports this -- such as an optional box to define new cards in and get them added to the tableau. Probably depends on #21, for sanity.
This data would become useful for someone trying to build a general ai that reacts.
For example, BM_Library very frequently beats DoubleMilitia so we would add a property to BM_Library as such:
counters: ['DoubleMilitia']
This information is useful to an experimenter trying to get ideas, just browsing and running the strategies.
This information may not be immediately used by this project, but could become useful to other projects.
Since everything counters SillyAI no need to mention what counters SillyAI.
This issue could be closed when at least half the strategies have recognized counters.
The counters list could be ordered in most significant counters to less significant.
I was playing around with Vault today and noticed two poor choices. The following scenarios demonstrate the problem:
Scenario 1:
I have a Vault, two Golds, a Silver, and an Estate in my hand
I should not play Vault because it likely won't help me, but it may help my opponent
Scenario 2:
I have five Coppers in my hand
I have no Gold in my deck and perhaps no actions
My opponent plays Vault
I should choose not to discard 2 cards, because it's unlikely whatever I draw will make up for the loss of $2
The AI makes the wrong choice in both of these scenarios. The first one is probably Vault-specific. The second one likely applies to other cards as well.
There's a spot in @Mats reserved for the Native Village mat. The only hard part is going to be deciding when to take cards off the mat.
Hi rspeer,
after some try-and-error I found a Big Money variant, which is superior to BankWharf unless in Colony/Platinum games.
Bank + Ill-Gotten Gains work very well together.
{
name: 'BankIGG'
author: 'Spixi'
requires: ['Bank','Ill-Gotten Gains']
gainPriority: (state, my) -> [
"Colony" if my.countInDeck("Platinum") > 0
"Province"
"Duchy" if state.gainsToEndGame() <= 4
"Estate" if state.gainsToEndGame() <= 2
"Ill-Gotten Gains" if state.countInSupply("Curse") >= 1
"Bank"
"Platinum"
"Gold"
"Silver"
"Copper" if state.gainsToEndGame() <= 3
]
}
What do you think about this strategy? Should we include it to the predefined strategies?
After the AI has put the first card on the deck, it doesn't know where the second one is.
The hard part is constructing the list of possible choices, taking into account price levels where there are no cards.
This got easier now that we have a well-defined method for choosing the Supply.
I believe the reaction effect of Secret Chamber may be difficult to get right, but it will help to assume that we only want to reveal Secret Chamber once.
Some things to take into account:
The decision of whether to discard your hand can probably reuse code from Navigator, but this needs to be weighed against +$2 somehow.
If the player draws 0 cards using Courtyard, the game crashes instead of taking "null" for an answer.
There should be a way for a card to store card-specific information in its own piece of the game state, instead of arbitrarily-named properties such as state.current.tacticians.
In addition to all the weird edge cases, an AI that's good at playing Possession will have to have an entirely different set of decision values when it's Possessing someone. I fully expect this to be the last card implemented.
First time trying this simulator, but no matter what strategies I choose, they run forever and don't seem to ever buy anything.
$ ./play.coffee strategies/SillyAI.coffee strategies/SillyAI.coffee
strategies/SillyAI.coffee
strategies/SillyAI.coffee
(SillyAI shuffles.)
SillyAI draws 5 cards: [Estate, Estate, Estate, Copper, Copper].
(SillyAI shuffles.)
SillyAI draws 5 cards: [Copper, Copper, Estate, Copper, Copper].
Tableau: [Pawn, Workshop, Venture, Nomad Camp, Herbalist, Pirate Ship, Scout, Tactician, Peddler, Quarry]
== SillyAI's turn 1 ==
SillyAI plays Copper.
SillyAI plays Copper.
Coins: 2, Potions: 0, Buys: 1
SillyAI draws 5 cards: [Copper, Copper, Copper, Copper, Copper].
== SillyAI's turn 1 ==
SillyAI plays Copper.
SillyAI plays Copper.
SillyAI plays Copper.
SillyAI plays Copper.
Coins: 4, Potions: 0, Buys: 1
SillyAI draws 5 cards: [Copper, Copper, Copper, Estate, Estate].
== SillyAI's turn 2 ==
SillyAI plays Copper.
SillyAI plays Copper.
SillyAI plays Copper.
SillyAI plays Copper.
SillyAI plays Copper.
Coins: 5, Potions: 0, Buys: 1
(SillyAI shuffles.)
SillyAI draws 5 cards: [Estate, Copper, Copper, Copper, Copper].
== SillyAI's turn 2 ==
SillyAI plays Copper.
SillyAI plays Copper.
SillyAI plays Copper.
Coins: 3, Potions: 0, Buys: 1
(SillyAI shuffles.)
SillyAI draws 5 cards: [Copper, Copper, Estate, Copper, Copper].
Pirate Ship when played doesn't choose to use it's money option until it reaches $5, which leads to it doing things like this:
== DoublePirateShip's turn 9 ==
DoublePirateShip plays Pirate Ship.
...attacking the other players.
...DoubleJack reveals [Copper, Jack of All Trades].
...DoublePirateShip trashes DoubleJack's Copper.
...DoubleJack discards [Jack of All Trades].
...DoublePirateShip takes a Coin token (4 on the mat).
DoublePirateShip plays Silver.
DoublePirateShip plays Silver.
DoublePirateShip plays Copper.
Coins: 5, Potions: 0, Buys: 1
DoublePirateShip buys Silver.
(DoublePirateShip shuffles.)
DoublePirateShip draws 5 cards: [Silver, Pirate Ship, Copper, Estate, Estate].
And this:
== DoublePirateShip's turn 11 ==
DoublePirateShip plays Pirate Ship.
...attacking the other players.
...DoubleJack reveals [Silver, Silver].
...DoublePirateShip trashes DoubleJack's Silver.
...DoubleJack discards [Silver].
...DoublePirateShip takes a Coin token (5 on the mat).
DoublePirateShip plays Silver.
DoublePirateShip plays Copper.
DoublePirateShip plays Copper.
Coins: 4, Potions: 0, Buys: 1
DoublePirateShip buys Silver.
DoublePirateShip draws 5 cards: [Silver, Copper, Silver, Copper, Copper].
Attacking other players when it should be buying Provinces.
After playing a second Minion the other player discards his had although he has only 4 cards in hand. Other players are only affected by Minion attacks if they have 5 or more cards in hand.
== Minion Chain's turn 10 ==
Minion Chain plays Minion.
Minion Chain discards the hand.
(Minion Chain shuffles.)
Minion Chain draws 4 cards: [Province, Minion, Copper, Minion].
Big Money discards the hand.
Big Money draws 4 cards: [Silver, Silver, Estate, Silver].
Minion Chain plays Minion.
Minion Chain discards the hand.
Minion Chain draws 4 cards: [Copper, Estate, Silver, Estate].
Big Money discards the hand.
Big Money draws 4 cards: [Estate, Copper, Gold, Copper].
Minion Chain plays Copper.
Minion Chain plays Silver.
Coins: 3, Potions: 0, Buys: 1
Coin Tokens left: 0
Minion Chain buys Silver.
Minion Chain draws 5 cards: [Copper, Silver, Copper, Copper, Estate].
Ideally, Forge will prune its decisions so it doesn't have to consider all 2^n possibilities. Lots of the possibilities will be equivalent except for $0 cards, or will be pointless because they bring the total over $8 or $11.
For Swindler in particular, we probably want to have a global list of cards ranked by their goodness. We could replace gainValue with it.
Giving your opponent more terminals than they can handle is also a good thing to do.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.