Quantcast
Channel: Envato Tuts+ Code - Mobile Development
Viewing all articles
Browse latest Browse all 1836

Creating a Game with Bonjour – Game Logic

$
0
0

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.

#import <UIKit/UIKit.h>
@interface MTViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIView *boardView;
@property (weak, nonatomic) IBOutlet UIButton *hostButton;
@property (weak, nonatomic) IBOutlet UIButton *joinButton;
@property (weak, nonatomic) IBOutlet UIButton *disconnectButton;
@end

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).

#import <UIKit/UIKit.h>
@interface MTViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIView *boardView;
@property (weak, nonatomic) IBOutlet UIButton *hostButton;
@property (weak, nonatomic) IBOutlet UIButton *joinButton;
@property (weak, nonatomic) IBOutlet UIButton *replayButton;
@property (weak, nonatomic) IBOutlet UIButton *disconnectButton;
@end
- (IBAction)replay:(id)sender {
}
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
#import <UIKit/UIKit.h>
@interface MTViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIView *boardView;
@property (weak, nonatomic) IBOutlet UIButton *hostButton;
@property (weak, nonatomic) IBOutlet UIButton *joinButton;
@property (weak, nonatomic) IBOutlet UIButton *replayButton;
@property (weak, nonatomic) IBOutlet UIButton *disconnectButton;
@property (weak, nonatomic) IBOutlet UILabel *gameStateLabel;
@end

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
#import <UIKit/UIKit.h>
typedef enum {
    MTBoardCellTypeEmpty = -1,
    MTBoardCellTypeMine,
    MTBoardCellTypeYours
} MTBoardCellType;
@interface MTBoardCell : UIView
@property (assign, nonatomic) MTBoardCellType cellType;
@end

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.

#import "MTBoardCell.h"
@implementation MTBoardCell
#pragma mark -
#pragma mark Initialization
- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Cell Type
        self.cellType = MTBoardCellTypeEmpty;
    }
    return self;
}
#pragma mark -
#pragma mark Setters & Getters
- (void)setCellType:(MTBoardCellType)cellType {
    if (_cellType != cellType) {
        _cellType = cellType;
        // Update View
        [self updateView];
    }
}
#pragma mark -
#pragma mark Helper Methods
- (void)updateView {
    // Background Color
    self.backgroundColor = (self.cellType == MTBoardCellTypeMine) ? [UIColor yellowColor] : (self.cellType == MTBoardCellTypeYours) ? [UIColor redColor] : [UIColor whiteColor];
}
@end

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.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Setup View
    [self setupView];
}
- (void)setupView {
    // Reset Game
    [self resetGame];
    // Configure Subviews
    [self.boardView setHidden:YES];
    [self.replayButton setHidden:YES];
    [self.disconnectButton setHidden:YES];
    [self.gameStateLabel setHidden:YES];
}

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.

#import "MTViewController.h"
#import "MTBoardCell.h"
#import "MTGameController.h"
#import "MTHostGameViewController.h"
#import "MTJoinGameViewController.h"
#define kMTMatrixWidth 7
#define kMTMatrixHeight 6
@interface MTViewController () <MTGameControllerDelegate, MTHostGameViewControllerDelegate, MTJoinGameViewControllerDelegate>
@property (strong, nonatomic) MTGameController *gameController;
@property (strong, nonatomic) NSArray *board;
@property (strong, nonatomic) NSMutableArray *matrix;
@end

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.

- (void)resetGame {
    // Hide Replay Button
    [self.replayButton setHidden:YES];
    // Helpers
    CGSize size = self.boardView.frame.size;
    CGFloat cellWidth = floorf(size.width / kMTMatrixWidth);
    CGFloat cellHeight = floorf(size.height / kMTMatrixHeight);
    NSMutableArray *buffer = [[NSMutableArray alloc] initWithCapacity:kMTMatrixWidth];
    for (int i = 0; i < kMTMatrixWidth; i++) {
        NSMutableArray *column = [[NSMutableArray alloc] initWithCapacity:kMTMatrixHeight];
        for (int j = 0; j < kMTMatrixHeight; j++) {
            CGRect frame = CGRectMake(i * cellWidth, (size.height - ((j + 1) * cellHeight)), cellWidth, cellHeight);
            MTBoardCell *cell = [[MTBoardCell alloc] initWithFrame:frame];
            [cell setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)];
            [self.boardView addSubview:cell];
            [column addObject:cell];
        }
        [buffer addObject:column];
    }
    // Initialize Board
    self.board = [[NSArray alloc] initWithArray:buffer];
    // Initialize Matrix
    self.matrix = [[NSMutableArray alloc] initWithCapacity:kMTMatrixWidth];
    for (int i = 0; i < kMTMatrixWidth; i++) {
        NSMutableArray *column = [[NSMutableArray alloc] initWithCapacity:kMTMatrixHeight];
        [self.matrix addObject:column];
    }
}

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.

- (void)setupView {
    // Reset Game
    [self resetGame];
    // Configure Subviews
    [self.boardView setHidden:YES];
    [self.replayButton setHidden:YES];
    [self.disconnectButton setHidden:YES];
    [self.gameStateLabel setHidden:YES];
    // Add Tap Gesture Recognizer
    UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(addDiscToColumn:)];
    [self.boardView addGestureRecognizer:tgr];
}

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
typedef enum {
    MTGameStateUnknown = -1,
    MTGameStateMyTurn,
    MTGameStateYourTurn,
    MTGameStateIWin,
    MTGameStateYouWin
} MTGameState;

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

#import <Availability.h>
#ifndef __IPHONE_4_0
#warning "This project uses features only available in iOS SDK 4.0 and later."
#endif
#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import "GCDAsyncSocket.h"
    #import "MTConstants.h"
#endif

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.

#import "MTViewController.h"
#import "MTBoardCell.h"
#import "MTGameController.h"
#import "MTHostGameViewController.h"
#import "MTJoinGameViewController.h"
#define kMTMatrixWidth 7
#define kMTMatrixHeight 6
@interface MTViewController () <MTGameControllerDelegate, MTHostGameViewControllerDelegate, MTJoinGameViewControllerDelegate>
@property (assign, nonatomic) MTGameState gameState;
@property (strong, nonatomic) MTGameController *gameController;
@property (strong, nonatomic) NSArray *board;
@property (strong, nonatomic) NSMutableArray *matrix;
@end

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.

- (void)addDiscToColumn:(UITapGestureRecognizer *)tgr {
    if (self.gameState >= MTGameStateIWin) {
        // Notify Players
    } else if (self.gameState != MTGameStateMyTurn) {
        NSString *message = NSLocalizedString(@"It's not your turn.", nil);
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Warning" message:message delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", nil) otherButtonTitles:nil];
        [alertView show];
    } else {
        NSInteger column = [self columnForPoint:[tgr locationInView:tgr.view]];
        [self addDiscToColumn:column withType:MTBoardCellTypeMine];
        // Update Game State
        // Send Packet
        // Notify Players if Someone Has Won the Game
    }
}

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

- (NSInteger)columnForPoint:(CGPoint)point {
    return floorf(point.x / floorf(self.boardView.frame.size.width / kMTMatrixWidth));
}

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.

- (void)addDiscToColumn:(NSInteger)column withType:(MTBoardCellType)cellType {
    // Update Matrix
    NSMutableArray *columnArray = [self.matrix objectAtIndex:column];
    [columnArray addObject:@(cellType)];
    // Update Cells
    MTBoardCell *cell = [[self.board objectAtIndex:column] objectAtIndex:([columnArray count] - 1)];
    [cell setCellType:cellType];
}

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.

- (void)startGameWithSocket:(GCDAsyncSocket *)socket {
    // Initialize Game Controller
    self.gameController = [[MTGameController alloc] initWithSocket:socket];
    // Configure Game Controller
    [self.gameController setDelegate:self];
    // Hide/Show Buttons
    [self.boardView setHidden:NO];
    [self.hostButton setHidden:YES];
    [self.joinButton setHidden:YES];
    [self.disconnectButton setHidden:NO];
    [self.gameStateLabel setHidden:NO];
}
- (void)endGame {
    // Clean Up
    [self.gameController setDelegate:nil];
    [self setGameController:nil];
    // Hide/Show Buttons
    [self.boardView setHidden:YES];
    [self.hostButton setHidden:NO];
    [self.joinButton setHidden:NO];
    [self.disconnectButton setHidden:YES];
    [self.gameStateLabel setHidden:YES];
}

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.

- (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket:(GCDAsyncSocket *)socket {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    // Update Game State
    [self setGameState:MTGameStateMyTurn];
    // Start Game with Socket
    [self startGameWithSocket:socket];
}
- (void)controller:(MTJoinGameViewController *)controller didJoinGameOnSocket:(GCDAsyncSocket *)socket {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    // Update Game State
    [self setGameState:MTGameStateYourTurn];
    // Start Game with Socket
    [self startGameWithSocket:socket];
}

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.

- (void)addDiscToColumn:(UITapGestureRecognizer *)tgr {
    if (self.gameState >= MTGameStateIWin) {
        // Notify Players
    } else if (self.gameState != MTGameStateMyTurn) {
        NSString *message = NSLocalizedString(@"It's not your turn.", nil);
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Warning" message:message delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", nil) otherButtonTitles:nil];
        [alertView show];
    } else {
        NSInteger column = [self columnForPoint:[tgr locationInView:tgr.view]];
        [self addDiscToColumn:column withType:MTBoardCellTypeMine];
        // Update Game State
        [self setGameState:MTGameStateYourTurn];
        // Send Packet
        // Notify Players if Someone Has Won the Game
    }
}

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.

#import <Foundation/Foundation.h>
@class GCDAsyncSocket;
@protocol MTGameControllerDelegate;
@interface MTGameController : NSObject
@property (weak, nonatomic) id<MTGameControllerDelegate> delegate;
#pragma mark -
#pragma mark Initialization
- (id)initWithSocket:(GCDAsyncSocket *)socket;
#pragma mark -
#pragma mark Public Instance Methods
- (void)addDiscToColumn:(NSInteger)column;
@end
@protocol MTGameControllerDelegate <NSObject>
- (void)controller:(MTGameController *)controller didAddDiscToColumn:(NSInteger)column;
- (void)controllerDidDisconnect:(MTGameController *)controller;
@end

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.

- (void)addDiscToColumn:(NSInteger)column {
    // Send Packet
    NSDictionary *load = @{ @"column" : @(column) };
    MTPacket *packet = [[MTPacket alloc] initWithData:load type:MTPacketTypeDidAddDisc action:0];
    [self sendPacket:packet];
}
typedef enum {
    MTPacketTypeUnknown = -1,
    MTPacketTypeDidAddDisc
} MTPacketType;

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.

- (void)parseBody:(NSData *)data {
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    MTPacket *packet = [unarchiver decodeObjectForKey:@"packet"];
    [unarchiver finishDecoding];
    /*
    NSLog(@"Packet Data > %@", packet.data);
    NSLog(@"Packet Type > %i", packet.type);
    NSLog(@"Packet Action > %i", packet.action);
    */
    if ([packet type] == MTPacketTypeDidAddDisc) {
        NSNumber *column = [(NSDictionary *)[packet data] objectForKey:@"column"];
        if (column) {
            // Notify Delegate
            [self.delegate controller:self didAddDiscToColumn:[column integerValue]];
        }
    }
}

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.

- (void)controller:(MTGameController *)controller didAddDiscToColumn:(NSInteger)column {
    // Update Game
    [self addDiscToColumn:column withType:MTBoardCellTypeYours];
    // Update State
    [self setGameState:MTGameStateMyTurn];
}

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.

- (void)addDiscToColumn:(UITapGestureRecognizer *)tgr {
    if (self.gameState >= MTGameStateIWin) {
        // Notify Players
    } else if (self.gameState != MTGameStateMyTurn) {
        NSString *message = NSLocalizedString(@"It's not your turn.", nil);
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Warning" message:message delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", nil) otherButtonTitles:nil];
        [alertView show];
    } else {
        NSInteger column = [self columnForPoint:[tgr locationInView:tgr.view]];
        [self addDiscToColumn:column withType:MTBoardCellTypeMine];
        // Update Game State
        [self setGameState:MTGameStateYourTurn];
        // Send Packet
        [self.gameController addDiscToColumn:column];
        // Notify Players if Someone Has Won the Game
    }
}

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.

- (void)dealloc {
    if (_delegate) {
        _delegate = nil;
    }
    if (_socket) {
        [_socket setDelegate:nil delegateQueue:NULL];
        _socket = nil;
    }
}

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.

- (void)dealloc {
    if (_delegate) {
        _delegate = nil;
    }
}

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.

typedef enum {
    MTPlayerTypeMe = 0,
    MTPlayerTypeYou
} MTPlayerType;

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.

- (BOOL)hasPlayerOfTypeWon:(MTPlayerType)playerType {
    BOOL _hasWon = NO;
    NSInteger _counter = 0;
    MTBoardCellType targetType = playerType == MTPlayerTypeMe ? MTBoardCellTypeMine : MTBoardCellTypeYours;
    // Check Vertical Matches
    for (NSArray *line in self.board) {
        _counter = 0;
        for (MTBoardCell *cell in line) {
            _counter = (cell.cellType == targetType) ? _counter + 1 : 0;
            _hasWon = (_counter > 3) ? YES : _hasWon;
            if (_hasWon) break;
        }
        if (_hasWon) break;
    }
    if (!_hasWon) {
        // Check Horizontal Matches
        for (int i = 0; i < kMTMatrixHeight; i++) {
            _counter = 0;
            for (int j = 0; j < kMTMatrixWidth; j++) {
                MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:i];
                _counter = (cell.cellType == targetType) ? _counter + 1 : 0;
                _hasWon = (_counter > 3) ? YES : _hasWon;
                if (_hasWon) break;
            }
            if (_hasWon) break;
        }
    }
    if (!_hasWon) {
        // Check Diagonal Matches - First Pass
        for (int i = 0; i < kMTMatrixWidth; i++) {
            _counter = 0;
            // Forward
            for (int j = i, row = 0; j < kMTMatrixWidth && row < kMTMatrixHeight; j++, row++) {
                MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:row];
                _counter = (cell.cellType == targetType) ? _counter + 1 : 0;
                _hasWon = (_counter > 3) ? YES : _hasWon;
                if (_hasWon) break;
            }
            if (_hasWon) break;
            _counter = 0;
            // Backward
            for (int j = i, row = 0; j >= 0 && row < kMTMatrixHeight; j--, row++) {
                MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:row];
                _counter = (cell.cellType == targetType) ? _counter + 1 : 0;
                _hasWon = (_counter > 3) ? YES : _hasWon;
                if (_hasWon) break;
            }
            if (_hasWon) break;
        }
    }
    if (!_hasWon) {
        // Check Diagonal Matches - Second Pass
        for (int i = 0; i < kMTMatrixWidth; i++) {
            _counter = 0;
            // Forward
            for (int j = i, row = (kMTMatrixHeight - 1); j < kMTMatrixWidth && row >= 0; j++, row--) {
                MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:row];
                _counter = (cell.cellType == targetType) ? _counter + 1 : 0;
                _hasWon = (_counter > 3) ? YES : _hasWon;
                if (_hasWon) break;
            }
            if (_hasWon) break;
            _counter = 0;
            // Backward
            for (int j = i, row = (kMTMatrixHeight - 1); j >= 0 && row >= 0; j--, row--) {
                MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:row];
                _counter = (cell.cellType == targetType) ? _counter + 1 : 0;
                _hasWon = (_counter > 3) ? YES : _hasWon;
                if (_hasWon) break;
            }
            if (_hasWon) break;
        }
    }
    // Update Game State
    if (_hasWon) {
        self.gameState = (playerType == MTPlayerTypeMe) ? MTGameStateIWin : MTGameStateYouWin;
    }
    return _hasWon;
}

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:.

- (void)addDiscToColumn:(UITapGestureRecognizer *)tgr {
    if (self.gameState >= MTGameStateIWin) {
        // Notify Players
        [self showWinner];
    } else if (self.gameState != MTGameStateMyTurn) {
        NSString *message = NSLocalizedString(@"It's not your turn.", nil);
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Warning" message:message delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", nil) otherButtonTitles:nil];
        [alertView show];
    } else {
        NSInteger column = [self columnForPoint:[tgr locationInView:tgr.view]];
        [self addDiscToColumn:column withType:MTBoardCellTypeMine];
        // Update Game State
        [self setGameState:MTGameStateYourTurn];
        // Send Packet
        [self.gameController addDiscToColumn:column];
        // Notify Players if Someone Has Won the Game
        if ([self hasPlayerOfTypeWon:MTPlayerTypeMe]) {
            // Show Winner
            [self showWinner];
        }
    }
}

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.

- (void)controller:(MTGameController *)controller didAddDiscToColumn:(NSInteger)column {
    // Update Game
    [self addDiscToColumn:column withType:MTBoardCellTypeYours];
    if ([self hasPlayerOfTypeWon:MTPlayerTypeYou]) {
        // Show Winner
        [self showWinner];
    } else {
        // Update State
        [self setGameState:MTGameStateMyTurn];
    }
}

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.

- (void)showWinner {
    if (self.gameState < MTGameStateIWin) return;
    // Show Replay Button
    [self.replayButton setHidden:NO];
    NSString *message = nil;
    if (self.gameState == MTGameStateIWin) {
        message = NSLocalizedString(@"You have won the game.", nil);
    } else if (self.gameState == MTGameStateYouWin) {
        message = NSLocalizedString(@"Your opponent has won the game.", nil);
    }
    // Show Alert View
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"We Have a Winner" message:message delegate:self cancelButtonTitle:NSLocalizedString(@"OK", nil) otherButtonTitles:nil];
    [alertView show];
}

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.

- (void)setGameState:(MTGameState)gameState {
    if (_gameState != gameState) {
        _gameState = gameState;
        // Update View
        [self updateView];
    }
}

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

- (void)updateView {
    // Update Game State Label
    switch (self.gameState) {
        case MTGameStateMyTurn: {
            self.gameStateLabel.text = NSLocalizedString(@"It is your turn.", nil);
            break;
        }
        case MTGameStateYourTurn: {
            self.gameStateLabel.text = NSLocalizedString(@"It is your opponent's turn.", nil);
            break;
        }
        case MTGameStateIWin: {
            self.gameStateLabel.text = NSLocalizedString(@"You have won.", nil);
            break;
        }
        case MTGameStateYouWin: {
            self.gameStateLabel.text = NSLocalizedString(@"Your opponent has won.", nil);
            break;
        }
        default: {
            self.gameStateLabel.text = nil;
            break;
        }
    }
}

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.

- (IBAction)replay:(id)sender {
    // Reset Game
    [self resetGame];
    // Update Game State
    self.gameState = MTGameStateMyTurn;
    // Notify Opponent of New Game
    [self.gameController startNewGame];
}

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.

#import <Foundation/Foundation.h>
@class GCDAsyncSocket;
@protocol MTGameControllerDelegate;
@interface MTGameController : NSObject
@property (weak, nonatomic) id<MTGameControllerDelegate> delegate;
#pragma mark -
#pragma mark Initialization
- (id)initWithSocket:(GCDAsyncSocket *)socket;
#pragma mark -
#pragma mark Public Instance Methods
- (void)startNewGame;
- (void)addDiscToColumn:(NSInteger)column;
@end
@protocol MTGameControllerDelegate <NSObject>
- (void)controller:(MTGameController *)controller didAddDiscToColumn:(NSInteger)column;
- (void)controllerDidStartNewGame:(MTGameController *)controller;
- (void)controllerDidDisconnect:(MTGameController *)controller;
@end

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.

- (void)startNewGame {
    // Send Packet
    NSDictionary *load = nil;
    MTPacket *packet = [[MTPacket alloc] initWithData:load type:MTPacketTypeStartNewGame action:0];
    [self sendPacket:packet];
}
typedef enum {
    MTPacketTypeUnknown = -1,
    MTPacketTypeDidAddDisc,
    MTPacketTypeStartNewGame
} MTPacketType;

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.

- (void)parseBody:(NSData *)data {
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    MTPacket *packet = [unarchiver decodeObjectForKey:@"packet"];
    [unarchiver finishDecoding];
    /*
    NSLog(@"Packet Data > %@", packet.data);
    NSLog(@"Packet Type > %i", packet.type);
    NSLog(@"Packet Action > %i", packet.action);
    */
    if ([packet type] == MTPacketTypeDidAddDisc) {
        NSNumber *column = [(NSDictionary *)[packet data] objectForKey:@"column"];
        if (column) {
            // Notify Delegate
            [self.delegate controller:self didAddDiscToColumn:[column integerValue]];
        }
    } else if ([packet type] == MTPacketTypeStartNewGame) {
        // Notify Delegate
        [self.delegate controllerDidStartNewGame:self];
    }
}

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.

- (void)controllerDidStartNewGame:(MTGameController *)controller {
    // Reset Game
    [self resetGame];
    // Update Game State
    self.gameState = MTGameStateYourTurn;
}

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.


Viewing all articles
Browse latest Browse all 1836

Trending Articles