C++开发斗地主(QT)第一篇之数据结构

斗地主,是一种在中国流行的纸牌游戏。游戏最少由3个玩家进行,用一副54张牌(连鬼牌),其中一方为地主,其余两家为另一方,双方对战,先出完牌的一方获胜。如今已风靡整个中国,并流行于互联网上!

从今天开始,我将会一步步详细讲解单机斗地主游戏开发过程。该游戏以C++为主,QT做为界面(包括显示 动画 声音等)的Window平台,没有用到第三方面库如CoCo2d 等,至于为什么要用QT,主要是自己得心应手(QT动画 透明 图像处理太简单了),比VS简单,而且还能跨平台,本游戏在最后开源。网上的斗地主算法 源码很多,如果写的不好,欢迎批评 探讨。为了提起大家的兴趣,我给我做好的游戏功能和界面简单给大家看一下:

该游戏的大部分资源都是人网上下载的,如有侵权,请及时通知,我将会删除。本软件主要算法用的是 宽立斗地主AI设计与实现

我在此基础上用QT在Windows上进行了详细开发,主要是为了研究算法,我在此基础上对算法做了适当修改。

游戏主要功能:音效开关  机器人模式  记牌器窗口 退出 功能按键,还有出牌控制 音效 报警等,以后会更加完善,加一些设置,如声音大小  游戏难度 人物选取 等功能。

现在开始来讲游戏的开发过程:

一、数据结构,任何软件开发都离不开数据结构和数据处理。

1.牌型枚举(CardTypes):

//牌型枚举
enum CardTypes
{
    Error_Card,//错误出牌
    Single_Card,//单牌
    Double_Card,//对子
    Three_Card,//三张
    ThreeOne_Card,//三带一
    ThreeTwo_Card,//三代二
    Line_Card,//单顺
    Double_Line_Card,//连对
    Plane_Card,//飞机(两个三张连)
    Plane_TwoSingle_Card,//飞机带俩单
    Plane_TwoDouble_Card,//飞机带两对
    Four_TwoSingle_Card,//四带俩单
    Four_TwoDouble_Card,//四带两对
    Bomb_Card,//炸弹
    Rocket_Card//王炸(火箭)
};

 

我给他们定义了15种牌型:

⒈错误牌型:不能出的牌型

⒉单牌:例一张A

⒊对子:例AA

⒋三张:AAA

⒌三带一:AAAB

⒍三带二:AAABB

⒎单顺:也叫连子3,4,5,6,7,8,9...A一直到A为止

⒏连对:33,44,55,66

⒐飞机:333,444或444,555,666

⒑飞机带单:33344458

⒒飞机带双:3334447799

⒓四带单:88889J 四张可以带2单

⒔四带双:88883377 四张可以带2个对子

⒕炸弹:7777

⒖王炸:又称火箭 大小王一起出,是最大牌型

牌型的大小:

火箭是最大的牌。

炸弹,除火箭和比自己大的炸弹外,比其它牌型都大。

对一般牌型而言,只有当牌型相同和总张数相同的牌,才可比较大小。

其中像三带一、三带二、飞机带翅膀等组合牌型,只要比较其牌数最多牌值就行。只有比当前出的牌(场牌)大的牌才能出。

2.牌值枚举(CardValue):

//牌值枚举
enum CardValue{
    // 以下为牌的面值,从3开始
    kCard_ValueLeast        =   2,
    kCard_Value3            =   3,
    kCard_Value4            =   4,
    kCard_Value5            =   5,
    kCard_Value6            =   6,
    kCard_Value7            =   7,
    kCard_Value8            =   8,
    kCard_Value9            =   9,
    kCard_ValueT            =   10,     
    kCard_ValueJ            =   11,//J
    kCard_ValueQ            =   12,//Q
    kCard_ValueK            =   13,//K
    kCard_ValueA            =   14,//A
    kCard_Value2            =   15,//2
    kCard_ValueJoker1       =   16,//小王
    kCard_ValueJoker2       =   17,//大王
    kCard_ValueMax          =   18,

    kCard_TableMax          =   20,
    kCard_KindMax           =   5,

    // 特殊牌值
    kCard_Joker1            =   53,
    kCard_Joker2            =   54,
    kCard_Flower            =   55,

    kCardMask_CardValue     =   0x00ff,     // 牌的面值
    kCardMask_AnyMatch      =   0x0100,     // 任意配


    kMaxCardNum             =   56,
    kMaxPlayers             =   3,

    // 牌型定义
    kCardType_Single        =   1,   // 单纯类型, seriaNum == 1
    kCardType_Serial        =   2,   // 单顺, 双顺, 三顺(飞机), 4顺
    kCardType_Rocket        =   3,   // 火箭(大小王)
    CarcAngle               =   130   //牌的角度,另外两家的牌是倾斜的
};

 

我把牌分成了54种类,即牌面大小(3,4,5,6,7,8,9,10,J(11),Q(12),K(13),A(14),2(15))13种,和四种花色(梅花,方块,黑桃,红桃),再加上小王(16),大王(17)。13*4+2=54;

3.牌型结构体( CardNode):


//cardType是牌型,只有三种,王炸,单纯,连续;
//value 是牌型的值,单纯类型为牌的面值,连续类型为起始牌的面值,相同牌型以此比较大小;
//mainNum是主牌张数,比如三带二和飞机里mainNum=3, 连对时, mainNum=2;
//seralNum是连续张数,seralNum=1是单纯牌型,顺子时seralNum>=5;
//subNum是副牌数目,三带一和四带二时subNum=1,三带二和四带两对时,subNum=2;
//cards是牌型里包括的牌的牌值,比如三带一时,可能就是[3, 16, 42, 4], 连对时,可能就是 [3, 16, 4, 17, 5, 18, 6, 19]等等
//aggregate是权重,根据不同的情况求出权重,再按照权重排序所有牌型。可以是本牌型的权重,也可以是手牌里除了本牌型外剩下所有牌加在一起的权重。

struct CardNode {
    int32_t cardType : 4;
    int32_t mainNum  : 4;
    int32_t value    : 10;
    int32_t seralNum : 10;
    int32_t subNum   : 4;
    float aggregate;
    std::vector<int>  cards;
public:
    CardNode();
    CardNode(int type, int val, int mainN, int len, int sub);
    CardNode(const CardNode &other);
    CardNode & operator = (const CardNode &other);
    bool isValidNode() const;
    void resetNode();
    int getTopValue() const;
    int getMaxCapacity() const;
    void fillJokers() ;
    void merge(const CardNode & other);
    bool isRocket() const;
    bool isBomb() const;
    bool isExactLessThan(const CardNode & other) const;
    bool isStrictLessThan(const CardNode &other) const;
    float getPower() const;
    bool operator < (const CardNode & other) const;
    bool isEqualTo(const CardNode & other) const;
    std::string description() const ;
};

这里先对几个定义的数据变量时行解释一下,构造函数不用讲,其它函数后其用到的时候再讲

cardType是牌型,只有三种,王炸,单纯,连续;
value 是牌型的值(3-17),单纯类型为牌的面值,连续类型为起始牌的面值,相同牌型以此比较大小;
mainNum是主牌张数,比如三带二和飞机里mainNum=3, 连对时, mainNum=2;
seralNum是连续张数,seralNum=1是单纯牌型,顺子时seralNum>=5;
subNum是副牌数目,三带一和四带二时subNum=1,三带二和四带两对时,subNum=2;
cards是牌型里包括的牌的牌值(1-54),比如三带一时,可能就是[3, 16, 42, 4], 连对时,可能就是 [3, 16, 4, 17, 5, 18, 6, 19]等等
aggregate是权重,根据不同的情况求出权重,再按照权重排序所有牌型。可以是本牌型的权重,也可以是手牌里除了本牌型外剩下所有牌加在一起的权重。

这里要注意的是 value 的是牌的面值:3-17,cards里的值是(1-54);在调用的时候要用一个转换函数,获取牌的面值:

//获取牌的面值
int getCardValue(int card) {
    //55为花牌,本软件中没有用到
    if (v == kCard_Flower) {
        return kCard_ValueMax;
    }
    //为53即为小王
    if (v == kCard_Joker1) {
        return kCard_ValueJoker1;
    }
    //为54即为大王
    if (v == kCard_Joker2) {
        return kCard_ValueJoker2;
    }
    int t = v % 13;
    //小于3为 A  2
    if (t < 3) {
        t += 13;
    }
    return t;
}

 

4.手牌的结构体(OneHand):

//手牌结构体
struct OneHand {
    float   totalPower;//权重
    int     handNum;//值
    CardNode  bestNode;//牌型(里面是哪些牌组成)

public:
    OneHand():bestNode() {
        totalPower = kMinPowerValue;
        handNum = 0;
    }
};

 

5.游戏主控制类( CardGame):

//游戏主控件类
class CardGame
{
public:
    //当前位置0-自己 1-右边(下家) 2-左边(上家)
    int     curSeatId;
    ///一副牌
    vector<int> allCards;
    //过牌次数
    int passtimes;
    //叫地主的次数
    int  m_times;
    //自己是否机器出牌
    bool isRobotMode=false;
    //是否开启音效
    bool isSound=true;
    //谁是地主
    int     landlordId;
    //翻倍数 取决于叫的倍数和炸弹
    int     multiple;
    //当前状态 0-发牌 1-叫分 2-出牌
    int     state;
    //胜利id
    int winId;
    //底分
    int scroe=1000;
     //当前上家最大的牌,也就是自己要打他的牌
    CardNode currCardNode;
    //选手金币输赢统计,初始化100000    
    int handsScore[kMaxPlayers]={100000,100000,100000};
    //选手名字,完全是瞎写 哈哈
    string handName[3]={"Keepmoving","Lily","AngelaBaby"};
    int minScore=50;//最低底分
    int maxScore=5000;//最高底分
    //记录打的牌
    int playedCards[kCard_TableMax];
    //选手牌数组
    LordCards  * seatHands[kMaxPlayers];

    unordered_map<std::string, OneHand> * powerOfCards;

public:
    //初始化
    void    init();
    //获取一副打乱的扑克
    vector<int> getCards();
};

 

主游戏类中主要存储一些游戏的常规变量,对游戏进制操控,分析比如记录谁是地主 底分是多少 翻几倍 打牌人出了什么牌等....

6.本游戏中最重点的类( LordCards),也是最复杂 难理解的一个类 暂时不用想太多,后面慢慢分析。


//主牌类
class LordCards
{
public:
    static int getMinSerialLength(int mainNum);
    static int getMaxSubNum(int mainNum);
    static int getDupSubNum(int mainNum);

    static int getCardSuit(int card);
    static int getCardValue(int v);

    static bool updateHandForNode(OneHand & best, OneHand &left, CardNode & node, bool isTrim);

public:
    LordCards(class CardGame * game,int id, const std::vector<int>&vec);
    LordCards(class CardGame * game, int id,int cards[], int num);
    ~LordCards();

    LordCards & operator = (const LordCards & other);
    void assign(class CardGame * game, const std::vector<int>&vec);
    void assign(class CardGame * game, int cards[], int num);

public:
    float winRateIfLord();
    bool  bigEnough();

    std::vector<int> removeSubset(const std::vector<int> & subset);

    int scanToTable();

public:
    std::string getKey(bool checkFlower, int &leastValue, int &maxCount);

    bool containsFlower(int value, int num);
    bool collectNode(CardNode & one, int value, int num);

    OneHand    calcPowerByRemoveNode(const CardNode & node);

    void       checkRocket (const std::string & key, OneHand & hand);

    void       checkBomb4 (const std::string & key, OneHand & hand, int top);
    void       checkSerial (const std::string & key, OneHand & hand, int top, int mainNum, int len, int subNum);
    void       checkSub (const std::string & key, OneHand & hand, int mainNum, int subNum, int poss);

    OneHand    calcPowerValue_noFlower();
    OneHand    calcPowerValue_expandAny(int countAny, int cardIndex);
    OneHand    calcPowerValue(bool checkFlower=false);

    //打出牌
    void        playcards(CardNode cards);
    CardNode   typeAndValueFind();


public:
    void collectAllNodes(std::set<CardNode> &possNodes, CardNode & node, int dup);
    void sortByFactorInNodes(std::vector<CardNode> &allNodes, const CardNode & other, bool isDirect);

    void                 getGreaterNodes_expandAny(int countAny, int cardIndex, std::set<CardNode> &possNodes, const CardNode &other);
    void                 getGreaterNodes_possNode(std::set<CardNode> &possNodes, const CardNode &other);
    std::vector<CardNode>  getNodesGreaterThan(const CardNode & node);
    //选择最好的出牌
    CardNode   getBestCardNode(CardNode simple=CardNode());
    void  getGreaterNodes_simple(std::set<CardNode> &possNodes, const CardNode &other);
    int get_GroupData();
public:
    class CardGame * theGame;
    //位置ID
    int id;
    CardNode curretCardNode;
    std::vector<int> theCards;
    //叫地主的倍(1 2 3)
    int multiple=-1;
    //一共打了几手牌,用于算“春天”
    int playTimes=0;
    //权重
    int cardWeight;
    //排序
    void sort();

    std::vector<int> m_fillCards[kCard_TableMax];
    //kCard_KindMax表示牌的面值大小
    //kCard_KindMax
    //0数组表示每张牌的数量
    //1表示单张序列,顺子 值>4表示顺子
    //2表示对子 值>1表示边对
    //3三条 值为>1表示飞机
    //4炸弹
    int cardsTable[kCard_KindMax][kCard_TableMax];     // 保存每牌面值的数目,比如A的牌有几张
};

 

这个类里面的函数相当多,是一个比较复杂的类,他是一个 AI 计算,它能列出所有牌型,一把牌能分几次出,怎么出最合理,怎么打容易胜利 等等...

本节内容就简单讲到这,没有实际结合 ,都是抽象的东西,下节将讲解与QT相结合,和UI一起分析 讲解更容易理解。

  • 14
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
C++写的基于MFC界面的斗地主游戏源码,内含详细注释,附带了简单的AI出牌规则,放出来供大家参考交流。vs2010编写,vs2015测试可用,理论上vs05及以上都可正常编译运行。 void Judge::MainFlow() { switch(DataCenter::Instance().GetPlayState()) { case EM_LandHolderBorn_PlayState: { //先检查是否已经问完了 //遍历玩家检查是否已经询问过了,如果已经都问过了,则设置叫分最高的为地主 BOOL bAllAsked = TRUE;//是否已经询问完了 vector & vecPlayer = DataCenter::Instance().GetPlayerList(); for (UINT i = 0; i m_nCurHighstScore) { m_nCurHighstScore = vecPlayer[i].GetLandOwerScore(); m_pToBeLandOwer = &vecPlayer;[i]; } if (vecPlayer[i].GetLandOwerScore() SetLandOwer(TRUE); } //然后根据情况执行询问流程 //如果地主已经产生,则跳入下一阶段 if (NULL != DataCenter::Instance().GetLandOwner()) { m_pCurPlayer = NULL; DataCenter::Instance().SetPlayState(EM_WaitPlayer_PlayState); MainFlow(); return; } //如果当前player为空,设置当前player为地主牌得主 if (m_pCurPlayer == NULL) { m_pCurPlayer = DataCenter::Instance().GetLandOwnerCardHolder(); } //对当前玩家执行地主问询 ASSERT(m_pCurPlayer); m_pCurPlayer->ExcuteCallLandOwer(); } break; case EM_WaitPlayer_PlayState: { //如果游戏已经结束,则执行结束逻辑 BOOL bLandOwerWin = FALSE; if (DataCenter::Instance().IsOver(bLandOwerWin)) { if (bLandOwerWin) { AfxMessageBox(_T("地主赢了!")); } else { AfxMessageBox(_T("佃户赢了!")); } DataCenter::Instance().SetPlayState(EM_WaitToStart_PlayState); //将所有玩家明牌 DataCenter::Instance().ShowAllPlayerCard(); RefreshView(); return; } //如果是出牌阶段而当前player为空,设置当前player为地主,并发予底牌 if (m_pCurPlayer == NULL) { m_pCurPlayer = DataCenter::Instance().GetLandOwner(); DataCenter::Instance().SendOutBottomCard(); RefreshView(); } ASSERT(m_pCurPlayer); m_pCurPlayer->ExcuteCallCardPlay(); } break; } } void Judge::CurPlayerCallScore(int nScore) { if (m_pCurPlayer == NULL) { ASSERT(FALSE); return; } //将玩家选择的分数设置给玩家 m_pCurPlayer->SetLandOwerScore(nScore); //如果当前玩家为空,直接返回 if(m_pCurPlayer == NULL) { return; } if (nScore == 3) { //如果玩家叫了三分,直接设为地主 m_pCurPlayer->SetLandOwer(TRUE); } else { //玩家叫的不是三分,则记下玩家叫的分数 m_pCurPlayer->SetLandOwerScore(nScore); } if (nScore == 0) { CString strWord; strWord.Format(_T("不叫")); m_pCurPlayer->Say(strWord); } else { CString strWord; strWord.Format(_T("%d分"), nScore); m_pCurPlayer->Say(strWord); } //玩家叫分后隐藏叫地主按钮 Judge::Instance().ShowCallLandOwerBtn(FALSE); //切换到下一个玩家,流程继续 SwitchToNextPlayer(); MainFlow(); }
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值