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

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
Create a multiplayer networked card game!

Create a multiplayer networked card game!

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on 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 introduction first. There you can see a video of the game, and we’ll invite you to our special Reader’s Challenge!

In the first, second, third, and fourth parts of the series, you created a basic user interface and created the networking infrastructure for the game. At this point, the clients connect to the server, handshake, and everyone is ready to play!

In this fifth part of the series, we’ll deal with the cards (pun intended!). :] We’ll create the model classes for the Card, Deck, and Stack, create the animations and logic for dealing the cards and allowing the first player to flip over cards.

Dealing the cards

All the setup you’ve done so far was to get the players connected and talking to each other. Now you can finally get to the fun stuff… the actual card game!

Let’s start with Game.m. Find the beginGame method and change it to:

- (void)beginGame
{
	_state = GameStateDealing;
	[self.delegate gameDidBegin:self];

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

This modifies the method so that when the game begins, the server picks a random player to start their turn, and deals cards to all the players. Inside these methods, you’ll be adding some code to notify the clients who got what cards so they can show the appropriate dealing animation as well.

For pickRandomStartingPlayer to work, you first need to add two instance variables:

@implementation Game
{
	. . .
	PlayerPosition _startingPlayerPosition;
	PlayerPosition _activePlayerPosition;
}

The _startingPlayerPosition variable keeps track of which player started the round. With each new round, you will increment this variable, so that the next player clockwise gets to start the next round. The _activePlayerPosition keeps track of whose turn it is in the current round.

Both variables will be filled in by pickRandomStartingPlayer, which you’ll implement now! Add this new method:

- (void)pickRandomStartingPlayer
{
	do
	{
		_startingPlayerPosition = arc4random() % 4;
	}
	while ([self playerAtPosition:_startingPlayerPosition] == nil);

	_activePlayerPosition = _startingPlayerPosition;
}

This picks one of the four possible positions at random, but only chooses it if there’s actually a Player object at that position (if less than four players are signed in). This works because arc4random() % 4 returns a number between 0 and 3, and that’s exactly the numeric values for the symbols from the PlayerPosition enum.

The dealCards method is going to be a bit more complex. Here you have to assign Card objects to the Players. The Card objects are drawn from a Deck of 52 cards. Each player has two Stacks of Cards: closed cards, which are face down, and open cards, which have been turned over. These are all new data model classes that you haven’t written yet, so let’s add these new classes now.

As a reminder, here’s the class diagram for this app. Pay particular attention to the Player, Stack, Card, and Deck classes and re-read the paragraph above, and make sure you understand how they all fit together.

The model-view-controller class diagram

For the time being, simply add an empty version of the dealCards method, so the code compiles again:

- (void)dealCards
{
}

Building the Card and Deck data model classes

Let’s begin with the Card class, which represents a playing card. A playing card has basically two attributes: a suit — spades, hearts, clubs or diamonds — and a value. So this will be a pretty simple class.

Add a new Objective-C class to the project, named Card, subclass of NSObject. To keep things tidy, put the new .h and .m files in the “Data Model” group. Then replace the contents of Card.h with:

typedef enum
{
	SuitClubs,
	SuitDiamonds,
	SuitHearts,
	SuitSpades
}
Suit;

#define CardAce   1
#define CardJack  11
#define CardQueen 12
#define CardKing  13

@interface Card : NSObject

@property (nonatomic, assign, readonly) Suit suit;
@property (nonatomic, assign, readonly) int value;

- (id)initWithSuit:(Suit)suit value:(int)value;

@end

Here you use an enum for the suit. The value ranges from 1 to 13, where 1 is the Ace, 2 through 10 are the number cards, 11 is the jack, 12 is the queen and 13 is the king. Snap! is played without jokers, so you don’t need a value for those.

Next replace Card.m with:

#import "Card.h"

@implementation Card

@synthesize suit = _suit;
@synthesize value = _value;

- (id)initWithSuit:(Suit)suit value:(int)value
{
	NSAssert(value >= CardAce && value <= CardKing, @"Invalid card value");

	if ((self = [super init]))
	{
		_suit = suit;
		_value = value;
	}
	return self;
}

@end

Initially, the cards are added to a Deck. The deck will contain 52 cards (4 suits of 13 values each). The deck can be shuffled and you can draw cards from it, which is what you'll do during dealing.

Let's create this class next. Add a new Objective-C class to the project, named Deck, subclass of NSObject. Replace Deck.h with:

@class Card;

@interface Deck : NSObject

- (void)shuffle;
- (Card *)draw;
- (int)cardsRemaining;

@end

And replace Deck.m with:

#import "Deck.h"
#import "Card.h"

@implementation Deck
{
	NSMutableArray *_cards;
}

- (void)setUpCards
{
	for (Suit suit = SuitClubs; suit <= SuitSpades; ++suit)
	{
		for (int value = CardAce; value <= CardKing; ++value)
		{
			Card *card = [[Card alloc] initWithSuit:suit value:value];
			[_cards addObject:card];
		}
	}
}

- (id)init
{
	if ((self = [super init]))
	{
		_cards = [NSMutableArray arrayWithCapacity:52];
		[self setUpCards];
	}
	return self;
}

- (int)cardsRemaining
{
	return [_cards count];
}

@end

Deck has a list of Card objects (an NSMutableArray) that is filled up in init by setUpCards. This puts the cards in a very definite order, first all the clubs, then all the diamonds, etc. So you need to add a shuffle method that randomizes the contents of the deck:

- (void)shuffle
{
	NSUInteger count = [_cards count];
	NSMutableArray *shuffled = [NSMutableArray arrayWithCapacity:count];

	for (int t = 0; t < count; ++t)
	{
		int i = arc4random() % [self cardsRemaining];
		Card *card = [_cards objectAtIndex:i];
		[shuffled addObject:card];
		[_cards removeObjectAtIndex:i];
	}

	NSAssert([self cardsRemaining] == 0, @"Original deck should now be empty");

	_cards = shuffled;
}

There are a couple of different ways that you can randomize the contents of an array, but I like this particular one. You first allocate a new mutable array named "shuffled". You loop through the original array, pick a card at a random position and add that to the end of the shuffled array. Then you remove that card from the original array, and repeat.

By the time the loop ends there should be no more cards in the original array -- as a defensive programming measure you verify that with the NSAssert. Just before you return, you assign the shuffled array back to _cards, so the Deck will use the randomized array from then on.

The last method for Deck is draw, which will remove the top-most card from the deck and return it:

- (Card *)draw
{
	NSAssert([self cardsRemaining] > 0, @"No more cards in the deck");
	Card *card = [_cards lastObject];
	[_cards removeLastObject];
	return card;
}

OK, now that you have both the Deck and Card classes, you can partially implement the dealCards method. So head on back to Game.m and import these new classes:

#import "Card.h"
#import "Deck.h"

Now change dealCards to:

- (void)dealCards
{
	NSAssert(self.isServer, @"Must be server");
	NSAssert(_state == GameStateDealing, @"Wrong state");

	Deck *deck = [[Deck alloc] init];
	[deck shuffle];

	while ([deck cardsRemaining] > 0)
	{
		for (PlayerPosition p = _startingPlayerPosition; p < _startingPlayerPosition + 4; ++p)
		{
			Player *player = [self playerAtPosition:(p % 4)];
			if (player != nil && [deck cardsRemaining] > 0)
			{
				Card *card = [deck draw];				
				NSLog(@"player at position %d should get card %@", player.position, card);
			}
		}
	}
}

You allocate a new Deck, then shuffle it. As long as the deck still has cards in it, you loop through the Player objects and draw a new Card for each player.

Note: Notice that the for-loop begins at _startingPlayerPosition and ends at _startingPlayerPosition + 4. This is a little trick that takes advantage of the fact that the PlayerPosition enum starts at 0 (PlayerPositionBottom) and goes clockwise (to left, top, right).

By adding 4 to the starting position and then looking at "p % 4", you always go around the table clockwise, no matter what the value of _startingPlayerPosition is. You'll use this trick a couple more times later on.

Try it out. The server should now output something like this:

player at position 1 should get card <Card: 0x924d280>
player at position 2 should get card <Card: 0x9253180>
player at position 0 should get card <Card: 0x923f5f0>
player at position 1 should get card <Card: 0x9220b60>
player at position 2 should get card <Card: 0x924fbc0>
player at position 0 should get card <Card: 0x9257210>
... and so on ...

In this case, the starting player is at position 1 (PlayerPostionLeft). You always deal out all the cards from the deck, so with three players one player has one card more than the others because 52 doesn't evenly divide by 3.

More data model: the Stack class

Now let's assign the Card objects to the Players. For this you need to make a new class, Stack, that simply keeps a pile of cards.

Add a new Objective-C class to the project, named Stack, subclass of NSObject. This is also a data model class. Replace Stack.h with:

@class Card;

@interface Stack : NSObject

- (void)addCardToTop:(Card *)card;
- (NSUInteger)cardCount;
- (NSArray *)array;

@end

And Stack.m with:

#import "Stack.h"
#import "Card.h"

@implementation Stack
{
	NSMutableArray *_cards;
}

- (id)init
{
	if ((self = [super init]))
	{
		_cards = [NSMutableArray arrayWithCapacity:26];
	}
	return self;
}

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

- (NSUInteger)cardCount
{
	return [_cards count];
}

- (NSArray *)array
{
	return [_cards copy];
}

@end

Instead of making this Stack class, you could have just given Player two NSMutableArrays to keep its piles of cards in, but having a separate Stack class makes the code clearer.

You'll give the Player two Stack properties, so add a forward declaration in Player.h:

@class Card;
@class Stack;

And add the properties:

@property (nonatomic, strong, readonly) Stack *closedCards;
@property (nonatomic, strong, readonly) Stack *openCards;

You declare these properties readonly because outside objects cannot replace the Stack object with another. In Player.m, import the classes you just made:

#import "Card.h"
#import "Stack.h"

Then synthesize the properties:

@synthesize closedCards = _closedCards;
@synthesize openCards = _openCards;

And create the objects in the init method:

- (id)init
{
	if ((self = [super init]))
	{
		_closedCards = [[Stack alloc] init];
		_openCards = [[Stack alloc] init];
	}
	return self;
}

Now switch back to Game.m and finish the dealCards method:

- (void)dealCards
{
	NSAssert(self.isServer, @"Must be server");
	NSAssert(_state == GameStateDealing, @"Wrong state");

	Deck *deck = [[Deck alloc] init];
	[deck shuffle];

	while ([deck cardsRemaining] > 0)
	{
		for (PlayerPosition p = _startingPlayerPosition; p < _startingPlayerPosition + 4; ++p)
		{
			Player *player = [self playerAtPosition:(p % 4)];
			if (player != nil && [deck cardsRemaining] > 0)
			{
				Card *card = [deck draw];
				[player.closedCards addCardToTop:card];
			}
		}
	}

	Player *startingPlayer = [self activePlayer];
	[self.delegate gameShouldDealCards:self startingWithPlayer:startingPlayer];
}

This requires some new imports:

#import "Player.h"
#import "Stack.h"

You're also calling a new GameDelegate method, gameShouldDealCards:startingWithPlayer:. The Player object that you give it comes from the [self activePlayer] method which you haven't added yet, so go ahead and add this simple implementation:

- (Player *)activePlayer
{
	return [self playerAtPosition:_activePlayerPosition];
}

Add the new delegate method to the protocol in Game.h:

- (void)gameShouldDealCards:(Game *)game startingWithPlayer:(Player *)startingPlayer;

And implement it in GameViewController.m:

- (void)gameShouldDealCards:(Game *)game startingWithPlayer:(Player *)startingPlayer
{
	self.centerLabel.text = NSLocalizedString(@"Dealing...", @"Status text: dealing");
}

There's more to come in that method, but this is a good time to build & run the app to see if it still works. The text on the screen should now change to "Dealing...".

The dealing cards animation

So far you have created only data model classes and used few of the standard views (UILabels, UIImageView). In this section you will make a CardView class that represents a Card on the screen. In a Cocos2D game you might make Card extend from CCSprite, in which case it serves both as the data model and the view, but in Snap! you'll strictly separate our classes along the lines of the Model-View-Controller pattern.

Add a new Objective-C class to the project, named CardView, subclass of UIView. Place it in the Views group. Replace the contents of CardView.h with:

const CGFloat CardWidth;
const CGFloat CardHeight;

@class Card;
@class Player;

@interface CardView : UIView

@property (nonatomic, strong) Card *card;

- (void)animateDealingToPlayer:(Player *)player withDelay:(NSTimeInterval)delay;

@end

The CardView object has a reference to the Card object that it represents, and a method that performs the dealing animation. Over the course of this tutorial you'll be adding more animation methods to this class.

Replace CardView.m with:

#import "CardView.h"
#import "Card.h"
#import "Player.h"

const CGFloat CardWidth = 67.0f;   // this includes drop shadows
const CGFloat CardHeight = 99.0f;

@implementation CardView
{
	UIImageView *_backImageView;
	UIImageView *_frontImageView;
	CGFloat _angle;
}

@synthesize card = _card;

- (id)initWithFrame:(CGRect)frame
{
	if ((self = [super initWithFrame:frame]))
	{
		self.backgroundColor = [UIColor clearColor];
		[self loadBack];
	}
	return self;
}

- (void)loadBack
{
	if (_backImageView == nil)
	{
		_backImageView = [[UIImageView alloc] initWithFrame:self.bounds];
		_backImageView.image = [UIImage imageNamed:@"Back"];
		_backImageView.contentMode = UIViewContentModeScaleToFill;
		[self addSubview:_backImageView];
	}
}

@end

CardView is a UIView (with dimensions of CardWidth by CardHeight points). It employs a UIImageView as a subview that contains the image of the card.

In the Images folder for last part's resources (which you already should have added to the project earlier), there are 52 images for the front of the cards, and one for the back.

I made these images by scanning in an ancient deck of cards -- a family heirloom -- and fixing them up in Photoshop. Here are some of these images:

The card images

Initially the card is face down, so you only load the back image. The animation is performed in the following method. Add it to CardView.m:

- (void)animateDealingToPlayer:(Player *)player withDelay:(NSTimeInterval)delay
{
	self.frame = CGRectMake(-100.0f, -100.0f, CardWidth, CardHeight);
	self.transform = CGAffineTransformMakeRotation(M_PI);

	CGPoint point = [self centerForPlayer:player];
	_angle = [self angleForPlayer:player];

	[UIView animateWithDuration:0.2f
		delay:delay
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.center = point;
			self.transform = CGAffineTransformMakeRotation(_angle);
		}
		completion:nil];
}

The card view starts out off-screen (a little beyond the top-left corner), rotated upside-down from the point of view of the bottom player (which is the user of the device). Then you calculate the final position and angle for the card, and set these in a UIView animation block. Pretty simple, but you do have to add these two helper methods:

- (CGPoint)centerForPlayer:(Player *)player
{
	CGRect rect = self.superview.bounds;
	CGFloat midX = CGRectGetMidX(rect);
	CGFloat midY = CGRectGetMidY(rect);
	CGFloat maxX = CGRectGetMaxX(rect);
	CGFloat maxY = CGRectGetMaxY(rect);

	CGFloat x = -3.0f + RANDOM_INT(6) + CardWidth/2.0f;
	CGFloat y = -3.0f + RANDOM_INT(6) + CardHeight/2.0f;

	if (player.position == PlayerPositionBottom)
	{
		x += midX - CardWidth - 7.0f;
		y += maxY - CardHeight - 30.0f;
	}
	else if (player.position == PlayerPositionLeft)
	{
		x += 31.0f;
		y += midY - CardWidth - 45.0f;
	}
	else if (player.position == PlayerPositionTop)
	{
		x += midX + 7.0f;
		y += 29.0f;
	}
	else
	{
		x += maxX - CardHeight + 1.0f;
		y += midY - 30.0f;
	}

	return CGPointMake(x, y);
}

There are four possible player positions and you want to make it look like the players are gathered around a table. So depending on the player's position you have to calculate the position of the cards, but also the rotation angle of these cards. You add a little random fudge factor to the final position, to give a more realistic feel to how the cards end up on a pile.

Also add the angleForPlayer: method:

- (CGFloat)angleForPlayer:(Player *)player
{
	float theAngle = (-0.5f + RANDOM_FLOAT()) / 4.0f;

	if (player.position == PlayerPositionLeft)
		theAngle += M_PI / 2.0f;
	else if (player.position == PlayerPositionTop)
		theAngle += M_PI;
	else if (player.position == PlayerPositionRight)
		theAngle -= M_PI / 2.0f;

	return theAngle;
}

Here you also fudge the angle a little bit, for added realism. The RANDOM_INT() and RANDOM_FLOAT() are macros that need to be added to Snap-Prefix.pch:

// Returns a random number between 0.0 and 1.0 (inclusive).
#define RANDOM_FLOAT() ((float)arc4random()/0xFFFFFFFFu)

// Returns a random number between 0 and n (inclusive).
#define RANDOM_INT(n) (arc4random() % (n + 1))

This gives us a CardView class that can be animated, so let's put it to good use.

In GameViewController.m, you have to create these CardView objects, add them to the main view (actually to the "Card Container" subview), and tell them to start animating. This happens in the gameShouldDealCards:startingWithPlayer: method. Augment this method to do the following:

- (void)gameShouldDealCards:(Game *)game startingWithPlayer:(Player *)startingPlayer
{
	self.centerLabel.text = NSLocalizedString(@"Dealing...", @"Status text: dealing");

	self.snapButton.hidden = YES;
	self.nextRoundButton.hidden = YES;

	NSTimeInterval delay = 1.0f;

	for (int t = 0; t < 26; ++t)
	{
		for (PlayerPosition p = startingPlayer.position; p < startingPlayer.position + 4; ++p)
		{
			Player *player = [self.game playerAtPosition:p % 4];
			if (player != nil && t < [player.closedCards cardCount])
			{
				CardView *cardView = [[CardView alloc] initWithFrame:CGRectMake(0, 0, CardWidth, CardHeight)];
				cardView.card = [player.closedCards cardAtIndex:t];
				[self.cardContainerView addSubview:cardView];
				[cardView animateDealingToPlayer:player withDelay:delay];
				delay += 0.1f;
			}
		}
	}
}

You loop through the players, clockwise from the starting player's position, and for each Card that this Player has you add a CardView and tell it to animate. The delay parameter prevents the cards from flying out all at once. Notice you loop from 0 to 26 because no player will ever have more than 26 cards at this point.

This requires a bunch of imports to compile:

#import "Card.h"
#import "CardView.h"
#import "Player.h"
#import "Stack.h"

You also need to add the cardAtIndex: method to Stack.h and Stack.m:

- (Card *)cardAtIndex:(NSUInteger)index
{
	return [_cards objectAtIndex:index];
}

Now you can run the app and have a cool dealing animation. A screenshot really doesn't do it justice, but this is what it looks like:

The cards dealing animation

Gratuitous sound effects

Let's spice this up with some sound!

The last part's resources also included a Sound folder that contains several .caf files. Add that folder to the project, and make sure they are added to your Snap target.

You'll be using the AVAudioPlayer class to play the sound effects, so import the headers for the AVFoundation.framework in Snap-Prefix.pch (the framework itself should already have been added to the project):

	#import <AVFoundation/AVFoundation.h>

Now add a new instance variable to GameViewController.m:

@implementation GameViewController
{
	. . .
	AVAudioPlayer *_dealingCardsSound;
}

Then add a new method named loadSounds to create this AVAudioPlayer object:

- (void)loadSounds
{
	AVAudioSession *audioSession = [AVAudioSession sharedInstance];
	audioSession.delegate = nil;
	[audioSession setCategory:AVAudioSessionCategoryAmbient error:NULL];
	[audioSession setActive:YES error:NULL];

	NSURL *url = [[NSBundle mainBundle] URLForResource:@"Dealing" withExtension:@"caf"];
	_dealingCardsSound = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
	_dealingCardsSound.numberOfLoops = -1;
	[_dealingCardsSound prepareToPlay];
}

This first sets up the audio session, just to play nice with any other sounds that may be playing on your device (such as iPod audio), and then loads the Dealing.caf sound.

Next add a line to call this new method from viewDidLoad:

- (void)viewDidLoad
{
	. . .
	[self loadSounds];
}

Then add a few lines to play this sound in gameShouldDealCards:startingWithPlayer:

- (void)gameShouldDealCards:(Game *)game startingWithPlayer:(Player *)startingPlayer
{
	. . .

	NSTimeInterval delay = 1.0f;

	_dealingCardsSound.currentTime = 0.0f;
	[_dealingCardsSound prepareToPlay];
	[_dealingCardsSound performSelector:@selector(play) withObject:nil afterDelay:delay];

	for (int t = 0; t < 26; ++t)
	{
		. . .
	}	

	[self performSelector:@selector(afterDealing) withObject:nil afterDelay:delay];	
}

This plays the dealing sound after 1 second. Because you set the AVAudioPlayer's numberOfLoops property to -1, the sound keeps looping indefinitely, so you have to stop it after the dealing animation completes. That's what the call to performSelector:withObject:afterDelay: is for.

Add this new method to the class:

- (void)afterDealing
{
	[_dealingCardsSound stop];
	self.snapButton.hidden = NO;
}

Just to make sure you clean up nicely, add the following lines to the dealloc method:

- (void)dealloc
{
	. . .

	[_dealingCardsSound stop];
	[[AVAudioSession sharedInstance] setActive:NO error:NULL];
}

And in the UIAlertViewDelegate method:

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
	if (buttonIndex != alertView.cancelButtonIndex)
	{
		[NSObject cancelPreviousPerformRequestsWithTarget:self];

		[self.game quitGameWithReason:QuitReasonUserQuit];
	}
}

Because you've used performSelector:withObject:afterDelay:, it is conceivable that the user taps the exit button while the cards are still being dealt. You don't want the afterDealing method to be called anymore in that case, so you use cancelPreviousPerformRequestsWithTarget: to stop any pending messages.

Try it out. The card dealing animation should now be accompanied by a cool sound effect!

Note: performSelector:withObject:afterDelay: is not the only way that you can schedule operations to run in the future. You can also use GCD with blocks, for example. Because you're using UIKit our game does not have a "game loop" that runs 30 or 60 times per second, so you have to do our timing using other mechanisms. If you really wanted to you could create your own a NSTimer, but performSelector:withObject:afterDelay: is just as easy, as long as you remember to cancel the requests when you no longer need them to be performed.

Dealing the cards at the clients

So far the dealing animation only happens on the server. The server also needs to tell the clients that the cards have been dealt. Because dealing cards includes a randomness factor, you cannot have each client do its own dealing. Instead, the server needs to send a message to each client that says which player has received which cards.

Add a new Objective-C class to the project, named PacketDealCards, subclass of Packet. Replace PacketDealCards.h with:

#import "Packet.h"

@class Player;

@interface PacketDealCards : Packet

@property (nonatomic, strong) NSDictionary *cards;
@property (nonatomic, copy) NSString *startingPeerID;

+ (id)packetWithCards:(NSDictionary *)cards startingWithPlayerPeerID:(NSString *)startingPeerID;

@end

The "cards" property is a dictionary of Card objects. The dictionary keys are peer IDs. The startingPeerID property contains the peerID of the player who gets the first turn. Replace PacketDealCards.m with:

#import "PacketDealCards.h"
#import "NSData+SnapAdditions.h"

@implementation PacketDealCards

@synthesize cards = _cards;
@synthesize startingPeerID = _startingPeerID;

+ (id)packetWithCards:(NSDictionary *)cards startingWithPlayerPeerID:(NSString *)startingPeerID
{
	return [[[self class] alloc] initWithCards:cards startingWithPlayerPeerID:startingPeerID];
}

- (id)initWithCards:(NSDictionary *)cards startingWithPlayerPeerID:(NSString *)startingPeerID
{
	if ((self = [super initWithType:PacketTypeDealCards]))
	{
		self.cards = cards;
		self.startingPeerID = startingPeerID;
	}
	return self;
}

+ (id)packetWithData:(NSData *)data
{
	size_t offset = PACKET_HEADER_SIZE;
	size_t count;

	NSString *startingPeerID = [data rw_stringAtOffset:offset bytesRead:&count];
	offset += count;

	NSDictionary *cards = [[self class] cardsFromData:data atOffset:offset];

	return [[self class] packetWithCards:cards startingWithPlayerPeerID:startingPeerID];
}

- (void)addPayloadToData:(NSMutableData *)data
{
	[data rw_appendString:self.startingPeerID];
	[self addCards:self.cards toPayload:data];
}

@end

This uses two methods you haven't seen before, addCards:toPayload: to write a dictionary of Card objects into an NSMutableData object, and cardsFromData:atOffset: to do the reverse, read a dictionary of Card objects from NSData.

Because you're going to be using these methods in a few other Packets as well, I decided to move them into the base class. Add their method signatures to Packet.h:

+ (NSDictionary *)cardsFromData:(NSData *)data atOffset:(size_t) offset;
- (void)addCards:(NSDictionary *)cards toPayload:(NSMutableData *)data;

And the implementations into Packet.m:

- (void)addCards:(NSMutableDictionary *)cards toPayload:(NSMutableData *)data
{
	[cards enumerateKeysAndObjectsUsingBlock:^(id key, NSArray *array, BOOL *stop)
	{
		[data rw_appendString:key];
		[data rw_appendInt8:[array count]];

		for (int t = 0; t < [array count]; ++t)
		{
			Card *card = [array objectAtIndex:t];
			[data rw_appendInt8:card.suit];
			[data rw_appendInt8:card.value];
		}
	}];
}

Writing the card data is pretty straightforward, you first write the peer ID of the player (which is in "key"), then the number of cards for this player. For each card, you write two bytes: the suit and the value. This does require an import for Card.h at the top of the file:

#import "Card.h"

Reading the Card objects back in is pretty simple too:

+ (NSMutableDictionary *)cardsFromData:(NSData *)data atOffset:(size_t) offset
{
	size_t count;

	NSMutableDictionary *cards = [NSMutableDictionary dictionaryWithCapacity:4];

	while (offset < [data length])
	{
		NSString *peerID = [data rw_stringAtOffset:offset bytesRead:&count];
		offset += count;

		int numberOfCards = [data rw_int8AtOffset:offset];
		offset += 1;

		NSMutableArray *array = [NSMutableArray arrayWithCapacity:numberOfCards];

		for (int t = 0; t < numberOfCards; ++t)
		{
			int suit = [data rw_int8AtOffset:offset];
			offset += 1;

			int value = [data rw_int8AtOffset:offset];
			offset += 1;
			
			Card *card = [[Card alloc] initWithSuit:suit value:value];
			[array addObject:card];
		}

		[cards setObject:array forKey:peerID];
	}

	return cards;
}

While you're editing Packet.m, you should add an import for the new PacketDealCards class at the top of the file:

#import "PacketDealCards.h"

And add a case-statement to packetWithData:

		case PacketTypeDealCards:
			packet = [PacketDealCards packetWithData:data];
			break;

Now let's do something useful with this new packet. Switch to Game.m and import the new file:

#import "PacketDealCards.h"

Then change dealCards to the following:

- (void)dealCards
{
	. . .

	Player *startingPlayer = [self activePlayer];

	NSMutableDictionary *playerCards = [NSMutableDictionary dictionaryWithCapacity:4];
	[_players enumerateKeysAndObjectsUsingBlock:^(id key, Player *obj, BOOL *stop)
	{
		NSArray *array = [obj.closedCards array];
		[playerCards setObject:array forKey:obj.peerID];
	}];

	PacketDealCards *packet = [PacketDealCards packetWithCards:playerCards startingWithPlayerPeerID:startingPlayer.peerID];
	[self sendPacketToAllClients:packet];

	[self.delegate gameShouldDealCards:self startingWithPlayer:startingPlayer];
}

Here you make a new NSMutableDictionary named playerCards that puts the Card objects into the dictionary under the player's peerID as the key. Then you create a PacketDealCards packet and send it to all the clients.

That's the sending part. For the receiving part add the following case-statement in clientReceivedPacket:

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

In the interest of keeping the code readable, I've moved the logic into a new method, handleDealCardsPacket, so add that next:

- (void)handleDealCardsPacket:(PacketDealCards *)packet
{
	[packet.cards enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
	{
		Player *player = [self playerWithPeerID:key];
		[player.closedCards addCardsFromArray:obj];
	}];

	Player *startingPlayer = [self playerWithPeerID:packet.startingPeerID];
	_activePlayerPosition = startingPlayer.position;

	Packet *responsePacket = [Packet packetWithType:PacketTypeClientDealtCards];
	[self sendPacketToServer:responsePacket];

	_state = GameStatePlaying;

	[self.delegate gameShouldDealCards:self startingWithPlayer:startingPlayer];
}

This takes the Card objects for each Player, and places them on its closedCards stack. It also figures out what the value for _activePlayerPosition needs to be.

Note: Recall that player positions are relative for each client, so the server couldn't simply send his position values. Instead, you always send peerIDs and then let the client figure out which position corresponds to that peer ID.

The client then sends a PacketTypeClientDealtCards message back to the server to let it know the client received the cards. Finally, it starts the exact same dealing animation that you saw on the server by calling the gameShouldDealCards:startingWithPlayer: delegate method.

This requires a few additional steps to make everything work. First of all, the Stack class needs a new method, addCardsFromArray:. Add the method to Stack.h and Stack.m:

- (void)addCardsFromArray:(NSArray *)array
{
	_cards = [array mutableCopy];
}

You could run the app at this point and see the dealing animation on both the server and the clients, but let's finish up first. The client sends the PacketTypeClientDealtCards packet back to the server. When the server has received this message from all the clients, it can change its state to "playing". Add the following case-statement to serverReceivedPacket:fromPlayer:

		case PacketTypeClientDealtCards:
			if (_state == GameStateDealing && [self receivedResponsesFromAllPlayers])
			{
				_state = GameStatePlaying;
			}
			break;

Of course, you should add a case-statement to Packet.m's packetWithData: as well, or this new packet type won't be recognized:

	switch (packetType)
	{
		case PacketTypeSignInRequest:
		case PacketTypeClientReady:
		case PacketTypeClientDealtCards:
		case PacketTypeServerQuit:
		case PacketTypeClientQuit:
			packet = [Packet packetWithType:packetType];
			break;

	. . .

And that's it as far as the dealing is concerned. Compile and run, and the game should deal cards on both devices, and you're ready to play!

Activating the players

The basic flow of the game is like this: the active player taps his stack to turn over the top-most card. Then the player to his left (you're always going clockwise) becomes active and everyone waits until that player turns over his top-most card. Activate the next player, turn over card, activate player, and so on.

This repeats until there's a matching pair on the table and someone yells "Snap!". In this section you'll focus on activating the players and having them turn over their cards. There's more to this than you may think!

Start by adding a new method signature to Game.h:

- (void)beginRound;

This method will initialize the new round and activate the first player (the one from _activePlayerPosition). Modify the afterDealing method in GameViewController.m to call this:

- (void)afterDealing
{
	[_dealingCardsSound stop];
	self.snapButton.hidden = NO;
	[self.game beginRound];
}

The dealing animation has finished, so now you can start the round and give control to the first player. Add the implementation of beginRound to Game.m:

- (void)beginRound
{
	[self activatePlayerAtPosition:_activePlayerPosition];
}

There will be more here soon, but for now it just calls activatePlayerAtPosition:. Add this new method as well:

- (void)activatePlayerAtPosition:(PlayerPosition)playerPosition
{
	if (self.isServer)
	{
		NSString *peerID = [self activePlayer].peerID;
		Packet* packet = [PacketActivatePlayer packetWithPeerID:peerID];
		[self sendPacketToAllClients:packet];
	}

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

If you're the server then it sends a PacketActivatePlayer packet to all the clients to let them know a new player is active. On both server and client, the GameDelegate is notified so that it can place a graphic next to the name of the newly activated player.

Let's do the delegate method first. Add its signature to the protocol in Game.h:

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

And implement it in GameViewController.m:

- (void)game:(Game *)game didActivatePlayer:(Player *)player
{
	[self showIndicatorForActivePlayer];
	self.snapButton.enabled = YES;
}

- (void)showIndicatorForActivePlayer
{
	[self hideActivePlayerIndicator];

	PlayerPosition position = [self.game activePlayer].position;

	switch (position)
	{
		case PlayerPositionBottom: self.playerActiveBottomImageView.hidden = NO; break;
		case PlayerPositionLeft:   self.playerActiveLeftImageView.hidden   = NO; break;
		case PlayerPositionTop:    self.playerActiveTopImageView.hidden    = NO; break;
		case PlayerPositionRight:  self.playerActiveRightImageView.hidden  = NO; break;
	}

	if (position == PlayerPositionBottom)
		self.centerLabel.text = NSLocalizedString(@"Your turn. Tap the stack.", @"Status text: your turn");
	else
		self.centerLabel.text = [NSString stringWithFormat:NSLocalizedString(@"%@'s turn", @"Status text: other player's turn"), [self.game activePlayer].name];
}

This requires that Game exposes the activePlayer method, though, so add that to Game.h as well:

- (Player *)activePlayer;

These changes take care of showing the active player on the server, but you still need to send out the PacketActivatePlayer packets.

Add a new Objective-C class to the project, named PacketActivatePlayer, subclass of Packet. Then replace PacketActivatePlayer.h with the following:

#import "Packet.h"

@interface PacketActivatePlayer : Packet

@property (nonatomic, copy) NSString *peerID;

+ (id)packetWithPeerID:(NSString *)peerID;

@end

Note: The new packet just has a peer ID. It's almost identical to the PacketOtherClientQuit message, which also just has a peer ID. You could have re-used that same packet class -- but with a different "packetType" value -- but I find that having a class name that describes what the packet is for helps to make the code easier to read.

Also, you never know if you need to change one of these packet classes later, making your code reuse actually work against you. In fact, that's what you'll do later on so that PacketOtherClientQuit and PacketActivatePlayer will actually work differently even though they carry the same data.

Next replace PacketActivatePlayer.m with the following:

#import "PacketActivatePlayer.h"
#import "NSData+SnapAdditions.h"

@implementation PacketActivatePlayer

@synthesize peerID = _peerID;

+ (id)packetWithPeerID:(NSString *)peerID
{
	return [[[self class] alloc] initWithPeerID:peerID];
}

- (id)initWithPeerID:(NSString *)peerID
{
	if ((self = [super initWithType:PacketTypeActivatePlayer]))
	{
		self.peerID = peerID;
	}
	return self;
}

+ (id)packetWithData:(NSData *)data
{
	size_t count;
	NSString *peerID = [data rw_stringAtOffset:PACKET_HEADER_SIZE bytesRead:&count];
	return [[self class] packetWithPeerID:peerID];
}

- (void)addPayloadToData:(NSMutableData *)data
{
	[data rw_appendString:self.peerID];
}

@end

In Packet.m add an #import for this subclass:

#import "PacketActivatePlayer.h"

And add a case-statement to packetWithData:

		case PacketTypeActivatePlayer:
			packet = [PacketActivatePlayer packetWithData:data];
			break;

Also import the packet in Game.m:

#import "PacketActivatePlayer.h"

Now the code compiles, but you don't do anything with these packets on the receiving end yet. In clientReceivedPacket:, add the following case-statement:

		case PacketTypeActivatePlayer:
			if (_state == GameStatePlaying)
			{
				[self handleActivatePlayerPacket:(PacketActivatePlayer *)packet];
			}
			break;

Add the handleActivatePlayerPacket: method as well:

- (void)handleActivatePlayerPacket:(PacketActivatePlayer *)packet
{
	NSString *peerID = packet.peerID;

	Player* newPlayer = [self playerWithPeerID:peerID];
	if (newPlayer == nil)
		return;

	_activePlayerPosition = newPlayer.position;
	[self activatePlayerAtPosition:_activePlayerPosition];
}

Now build and run the app again for the clients and the server and you should see something like this:

The game now highlights the active player

The first player, which was randomly chosen in beginGame, is now highlighted. The screenshot was taken from the server, but on the clients it should also highlight the same player, even though it will be in a different position on the screen.

Turning over the cards

The GameViewController nib has an invisible UIButton that roughly covers the bottom area of the screen where the user's closed cards stack is:

When the player taps this button, you will turn over his top-most card and then activate the next player.

The button is hooked up to several IBAction methods in GameViewController.m, so implement these now to respond to taps on the button:

- (IBAction)turnOverPressed:(id)sender
{
	[self showTappedView];
}

- (IBAction)turnOverEnter:(id)sender
{
	[self showTappedView];
}

- (IBAction)turnOverExit:(id)sender
{
	[self hideTappedView];
}

- (IBAction)turnOverAction:(id)sender
{
	[self hideTappedView];
}

The showTappedView and hideTappedView methods will place a special image on top of the top-most card in other to highlight it. It's always a good idea to show to the user whether or not their taps have an effect on on-screen elements.

First add a new instance variable:

@implementation GameViewController
{
	. . .
	UIImageView *_tappedView;
}

And then these two new methods:

- (void)showTappedView
{
	Player *player = [self.game playerAtPosition:PlayerPositionBottom];
	Card *card = [player.closedCards topmostCard];
	if (card != nil)
	{
		CardView *cardView = [self cardViewForCard:card];

		if (_tappedView == nil)
		{
			_tappedView = [[UIImageView alloc] initWithFrame:cardView.bounds];
			_tappedView.backgroundColor = [UIColor clearColor];
			_tappedView.image = [UIImage imageNamed:@"Darken"];
			_tappedView.alpha = 0.6f;
			[self.view addSubview:_tappedView];
		}
		else
		{
			_tappedView.hidden = NO;
		}

		_tappedView.center = cardView.center;
		_tappedView.transform = cardView.transform;
	}
}

- (void)hideTappedView
{
	_tappedView.hidden = YES;
}

The _tappedView itself is a UIImageView that is lazily loaded. The Darken.png image is a rounded rectangle, basically the shape of a playing card, that is completely black. You set the opacity of the image view to 0.6, so that it darkens whatever is below it. That happens to be the CardView for the top-most card on the Player's closedCards stack. You set the position and transform of the _tappedView to those of the CardView so that it is placed and rotated in exactly the same way.

This needs two new methods, first cardViewForCard:. Add this to the view controller:

- (CardView *)cardViewForCard:(Card *)card
{
	for (CardView *cardView in self.cardContainerView.subviews)
	{
		if (cardView.card == card)
			return cardView;
	}
	return nil;
}

This method simply loops through all the subviews of the "card container" UIView to find the CardView that represents the specified Card object. This is why you added the CardViews to their own container, so that you don't have to look at all the UIViews in our screen, only those that are guaranteed to be CardViews.

The other method is topmostCard, which belongs in Stack. Add it to Stack.h and Stack.m:

- (Card *)topmostCard
{
	return [_cards lastObject];
}

This just lets you peek at the top-most Card, but doesn't actually remove it from the Stack. Run the app and tap on your player's closed stack. It should look something like this:

Example of a highlighted card

Beyond highlighting the card, tapping doesn't do anything yet, so let's make that happen now. Change GameViewController's turnOverAction: to:

- (IBAction)turnOverAction:(id)sender
{
	[self hideTappedView];
	[self.game turnCardForPlayerAtBottom];
}

This calls a new method on Game, so add the signature to Game.h:

- (void)turnCardForPlayerAtBottom;

And the method itself in Game.m:

- (void)turnCardForPlayerAtBottom
{
	if (_state == GameStatePlaying 
		&& _activePlayerPosition == PlayerPositionBottom
		&& [[self activePlayer].closedCards cardCount] > 0)
	{
		[self turnCardForPlayer:[self activePlayer]];
	}
}

- (void)turnCardForPlayer:(Player *)player
{
	NSAssert([player.closedCards cardCount] > 0, @"Player has no more cards");

	Card *card = [player turnOverTopCard];
	[self.delegate game:self player:player turnedOverCard:card];
}

The reason turnCardForPlayerAtBottom calls turnCardForPlayer:, is that you will also want to turn cards from elsewhere in the code later on. In turnCardForPlayerAtBottom you make sure that only the active player can turn a card, and that this can only happen while the game is in the GameStatePlaying state. And of course, the active player must actually have any cards to turn over.

The Player class gets a new method, turnOverTopCard, that transfers the Card from the Stack of closed cards to the open cards. Add the signature to Player.h and the method itself to Player.m:

- (Card *)turnOverTopCard
{
	NSAssert([self.closedCards cardCount] > 0, @"No more cards");

	Card *card = [self.closedCards topmostCard];
	card.isTurnedOver = YES;
	[self.openCards addCardToTop:card];
	[self.closedCards removeTopmostCard];

	return card;
}

This assumes Card has an isTurnedOver property, which it doesn't yet have. In Card.h add:

@property (nonatomic, assign) BOOL isTurnedOver;

And synthesize this new property in Card.m:

@synthesize isTurnedOver = _isTurnedOver;

You also need to add a removeTopmostCard method to Stack.m, and its declaration to Stack.h:

- (void)removeTopmostCard
{
	[_cards removeLastObject];
}

As you can see, Stack is a pretty basic class that just has wrappers around the methods from NSMutableArray. But it does read a lot more naturally for our problem domain: addCardToTop and removeTopmostCard actually sound like operations you would do on a pile of cards.

One more thing to do. The turnCardForPlayer: method in Game.m calls a new delegate method, game:player:turnedOverCard:. Add the signature for this method to the delegate protocol in Game.h:

- (void)game:(Game *)game player:(Player *)player turnedOverCard:(Card *)card;

And implement this method in GameViewController.m:

- (void)game:(Game *)game player:(Player *)player turnedOverCard:(Card *)card
{
	[_turnCardSound play];

	CardView *cardView = [self cardViewForCard:card];
	[cardView animateTurningOverForPlayer:player];
}

This plays a new sound and performs a new animation. Let's first add the "turning card sound". Add a new instance variable:

@implementation GameViewController
{
	. . .
	AVAudioPlayer *_turnCardSound;
}

The code to load this sound effect goes into loadSounds:

- (void)loadSounds
{
	. . .

	url = [[NSBundle mainBundle] URLForResource:@"TurnCard" withExtension:@"caf"];
	_turnCardSound = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
	[_turnCardSound prepareToPlay];
}

Now the animation. Add the method signature to CardView.h:

- (void)animateTurningOverForPlayer:(Player *)player;

And the method itself to CardView.m:

- (void)animateTurningOverForPlayer:(Player *)player
{
	[self loadFront];
	[self.superview bringSubviewToFront:self];

	UIImageView *darkenView = [[UIImageView alloc] initWithFrame:self.bounds];
	darkenView.backgroundColor = [UIColor clearColor];
	darkenView.image = [UIImage imageNamed:@"Darken"];
	darkenView.alpha = 0.0f;
	[self addSubview:darkenView];

	CGPoint startPoint = self.center;
	CGPoint endPoint = [self centerForPlayer:player];
	CGFloat afterAngle = [self angleForPlayer:player];

	CGPoint halfwayPoint = CGPointMake((startPoint.x + endPoint.x)/2.0f, (startPoint.y + endPoint.y)/2.0f);
	CGFloat halfwayAngle = (_angle + afterAngle)/2.0f;

	[UIView animateWithDuration:0.15f
		delay:0.0f
		options:UIViewAnimationOptionCurveEaseIn
		animations:^
		{
			CGRect rect = _backImageView.bounds;
			rect.size.width = 1.0f;
			_backImageView.bounds = rect;

			darkenView.bounds = rect;
			darkenView.alpha = 0.5f;

			self.center = halfwayPoint;
			self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(halfwayAngle), 1.2f, 1.2f);
		}
		completion:^(BOOL finished)
		{
			_frontImageView.bounds = _backImageView.bounds;
			_frontImageView.hidden = NO;

			[UIView animateWithDuration:0.15f
				delay:0
				options:UIViewAnimationOptionCurveEaseOut
				animations:^
				{
					CGRect rect = _frontImageView.bounds;
					rect.size.width = CardWidth;
					_frontImageView.bounds = rect;

					darkenView.bounds = rect;
					darkenView.alpha = 0.0f;

					self.center = endPoint;
					self.transform = CGAffineTransformMakeRotation(afterAngle);
				}
				completion:^(BOOL finished)
				{
					[darkenView removeFromSuperview];
					[self unloadBack];
				}];
		}];
}

That's a big one! Several things are going on here. The new loadFront method will load the UIImageView with the card's front-facing picture. You also load Darken.png (the same one you used earlier in GameViewController to highlight the card when it was tapped) into a new UIImageView and add it as a subview, but its alpha is initially 0.0, so it is fully transparent.

Then you calculate the end position and angle for the card. To make this work you will have to change centerForPlayer: to recognize that the Card is now turned over, which you'll do in a sec. For a turned-over card, centerForPlayer: returns a slightly different position, so that it moves over to the open pile.

The animation itself happens in two steps, which is why you calculate the halfway point and the halfway angle. The first step of the animation reduces the width of the card to 1 point, while at the same time making the darkenView more opaque. Because the darkenView covers the entire surface of the card, the CardView now appears darker, which simulates the shadow that the light casts on the card. It's only a subtle effect, but subtle is your new middle name.

Step 1 of the turn-over animation

At this point the card is half flipped over. Now you swap the back image with the front image and resize the card view back to its full width, while simultaneously making the darken view fully transparent again. Once the animation is complete, you remove the darken view and the UIImageView for the back because you no longer need them, and the card is turned over:

Step 2 of the turn-over animation

Notice also that you slightly scale up the card view (by 120%) as it approaches the halfway point. That makes it seem like the card is actually lifted up by the player. Another subtle tweak that makes our animation more lifelike.

Here's the promised change to the centerForPlayer: method:

- (CGPoint)centerForPlayer:(Player *)player
{
	CGRect rect = self.superview.bounds;
	CGFloat midX = CGRectGetMidX(rect);
	CGFloat midY = CGRectGetMidY(rect);
	CGFloat maxX = CGRectGetMaxX(rect);
	CGFloat maxY = CGRectGetMaxY(rect);

	CGFloat x = -3.0f + RANDOM_INT(6) + CardWidth/2.0f;
	CGFloat y = -3.0f + RANDOM_INT(6) + CardHeight/2.0f;

	if (self.card.isTurnedOver)
	{
		if (player.position == PlayerPositionBottom)
		{
			x += midX + 7.0f;
			y += maxY - CardHeight - 30.0f;
		}
		else if (player.position == PlayerPositionLeft)
		{
			x += 31.0f;
			y += midY - 30.0f;
		}
		else if (player.position == PlayerPositionTop)
		{
			x += midX - CardWidth - 7.0f;
			y += 29.0f;
		}
		else
		{
			x += maxX - CardHeight + 1.0f;
			y += midY - CardWidth - 45.0f;
		}	
	}
	else
	{
		if (player.position == PlayerPositionBottom)
		{
			x += midX - CardWidth - 7.0f;
			y += maxY - CardHeight - 30.0f;
		}
		else if (player.position == PlayerPositionLeft)
		{
			x += 31.0f;
			y += midY - CardWidth - 45.0f;
		}
		else if (player.position == PlayerPositionTop)
		{
			x += midX + 7.0f;
			y += 29.0f;
		}
		else
		{
			x += maxX - CardHeight + 1.0f;
			y += midY - 30.0f;
		}
	}

	return CGPointMake(x, y);
}

And of course you have to supply the missing loadFont and unloadBack methods:

- (void)unloadBack
{
	[_backImageView removeFromSuperview];
	_backImageView = nil;
}

- (void)loadFront
{
	if (_frontImageView == nil)
	{
		_frontImageView = [[UIImageView alloc] initWithFrame:self.bounds];
		_frontImageView.contentMode = UIViewContentModeScaleToFill;
		_frontImageView.hidden = YES;
		[self addSubview:_frontImageView];

		NSString *suitString;
		switch (self.card.suit)
		{
			case SuitClubs:    suitString = @"Clubs"; break;
			case SuitDiamonds: suitString = @"Diamonds"; break;
			case SuitHearts:   suitString = @"Hearts"; break;
			case SuitSpades:   suitString = @"Spades"; break;
		}

		NSString *valueString;
		switch (self.card.value)
		{
			case CardAce:   valueString = @"Ace"; break;
			case CardJack:  valueString = @"Jack"; break;
			case CardQueen: valueString = @"Queen"; break;
			case CardKing:  valueString = @"King"; break;
			default:        valueString = [NSString stringWithFormat:@"%d", self.card.value];
		}

		NSString *filename = [NSString stringWithFormat:@"%@ %@", suitString, valueString];
		_frontImageView.image = [UIImage imageNamed:filename];
	}
}

A keen eye will have noticed that you set the scale mode to UIViewContentModeScaleToFill. That's necessary to make the card-flip illusion work, because you are resizing the image view (by reducing its width) and you do want the image itself to resize accordingly.

And that's it for the animation. The player who is now first active can keep flipping over cards until he runs out.

Tip: To debug animations you can choose the Toggle Slow Animations option from the Simulator's Debug menu, or tap the Shift key three times in succession while the Simulator window is active. That will show you the animations in slow motion, which is a great help.

If you're a fan of Core Animation, then you may find it easier to flip the card with a 3D animation around the Y axis. For Snap!, reducing the width looks good enough, even though it is not 100% perspective-correct. If you were thinking about using the built-in "TransitionFlipFromLeft" UIView-transition, then know that won't work very well because our card views are rotated already around the center of the table and then an additional rotation makes it look weird.

Where To Go From Here?

Here is a sample project with all of the code from the tutorial series so far.

You've made great progress on the game so far - you can connect multiple devices together, deal cards, and flip cards - complete with animations and sound effects.

What's next? Right now only the starting player can turn over his card. In Part 6, you will make the going-round-the-table logic work, so that each player gets a turn!

In the meantime, if you have any questions or comments about this part of the series, please join the forum discussion below!


This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on and Twitter.

Contributors

Over 300 content creators. Join our team.