Sunday, September 16, 2012

Programming Dominion

I recently learned to play Dominion, a game that spawned a genre known as deck-building card games. I’m a terrible player. While suffering defeats at the hands of a simple AI, I realized I might have more fun writing a Dominion-playing program.

Implementing just the basic rules is a boring exercise. Luckily, Dominion is a self-modifying game. For example, each turn, you’re supposed to start with one Action and 5 cards in your hand, but there are ways of increasing your Action count, or changing the number of cards in your hand.

Moreover, rule modifications interact with one another, further increasing complexity. For example, playing Witch causes other players to gain a Curse card, but not if the supply of Curse cards is exhausted, or a player is holding a Moat. Or take Throne Room, which plays another Action card twice. How can we design software to handle so many special cases?

Of course, sufficient spaghetti can get anything working. But we should try to minimize mess; ideally the logic for each card should be as isolated as possible. It’d awful if, say, Throne Room required us to bury code somewhere in the Action-playing routine so it runs twice instead of once.

Dominion in Go

I’m reasonably pleased with my first attempt. For the simplest cards, the logic is completely contained in a string, in a tiny domain-specific language:

Village,3,Action,+C1,+A2
Woodcutter,3,Action,+B1,$2

Less trivial cards require a bit more:

case "Feast":
add(func(game *Game) {
p := game.NowPlaying()
game.trash = append(game.trash, p.played[len(p.played)-1])
p.played = p.played[:len(p.played)-1]
pickGain(game, 5)
})

And that’s it! To add a card, just one string, and maybe one block of code. As time passed, it became easier to add new cards. For some cards, it was more like data entry than programming.

Moat is an exception. As the only Reaction card in the Base set, rather than figure out a clean way to implement it, I sprinkle ad hoc code here and there to get it working. If I were to add more Reaction cards, I’d factor out the common parts. There’s no reason to do so pre-emptively. In fact, that’s what happened with other cards: I would only refactor once there was duplicate code to eliminate.

Intrepid readers can browse my git repo: https://github.com/blynn/gominion.git

But beware. It’s all in one untidy monolithic file, the UI is horrible, and the AI is stupid, though it still beats me when I get too greedy with Action cards! The game state is shared by all players. If network play were added, to prevent cheating, information would need to be more tightly controlled.

I have no plans to work much more on this, as many mature implementations already exist, and Rio Grande Games plans to release an official online version soon. All the same, I highly recommend learning to play Dominion, and then trying to program it. Both are enlightening experiences.