现在的位置: 首页 > 综合 > 正文

同花顺扑克牌游戏-C++

2017年12月15日 ⁄ 综合 ⁄ 共 9617字 ⁄ 字号 评论关闭
文章目录

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

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

1      游戏规则

[游戏背景]

 我们的一付牌里,有 52张普通牌和 5张鬼。

 52 张普通牌分成 4种花色,从大到小依次为黑桃(Spade)、红桃(Heart)、方块(Diamond)、草花(club),每种花色是13张牌2-10JQKA

 5张鬼不分大小,可以当作任意牌来组成牌型。

 

  牌型比较:五鬼>五条>同花顺>四条>葫芦>同花>顺子>三条>二对>单对>散牌。

   数字比较:AKQJ1098765432

   花式比较:黑桃>红桃>方块>草花

 

 五鬼——五张鬼

 五条——五张相同数字的牌,其中至少一张为鬼。

 同花顺——拥有五张连续性同花色的顺子。以A为首的同花顺最大。A只能出现在顺子的头或尾,不能出现在中间。KA234不是顺子。

   四条——四张相同数字的牌,外加一单张。比数字大小,四条「A」最大

   葫芦——由「三条」加一个「对子」所组成的牌,若别家也有此牌型,则比三条数字大小,三条相同,则比对子,都相同则比花色

   同花——不构成顺子的五张同花色的牌。先比数字最大的单张,如相同再比第二支、依此类推

   顺子——五张连续数字的牌组。A为首的顺子最大,如果大家都是顺子,比最大的一张牌,如果大小还一样就比这张牌的花式

   三条——牌型由三张相同的牌组成,以A为首的三条最大

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

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

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

2      我的想法

2.1    程序整体考虑

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

2.2    程序简单结构

根据上面的考虑,初步定义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代表一张牌。之所以有撤销上一张的牌,考虑的是后期游戏可能需要的撤销功能,不过在这里根本没有用。

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里面
        }

这里将“鬼”放到最多个数的牌型里面。例如对于{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;//五鬼
                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(): "<<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   程序源代码链接

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

抱歉!评论已关闭.