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:
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 inGameState
, but in keeping with the principle of encapsulating game logic inForwardModel
, this is where it should go.- 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. 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 appliedaction
tostate
. 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.)
- Execute any other required game rules (e.g. change the
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.
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. "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
_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 (seeDominionGameState._getHeuristicScore()
for an example of this; and contrast toDominionHeuristic
for a more sophisticated approach). - Check that all the required variables in the GameState are correctly initialised in
ForwardModel._setup()
. - Ensure good
equals()
andhashCode()
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),