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

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

Recycling cards

What happens when a player runs out of cards? According to the rules of Snap!, he takes his pile of face-up cards and flips it over, so these now become his closed cards again. I named this process "recycling" and you'll have to add a nice animation for that.

Add the following method to Player.h and Player.m:

- (BOOL)shouldRecycle
{
	return ([self.closedCards cardCount] == 0) && ([self.openCards cardCount] > 1);
}

This means the player's cards should be recycled when he has no closed cards left and at least more than one open card. You don't recycle the cards when there is just one open card because that is a little silly, the player will just be turning over that same card all the time.

Now the question is, where do you check whether the player's cards need to be recycled? I decided a good moment is when the next player is activated. If this new player has no more cards to turn over at that point, you first recycle his old cards.

In Game.m, change the activateNextPlayer method to:

- (void)activateNextPlayer
{
	NSAssert(self.isServer, @"Must be server");

	while (true)
	{
		_activePlayerPosition++;
		if (_activePlayerPosition > PlayerPositionRight)
			_activePlayerPosition = PlayerPositionBottom;

		Player *nextPlayer = [self activePlayer];
		if (nextPlayer != nil)
		{
			if ([nextPlayer.closedCards cardCount] > 0)
			{
				[self activatePlayerAtPosition:_activePlayerPosition];
				return;
			}

			if ([nextPlayer shouldRecycle])
			{
				[self activatePlayerAtPosition:_activePlayerPosition];
				[self recycleCardsForActivePlayer];
				return;
			}
		}
	}
}

This now loops through all the players until it finds one that still has cards left to turn over, or cards that can be recycled. Note that this skips players who have just one open card.

The recycleCardsForActivePlayer method is new:

- (void)recycleCardsForActivePlayer
{
	Player *player = [self activePlayer];

	NSArray *recycledCards = [player recycleCards];
	NSAssert([recycledCards count] > 0, @"Should have cards to recycle");

	[self.delegate game:self didRecycleCards:recycledCards forPlayer:player];
}

This is pretty simple, it calls a new recycleCards method on the Player object, and then notifies the delegate so that it can do an animation to move the cards from the open stack back to the closed stack.

Add the recycleCards signature to Player.h:

- (NSArray *)recycleCards;

And the method itself to Player.m:

- (NSArray *)recycleCards
{
	return [self giveAllOpenCardsToPlayer:self];
}

- (NSArray *)giveAllOpenCardsToPlayer:(Player *)otherPlayer
{
	NSUInteger count = [self.openCards cardCount];
	NSMutableArray *movedCards = [NSMutableArray arrayWithCapacity:count];

	for (NSUInteger t = 0; t < count; ++t)
	{
		Card *card = [self.openCards cardAtIndex:t];
		card.isTurnedOver = NO;
		[otherPlayer.closedCards addCardToBottom:card];
		[movedCards addObject:card];
	}

	[self.openCards removeAllCards];
	return movedCards;
}

As you can see, recycleCards is actually implemented in terms of another method, giveAllOpenCardsToPlayer:. That is because later on you will also need to move those cards around when another player yells "Snap!" and wins the cards -- it's the same logic, whether the source and destination player are different or the same person.

This does require us to add the addCardToBottom: and removeAllCards methods to the Stack class. Add the signatures to Stack.h and the methods to Stack.m:

- (void)addCardToBottom:(Card *)card
{
	NSAssert(card != nil, @"Card cannot be nil");
	NSAssert([_cards indexOfObject:card] == NSNotFound, @"Already have this Card");
	[_cards insertObject:card atIndex:0];
}

- (void)removeAllCards
{
	[_cards removeAllObjects];
}

That's it for the data model methods, now the new delegate method. Add this to the GameDelegate protocol:

- (void)game:(Game *)game didRecycleCards:(NSArray *)recycledCards forPlayer:(Player *)player;

And implement the method in GameViewController.m:

- (void)game:(Game *)game didRecycleCards:(NSArray *)recycledCards forPlayer:(Player *)player
{
	self.snapButton.enabled = NO;
	self.turnOverButton.enabled = NO;

	NSTimeInterval delay = 0.0f;
	for (Card *card in recycledCards)
	{
		CardView *cardView = [self cardViewForCard:card];
		[cardView animateRecycleForPlayer:player withDelay:delay];
		delay += 0.025f;
	}

	[self performSelector:@selector(afterRecyclingCardsForPlayer:) withObject:player afterDelay:delay + 0.5f];
}

- (void)afterRecyclingCardsForPlayer:(Player *)player
{
	self.snapButton.enabled = YES;
	self.turnOverButton.enabled = YES;

	[self.game resumeAfterRecyclingCardsForPlayer:player];
}

Game calls this delegate method with the list of cards that were recycled (you got that list from Player's recycleCards method). You simply call the animateRecycleForPlayer:withDelay: method on each CardView from the list of recycled Cards. Then half a second later you call afterRecyclingCardsForPlayer: to resume the game.

Add this new method to Game.h and Game.m but leave it empty for now. Later you'll do more stuff here. (It may happen that a player yells "Snap!" when there are no matching cards on the table and then the player has to pay one card to every other player. In that case it can occur that the player must first recycle his cards, that's what this method is really for.)

- (void)resumeAfterRecyclingCardsForPlayer:(Player *)player
{
}

Then finally the animation in CardView. Add the following method to both CardView.h and CardView.m:

- (void)animateRecycleForPlayer:(Player *)player withDelay:(NSTimeInterval)delay
{
	[self.superview sendSubviewToBack:self];

	[self unloadFront];
	[self loadBack];

	[UIView animateWithDuration:0.2f
		delay:delay
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.center = [self centerForPlayer:player];
		}
		completion:nil];
}

- (void)unloadFront
{
	[_frontImageView removeFromSuperview];
	_frontImageView = nil;
}

Cool, now the code should compile without errors and you can try it out. However, if you only have two players then there's a lot of tapping involved before you get to the end of a player's stack, so just for testing it makes sense to reduce the number of cards in the deck. In Deck.m's setUpCards method, replace the second for-loop with:

		for (int value = /*CardAce*/CardQueen; value <= CardKing; ++value)

I simply commented out the starting value (CardAce) and replaced it with CardQueen. Now there will be only eight cards in the deck (four queens and four kings). Of course you do have to remember to restore this back to the original value when you're done testing!

Try it out. When a player runs out of cards, on his next turn the cards are recycled and moved from his open stack to his closed stack again. Except... it doesn't work on the client yet.

The server shows the animation just fine, regardless of who it is performed on, a client or the server itself, but clients don't. That's because you only call recycleCardsForActivePlayer from activateNextPlayer and that method can only be called by the server. Clients call activatePlayerWithPeerID: instead, so you have to put the recycle detection logic in there too (in Game.m):

- (void)activatePlayerWithPeerID:(NSString *)peerID
{
	. . .

	if ([player shouldRecycle])
	{
		[self recycleCardsForActivePlayer];
	}
}

Awesome, now it works on the clients too.

Contributors

Over 300 content creators. Join our team.