很久没有写过博客了,等过了这两周开始将自己学的东西汇总整理一下。

今天写的是一个扑克牌的游戏的一个片段,它仅仅是一个命令行的工程而已,它还不具有对弈功能。

1      游戏规则

[游戏背景]

我们的一付牌里,有 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为首的三条最大

  二对——牌型中五张牌由两组两张同数字的牌所组成。若遇相同则先比这副牌中最大的一对,如又相同再比第二对,如果还是一样,比大对子中的最大花式

  单对——牌型由两张相同的牌加上三张单张所组成。如果大家都是对子,比对子的大小,如果对子也一样,比这个对子中的最大花色

  散牌——单一型态的五张散牌所组成,不成对(二对),不成三条,不成顺(同花顺),不成同花,不成葫芦,不成四条。先比最大一张牌的大小,如果大小一样,比这张牌的花色 .

2      我的想法

2.1    程序整体考虑

此游戏需要存储所有的57张牌,并且对存储的牌具有洗牌和随机选牌功能。针对这种情况,我一般的做法是使用一个扑克牌管理者CardManager,它负责存储所有的牌(每张牌自身是一个类),并进行相应的洗牌和选择牌的功能,并能返回用户选择的牌。同时还需要有游戏者,对于游戏者来说,他本身是一个类,能够选牌,并判断自己的牌型,以及和游戏对手的牌型进行比较。将CardManager的存储逻辑和用户自己的业务分离可以减少程序的耦合性,编程时候结构会清晰很多。

2.2    程序简单结构

根据上面的考虑,初步定义3个类:CardManger,Card, GamerNormal,以及相应的基本的功能方法。

CardMangervoidrefreshAllCards();//洗牌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代表一张牌。之所以有撤销上一张的牌,考虑的是后期游戏可能需要的撤销功能,不过在这里根本没有用。

3.     具体思路

3.1   如何实现随机洗牌和随机选牌

洗牌功能:我的设想是进行for循环一百次并每次随机生成两个索引,交换两个索引对应的牌即可达到洗牌功能。用户随机选牌功能:生成一个随机数作为索引,然后取得索引对应的牌。为了提高交换两个牌的效率,可采用堆分配内存,并存储牌的指针的方式,这样每次交换也就是一个指针大小交换的开销。用户选择一张牌之后,对于这张牌应该怎么处理,是直接删除这张牌的指针并释放它的内存,还是置标记删除,还是在删除这张牌的指针后将这张被删除的指针先放到回收站,以便于下次洗牌再使用? 思考一下可知,直接删除这张牌的指针并释放它的内存被最先否决,要不下次重新洗牌的话,还需要重新new所有的牌的内存,对于程序来说,少new一些可以有效的减少内存碎片。对于置标记删除,也就是常说的假删除,删除效率极高,只需要true或者false就行了,刚开始我没有思考太多,就立马采用的这种方式,有一个标记数组,记录着哪些索引被删除,但是后来在实现用户随机选牌的功能的时候,我发现了一个问题,由于用户每次只能选择没有删除的牌,而选牌的索引是随机的,如果剩余的没有被选的牌的数量很少的情况下,例如极端情况下,只有1张,其他56张都已经被用户选完了,那么当前用户可能需要经过n多次的随机索引才能找到这张没有被删除的牌,更有甚者,如果随机函数不够随机,那么可能死循环也选不到这张牌,这是一个很严重的问题,因此我只有把原来的代码删除,重新考虑。对于将删除的指针放入到回收站,是我最终敲定的方案。过去在做服务器项目的时候,经常会以session的方式处理一个个的任务,一个任务就是一个session;任务完成,session就会被释放;任务到来,session就会被创建。这样频繁的进行session的创建和释放会降低消耗服务器的效率,因此在session结束的时候,不进行session的destory,而仅仅是将其放入到了回收站中,当需要创建session的时候,首先看回收站有没有现成可用的session,如果有直接拿来并进行自己的初始化即可,效率很高。这里在用户选择了某一张牌之后,仅仅将这张牌的指针放入回收站,在下一次进行洗牌之前,从回收站中取出所有的牌即可。

3.2   如何存储所有的牌

存储应该与需要实现的功能息息相关。简单的想法一个是顺序存储,一个结构化存储。我这里为了简单就使用的标准库的vector,它可以以O(1)的级别取得指定索引的牌,但是在用户选牌和删除对应索引的牌上,它的效率就次多了,需要O(n)的级别。另一种考虑非顺序的树形结构,上大学时候学过堆排序的同学们应该会立刻想到堆的存储结构,它的内部是数组,因此具有O(1)的获取效率,它的删除的效率是O(log n)级别,因此是一种更佳的方式,不过为了我的快速实现,我就没有单独实现一个堆结构,大家有兴趣可以看看数据结构与c++描述一书,上面有一个基本版本的堆实现,代码清晰简单,当然你也可以自己写一个,也就当练手了。其实用顺序存储也有一个考虑,如果真的实现客户端和服务器端的联机游戏,那么服务器端的压力是会很大,那样的话,用户的随机选牌功能就可以给干掉了,鉴于牌已经洗完了,就直接按顺序一张一张的取牌就可以了如果采用此种方式,使用数组的删除效率也是O(1)级别,不过使用服务器要注意一个问题,可能有几千组玩家在玩,不可能给每一组玩家都分配一个57张牌的内存,这样太损失内存,因为牌型是固定的;可以考虑给57牌分配固定的存储,给每一组玩家分配一个int型的扑克牌索引数组,此索引指向固定57张牌的存储。

3.3   如何判断5张牌对应的牌型

这是一个比较复杂的问题,困扰了我很久。牌的类型比较多,像同花,顺子,葫芦,四条等等。绝不应该拿着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 <SELECTCARDNUM-1 &&
m_cardMap.size() +m_numOfJokers == SELECTCARDNUM &&
maxKey - minKey <= SELECTCARDNUM-1) {
//为顺子的情况
}

判断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..."<<endl;return;}cout<<"begin to Judge Cards Style:"<<endl;buildCardNumMap();if(m_cardMap.size() ==SELECTCARDNUM){bool isOddCard = false; //只能是单张或者顺子(计算key值是不是连续的)int maxKey, minKey;getMaxMinKey(maxKey, minKey);//得到最大和最小的牌数字isOddCard = (maxKey-minKey > 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;//五鬼elsem_cardStyle =CardsStyleFiveOfKind;//五张(2,2,2,鬼,鬼)}break;case 2://4+1, 2+3{if(maxCount ==4)//4+1m_cardStyle =CardsStyleFourOfKind;//四条else//2+3m_cardStyle =CardsStyleFullHouse;//葫芦}break;case 3://2+2+1, 3+1+1{if(maxCount ==3)m_cardStyle =CardsStyleThreeOfKind;elsem_cardStyle =CardsStyleTwoPair;}break;case 4://2+1+1+1+1{m_cardStyle = CardsStyleOnePair;}break;default:{cout<<"GamerNormal::judgeCardsStyle()error m_cardMap.size(): "<<m_cardMap.size()<<endl;}break;}}//else}

3.4   如何具体实现洗牌

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]);}
}

3.5   如何具体实现用户选牌功能

//选择一张牌。从剩余的牌中删除它,并将其加入到回收站中

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 <selectNum; ++i) {Card* pCard = randomSelectOneCards();assert(pCard != NULL);selectCards.push_back(pCard);}return selectCards;
}

3.6   如何实现单张的扑克牌对象

扑克牌对象就是一个简单的数据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();

3.7   游戏的main函数

    char yesOrNo;CardManager manager;bool isRun = true;manager.refreshAllCards();while (isRun) {cout<<"Input Y to deal, input N to quit..."<<endl;cout<<"> ";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."<<endl;isRun = false;}else{cout<<"Unknown Command!!"<<endl;}}return 0;

3.8   程序源代码链接

源码地址 期待您的批评指正

同花顺扑克牌游戏-C++相关推荐

  1. Java顺序表 实现扑克牌游戏简单 (梭哈 / 斗牛)

      简单的扑克牌游戏 梭哈:   梭哈用的是扑克牌共52张牌,因为不容易出好牌,也有去掉 234567 的简易玩法,规则玩法花样很多.   在这里我们采用:52 张牌,3 个人,一人 5 张牌,按规则 ...

  2. JAVA控制台扑克牌游戏,洗牌,发牌,比较大小

    此游戏是一个简单的并且很基础的java控制台程序.这个扑克牌游戏主要的游戏过程是:首先创建一副扑克牌,创建好了后要进行洗牌,牌洗好了,需要玩家来玩,接下来就创建玩家.有洗好的牌,也有玩家了,那么就开始 ...

  3. python纸牌游戏_《升级》扑克牌游戏——Python实现

    [实例简介] <升级>扑克牌游戏--Python实现,包括UI界面,AI玩家,裁判监督三大模块. [实例截图] [核心代码] UI └── UI ├── Readme.pdf ├── UI ...

  4. Python 抽扑克牌游戏

    ''' Python 抽扑克牌游戏 by 郑瑞国 ''' import random puke=[x for x in range(1,55)] pukes={} #print(puke)types ...

  5. Java实现扑克牌游戏(简易炸金花)

    本篇将实现Java版的扑克牌游戏 目录 一.创建扑克牌Poker 二.游戏玩法创建 2.1买牌 2.2洗牌 2.3揭牌 三.测试游戏代码 一.创建扑克牌Poker 在游戏开始之前要创建扑克牌,其中扑克 ...

  6. style=扑克牌游戏大家应该都比较熟悉了,一副牌由54张组成,含3~A、2各4张,小王1张,大王1张。 牌面从小到大用如下字符和字符串表示(其中,小写joker表示小王,大写JOKER表示大

    题目:扑克牌游戏大家应该都比较熟悉了,一副牌由54张组成,含3~A.2各4张,小王1张,大王1张.牌面从小到大用如下字符和字符串表示(其中,小写joker表示小王,大写JOKER表示大王):3 4 5 ...

  7. JAVA钓鱼游戏_5个小时写一个扑克牌游戏——金钩钓鱼

    罗大佑有歌云:"无聊的日子总是会写点无聊的歌曲......",我不是歌手,我是程序员,于是无聊的日子总是会写点无聊的程序.程序不能太大,不然没有时间完成:程序应该有趣,不然就达不到 ...

  8. c语言编程的扑克牌游戏,扑克牌加减乘除游戏

    扑克牌加减乘除游戏,是一种集技巧性和运气性于一体的扑克牌游戏. 现实中可以单人玩,也可以不定人数对抗,用若干副扑克牌去掉王即可游戏,推荐用一副牌双人对抗,乐趣无穷. 本程序利用Card类数组模拟扑克牌 ...

  9. 扑克牌游戏——老牛拉破车

    目录 引子 构思以及代码部分 初始菜单以及变量的初始化 判断牌的花色及牌面数字 洗牌 帮助界面 出牌堆 轮 主函数 游戏界面效果展示 存在的问题以及未来展望 引子 不知道大家有没有玩过一个叫做&quo ...

最新文章

  1. [UWP]了解模板化控件(5):VisualState
  2. 【干货】JMeter BeanShell 应用
  3. 世界经济增速统一放缓,网络拓扑发现统一增速
  4. 计算机专硕没有宿舍,没有补贴,不提供宿舍,读研究生还要家里支持,应届生读非全太难...
  5. com.alibaba.druid.pool.DruidDataSource.error解决办法
  6. Windows系统下,好用的录屏软件工具/屏幕录制工具
  7. java mongodb 关闭连接_如何在mongodb上使用java驱动程序保持连接池关闭?
  8. keras中的模型保存和加载
  9. storm-基本概念
  10. 网页布局02 盒子模型
  11. 条码软件上的多行文字如何换行
  12. 花了一天的时间给粉丝做了一个小米官网(高仿)
  13. matlab 生命游戏
  14. lenovo L480 进入bios_rx5700刷bios秒变rx5700xt!rx5700刷rx5700xt bios图文教程
  15. 金融数字化平台建设的三大误区和破局之道
  16. 51万年历林贤文:做一个不“安分”的程序员
  17. python画矢量图_使用基于matplotlib的SciencePlots绘制精美图表
  18. 【深度学习】01 - 图像识别
  19. git pull拉代码git did not exit cleanly (exit code 1)
  20. ​GIS方向考研简述

热门文章

  1. 安全生产培训教育课件PPT模板
  2. 【华为OD机试模拟题】用 C++ 实现 - 新员工座位安排系统(2023.Q1)
  3. 计算机桌面图片唐诗,古诗词高清电脑壁纸 古诗词电脑桌面壁纸
  4. 学习笔记——条件随机场(基于自然语言和机器学习理解)
  5. 汽车操作系统研发:“广义”带动“狭义”——东软睿驰总经理曹斌谈“软件定义汽车”
  6. VisualSVN Server使用手册
  7. Win10开机wsappx进程占用CPU资源过高,最全的解决方法总结。
  8. 项目管理|如何制定项目进度计划?
  9. python networkx案例_Python包 - networkx
  10. 跨境边民带货溯源方案分享