Designing a robust Dominion game engine: Part 2 The game engine

Having thought about how to implement a card structure, we can move to the next step of designing our game engine. A game engine allows the players to take actions. The engine then calculates and presents the results. Seeing the results, the player can take actions, and the cycle restarts. The cycle is this:

  1. Present current state
  2. Accept player actions
  3. Calculate outcomes
  4. Repeat

In our case, the engine should handle the three turn phases of the game for each player. In the Action phase, the player may play an Action card. In the Buy phase, the player may buy a card. In the Clean-up phase, the player must discard his hand and played cards and draw five cards. To start this cycle, the player starts with five drawn cards.

The Clean-up phase has no player interaction and can. Therefore, we handle it in one go in the Calculate outcomes step without the need to ask the player to make a choice:

  1. Move all cards from the play area onto the discard pile
  2. Place all remaining hand cards onto the discard pile
  3. Draw 5 cards from the deck.

As always, if there is no card left to draw in the deck, reshuffle the deck and replace the (empty) deck with it.

The Buy and the Action phase offer options to the player and so they are split into sub steps which get repeated over and over. The Action phase boils down to:

  1. Pick Action card from hand
  2. Resolve the card
  3. If the card gives back at least one action, go to 1., else the Action phase ends.

The Buy phase plays similarly but without the second resolve card step:

  1. Calculate coin value
  2. Pick a card with its cost lower or equal to the currency value in hand
  3. Place the bought card into the play area
  4. If the player has another buy and still some money left, go to 1., else the Buy phase end.

Both the Buy and the Action phase are optional. The player does not have to play or buy a card. At the end of the Buy phase, we need to check if the game is over by having depleted the Provinces stack (or the Colony stack) or three other Supply piles.

Using these general descriptions of the game flow we now define our objects and classes. As the phases should be immutable and no different for each player, we describe each stage as a Class from which we call its static methods. However, the turn-state itself is mutable. The player starts with five cards, one buy and one action which will change the current state. Action cards may allow for card draw or more actions/purchases which all modify this hand and his options. For this, we implement a turn-state class and each turn we instantiate an object which gets altered over the course of his (and possibly other players turn).

What we described here is a finite-state machine. We move from game states to game state by taking actions. Not wanting to play an action card and directly buying a card is also an action on the state machine. The modifications of the turn-state are the outputs of our finite-state machine. We can use the State design pattern to implement our game engine. Here is an example design in UML:

The players are stored as a list of turn-states. The handle method then takes the current player and applies its logic to it, depending on the sub-class. The game engine then switches to the next phase. How we shift to other states depends on the implementation of the handle method.

In the next blog post, we start coding this thing.

About me

Leave a Reply

Your email address will not be published. Required fields are marked *