很久没有写过博客了,等过了这两周开始将自己学的东西汇总整理一下。
今天写的是一个扑克牌的游戏的一个片段,它仅仅是一个命令行的工程而已,它还不具有对弈功能。
[游戏背景]
我们的一付牌里,有 52张普通牌和 5张鬼。
52 张普通牌分成 4种花色,从大到小依次为黑桃(Spade)、红桃(Heart)、方块(Diamond)、草花(club),每种花色是13张牌2-10,J,Q,K,A。
5张鬼不分大小,可以当作任意牌来组成牌型。
牌型比较:五鬼>五条>同花顺>四条>葫芦>同花>顺子>三条>二对>单对>散牌。
数字比较:A>K>Q>J>10>9>8>7>6>5>4>3>2
花式比较:黑桃>红桃>方块>草花
五鬼——五张鬼
五条——五张相同数字的牌,其中至少一张为鬼。
同花顺——拥有五张连续性同花色的顺子。以A为首的同花顺最大。A只能出现在顺子的头或尾,不能出现在中间。KA234不是顺子。
四条——四张相同数字的牌,外加一单张。比数字大小,四条「A」最大
葫芦——由「三条」加一个「对子」所组成的牌,若别家也有此牌型,则比三条数字大小,三条相同,则比对子,都相同则比花色
同花——不构成顺子的五张同花色的牌。先比数字最大的单张,如相同再比第二支、依此类推
顺子——五张连续数字的牌组。以A为首的顺子最大,如果大家都是顺子,比最大的一张牌,如果大小还一样就比这张牌的花式
三条——牌型由三张相同的牌组成,以A为首的三条最大
二对——牌型中五张牌由两组两张同数字的牌所组成。若遇相同则先比这副牌中最大的一对,如又相同再比第二对,如果还是一样,比大对子中的最大花式
单对——牌型由两张相同的牌加上三张单张所组成。如果大家都是对子,比对子的大小,如果对子也一样,比这个对子中的最大花色
散牌——单一型态的五张散牌所组成,不成对(二对),不成三条,不成顺(同花顺),不成同花,不成葫芦,不成四条。先比最大一张牌的大小,如果大小一样,比这张牌的花色 .
此游戏需要存储所有的57张牌,并且对存储的牌具有洗牌和随机选牌功能。针对这种情况,我一般的做法是使用一个扑克牌管理者CardManager,它负责存储所有的牌(每张牌自身是一个类),并进行相应的洗牌和选择牌的功能,并能返回用户选择的牌。同时还需要有游戏者,对于游戏者来说,他本身是一个类,能够选牌,并判断自己的牌型,以及和游戏对手的牌型进行比较。将CardManager的存储逻辑和用户自己的业务分离可以减少程序的耦合性,编程时候结构会清晰很多。
根据上面的考虑,初步定义3个类:CardManger,Card, GamerNormal,以及相应的基本的功能方法。
CardManger
voidrefreshAllCards();//洗牌
void showAllCards();
Card* randomSelectOneCards();//选一张牌
void undoLastSelectOneCards();//撤销上一张选的牌
Card* reSelectOneCards();//用户撤销上一张选的牌,并重新选一张牌
VectorCard randomSelectCards(int selectNum=SELECTCARDNUM);//用户选择多张牌
Card
void setCardTypeAndNum(CardType cardType,int cardNum);
const char* getCardTypeString()const;
const char* getCardNumString() const;
CardType getType() const {return m_cardType;}
int getNum() const { return m_cardNum;}
GamerNormal
void setSelectCardsVec(const VectorCard& selectCards)
void showMyCards();
void showMyCardsStyle();
int compareWithOther(const GamerNormal& anoterGamer);
void judgeCardsStyle();
CardManger类是游戏管理者类,它管理所有的Card,每个Card代表一张牌。之所以有撤销上一张的牌,考虑的是后期游戏可能需要的撤销功能,不过在这里根本没有用。
洗牌功能:我的设想是进行for循环一百次并每次随机生成两个索引,交换两个索引对应的牌即可达到洗牌功能。用户随机选牌功能:生成一个随机数作为索引,然后取得索引对应的牌。为了提高交换两个牌的效率,可采用堆分配内存,并存储牌的指针的方式,这样每次交换也就是一个指针大小交换的开销。用户选择一张牌之后,对于这张牌应该怎么处理,是直接删除这张牌的指针并释放它的内存,还是置标记删除,还是在删除这张牌的指针后将这张被删除的指针先放到回收站,以便于下次洗牌再使用? 思考一下可知,直接删除这张牌的指针并释放它的内存被最先否决,要不下次重新洗牌的话,还需要重新new所有的牌的内存,对于程序来说,少new一些可以有效的减少内存碎片。对于置标记删除,也就是常说的假删除,删除效率极高,只需要true或者false就行了,刚开始我没有思考太多,就立马采用的这种方式,有一个标记数组,记录着哪些索引被删除,但是后来在实现用户随机选牌的功能的时候,我发现了一个问题,由于用户每次只能选择没有删除的牌,而选牌的索引是随机的,如果剩余的没有被选的牌的数量很少的情况下,例如极端情况下,只有1张,其他56张都已经被用户选完了,那么当前用户可能需要经过n多次的随机索引才能找到这张没有被删除的牌,更有甚者,如果随机函数不够随机,那么可能死循环也选不到这张牌,这是一个很严重的问题,因此我只有把原来的代码删除,重新考虑。对于将删除的指针放入到回收站,是我最终敲定的方案。过去在做服务器项目的时候,经常会以session的方式处理一个个的任务,一个任务就是一个session;任务完成,session就会被释放;任务到来,session就会被创建。这样频繁的进行session的创建和释放会降低消耗服务器的效率,因此在session结束的时候,不进行session的destory,而仅仅是将其放入到了回收站中,当需要创建session的时候,首先看回收站有没有现成可用的session,如果有直接拿来并进行自己的初始化即可,效率很高。这里在用户选择了某一张牌之后,仅仅将这张牌的指针放入回收站,在下一次进行洗牌之前,从回收站中取出所有的牌即可。
存储应该与需要实现的功能息息相关。简单的想法一个是顺序存储,一个结构化存储。我这里为了简单就使用的标准库的vector,它可以以O(1)的级别取得指定索引的牌,但是在用户选牌和删除对应索引的牌上,它的效率就次多了,需要O(n)的级别。另一种考虑非顺序的树形结构,上大学时候学过堆排序的同学们应该会立刻想到堆的存储结构,它的内部是数组,因此具有O(1)的获取效率,它的删除的效率是O(log n)级别,因此是一种更佳的方式,不过为了我的快速实现,我就没有单独实现一个堆结构,大家有兴趣可以看看数据结构与c++描述一书,上面有一个基本版本的堆实现,代码清晰简单,当然你也可以自己写一个,也就当练手了。其实用顺序存储也有一个考虑,如果真的实现客户端和服务器端的联机游戏,那么服务器端的压力是会很大,那样的话,用户的随机选牌功能就可以给干掉了,鉴于牌已经洗完了,就直接按顺序一张一张的取牌就可以了如果采用此种方式,使用数组的删除效率也是O(1)级别,不过使用服务器要注意一个问题,可能有几千组玩家在玩,不可能给每一组玩家都分配一个57张牌的内存,这样太损失内存,因为牌型是固定的;可以考虑给57牌分配固定的存储,给每一组玩家分配一个int型的扑克牌索引数组,此索引指向固定57张牌的存储。
这是一个比较复杂的问题,困扰了我很久。牌的类型比较多,像同花,顺子,葫芦,四条等等。绝不应该拿着5张牌,一种一种的试验,这样不管从效率上还是从程序结构上都不是一个好的编程实践。如何能够实现对牌型进行统一化的判断是我关注的焦点。
1,其实最主要的是鬼的存在影响了整体的判断,如果没有鬼,一切看起来都是那么顺利。于是我的关注点转移到如何将鬼转换为普通的牌型再进行判断。
2,如何判断牌全是单张?我使用了一种比较特别的办法,首先对5张牌除了鬼之外的牌构建一个map,map的key是牌的数字,value是对应牌的数字个数,这里不考虑具体的牌的花色,使用numOfJokers保存鬼的个数。例如(2,3,鬼,3,1)这五张牌构成的map就是{2,1}, {3,2}, {1,1},numOfJokers=1。通过这个例子可以发现,散牌应该可以和成对的牌归为一类,可以将顺子单独提出来判断。
l 判断是否是顺子
if(m_numOfJokers
判断m_numOfJokers小于4是考虑到如果有4个或者5个鬼,那么不应该向顺子去靠拢,比较顺子没有五鬼或者五条的级别大。判断m_cardMap.size() + m_numOfJokers==5是考虑到这种情况才没有直接的对子,才有可能构成顺子。判断maxKey – minKey<=4是考虑到如果它们的差>4,那么即使把鬼插入到它们的空当,它们肯定构不成顺子。
l 是顺子的操作
//map里面的任何一个元素的个数都是1,都只有一张牌和鬼在一起可以组成顺子
int numJokers = m_numOfJokers;
for(int i = minKey+1; numJokers; ++i)
{
if(m_cardMap.count(i) ==0)//即不存在此张牌
{
m_cardMap[i] =1;//这是一个鬼,将鬼变到中间,练成顺子
--numJokers;
}
}
采用的策略是将“鬼”插入到牌的空位中。例如3,4,5,6,鬼。那么将“鬼”插入到7的位置,即构成顺子。请注意不要插入到2的位置,虽然是顺子,但是更小了。
l 不是顺子的操作
//如果有成对的牌型,将鬼加入到最多个数的牌里面
//也或者是没有一个鬼,直接是散牌
for(int i =0; i < m_numOfJokers; ++i)
{
m_cardMap[maxCountKey]++;//将所有的鬼配置到value最大的key里面
}
/如果有成对的牌型,将鬼加入到最多个数的牌里面
//也或者是没有一个鬼,直接是散牌
for(int i =0; i < m_numOfJokers; ++i)
{
m_cardMap[maxCountKey]++;//将所有的鬼配置到value最大的key里面
}
这里将“鬼”放到最多个数的牌型里面。例如对于{2,3,4,3,鬼}这种牌型,对于map来说,3的个数最多,那么应该将“鬼”和3配对才能发挥它的最大功效,得到最大的牌。
l 判断牌型的操作
现在很开心,鬼不再是鬼了,它已经被同化到map中,跟普通的牌型一样。有了这个map,一切看起来都是那么简单。根据map.size()的大小逐个判断即可了。如果是5个的情况,需要判断其是不是顺子(其实前面构建map的时候已经知道它是不是顺子了,但是这里为了统一化起见,再判断一遍)。如果是4个,那么其只能是只有一对的情况;如果是3个,那么其有两种情况,两对或者三条,这个可以根据map中具有最大个数的数字的个数是2个还是3个就可以判断;如果是2个,那么可能是四条或者葫芦,同样可以根据最大的个数是4个还是3个判断;如果是1个,那么是五条或者是五鬼,根据牌的数值既可以判断。到这里是不是发现还缺少一个同花呢?关于同花只需要在构建map的过程中,遍历所有非鬼的牌,判断它们的花色是否一致即可,这个比较简单单独提了出来,主要是因为牌型只有同花顺,却没有同花四条,同花单对之类的说法。例如{2,3,5,7,鬼}同花色,它们可以构成“对7”,也是同花。你说是同花也可,说是“单对”也可,为了避免这种判断,我单独设置了一个bool变量,保存是否是同花,如果后期需要改进这些叫法,我只需要根据此bool变量稍加一些判断就OK了。
l 下面为具体的判断style的代码
void GamerNormal::judgeCardsStyle()
{
if(CardsStyleNone !=m_cardStyle){
cout<<"Already judged Cards Style..."< 4)? true: false;//最大的key与最小的key的差值是否大于4(例如4,5,6,7,8)
if(isOddCard){
m_cardStyle = CardsStyleOddCard;//散牌
}
else{
m_cardStyle = CardsStyleStraight;//顺子
}
}
else
{//有非散牌的组合(2个,3个,4个,5个)
int maxCount, minCount;
getMaxMinCount(maxCount, minCount);
switch (m_cardMap.size())
{
case 1://5(五鬼,或者五张)
{
if(m_cardMap.count(CardsNumJoker) >0)//如果有非鬼的牌,鬼被同化为普通的配对牌
m_cardStyle =CardsStyleFiveOfJokers;//五鬼
else
m_cardStyle =CardsStyleFiveOfKind;//五张(2,2,2,鬼,鬼)
}
break;
case 2://4+1, 2+3
{
if(maxCount ==4)//4+1
m_cardStyle =CardsStyleFourOfKind;//四条
else//2+3
m_cardStyle =CardsStyleFullHouse;//葫芦
}
break;
case 3://2+2+1, 3+1+1
{
if(maxCount ==3)
m_cardStyle =CardsStyleThreeOfKind;
else
m_cardStyle =CardsStyleTwoPair;
}
break;
case 4://2+1+1+1+1
{
m_cardStyle = CardsStyleOnePair;
}
break;
default:
{
cout<<"GamerNormal::judgeCardsStyle()error m_cardMap.size(): "<
static constint CardNumber=57;//一共57张牌
void CardManager::refreshAllCards()//洗牌,1,回收用户手中的牌,2,调用交换程序进行洗牌
{
//回收用户手中的牌
while (!m_recycleCardsVec.empty())
{
Card *pOld = m_recycleCardsVec.back();
m_recycleCardsVec.pop_back();
m_remainCardsVec.push_back(pOld);
}
assert(m_remainCardsVec.size() ==CardNumber);
//循环一百次,任意选择两个索引,然后交换
for (int i =0; i< 100; ++i)
{
int a = rand() % CardNumber;
int b = rand() % CardNumber;
swap(m_remainCardsVec[a],m_remainCardsVec[b]);
}
}
//选择一张牌。从剩余的牌中删除它,并将其加入到回收站中
Card* CardManager::randomSelectOneCards()
{
if(m_remainCardsVec.size() <=0)
return NULL;
int cur = rand() % m_remainCardsVec.size();
Card* pCard = m_remainCardsVec[cur];
m_remainCardsVec.erase(m_remainCardsVec.begin()+cur);//原来的牌中将其删除
m_recycleCardsVec.push_back(pCard);//将选择的牌中将其加入到回收站中
return pCard;
}
//选择多张牌,循环调用选一张牌,并返回用户所选到的牌的数组(selectNum=5)
VectorCard CardManager::randomSelectCards(int selectNum)
{
VectorCard selectCards;
for (int i =0; i
扑克牌对象就是一个简单的数据model。如游戏规则所说,可知其应该具有花色类型和牌的数字。很容易想到有“鬼”比较特别,它不具有数字属性,因此将它的花色与另外四种花色定义在一起,以花色决定其特别之处。对于J,Q,K,A这四种牌的数字,为了简单起见,我分别使用11,12,13,14. 这样牌一共有五种花色,一共有2-14这样的数字。
1,下面为我进行的牌类型定义,为了与其他带花色的区别,我另外定义了一种None的花色:
typedef enum CardType{
//黑桃>红桃>方块>草花
CardTypeSpade=4,
CardTypeHeart=3,
CardTypeDiamond=2,
CardTypeClub=1,
CardTypeJoker=0,
CardTypeNone=-1,
}CardType;
2,下面为进行的牌的数字的定义,仅定义2-14范围之外的值,便于程序中给变量赋初始值
typedef enum CardsNum{
CardsNumJoker = -1,
CardsNumOverMin = 0,
CardsNumOverMax = 16,
}CardsNum;
3,添加一些set和get方法,以及为了方便打印进行的 运算符的重载,还有一些便利方法,如判断是不是鬼,获取牌的数字字母表示,获取牌的花色字母表示。基本上命名和其功能对应,希望能起到见名知意的效果。
voidsetCardTypeAndNum(CardType cardType,int cardNum);
const char*getCardTypeString() const;
const char* getCardNumString() const;
CardType getType() const {return m_cardType;}
int getNum() const { return m_cardNum;}
public:
bool isJoker() const { return m_cardType == CardTypeJoker;}
public:
friend ostream& operator<<(ostream& os, const Card& card);
friend ostream& operator<< (ostream& os, const Card* card);
3.6 如何实现游戏用户对象
游戏用户是真正的游戏驱动者,它通过使用牌管理者(CardManager)提供的选牌接口,来进行选牌。它应该具有打印自己的牌,判断自己的牌类型,以及与其他用户牌型进行比较的功能。将游戏用户和牌管理者分离,使游戏用户不必关心具体如何随机选牌的实现细节,仅仅关心对选到的牌如何处理就OK。这里我通过将CardManager的选牌的接口返回的牌数组传递给游戏用户即可。 注意:如果是服务器端程序,那么会有很多的游戏用户,对于很多的用户同样需要一个UserManager的概念,它负责具体的创建,删除,查找用户的等等操作,当然你也可以使用前面CardManager介绍的回收站的概念以提高服务器效率,对于真的想做服务器的朋友,可以采用事件驱动的机制,客户端的所有请求到服务器都是一个个的事件,固定好事件id,从事件队列取出一个事件,根据事件id处理事件,有很多这样的类似机制,我看过的windows 消息,pjsip的消息处理,我们公司的服务器,ctevent库等等都是这种方式,可以动态多线程处理,效率还可以。
1,下面为传递给用户5张牌数组的代码:
voidsetSelectCardsVec(constVectorCard& selectCards){
m_selectCardsVec =selectCards;
assert(m_selectCardsVec.size() ==SELECTCARDNUM);
m_cardStyle = CardsStyleNone;//恢复初始状态
m_numOfJokers = 0;
}
2,下面为用户的一些外部可调用接口,包括显示自己的牌,显示自己的牌型,判断自己的牌型,与其他人的牌型进行比较。
public:
void showMyCards();
voidshowMyCardsStyle();
int compareWithOther(constGamerNormal& anoterGamer);
void judgeCardsStyle();
public:
void showMyCards();
voidshowMyCardsStyle();
int compareWithOther(constGamerNormal& anoterGamer);
void judgeCardsStyle();
char yesOrNo;
CardManager manager;
bool isRun = true;
manager.refreshAllCards();
while (isRun) {
cout<<"Input Y to deal, input N to quit..."< ";
cin>>yesOrNo;
if (yesOrNo == 'Y' || yesOrNo == 'y')
{
VectorCard myCards = manager.randomSelectCards(5);
GamerNormal gamer(myCards, "lipeng");
gamer.showMyCardsStyle();
}
else if(yesOrNo == 'N' || yesOrNo == 'n') {
cout<<" Quit game."<
源码地址 期待您的批评指正