Xungerrrr's Blog

Card Game

Word count: 1.7kReading time: 8 min
2017/05/16 Share

基于Objective-C的iOS编程报告 - Card Game范例的实现

基于Stanford CS193P

游戏介绍

Card Game是一款纸牌匹配游戏。游戏会在一副扑克牌中随机抽取12 张牌,反扣在屏幕上。玩家可以点击扑克牌进行翻转,每次翻转减1分,最多能同时翻开两张牌。若花色匹配,则加4分,若数字匹配,则加16分(这是因为,数字匹配的概率更低)。若两张牌不匹配,则减2分。以此循环,直至没有牌能够匹配为止,最后得出总分。

设计思路

iOS下的编程一般采取MVC的思路,即Model-View-Controller。Model包含整个程序的核心算法和功能,View是程序的UI部分,而Controller则负责控制UI和代码的交互。在这个游戏采取面向对象的编程方法,将游戏的核心都放到Model里,其中包括了Card、Deck、PlayingCard、PlayingCardDeck这些基础的类,以及控制游戏逻辑的CardMatchingGame类的声明和实现,并利用Controller实现UI和代码的交互,从而完成整个游戏的制作。游戏制作范例来源于2013年Stanford University的CS193P课程。

游戏实现

UI部分

十二张扑克牌、一个分数显示栏和绿色的背景构成了整个游戏的界面。这个部分的实现完全不需要输入代码,因此,没有编程基础的人也能轻松完成界面的设计。

为了游戏的美观,相对于范例,我做了三点改进:

  • 增加了游戏对横屏的支持。范例中,游戏只考虑了竖屏下的显示效果。如果旋转设备,扑克牌将会显示不全,分数栏也会消失不见。我利用Xcode的stack view功能,将扑克牌和分数栏固定,从而实现横屏游戏。

  • 增加游戏图标。范例中,游戏缺少图标,在实际设备上安装时,只有一个默认图标。我利用photoshop,制作了中山大学的图标,附加到了游戏上。

  • 将扑克牌背面的斯坦福大学图标改为中山大学图标

Model部分

Card类

Card.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//
// Card.h
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#ifndef Card_h
#define Card_h

#import <Foundation/Foundation.h>

@interface Card : NSObject

@property (strong, nonatomic) NSString *contents;

@property (nonatomic, getter=isChosen) BOOL chosen;

@property (nonatomic, getter=isMatched) BOOL matched;

- (int)match:(NSArray *)otherCards;

@end

#endif /* Card_h */

Card.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//
// Card.m
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import "Card.h"

@interface Card()

@end

@implementation Card

- (int)match:(NSArray *)otherCards
{
int score = 0;
for (Card *card in otherCards) {
if ([card.contents isEqualToString:self.contents]) {
score = 1; }
}
return score;
}

@end

Deck类

Deck.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//
// Card+Deck.h
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "Card.h"

@interface Deck : NSObject

- (void)addCard:(Card *)card atTop:(BOOL)atTop;
- (void)addCard:(Card *)card;

- (Card *)drawRandomCard;

@end

Deck.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//
// Card+Deck.m
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import "Deck.h"

@interface Deck()
@property (strong, nonatomic) NSMutableArray *cards;
@end

@implementation Deck

- (NSMutableArray *)cards
{
if (!_cards)
_cards = [[NSMutableArray alloc] init];
return _cards;
}

- (void)addCard:(Card *)card atTop:(BOOL)atTop
{
if (atTop) {
[self.cards insertObject:card atIndex:0];
} else {
[self.cards addObject:card];
}
}

- (void)addCard:(Card *)card
{
[self addCard:card atTop:NO];
}

- (Card *)drawRandomCard
{
Card *randomCard = nil;
if ([self.cards count]) {
unsigned index = arc4random() % [self.cards count];
randomCard = self.cards[index];
[self.cards removeObjectAtIndex:index];
}
return randomCard;

}

@end

PlayingCard类

PlayingCard.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//
// PlayingCard.h
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import "Card.h"

@interface PlayingCard : Card

@property (strong, nonatomic) NSString *suit;
@property (nonatomic) NSUInteger rank;

+ (NSArray *)validSuits;
+ (NSUInteger)maxRank;

@end

PlayingCard.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//
// PlayingCard.m
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import "PlayingCard.h"

@implementation PlayingCard

- (int)match:(NSArray *)otherCards
{
int score = 0;

if ([otherCards count] == 1) {
PlayingCard *otherCard = [otherCards firstObject];
if (otherCard.rank == self.rank) {
score = 4;

} else if ([otherCard.suit isEqualToString:self.suit]) {
score = 1;
}
}
return score;
}

- (NSString *)contents
{
NSArray *rankStrings = [PlayingCard rankStrings];
return [rankStrings[self.rank] stringByAppendingString:self.suit];
}

@synthesize suit = _suit;

+ (NSArray *)validSuits
{
return @[@"♥︎",@"♦︎",@"♠︎",@"♣︎"];
}

- (void)setSuit:(NSString *)suit
{
if ([[PlayingCard validSuits] containsObject:suit]) {
_suit = suit;
}
}

- (NSString *)suit
{
return _suit ? _suit : @"?";
}

+ (NSArray *)rankStrings
{
return @[@"?",@"A",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10",@"J",@"Q",@"K"];
}

+ (NSUInteger)maxRank
{
return [[self rankStrings] count]-1;
}

- (void)setRank:(NSUInteger)rank
{
if (rank <= [PlayingCard maxRank]) {
_rank = rank;
}
}

@end

PlayingCardDeck类

PlayingCardDeck.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
//
// Deck+PlayingCardDeck.h
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import "Deck.h"

@interface PlayingCardDeck : Deck

@end

PlayingCardDeck.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//
// Deck+PlayingCardDeck.m
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import "PlayingCardDeck.h"
#import "PlayingCard.h"
@implementation PlayingCardDeck

- (instancetype)init
{
self = [super init];

if (self) {
for (NSString *suit in [PlayingCard validSuits]) {
for (NSUInteger rank = 1; rank <= [PlayingCard maxRank]; rank++) {
PlayingCard *card = [[PlayingCard alloc] init];
card.rank = rank;
card.suit = suit;
[self addCard:card];
}
}
}
return self;
}

@end

CardMatchingGame类

CardMatchingGame.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
// NSObject+CardMatchingGame.h
// Card Game
//
// Created by Xungerrrr on 2017/5/9.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "Deck.h"
#import "Card.h"

@interface CardMatchingGame : NSObject

// designated initializer
- (instancetype)initWithCardCount:(NSUInteger)count
usingDeck:(Deck *)deck;

- (void)chooseCardAtIndex:(NSUInteger)index;
- (Card *)cardAtIndex:(NSUInteger)index;

@property (nonatomic, readonly) NSInteger score;

@end

CardMatchingGame.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//
// NSObject+CardMatchingGame.m
// Card Game
//
// Created by Xungerrrr on 2017/5/9.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import "CardMatchingGame.h"

@interface CardMatchingGame()
@property (nonatomic, readwrite) NSInteger score;
@property (nonatomic, strong) NSMutableArray *cards; // of Card
@end

@implementation CardMatchingGame

- (NSMutableArray *)cards
{
if (!_cards)
_cards = [[NSMutableArray alloc] init];
return _cards;
}

- (instancetype)initWithCardCount:(NSUInteger)count
usingDeck:(Deck *)deck
{
self = [super init]; // super's designated initializer
if (self) {
for (int i = 0; i < count; i++) {
Card *card = [deck drawRandomCard];
if (card) {
[self.cards addObject:card];
} else {
self = nil;
break;
}
}
}
return self;
}

- (Card *)cardAtIndex:(NSUInteger)index
{
return (index < [self.cards count]) ? self.cards[index] : nil;
}

static const int MISMATCH_PENALTY = 2;
static const int MATCH_BONUS = 4;
static const int COST_TO_CHOOSE = 1;

- (void)chooseCardAtIndex:(NSUInteger)index
{
Card *card = [self cardAtIndex:index];

if (!card.isMatched) {
if (card.isChosen) {
card.chosen = NO;
} else {
// match against other chosen cards
for (Card *otherCard in self.cards) {
if (otherCard.isChosen && !otherCard.isMatched) {
int matchScore = [card match:@[otherCard]];
if (matchScore) {
self.score += matchScore * MATCH_BONUS;
otherCard.matched = YES;
card.matched = YES;
} else {
self.score -= MISMATCH_PENALTY;
otherCard.chosen = NO;
}
break; // can only choose 2 cards for now
}
}
self.score -= COST_TO_CHOOSE;
card.chosen = YES;
}
}
}
@end

Controller部分

CardGameViewController类

与按钮交互,是通过变量cardButtons和方法touchCardButton来实现的,而画面以及分数栏的实时更新,则是通过变量scoreLabel和方法updateUI来实现的。这部分相对于Model的设计来说,更加简单,但是却十分重要。

CardGameViewController.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//
// ViewController.h
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController


@end

CardGameViewController.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//
// ViewController.m
// Card Game
//
// Created by Xungerrrr on 2017/5/7.
// Copyright © 2017年 Xungerrrr. All rights reserved.
//

#import "CardGameViewController.h"
#import "PlayingCardDeck.h"
#import "CardMatchingGame.h"

@interface ViewController ()
@property (strong, nonatomic) CardMatchingGame *game;
@property (strong, nonatomic) IBOutletCollection(UIButton) NSArray *cardButtons;
@property (weak, nonatomic) IBOutlet UILabel *scoreLabel;
@end

@implementation ViewController

- (CardMatchingGame *)game
{
if (!_game)
_game = [[CardMatchingGame alloc] initWithCardCount:[self.cardButtons count] usingDeck:[self createDeck]];
return _game;
}


- (Deck *)createDeck
{
return [[PlayingCardDeck alloc] init];
}


- (IBAction)touchCardButton:(UIButton *)sender {
NSUInteger chosenButtonIndex = [self.cardButtons indexOfObject:sender];
[self.game chooseCardAtIndex:chosenButtonIndex];
[self updateUI];

}

- (void)updateUI {
for (UIButton *cardButton in self.cardButtons) {
NSUInteger cardButtonIndex = [self.cardButtons indexOfObject:cardButton];
Card *card = [self.game cardAtIndex:cardButtonIndex];
[cardButton setTitle:[self titleForCard:card] forState:UIControlStateNormal];
[cardButton setBackgroundImage:[self backgroungImageForCard:card] forState:UIControlStateNormal];
cardButton.enabled = !card.isMatched;
self.scoreLabel.text = [NSString stringWithFormat:@"Score: %ld", (long)self.game.score];
}
}

- (NSString *)titleForCard:(Card *)card
{
return card.isChosen ? card.contents : @"";
}

- (UIImage *)backgroungImageForCard:(Card *)card
{
return [UIImage imageNamed:card.isChosen ? @"cardfront" : @"cardback"];

}
@end

运行实例(iPad Pro 9.7 inch)

GitHub

https://github.com/Xungerrrr/Card-Game

CATALOG
  1. 1. 基于Objective-C的iOS编程报告 - Card Game范例的实现
    1. 1.0.1. 基于Stanford CS193P
  2. 1.1. 游戏介绍
  3. 1.2. 设计思路
  4. 1.3. 游戏实现
    1. 1.3.1. UI部分
    2. 1.3.2. Model部分
      1. 1.3.2.1. Card类
      2. 1.3.2.2. Deck类
      3. 1.3.2.3. PlayingCard类
      4. 1.3.2.4. PlayingCardDeck类
      5. 1.3.2.5. CardMatchingGame类
    3. 1.3.3. Controller部分
      1. 1.3.3.1. CardGameViewController类
    4. 1.3.4. 运行实例(iPad Pro 9.7 inch)
  5. 1.4. GitHub