斯坦福大学iOS公开课学习笔记(3)-完成翻纸牌游戏

第三课没有太多的知识点,主要是完成前两节课中的一个小的纸牌游戏。因为上课的时间有限,所以课上并没有太多复杂的逻辑,主要是通过这样的一个小demo来加深对MVC架构的理解。

设计需求

  • 显示多张卡牌,点击任意一张卡牌可以翻过卡牌
  • 匹配两张卡牌的内容,花色或数字相同即为匹配成功,并且将按钮置于不能点击的状态,而且有不同的对应分值。若匹配不成功则将卡牌扣回
  • 每次点击卡牌都会消耗分值
  • 每次分值改变(翻卡牌)之后都要更新UI
logo

结构设计

Card(卡牌)

包含公共变量三个,内容contents,选中状态chosen,匹配状态matched

1
2
3
@property (nonatomic ,strong) NSString *contents;
@property (nonatomic ,assign) BOOL chosen;
@property (nonatomic ,assign) BOOL matched;

一个公共方法用来判断是否匹配。这个方法比较简单粗暴,直接判断两个字符串是否相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (int)match:(NSArray *)otherCards
{
int score = 0;
for(Card *card in otherCards)
{
if([card.contents isEqualToString:self.contents])
{
score = 1;
}
}
return score;
}

Deck(牌堆)

只有一个私有变量cards用来存放Card。

1
@property (nonatomic ,strong) NSMutableArray *cards;

另外有三个公有方法,其中两个是添加卡牌到牌堆,还有一个是随机抽取一张卡牌。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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;
}

PlayingCard(扑克牌)

继承于Card类,是扑克牌的具体化表现。有花色和大小两个公有变量

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

在这里重写了父类中match:方法,实现了需求中对花色和大小进行判断的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (int)match:(NSArray *)otherCards
{
int score = 0;
if([otherCards count] == 1)
{
PlayingCard *otherCard = [otherCards firstObject];
if([self.suit isEqualToString:otherCard.suit])
{
score = 1;
}
else if(self.rank == otherCard.rank)
{
score = 4;
}
}
return score;
}

PlayingCardDeck(扑克牌堆)

继承与Deck,这里只是在初始化的时候生成全部52张扑克牌。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (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.suit = suit;
card.rank = rank;
[self addCard:card];
}
}
}
return self;
}

CardMatchingGame (卡牌匹配)

这是整个游戏的核心部分,完成卡牌匹配的判断工作。这里重写了初始化函数,因为简单的init方法已经不够实现我们需要的功能,这里在初始化函数中添加了数量和牌堆的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (instancetype)initWithCardCount:(NSUInteger)count usingDeck:(Deck *)deck
{
self = [super init];
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;
}

然后有一个核心匹配方法chooseCardAtIndex:用来实现整个游戏的功能。

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
- (void)chooseCardAtIndex:(NSUInteger)index
{
Card *card = [self cardAtIndex:index];
if(!card.matched)
{
if(card.chosen)
{
card.chosen = NO;
}
else
{
for(Card *otherCard in self.cards)
{
if(otherCard.chosen && !otherCard.matched)
{
int macthScore = [card match:@[otherCard]];
if(macthScore)
{
self.score += macthScore * MACTH_BOUNS;
card.matched = YES;
otherCard.matched = YES;
}
else
{
self.score -= MISMACTH_PENALTY;
otherCard.chosen = NO;
}
break;
}
}
card.chosen = YES;
self.score -= COST_TO_CHOOSE;
}
}
}

在这里使用了常量并且介绍了两种常量的使用方式

1
2
#define MISMACTH_PENALTY 2
static const int MISMACTH_PENALTY = 2;

其中#define 只是简单的使用 2 来替换 MISMACTH_PENALTY关键字,并没有指定的类型,而static const int MISMACTH_PENALTY有指定的类型,并不是简单的替换。

而且对于分数score属性在共有和私有中做了不同的处理。在公有中使用了readonly关键字来修饰,在私有中使用了readwrite关键字来修饰。来防止外部对分数的修改。达到外部只能看到分数而不能插手干预分数的目的。

MatchingGameViewController(控制器)

作为整个应用的控制器,MatchingGameViewController负责接受UI的事件,并告诉Model,Model根据收到的信息对自身数据进行改变后再通知控制器,控制器再根据数据更新UI。

具体实现如下:

  • 控制器接收到按钮的点击事件触发touchCardButton:方法
  • 控制器使用CardMatchingGame类中的chooseCardAtInde:方法告诉Model点击的卡牌,由Model对匹配进行处理
  • 使用updateUI方法对UI进行更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (IBAction)touchCardButton:(UIButton *)sender
{
NSUInteger cardIndex = [self.cardButtons indexOfObject:sender];
[self.game chooseCardAtIndex:cardIndex];
[self updateUI];
}
- (void)updateUI
{
for(UIButton *cardButton in self.cardButtons)
{
NSUInteger cardIndex = [self.cardButtons indexOfObject:cardButton];
Card *card = [self.game cardAtIndex:cardIndex];
[cardButton setTitle:[self titleForCard:card] forState:UIControlStateNormal];
[cardButton setBackgroundImage:[self backgroundForCard:card] forState:UIControlStateNormal];
cardButton.enabled = !card.matched;
}
self.ScoreLabel.text = [NSString stringWithFormat:@"Score: %ld",(long)self.game.score];
}

这里使用了titleForCard:backgroundForCard:提炼了对卡牌的设置

1
2
3
4
5
6
7
8
9
- (NSString *)titleForCard:(Card *)card
{
return card.chosen ? card.contents : @"";
}
- (UIImage *)backgroundForCard:(Card *)card
{
return [UIImage imageNamed:card.chosen ? @"RectanglecardFace":@"cardBcak"];
}

至此这个纸牌游戏的Demo就完成了,具体功能样式如下图:

logo

其他小知识

数组中第一个元素和最后一个元素的选择

一共有三种方法来定位数组中的第一个元素

1
2
3
4
5
PlayingCard *otherCard = [otherCards firstObject];
PlayingCard *otherCard = otherCards[0];
PlayingCard *otherCard = [otherCards objectAtIndex:0];

其中建议使用第一种方法,因为如果当数组为空的时候,使用第一种方法只会得到一个nil的元素,而并不会引起崩溃。但是第二第三种方法则会因为数组下标越界而引起崩溃。

总结

这一课中大的知识点并不多,主要还是强调了MVC的重要性,并且在代码编写的过程中不断强调要优雅的实现功能。而且要对自己的代码进行一些保护,增强代码的健壮性。