Best Practices¶
Game State¶
The Game State of a game is just an opaque binary string for the server. Its format is up to the Game implementor.
Versioning¶
Different game clients are not necessarily compatible with one another. If there were modifications in the game rules (for instance, a buggy card effect was corrected), players with updated version and players with bugged version will have serious troubles when playing together (the action is not applied the same way).
A good way to avoid such issues is by implementing a version management system. It would tell which versions of the game are compatible with each versions, so game clients with incompatible game versions will not be able to play one against another.
The Scalable Server can handle a rules_engine_version
when providing a GameConfiguration, but please note that it is up to the clients to perform the version check.
Serialization Formats¶
We have found by experience that using a well-known serialization format like Protocol Buffers is really a good idea over a custom serialization because:
it is safer, less bug-prone
most of the time optimal in terms of size
schemas are awesome
backward compatibility is free (allowing older producers to communicate with newer consumers)
less boilerplate code: it generates (efficient) parsing/unparsing code for you
it is cross-platform
it is type safe
See this comparison of serialization formats for more choices.
One can also use a generic serialization format like JSON or XML, which allows for clear-text readable outputs. The problem with these serialization formats:
larger outputs
schema evolution is harder
not type safe
most of the libraries require writing a parser that knows the semantics
Event Sourcing¶
At Asmodee.net we are great fans of Event Sourcing as a way to model persistent data.
In a regular game state, all the individual entities representing the game are modeled with their own state immediate data, for instance in a card game, all players hands would be their own entities containing their own data. The game state would be the list of all players hands.
In an Event Sourcing model, we don’t keep the immediate state, but only the events that generated the state, and we keep them in an ordered log (like a database is doing for point in time recovery). To retrieve the state we just need to replay all the events.
A variant of Event Sourcing is Command Sourcing where instead of storing the events, we store the user actions. The events are usually just a byproduct of the user command. In Event Sourcing we store events labelled in the past for instance CardPlayed
, in Command Sourcing we would store PlayCard
.
For example in our hypothetical card game, instead of persisting the content of player hands, we would only store when they played a card, plus an initial value (for instance either a list of events of ‘dealing cards’). This is all what is needed to rebuild the state at any point in time.
One might argue that it can be costly to replay the state constantly, but in practice our experience show that it isn’t, and if it was, it is still possible to keep an intermediate snapshot of the previous turn and rebuild from this event.
Here are the advantages of using Event Sourcing for keeping the Game State:
it is coherent (an event happened or not)
it makes it harder to tamper with the game state in a cheating attempt
it allows to replay the game (which can be a killer feature for tournaments)
it allows easier debugging (since you can replay the game it can help understanding bugs in the game logic)
A Concrete Example¶
Here is an example Protobuf (version 2) data model for Asmodee.net Gang of Four.
// an individual command
message GameEvent {
enum Type
{
SEED_CHOSEN = 0;
CARD_PLAYED = 1;
CARD_EXCHANGED = 2;
}
Type type = 1; // type of this event
// only one of those fields will be filled
// and it will correspond to the above `type`
SeedChosen new_game_seed = 2;
CardPlayed card_played = 3;
CardExchanged card_exchanged = 4;
}
// the whole state as a log
message GameState {
repeated GameEvent event = 1;
}
With the various commands defined like this:
message SeedChosen
{
uint32 seed = 1;
}
message CardPlayed
{
bool skip = 1;
repeated uint32 cards_UID = 2;
}
message CardExchanged
{
uint32 cards_UID = 1;
}
In this specific example the first event that is stored is the whole game seed. This is a trick to make sure the same event log gives the same result everytime. The game is using the same Pseudo Random Number Generator on every platform.
By storing the game seed, we make sure that we initialize the random generator exactly the same way every time we need to rebuild the game state. Of course the chosen PRNG needs to be deterministic.
Another option would be to store each random number generated with its own event as part of the Game State.