2012-03-13

firebase

Firebase from Scratch, pt VII: Actions On The Table

In a series of posts we’ll do “Firebase From Scratch”, an introduction to Firebase and its concepts and ideas. Hopefully, reading this series will give you a firm grasp of what Firebase is and what it can do.

Now let’s talk a bit more about handling actions in a server game. Why is the actions binary data as opposed to objects? Who do you do scheduling? Can you send actions internally between games, tournaments, services etc? Read on for some answers.

The Table Instance
When an action comes in to either of the “handle data action” or “handle object action”, you are given two objects, the action itself and a table. It is important to re-iterate at this point that your server Game is a collection of objects that collaborate: the game instance and it’s processors, the activator and all currently existing tables.

You should never keep references to table instances in your game or activator.

The above rule is because Firebase is build to transparently scale across multiple servers: Firebase needs to be able to “move tables” between different servers and will do so to make sure there’s no data loss even if servers are crashing.

In fact, the table instance you get when executing an event is only valid for the duration of the given action. Attempts to save the table, or any component given by the table such as the scheduler and reuse later will fail, and unfortunately often fail silently. (Yes, this is a real problem, and there’s a ticket for a future Firebase release to improve the error reporting if this should happen).

So what exactly does the table give you?

  • LobbyTableAttributeAccessor – This object is how you modify the lobby attributes for the table. You can set any attribute you’d like (but attributes starting with “_” are reserved for Firebase) and the attribute will be visible in the lobby and propagated to all clients.
  • ExtendedDetailsProvider – This is normally your game instance and as such you should not have to use it. A game implements this interface to provide extra information about itself to clients.
  • TableGameState – This is where you store the state of your game between actions. Your game state should be a Java object and should be serializable if you ever need to run on a cluster. The initial state can either be set by the activator when the table is created, or on the first action.
  • TableInterceptor – We discussed this briefly in component in part 5, this is the interface your game can implement to allow or deny individual join or leave requests. As such, it’s not very interesting when you are processing actions.
  • TableListener – Also discussed in part 5, and again: not very interesting when you are processing actions.
  • TableMetaData – This is the name and ID of the table as compiled by Firebase when the table was created. It is a static object that will never change.
  • GameNotifier – This is your messaging hub. You use it to whenever you need to talk to the players of your game. Generally you can “notify player”, “send to client” or broadcast. A “player” in this context is a player which is seated at the table, and this is what you will usually use. A “client” is any client regardless if it’s seated at the table. And “broadcast” is all clients… Naturally you should be a bit careful about that one.
  • TablePlayerSet – This is the Firebase representation of the players at your table. You will normally have your own internal representation in the game state of the players and their seat, but it is good to be able to check with the Firebase view now and then.
  • TableScheduler – This is what you’ll use to schedule actions for later execution. More on this below.
  • TournamentNotifier – We’ll discuss tournaments in Firebase later, but if your table is participating in a tournament you can use this object to send actions ot the tournament instance itself.
  • TableWatcherSet – Like the player set, but this time with watchers.

When processing events, it’s the current action, the game state, notifier and scheduler which are interesting, so let’s talk some more about those shall we?

Data And Object Actions
There are two flavors of actions: The “data action” and the “object action”. You if associate data with “binary data” and object with “java object” you get the gist. The processor interface looks like this:

public interface GameProcessor {

    public void handle(GameDataAction action, Table table);

    public void handle(GameObjectAction action, Table table);

}

First things first: you will never get object actions from players. These are plain Java objects and will originate somewhere in the server, either they are scheduled actions or they’re coming from the activator or from some service. Data actions are what your players will be sending. So in the normal case, this is what you need to remember:

  • Data Actions = Client Actions
  • Object Actions = Internal Actions

It’s a bit simplified, but does the job. The next complication comes from the fact that the data actions are just an array of bytes.

The protocol between the clients and your server is yours and yours alone. Firebase treats it as bytes to give you freedom to choose.

Internally we can assume everything is going to be in Java, so an object action is perfectly fine, but when you talk to your clients Firebase does not assume anything and your free to use whatever you want. That means though that your “handle” method will probably take this form (in pseudo code):

public void handle(GameDataAction action, Table table) {
    SomeObject myAction = // translate bytes to internal bean
    SomeObject myAnswer = // do some logic here
    byte[] outgoing = // translate internal bean to bytes
    // send outgoing to one or more players
}

The sooner you realize exactly how your own pattern will look and extract it into a base class or helper classes, the happier you will be.

Game State
In order to keep track of your state bwteen actions you put store it at the table using the TableGameState helper. Firebase will keep a copy of your game state while you execute and update when you’re finished. This will be pretty simple to start with, but here’s some more advance points to remember for later:

  • Your game state must be serializable in order to support a full cluster deployment. For high performance systems, the size of the game state may be crucial as well. You may not realize you have serialization problems when you develop though: Firebase will optimize the execution and only partly serialize your object. To check if your still good, start Firebase with this flag “-Dcom.cubeia.forceDeserialization”.
  • What happens if you execute an action and change the game state, only to encounter an error further down the line and get an exception? If the exception is not caught and propagates up to the handling Firebase code your state will be rolled back. In practise, Firebase will keep a copy of your state while you execute and only update the “global copy” when the handle method on the processor has returned successfully. So yes, there’s a transaction going on here, but we’ll leave that for later.

Oh and by the way: you only need to “set” the state in the TableGameState once (when it is created the first time). Firebase will keep track of it from there. Of curse, if you change state object completely you need to set it again.

Notifier
You send actions to your players using the game notifier. In order to to this you need to create a new game data action to send:

int playerId = // current player, or -1 if "from server"
int tableId = // current table, eg: table.getId();
GameDataAction dataAction = new GameDataAction(playerId, tableId);
ByteBuffer action = // the actual action as a byte object
dataAction.setData(action);

Again, the sooner you create helper classes for the above, the happier you will be. Given that you now have a game data action, you can send it to one or more players at the table. Have a look at the notifier Java API documentation for more information, but a normal thing to do is to send an action to all players at a table, eg “player X just did Y”:

GameNotifier notifier = table.getNotifier();
notifier.notifyAllPlayers(dataAction);

The notifier will not send messages to player that are not properly seated. If you want to send to a player without looking at it’s seating status you can use the “send to client” methods, but these should be used sparingly.

Just as with the game state, there is actually a transaction involved in the execution of any given action. This means that any actions you’ve sent will not be put to the network if your handle method throws an exception. Firebase won’t actually send the actions until the handle method has completed successfully.

Scheduling Actions
Due to the fact the Firebase supports fail-over and high availability when run as a cluster, you shouldn’t rely on your own timers for scheduled actions but let Firebase handle it, because then…

  • … you’re guaranteed one action at the time per table.
  • … fail-over will work auto-magically.

So to schedule events for later execution you use the table scheduler from the table:

TableScheduler sch = table.getScheduler();
// schedule action with 1000 millis delay (1 sec)
sch.scheduleAction(dataAction, 1000);

The most common thing to schedule is to check if a player has acted within a specific time. For example, a poker player will normally have 20-30 seconds to act before he is auto-folded and game play continues to the next player. So the poker server will handle an action from a player, move o the next player and schedule a timeout. Normally this scheduled action will be an “object action” as per above: it is simpler to handle and since the action will stay within Firebase it is safe to let it stay as an object.

Is the scheduler also transactional? You bet: if your code throws a runtime exception Firebase will not commit any scheduled actions.

A complication arises when you want to cancel actions. Let’s say our poker player above does act within hes 30 seconds, then the nice thing would be to cancel his scheduled action. In reality you can actually ignore it as long as you realize the timeout is void when it occurs, but it is cleaner and more correct to cancel it when it isn’t needed anymore. For this reason each scheduled action is associated with an ID that you can store away in the game state, and in pseudo-code it might look something like this:

TableScheduler sch = table.getScheduler();
TimeoutAction timouet = // create internal timeout action
GameObjectAction action = new GameObjectAction(table.getId());
action.setAttachment(timeout);
// schedule action and save id
UUID id = sch.scheduleAction(action, PLAYER_TIMEOUT);
// store the above id for the player so we can cancel later
MyPlayer player = myGameState.getCurrentPlayer();
player.setTimeoutActionId(id);

Now, when a player acts you can start by checking if he has a scheduled timeout, and if he has, cancel it:

TableScheduler sch = table.getScheduler();
MyPlayer player = myGameState.getCurrentPlayer();
UUID id = player.getTimeoutActionId();
if(id != null) {
    sch.cancelScheduledAction(id);
}

So there you go. Action handling in Firebase may seem complex, but it does give you freedom to implement almost any game unimpeded. Remember to extract common code to utility methods and consider using Guice for dependency injection to keep you code cleaner.