How to Develop an iPad Board Game App: Part 1/2

In this tutorial, you’ll learn how to create a more laid-back kind of game: the classic board game. Specifically, you will create the time-honoured classic Reversi for the iPad, using plain UIKit. By Colin Eberhardt.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

All Tapped Out - Gesture Recognition

Open up SHCBoardSquare.m, and add the following code at the end of initWithFrame :

// add a tap recognizer
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
                                         initWithTarget:self action:@selector(cellTapped:)];
[self addGestureRecognizer:tapRecognizer];

The code above adds a tap gesture recognizer, which results in the cellTapped method being invoked.

Add the following method implementation to SHCBoardSquare.m:

- (void)cellTapped:(UITapGestureRecognizer*)recognizer
{
    if ([_board isValidMoveToColumn:_column andRow:_row])
    {
        [_board makeMoveToColumn:_column andRow:_row];
    }
}

This checks whether a move is valid, and if so, makes the move. The SHCReversiBoard class keeps track of whose turn it is.

Okay! Now's your chance to see this all in action — you've earned it! :]

Build and run the application, and tap on the empty spaces on the board. You should see alternate black and white pieces being placed, as such:

The game is starting to take shape. But there's something not quite right about it, which you will instantly recognize if you have played Reversi before. The game just lets you place pieces wherever you like, which is contrary to the rules of the game.

It's not quite total anarchy, but you'll need to start implementing the rules of the game in order to maintain some semblance of order around here! :]

Devil's in the Details - Detailed Game Logic

In Reversi, when a piece is played, it must surround one or more of your opponent's pieces either horizontally, vertically or diagonally.

To illustrate this. consider the playing board below, with black set to play next:

The position marked ‘A’ on the board is a valid move, because it would surround a white piece to the left. However, ‘B’ is not a valid move as it would not surround any white pieces in any direction.

So in order to determine whether a move is valid or not, you need to check for surrounded pieces in eight different directions.

This sounds like a lot of work! However, the logic applied for all eight directions is exactly the same: one or more pieces must be surrounded. You can use this commonality to come up with a concise way of checking if a move is valid.

Open up SHCReversiBoard.m, and add the following at the top of the file just beneath the #import statements:

// A 'navigation' function. This takes the given row / column values and navigates in one of the 8 possible directions across the playing board.
typedef void (^BoardNavigationFunction)(NSInteger*, NSInteger*);

BoardNavigationFunction BoardNavigationFunctionRight = ^(NSInteger* c, NSInteger* r) {
    (*c)++;
};

BoardNavigationFunction BoardNavigationFunctionLeft = ^(NSInteger* c, NSInteger* r) {
    (*c)--;
};

BoardNavigationFunction BoardNavigationFunctionUp = ^(NSInteger* c, NSInteger* r) {
    (*r)--;
};

BoardNavigationFunction BoardNavigationFunctionDown = ^(NSInteger* c, NSInteger* r) {
    (*r)++;
};

BoardNavigationFunction BoardNavigationFunctionRightUp = ^(NSInteger* c, NSInteger* r) {
    (*c)++;
    (*r)--;
};

BoardNavigationFunction BoardNavigationFunctionRightDown = ^(NSInteger* c, NSInteger* r) {
    (*c)++;
    (*r)++;
};

BoardNavigationFunction BoardNavigationFunctionLeftUp = ^(NSInteger* c, NSInteger* r) {
    (*c)--;
    (*r)++;
};

BoardNavigationFunction BoardNavigationFunctionLeftDown = ^(NSInteger* c, NSInteger* r) {
    (*c)--;
    (*r)--;
};

The above code typedefs a block, BoardNavigationFunction, which takes as its arguments pointers to two integers. It is then followed by eight blocks, each of which de-reference the two arguments and either increment or decrement them. It looks a little strange, to be sure, but you'll soon see what this is all about.

Navigate a little further down the same file and add an instance variable and init method as follows:

@implementation SHCReversiBoard
{
    BoardNavigationFunction _boardNavigationFunctions[8];
}

- (id)init
{
    if (self = [super init]) {
        [self commonInit];
        [self setToInitialState];
    }
    return self;
}

- (void)commonInit
{
    // create an array of all 8 navigation functions
    _boardNavigationFunctions[0] = BoardNavigationFunctionUp;
    _boardNavigationFunctions[1] = BoardNavigationFunctionDown;
    _boardNavigationFunctions[2] = BoardNavigationFunctionLeft;
    _boardNavigationFunctions[3] = BoardNavigationFunctionRight;
    _boardNavigationFunctions[4] = BoardNavigationFunctionLeftDown;
    _boardNavigationFunctions[5] = BoardNavigationFunctionLeftUp;
    _boardNavigationFunctions[6] = BoardNavigationFunctionRightDown;
    _boardNavigationFunctions[7] = BoardNavigationFunctionRightUp;
    
}

The above code creates an array of these navigation functions, adding each of the eight blocks defined earlier.

In order to put all those blocks to use, you now need to implement the logic that checks whether placing your piece on a specific board square would surround some of the opponent's playing pieces.

Add the following method to SHCReversiBoard.m:

- (BOOL) moveSurroundsCountersForColumn:(NSInteger) column andRow:(NSInteger)row withNavigationFunction:(BoardNavigationFunction) navigationFunction toState:(BoardCellState) state
{
    NSInteger index = 1;
    
    // advance to the next cell
    navigationFunction(&column, &row);
    
    // while within the bounds of the board
    while(column>=0 && column<=7 && row>=0 && row<=7)
    {
        BoardCellState currentCellState = [super cellStateAtColumn:column andRow:row];
        
        // the cell that is the immediate neighbour must be of the other colour
        if (index == 1)
        {
            if(currentCellState!=[self invertState:state])
            {
                return NO;
            }
        }
        else
        {
            // if we have reached a cell of the same colour, this is a valid move
            if (currentCellState==state)
            {
                return YES;
            }
            
            // if we have reached an empty cell - fail
            if (currentCellState==BoardCellStateEmpty)
            {
                return NO;
            }
        }
        
        index++;
        
        // advance to the next cell
        navigationFunction(&column, &row);
    }
    
    return NO;
}

This method determines whether a move to a specific location on the board would surround one or more of the opponent’s pieces. This method uses the supplied navigationFunction to move from one cell to the next. Notice that because the row and column are integers, which are passed by value, the ampersand (&) operator is used to pass a pointer to the integer, allowing its value to be changed by the navigation block.

Within the while loop, the required conditions are checked: the neighbouring cell must be occupied by a piece of the opposing colour, and following cells can be either the opposing colour (in which case the while loop continues), or the player’s colour – which means that a group has been surrounded.

Note: Do you want some more practice with unit testing? The moveSurroundsCountersForColumn method is an excellent opportunity to practice your unit test skills to ensure that you have full branch coverage!

Finally, you'll need to update the logic in isValidMoveForColumn:andRow which checks whether a board square is empty. Replace the previous implementation of the method with the following:

- (BOOL)isValidMoveToColumn:(int)column andRow:(int) row;
{
    return [self isValidMoveToColumn:column andRow:row forState:self.nextMove];
}

- (BOOL)isValidMoveToColumn:(int)column andRow:(int)row forState:(BoardCellState)state
{
    // check the cell is empty
    if ([super cellStateAtColumn:column andRow:row] != BoardCellStateEmpty)
        return NO;
    
    // check each direction
    for(int i=0;i<8;i++)
    {
        if ([self moveSurroundsCountersForColumn:column
                                          andRow:row
                          withNavigationFunction:_boardNavigationFunctions[i]
                                         toState:state])
        {
            return YES;
        }
    }
    
    // if no directions are valid - then this is not a valid move
    return NO;
}

The above code uses the array of navigation functions to check each in turn to see if any of the opponent’s pieces are surrounded. Note that this method has been split into two parts – one which has a state argument, and the other that uses the nextMove property. You'll see the reason for this shortly.

Build and run your app, and tap on some of the board squares. Now you will see that the game is much fussier about where you can place the playing pieces, as in the image below:

However, the game is still a little dull – and since you haven't yet implemented all of the rules yet, every game will result in a draw.

One of the key features of the game of Reversi is that any pieces that are surrounded by pieces of the opposite color are ‘flipped’ to the opposite color. It's time to put this rule into action!

Add the following code to SHCReversiBoard.m:

- (void) flipOponnentsCountersForColumn:(int) column andRow:(int)row withNavigationFunction:(BoardNavigationFunction) navigationFunction toState:(BoardCellState) state
{
    // are any pieces surrounded in this direction?
    if (![self moveSurroundsCountersForColumn:column
                                       andRow:row
                       withNavigationFunction:navigationFunction
                                      toState:state])
        return;
    
    BoardCellState opponentsState = [self invertState:state];
    BoardCellState currentCellState;
    
    // flip counters until the edge of the boards is reached, or
    // a piece of the current state is reached
    do
    {
        // advance to the next cell
        navigationFunction(&column, &row);
        currentCellState = [super cellStateAtColumn:column andRow:row];
        [self setCellState:state forColumn:column  andRow:row];
    }
    while(column>=0 && column<=7 &&
          row>=0 && row<=7 &&
          currentCellState == opponentsState);
    
}

The above method shows just how useful those navigation functions are; after checking whether any pieces will be surrounded in the given direction, a short do-while loop goes about the business of flipping the opponent's pieces.

To employ this logic when a move is made, update makeMoveToColumn:andRow: in SHCReversiBoard.m as follows:

- (void)makeMoveToColumn:(NSInteger)column andRow:(NSInteger)row
{
    // place the playing piece at the given location
    [self setCellState:self.nextMove forColumn:column  andRow:row];
    
    // check the 8 play directions and flip pieces
    for(int i=0; i<8; i++)
    {
        [self flipOponnentsCountersForColumn:column
                                      andRow:row
                      withNavigationFunction:_boardNavigationFunctions[i]
                                     toState:self.nextMove];
    }
    
    _nextMove = [self invertState:_nextMove];
    
}

This uses the array of navigation functions to flip pieces in all eight surrounding squares.

Build and run your app. You should see it behaving properly now:

Congratulations — you've now implemented all of the gameplay functions to play a basic game of Reversi!

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.