基于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.
//
@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
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.
//
@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.
//
@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.
//
@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.
//
@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.
//
@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.
//
@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.
//
@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.
//
@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.
//
@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.
//
@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.
//
@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