Core Classes

Core Classes

Implementing a new game

Let’s start with a hypothetical game called ‘Foobar’.

Firstly create a branch or fork to keep the code in (with all the usual Github hygiene), and a new package games.foobar.

Now create an initial sets of Classes as listed below (FoobarGame, FoobarForwardModel, FoobarGameState, FoobarTurnOrder, FoobarParams). At this stage these will be empty. We’ll go through them one by one shortly to highlight the key required sections.

FoobarForwardModel extends AbstractForwardModel

The rationale of the ForwardModel is that it contains the core game logic, while the GameState contains the underlying game data. Usually this means that ForwardModel is stateless, and this is a good principle to adopt, but as ever there will always be exceptions.

This has a number of core methods that must be implemented:

  1. void _setup(AbstractGameState state). This performs the initial game setup according to the game rules, initialising all components in the given game state (e.g. give each player their starting hand, place tokens on the board etc.). It may feel more natural to put this setup logic in Gamestate, but in keeping with the principle of encapsulating game logic in ForwardModel, this is where it should go.
  2. void _next(AbstractGameState state, AbstractAction action). This is called every time an action is taken by one of the player, human or AI. Avoid the temptation to put large amounts of logic here - have a look at LoveLetterForwardModel for a simple example without any need to change the phase of a game, or DominionForwardModel or ColtExpressForwardModel for slightly more complicated examples which do have phase changes.
    • Apply the given action to the game state. This logic should be in the AbstractAction.execute() method, which we’ll get to later, so this should be as simple as action.execute(state).
    • Execute any other required game rules (e.g. change the phase of the game);
    • Check for game end;
    • Move to the next player (if required, and if the game has not ended). This is usually achieved with state.getTurnOrder().endPlayerTurn(state), with this logic encapsulated in FoobarTurnOrder.
  3. In the _computeAvailableActions(AbstractGameState gameState) method, return a list with all actions available for the current player, in the context of the game state object.
  4. In the _copy() method, return a new instance of the Forward Model object with any variables copied.
  5. You may override the endGame() method if your game requires any extra end of game computation (e.g. to update the status of players still in the game to winners).

Note: Forward model classes can instead extend from the core.rules.AbstractRuleBasedForwardModel.java abstract class instead, if they wish to use the rule-based system instead; this class provides basic functionality and documentation for using rules and an already implemented _next() function.

FoobarGameState extends AbstractGameState

Create a game state class named e.g. "foobar.FoobarGameState.java" which extends from the core.AbstractGameState.java class.

  • In the _getAllComponents() method, return a list of all (parents, and those nested as well) components in your game state. The method is called after game setup, so you may assume all components are already created. Decks and Areas have all of their nested components automatically added.
  • In the _copy(int playerId) method, define a reduced, player-specific, copy of your game state. This includes only those components (or parts of the components) which the player with the given ID can see. For example, some decks may be face down and unobservable to the player. All of the components in the observation should be copies of those in the game state (pay attention to any references that need reassigning). For much more detail on what this method should do, see Hiding Information.
  • In the _reset() method, reset any variables that would have been changed (and not directly reset in the ForwardModel._setup() method) to their initial state.
  • In the _getGameScore(int playerId) method, return the player’s score for the current game state. This may not apply for all games; Exploding Kittens for example is a knock-out game with no score. The winner is just the last one standing.
  • In _getHeuristicScore(int playerId)Implement a rough-and-ready heuristic (or a very sophisticated one) that gives an estimate of how well a player is doing in the range [-1, +1], where -1 is immediate loss, and +1 is immediate win. This is used by a number of agents as a default, including MCTS, to value the current state. If the game has a direct score, then the simplest approach here is just to scale this in line with some plausible maximum (see DominionGameState._getHeuristicScore() for an example of this; and contrast to DominionHeuristic for a more sophisticated approach).

Note:

  • Game state classes can implement the core.interfaces.IFeatureRepresentation.java interface - implementing all of the methods required according to the code documentation allows AI players to extract further generic information about the game state, in terms of abstract features.
  • Game state classes can implement the core.interfaces.IVectorObservation.java interface - implementing all of the methods required according to the code documentation allows AI players to receive a vector observation from the game state (enabling methods that require such linear representations).

FoobarTurnOrder extends TurnOrder

The central responsibility of TurnOrder is to track whose turn it is, and move on to the next player correctly. If the game has a simple alternating structure of each player taking their turn in order, then you can just use AlternatingTurnOrder directly as both DotsAndBoxes and Virus do. (This also support the turn order reversing direction at points in the game.)

FoobarParams extends AbstractParameters

The Parameters of the game should contain any Game constants. Having them all in one place makes it easy to amend them, and also to tune them as part of Game Design. For straightforward and simple examples, have a look at those for Diamant and DotsAndBoxes. (We would have recommended TicTacToe, except that it is implemented to also be Tunable…which makes it more complex despite only having a single parameter for the grid size.)

Tying it all together

Add a new enum to games.GameType that records the min and max players, and what type of game it is (if necessary, create newCategory or Mechanic enums so that your game is classified correctly):

Foobar(2, 6,
        new ArrayList<Category>() ,
        new ArrayList<Mechanic>() )

There are three more changes needed in GameType, for both of which you can easily follow the pattern of all the other games:

  1. In createGameInstance add an instantiation of your new FoobarForwardModel and FoobarGameState
  2. In getDefaultParams add an instantiation of your new FoobarParams
  3. In stringToGameType(String game), add a new case for your game so that the string name can be converted to the correct game type.

Finally, create a new class FoobarGame extends Game. This is just boilerplate to link the new GameType to an instantiable game. (The main method is often used to test-run your game with various parameter/player settings.) All you need is an initial constructor with the template below.

public FoobarGame(List<AbstractPlayer> agents, FoobarParams params) {
    super(GameType.Foobar, agents, 
          new FoobarForwardModel(), 
          new FoobarGameState(params, agents.size()));
}

public static void main(String[] args) {
    ...use this for running the game either with or without a GUI. See any existing game for ideas here... 
}