TicTacToe: Multi-device Syncing part #2

In the first part of our tutorial, we discussed the data model we’ll use to power a multi-device TicTacToe game.

We also learned how to integrate Simperium into an iOS app, and how to authenticate our clients by means of a pre-generated token.

One of the biggest technical challenges of this app is: suppose our users have a Match initiated between two players. In our current data model, this is directly translated into: the two clients each have a Player object in the Player bucket, and there is also a Match object linking both players.

Due to the nature of mobile devices, any of the clients can suffer a network disruption at any-time (the device itself might even shutdown due to low battery!)

So… how do we notify our users that their opponents went offline?

1. Simperium’s Presence

Presence is one of the coolest features offered by Simperium. What does it do, exactly?

The presence feature turns a bucket into an ephemeral store. There are two options:

  • User Presence
    Whenever a given user gets disconnected from Simperium’s backend, all of the objects he inserted into buckets with user presence enabled will be automatically deleted. No client interaction is needed, other than the disconnection event itself!.
  • Client Presence
    It works exactly the same way as User Presence, but it is client (or connection) based instead. In Simperium presence’s jargon, a client is represented by a single connection to Simperium. Let’s suppose that someone has both an iPad and an iPhone running the app, these would be two separate clients. Objects created by the iPhone then, would be removed when the iPhone disconnected, but objects created by the iPad would remain as long as the iPad stayed connected.

In our particular scenario, due to our decision to automatically authenticate every device with the same pre-generated auth-token, we’ll need to enable Client Presence in both the Match and Player buckets.

By toggling client presence on, whenever a device running our game goes offline, Simperium will automatically remove all of the Player and Match entities that were created by that device. 

In order to toggle presence for a given bucket, we’ll need to run a simple curl request:

curl -H 'X-Simperium-Token:APP_ADMIN_TOKEN' \
    https://api.simperium.com/1/APP_ID/__options__/i/BUCKET_NAME \
    -d '{"presence":"client"}'

The APP_ID and APP_ADMIN_TOKEN are both available in your Simperium application’s dashboard, as seen here:

SimperiumAdminKey

Since we require client presence to be enabled in both, Player and Match buckets, we’ll need to run two curl requests.

Once that’s ready, Simperium’s backend will make sure that whenever a client goes offline, all of the objects that were inserted by that client are removed.

2. Signaling Player’s Presence

In order to signal a Player’s presence, let’s insert a new Player entity into CoreData, whenever the app becomes active:

Simperium* simperium	= [[TTAppDelegate sharedDelegate] simperium]
Player *player			= [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([Player class]) inManagedObjectContext:simperium.managedObjectContext];
player.playerID			= simperium.clientID;
[simperium save];

// Keep a reference to the player
self.player = player;

We’ve already written the code that will disconnect the WebSocket when the app becomes inactive. When that happens, we can expect the Player entity to be deleted (due to our presence settings): all we need to do is to clear the reference to the player instance!

3. Initiating a Match

We’ve discussed before that a match entity will be a simple object that carries the playerID of the involved parties, plus an object representing the TicTacToe board itself. It’s time for us to discuss some application logic…

A match should be initiated when:

  • The app is just launched.
  • Your opponent is removed from the Player’s bucket: maybe he closed the app, his device was shutdown… or he just lost connectivity!
  • The match in which you’re playing suddenly gets removed. This will happen if your opponent initiated the match, and signed off from Simperium’s backend.

An opponent is eligible if:

  • He is not already playing a match with someone else.
  • His playerID is not the same as our own playerID. Otherwise we’d be initiating a match with ourselves!.

All of these conditions can be easily checked, locally, by means of a couple CoreData queries. You don’t need to perform a single backend request: Simperium informs you of any changes to the data.

Code for the entire startMatch method implementation is available here.

4. Listening for new changes

Our application logic is based on two simple data structures. In order to react accordingly to the game state as new changes come in, we’ll need to setup our two Simperium Bucket’s delegates (Player and Match) as follows:

- (void)listenForBucketUpdates
{
	Simperium *simperium	= [[TTAppDelegate sharedDelegate] simperium];
	NSArray *buckets		= @[ NSStringFromClass([Player class]), NSStringFromClass([Match class]) ];

	for (NSString *name in buckets)
	{
		SPBucket *bucket	= [simperium bucketForName:name];
		NSAssert(bucket != nil, @"Bucket not initialized");

		bucket.delegate		= self;
	}
}

You can take a look at the entire delegate implementation here. The different events that are being handled are:

  • Player Inserted
    We will simply refresh the label that displays the number of active players.
  • Player Deleted
    When a player gets deleted, we’ll need to check if the playerID is the same as our opponent’s playerID: if our opponent was removed, we’ll need to move on and try to begin a new match with another player.
  • Match Inserted
    When a new match event gets triggered, we’ll need to react only if our own  playerID is equal to either the crossPlayerID or the circlePlayerID.If that condition is affirmative, and we’re not currently in a match with another player, we should proceed with accepting the new challenge, and just refresh the UI. However, if we were already in a match with another player, we’ll simply delete the new Match object, and let the opponent realize that we’re busy!.
  • Match Updated
    Our app will have, all the time, the ID of the match we’re currently playing (if any). Whenever our own match is updated, we’ll just refresh the interface. If it’s our turn, the user will get to place a cross (or a circle). If not, we’ll have to wait for our opponent.
  • Match Deleted
    A match can be deleted for a variety of reasons: maybe our opponent went offline, or we initiated a match with a player that was already busy.
    Whenever that happens, we’ll need to refresh the UI, and begin a new match with someone else.!

5. Sync’ing the GameBoard

The TicTacToe board itself is represented by a NSArray instance with 9 NSNumber’s in it.  We’ve defined a handy NS_ENUM to specify the allowed matrix values: empty, circle, and cross.

We’ll rely on our Match entity just as a Transport Object: whenever there is a change that requires the UI to be refreshed, we’ll feed our TTGameBoard instance with the updated match.matrix property, and simply request our TTBoardView object to get refreshed.

You can check out the code for refreshing the user interface here.

6. Full Source Available!

You’re welcome to take a look at the final project. Note that you should open TicTacToe-Final.xcodeproj to get the final version of the project.

Got any questions? we’d love to help! Please leave a comment, ask a question, or post an issue, and we’ll get back to you shortly.

Leave a comment