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.
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 Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 5
55 mins
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 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.
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.
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.