How to Develop an iPad Board Game App: Part 2/2
In this second and final part of the series, you will keep score, add some nice animations, and add a computer opponent. In the end, you’ll have a complete and fun game. By Colin Eberhardt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Develop an iPad Board Game App: Part 2/2
30 mins
Welcome back to the final part of our 2-part iPad Board Game App Tutorial Series!
In the first part of the series, you learned how to create the game model, perform unit testing, add the game views, and add some basic game logic.
In this second and final part of the series, you will keep score, add some nice animations, and add a computer opponent. In the end, you’ll have a complete and fun game.
This tutorial picks up where you left things off in the last part. If you don’t have it already, here is project where we left things off last time.
Let’s get back to making a board game!
Who’s on First? – Keeping Score
Reversi is a competitive game, so you’ll need to add a scoring mechanism to the game. Each player’s score is simply the number of pieces each player has on the board. Sounds easy enough!
Open up SHCBoard.h and add the following:
// counts the number of cells with the given state
- (NSUInteger) countCellsWithState:(BoardCellState)state;
Then open up SHCBoard.m and implement the methods as follows:
- (NSUInteger) countCellsWithState:(BoardCellState)state
{
int count = 0;
for (int row = 0; row < 8; row++)
{
for (int col = 0; col < 8; col++)
{
if ([self cellStateAtColumn:col andRow:row] == state)
{
count++;
}
}
}
return count;
}
The countCellsWithState method simply loops over every row and column in the board, counting cells with the given state.
Now you need to make use of this new method. Open up SHCReversiBoard.m and locate makeMoveToColumn:andRow:, adding the following lines to the end of the method:
_whiteScore = [self countCellsWithState:BoardCellStateWhitePiece];
_blackScore = [self countCellsWithState:BoardCellStateBlackPiece];
This will update the whiteScore and blackScore properties every time a move is made. Updating the property won't automatically update the score value displayed to the players – you still need to notify the view that the scores have changed.
This sounds like another job for the multicasting delegate that you used in Part 1 of the tutorial. That delegate is coming in pretty handy, isn't it? :]
Highlight the ‘Model’ group in the project and create a new file with the iOS\Cocoa Touch\Objective-C protocol template, named SHCReversiBoardDelegate and add the following method to the newly created protocol under the @protocol line:
// indicates that the game state has changed
- (void) gameStateChanged;
Open up SHCReversiBoard.h and add a property that multicasts to this delegate:
// multicasts game state changes
@property (readonly) SHCMulticastDelegate* reversiBoardDelegate;
Following the same pattern that you have already used in SHCBoard, open up SHCReversiBoard.m and add the protocol import:
#import "SHCReversiBoardDelegate.h"
Add an instance variable further down in the same file, right after @implementation and next to the current _boardNavigationFunctions variable:
id<SHCReversiBoardDelegate> _delegate;
Finally, create the multicasting delegate within commonInit by adding the following lines to the end of the method:
_reversiBoardDelegate = [[SHCMulticastDelegate alloc] init];
_delegate = (id)_reversiBoardDelegate;
You don't want to keep your delegates in the dark, so when a player makes a move, the delegate should be informed that the game state has changed.
Add the following lines to the end of makeMoveToColumn:andRow: (just after the lines you added earlier to update the score):
if ([_delegate respondsToSelector:@selector(gameStateChanged)]) {
[_delegate gameStateChanged];
}
The SHCViewController has the labels that need to be updated when the score changes, so open up the SHCViewController.h to import and adopt this delegate protocol:
#import "SHCReversiBoardDelegate.h"
@interface SHCViewController : UIViewController <SHCReversiBoardDelegate>
Within SHCViewController.mlocate the viewDidLoad method and add the following to the end of the method:
[self gameStateChanged];
[_board.reversiBoardDelegate addDelegate:self];
And finally add the delegate method implementation somewhere within the file:
- (void)gameStateChanged
{
_whiteScore.text = [NSString stringWithFormat:@"%d", _board.whiteScore];
_blackScore.text = [NSString stringWithFormat:@"%d", _board.blackScore];
}
The delegate method ensures that whenever the game state changes, the labels are updated to reflect the current score. You will also notice that gameStateChanged is called initially in viewDidLoad, so that the labels reflect the state of the game when it starts.
If you recall, the starting position in Reversi is that each player has two pieces on the board — so therefore each player starts with 2 points.
Build and run your app!
Play a game against yourself and see the scoring mechanism in action, as below:
So, how did you do playing against yourself? I sure hope you won. :]
The game screens look quite nice, but the game is lacking a dynamic feel. The next section shows you how to add a bit of visual flair by using some subtle animations as the pieces are manipulated on the screen.
37 Pieces of Flair - Adding Game Animation
Open up SHCBoardSquare.m and locate the update method. Currently this method simply shows or hides the white and black playing piece images based on the cell state.
Replace update with the following code:
// updates the UI state
- (void)update
{
BoardCellState state = [_board cellStateAtColumn:_column andRow:_row];
[UIView animateWithDuration:0.5f animations:^{
_whiteView.alpha = state == BoardCellStateWhitePiece ? 1.0 : 0.0;
_whiteView.transform = state == BoardCellStateWhitePiece ? CGAffineTransformIdentity : CGAffineTransformMakeTranslation(0, -20);
_blackView.alpha = state == BoardCellStateBlackPiece ? 1.0 : 0.0;
_blackView.transform = state == BoardCellStateBlackPiece ? CGAffineTransformIdentity : CGAffineTransformMakeTranslation(0, 20);
}];
}
This improved method changes both the positioning of the game piece as well as the alpha value — or transparency value — of the playing piece when the state of the cell changes.
Build and run to see the animations in action!
Now have some nicely animated pieces, but you shouldn't just let the player keep playing forever. You need some end-game logic to check for wins or losses.
Game Over, Man - Handling the End Game
Well, there's good news — and bad news — about the end-game condition.
The bad news is that in Reversi determining the end-game condition is not as simple as checking whether or not the board is full of pieces. The game really ends when neither player can play a piece that would result in one or more of their opponent’s pieces being flipped.
The good news is that this is a very easy condition to check. Open SHCReversiBoard.m and add the following methods:
- (BOOL) hasGameFinished
{
return ![self canPlayerMakeAMove:BoardCellStateBlackPiece] &&
![self canPlayerMakeAMove:BoardCellStateWhitePiece];
}
- (BOOL) canPlayerMakeAMove:(BoardCellState) state
{
// test all the board locations to see if a move can be made
for (int row = 0; row < 8; row++)
{
for (int col = 0; col < 8; col++)
{
if ([self isValidMoveToColumn:col andRow:row forState:state])
{
return YES;
}
}
}
return NO;
}
The canPlayerMakeAMove method simply checks every square on the board to see if it would be a valid move for the given player. The hasGameFinished method does this check for both players.
To put the methods above in action, add a property to SHCReversiBoard.h that will reflect the game state:
// indicates whether the game has finished
@property (readonly) BOOL gameHasFinished;
Now head back to SHCReversiBoard.m and add the following line to update the state of property makeMoveToColumn:andRow: just before the line where the scores are updated:
_gameHasFinished = [self hasGameFinished];
The last rule to implement is that if one player cannot make a move, play switches back to the opponent.
Add the following method to SHCReversiBoard.m:
- (void) switchTurns
{
// switch players
BoardCellState nextMoveTemp = [self invertState:self.nextMove];
// only switch play if this player can make a move
if ([self canPlayerMakeAMove:nextMoveTemp])
{
_nextMove = nextMoveTemp;
}
}
Here you add a check to make sure that the player can make a move before switching turns.
To make use of this method, find the makeMoveToColumn:andRow: method in SHCReversiBoard.m and locate the line that switches from one player’s turn to the other:
_nextMove = [self invertState:_nextMove];
Replace that code with the following:
[self switchTurns];
This ensures that before switching turns, a check is made to determine if the player can actually make a move.
Finally, you will want to display something to the user once the game has finished, rather than have them staring at a board where they can't make a move.
Open up SHCViewController.m and add the following code to gameStateChanged:
_gameOverImage.hidden = !_board.gameHasFinished;
This will display the game-over image when the game ends.
Build and run your app. You can give the completed game a try, although you'll still have to play against yourself:
You’ll notice that when the game is finished, the message at the bottom of the screen tells them to “tap to restart”. You should probably deliver on that promise!
InsideSHCViewController.m, add the following code to the end of viewDidLoad:
// add a tap recognizer
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(restartGame:)];
[self.view addGestureRecognizer:tapRecognizer];
Then add the following method, which will start a new game if the current game really has finished:
- (void)restartGame:(UITapGestureRecognizer*)recognizer
{
if (_board.gameHasFinished)
{
[_board setToInitialState];
[self gameStateChanged];
}
}
You can now play over and over and over again to your heart's content!
However, after a few games it's apparent that playing against yourself always guarantees a win. If you're losing against yourself, then you've got bigger issues at hand! :]
Time to add a computer opponent to this game!