How To Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 7

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on Google+ and Twitter. Welcome back to our monster 7-part tutorial series on creating a multiplayer card game over Bluetooth or Wi-Fi using UIKit! If you are new to this series, check out the […] By Matthijs Hollemans.

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

Single player mode

The reason this game has a single player mode is that I actually wrote it as a single-player game first, just to figure out how the gameplay logic should work. As you've seen, multiplayer networking code can get pretty complicated and as a general development principle, I try to avoid dealing with too much complexity at once.

For the purposes of explaining everything, however, it made sense that the tutorial started with the multiplayer aspects first. Just to show you how, you'll now put the single player mode back into the game.

There are basically two ways you can add single-player functionality into a multiplayer game:

  1. Keep the networking logic but instead of sending packets over the network, deliver them immediately to the clientReceivedPacket: method. The client and server logic are handled by one and the same Game object. (Of course, you can also make two different Game objects if that makes sense for your game, one that handles the client logic and one that does the server logic.)
  2. Don't send any packets, but use timers to call the Game methods directly. This is the approach taken by Snap!, because it makes a bit more sense for our game. Each client has a slightly different view of the game world -- player positions are rotated, for example, so that the client's own player always sits at the bottom -- and that makes it tricky to make Game act as both client and server.

Add the following methods to Game.m:

- (void)startSinglePlayerGame
{
	self.isServer = YES;

	Player *player = [[Player alloc] init];
	player.name = NSLocalizedString(@"You", @"Single player mode, name of user (bottom player)");
	player.peerID = @"1";
	player.position = PlayerPositionBottom;
	[_players setObject:player forKey:player.peerID];

	player = [[Player alloc] init];
	player.name = NSLocalizedString(@"Ray", @"Single player mode, name of left player");
	player.peerID = @"2";
	player.position = PlayerPositionLeft;
	[_players setObject:player forKey:player.peerID];

	player = [[Player alloc] init];
	player.name = NSLocalizedString(@"Lucy", @"Single player mode, name of top player");
	player.peerID = @"3";
	player.position = PlayerPositionTop;
	[_players setObject:player forKey:player.peerID];

	player = [[Player alloc] init];
	player.name = NSLocalizedString(@"Steve", @"Single player mode, name of right player");
	player.peerID = @"4";
	player.position = PlayerPositionRight;
	[_players setObject:player forKey:player.peerID];

	[self beginGame];
}

- (BOOL)isSinglePlayerGame
{
	return (_session == nil);
}

Also add the startSinglePlayerGame method signature to Game.h. Note: You set the isServer property to YES because you want the game to act as the server but with four players instead of one.

Now all the way back in MainViewController.m (remember that one?), replace the following method:

- (IBAction)singlePlayerGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{
			[self startGameWithBlock:^(Game *game)
			{
				[game startSinglePlayerGame];
			}];
		}];
	}
}

You can run the game now and tap the Single Player Game button from the main menu:

Single player game

There are two things you should notice: the computer players (the ones on the left, top and right) don't actually turn over their cards, and the Xcode debug output pane says this:

Error sending data to clients: (null)

Let's fix this second issue first. The server still tries to send out DealCards and ActivatePlayer packets, even though there is no one to send them to. To prevent this, change sendPacketToAllClients: in Game.m to:

- (void)sendPacketToAllClients:(Packet *)packet
{
	if ([self isSinglePlayerGame])
		return;

	. . .
}

As a defensive programming measure, you should also add an assertion to sendPacketToServer:

- (void)sendPacketToServer:(Packet *)packet
{
	NSAssert(![self isSinglePlayerGame], @"Should not send packets in single player mode");

	. . .
}

In the quitGameWithReason: method, change the if-statement to:

	if (reason == QuitReasonUserQuit && ![self isSinglePlayerGame])
	{
		. . .
	}

Now you should no longer get any networking errors (because you no longer do any networking).

In beginRound, make the following change:

- (void)beginRound
{
	if ([self isSinglePlayerGame])
		_state = GameStatePlaying;

	. . .
}

The reason for this is that in multiplayer mode, the game state changes from GameStateDealing to GameStatePlaying after the server received ClientDealtCards packets from all clients. But because that never happens now, you have to change the state manually here.

There are two things that the computer players need to do: 1) turn over their cards, 2) yell "Snap!" whenever they detect a match. You'll do the card flipping first. Change the activatePlayerAtPosition: method to the following:

- (void)activatePlayerAtPosition:(PlayerPosition)playerPosition
{
	_hasTurnedCard = NO;

	if ([self isSinglePlayerGame])
	{
		if (_activePlayerPosition != PlayerPositionBottom)
			[self scheduleTurningCardForComputerPlayer];
	}
	else if (self.isServer)
	{
		NSString *peerID = [self activePlayer].peerID;
		Packet* packet = [PacketActivatePlayer packetWithPeerID:peerID];
		[self sendPacketToAllClients:packet];
	}

	[self.delegate game:self didActivatePlayer:[self activePlayer]];
}

Only the first if-statement is new. If this is a single-player game and the active player is not the user (the one at the bottom), then call scheduleTurningCardForComputerPlayer to turn over the card. It doesn't turn the card immediately but after a random delay, to keep things more exciting. Add this new method:

- (void)scheduleTurningCardForComputerPlayer
{
	NSTimeInterval delay = 0.5f + RANDOM_FLOAT() * 2.0f;
	[self performSelector:@selector(turnCardForActivePlayer) withObject:nil afterDelay:delay];
}

Now you can run the app again and play against the computer players. It'll be a pretty easy game, though, because you're still the only one who will yell "Snap!". You will put the logic for that in turnCardForActivePlayer, because that's the moment when a new match will become visible:

- (void)turnCardForActivePlayer
{
	if ([self isSinglePlayerGame])
	{
		[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(playerCalledSnap:) object:[self playerAtPosition:PlayerPositionLeft]];
		[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(playerCalledSnap:) object:[self playerAtPosition:PlayerPositionTop]];
		[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(playerCalledSnap:) object:[self playerAtPosition:PlayerPositionRight]];
	}

	[self turnCardForPlayer:[self activePlayer]];

	if ([self isSinglePlayerGame])
	{
		if ([_matchingPlayers count] > 0 || RANDOM_INT(50) == 0)
		{
			for (PlayerPosition p = PlayerPositionLeft; p <= PlayerPositionRight; ++p)
			{
				Player *computerPlayer = [self playerAtPosition:p];
				if ([computerPlayer totalCardCount] > 0)
				{
					NSTimeInterval delay = 0.5f + RANDOM_FLOAT() * 2.0f;
					[self performSelector:@selector(playerCalledSnap:) withObject:computerPlayer afterDelay:delay];
				}
			}
		}
	}

	if (self.isServer)
		[self performSelector:@selector(activateNextPlayer) withObject:nil afterDelay:0.5f];
}

First you cancel any "Snap!" messages from computer players that still may be pending. These are for the previous set of cards that you're on the table and therefore are now out-of-date. Then if there is a match, or randomly every one out of 50 or so times, you'll schedule a playerCalledSnap: message after a random delay for all computer players. The RANDOM_INT(50) bit is so that even computer players sometimes yell "Snap!" when there aren't any matches.

There are a few small things you still need to do in order to wrap this up. In playerCalledSnap: you need to cancel any scheduled calls to turnCardForActivePlayer. You don't want the active player to flip over his top card when someone just yelled "Snap!", because that might invalidate the Snap!. In a multiplayer game you cannot prevent this from happening -- everything happens completely asynchronously there -- but it's also less likely to occur. In a single-player game it happens all the time and looks weird. So change the method to:

- (void)playerCalledSnap:(Player *)player
{
	if (self.isServer)
	{
		if (_haveSnap)
		{
			. . .
		}
		else
		{
			if ([self isSinglePlayerGame])
				[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(turnCardForActivePlayer) object:nil];

			. . .

And finally, in resumeAfterMovingCardsForPlayer:, you have to turn that card if you cancelled it earlier:

- (BOOL)resumeAfterMovingCardsForPlayer:(Player *)player
{
	. . .

	else if ([self isSinglePlayerGame] && _activePlayerPosition != PlayerPositionBottom)
	{
		[self scheduleTurningCardForComputerPlayer];
		return NO;
	}

	return NO;
}

Believe it or not, that completes the single-player mode! It was pretty easy to add because all the game logic already existed, you just had to build some simple rules to drive the computer AI.

Contributors

Over 300 content creators. Join our team.