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. Use the gametemplate package as a basis for this. This has the initial sets of Classes in place that can be renamed to the new game, say FoobarForwardModel, FoobarGameState, FoobarParams.

We’ll go through them one by one shortly to highlight the key required sections.

FoobarForwardModel extends StandardForwardModel

The rationale of the ForwardModel is that it contains the core game logic, while the GameState contains the underlying game data. This means that ForwardModel should be stateless. If you find yourself putting any class variables or other state in the forward model, then check to see if there is not a better way to put these into the GameState.

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. 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.
  3. void _afterAction(AbstractGameState state, AbstractAction action). This is called every time an action is taken by one of the players, human or AI. It is called after the framework has already applied action to state. It should:
    • 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 achieved with endPlayerTurn(state, nextPlayer). For the most common pattern of each player taking turns in clockwise order, endPlayerTurn(state) can be used. endRound() does the same to end a round in the framework. (A Round is usually interpreted as each player taking a Turn, but this can vary between games. For example a Colt Express ‘round’ ties in with that game’s concept of a round encompassing one complete Planning phase and one complete Stealing phase; and Stratego does not use Rounds at all, but just alternates Turns between players.)

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.

  1. void _beforeAction(AbstractGameState state, AbstractAction action). This is as for _afterAction, but before the action is applied (in case your game needs to insert logic at this point).

FoobarGameState extends AbstractGameState

Create a game state class named e.g. "" which extends from the 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 _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).
  • Check that all the required variables in the GameState are correctly initialised in ForwardModel._setup().
  • Ensure good equals() and hashCode() methods are implemented that include all the data in the GameState.

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.)

FoobarGUI extends AbstractGUIManager

A GUI is optional, and while helpful to debug a game can often be left until a later stage in any implementation. The main method to implement in the GUIManager is _update(AbstractPlayer player, AbstractGameState gameState). This should update the GUI with the current state. There is a little more detail on the GUI page.

Another option in place of a full GUI is to use the IPrintable interface (see, for example, Can’t Stop, Love Letter or Connect 4). This requires a string output of the GameState to be implemented in getString(). This can then be used with the HumanConsolePlayer to play the game against AI opponents. Once you’re happy with this, a full GUI can follow.

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). It also critically includes the classes that implement the AbstractGameState, AbstractForwardModel and AbstractParameters interfaces. The framework will then automatically pick these up when instantiating a new Game.

    Catan(3, 4,
            Arrays.asList(Strategy, Cards),
            Arrays.asList(Memory, GridMovement, ModularBoard),
            CatanGameState.class, CatanForwardModel.class, CatanParameters.class, CatanGUI.class),