今天我们来聊一个面向对象的话题:21点扑克。

 

面向对象的核心就是要还原世界。为了还原世界,我们就先看看现实中大家是怎么玩21点扑克的。如图,是一个21点的游戏界面。这里有哪些核心元素呢?

 

中上方是Dealer,负责发牌,下面的一排则是好几个玩家。Dealer和玩家都有手牌(HandCards),而手牌有的是亮着的,有的是盖着的。右上角是牌盒(CardBox),Dealer从牌盒里拿牌发给大家。牌盒里不只有一副牌,而是多副牌的组合。以上整体组合在一起就是一个游戏大厅(GameRoom)。

 

我们再看一下比较流行的另一种场景

 

场景中间是Dealer,玩家通过视频进行游戏,而手牌则在Dealer前面的桌子上。手牌、牌盒等道具的设置都和上一个游戏场景一样。同理,整体组合在一起就构成了游戏大厅。

 

在场景的下方还有许多辅助功能,包括计时器、筹码、聊天室、操作按钮等。虽然这个游戏世界比上一个更加复杂、真实,但其实内容是不变的。

 

牌的设计

 

了解了这些基本概念之后,我们就可以开始模拟世界。第一步,就是要模拟牌。首先,我们看一看一张牌里有哪些元素呢?

 

Card

int  mType

int  mValue

string  mName

bool  mFace

 

  1. 牌的类型(Type):方块、红桃、黑桃、梅花、大小王
  2. 值(Value):牌代表的数字
  3. 名字(Name):把字符串名字写出来,便于打印(如“红桃3”)
  4. 牌面(Face):表示牌是亮着的还是盖着的

 

把这些牌全都放在一起,装在牌盒里,那么这盒牌就成为了一套牌的list,且带有初始化的功能。一般一套牌有54张,但有时会去掉大小王,就变成了52张。

 

Deck
list <Card> mCardList

Initial()

 

再进一步就是牌盒。实际上真正在玩21点的时候,大家往往会把4到6副牌洗到一起,因为这样可以打破概率。曾经有一群数学家发现,在21点游戏中,玩家从概率上来说是不可能赢过庄家的。但是通过观察已经出现的牌,再推算得到牌盒里剩的牌偏大还是偏小,就可以提高胜率。因此为了防止玩家计算概率,就要把好几副牌混在一起。在这里,初始化、往牌库里添牌、洗牌等操作都可以实现。而最重要的则是最后一个操作:从牌盒里拿出一张牌。

 

Cardbox
list <Card> mCardList

Initial()

AppendDecks()

Shuffle()

PopCard()

 

实际上,即使设计到这个程度也还有很多问题存在,比如洗牌:为什么不在Deck中洗牌,而要在Cardbox中洗呢?在Cardbox中洗牌时,是洗所有的牌,还是只洗刚刚放进去的部分?这里还有许多要考虑的问题,需要不断去优化系统设计。

 

玩家和庄家的设计

 

首先考虑一个问题:玩家和庄家在设计时应该是相同处理还是区别对待?很多人认为应当区别对待,但我认为两者间其实有很多相同之处,所以我就把他们都设计成Gamer。玩家和庄家有什么共同属性呢?

 

Gamer
int mID

string mName

double mMoney

list <Card> mHandCard

Strategy* mStrategy

MakeDecision(Question)

CalculateHandScore()

AddCard(Card)

Showhand()

 

  1. 账号(ID)和名字(Name)
  2. 钱(Money):玩家有钱很重要,但是给庄家设置钱有什么意义呢?这是为了计算庄家赢了多少钱、输了多少钱,便于统计。
  3. 手牌(HandCard):我们也可以选择把Card中的朝向放进手牌,因为牌盒里的牌看不到朝向。也就是说,牌的朝向可以表示在Card中,也可以表示在Gamer中,这是两种设计理念。如果将Face写在Card中会更简单些。
  4. 策略(Strategy)玩牌的时候不同的人有不同的玩法,而庄家和玩家的玩法也不同。算法如果对应的是人,就是人类玩家;对应的是机器策略,就是机器玩家,也即人工智能。
  5. 决策(Decision):做什么Decision是根据Question不同而定的,如是否加码,是否买保险等等。
  6. 计算手牌的得分(CalculateHandScore):因为要比较各自的手牌大小,确定输赢,就需要设定一个算分的方式。
  7. 添加手牌(AddCard):往手里加牌。
  8. 亮牌(Showhand):最后比较牌的大小时,需要把所有的手牌亮出来。

以上就是Gamer的基本操作,无论庄家、玩家都会有。

 

如何设计玩家?

 

玩家需要有基本的策略,这里我们做一个人工智能的策略。这里就出现了策略模式。什么是策略模式呢?我们默认策略是MakeDecision(MakeDecision本质上是Strategy做的),玩家和庄家各自有不同的Strategy,且还可以针对不同玩家设计不同的Strategy,如保守型、激进型、随机型等。

与游戏内容相关的,需要存在Player中的元素:押码、是否加倍、是否买保险等。

设计人工智能:要想设计人工智能,就需要了解历史中发生了什么。我们可以在Player里可以放入一个ActionList,相当于History或者Log。每发生一个事件,就要求大厅或Dealer通知我一下。通知的内容包括事件的序列、事件的类型等。比如有人要一张牌、出一张牌、押钱、买保险、谁赢谁输、发生时间是什么,这些具体信息内容都要通知。有了这些历史信息,存到Player中以后,才能进行各种人工智能的算法。

 

Player
mStrategy = new PlayerStrategy()

double mBet

double mDouble

double minsurance

list <Action> mActionList

Notify(Action)

 

Action
int mID

int mActionType

Time mActionTime

string information

 

如何设计庄家?

 

庄家是整个游戏过程的控制者,有特殊的权限,其设计元素如下:

 

  1. 策略(Strategy):庄家的策略是固定的,如在某一分数以下就一定要多抓一张牌。
  2. 牌盒(Cardbox):庄家掌管着牌盒,因此Cardbox的设计可以写在Dealer中,也可以写在Dealer外。放在Dealer中,就相当于庄家拿着牌盒,负责发牌。
  3. 游戏状态(GameState):庄家还维护着游戏进程,比如确认玩家是否已经爆牌等。
  4. 启动游戏(RunGame):庄家可以开始一场游戏。
  5. 重启游戏(Reset):庄家还可以重启游戏。比如玩到一半,有人员出问题了,就可以Reset。
  6. 决定胜者(DecideWinner):在游戏结束时比较牌的大小,并进行结算。

 

Dealer
mStrategy = new DealerStrategy()

Cardbox = mCardbox

int mGameState

RunGame()

Reset()

DecideWinner()

 

所有这些加在一起,就构成了Gamer的设计。

 

游戏大厅(GameRoom)的设计

 

设计到这里还没有结束,我们现在还缺一个前面反复强调过的游戏大厅。游戏大厅中,可以放进Dealer、Player,也可以进行加入玩家、踢掉玩家、开启游戏世界等操作。有了游戏大厅,就可以管理整个游戏世界了。

 

GameRoom
Dealer* mDealer

list <Player*> mPlayList

JoinPlayer(Player*)

LeavePlayer(Player*)

OpenGameRoom()

 

到这里为止,我们也就完成了基本的21点游戏的设计了。

 

总结

 

  • 还原世界

 

面向对象的核心就是还原世界。对世界观察得越仔细,就能越真实地对其进行还原。

 

  • 探求合理

 

还原世界的同时,也不要执着于世界。世界中不合理的地方,可以在系统设计中对其合理化,即探求合理性。

 

  • 生长迭代

 

不要一口吃个胖子,刚才设计的过程中还有很多问题,需要大家不断优化。