我的第一篇博客_在Linux下用C++编写的德州扑克游戏

序言

此博客仅记录个人程序编写思路
未经允许严禁转载
用面向对象的方式编写能在Linux的命令行下能游玩的德州扑克游戏。Gitee仓库

抽象思路

对现实世界的单局游戏来说:

  • 一组玩家
  • 牌桌
  • 荷官

其中荷官可以看成是整局的游戏的管理者,将会对整局游戏进行调度。

第一次抽象:

  • 一组玩家
    • 玩家:
      • 手牌
      • 筹码
      • 牌力
  • 牌桌
    • 牌堆
    • 公共牌
    • 奖池

这当中, 筹码奖池 可以一同泛化为 筹码币
筹码币 的行为主要是 获得使用
这部分内容属于搭建游戏靠后的内容,或者说可以单独列出来。
因为很多基于喊注的游戏来说,都有着和德州类似的下注流程。

其次就是 手牌牌堆公共牌 都可以看成是 一组牌
一组牌 又是由很多张单独的卡牌组合而成。
可以顺利成章的泛化出 这个基类。

最后就是 玩家牌力 ,其中玩家可以派生出 人类玩家电脑玩家
电脑玩家也可以接着派生出初级、中级、高级电脑玩家。
而对于 牌力 ,又或者在德州扑克里叫做 牌型。
本质上就是对 5张牌 的类型 判断比较

牌力的判断和比较,可以说是第一个核心部分。
另一部分就是电脑玩家的 下注逻辑 编写。

类的抽象

可以说是从易到难,依次是

  • 牌类
  • 筹码币类
  • 玩家类
  • 牌力算法

牌类(Card)

对于现实世界来说,每一张扑克牌都有两个属性

  • 点数
  • 花色

这里只针对德州扑克的52(4x13)张,不考虑大小王或者癞子的加入。
因为牌的数目很小,其实可以就用从0-51的数字表示每一张牌。
自然而然用一个 char 型就能保存了。

using CardType=char;

class Card
{
protected:
	CardType _card;
public:
	//默认构造
	Card():_card(0){}
	~Card(){}

那么问题来了,如何获取点数和花色呢?
很简单,通过对 _card 的运算可以获得。

  • 点数
    0-51可以分成4组0-12,13-25,26-38,39-51。
  • 花色
    每一组自然对应四种花色♠、♥、♣、♦

具体如何映射看个人,笔者这里就随意了。
可以简单用一个枚举或者宏定义。如下

using ColourType=int;
using NumType=int;
//0,1,2,3
//♠,♥,♣,♦
enum Suits:ColourType{
    SUITS_SPADE,//♠
    SUITS_HEARTS,//♥
    SUITS_DIAMONDS,//♣
    SUITS_CLUBS//♦
};
//点数2,3,4,5,6,7,8,9,10,J ,Q ,K ,A
//对应2,3,4,5,6,7,8,9,10,11,12,13,14
enum Nums : NumType
{
    NUM_2 = 2,
    NUM_3,
    NUM_4,
    NUM_5,
    NUM_6,
    NUM_7,
    NUM_8,
    NUM_9,
    NUM_10,
    NUM_J,
    NUM_Q,
    NUM_K,
    NUM_A
};

通过对 _cards 取余做除 获得 点数花色


using CardType=char;
class Card
{
 protected:
    CardType _card;
    //用char一个字节来表示卡牌的点数和花色
    //0 ,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,
    //_card%13+2=点数
    //_card/13=花色   
    
public:
    Card()
    :_card(0){}
    Card(NumType num,ColourType colour) 
    :_card(colour*13+(num-2)){}
    NumType GetNum()const{return (_card%13)+2;}
    ColourType GetColour()const{return _card/13;}

为什么没有去定义一个可以修改_card的成员函数?
或者可以改变点数或者花色的成员函数?

  • 因为从现实世界来说,每张牌生成之后,它的点数和花色就应该是固定的,无法被修改,我们只能查看它。

那为什么不考虑在堆上声请堆空间保存_card呢?

  • 没必要,申请堆空间同样需要时间,而且纵观整个游戏流程来说:
    1.存在大量卡牌的交换。
    2.牌力算法上可能会涉及大量的运算和比较。
    3.就一个char,很小。

!还没设计好
既然提到了大量的使用,必然存在很多 复制、调用和比较
所以需要将其 比较函数复制函数 全部补全。
(这里就不做展示了,详见仓库)Card.hpp
最后还得补充上每张牌的可视化,数号和花色如何打印。
同时还得考虑到后面的一组牌的可视化。
以下代码仅供参考:

//公共函数
namespace CARD{

inline void PrintNum(const NumType&);
inline void PrintColour(const ColourType&);

}//end of CARD

namespace CARD{

inline void PrintColour(const ColourType&colour){
        if(colour==SUITS_SPADE){
            cout<<"\033[34m♠\033[0m";
        }else if(colour==SUITS_CLUBS){
            cout<<"\033[31m♦\033[0m";
        }else if(colour==SUITS_HEARTS){
            cout<<"\033[31m♥\033[0m";
        }else if(colour==SUITS_DIAMONDS){
            cout<<"\033[34m♣\033[0m";
        }
    }

inline void PrintNum(const NumType&num){
     if(num==NUM_J){cout<<"J";}
    else if(num==NUM_Q){cout<<"Q";}
    else if(num==NUM_K){cout<<"K";}
    else if(num==NUM_A){cout<<"A";}
    else {cout<<static_cast<int>(num);}
}    

}//end of CARD

之后便是在基础Card的基础上去设计一组卡牌的类了。

一组牌(GroupCards)

首先最好想到的一点就是用一个容器去存储Card,
那不如就用 vector 吧。
其次一组牌还得有一个 牌的数目 的数据成员,毕竟对于其派生类来说,最重要的区别就是在于牌的数目。

当然,可以把这个类设计为虚类,虽然表面看,不同一组牌直接没有明显的“多态的形式”。虽然它们都可以泛化为一组牌。

参考代码如下:

using Cards=vector<Card>;
using NumsType=unsigned int;

class GroupCards 
{
public:
    GroupCards(NumsType nums)
    :_nums(nums)
    ,_cards()
    {
        _cards.reserve(_nums);
    }
    virtual~GroupCards() {}//没啥用,毕竟vector很懂事
    NumsType GetNums()const{return _nums;}
    size_t GetSize()const{return _cards.size();}
    const Card*GetFirstCards()const{return &_cards.front();}
    Cards& ReturnCards(){return _cards;}
    Cards ReturnCards()const{return _cards;}
protected:
    Cards _cards;
    NumsType _nums;

};

之后就是对一组牌的一些可能会用到的操作

  1. 排序 ,根据数号,根据花色(升序降序要结合之后的牌力算法考虑)
  2. 图像化,最好写一个万能接口,能根据想要的形式打印。还得安全。
  3. 可能需要一组牌与另一组牌的 相加 ,返回更长的一组牌。(或者别的什么运算,综合牌力算法考虑)
namespace GROUP_CARDS{
    //指定几行几列打印卡牌
    //row:行 cow:列
    void PrintCards(const GroupCards &cards, const int row=1,const int cow=5);
    void SortCardsNum(Cards& cards);
    void SortCardsColour(Cards& cards);
    Cards CardsAddCards(Cards& Lcards,Cards& Rcards);
}//end of CARDS

方法具体实现:(仅展示排序和相加,毕竟图像化可以多种多样)

void GROUP_CARDS::SortCardsNum(Cards&cards){
    sort(cards.begin(),cards.end(),[](Card a,Card b){
        if(a.GetNum()==b.GetNum()){
            return a.GetColour()<b.GetColour();
        }
        return a.GetNum()<b.GetNum();
    });
}

void GROUP_CARDS::SortCardsColour(Cards&cards){
        sort(cards.begin(),cards.end(),[](Card a,Card b){
        if(a.GetColour()==b.GetColour()){
            return a.GetNum()<b.GetNum();
        }
        return a.GetColour()<b.GetColour();
    });
}

Cards GROUP_CARDS::CardsAddCards(Cards& Lcards,Cards& Rcards){
    Cards tmp;
    tmp.reserve(Lcards.size()+Rcards.size());
    for(auto &rc:Lcards){
        tmp.push_back(rc);
    }
    for(auto &rc:Rcards){
        tmp.push_back(rc);
    }
    return tmp;
}

写排序的时候,可以练习到 Lambda表达式 ,当然这是C++11的特性。
在写这一部分的时候,考虑到视觉上的比较效果。
我对基于数号的排序设置为,从左到右的升序。
同时考虑到下标运算符对 Cards 也就 vector<Card> 的越界访问,
我相当于重载了 Cards 的下标访问运算符。(个人用法)

//!!!!记得放到.c文件中,不然会重定义。
template<>
Card& Cards::operator[](size_t x){
    x=x%size();//对长度取余
    return *(begin()+x);
}

时间记录
2022.11.18

在一组牌的基础上去派生其他类

牌堆(DeckCards)
牌堆类设计

直接上代码:

#ifndef _CTL_DECKCARDS_HPP_
#define _CTL_DECKCARDS_HPP_

#include "GroupCards.hpp"
#include <random>

using UseType=unsigned int;

class DeckCards
:public GroupCards
{
public:
	//对于德州来说,卡组就只有52张
    DeckCards(NumsType nums=52)
    :GroupCards(nums)
    ,_used(0)
    {}
    ~DeckCards() {}
    void PrintDeckCards(int row=13,int cow=4)const; 
    void SetDeckCards();//初始化卡组

    Card SendCard(){
        _used=_used%_nums;//防止发牌越界
        return _cards[_used++];}
    void ResetSend(){_used=0;}
	
    void ShuffleDeck(int times=1);//洗牌
private:
	//记录发牌的位置
    UseType _used;
};
#endif

对于DeckCards来说

  • 增加的数据成员
    • 发牌的标记位 _used
  • 增加的方法
    • 初始化卡组 SetDeckCards()
    • 洗牌 ShuffleDeck()
    • 发牌 SendCard()
    • 重置发牌位 ResetSend()
初始化卡牌

先看初始化卡牌 :

//初始化卡组
void DeckCards::SetDeckCards(){
    for(int i=0;i<13;++i){
        for(int j=0;j<4;++j){
        //i代表点数,j代表花色
        //注意对于Card的属性的映射
        //这里i+2对应实际牌点数的大小
            _cards.push_back(Card(i+2,j));
        }
    }
}
  • 可能会觉得这样利用 vector 的 push_back 将 Card 加进去,在结束的时候应该加上 shrink_to_fit()
  • 其实没必要,因为在 GroupCards 的构造函数 里就已经根据张数给容器预留空间了。
洗牌

关于洗牌的实现,笔者纠结了很久,因为如果不是随机的洗牌,可能会导致每个玩家的获胜概率不均匀。尽可能要在洗牌的时候,让每张牌在每个位置出现的概率相同。

//洗牌,默认1次
void DeckCards::ShuffleDeck(int times){
    ::srand(time(NULL));
    Card tmp;
    for(int j=0;j<times;++j){
        for(int i=52-1;i>=0;--i){
            tmp=_cards[i];
            int ch=::rand()%(i+1);
            _cards[i]=_cards[ch];
            _cards[ch]=tmp;
        }
    }
}
  • 洗牌的设计思路:
    1. 遍历卡组
    2. 将当前卡牌暂存
    3. 生成一个随机数,范围是未洗牌的下标范围
    4. 将当前的卡牌 与 随机数下标的卡牌交换
    • 换个角度想,就是手里拿着一副牌,每次随机的,从剩余的牌里选一张出来,将其放置。将所有的牌放置完以后,得到的就是一副每张牌在每个位置出现概率相同的卡组
  • 出于习惯,笔者在C++里用到的所有 C的头文件,均采用C++提供的,例如此处用到的是<cstdlib>
    这里卡牌的交换可以优化。
与手牌公共牌的关系
  • 现实游戏里,无论是手牌的还是公共牌都是从牌堆获得卡牌
  • 自然,手牌公共牌 都是依赖于 牌堆
  • 或者说, 手牌公共牌获取 都需要通过 牌堆的 SendCard() 得到。

    SendCard()函数详见DeckCards的头文件
    类图如下:
    牌的类图

手牌(HandCards)

代码如下:

#ifndef _CTL_HANDCARDS_HPP_
#define _CTL_HANDCARDS_HPP_

#include "DeckCards.hpp"

class HandCards
:public GroupCards//公有继承
{
public:
    HandCards(DeckCards& deck)//构造时必须绑定Deck,语义上没有卡组,哪来的手牌。
    :GroupCards(2)
    ,_deck(&deck)
    {}
    ~HandCards() {}
    void show()const;
    void AutoGetSafe();//获取卡牌
    void AutoGet();
    void CleanCards(){_cards.clear();}//清空手牌
    //仅提供一个换绑接口,没啥用,或许存在让手牌再去绑定另一个卡组的情况?
    void RebindDeck(DeckCards& deck){_deck=&deck;}
private:
    DeckCards* _deck;
};
//德州手牌获取
inline void HandCards::AutoGetSafe(){
    if(GetSize()==0||GetSize()==1){
        _cards.push_back((*_deck).SendCard());
    }else{}
}

inline void HandCards::AutoGet(){
    _cards.push_back((*_deck).SendCard());
}

#endif
  • 需要注意的点
    • 确保获取卡牌时,不会让手牌的数目超过上限。安全的获得卡牌。
    • 记得提供 清空手牌 的接口。
公共牌(PublicCards)

公共牌的设计和手牌类似
代码如下:

#ifndef _CTL_PUBLICCARDS_HPP_
#define _CTL_PUBLICCARDS_HPP_

#include "DeckCards.hpp"

class PublicCards
:public GroupCards
{
public:
    PublicCards(DeckCards& deck)
    :GroupCards(5)
    ,_deck(&deck)
    {}
    ~PublicCards() {}
    void show()const;
    void AutoGetSafe();
    void AutoGet();
    void CleanCards(){_cards.clear();}
    void RebindDeck(DeckCards & deck){_deck=&deck;}
private:
    DeckCards* _deck;
};

inline void PublicCards::AutoGetSafe(){
    if(GetSize()==0){//发牌圈,发三张弃一张
        _cards.push_back((*_deck).SendCard());
        _cards.push_back((*_deck).SendCard());
        _cards.push_back((*_deck).SendCard());
        (*_deck).SendCard();
        return;
    }else if (GetSize()==3){//转牌圈,发一张弃一张
        _cards.push_back((*_deck).SendCard());
        (*_deck).SendCard();
        return;
    }else if(GetSize()==4){//河牌圈,发一张弃一张
        _cards.push_back((*_deck).SendCard());
        return;
    }else{}
}

inline void PublicCards::AutoGet(){
    _cards.push_back((*_deck).SendCard());
}

#endif

公共牌和手牌基本没有什么差别。
AutoGetSafe() 里可以再增加错误情况的打印信息。
至此,所有和牌有关的实体类设计完成。

算法

整个游戏最核心的无非就两点

  • 牌型 判断
  • 牌型 比较

首先我们先了解德州扑克的所有牌型
德州扑克牌型

注意在顺子中会多出一个最小的顺子A2345

设计思路

  • 其实想实现牌型的判断并不难,无非就是循环判断。但怎么写才能精简,直取牌型判断的关键。
  • 以下是笔者个人理解:
    • 德州的牌型分为两类
    1. 花色类
    2. 点数类

其中点数类,可以细分为

  1. 4Kind,1Kind (四条)
  2. 3Kind,2Kind (葫芦)
  3. 3Kind,1Kind,1Kind (三条)
  4. 2Kind,2Kind,1Kind (两对)
  5. 2Kind,1Kind,1Kind,1Kind (一对)
  6. 1Kind,1Kind,1Kind,1Kind,1Kind (高牌)

这六种情况,其中高牌包含了9+1种顺子的情况。
如此分析下来,如果仅对5张牌来说。
至少需要:

  • 统计:相同点数的牌的次数
    • 出现4次的
    • 出现3次的
    • 出现2次的
    • 出现1次的
  • 统计:相同花色的牌的次数
    • 四种花色分别出现了多少次
  • 排序:按照数号升序排序,如果数号相同就按照花色大小。(降序也可以,但一定要确定排序后是固定的)

//当然也完全可以给每种牌型单独设立一个判断函数,最后组合起来看什么类型。

方案一

获取到5张牌的这些信息后,基本就可以很快的判断出它是什么牌型了。
笔者这里展示自己的代码屎山 (这个版本在后面被舍弃了)

TypeType TYPE::GetType(const Cards&cards5){
    //暂存,排序
    Cards tmp=cards5;
    GROUP_CARDS::SortCardsNum(tmp);

    //统计相同点数的出现次数
    map<NumType,int> kind_map;
    //统计相同花色的出现次数
    map<ColourType,int> colour_map;
    for(auto &rc:tmp){
        ++kind_map[rc.GetNum()];
        ++colour_map[rc.GetColour()];
    }
    int arrkind[4]={0};
    bool colour5=false;
    for(auto &rc:kind_map){
        if(rc.second==1){++arrkind[0];}
        else if(rc.second==2){++arrkind[1];}
        else if(rc.second==3){++arrkind[2];}
        else if(rc.second==4){++arrkind[3];}
    }
    for(auto &rc:colour_map){
        if(rc.second==5){colour5=true;}
    }
    bool straight5=false;
    if(arrkind[0]==5){straight5=TYPE::IsStraight(tmp);}
    bool RoyalFlush5=false;
    if(colour5&&straight5){RoyalFlush5=TYPE::IsRoyalFlush(tmp);}

    TypeType typeTmp=0;
    if(RoyalFlush5){typeTmp=ROYAL_FLUSH;}
    else if(colour5&&straight5){typeTmp=STRAIGHT_FLUSH;}
    else if(arrkind[3]==1){typeTmp=FOUR_OF_A_KIND;}
    else if(arrkind[2]==1&&arrkind[1]==1){typeTmp=FULL_HOUSE;}
    else if(colour5){typeTmp=FLUSH;}
    else if(arrkind[0]==5&&straight5){typeTmp=STRAIGHT;}
    else if(arrkind[2]==1&&arrkind[0]==2){typeTmp=THREE_OF_A_KIND;}
    else if(arrkind[1]==2&&arrkind[0]==1){typeTmp=TWO_PAIRS;}
    else if(arrkind[1]==1&&arrkind[0]==3){typeTmp=ONE_PAIR;}
    else {typeTmp=HIGH_CARD;}

    return  typeTmp;
}
#endif

当然,既然是C++那自然牌型也得设计成一个类。

  • 这个类要有自己的卡牌容器,用于保存当前判断的牌型具体
  • 这个类会被作为玩家的属性,当进入翻牌圈之后,玩家就有自己的牌型了(公共牌+手牌)
  • 这个类需要实现自身的比较函数

因为笔者最后选择的将所有牌组合生成一个哈希表。所有这里不展示过去版本的代码。
可以参考该头文件Type.hpp
这个头文件是真正的屎山 ,笔者一开始打算用基于对象的方式,为每种类型设置比较函数。
然后,这个类会根据自身的类型值,自动选择绑定比较函数,实现相同牌型的自比较。
第一个版本就是基于这种算法实现的。可以说思路简单粗暴,而且对于德州的游戏进程来说,也称得上高效。
代码如下:

bool GREADER::KindCard(const Type & Left,const Type & Right){
    map<NumType,int,std::greater<NumType> > numLMap;//左操作数的卡牌相同点数出现次数统计图
    map<NumType,int,std::greater<NumType> > numRMap;//右操作数的卡牌相同点数出现次数统计图
    auto Cleft=Left.ReturnCards();
    auto Cright=Right.ReturnCards();
    for(int i=0;i<5;++i){
        ++numLMap[Cleft[i].GetNum()];
        ++numRMap[Cright[i].GetNum()];
    }
    int kind4[2]={0};//记录出现4次的卡牌点数
    int kind3[2]={0};//记录出现3次的卡牌点数
    int Lkind2[2]={0};//记录左操作数出现2次的卡牌点数
    int Rkind2[2]={0};//记录右操作数出现2次的卡牌点数
    int Lkind1[5]={0};//记录左操作数出现1次的卡牌点数
    int Rkind1[5]={0};//记录右操作数出现1次的卡牌点数
    int pos2l,pos2r,pos1l,pos1r;
    pos2l=pos2r=pos1l=pos1r=0;
    //统计
    for(auto &rc:numLMap){
        if(rc.second==4){kind4[0]=rc.first;}
        if(rc.second==3){kind3[0]=rc.first;}
        if(rc.second==2){Lkind2[pos2l++]=rc.first;}
        if(rc.second==1){Lkind1[pos1l++]=rc.first;}
    }
    for(auto &rc:numRMap){
        if(rc.second==4){kind4[1]=rc.first;}
        if(rc.second==3){kind3[1]=rc.first;}
        if(rc.second==2){Rkind2[pos2r++]=rc.first;}
        if(rc.second==1){Rkind1[pos1r++]=rc.first;}
    }
    if(kind4[0]!=0&&kind4[1]!=0){//四条的情况
        if(kind4[0]!=kind4[1]){
            return kind4[0]>kind4[1];
        }else{return Lkind1[0]>Rkind1[0];}
    }
   else if(kind3[0]!=0&&kind3[1]!=0){
        if(Lkind2[0]!=0&&Rkind2[0]!=0){//葫芦的情况
            if(kind3[0]!=kind3[1]){return kind3[0]>kind3[1];}
            else{return Lkind2[0]>Rkind2[0];}
        }else{//三条的情况
            if(kind3[0]!=kind3[1]){return  kind3[0]>kind3[1];}
            else{
                for(int i=0;i<3;++i){if(Lkind1[i]!=Rkind1[i]){return Lkind1[i]>Rkind1[i];}}
                return false;
            }
        }
    }
   else if(Lkind2[0]!=0&&Lkind2[1]!=0&&Rkind2[0]!=0&&Rkind2[1]!=0){//两对的情况
        for(int i=0;i<2;++i){
            if(Lkind2[i]!=Rkind2[i]){return Lkind2[i]>Rkind2[i];}
        }
        return Lkind1[0]>Rkind1[0];
    }
    else if(Lkind2[0]!=0&&Rkind2[0]!=0){//一对的情况
        if(Lkind2[0]!=Rkind2[0]){return Lkind2[0]>Rkind2[0];}
        for(int i=0;i<3;++i){
            if(Lkind1[i]!=Rkind1[i]){return Lkind1[i]>Rkind1[i];}
        }
        return false;
    }
    else{//高牌
        for(int i=0;i<5;++i){
            if(Lkind1[i]!=Rkind1[i]){return Lkind1[i]>Rkind1[i];}
        }
        return false;
    }
}

这个函数基本上把所有情况都考虑进去了,以至于笔者后面根本不需要去注册每种类型的比较函数,直接都用这个就好,毕竟相同点数的牌必须统计。逃不掉遍历卡牌。

只要能判断5张牌的类型,同时实现类型的自比较,那自然5+2张牌的问题就很好解决了。

  • 无非就是从7张牌中选取5张的组合。
  • 在遍历的时候,每次保存最大的那个,最后自然就是玩家的最大牌力。

从7张牌中选5张

Type TYPE::GainType(Cards cards){
    int Cnum=cards.size();
    if(Cnum<5){
        cerr<<"TYPE::GainType() Cnum<5 \n";
        exit(1);
    }
    Type Tret;
    if(Cnum==5){
        Tret.SetCards(cards);
        return Tret;
    }else{
        //从N张牌中选5张
        Cards Ctmp;
        Type Ttmp;
        const int Five=5;
        Ctmp.resize(Five);
        int Pick1,Pick2,Pick3,Pick4,Pick5;
        for(Pick1=0;Pick1<=Cnum-Five;++Pick1){
            for(Pick2=Pick1+1;Pick2<=Cnum-Five+1;++Pick2){
                for(Pick3=Pick2+1;Pick3<=Cnum-Five+2;++Pick3){
                    for(Pick4=Pick3+1;Pick4<=Cnum-Five+3;++Pick4){
                        for(Pick5=Pick4+1;Pick5<=Cnum-Five+4;++Pick5){
                            Ctmp[0]=cards[Pick1];
                            Ctmp[1]=cards[Pick2];
                            Ctmp[2]=cards[Pick3];
                            Ctmp[3]=cards[Pick4];
                            Ctmp[4]=cards[Pick5];
                            Ttmp.SetCards(Ctmp);
                            if(Ttmp>Tret){//这里就会体现比较函数
                                Tret.SetCards(Ctmp);
                            }
                        }
                    }
                }
            }
        }
        return Tret;
    }
}
方案二

还有更好的做法吗?
其实笔者迭代了几个版本后发现

  • 判断出牌型:
    • 其实也没必要从7张中选出5张,如果把所有属性抽出来,也可以判断出。
    • 但是这样需要保存每张牌的标记位,因为最后要获取最大的牌型具体。
    • 对于德州来说需要判断最多的情况也就是2+5,也就判断牌型了21次,比较了21次。

综合考虑下,对于一个玩家的一局游戏而已,每次5张牌的去判断,也只需要判断1+6+21=28次。
那不如从另一方面入手。

  • 比较不同的牌型:
    • 其实 每种,经过 确定的排序方式 的排序 后,它的牌力,战斗力是确定的。

那不如就直接生成一个所有5张组合-牌力的哈希表吧。

(这个数量还是蛮大的,从52张牌中选出5张,一共有:2598960 种情况)
因为笔者在设立 Card 的时候,它的数据成员就是一个 char
所有可以利用 unordered_map 生成一个哈希表。
主键为 经过排序后的 char[5], 值就是对应的牌力 short
虽然一个只有7字节,算下来7*2598960也有17MB的大小。

但是如果做出来了,判断牌型和牌型之间的比较就会变得很快。
因为判断牌型,只需要从 unordered_map 里去 find()
众所周知,哈希查找的时间复杂度为常数,最坏情况和容器大小成线性。

那就先从最大的开始吧(因为逻辑上好思考)
皇家同花顺

void Level::CreatMapRoyalFlush()
{
    int lv = 1;
    lv = 1;
    //对皇家同花顺
    //只有一种等级
    for (int i = 0; i < 4; ++i)
    {
        char ch4 = i * 13 + 12; // A
        char ch5 = i * 13 + 8;  // 10
        char ch6 = i * 13 + 9;  // J
        char ch7 = i * 13 + 10; // Q
        char ch8 = i * 13 + 11; // K
        string tmp({ch5, ch6, ch7, ch8, ch4});
        _cardsmap.insert({tmp, lv});
    }
}
  • 这里可能会有人问,为什么主键是 string 而不是 char[5]
  1. 因为一开始打算将主键设计的可读。
  2. 因为 char[5] 不是一个类型,不能作为 unordered_map 的参数
  3. 因为默认哈希算法用 string 更方便。

*诚然,虽然磁盘化的时候是17MB,但加载到内存上时,因为string是3个指针的大小,大了很多。
但,目前版本就开发到这里,暂不考虑内存上的空间优化(都是堆上管干嘛

接着是同花顺的代码

void Level::CreatMapStraightFlush()
{
    int lv = 1;
    //对同花顺;
    for (int i = 0; i < 4; ++i)
    {
        char colour = i * 13;
        //8种同花顺子
        //顶顺为皇家同花顺
        for (int j = 11; j >= 4; --j)
        {
            char numS = j;
            char card0 = colour + numS, card1 = card0 - 4,
                 card2 = card0 - 3, card3 = card0 - 2, card4 = card0 - 1;
            string tmp({card1, card2, card3, card4, card0});
            _cardsmap.insert({tmp, ++lv});
        }
        lv = 1;
    }
    lv = 1 + 8;
    for (int i = 0; i < 4; ++i)
    {
        char colour = i * 13;
        char card = colour + 0, card1 = colour + 1,
        	 card2 = colour + 2, card3 = colour + 3;
        char cardA = colour + 12;
        //最小的同花顺
        string tmp({card, card1, card2, card3, cardA});
        _cardsmap.insert({tmp, lv});
    }
}

同花顺没什么好说的,就是要注意有一个特例2345A的 同花顺

四条

void Level::CreatMapFourOfAKind()
{
    // lv=1+8+1;
    int lv = 10;
    //四条
    for (int i = 12; i >= 0; --i)
    {
        char nums = i, nums1 = nums + 13, nums2 = nums1 + 13, nums3 = nums2 + 13;
        string tmp;
        tmp.resize(4);
        tmp[0] = nums;
        tmp[1] = nums1;
        tmp[2] = nums2;
        tmp[3] = nums3;
        for (int j = 0; j < 4; ++j)
        {
            char colour = j;
            for (int k = 12; k >= 0; --k)
            {
                if (k == i){continue;}//避免取到四条的点数
                char card = colour * 13 + k;
                string use;
                if (i > k){use = card + tmp;}//保障点数的升序
                else{use = tmp + card;}
                _cardsmap.insert({use, ++lv});
            }
            lv -= 12;
        }
        lv += 12;
    }
}

对于四条而言

  • 先选出 4kind 的牌,再选出 1kind 的牌 。
  • 合计13*12=156种等级情况。
  • 别忘了加上花色,而且也得符合升序。

之后是葫芦

void Level::CreatMapFullHouse()
{
    // lv=1+8+1+12*13;
    int lv = 166;
    //葫芦
    char colourarr[4] = {0, 13, 26, 39};
    for (int i = 12; i >= 0; --i)
    {
        char num0 = i;
        for (int c1 = 0; c1 <= 4 - 3; ++c1)
        {
            for (int c2 = c1 + 1; c2 <= 4 - 3 + 1; ++c2)
            {
                for (int c3 = c2 + 1; c3 <= 4 - 3 + 2; ++c3)
                {
                    char num1 = num0 + colourarr[c1];
                    char num2 = num0 + colourarr[c2];
                    char num3 = num0 + colourarr[c3];
                    for (int s1 = 0; s1 <= 4 - 2; ++s1)
                    {
                        for (int s2 = s1 + 1; s2 <= 4 - 2 + 1; ++s2)
                        {
                            for (int k = 12; k >= 0; --k)
                            {
                                if (k == i)//避免点数相同
                                {
                                    continue;
                                }
                                char num4 = k + colourarr[s1];
                                char num5 = k + colourarr[s2];
                                if (k > i)//比较点数,确保键是从小到大(或者说固定的牌型方式)
                                {
                                    string use({num1, num2, num3, num4, num5});
                                    _cardsmap.insert({use, ++lv});
                                }
                                else
                                {
                                    string use({num4, num5, num1, num2, num3});
                                    _cardsmap.insert({use, ++lv});
                                }
                            }
                            lv -= 12;
                        }
                    }
                }
            }
        }
        lv += 12;
    }
}
  • 其实看到这里基本处理思路就基本大致相同了
  • 先挑选出牌型比较时,优先级高的部分,例如葫芦就是先3kind的那部分。
  • 然后去选出剩下的部分,注意已经选择的点数,是不能选的。
  • 因为花色是不会影响比较的,所有再插入 lv值 的时候,要去除花色的影响。
  • 最后要注意顺序的问题,对于葫芦来说,就两个点数,但是两个点数没有明显的大小关系。所有必须判断后调整位置。

之后是同花,这一个牌型可以说是高牌中的特例,能写出这个,那高牌无法就是跳过花色相同的情况。
同花代码:

void Level::CreatMapFlush()
{
    // lv=10+156+156;
    int lv = 322;
    //同花
    for (int co = 0; co < 4; ++co)
    {
        char colour = co * 13;
        int timetmp = 0;
        for (int n1 = 12; n1 >= 4; --n1)
        {
            for (int n2 = n1 - 1; n2 >= 3; --n2)
            {
                for (int n3 = n2 - 1; n3 >= 2; --n3)
                {
                    for (int n4 = n3 - 1; n4 >= 1; --n4)
                    {
                        for (int n5 = n4 - 1; n5 >= 0; --n5)
                        {
                            if (n5 + 1 == n4 && n4 + 1 == n3 && n3 + 1 == n2 && n2 + 1 == n1)//跳过顺子
                            {
                                continue;
                            }
                            if (n5 == 0 &&n5 + 1 ==n4 &&n4 + 1 == n3 &&n3 + 1 == n2 &&n1 == 12)//跳过顺子
                            {
                                continue;
                            }
                            char card0 = n5 + colour;
                            char card1 = n4 + colour;
                            char card2 = n3 + colour;
                            char card3 = n2 + colour;
                            char card4 = n1 + colour;
                            //隐含n1>n2>n3>n4>n5;
                            string tmp({card0, card1, card2, card3, card4});
                            // tmp需要排除顺子的可能
                            _cardsmap.insert({tmp, ++lv});
                            ++timetmp;
                        }
                    }
                }
            }
        }
        lv -= timetmp;
    }
}
  • 同花说白了就是,C(13,5) 的组合情况1287种情况,当然得减去10种顺子的情况,因为成顺就是同花顺了。

顺子

void Level::CreatMapStraight()
{
    // lv=322+1287-10;
    int lv = 1599;
    //顺子
    for (int num4 = 12; num4 >= 4; --num4)
    {
        ++lv;
        for (int colour4 = 0; colour4 < 4; ++colour4)
        {
            char card4 = num4 + colour4 * 13; //第五张牌
            for (int colour3 = 0; colour3 < 4; ++colour3)
            {
                char card3 = num4 - 1 + colour3 * 13; //第四张牌
                for (int colour2 = 0; colour2 < 4; ++colour2)
                {
                    char card2 = num4 - 2 + colour2 * 13; //第三张牌
                    for (int colour1 = 0; colour1 < 4; ++colour1)
                    {
                        char card1 = num4 - 3 + colour1 * 13; //第二张牌
                        for (int colour0 = 0; colour0 < 4; ++colour0)
                        {
                            if (colour0 == colour1 && colour1 == colour2 &&
                                colour2 == colour3 && colour3 == colour4)//避免同花
                            {
                                continue;
                            }
                            char card0 = num4 - 4 + colour0 * 13;
                            string tmp({card0, card1, card2, card3, card4});
                            _cardsmap.insert({tmp, lv});
                        }
                    }
                }
            }
        }
    }
    ++lv;
    for (int colour4 = 0; colour4 < 4; ++colour4)
    {
        char card4 = 12 + colour4 * 13; //第五张牌 A
        for (int colour3 = 0; colour3 < 4; ++colour3)
        {
            char card3 = 3 + colour3 * 13; //第四张牌 5
            for (int colour2 = 0; colour2 < 4; ++colour2)
            {
                char card2 = 2 + colour2 * 13; //第三张牌 4
                for (int colour1 = 0; colour1 < 4; ++colour1)
                {
                    char card1 = 1 + colour1 * 13; //第二张牌 3
                    for (int colour0 = 0; colour0 < 4; ++colour0)
                    {
                        if (colour0 == colour1 && colour1 == colour2 &&
                            colour2 == colour3 && colour3 == colour4)//避免同花
                        {
                            continue;
                        }
                        char card0 = 0 + colour0 * 13; //第一张牌 2
                        string tmp({card0, card1, card2, card3, card4});
                        _cardsmap.insert({tmp, lv});
                    }
                }
            }
        }
    }
}
  • 顺子和写同花顺相比,就是多了花色的选择,同时需要将同花的情况排除。
  • 需要注意,笔者这里取花色的时候从0开始的,正好满足点数相同时,花色也是升序的。(后面的牌型也是)所有不用特别对花色进行比较。
  • 反复强调,要保障这里最后给哈希表的键一定要是固定排序的。

之后的三条、两对、一对,思路和做法都相同,而对于高牌,无法就是排除同花和顺子的情况,所有就一并放在这里。

void Level::CreatMapThreeOfAKind()
{
    // lv=1599+10;
    int lv = 1609;
    //三条YX1X2
    for (int numY = 12; numY >= 0; --numY)
    { //确定三张点数一样的牌的点数
        for (int co1 = 0; co1 <= 4 - 3; ++co1)
        {
            for (int co2 = co1 + 1; co2 <= 4 - 3 + 1; ++co2)
            {
                for (int co3 = co2 + 1; co3 <= 4 - 3 + 2; ++co3)
                {
                    char card0 = numY + co1 * 13;
                    char card1 = numY + co2 * 13;
                    char card2 = numY + co3 * 13;
                    string tmpY({card0, card1, card2}); //一组三张点数一样的牌
                    //从剩余的12中选两个点数出来
                    for (int num1 = 12; num1 >= 1; --num1)
                    {
                        if (num1 == numY)
                        {
                            continue;
                        }
                        for (int num2 = num1 - 1; num2 >= 0; --num2)
                        {
                            if (num2 == numY){continue;}//避免点数相同
                            ++lv;
                            for (int colour1 = 0; colour1 < 4; ++colour1)
                            {
                                for (int colour2 = 0; colour2 < 4; ++colour2)
                                {
                                    char cardX1 = num1 + colour1 * 13;
                                    char cardX2 = num2 + colour2 * 13;
                                    string tmpX1({cardX1});
                                    string tmpX2({cardX2});
                                    string tmp;
                                    //隐含num2<num1;
                                    if (numY < num2)
                                    {
                                        tmp = tmpY + tmpX2 + tmpX1;
                                    }
                                    else if (numY < num1)
                                    {
                                        tmp = tmpX2 + tmpY + tmpX1;
                                    }
                                    else
                                    {
                                        tmp = tmpX2 + tmpX1 + tmpY;
                                    }
                                    _cardsmap.insert({tmp, lv});
                                }
                            }
                        }
                    }
                    lv -= 66;
                }
            }
        }
        lv += 66;
    }
}
void Level::CreatMapTwoPair()
{
    // lv=1609+66*13;
    int lv = 2467;
    //两对 从13种数号中选2种
    for (int numY1 = 12; numY1 >= 1; --numY1) //大的对子
    {
        for (int numY2 = numY1 - 1; numY2 >= 0; --numY2) //小的对子
        {
            for (int colour1 = 0; colour1 <= 4 - 2; ++colour1)
            {
                for (int colour2 = colour1 + 1; colour2 <= 4 - 2 + 1; ++colour2)
                {
                    for (int colour3 = 0; colour3 <= 4 - 2; ++colour3)
                    {
                        for (int colour4 = colour3 + 1; colour4 <= 4 - 2 + 1; ++colour4)
                        {
                            //确定两对每张牌的花色
                            char card0 = numY1 + colour1 * 13;
                            char card1 = numY1 + colour2 * 13;
                            //隐含colour2>colour1
                            string tmpY1({card0, card1});
                            char card2 = numY2 + colour3 * 13;
                            char card3 = numY2 + colour4 * 13;
                            string tmpY2({card2, card3});
                            for (int numX = 12; numX >= 0; --numX)
                            {
                                if (numX == numY1 || numX == numY2)
                                {
                                    continue;
                                }
                                ++lv;
                                for (int colour5 = 0; colour5 < 4; ++colour5)
                                {
                                    char card4 = numX + colour5 * 13;
                                    string tmpX({card4});
                                    //隐含numY1>numY2;
                                    string tmp;
                                    if (numX > numY1)
                                    {
                                        tmp = tmpY2 + tmpY1 + tmpX;
                                    }
                                    else if (numX > numY2)
                                    {
                                        tmp = tmpY2 + tmpX + tmpY1;
                                    }
                                    else
                                    {
                                        tmp = tmpX + tmpY2 + tmpY1;
                                    }
                                    _cardsmap.insert({tmp, lv});
                                }
                            }
                            lv -= 11;
                        }
                    }
                }
            }
            lv += 11;
        }
    }
}
void Level::CreatMapOnePair()
{
    // lv=2467+78*11;
    int lv = 3325;
    //一对
    for (int numY = 12; numY >= 0; --numY) //对子的数号
    {
        for (int colour1 = 0; colour1 <= 4 - 2; ++colour1)
        {
            for (int colour2 = colour1 + 1; colour2 <= 4 - 2 + 1; ++colour2)
            {
                //确定对子的花色组合
                char card0 = numY + colour1 * 13;
                char card1 = numY + colour2 * 13;
                string tmpY({card0, card1});
                //从剩余的12中选出3个点数出来
                for (int num1 = 12; num1 >= 2; --num1)
                {
                    for (int num2 = num1 - 1; num2 >= 1; --num2)
                    {
                        for (int num3 = num2 - 1; num3 >= 0; --num3)
                        {
                            if (num1 == numY || num2 == numY || num3 == numY)
                            {
                                continue;
                            }
                            ++lv;
                            for (int colour3 = 0; colour3 < 4; ++colour3)
                            {
                                for (int colour4 = 0; colour4 < 4; ++colour4)
                                {
                                    for (int colour5 = 0; colour5 < 4; ++colour5)
                                    {
                                        char card2 = num1 + colour3 * 13;
                                        string tmpX1({card2});
                                        char card3 = num2 + colour4 * 13;
                                        string tmpX2({card3});
                                        char card4 = num3 + colour5 * 13;
                                        string tmpX3({card4});
                                        string tmp;
                                        //隐含num3<num2<num1
                                        if (numY < num3)
                                        {
                                            tmp = tmpY + tmpX3 + tmpX2 + tmpX1;
                                        }
                                        else if (numY < num2)
                                        {
                                            tmp = tmpX3 + tmpY + tmpX2 + tmpX1;
                                        }
                                        else if (numY < num1)
                                        {
                                            tmp = tmpX3 + tmpX2 + tmpY + tmpX1;
                                        }
                                        else
                                        {
                                            tmp = tmpX3 + tmpX2 + tmpX1 + tmpY;
                                        }
                                        _cardsmap.insert({tmp, lv});
                                    }
                                }
                            }
                        }
                    }
                }
                lv -= 220;
            }
        }
        lv += 220;
    }
}
void Level::CreatMapHighCard()
{
    // lv=3325+220*13;
    int lv = 6185;
    //高牌
    int times = 0;
    for (int numX1 = 12; numX1 >= 4; --numX1)
    {
        for (int numX2 = numX1 - 1; numX2 >= 3; --numX2)
        {
            for (int numX3 = numX2 - 1; numX3 >= 2; --numX3)
            {
                for (int numX4 = numX3 - 1; numX4 >= 1; --numX4)
                {
                    for (int numX5 = numX4 - 1; numX5 >= 0; --numX5)
                    {
                        if (numX5 + 1 == numX4 && numX4 + 1 == numX3 &&
                            numX3 + 1 == numX2 && numX2 + 1 == numX1)
                        {
                            continue;
                        }
                        if (numX5 == 0 && numX5 + 1 == numX4 && numX4 + 1 == numX3 &&
                            numX3 + 1 == numX2 && numX1 == 12)
                        {
                            continue;
                        }
                        ++lv;
                        times = 0;
                        for (int colour1 = 0; colour1 < 4; ++colour1)
                        {
                            for (int colour2 = 0; colour2 < 4; ++colour2)
                            {
                                for (int colour3 = 0; colour3 < 4; ++colour3)
                                {
                                    for (int colour4 = 0; colour4 < 4; ++colour4)
                                    {
                                        for (int colour5 = 0; colour5 < 4; ++colour5)
                                        {
                                            if (colour1 == colour2 && colour2 == colour3 &&
                                                colour3 == colour4 && colour4 == colour5)
                                            {
                                                continue;
                                            }
                                            char card0 = numX1 + colour1 * 13;
                                            char card1 = numX2 + colour2 * 13;
                                            char card2 = numX3 + colour3 * 13;
                                            char card3 = numX4 + colour4 * 13;
                                            char card4 = numX5 + colour5 * 13;
                                            //隐含numX5<numX4<numX3<numX2<numX1
                                            string tmp({card4, card3, card2, card1, card0});
                                            _cardsmap.insert({tmp, lv});
                                        }
                                    }
                                }
                            }
                        }
                        //会跳过同花顺,同花,顺子
                    }
                }
            }
        }
    }
}
  • 行数有点超标,VS的自动对齐
  • 一套 屎山 代码下来,我们 C(52,5) 的所有情况就全部插入到哈希表里了。
  • 之后只需要用find()就可以获取到这种牌组合下,牌力是多少了。

显然每次打开游戏都取生成这个哈希表是很不合理的。
其次,7个字节乘以C(52,5)=2598960,也是13MB多的大小。这只是键值对所占的空间。
还记得吗,一开始笔者为了直接用C++自带的对string的哈希,把 unordered_map<key,val> 种的key定义的是string。
这样做确实省事了,但是有几个问题。

  1. string会占3个指针的大小,表里有2598960个元素,啧啧,直接用了45MB的堆空间。
  2. 虽然可以将键值对序列化(磁盘化),但是读出来也不可能直接读到 unordered_map 的哈希表里,只能一个一个插入。
    除非再去把每个哈希值保存下来,序列化,然后每次读取的时候直接加载到 unordered_map 的哈希表里。
  3. unordered_map 提供的string的哈希函数的边界很大很大,260万条数据太少了,浪费。


分析下来,目前的问题: C++提供的哈希表对于德州扑克来说,性能过剩了。而且不方便序列化存储和读取。

接下来就是有趣的部分了,如何去优化这个哈希表?
或者说,如何去优化这个哈希函数?
毕竟只要有哈希函数,我们可以自己实现哈希搜索。也可以很好的序列化(磁盘化),已经读取加载了。

优化哈希表

因为本篇文章真的写的太长了,所有具体哈希表的优化我会重开一篇。
文章链接如下:
优化哈希表的方法
时间记录
2022.11.22
尚未结束···

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

人生苦难处

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值