Creating a Game with Bonjour - Game Logic

In the previous articles, we predominantly focused on the network aspect of the game. In this final installment, it is time to zoom in on the game itself. We will implement the game and leverage the foundation we laid in the previous articles to create a multiplayer game.


Introduction

In this article, we will discuss two topics, (1) creating the game and (2) leveraging the foundation that we created in the previous articles. In the previous article, a bug has found its way into our project. I must admit that it took me quite some time to discover this nasty, little creature. Don't worry, though. We will squash this bug as we go and I will show you exactly where it is causing havoc. Even though I could have updated the previous article to get rid of the bug, I prefer to show you how to find and fix the bug as it will help understand how the CocoaAsyncSocket library works. We have quite a bit of work ahead of us so let's get started.


1. Updating the User Interface

Let me start this article by briefly talking about the game, Four in a Row. If you haven't heard of Four in a Row, then I suggest you pay Wikipedia a visit. By the way, Four in a Row is known by many names, such as as Connect Four, Find Four, and Plot Four. The concept is simple. We have a board or grid with seven columns each containing six cells. The user can tap a column to add a disc to that column. Each time a player adds a disc to a column, we invoke a method to check if the player has won the game, that is, four discs in a row. Rows can be horizontal, vertical, or diagonal.

This implies that we need to keep track of quite a few variables. To keep track of the state of the game, we create a data structure, an array of arrays, mirroring the board or grid of cells. Each array in the array represents a column. Whenever a player adds a disc to a column, we update the data structure that backs the game and check whether the player has won the game.

I am not an expert in game development and the approach we use in this project is not the only solution to implement Four in a Row. It probably isn't the most efficient implementation either. However, by using well known Objective-C patterns and sticking to Foundation classes, most of you should be able to keep pace without much difficulty.

While exploring Four in a Row, I stumbled upon a Stack Overflow answer that outlines an algorithm for Four in a Row using bitboards. This is a very efficient and fast solution so if you are serious about board games, such as tic-tac-toe or chess, then I recommend exploring bitboards in more detail.

As I said, we will be using an array of arrays as the data structure of the game. The board itself will be a simple view with 42 subviews or board cells. Each subview or board cell corresponds to a position in the data structure. Because we need an easy way to keep a reference to each board cell, we manage a second data structure, another array of arrays, to store a reference to each board cell. This makes it easy to update the board view, but it also has some other benefits that will become evident a bit later in this tutorial.

Step 1: Adding the Board View

Let's start by creating the board view. Open MTViewController.xib, add a UIView instance to the view controller's view and set its dimensions to 280 points by 240 points (figure 1). Modify the constraints of the view in such a way that the board view has a fixed width and height. The board view should also be horizontally and vertically centered in the view controller's view. Autolayout makes this a breeze.

Creating a Game with Bonjour - Game Logic - Adding the Board View
Figure 1: Adding the Board View

Create an outlet in MTViewController.h for the board view and name it boardView. In Interface Builder, connect the outlet to the board view. We will add the board view's subviews programmatically.

Step 2: Adding a Replay Button

When the game ends, we want to give the player the opportunity to start a new game. Add a new button to the view controller's view and give it a title of Replay (figure 2). Create an outlet, replayButton, for the button in MTViewController.h and an action named replay: in MTViewController.m. Connect the outlet and action to the replay button in Interface Builder (figure 2).

Creating a Game with Bonjour - Game Logic - Add a Replay Button
Figure 2: Add a Replay Button

Step 3: Adding a State Label

The player of the game should be informed about the state of the game. Whose turn is it? Who has won the game? We add a label to the view controller's view and update it whenever the game state changes. Revisit MTViewController.xib and add a label (UILabel) to the view controller's view (figure 3). Create an outlet for the label in the view controller's header file, name it gameStateLabel, and connect it to the label in Interface Builder (figure 3).

Creating a Game with Bonjour - Game Logic - Add a State Label
Figure 3: Add a State Label

2. Laying Out the Board

Step 1: Creating the Board Cell Class

As I mentioned earlier, the board view contains 42 subviews or board cells. We will create a UIView subclass to make each board cell a bit smarter and easier to use. Create a UIView subclass and name it MTBoardCell (figure 4). The MTBoardCell class has one property, cellType of type MTBoardCellType, which is declared at the top of the header file.

Creating a Game with Bonjour - Game Logic - Creating the Board Cell Class
Figure 4: Creating the Board Cell Class

In the designated initializer, we set cellType to MTBoardCellTypeEmpty to mark the board cell as empty. In the implementation file of the class, we also override the setter of cellType. In setCellType:, we update the view by invoking updateView, a helper method in which we update the view's background color.

Step 2: Setting Up a Game

To set up a new game, we invoke the main view controller's resetGame method. We will invoke resetGame in various places in our project. One of those places is the view controller's viewDidLoad method. Because I prefer to keep the viewDidLoad method concise, I generally move the view's setup logic to a separate setupView helper method which is invoked in viewDidLoad. In setupView, we also hide all the view's subviews with the exception of the host and join button.

Before we can implement resetGame, we need to create the data structure that stores the game state and the data structure that stores the references to the board cells of the board view. Add a class extension at the top of MTViewController.h and create two properties, board (NSArray) and matrix (NSMutableArray). We also import the header file of MTBoardCell and define to constants, kMTMatrixWidth and kMTMatrixHeight, that store the dimensions of the board.

The implementation of resetGame isn't rocket science as you can see below. Because resetGame will also be invoked when a player taps the replay button, the implementation starts with hiding the replay button. We calculate the size of a board cell, create a mutable array for each column of the board, and add six board cells to each column. This array of arrays is stored in the class's board property as an immutable array. The class's matrix property is very similar. It also stores an array of arrays. The main differences are that (1) the columns contain no objects when the game is reset and (2) each column is an instance of NSMutableArray.


3. Adding Interaction

Step 1: Adding a Gesture Recognizer

Adding interaction to the game is as simple as adding a tap gesture recognizer to the board view in the view controller's setupView method. Each time a player taps the board view, the addDiscToColumn: message is sent to our MTViewController instance.

Before we implement addDiscToColumn:, we need to take a detour and talk about the game state. The MTViewController class needs to keep track of the state of the game. By game state, I don't refer to the data structures (board and matrix) that we created earlier. I simply mean a property that keeps tracks of whose turn it is and whether a player has won the game. To make things easier, it is a good idea to declare a custom type for the game state. Because we will use this custom type in various places in our project, it is best to declare it in a separate file, MTConstants.h, and add an import statement for MTConstants.h to the project's precompiled header file.

Create a new NSObject subclass named MTConstants (figure 5), delete the implementation file (MTConstants.m), and clear the contents of MTConstants.h. In MTConstants.h, we define MTGameState as shown below.

Creating a Game with Bonjour - Game Logic - Creating MTConstants.h
Figure 5: Creating MTConstants.h

Add an import statement for MTConstants.h to the project's precompiled header file so that its contents are available throughout the project.

In MTConstants.h, we declare the various states of the game. In a more complex game, this might not be the best strategy or you may need to add additional states. For this project, this approach will suffice. Because Four in a Row is a turn based game, most of the game is spent in the MTGameStateMyTurn and MTGameStateYourTurn states, that is, it is either your turn or your opponent's turn to add a disc to the board. The last two states are used when the game has ended with one of the players as the winner of the game.

With MTGameState defined in MTConstants.h, it is time to declare the gameState property in the MTViewController class extension that we created earlier. As you might have guessed, the gameState property is of type MTGameState.

It is time to implement the addDiscToColumn: method. The implementation of addDiscToColumn: shown below is incomplete as you can see by the comments in its implementation. We will complete its implementation as we go. The main element to focus on at this point is the method's flow. We start by checking if the game has already been won by one of the players. If it has, then there is no need to add any more discs to the board. The second check we make is whether the player can add a disc, that is, is it the player's turn to add a disc to the board. If this isn't the case, then we show an alert view informing the player that it's not their turn.

The interesting part of addDiscToColumn: is what happens if the game hasn't ended and the player is allowed to add a disc to the board. We calculate which column the player has tapped by invoking columnForPoint: and pass the location in the board view that the player has tapped. The column variable is then passed as an argument to addDiscToColumn:withType:. The second parameter of this method is the cell type, which is MTBoardCellTypeMine in this case.

The columnForPoint: method is nothing more than a simple calculation to infer the column based on the coordinates of point.

In addDiscToColumn:withType:, we update the game state by updating the view controller's matrix property. We then fetch a reference to the corresponding board cell, stored in the view controller's board property, and set its cell type to cellType. Because we overrode the setCellType: method in MTBoardCell, the board cell's background color will be updated automatically.

Before testing the game, we need to amend the startGameWithSocket: and endGame methods. In these methods, we update the view controller's view based on the state of the game. Run two instances of the application and test the game in its current state.


4. Improving Interaction

At the moment, there are no limits to the number of discs a player can add and the actions of player A are not visible to player B, and vice versa. Let's fix that.

Step 1: Limiting Interaction

To limit interaction, we need to update the view controller's gameState property at the appropriate time. The interaction with the board is already limited by the value of gameState in addDiscToColumn:, but this isn't very useful if we don't update the gameState property.

First of all, we need to decide who's turn it is when a new game starts. We could do something fancy like a coin toss, but let's keep it simple and let the player hosting the game make the first move. This is easy enough. We simply update the gameState property in the controller:didHostGameOnSocket: and controller:didJoinGameOnSocket: delegate methods. The result is that only the player hosting the game can add a disc to the board.

The second change we need to make is update the game state whenever the player makes a valid move. We do this in addDiscToColumn: as shown below. Each time a player adds a disc to the board, the game state is set to MTGameStateYourTurn, which means that the player cannot add any more discs to the board as long as the game state isn't updated. Before we continue, test the application one more time to see the result of our changes.

Step 2: Sending Updates

Even though we establish a connection when a new game is started, thus far, we haven't done much with that connection. The class that is in charge of the connection is MTGameController, which we created in the previous article. Open MTGameController.h and declare an instance method named addDiscToColumn:. The view controller will invoke this method to inform the game controller that the other player needs to be updated about the changed game state. This is also a good moment to expand the MTGameControllerDelegate protocol. When the game controller receives an update, it needs to notify its delegate, the main view controller, about the update because the main view controller is in charge of updating the board view. Take a look at the updated header file of the MTGameController class.

The addDiscToColumn: method is very easy to implement thanks to the groundwork we did in the previous articles. I have updated the header file of the MTPacket class by adding MTPacketTypeDidAddDisc to the enumeration of packet types. Even though we declared the action property in the MTPacket class, we won't be needing it in this project.

The parseBody: method also needs to be updated. In its current implementation, all we do is log the packet's data to the console. In the updated implementation, we check the packet's type and notify the delegate that the opponent added a disc to a column if the packet's type is equal to MTPacketTypeDidAddDisc.

Implement the new delegate method of the MTGameControllerDelegate protocol in the MTViewController class as shown below. We invoke addDiscToColumn:withType: and pass the column and cell type (MTBoardCellTypeYours). The view controller's gameState property is also updated to ensure that the player can add a new disc to the board.

Last but not least, we need to invoke the addDiscToColumn: method of the MTGameController class in the view controller's addDiscToColumn: method. This is the last piece of the puzzle.

Run two instances of the application and test the game one more time. Did you run into a problem? It is time to squash that bug that I told you about earlier in this article. The bug is located in the MTJoinGameViewController class. In the socket:didConnectToHost:port: method of the GCDAsyncSocketDelegate protocol, we notify the delegate of the MTJoinGameViewController class and pass it a reference to the socket. We stop browsing for new services and dismiss the join game view controller.

By dismissing the join game view controller, we implicitly get rid of the join game view controller as it is no longer needed. This means that the class's dealloc method is invoked when the object is released. The current implementation of the dealloc method is shown below.

In the dealloc method of the MTJoinGameViewController class, we clean everything up. However, because this socket is managed by the game controller, we shouldn't set the delegate to nil and neither should we set the delegate queue to NULL. The game controller is instantiated before the dealloc method is invoked, which means that the delegate of the game controller's socket is (re)set to nil when the join game view controller is deallocated. In other words, even though the game controller has a reference to the socket, the socket's delegate is set to nil and this renders the socket unusable to us. The solution is as simple as removing the last few lines of the dealloc method in which we set the socket's delegate to nil and the socket's delegate queue to NULL. Run the application one more time to see if we have successfully fixed that nasty bug.


5. Winning the Game

In its current state, it is not possible to win a game because we haven't implemented an algorithm that checks if one of the players has four of its own discs in a row. I have created a hasPlayerOfTypeWon: method for this purpose. It takes one argument of type MTPlayerType and checks the board if the player of the passed type has won the game. The MTPlayerType type is defined in MTConstants.h. Even though we could pass 0 for player A and 1 for player B, our code becomes much more readable (and maintainable) by declaring a custom type.

As you might expect, hasPlayerOfTypeWon: returns a boolean value. I won't discuss its implementation in detail because it is quite lengthy and not that difficult. The gist of it is that we check all possible winning combinations. It searches for horizontal, vertical, and diagonal matches. This is certainly not the best way to check for matches, but it is a method that I am sure most of you can understand without much difficulty. At the end of the hasPlayerOfTypeWon: method, we also update the view controller's gameState property if appropriate.

The hasPlayerOfTypeWon: method is invoked in two places in the MTViewController class. The first place is in the addDiscToColumn: method. After the player has added a disc to the board, we check if the player has won the game by passing MTPlayerMe as the argument of hasPlayerOfTypeWon:.

If the player did win the game, we invoke showWinner, which we will implement shortly. Notice that we also invoke the showWinner method at the beginning of the addDiscToColumn: method if the user taps the board view when the game has already ended.

The hasPlayerOfTypeWon: method is also invoked in the controller:didAddDiscToColumn: method of the MTGameControllerDelegate protocol. Take a look at its updated implementation below. If the player's opponent has won the game, we also invoke the showWinner method.

In the showWinner method, we update the view by displaying the replay button and showing an alert view that tells the player about the winner of the game.


6. Filling the Gaps

There are two pieces of functionality that I'd like to add before wrapping up this project, (1) updating the game state label whenever the game state changes and (2) enabling the replay button. Both are easy to implement.

Step 1: Updating the Game State Label

To update the game state label, we need to update the view whenever the gameState property changes. We could use KVO (Key Value Observing) for this, but I prefer to simply override the setter of the gameState property. Whenever the value of _gameState changes, we invoke updateView, another helper method.

The updateView method is, just like setupView, a helper method. In updateView, we update the text property of gameStateLabel.

Step 2: Enabling the Replay Button

To enable the replay button, we should start by implementing the replay: action. This action is invoked when the player taps the replay button, which appears when the game ends. We do three things in replay:, (1) invoke resetGame to reset the game, (2) update the game state to MTGameStateMyTurn, and send the game controller a message of startNewGame. This means that the player initiating the new game can make the first move.

We need to implement the startNewGame method on the MTGameController class and extend the MTGameControllerDelegate protocol. Open the header file of the MTGameController class and declare the startNewGame method and the new delegate method of the MTGameControllerDelegate protocol.

Again, thanks to the foundation we laid in the previous article, the startNewGame method is short and simple. To make all this work, we need to revisit the MTPacket class and update the MTPacketType enumeration.

In the parseBody: method of the MTGameController class, we send the delegate a message of controllerDidStartNewGame: if the packet's type is equal to MTPacketTypeStartNewGame.

The last bit of work that we need to do is implementing the controllerDidStartNewGame: delegate method in the MTViewController class. We invoke resetGame, as we did in the replay: action, and update the gameState property.

Run two instances of the application and play the game with a friend to see if everything works as it should.


Conclusion

Even though we now have a playable game, I think you agree that it still needs some tweaking and polishing. The design is pretty basic and a few animations would be nice too. However, the goal of this project has been achieved, creating a multiplayer game using Bonjour and the CocoaAsyncSocket library. You should now have a basic understanding of Bonjour and the CocoaAsyncSocket library and know what each can do for you.

Tags:

Comments

Related Articles