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 4 of 6 of this article. Click here to view the first page.

Game Over man, Game Over!

You're now at a point where you can build in the end-of-round logic that tests whether there is a winner. The winner is the player who now has all of the cards, i.e. all of the other players have zero cards left.

Add the following method to Game.m:

- (Player *)checkWinner
{
	__block Player *winner;

	[_players enumerateKeysAndObjectsUsingBlock:^(id key, Player *obj, BOOL *stop)
	{
		if ([obj totalCardCount] == 52)
		{
			winner = obj;
			*stop = YES;
		}
	}];

	return winner;
}

The checkWinner method loops through the players and if it finds one with 52 cards, then there is a winner. If not, it returns nil. You will call this method from resumeAfterMovingCardsForPlayer:. That's the logical place for it because this is where you end up after someone has yelled "Snap!" and you did all the animations. The changes are as follows:

- (BOOL)resumeAfterMovingCardsForPlayer:(Player *)player
{
	_mustPayCards = NO;

	Player *winner = [self checkWinner];
	if (winner != nil)
	{
		[self endRoundWithWinner:winner];
		return YES;
	}

	. . .
}

The endRoundWithWinner: method is pretty simple:

- (void)endRoundWithWinner:(Player *)winner
{
	#ifdef DEBUG
	NSLog(@"End of the round, winner is %@", winner);
	#endif

	_state = GameStateGameOver;

	winner.gamesWon++;
	[self.delegate game:self roundDidEndWithWinner:winner];
}

You change the state to GameStateGameOver, increment the gamesWon property of the winning Player, and call a new delegate method. Add this method to GameDelegate:

- (void)game:(Game *)game roundDidEndWithWinner:(Player *)player;

And implement it in GameViewController.m:

- (void)game:(Game *)game roundDidEndWithWinner:(Player *)player
{
	self.centerLabel.text = [NSString stringWithFormat:NSLocalizedString(@"** Winner: %@ **", @"Status text: winner of a round"), player.name];

	self.snapButton.hidden = YES;
	self.nextRoundButton.hidden = !game.isServer;

	[self updateWinsLabels];
	[self hideActivePlayerIndicator];
}

Try it out. For testing purposes you may want to reduce the number of cards on the deck (the way you did it before). If you do, make sure you also change the number of cards that checkWinner checks for from 52 to the number on the deck.

The end of a round

The next round

The delegate method also enabled the "next round" button on the server (bottom-right corner). Tapping this button will start a new round and deals out new cards. The UIButton is already hooked up to the nextRoundAction: method in GameViewController.m. Replace that method with:

- (IBAction)nextRoundAction:(id)sender
{
	[self.game nextRound];
}

Add the nextRound method to Game.h and Game.m:

- (void)nextRound
{
	[NSObject cancelPreviousPerformRequestsWithTarget:self];

	_state = GameStateDealing;
	_firstTime = YES;
	_busyDealing = YES;

	[self.delegate gameDidBeginNewRound:self];

	[_players enumerateKeysAndObjectsUsingBlock:^(id key, Player *player, BOOL *stop)
	{
		[player.closedCards removeAllCards];
		[player.openCards removeAllCards];
	}];

	if (self.isServer)
	{
		[self pickNextStartingPlayer];
		[self dealCards];
	}
}

The nextRound method is similar to beginGame, except that beginGame always starts from a clean slate while nextRound has to clean up the mess from the previous round first. After the dealCards method is called, the new round starts exactly the way you've seen before -- the delegate animates the dealing of the cards and then called Game's beginRound method, which activates the starting player, and so on.

You need to add a few additional methods. The pickNextStartingPlayer method looks at who started the previous round and then picks the next player clockwise to start the new round:

- (void)pickNextStartingPlayer
{
	do
	{
		_startingPlayerPosition++;
		if (_startingPlayerPosition > PlayerPositionRight)
			_startingPlayerPosition = PlayerPositionBottom;
	}
	while ([self playerAtPosition:_startingPlayerPosition] == nil);

	_activePlayerPosition = _startingPlayerPosition;
}

There is also a new delegate method, gameDidBeginNewRound:. Add this to the GameDelegate protocol in Game.h:

- (void)gameDidBeginNewRound:(Game *)game;

And implement it in GameViewController.m:

- (void)gameDidBeginNewRound:(Game *)game
{
	[self removeAllRemainingCardViews];
}

This calls a helper method, removeAllRemainingCardViews, to remove all the existing CardView objects from the screen, in order to make room for the new cards:

- (void)removeAllRemainingCardViews
{
	for (PlayerPosition p = PlayerPositionBottom; p <= PlayerPositionRight; ++p)
	{
		Player *player = [self.game playerAtPosition:p];
		if (player != nil)
		{
			[self removeRemainingCardsFromStack:player.openCards forPlayer:player];
			[self removeRemainingCardsFromStack:player.closedCards forPlayer:player];
		}
	}
}

- (void)removeRemainingCardsFromStack:(Stack *)stack forPlayer:(Player *)player
{
	NSTimeInterval delay = 0.0f;

	for (int t = 0; t < [stack cardCount]; ++t)
	{
		NSUInteger index = [stack cardCount] - t - 1;
		CardView *cardView = [self cardViewForCard:[stack cardAtIndex:index]];
		if (t < 5)
		{
			[cardView animateRemovalAtRoundEndForPlayer:player withDelay:delay];
			delay += 0.05f;
		}
		else
		{
			[cardView removeFromSuperview];
		}
	}
}

Because there will be 52 CardViews on the screen (all belonging to one player), animating them all would take a while and it's not really that interesting to see all 52 views move off the screen. So instead, you only animate the top 5 cards from the stack, which is enough to give the user the impression that the table is being wiped clean, and simply remove all the other card views without an animation.

This requires a new method to be added to CardView.h and CardView.m:

- (void)animateRemovalAtRoundEndForPlayer:(Player *)player withDelay:(NSTimeInterval)delay
{
	[UIView animateWithDuration:0.2f
		delay:delay
		options:UIViewAnimationOptionCurveEaseIn
		animations:^
		{
			self.center = CGPointMake(self.center.x, self.superview.bounds.size.height + CardHeight);
		}
		completion:^(BOOL finished)
		{
			[self removeFromSuperview];
		}];
}

You can try it out now, but the next round starts only at the server. That is because the server hasn't told the clients yet that a new round is starting. You could introduce a new packet type for that, but what actually happens is that the server already sends the clients a DealCards packet (because you call the dealCards method), except that the clients weren't expecting this packet at that time so they ignored it.

You can say that if a client is in the "game over" state, any DealCards packet that is received will begin the new round. Change the case-statement in clientReceivedPacket: in Game.m to:

		case PacketTypeDealCards:
			if (_state == GameStateGameOver)
			{
				[self nextRound];
			}
			if (_state == GameStateDealing)
			{
				[self handleDealCardsPacket:(PacketDealCards *)packet];
			}
			break;

Notice that the second if-statement is not an "else if". After calling nextRound, the state equals GameStateDealing, and you have to actually deal out the cards.

W00t, that's it for the multiplayer game! :-)

Note: The game still needs quite a bit of work before it's ready to go on the App Store. In its current form it's a bit unfair. The player who hosts the game has an advantage because he sees everything a fraction of a second before the other players. This could be solved by delaying the animations on the server by the average "ping" time.

The restriction that you put in that disables the Snap! button after a player turns a card is also very unfair. That was done to avoid a problem with the networking, but it's obviously not how you really should handle this.

Regardless of these issues, I hope that this has been a good illustration of everything that's involved in making networked multiplayer games, and card games in particular. I wanted to show you the difficulties of writing multiplayer code, so that you know to deal with such situations in your own games.

Note: The game still needs quite a bit of work before it's ready to go on the App Store. In its current form it's a bit unfair. The player who hosts the game has an advantage because he sees everything a fraction of a second before the other players. This could be solved by delaying the animations on the server by the average "ping" time.

The restriction that you put in that disables the Snap! button after a player turns a card is also very unfair. That was done to avoid a problem with the networking, but it's obviously not how you really should handle this.

Regardless of these issues, I hope that this has been a good illustration of everything that's involved in making networked multiplayer games, and card games in particular. I wanted to show you the difficulties of writing multiplayer code, so that you know to deal with such situations in your own games.

Contributors

Over 300 content creators. Join our team.