1. 简介

这是一个基于Qt开发的单机版的斗地主小游戏。该项目一共涉及到的类有如下:

1.卡牌类

  • 单张卡牌:Card
  • 多张卡牌:Cards

2.玩家类

  • 玩家类(父类):Player

  • 机器人玩家(子类):Robot

  • 非机器人玩家(子类):UserPlayer

3.窗口类

  • 游戏开始加载动画窗口:Loading

  • 游戏主窗口:GamePanel

  • 单张卡牌窗口:CardPanel

  • 特效动画窗口:AnimationWindow

  • 游戏窗口中的按钮窗口:ButtonGroup

  • 游戏结束玩家的成绩窗口:EndingPanel

  • 自定义按钮:MyButton

  • 游戏分数面板窗口:ScorePanel

4.游戏策略类

  • 出牌类:PlayHand

  • 游戏策略类:Strategy

5.游戏控制类

  • 游戏控制类:GameControl

6.线程类

  • 机器人玩家抢地主:RobotGrapLord

  • 机器人玩家出牌:RobotPlayHand

7.音频类

  • 控制播放游戏中的所有音频:BGMControl

首先,创建一个项目,项目名为Landlords,再创建一个游戏主窗口类GamePanel,继承的基类是QMainWindow。该类也将作为斗地主小游戏的一个主窗口。

2. 单张卡牌类Card

单张卡牌类Card主要完成的是扑克牌中花色和点数的定义,以及一些操作符重载,以方便后序的开发中更加简便和高效。

单张卡牌类Card的创建:通过选择新建、c++、c++class、类名为Card,基类为Custom,意思是自定义,不给Card类提供基类。

2.1 Card类的头文件

该头文件主要就是定义了卡牌花色和点数的枚举类,并通过有参构造来定义一张扑克牌。在这里为了后期的开发简便,事先定义了两个card类的排序函数,后期可以通过这两个函数实现对玩家手牌的排序。由于卡牌是存储在QSet容器里面的(定义在Cards类里面的),Qt中规定该容器里面存储的元素数据必须是可以分配和可以指定的类型,如果要存储一个自定义对象类型,需要提供其比较操作符函数。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Card              //卡牌类:每张牌的一个类
{
public:
//花色枚举类(为了操作方便,给枚举类加上开始和结尾)
enum CardSuit{
Suit_Begin,
Diamond, //方块
Club, //梅花
Heart, //红桃
Spade, //黑桃
Suit_End
};
//点数枚举类
enum CardPoint{
Card_Begin, //对应整数0
Card_3,
Card_4,
Card_5,
Card_6,
Card_7,
Card_8,
Card_9,
Card_10,
Card_J,
Card_Q,
Card_K,
Card_A,
Card_2,
Card_SJ, //小王
Card_BJ, //大王
Card_End
};
Card();
Card(CardPoint point, CardSuit suit); //通过点数和花色创建一张扑克牌
//设置成员变量
void setPoint(CardPoint point);
void setSuit(CardSuit suit);
//返回成员变量
CardPoint point() const;
CardSuit suit() const;

private:
CardPoint m_point; //成员变量点数
CardSuit m_suit; //成员变量花色
};

//对象比较
bool lessSort(const Card& c1, const Card& c2); //升序调用
bool greaterSort(const Card& c1, const Card& c2); //降序调用
bool operator <(const Card& c1, const Card& c2); //操作符重载(<)

//如果使用QSet容器,里面的使用的数据必须是可以分配和可以指定的类型(常用的基础数据类型),如果是要存储一个对象的,不能处理的情况,就需要提供一个比较操作符的重载,并且重写
//一个qHash的全局函数,该函数作用是得到某个对象对应的哈希值,计算方式可以自己指定。
//操作符重载(==)
bool operator ==(const Card& left, const Card& right); //因为QSet容器存储自定义类型Card是有问题的,不能进行比较
//重写全局函数qHash
uint qHash(const Card& card);
//定义类型的别名
using CardList = QVector<Card>;

2.2 Card类函数实现

这部分就是对单张卡牌Card类声明的函数进行实现,即卡牌进行构造,设置卡牌点数、花色,获取卡牌点数、花色等。也完成了一些Card类运算符的重载实现。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Card::Card(){}
//提供卡牌点数和花色初始化一张牌
Card::Card(Card::CardPoint point, Card::CardSuit suit)
{
setPoint(point);
setSuit(suit);
}
//设置卡牌点数
void Card::setPoint(Card::CardPoint point)
{
m_point = point;
}
//设置卡牌花色
void Card::setSuit(Card::CardSuit suit)
{
m_suit = suit;
}
//获取卡牌的点数
Card::CardPoint Card::point() const
{
return m_point;
}
//获取卡牌的花色
Card::CardSuit Card::suit() const
{
return m_suit;
}
bool lessSort(const Card &c1, const Card &c2)
{
//const对象只能调用带const限定的函数,所以point和suit需要加const限定
if(c1.point() == c2.point()){ //点数相同,就比较花色(黑桃>红桃>梅花>方块)
return c1.suit() < c2.suit();
}else{
return c1.point() < c2.point(); //不同就比较点数
}
}
bool greaterSort(const Card &c1, const Card &c2)
{
if(c1.point() == c2.point()){ //点数相同,就比较花色(黑桃>红桃>梅花>方块)
return c1.suit() > c2.suit();
}else{
return c1.point() > c2.point(); //不同就比较点数
}
}
//运算符重载
bool operator ==(const Card& left, const Card& right){
return (left.point()==right.point()&&left.suit()==right.suit()); //花色和点数完成相等放回true,否则返回false
}
//哈希函数
uint qHash(const Card &card)
{
return card.point()*100+card.suit();
}
//运算符重载
bool operator <(const Card& c1, const Card& c2)
{
return lessSort(c1, c2);
}

3. 多张卡牌类Cards

多张卡牌类Cards主要是在Card类的基础上,将对卡牌的一些功能操作进行完善和增添。

多张卡牌类Cards创建:通过选择新建、c++、c++class、类名为Cards,基类为Custom,意思是自定义,不给Card类提供基类。

3.1 Cards类的头文件

该头文件主要定义了一个成员变量m_cards,它是存储多张卡牌的的一个容器对象,以及定义了一系列的成员函数,如对卡牌的添加、删除,和获得m_cards对象的属性,如几张卡牌,是否为空等。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Cards                  //多张卡牌类
{
public:
enum SortType{Asc,Desc,NoSort}; //枚举类,卡牌排序类型
Cards();
Cards(const Card& card); //带参的构造函数

//添加扑克牌
void add(const Card& card); //添加单张
void add(const Cards& cards); //添加多张
void add(const QVector<Cards>& cards); //添加一个容器的牌

//一次性插入多个数据(操作符重载<<)
Cards& operator <<(const Card& card);
Cards& operator <<(const Cards& cards);

//删除扑克牌
void remove(const Card& card);
void remove(const Cards& cards);
void remove(const QVector<Cards>&cards);

//扑克牌数量
int cardCount();
//是否为空
bool isEmpty();
//清空扑克牌
void clear();

//最大点数
Card::CardPoint maxPoint();
//最小点数
Card::CardPoint minPoint();
//指定点数的牌的数量
int pointCount(Card::CardPoint point);
//某张(些)牌是否在集合中(底层是通过是否存在子集来返回)
bool contains(const Card& card);
bool contains(const Cards& cards);

//随机取出一张扑克牌(发牌时会用)
Card takeRandomCard();

//将QSet容器转为Qvector容器,实现排序功能
CardList toCardList(SortType type = Desc); //CardList是在card.h里面定义的一种QVector<Card>的一个别名

private:
QSet<Card> m_cards; //容器里存的是单张卡牌类对象(不重复、无序的),不能进行排序,要排序就只能转QVector
};

3.2 Cards类函数实现

这部分就是对多张卡牌Cards类声明的函数进行实现。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
Cards::Cards(){}

Cards::Cards(const Card &card)
{
add(card);
}
//插入单张卡牌
void Cards::add(const Card &card)
{
m_cards.insert(card); //添加单张卡牌就直接往容器m_cards插入即可
}
//插入多张卡牌
void Cards::add(const Cards &cards)
{
m_cards.unite(cards.m_cards); //添加多张卡牌就通过并联添加到容器m_cards里
}
//添加多张卡牌
void Cards::add(const QVector<Cards> &cards)
{
for(int i=0; i<cards.count(); ++i){
add(cards.at(i));
}

}
//重写操作符
Cards &Cards::operator <<(const Card& card){
add(card);
return *this; //返回当前类对象的引用
}
Cards &Cards::operator <<(const Cards& cards){
add(cards);
return *this; //返回当前类对象的引用
}
//删除多张卡牌
void Cards::remove(const QVector<Cards> &cards)
{
for(int i=0; i<cards.size(); i++){
remove(cards.at(i));
}
}
//删除单张卡牌
void Cards::remove(const Card &card)
{
m_cards.remove(card); //从容器中移出卡牌card
}
//删除多张卡牌
void Cards::remove(const Cards &cards)
{
m_cards.subtract(cards.m_cards); //通过差集将容器m_cards移出多张卡牌
}
//获取卡牌数量
int Cards::cardCount()
{
return m_cards.size();
}
//判断手牌是否为空
bool Cards::isEmpty()
{
return m_cards.isEmpty();
}
//清除m_cards容器里的卡牌
void Cards::clear()
{
m_cards.clear();
}
//获取手牌中最大点数的卡牌
Card::CardPoint Cards::maxPoint()
{
Card::CardPoint max = Card::Card_Begin; //Card_Begin对应的枚举类值为0
if(!m_cards.isEmpty()){
for(auto it = m_cards.begin(); it!=m_cards.end(); it++){
if(it->point() > max){
max = it->point();
}
}
}
return max; //返回容器中最大点数的卡牌
}
//获取手牌中最小点数的卡牌
Card::CardPoint Cards::minPoint()
{
Card::CardPoint min = Card::Card_End;
if(!m_cards.isEmpty()){
for(auto it = m_cards.begin(); it!=m_cards.end(); it++){
if(it->point() < min){
min = it->point();
}
}
}
return min; //返回容器中最小点数的卡牌
}
//找到点数为point的卡牌
int Cards::pointCount(Card::CardPoint point)
{
int count = 0; //记录数量
for(auto it = m_cards.begin(); it!=m_cards.end(); it++){
if(it->point() == point){
count++; //如果等于指定点数,数量就+1
}
}
return count;
}

bool Cards::contains(const Card &card)
{
return m_cards.contains(card); //是否存在该张卡牌
}

bool Cards::contains(const Cards &cards)
{
return m_cards.contains(cards.m_cards); //是否存在该些卡牌
}
//随机获得一张卡牌,后面发牌操作会用到
Card Cards::takeRandomCard() //随机取出一张卡牌
{
//生成一个随机数
int num = QRandomGenerator::global()->bounded(m_cards.size()); //返回一个0到(卡牌数-1)范围的数字,即0到51
QSet<Card>::const_iterator it = m_cards.constBegin(); //const_iterator是只读迭代器
for(int i=0; i<num; i++,it++);
Card card = *it; //记录随机数对应的卡牌
m_cards.erase(it); //从容器中移出该卡牌
return card; //返回随机数对应的卡牌
}
//对手牌进行升序还是降序
CardList Cards::toCardList(Cards::SortType type)
{
CardList list;
for(auto it=m_cards.begin(); it!=m_cards.end(); it++){
list << *it; //每遍历一个元素,就将元素存入list
}
if(type == Asc){
std::sort(list.begin(),list.end(),lessSort); //升序
}else if(type == Desc){
std::sort(list.begin(),list.end(),greaterSort); //降序
}
return list;
}

4. 卡牌窗口类CardPanel

因为每张卡牌在主界面中都是以窗口的形式出现,所以该卡牌窗口类CardPanel相当于是对卡牌对象更充分的完善。

卡牌窗口类创建:通过选择新建、c++、c++class、类名为CardPanel,基类为QWidget。

4.1 CardPanel类头文件

该头文件加载了每张卡牌的的图片,其定义了一系列的成员变量和成员函数

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class CardPanel : public QWidget          //卡牌窗口类
{
Q_OBJECT
public:
explicit CardPanel(QWidget *parent = nullptr);

//设置获取图片函数
void setImage(const QPixmap &front, const QPixmap &back); //保存图片信息的,参数1是正面图片;参数2是背面图片
QPixmap getImage(); //获取正面图片信息(背面都是一样的)

//扑克牌显示哪一面
void setFrontSide(bool flag);
bool isFrontSide();

//记录窗口是否被选中
void setSeclected(bool flag);
bool isSelected();

//扑克牌的花色以及点数
void setCard(Card& card);
Card getCard();

//扑克牌的所有者
void setOwner(Player* player);
Player* getOwner();

//模拟扑克牌的点击事件
void clicked();

protected:
void paintEvent(QPaintEvent *event); //事件处理函数,更新窗口信息的
void mousePressEvent(QMouseEvent *event); //重写鼠标按下事件

signals:
void cardSelected(Qt::MouseButton button); //用户玩家的鼠标选择信号

private:
QPixmap m_front;
QPixmap m_back;
bool m_isfront=true; //是否是正面
bool m_isSelect=false; //是否被选中
Card m_card; //卡牌类对象
Player* m_owner=nullptr; //玩家类对象

};

4.2 CardPanel类函数实现

这部分就是卡牌窗口类CardPanel里面声明的函数进行实现。可以通过该类成员获得卡牌窗口的一些属性。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
CardPanel::CardPanel(QWidget *parent) : QWidget(parent){}
//加载卡牌窗口图片
void CardPanel::setImage(const QPixmap &front, const QPixmap &back)
{
m_front = front;
m_back = back;
setFixedSize(m_front.size()); //设置当前的窗口大小为图片大小
update(); //刷新窗口,调用的是paintEvent()函数
}
//获取卡牌窗口的正面
QPixmap CardPanel::getImage()
{
return m_front;
}
//设置是正面还是反面
void CardPanel::setFrontSide(bool flag)
{
m_isfront = flag;
}
bool CardPanel::isFrontSide()
{
return m_isfront;
}
//窗口是否被选中
void CardPanel::setSeclected(bool flag)
{
m_isSelect = flag;
}
bool CardPanel::isSelected()
{
return m_isSelect;
}
//扑克牌的花色以及点数
void CardPanel::setCard(Card &card)
{
m_card = card;
}
Card CardPanel::getCard()
{
return m_card;
}

void CardPanel::setOwner(Player *player)
{
m_owner = player;
}
//扑克牌的所有者
Player *CardPanel::getOwner()
{
return m_owner;
}
//模拟扑克牌的点击事件
void CardPanel::clicked()
{
emit cardSelected(Qt::LeftButton);
}
//窗口刷新函数
void CardPanel::paintEvent(QPaintEvent *event) //当setImage()函数把图片设置好后,就可以重新绘制窗口了,即调用该函数
{
Q_UNUSED(event); //处理event参数没有使用的警告
QPainter p(this); //定义一个画家类
if(m_isfront){
p.drawPixmap(rect(),m_front); //如果是正面,就画正面,大小和当前窗口一样大rect()
}else{
p.drawPixmap(rect(),m_back); //如果是背面,就画背面
}
}
//重新写鼠标划过函数
void CardPanel::mousePressEvent(QMouseEvent *event)
{
emit cardSelected(event->button()); //游戏的主窗口接收该信号(鼠标按键的选择)
}

5. 玩家类Player

该类是作为游戏里的三个玩家的基类,即两个机器人玩家类和用户玩家类。作为基类,它定义了一些共同的属性,提示为后序的开发提供了一些辅助函数,方便得到需要的属性内容。

玩家类创建:通过选择新建、c++、c++class、类名为Player,基类为QObject。

5.1 Player类头文件

该头文件不仅定义了许多关于玩家的成员变量和成员函数,还定义了四个虚函数,通过多态的方式来实现不同机器人玩家的这个处理逻辑。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class Player : public QObject               //玩家类
{
Q_OBJECT
public:
enum Role{Lord, Farmer}; //角色
enum Sex{Man, Woman}; //性别
enum Direction{Left, Right}; //头像的显示方位
enum Type{Robot, User, UnKnow}; //玩家的类型
explicit Player(QObject *parent = nullptr);
explicit Player(QString name, QObject *parent = nullptr);

//名字
void setName(QString name);
QString getName();

//角色
void setRole(Role role);
Role getRole();

//性别
void setSex(Sex sex);
Sex getSex();

//头像方位
void setDirection(Direction direction);
Direction getDirection();

//玩家类型
void setType(Type type);
Type getType();

//玩家的分数
void setScore(int score);
int getScore();

//游戏结果
void setWin(bool flag);
bool isWin();

//提供当前对象的上家/下家对象
void setPrevPlayer(Player* player);
void setNextPlayer(Player* player);
Player* getPrevPlayer();
Player* getNextPlayer();

//叫地主、抢地主(传出一个信号)
void grabLordBet(int point); //传入的是分数(1,2,3,0)

//存储扑克牌(发牌的时候得到的)
void storeDispatchCard(Card& card);
void storeDispatchCard(Cards& cards); //抢地主得到的牌

//得到所有的牌
Cards getCards();
//清空玩家手中所有的牌
void clearCards();
//出牌
void playHand(Cards& cards); //出牌可以出一张也可以出多张

// 获取待出牌玩家对象以及这个玩家打出的牌(比如说当前玩家出牌后,要记录出牌的玩家和出的牌,下一个出牌玩家会用到)
Player* getPendPlayer();
Cards getPendCards();

//存储出牌玩家对象和打出的牌
void storePendingInfo(Player* player, const Cards& cards);

//虚函数 通过多态来实现机器人玩家A和机器人玩家B(有相同的同名函数,但处理逻辑不一样,所以用多态实现)
//下面4个虚函数在play只需要定义即可,功能实现交给子类,即机器人玩家和用户玩家
virtual void prepareCallLord(); //准备叫地主(启动 考虑叫地主 子线程来完成)
virtual void preparePlayHand(); //准备出牌(启动 考虑出牌 子线程来完成)
virtual void thinkCallLord(); //考虑叫地主(计算权重)--->机器人玩家类Robot会重写该函数
virtual void thinkPlayHand(); //考虑出牌--->计算机玩家类Robot会重写该函数

signals:
//通知已经叫地主下注
void notifyGrabLordBet(Player* player, int bet); //参数:叫地主的玩家,下的分数
//通知已经出牌(playHand()出牌函数会使用,当玩家出完牌后,需要发送的信号)
void notifyPlayHand(Player* player, Cards& card); //参数:出牌玩家,出的牌
//向主窗口通知已经得到了卡牌(两种情况,普通得牌和得3张底牌)
void notifyPickCards(Player* player, Cards& cards);

protected: //子类也需要访问这些成员属性遍历,所以得是protected类型
QString m_name; //玩家姓名
Role m_role; //玩家角色(枚举类),地主还是农民
Sex m_sex; //玩家性别(枚举类),角色性别
Direction m_direction; //玩家的头像的显示方位(枚举类),左或右
Type m_type; //玩家的类型(枚举类),机器人、用户或未知
int m_score; //玩家的分数
bool m_isWin; //玩家是否获胜

Player* m_prev; //上家
Player* m_next; //下家

Cards m_cards; //存储多张扑克牌(玩家手中的牌)

Cards m_pendCards; //打出的扑克牌
Player* m_pendPlayer = nullptr; //打出扑克牌的玩家(初始化先置为空)
};

5.2 Player类函数实现

该玩家类Player实现了两种类的构造函数方法,同时实现了设置和获取玩家的相关属性。而虚函数在这里只需要定义出来即可,不需要实现,留给子类实现。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
Player::Player(QObject *parent) : QObject(parent)        //玩家类的构造函数
{
m_score = 0; //分数初始化为0
m_isWin = false; //初始化是否赢
m_pendPlayer = nullptr; //上次出牌玩家开始指向空
}
//当用的是这个构造函数时,也会初始化父类的构造函数
Player::Player(QString name, QObject *parent) : Player(parent)
{
m_name = name;
}

//设置和获取玩家的名字
void Player::setName(QString name)
{
m_name = name;
}
QString Player::getName()
{
return m_name;
}

//设置和获取玩家的角色
void Player::setRole(Role role)
{
m_role = role;
}
Player::Role Player::getRole()
{
return m_role;
}

//设置和获取玩家性别
void Player::setSex(Player::Sex sex)
{
m_sex = sex;
}
Player::Sex Player::getSex()
{
return m_sex;
}

//设置和获取玩家的头像方位
void Player::setDirection(Player::Direction direction)
{
m_direction = direction;
}
Player::Direction Player::getDirection()
{
return m_direction;
}

//设置和获取玩家的类型(机器人玩家还是用户玩家)
void Player::setType(Player::Type type)
{
m_type = type;
}
Player::Type Player::getType()
{
return m_type;
}

//设置和获取玩家的分数
void Player::setScore(int score)
{
m_score = score;
}
int Player::getScore()
{
return m_score;
}

//设置玩家身份获胜
void Player::setWin(bool flag)
{
m_isWin = flag;
}
bool Player::isWin()
{
return m_isWin;
}

//设置玩家的上一个位置玩家
void Player::setPrevPlayer(Player *player)
{
m_prev = player;
}
//设置玩家的下一个位置玩家
void Player::setNextPlayer(Player *player)
{
m_next = player;
}
//返回玩家的上一个位置玩家
Player *Player::getPrevPlayer()
{
return m_prev;
}
//返回玩家的下一个位置玩家
Player *Player::getNextPlayer()
{
return m_next;
}

void Player::grabLordBet(int point) //抢地主函数
{
emit notifyGrabLordBet(this, point); //发出玩家叫地主的信号,游戏控制类接收该信号,参数:抢地主玩家,下的分数
}

//存入单张扑克牌
void Player::storeDispatchCard(Card &card)
{
m_cards.add(card);
Cards cs;
cs.add(card);
emit notifyPickCards(this, cs); //向主窗口通知得到牌了(发牌阶段)
}
//存入多张扑克牌
void Player::storeDispatchCard(Cards &cards)
{
m_cards.add(cards); //添加底牌
emit notifyPickCards(this,cards); //向主窗口通知得到3张底牌了(在游戏控制类中becomelords()中,添加底牌会调用该函数)
}

//获取所有牌
Cards Player::getCards()
{
return m_cards;
}
//清空牌
void Player::clearCards()
{
m_cards.clear();
}

//出牌
void Player::playHand(Cards &cards)
{
m_cards.remove(cards); //从玩家手中的牌移出要出的牌
//发出信号,因为该信号可能是机器人玩家发出,也可能是用户玩家发出,所以就通过基类指针this指向子类对象
emit notifyPlayHand(this, cards); //该信号由游戏控制类接收处理
}

//获得上一次的出牌玩家
Player *Player::getPendPlayer()
{
return m_pendPlayer;
}
//获得上一次出的牌
Cards Player::getPendCards()
{
return m_pendCards;
}
//记录上一次的出牌玩家和出的牌
void Player::storePendingInfo(Player *player, const Cards &cards)
{
m_pendPlayer = player;
m_pendCards = cards;
}

//只定义出四个虚函数即可,留给子类实现
void Player::prepareCallLord(){}
void Player::preparePlayHand(){}
void Player::thinkCallLord(){}
void Player::thinkPlayHand(){}

6. 机器人玩家类Robot

机器人玩家类Robot的基类是玩家类Player,它主要任务就是实现从父类继承下来的虚函数。

机器人玩家类创建:通过选择新建、c++、c++class、类名为Robot,基类设为Custom,选择Player,勾选Include QObject和Add QOBJECT。创建好后,将基类改为Player类。

6.1 Robot类头文件

机器人玩家类Robot只需要实现从父类继承来的虚函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Robot : public Player          //机器人玩家子类
{
Q_OBJECT
public:
//使用继承构造函数(就不需要自己重新写了)
using Player::Player; //using是告诉当前的类,可以使用基类Player里面的所有的构造函数
explicit Robot(QObject *parent = nullptr);

void prepareCallLord() override; //重写父类函数,创建子线程类对象,启动
void preparePlayHand() override; //重写父类函数,创建子线程类对象,启动

void thinkCallLord() override; //考虑叫地主
void thinkPlayHand() override; //考虑出牌
};

6.2 Robot类函数实现

这部分主要就是实现了准备叫地主、考虑叫地主、准备出牌和考虑出牌。

当是机器人玩家时,它会将从父类Player继承下来的虚函数进行重写。首先是在准备叫地主函数prepareCallLord()中,它会创建一个叫地主的子线程类RobotGrapLord(在后面),然后执行strat(),它会启动子线程类里面的run()函数,而run()函数是睡眠了2s后,调用考虑叫地主函数thinkCallLord()。机器人玩家通过计算手牌的权重来得出是否叫地主的决定。对于准备和考虑出牌过程和这个一模一样。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Robot::Robot(QObject *parent):Player(parent)
{
m_type = Player::Robot; //确定玩家类型
}
//准备叫地主函数
void Robot::prepareCallLord()
{
RobotGrapLord* subThread = new RobotGrapLord(this); //创建一个考虑叫地主的子线程类
//当子线程里面内容都执行完了,会发出finished信号过后,就调用deleteLater()方法,释放掉该线程占用的内存
connect(subThread, &RobotGrapLord::finished, this, [=](){
qDebug() <<"RobotGrapLord 子线程对象析构....." << ",Robot name: " << this->getName();
subThread->deleteLater();
});
subThread->start(); //启动考虑叫地主子线程类的run,run函数里面会调用thinkCallLord()来计算权重
}

//准备出牌函数
void Robot::preparePlayHand()
{
RobotPlayHand* subThread = new RobotPlayHand(this); //创建一个考虑出牌的子线程类
connect(subThread, &RobotGrapLord::finished, this, [=](){
qDebug() <<"RobotPlayHand 子线程对象析构....." << ",Robot name: " << this->getName();
subThread->deleteLater();
});
subThread->start(); //启动run函数
}

//考虑叫地主
void Robot::thinkCallLord()
{
/* 机器人玩家基于手中的牌计算权重,根据权重来考虑是否叫地主
大小王:6
顺子/炸弹:5
三张点数相同的牌:4
2的权重:3
对牌:1
*/
int weight = 0;
Strategy st(this, m_cards);
weight += st.getRangeCards(Card::Card_SJ,Card::Card_BJ).cardCount()*6; //得到手牌中大小王的权重

QVector<Cards>optSeq = st.pickOptimalSeqSingles(); //pickOptimalSeqSingles函数内部还是再找顺子之前,已经剔除了炸弹、3带1和飞机类型牌
weight += optSeq.size()*5; //得到手牌中的顺子的权重

QVector<Cards>bombs = st.findCardsByCount(4);
weight += bombs.size()*5; //得到手牌中炸弹的权重

weight += m_cards.pointCount(Card::Card_2) * 3; //得到2的权重

Cards tmp = m_cards; //防止计算权重的牌重复,先复制手牌
tmp.remove(optSeq); //剔除顺子
tmp.remove(bombs); //剔除炸弹
Cards card2 = st.getRangeCards(Card::Card_2,Card::Card_2); //先取出点数为2的牌
tmp.remove(card2); //剔除2
QVector<Cards>triples = Strategy(this,tmp).findCardsByCount(3); //在剔除相应牌型后,再继续找三张相同的牌
weight += triples.size()*4; //得到三张点数相同的牌的权重

tmp.remove(triples); //剔除三张相同的牌
QVector<Cards>pairs = Strategy(this,tmp).findCardsByCount(2); //在tmp里找对牌
weight += pairs.size()*1; //得到对牌的权重

if(weight >= 22){
grabLordBet(3);
}else if(weight<22 && weight>=18){
grabLordBet(2);
}else if(weight<18 && weight>=10){
grabLordBet(1);
}else{
grabLordBet(0);
}
}

//考虑出牌
void Robot::thinkPlayHand()
{
Strategy st(this, m_cards); //先构造一个Strategy对象,这样就可以使用Strategy类里面的函数了
Cards cs = st.makeStrategy(); //调用出牌策略函数,可以直接得到机器人玩家要出的牌
qDebug() << "打出的牌数量:" <<cs.cardCount();
playHand(cs); //出牌,即从手牌中移除要出的牌cs
}

7. 用户玩家类UserPlayer

用户玩家类UserPlayer只需要实现从基类Player继承下来的虚函数准备叫地主和准备出牌,且都不需要写太多程序操作,因为这些过程都是用户通过鼠标点击来完成的。

非机器人玩家类创建:通过选择新建、c++、c++class、类名为UserPlayer,基类设为Custom,选择Player,勾选Include QObject和Add QOBJECT。创建好后,将其基类改为Player。

7.1 UserPlayer类头文件

该类只定义从基类继承下来的虚函数准备叫地主和准备出牌。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UserPlayer : public Player              //非机器人(用户)玩家子类
{
Q_OBJECT
public:
//使用继承构造函数(就不需要自己重新写了)
using Player::Player; //using是告诉当前的类,可以使用基类Player里面的所有的构造函数
explicit UserPlayer(QObject *parent = nullptr);

void prepareCallLord() override; //用户玩家的这个类是空的,机器人玩家才会使用该函数
void preparePlayHand() override; //用户玩家准备出牌

signals:
void startCountDown();
};

7.2 UserPlayer类函数实现

该部分实现的两个虚函数都不需要做太多操作,因为是用户玩家,这些过程都是通过鼠标完成。但在准备出牌函数中,发出了一个信号startCountDown,表示从轮到用户玩家出牌开始就通知主窗口计时,当秒数从15变为0时,就默认用户玩家放弃出牌。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UserPlayer::UserPlayer(QObject *parent):Player(parent)
{
m_type = Player::User; //确定玩家类型(用户)
}
//准备叫地主函数
void UserPlayer::prepareCallLord() //用户玩家的这个类是空的,机器人玩家才会使用该函数
{
}
//准备出牌函数
void UserPlayer::preparePlayHand() //用户玩家准备出牌
{
emit startCountDown(); //发射一个信号,告知主窗口是用户玩家准备出牌,要开始倒计时
}

8. 叫地主线程类RobotGrapLord

叫地主线程类RobotGrapLord是专为机器人玩家设计的类,就是负责模拟叫地主这一过程。

机器人玩家叫地主线程类创建:新建、c、c++、类名为RobotGrapLord,基类为QObject,创建好后修改基类为QThread。

8.1 RobotGrapLord类头文件

叫地主线程类RobotGrapLord就定义了一个从基类继承下来的run()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RobotGrapLord : public QThread
{
Q_OBJECT
public:
explicit RobotGrapLord(Player* player, QObject *parent = nullptr);

protected:
//重写QThread类里面的run方法
void run(); //执行机器人玩家叫地主

signals:

private:
Player* m_player;
};

8.2 RobotGrapLor类函数实现

该子线程就睡眠了2s,模拟机器人玩家考虑的这个过程,然后调用机器人玩家类的考虑叫地主函数。

1
2
3
4
5
6
7
8
9
10
RobotGrapLord::RobotGrapLord(Player* player, QObject *parent) : QThread(parent)
{
m_player= player;
}
//可以通过start()来调用run()函数实现
void RobotGrapLord::run()
{
msleep(2000); //睡2秒,模拟机器人玩家一个思考的过程
m_player->thinkCallLord(); //直接调用Robot类写的考虑叫地主函数
}

9. 出牌线程类RobotPlayHand

出牌线程类RobotPlayHand是专为机器人玩家设计的类,就是负责模拟出牌这一过程。

机器人玩家出牌线程类创建:新建、c、c++、类名为RobotPlayHand,基类为QObject,创建好后修改基类为QThread。

9.1 RobotPlayHand类头文件

出牌线程类RobotPlayHand就定义了一个从基类继承下来的run()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//机器人玩家出牌子线程类
class RobotPlayHand : public QThread
{
Q_OBJECT
public:
explicit RobotPlayHand(Player* player, QObject *parent = nullptr);

signals:

protected:
void run() override; //重写
private:
Player* m_player;
};

9.2 RobotPlayHand类函数实现

该子线程就睡眠了2s,模拟机器人玩家考虑的这个过程,然后调用机器人玩家类的考虑出牌函数。

1
2
3
4
5
6
7
8
9
RobotPlayHand::RobotPlayHand(Player* player, QObject *parent) : QThread(parent)
{
m_player = player;
}

void RobotPlayHand::run(){
msleep(2000);
m_player->thinkPlayHand(); //调用考虑出牌函数
}

10. 玩家分数窗口类ScorePanel

分数窗口类ScorePanel就是负责显示各个玩家的分数,在该小项目中,有两处会使用该分数窗口。一个是主窗口的左上角会显示各个玩家的分数;还有一个是一局游戏结束后,在结束面板上显示各个玩家的分数。

10.1 制作分数面板窗口

创建子窗口:

1.游戏分数面板子窗口:通过选择新建、Qt、Qt设计师界面类、选择Widget类型的窗口(可以内嵌的,以没有边框的形式完美的附着在父窗口上面),类名为ScorePanel

2.将分数窗口添加到主窗口(右上方)

在主窗口拖入一个Widget子窗口,因为它是一个Widget类型,而不是分数面板的类型,所以就需要进行提升,即就是把父类变为子类。基于这个理论,可以发现分数面板类ScorePanel的基类是Widget类型,所以就可以将该Widget类提升为ScorePanel类型。这样就将Qt中的标准控件变为了自定义控件(把一个基类变成了子类类型)。

10.2 ScorePanel类头文件

分数窗口类ScorePanel就定义了一些设置属性的函数,如字体的大小和颜色,这样后期要使用分数窗口的时候,也可以根据环境来设置适合的字体和颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace Ui {
class ScorePanel;
}

class ScorePanel : public QWidget
{
Q_OBJECT

public:
enum FontColor{Black, White,Red,Blue,Green};
explicit ScorePanel(QWidget *parent = nullptr);
~ScorePanel();

//设置玩家的得分(将得分显示在窗口上)
void setScores(int left, int right, int user); //参数是三个玩家的得分
//设置字体大小
void setMyFontSize(int point);
//设置字体的颜色
void setMyFontColor(FontColor color);

private:
Ui::ScorePanel *ui;
QVector<QLabel*> m_list;
};

10.3 ScorePanel类函数实现

该部分就是实现了头文件定义的成员函数。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const QString MYCOLOR[] = {"black", "white", "red", "blue", "green"};
ScorePanel::ScorePanel(QWidget *parent) :
QWidget(parent),
ui(new Ui::ScorePanel)
{
ui->setupUi(this);
//将拖到ui上的标签都存入到m_list容器中
m_list << ui->meScore << ui->leftScore << ui->rightScore
<< ui->meTitle << ui->leftTitle << ui->rightTitle
<< ui->score1 << ui->score2 << ui->score3;
}

ScorePanel::~ScorePanel()
{
delete ui;
}
//设置3个玩家的分数
void ScorePanel::setScores(int left, int right, int user)
{
//需要将整形数据转换为字符串类型数据
ui->leftScore->setText(QString::number(left));
ui->rightScore->setText(QString::number(right));
ui->meScore->setText(QString::number(user));
}
//设置字体
void ScorePanel::setMyFontSize(int point)
{
QFont font("微软雅黑", point, QFont::Bold); //定义一个字体对象
for(int i=0; i<m_list.size(); i++){ //遍历分数面板的所有按钮,都设置为该字体
m_list[i]->setFont(font);
}
}
//设置颜色
void ScorePanel::setMyFontColor(ScorePanel::FontColor color)
{
QString style = QString("QLabel{color:%1}").arg(MYCOLOR[color]);
for(int i=0; i<m_list.size(); i++){ //遍历分数面板的所有按钮,都设置为该颜色
m_list[i]->setStyleSheet(style);
}
}

11. 自定义按钮类MyButton

自定义按钮类MyButton主要就是对按钮进行美化,当鼠标经过按钮、按下按钮都加载显示不同的图片,起到一个有点击的效果。

11.1 自定义按钮类

创建自定义按钮类:

通过选择新建、c++、c++class、类名为MyButton,基类设为QWidget。但创建好后,就将MyButton的基类QWidget修改为QPushButton。

不用带ui,该类只对按钮做美化,按钮上要放什么东西,不在考虑内。从QpushButton派生,然后基于QpushButton在按钮上做美化。

11.2 MyButton类头文件

自定义按钮类MyButton主要就是将从基类继承下来的函数进行定义,如鼠标按下、鼠标释放、鼠标进入和鼠标离开。

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
26
27
28
29
30
class MyButton : public QPushButton
{
Q_OBJECT
public:
explicit MyButton(QWidget *parent = nullptr);

void setImage(QString normal, QString hover, QString pressed);

protected:
//鼠标按下
void mousePressEvent(QMouseEvent* ev);
//鼠标释放
void mouseReleaseEvent(QMouseEvent* ev);
//鼠标进入
void enterEvent(QEvent* ev);
//鼠标离开
void leaveEvent(QEvent* ev);
//绘图
void paintEvent(QPaintEvent* ev);

signals:

private:
//成员变量,对应三张图片的路径
QString m_normal; //定义的是正常情况下的图片路径
QString m_hover; //定义的是鼠标滑过按钮的图片路径
QString m_pressed; //定义的是鼠标按下按钮的图片路径

QPixmap m_pixmap; //得到的路径需要转为QPixmap对象
};

11.3 MyButton类函数实现

这部分就是对自定义按钮类MyButton的头文件定义的函数进行实现,不同情况,加载不同图片,也显示不同图片。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
MyButton::MyButton(QWidget *parent) : QPushButton(parent){}

void MyButton::setImage(QString normal, QString hover, QString pressed)
{
m_normal = normal;
m_hover = hover;
m_pressed = pressed;
m_pixmap.load(m_normal); //默认情况下,就加载m_normal图片
}

void MyButton::mousePressEvent(QMouseEvent *ev) //鼠标按下
{
if(ev->button() == Qt::LeftButton){ //如果鼠标按下的是左键
m_pixmap.load(m_pressed); //加载m_pressed图片
update(); //鼠标事件产生之后,强制重绘
}
//因为重写了父类的虚函数,而又需要使用到父类该虚函数定义的一些功能,所以就等执行完重写的后,再执行父类的该虚函数
QPushButton::mousePressEvent(ev);
}

void MyButton::mouseReleaseEvent(QMouseEvent *ev) //鼠标释放
{
if(ev->button() == Qt::LeftButton){ //如果鼠标释放的是左键
m_pixmap.load(m_normal); //加载m_normal图片
update(); //鼠标事件产生之后,强制重绘
}
//因为重写了父类的虚函数,而又需要使用到父类该虚函数定义的一些功能,所以就等执行完重写的后,再执行父类的该虚函数
QPushButton::mouseReleaseEvent(ev);
}

void MyButton::enterEvent(QEvent *ev) //鼠标进入
{
Q_UNUSED(ev); //处理ev参数没有使用的警告
m_pixmap.load(m_hover); //鼠标经过按钮,加载hover图片
update(); //鼠标事件产生之后,强制重绘
}

void MyButton::leaveEvent(QEvent *ev) //鼠标离开
{
Q_UNUSED(ev); //处理ev参数没有使用的警告
m_pixmap.load(m_normal); //鼠标经过按钮,加载normal图片
update(); //鼠标事件产生之后,强制重绘
}

void MyButton::paintEvent(QPaintEvent *ev) //绘图,当窗口刷新的时候被调用重绘,而产生鼠标事件是不会调用该对象的,所以需要手动强制重绘update()
{
Q_UNUSED(ev); //处理ev参数没有使用的警告
//对应的事件产生之后,就需要将相关的图片挂到当前的按钮对象上
QPainter p(this); //创建一个画家类,参数是指定绘图设备(当前按钮对象)
p.drawPixmap(rect(), m_pixmap); //将对应图片完整画到当前对象上。参数:当前按钮所对应的矩形区域、指定m_pixmap对象
}

12. 按钮组窗口类ButtonGroup

按钮组窗口类主要是为用户玩家服务的,因为开始游戏界面、抢地主界面、必须出牌界面和可放弃出牌界面都需要显示出不同的按钮,所以得在ui中使用Stacked Widget栈窗口,它可以容纳多张不同的窗口,然后通过函数调用切换即可。

12.1 创建按钮组窗口

1.窗口创建:

选择新建、Qt、Qt设计师界面类(是带ui界面的)、选择Widget类型的窗口,类名为ButtonGroup

该类是在ui界面上拖入了一个Stacked Widget栈窗口,在该栈窗口中建了5个页面。第1个页面是开始页面;第2个页面是必须出牌页面;第3个为可放弃出牌页面;第4个为叫地主页面;第5个是空白页面。并利用信号槽机制实现了connect操作,即按下按钮,就会触发响应的按钮信号,然后发送自定义信号。该类只负责发送信号即可,不用处理相关的操作。

2.对每个页面的按钮做美化

将Stacked Widget栈窗口中的5个窗口的按钮都提升为MyButton类。然后基于MyButton类对窗口所有按钮进行美化。

3.添加资源文件:选择新建、Qt、Qt Resource File、名称为res。

4.将按钮组窗口添加到主窗口(中下方)

在主窗口的下方拖入一个Widget窗口,将其提升为ButtonGroup类。因为主窗口最下方放的是扑克牌,所以得在最下面放一根弹簧将ButtonGroup窗口撑起来一点。然后在主窗口析构函数中,初始化按钮组,并设定刚开的页面为游戏开始页面的按钮组即可。

12.2 ButtonGroup类头文件

按钮组窗口类ButtonGroup就是定义了一个Page页的切换函数,通过传入的参数配置页枚举类的不同,会发出信号通知主窗口该显示哪个按钮组窗口。

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
26
27
28
29
30
31
32
namespace Ui {
class ButtonGroup;
}

class ButtonGroup : public QWidget
{
Q_OBJECT

public:
enum Panel{Start, PlayCard, PassOrPlay,CallLord,Empty}; //配置页枚举类
explicit ButtonGroup(QWidget *parent = nullptr);
~ButtonGroup();

//初始化按钮
void initButtons();

//处理Page页的切换(因为第2个参数是只有抢地主页面按钮组时才会用到,其它按钮组页面窗口用不到,所以设置默认参数)
void selectPanel(Panel type, int bet = 0); //传入的参数是配置页的枚举类,即窗口,当是叫地主窗口时,要目前下注最高分数(用户玩家需要知道)

signals:
//开始游戏
void startGame();
//出牌
void playHand();
//不出牌
void pass();
//抢地主
void betPoint(int bet);

private:
Ui::ButtonGroup *ui;
};

12.3 ButtonGroup类函数实现

这部分代码实现了头文件定义的函数,对每个按钮都加载了三种图片,即常规状态下、鼠标滑过状态下和点击按钮状态下。同时也通过信号槽机制,对每个按钮按下后,都会发出信号,触发对应的槽函数处理。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
ButtonGroup::ButtonGroup(QWidget *parent) :
QWidget(parent),
ui(new Ui::ButtonGroup)
{
ui->setupUi(this);
}

ButtonGroup::~ButtonGroup()
{
delete ui;
}
//所有按钮的图片初始化
void ButtonGroup::initButtons()
{
//开始游戏
ui->start->setImage(":/images/start-1.png", ":/images/start-3.png", ":/images/start-2.png");
//出牌
ui->playCard->setImage(":/images/chupai_btn-1.png", ":/images/chupai_btn-3.png", ":/images/chupai_btn-2.png");
ui->playCard1->setImage(":/images/chupai_btn-1.png", ":/images/chupai_btn-3.png", ":/images/chupai_btn-2.png");
//不要
ui->pass->setImage(":/images/pass_btn-1.png", ":/images/pass_btn-3.png", ":/images/pass_btn-2.png");
//不抢
ui->giveup->setImage(":/images/buqiang-1.png", ":/images/buqiang-3.png", ":/images/buqiang-2.png");
//1,2,3分
ui->oneScore->setImage(":/images/1fen-1.png", ":/images/1fen-3.png", ":/images/1fen-2.png");
ui->twoScore->setImage(":/images/2fen-1.png", ":/images/2fen-3.png", ":/images/2fen-2.png");
ui->threeScore->setImage(":/images/3fen-1.png", ":/images/3fen-3.png", ":/images/3fen-2.png");

//设置按钮的大小
QVector<MyButton*>btns;
btns << ui->start << ui->playCard << ui->playCard1 << ui->pass << ui->giveup << ui->oneScore << ui->twoScore << ui->threeScore;
for(int i=0; i<btns.size(); i++){
btns[i]->setFixedSize(90,45);
}

//点击不同按钮,发出信号后,处理相应的槽函数
connect(ui->start, &MyButton::clicked, this, &ButtonGroup::startGame); //按下开始按钮(初始界面)
connect(ui->playCard, &MyButton::clicked, this, &ButtonGroup::playHand); //按下出牌按钮(必须出牌界面)
connect(ui->playCard1, &MyButton::clicked, this, &ButtonGroup::playHand); //按下出牌按钮(可出可不出界面)
connect(ui->pass, &MyButton::clicked, this, &ButtonGroup::pass); //按下放弃出牌按钮(可出可不出界面)
connect(ui->giveup, &MyButton::clicked, this, [=](){ //按下不抢地主按钮(抢地主界面)
emit betPoint(0); //当点击的是放弃抢地主,发出的信号
});
connect(ui->oneScore, &MyButton::clicked, this, [=](){ //按下下注1分按钮(抢地主界面)
emit betPoint(1); //当点击1分按钮,发出的信号
});
connect(ui->twoScore, &MyButton::clicked, this, [=](){ //按下下注2分按钮(抢地主界面)
emit betPoint(2); //当点击2分按钮,发出的信号
});
connect(ui->threeScore, &MyButton::clicked, this, [=](){ //按下下注3分按钮(抢地主界面)
emit betPoint(3); //当点击3分按钮,发出的信号
});
}

//显示对应的按钮组窗口
void ButtonGroup::selectPanel(ButtonGroup::Panel type, int bet) //传入的是页数,分数(用户玩家才会调用该函数,要知道目前下注的最高分)
{
ui->stackedWidget->setCurrentIndex(type); //设置当前要找展示的按钮页面窗口
if(type != CallLord){ //如果不是叫地主状态,就直接返回(说明用户玩家不叫地主)
return;
}
//下面是用户玩家是叫地主状态
if(bet == 0){ //如果目前最高分是0,说明还可以显示1,2,3分三个按钮
ui->oneScore->setVisible(true);
ui->twoScore->setVisible(true);
ui->threeScore->setVisible(true);
}else if(bet == 1){
ui->oneScore->setVisible(false); //1分按钮隐藏
ui->twoScore->setVisible(true);
ui->threeScore->setVisible(true);
}else if(bet == 2){
ui->oneScore->setVisible(false); //1分按钮隐藏
ui->twoScore->setVisible(false); //2分按钮隐藏
ui->threeScore->setVisible(true);
}
}

13. 游戏控制类GameControl

游戏控制类负责对游戏整个过程进行一个管理和控制,像游戏状态、玩家状态等都想要通过该类来进行一个维护。同时该类也会和主窗口类直接连接,在游戏控制过程中,每个玩家的变化、游戏的变化都需要通过信号的方式通知主窗口。

游戏控制类添加:通过选择新建、c++、c++class、类名为GameControl,基类为QObject。

13.1 GameControl类头文件

游戏控制类GameControl定义了枚举类游戏状态和玩家状态,当它们发送变化时,都会通过信号发送出去。同时,该头文件还定义了许多游戏过程函数,如发牌、叫地主、出牌等。而且有些后面开发会用到的信息都会记录保存。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
struct BetRecord{     //存放三个玩家叫地主情况的结构体(因为c++的结构体类似于类,所以可以有构造函数)
BetRecord(){ //当创建BetRecord时,就自动清空,初始化了
reset();
}
void reset(){
player = nullptr;
bet = 0;
times = 0;
}
Player* player;
int bet;
int times; //第几次叫地主
};

class GameControl : public QObject
{
Q_OBJECT
public:
//游戏状态
enum GameStatus{
DispatchCard, //发牌状态
CallingLord, //叫地主状态
PlayingHand //出牌状态
};
//玩家状态
enum PlayerStatus{
ThinkingForCallLord, //考虑叫地主
ThinkingForPlayHand, //考虑出牌
Winning //玩家赢了
};
explicit GameControl(QObject *parent = nullptr);

//初始化玩家,将三个对象创建出来
void playerInit();

//得到玩家的实例化对象
Robot* getLeftRobot();
Robot* getRightRobot();
UserPlayer* getUserPlayer();

//设置和获取当前玩家
void setCurrentPlayer(Player* player); //参数是玩家类型,由于不知道具体玩家类型,传它们的父类
Player *getCurrentPlayer();

//得到出牌玩家和打出的牌
Player* getPendPlayer();
Cards getPendCards();

//初始化扑克牌(所有)
void initAllCards();
//每次发一张牌
Card takeOneCard();
//得到最后的三张底牌
Cards getSurplusCards();
//重置卡牌数据(当完成一局游戏就会清空玩家手中的扑克牌)
void resetCardData();

//准备叫地主
void startLordCard(); //该函数会发出信号,主窗口接收
//成为地主
void becomeLord(Player* player, int bet);
//清空所有玩家的得分
void clearPlayerScore();
//得到玩家下注的最高得分
int getPlayerMaxBet();

//处理叫地主(槽函数)
void onGrabBet(Player* player, int bet); //参数:具体是哪个玩家,玩家叫地主时下的分数
//处理出牌(槽函数) --->当玩家调用了playhand()出牌函数后,会发出出牌信号,由该槽函数接收处理
void onPlayHand(Player* player, Cards& card); //参数:出牌的玩家,出的牌

signals:
//向主窗口通知 玩家的状态变化
void playerStatusChanged(Player* player, PlayerStatus status); //告诉主窗口是哪个玩家,当前玩家是什么状态(考虑叫地主,考虑出牌,玩家赢了)
//向主窗口通知是哪个玩家抢了地主以及下注分数(然后轮到其它玩家抢地主下分)
void notifyGrabLordBet(Player* player, int bet, bool flag);
//游戏状态变化的信号
void gameStatusChanged(GameStatus status); //参数是游戏的状态

//向主窗口通知玩家出牌了
void notifyPlayHand(Player* player, Cards& card); //参1:出牌玩家;参2:出的牌
//给玩家传递出牌数据
void pendingInfo(Player* player, Cards& card); //参1:出牌玩家;参2:出的牌

private:
Robot* m_robotLeft=nullptr;
Robot* m_robotRight=nullptr;
UserPlayer* m_user=nullptr;

Player* m_currPlayer=nullptr; //保存当前玩家对象的指针(实时变化的)

Player* m_pendPlayer=nullptr; //出牌的玩家
Cards m_pendCards; //出牌玩家打出的牌

Cards m_allCards; //一副扑克牌

BetRecord m_betRecord; //存放三个玩家叫地主的情况结构体对象
int m_curBet = 0; //记录下注底分,方便后期计算
};

13.2 GameControl类函数实现

这部分实现了头文件定义的函数。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
GameControl::GameControl(QObject *parent) : QObject(parent){}

void GameControl::playerInit()
{
//1.对象实例化,通过构造函数指定名字,并给该对象指定一个父对象(当释放GameControl时,会自动释放该对象)
m_robotLeft = new Robot("机器人A", this);
m_robotRight = new Robot("机器人B",this);
m_user = new UserPlayer("我自己",this);

//2.给各对象设置属性信息
//头像的显示
m_robotLeft->setDirection(Player::Left);
m_robotRight->setDirection(Player::Right);
m_user->setDirection(Player::Right);
//性别
Player::Sex sex;
sex = (Player::Sex)QRandomGenerator::global()->bounded(2); //生成随机数0或1,0表示男,1表示女,然后需要做强制类型转换,转换为枚举类型
m_robotLeft->setSex(sex); //把生成的随机性别设置给对应玩家
sex = (Player::Sex)QRandomGenerator::global()->bounded(2); //生成随机数0或1,0表示男,1表示女,然后需要做强制类型转换,转换为枚举类型
m_robotRight->setSex(sex); //把生成的随机性别设置给对应玩家
sex = (Player::Sex)QRandomGenerator::global()->bounded(2); //生成随机数0或1,0表示男,1表示女,然后需要做强制类型转换,转换为枚举类型
m_user->setSex(sex); //把生成的随机性别设置给对应玩家
//玩家出牌顺序
// user
m_user->setPrevPlayer(m_robotLeft);
m_user->setNextPlayer(m_robotRight);

// left robot
m_robotLeft->setPrevPlayer(m_robotRight);
m_robotLeft->setNextPlayer(m_user);

// right robot
m_robotRight->setPrevPlayer(m_user);
m_robotRight->setNextPlayer(m_robotLeft);

//指定当前玩家为用户(优先叫地主的权力)
m_currPlayer = m_user;

//处理玩家发射出的抢地主信号(几分)
connect(m_user, &UserPlayer::notifyGrabLordBet,this,&GameControl::onGrabBet);
connect(m_robotLeft, &UserPlayer::notifyGrabLordBet,this,&GameControl::onGrabBet);
connect(m_robotRight, &UserPlayer::notifyGrabLordBet,this,&GameControl::onGrabBet);

//传递出牌玩家对象和玩家打出的牌给其它玩家
connect(this, &GameControl::pendingInfo, m_robotLeft, &Robot::storePendingInfo);
connect(this, &GameControl::pendingInfo, m_robotRight, &Robot::storePendingInfo);
connect(this, &GameControl::pendingInfo, m_user, &Robot::storePendingInfo);

//处理玩家出牌
connect(m_robotLeft, &Robot::notifyPlayHand, this, &GameControl::onPlayHand);
connect(m_robotRight, &Robot::notifyPlayHand, this, &GameControl::onPlayHand);
connect(m_user, &Robot::notifyPlayHand, this, &GameControl::onPlayHand);
}
//得到左手玩家对象
Robot *GameControl::getLeftRobot()
{
return m_robotLeft; //返回对应玩家对象指针
}
//得到右手玩家对象
Robot *GameControl::getRightRobot()
{
return m_robotRight; //返回对应玩家对象指针
}
//得到用户玩家对象
UserPlayer *GameControl::getUserPlayer()
{
return m_user; //返回对应玩家对象指针
}
//设置和获取当前出牌玩家
void GameControl::setCurrentPlayer(Player *player)
{
m_currPlayer = player;
}
Player *GameControl::getCurrentPlayer()
{
return m_currPlayer;
}
//得到上次出牌的玩家
Player *GameControl::getPendPlayer()
{
return m_pendPlayer;
}
//得到上次出的牌
Cards GameControl::getPendCards()
{
return m_pendCards;
}
//初始化所有的牌(发牌会用)
void GameControl::initAllCards()
{
m_allCards.clear(); //先对一副扑克牌做清空操作
for(int p=Card::Card_Begin+1; p<Card::Card_SJ; p++){ //遍历点数,枚举类型不能做算术运算,所以定义p只能是int,不能是Card::CardPoint
for(int s=Card::Suit_Begin+1; s<Card::Suit_End; s++){ //遍历花色
Card c((Card::CardPoint)p, (Card::CardSuit)s); //再将int类型强制转换为枚举类型
m_allCards.add(c); //创建完一张扑克牌,就插入到m_allCards中
}
}
m_allCards.add(Card(Card::Card_SJ, Card::Suit_Begin)); //插入小王
m_allCards.add(Card(Card::Card_BJ, Card::Suit_Begin)); //插入大王
}
//随机获得一张卡牌(发牌)
Card GameControl::takeOneCard() //一次发一张扑克牌
{
return m_allCards.takeRandomCard(); //调用Cards类里面定义的函数,随机返回一张扑克牌
}
//地主得到3张底牌
Cards GameControl::getSurplusCards() //获取底牌函数
{
return m_allCards; //当发完后,做后留下的三张牌就作为底牌
}
//发牌前的初始化函数
void GameControl::resetCardData()
{
//洗牌
initAllCards();
//清空所有玩家的牌(就是将玩家对象里的Cards容器清空)
m_robotLeft->clearCards();
m_robotRight->clearCards();
m_user->clearCards();
//初始化出牌玩家和牌(新一局开始之前,是没有出牌玩家的)
m_pendPlayer = nullptr; //出牌对象也先默认为空
m_pendCards.clear(); //打出的牌也是空的
}

void GameControl::startLordCard() //准备叫地主
{
//调用叫地主函数
m_currPlayer->prepareCallLord(); //m_currPlayer可能是用户玩家,也可能是机器人玩家(多态实现)
emit playerStatusChanged(m_currPlayer, ThinkingForCallLord); //发出信号,参数:当前玩家,当前玩家状态(准备叫地主)
}

void GameControl::becomeLord(Player *player, int bet) //成为地主,参数传进来的是地主角色,其它两个角色就是农民了
{
m_curBet = bet; //保存下注的底分,出牌(炸弹)和打完牌后计算需要用到
player->setRole(Player::Lord);
player->getPrevPlayer()->setRole(Player::Farmer); //设置地主角色的上一家是农民
player->getNextPlayer()->setRole(Player::Farmer); //设置地主角色的下一家是农民

//成为地主的是要先出牌,所以他是当前玩家
m_currPlayer = player;
player->storeDispatchCard(m_allCards); //给该玩家的手牌添加三张底牌(m_allCards是发完牌后,剩下三张)

QTimer::singleShot(1000, this, [=](){
emit gameStatusChanged(PlayingHand); //发送游戏的状态信号(出牌状态)
emit playerStatusChanged(player, ThinkingForPlayHand); //玩家状态(考虑出牌)
//调用准备出牌函数
m_currPlayer->preparePlayHand();
});
}

void GameControl::clearPlayerScore()
{
m_robotLeft->setScore(0);
m_robotRight->setScore(0);
m_user->setScore(0);
}

int GameControl::getPlayerMaxBet()
{
return m_betRecord.bet; //结构体里面存的就是最高分(因为高分信息会覆盖低分信息)
}

void GameControl::onGrabBet(Player *player, int bet) //处理叫地主
{
//1. 通知主界面,该玩家叫地主了(主界面就要更新信息提示)
if(bet==0 || m_betRecord.bet>=bet){ //这种情况说明该玩家放弃抢地主
emit notifyGrabLordBet(player, 0, false);
}else if(bet>0 && m_betRecord.bet==0){ //如果当前玩家下注的分数大于0,且之前结构体存的是0分(说明之前玩家没有抢地主或现在是第一次下注)
//第1个抢地主的玩家
emit notifyGrabLordBet(player, bet, true); //如果是第一个抢地主的玩家,参数3置为true(主窗口需要知道第一个抢地主的玩家)
}else{
//第2或3个抢地主的玩家
emit notifyGrabLordBet(player, bet, false);
}
//2. 判断玩家下注是不是3分,如果是就抢地主成功(抢地主就结束了)
if(bet == 3){
//该玩家成为地主
becomeLord(player,bet); //调用成为地主的函数
//清空抢地主结构体数据
m_betRecord.reset();
return; //直接退出了。当某个玩家给出3分时,那么该玩家就是地主了,不用再执行下面内容
}
//3.下注不够3分,对玩家的分数进行比较,分数高的是地主(低的被覆盖)
if(m_betRecord.bet < bet){ //m_betRecord结构体是玩家抢地主,就存其信息,当下一个玩家也抢地主,如果分数比之前的高,m_betRecord就进行更新
m_betRecord.bet = bet; //重新存储分数
m_betRecord.player = player; //重新存储地主玩家
}
m_betRecord.times++; //下注抢地主的次数++
//如果每个玩家都抢过一次地主,抢地主结束
if(m_betRecord.times == 3){
if(m_betRecord.bet == 0){ //如果分数还是为0,说明没有玩家叫地主
emit gameStatusChanged(DispatchCard); //通知此时需要改变为发牌状态
}else{ //如果不为0,说明有玩家会成为地主
becomeLord(m_betRecord.player, m_betRecord.bet); //成为地主的玩家是结构体中保存的玩家对象
}
m_betRecord.reset(); //结构体重置,为下一节游戏清空结构体(因为到了下一句游戏,不会重新创建,还是用这个创建好的结构体)
return; //直接返回,就不用执行下面玩家切换了
}
//4.切换玩家,通知下一个玩家继续抢地主
m_currPlayer = player->getNextPlayer(); //改变当前玩家
//发送信号给主界面,告知当前状态还是为抢地主状态
emit playerStatusChanged(m_currPlayer,ThinkingForCallLord);
//每个玩家都有一个prepareCallLord()函数,即考虑叫地主,如果是机器人玩家,有专门的线程来处理该操作,如果是用户玩家,该函数就是空的(只需要点击按钮完成操作)
m_currPlayer->prepareCallLord(); //告诉新的当前玩家继续抢地主
}

void GameControl::onPlayHand(Player *player, Cards &card) //处理出牌
{
//1. 将玩家出牌信号转给主界面(主界面更新出出牌窗口内容)
emit notifyPlayHand(player, card);
//2. 如果不是空牌,给其它玩家发送信号,保存出牌玩家对象和打出的牌
if(!card.isEmpty()){
m_pendCards = card;
m_pendPlayer = player;
emit pendingInfo(player, card); //发出信号给玩家,参数是打出牌的当前玩家和打出的牌(其它玩家需要根据出的牌和玩家,做相应的处理)
}
//如果有炸弹,底分翻倍
PlayHand::HandType type = PlayHand(card).getHandType(); //传入当前玩家出的牌,得到出牌类型
if(type == PlayHand::Hand_Bomb || type == PlayHand::Hand_Bomb_Jokers){ //如果出的牌是炸弹和王炸
m_curBet = m_curBet*2; //将底分*2
}
//3. 如果玩家的牌都出完了,计算本局游戏的总分
if(player->getCards().isEmpty()){
Player* prev = player->getPrevPlayer(); //获取当前玩家的上一个玩家
Player* next = player->getNextPlayer(); //获取当前玩家的下一个玩家
if(player->getRole() == Player::Lord){ //如果当前玩家为地主
player->setScore(player->getScore() + 2*m_curBet); //当前玩家加分(原来得分+当前得分)
prev->setScore(prev->getScore() - m_curBet);
next->setScore(next->getScore() - m_curBet);
player->setWin(true);
prev->setWin(false);
next->setWin(false);
}else{ //当前玩家为农民
player->setWin(true);
player->setScore(player->getScore() + m_curBet);
if(prev->getRole() == Player::Lord){ //如果当前玩家的上一个玩家是地主
prev->setScore(prev->getScore() - 2*m_curBet);
next->setScore(next->getScore() + m_curBet);
prev->setWin(false);
next->setWin(true);
}else{ //如果当前玩家的上一个玩家是农民
next->setScore(next->getScore() - 2*m_curBet);
prev->setScore(prev->getScore() + m_curBet);
prev->setWin(true);
next->setWin(false);
}
}
emit playerStatusChanged(player, GameControl::Winning); //向主窗口通知 当前玩家的状态变化(赢了)
return; //赢了就退出了,不然还会执行下面程序
}
//4. 如果牌没有出完,下一个玩家继续出牌
m_currPlayer = player->getNextPlayer(); //将下一个玩家更新为当前玩家
m_currPlayer->preparePlayHand(); //更新的当前玩家准备出牌(该函数是一个多态实现,不同类型玩家调用会执行不同的处理)
emit playerStatusChanged(m_currPlayer, GameControl::ThinkingForPlayHand); //向主窗口通知 更新的当前玩家的状态变化(考虑出牌)
}

14. 出牌类PlayHand

该类主要是对一些牌进行分类和识别,得出调用者传入牌的一个牌型。

出牌类(策略):新建、c、c++、类名为PlayHand,基类为Custom。

14.1 PlayHand类头文件

出牌类PlayeHand定义了一个牌的类型枚举类,它里面都是斗地主小游戏里面所有的牌型。然后定义了对牌的分类函数和识别函数。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class PlayHand
{
public:
// 出牌组合或者方式
enum HandType
{
Hand_Unknown, // 未知
Hand_Pass, // 过
Hand_Single, // 单
Hand_Pair, // 对
Hand_Triple, // 三个
Hand_Triple_Single, // 三带一
Hand_Triple_Pair, // 三带二
Hand_Plane, // 飞机,555_666
Hand_Plane_Two_Single, // 飞机带单,555_666_3_4
Hand_Plane_Two_Pair, // 飞机带双,555_666_33_44
Hand_Seq_Pair, // 连对,33_44_55(_66...)
Hand_Seq_Single, // 顺子,34567(8...)
Hand_Bomb, // 炸弹
Hand_Bomb_Single, // 炸弹带一个
Hand_Bomb_Pair, // 炸弹带一对
Hand_Bomb_Two_Single, // 炸弹带两单
Hand_Bomb_Jokers, // 王炸
Hand_Bomb_Jokers_Single, // 王炸带一个
Hand_Bomb_Jokers_Pair, // 王炸带一对
Hand_Bomb_Jokers_Two_Single // 王炸带两单
};

PlayHand();
// 传递给类一组牌, 通过类分析出牌型, 点数, 以及相关的附属信息(比如顺子: 记录牌的数量)
explicit PlayHand(Cards& cards);
PlayHand(HandType type, Card::CardPoint pt, int extra);


// 得到牌的属性信息
HandType getHandType();
Card::CardPoint getCardPoint();
int getExtra();

// 比较自己的牌和其他人的牌的大小
bool canBeat(const PlayHand& other);
private:
// 1. 对扑克牌进行分类: 1张相同的, 2张相同的, 3张相同的, 4张相同的
void classify(Cards& cards);
// 2. 对牌型进行分类
void judgeCardType();
// 判断牌的类型
bool isPass(); // 放弃出牌
bool isSingle(); // 单
bool isPair(); // 对
bool isTriple(); // 三个(相同)
bool isTripleSingle(); // 三带一
bool isTriplePair(); // 三带二
bool isPlane(); // 飞机
bool isPlaneTwoSingle(); // 飞机带两单
bool isPlaneTwoPair(); // 飞机带2对
bool isSeqPair(); // 连对
bool isSeqSingle(); // 顺子
bool isBomb(); // 炸弹
bool isBombSingle(); // 炸弹带一单
bool isBombPair(); // 炸弹带一对
bool isBombTwoSingle(); // 炸弹带两单
bool isBombJokers(); // 王炸
bool isBombJokersSingle(); // 王炸带一单
bool isBombJokersPair(); // 王炸带一对
bool isBombJokersTwoSingle(); // 王炸带两单

private:
HandType m_type; //牌型
Card::CardPoint m_pt; //点数
int m_extra; //附加类型(如顺子是5张还是6张还是7张,连对是3对还是4对还是5对)
QVector<Card::CardPoint> m_oneCard; //存储单张相同点数的牌
QVector<Card::CardPoint> m_twoCard; //存储两种相同点数的牌
QVector<Card::CardPoint> m_threeCard; //存储三张相同点数的牌
QVector<Card::CardPoint> m_fourCard; //存储四张相同点数的牌
};

14.2 PlayHand类函数实现

该部分实现了头文件定义的函数,首先是通过函数classify()对传进来的牌进行分类,用4个容器进行存放,分别为1张相同点数的牌容器、2张相同点数的牌容器、3张相同点数的牌容器和4张相同点数的牌容器。然后通过judgeCardType()函数基于分类的4个容器来对牌进行分析。记录扑克牌的牌型、点数和附加类型(连对的对数和顺子的张数)。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
#include "playhand.h"

PlayHand::PlayHand(){}

PlayHand::PlayHand(Cards &cards)
{
//1.对扑克牌进行分类:1张的,2张,3张,4张有多少种
classify(cards);
//2.对牌型进行分类
judgeCardType();
}

PlayHand::PlayHand(PlayHand::HandType type, Card::CardPoint pt, int extra)
{
m_type = type;
m_pt = pt;
m_extra = extra;
}
//获取扑克牌的牌型
PlayHand::HandType PlayHand::getHandType()
{
return m_type;
}
//获取扑克牌的点数
Card::CardPoint PlayHand::getCardPoint()
{
return m_pt;
}
//获取附加类型
int PlayHand::getExtra()
{
return m_extra;
}
//比较牌是否大于对方
bool PlayHand::canBeat(const PlayHand &other) //和对方比较牌型
{
//我的牌型是未知的
if(m_type == Hand_Unknown){
return false;
}
//对方放弃出牌
if(other.m_type == Hand_Pass){
return true;
}
//我是王炸
if(m_type == Hand_Bomb_Jokers){
return true;
}
//自己是炸弹,而对方是比炸弹小的牌型(牌类型的枚举类是按照牌型从小到大排列的)
if(m_type==Hand_Bomb && other.m_type>=Hand_Single && other.m_type<=Hand_Seq_Single){
return true;
}
//双方的牌型是一致的(比点数)
if(m_type == other.m_type){
if(m_type == Hand_Seq_Pair || m_type == Hand_Seq_Single){ //如果是连对或是顺子
return m_pt>other.m_pt && m_extra == other.m_extra; //就要求最小点数要比它大,同时个数要一样,才返回true
}else{
return m_pt > other.m_pt; //如果是其它类型,就直接比点数了
}
}
return false; //如果都不满足上面,就返回false
}
//将传进来的牌按张数进行分类
void PlayHand::classify(Cards &cards) //分析传进来的牌
{
CardList list = cards.toCardList(); //将转进来牌存到QVector容器
int cardRecord[Card::Card_End]; //创建一个数组,大小为不同点数类型大小
memset(cardRecord, 0, sizeof(int)*Card::Card_End); //数组清0
for(int i=0; i<list.size(); i++){ //遍历每一张牌
Card c = list.at(i); //取出扑克牌
cardRecord[c.point()]++; //卡牌点数与数组里元素的位置是有对应关系的
}
//先清空四个容器(分别存储相同点数只有1张、2张、3张和4张的牌型)
m_oneCard.clear();
m_twoCard.clear();
m_threeCard.clear();
m_fourCard.clear();
for(int i=0; i<Card::Card_End; i++){ //遍历数组
if(cardRecord[i] == 1){ //数组某下标位置元素为1,则说明该点数的牌就为1
m_oneCard.push_back((Card::CardPoint)i); //将其转换后存到1张相同牌的容器里
}else if(cardRecord[i] == 2){
m_twoCard.push_back((Card::CardPoint)i); //将其转换后存到2张相同牌的容器里
}else if(cardRecord[i] == 3){
m_threeCard.push_back((Card::CardPoint)i); //将其转换后存到3张相同牌的容器里
}else if(cardRecord[i] == 4){
m_fourCard.push_back((Card::CardPoint)i); //将其转换后存到4张相同牌的容器里
}
}
}
//分析牌的一个类型
void PlayHand::judgeCardType()
{
m_type = Hand_Unknown; //牌的类型先初始化为未知
m_pt = Card::Card_Begin; //初始化点数
m_extra = 0; //这个一般是记录像连队多少对,顺子多少张
if(isPass()){ //放弃出牌
m_type = Hand_Pass;
}
if(isSingle()){ //单牌
m_type = Hand_Single; //记录类型
m_pt = m_oneCard[0]; //获取其点数
}else if(isPair()){ //一对
m_type= Hand_Pair;
m_pt = m_twoCard[0];
}else if(isTriple()){ //3张点数相同
m_type= Hand_Triple;
m_pt = m_threeCard[0];
}else if(isTripleSingle()){ //3带1
m_type= Hand_Triple_Single;
m_pt = m_threeCard[0];
}else if(isTriplePair()){ //3带一对
m_type= Hand_Triple_Pair;
m_pt = m_threeCard[0];
}else if(isPlane()){ //飞机
m_type= Hand_Plane;
//记录点数最小的牌
m_pt = m_threeCard[0];
}else if(isPlaneTwoSingle()){ //飞机带两单
m_type= Hand_Plane_Two_Single;
m_pt = m_threeCard[0];
}else if(isPlaneTwoPair()){ //飞机带两对
m_type= Hand_Plane_Two_Pair;
m_pt = m_threeCard[0];
}else if(isSeqPair()){ //连对
m_type= Hand_Seq_Pair;
m_pt = m_twoCard[0];
//记录连对的个数
m_extra = m_twoCard.size();
}else if(isSeqSingle()){ //顺子
m_type= Hand_Seq_Single;
m_pt = m_oneCard[0];
m_extra = m_oneCard.size();
}else if(isBomb()){ //炸弹
m_type= Hand_Bomb;
m_pt = m_fourCard[0];
}else if(isBombSingle()){ //炸弹带1单
m_type= Hand_Bomb_Single;
m_pt = m_fourCard[0];
}else if(isBombPair()){ //炸弹带一对
m_type= Hand_Bomb_Pair;
m_pt = m_fourCard[0];
}else if(isBombTwoSingle()){ //炸弹带2单
m_type= Hand_Bomb_Two_Single;
m_pt = m_fourCard[0];
}else if(isBombJokers()){ //王炸
m_type= Hand_Bomb_Jokers;
}else if(isBombJokersSingle()){ //王炸带1单
m_type= Hand_Bomb_Jokers_Single;
}else if(isBombJokersPair()){ //王炸带一对
m_type= Hand_Bomb_Jokers_Pair;
}else if(isBombJokersTwoSingle()){ //王炸带2单
m_type= Hand_Bomb_Jokers_Two_Single;
}
}

bool PlayHand::isPass() //放弃出牌
{
if(m_oneCard.size()==0 && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
return true; //每个容器中都为空,说明是放弃出牌了
}
return false;
}

bool PlayHand::isSingle() //判断牌(打出的)是否是单牌
{
//如果存在1张相同点数容器大小为1,其它2、3、4张相同点数的牌容器为空,说明打出的是单牌
if(m_oneCard.size()==1 && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
return true; //打出的是单牌,返回true
}
return false;
}

bool PlayHand::isPair() //判断牌(打出的)是否是一对
{
if(m_oneCard.isEmpty() && m_twoCard.size()==1 && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
return true;
}
return false;
}

bool PlayHand::isTriple() //判断牌(打出的)是否是三张一样的
{
if(m_oneCard.isEmpty() && m_twoCard.isEmpty() && m_threeCard.size()==1 && m_fourCard.isEmpty()){
return true;
}
return false;
}

bool PlayHand::isTripleSingle() //判断牌(打出的)是否是三带一
{
if(m_oneCard.size()==1 && m_twoCard.isEmpty() && m_threeCard.size()==1 && m_fourCard.isEmpty()){
return true;
}
return false;
}

bool PlayHand::isTriplePair() //三带二(两张要一样)
{
if(m_oneCard.isEmpty() && m_twoCard.size()==1 && m_threeCard.size()==1 && m_fourCard.isEmpty()){
return true;
}
return false;
}

bool PlayHand::isPlane() //飞机
{
if(m_oneCard.isEmpty() && m_twoCard.isEmpty() && m_threeCard.size()==2 && m_fourCard.isEmpty()){
std::sort(m_threeCard.begin(),m_threeCard.end()); //进行排序(升序)
if(m_threeCard[1]-m_threeCard[0]==1 && m_threeCard[1]<Card::Card_2){ //必须是相连且不涉及卡牌2
return true;
}
}
return false;
}

bool PlayHand::isPlaneTwoSingle() //飞机带两单(不能是大小王)
{
if(m_oneCard.size()==2 && m_twoCard.isEmpty() && m_threeCard.size()==2 && m_fourCard.isEmpty()){
std::sort(m_threeCard.begin(),m_threeCard.end()); //进行排序(升序)
std::sort(m_oneCard.begin(),m_oneCard.end()); //进行排序(升序)
if(m_threeCard[1]-m_threeCard[0]==1 && m_threeCard[1]<Card::Card_2 && m_oneCard[0]!=Card::Card_SJ && m_oneCard[1]!=Card::Card_BJ){ //必须是相连且不涉及卡牌2且两单不是大小王
return true;
}
}
return false;
}

bool PlayHand::isPlaneTwoPair() // 飞机带2对
{
if(m_oneCard.isEmpty() && m_twoCard.size()==2 && m_threeCard.size()==2 && m_fourCard.isEmpty()){
std::sort(m_threeCard.begin(),m_threeCard.end()); //进行排序(升序)
if(m_threeCard[1]-m_threeCard[0]==1 && m_threeCard[1]<Card::Card_2){ //必须是相连且不涉及卡牌2
return true;
}
}
return false;
}

bool PlayHand::isSeqPair() //连对
{
if(m_oneCard.isEmpty() && m_twoCard.size()==3 && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
std::sort(m_twoCard.begin(),m_twoCard.end()); //进行排序(升序)
//如果是三个连续的就满足:(最大数-最小数)=(牌类型总数-1),如456。还必须满足卡牌是3到2之间(不包括2)
if(m_twoCard.last()-m_twoCard.first()==(m_twoCard.size()-1) && m_twoCard.first()>=Card::Card_3 && m_twoCard.last()<Card::Card_2){
return true;
}
}
return false;
}

bool PlayHand::isSeqSingle() //顺子
{
if(m_oneCard.size()>=5 && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
std::sort(m_oneCard.begin(),m_oneCard.end()); //进行排序(升序)
if(m_oneCard.last()-m_oneCard.first()==(m_oneCard.size()-1) && m_oneCard.first()>=Card::Card_3 && m_oneCard.last()<Card::Card_2){
return true;
}
}
return false;
}

bool PlayHand::isBomb() //炸弹
{
if(m_oneCard.isEmpty() && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.size()==1){
return true;
}
return false;
}

bool PlayHand::isBombSingle() //炸弹带一个单
{
if(m_oneCard.size()==1 && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.size()==1){
return true;
}
return false;
}

bool PlayHand::isBombPair() //炸弹带一对
{
if(m_oneCard.isEmpty() && m_twoCard.size()==1 && m_threeCard.isEmpty() && m_fourCard.size()==1){
return true;
}
return false;
}

bool PlayHand::isBombTwoSingle() //炸弹带两单
{
if(m_oneCard.size()==2 && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.size()==1){
std::sort(m_oneCard.begin(), m_oneCard.end());
if(m_oneCard.first()!=Card::Card_SJ && m_oneCard.last()!=Card::Card_BJ){
return true;
}
}
return false;
}

bool PlayHand::isBombJokers() //王炸
{
if(m_oneCard.size()==2 && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
std::sort(m_oneCard.begin(), m_oneCard.end());
if(m_oneCard.first()==Card::Card_SJ && m_oneCard.last()==Card::Card_BJ){
return true;
}
}
return false;
}

bool PlayHand::isBombJokersSingle() //王炸带一单
{
if(m_oneCard.size()==3 && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
std::sort(m_oneCard.begin(), m_oneCard.end());
if(m_oneCard[1]==Card::Card_SJ && m_oneCard[2]==Card::Card_BJ){
return true;
}
}
return false;
}

bool PlayHand::isBombJokersPair() //王炸带一对
{
if(m_oneCard.size()==2 && m_twoCard.size()==1 && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
std::sort(m_oneCard.begin(), m_oneCard.end());
if(m_oneCard[0]==Card::Card_SJ && m_oneCard[1]==Card::Card_BJ){
return true;
}
}
return false;
}

bool PlayHand::isBombJokersTwoSingle() //王炸带两单
{
if(m_oneCard.size()==4 && m_twoCard.isEmpty() && m_threeCard.isEmpty() && m_fourCard.isEmpty()){
std::sort(m_oneCard.begin(), m_oneCard.end());
if(m_oneCard[2]==Card::Card_SJ && m_oneCard[3]==Card::Card_BJ){ //两王比两单大
return true;
}
}
return false;
}

15. 游戏策略类Strategy

游戏策略类主要是负责制定斗地主小游戏的规则,和模拟机器人玩家出牌的策略。

游戏策略类创建:选择新建、c、c++、类名为Strategy,基类为Custom。

15.1 Strategy类头文件

游戏策略类Strategy头文件定义的多数是一些游戏的规则,和对机器人玩家的出牌策略,机器人玩家会根据自己的手牌来判断该出什么牌,是否抢地主等操作。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Strategy
{
public:
Strategy(Player* player, const Cards& cards); //只有机器人玩家才会调用该函数

//1.制定出牌策略
Cards makeStrategy();
//2.第一个出牌firstPlay(没有限制,想出啥出啥)--->机器人玩家使用的函数
Cards firstPlay();
//3.得到比指定牌型大的牌
Cards getGreaterCards(PlayHand type);
//4.能大过指定的牌时,判断是出牌还是放行,返回true是出牌。返回false是放行
bool whetherToBeat(Cards& cs);

//5.找出指定数量(count)的相同点数的牌(point),找出count张点数为point的牌
Cards findSamePointCards(Card::CardPoint point, int count); //满足指定点数、指定数量的牌
//6.找出所有点数数量为count的牌 ---> 得到一个多张扑克牌数组
QVector<Cards>findCardsByCount(int count); //满足指定数量的牌
//7.根据点数范围找牌
Cards getRangeCards(Card::CardPoint begin, Card::CardPoint end); //满足指定范围的牌
//8.按牌型找牌,并且指定要找的牌是否要大过指定的牌型
QVector<Cards>findCardType(PlayHand hand, bool beat);

//9.从指定的Cards对象中挑选出满足条件的顺子(allSeqRecord中每个元素都是一个容器,里面装着满足条件的顺子)
void pickSeqSingles(QVector<QVector<Cards>>&allSeqRecord, const QVector<Cards>&seqSingle, const Cards& cards);
//10.筛选最优的顺子的集合
QVector<Cards> pickOptimalSeqSingles();

private:
using function = Cards (Strategy::*)(Card::CardPoint point); //定义一个函数指针
struct CardInfo{
Card::CardPoint begin;
Card::CardPoint end;
int extra; //顺子或连对的数量
bool beat;
int number; //指定点数的牌的数量(1表示找单排,就是寻找顺子;2表示找对子,就是寻找连对)
int base; //最基础的顺子或者连对的数量(连对是3或顺子是5)
function getSeq; //函数指针对象,主要是用来生成基础连对和基础顺子
};
QVector<Cards>getCards(Card::CardPoint point, int number); //从point点数开始查找,找number张---找单、双、三
QVector<Cards>getTripleSingleOrPair(Card::CardPoint begin, PlayHand::HandType type); //起始点开始找;带1还是带对
QVector<Cards>getPlane(Card::CardPoint begin); //飞机不带牌
QVector<Cards>getPlane2SingleOr2Pair(Card::CardPoint begin, PlayHand::HandType type); //飞机带2单还是2对
//QVector<Cards>getSepPairOrSeqSingle(Card::CardPoint begin, int extra, bool beat); //连对或顺子
QVector<Cards>getSepPairOrSeqSingle(CardInfo &info); //连对或顺子
Cards getBaseSeqPair(Card::CardPoint point); //生成基础连对函数
Cards getBaseSeqSingle(Card::CardPoint point); //生成基础顺子函数
QVector<Cards>getBomb(Card::CardPoint begin); //炸弹

private:
Player* m_player;
Cards m_cards; //存放的是玩家手牌
};

15.2 Strategy类函数实现

这部分是对游戏策略类的头文件中定义的函数进行实现。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
Strategy::Strategy(Player *player, const Cards &cards):
m_player(player),
m_cards(cards)
{
}

//出牌策略,机器人玩家出牌时直接调用即可,是否出牌和出什么牌都已经处理好了
Cards Strategy::makeStrategy()
{
//得到出牌玩家对象以及打出的牌
Player* pendPlayer = m_player->getPendPlayer();
Cards pendCards = m_player->getPendCards();

//判断上次出牌的玩家是不是自己
if(pendPlayer == m_player || pendPlayer == nullptr){ //情况1是上次出牌是我自己,其余玩家没有出牌;情况2是刚开时,上一次出牌玩家就为空
//直接出牌(没有限制)
return firstPlay();
}else{
//如果不是自己想要,需要找出比出牌玩家点数大的牌
PlayHand type(pendCards); //构造一个playhand对象,对出的牌进行分析
Cards beatCards = getGreaterCards(type); //通过分析出类型,得到比上个玩家出队牌更大的牌
//找到了点数大的牌需要考虑是否出牌
bool shouldBeat = whetherToBeat(beatCards); //将找到更大的牌作为参数传入
if(shouldBeat){ //如果出牌,打出要出的牌
return beatCards;
}else{
return Cards(); //否则返回空
}
}
return Cards(); //返回空对象
}


//机器人玩家自由出牌函数
Cards Strategy::firstPlay()
{
//判断玩家手中是否只剩下单一的牌型
PlayHand hand(m_cards); //传入手牌进行分析
if(hand.getHandType() != PlayHand::Hand_Unknown){ //如果只剩单一牌型
return m_cards;
}
//不是单一的牌型
//判断玩家手中是否有顺子
QVector<Cards> optimalSeq = pickOptimalSeqSingles(); //获取顺子(筛选后较大的顺子)--->optimalSeq容器里可能有多个顺子
if(!optimalSeq.isEmpty()){ //如果不为空
//得到单牌的数量
int baseNum = findCardsByCount(1).size(); //没有剔除顺子前的单牌数量
//把得到的顺子的集合从玩家手中删除
Cards save = m_cards; //复制手牌
save.remove(optimalSeq); //删除顺子
int lastNum = Strategy(m_player,save).findCardsByCount(1).size(); //在去除顺子的手牌中找到单牌数量
if(baseNum > lastNum){ //这种情况是说明剔除顺子后,单牌数量更少了,是想要的结果
return optimalSeq[0]; //返回其中一个顺子打出
}
}

bool hasPlane, hasTriple, hasPair;
hasPair = hasTriple = hasPlane = false;
Cards backup = m_cards; //复制手牌

//有没有炸弹
QVector<Cards> bombArray = findCardType(PlayHand(PlayHand::Hand_Bomb, Card::Card_Begin,0),false);
backup.remove(bombArray); //剔除炸弹

//有没有飞机
QVector<Cards>planeArray = Strategy(m_player,backup).findCardType(PlayHand(PlayHand::Hand_Plane,Card::Card_Begin,0),false);
if(!planeArray.isEmpty()){
hasPlane = true;
backup.remove(planeArray); //剔除飞机
}

//有没有三张点数相同的牌
QVector<Cards>seqTripleArray = Strategy(m_player,backup).findCardType(PlayHand(PlayHand::Hand_Triple,Card::Card_Begin,0),false);
if(!seqTripleArray.isEmpty()){
hasTriple = true;
backup.remove(seqTripleArray); //剔除3张点数相同的牌
}

//有没有连对
QVector<Cards>seqPairArray = Strategy(m_player,backup).findCardType(PlayHand(PlayHand::Hand_Seq_Pair,Card::Card_Begin,0),false);
if(!seqPairArray.isEmpty()){
hasPair = true;
backup.remove(seqPairArray); //剔除连对
}

if(hasPair){ //找到了连对不为空,就返回最大连对
Cards maxPair;
for(int i=0; i<seqPairArray.size(); i++){ //seqPairArray的元素是类似于34567、345678、3456789....
if(seqPairArray[i].cardCount() > maxPair.cardCount()){ //比较找出最大连对
maxPair = seqPairArray[i];
}
}
return maxPair;
}

if(hasPlane){ //找到了飞机
//1.飞机带2个对
bool twoPairFond = false;
QVector<Cards> pairArray;
//搜索对子(3-10)
for(Card::CardPoint point=Card::Card_3; point<=Card::Card_10; point=Card::CardPoint(point+1)){
Cards pair = Strategy(m_player,backup).findSamePointCards(point,2); //将搜索的的对子放入pair
if(!pair.isEmpty()){ //如果不为空
pairArray.push_back(pair); //添加到pairArray
if(pairArray.size()==2){ //如果搜索到2对了,就直接退出循环
twoPairFond = true;
break;
}
}
}
if(twoPairFond){ //如果成功找到2个对
Cards tmp = planeArray[0]; //先使用最小的飞机,将找到的最小飞机复制给tmp
tmp.add(pairArray);
return tmp;
}else{ //没有找到2个对,就找2个单牌
//2.飞机带2个单牌
bool twoSingleFond = false;
QVector<Cards> singleArray;
for(Card::CardPoint point=Card::Card_3; point<=Card::Card_10; point=Card::CardPoint(point+1)){ //搜索对子(3-10)
if(backup.pointCount(point) == 1){ //先查看该点数的牌是否是只有1张,只有1张才进行添加(防止把一些对拆开)
Cards single = Strategy(m_player,backup).findSamePointCards(point,1); //将搜索的的单牌放入single
if(!single.isEmpty()){ //如果不为空
singleArray.push_back(single); //添加到pairArray
if(singleArray.size()==2){ //如果搜索到2对了,就直接退出循环
twoSingleFond = true;
break;
}
}
}
}
if(twoPairFond){
Cards tmp = planeArray[0]; //先使用最小的飞机,将找到的最小飞机复制给tmp
tmp.add(singleArray);
return tmp;
}else{ //如果2对和2单都没有找到满足条件的,就直接打出飞机不带
//3.直接是飞机
return planeArray[0];
}
}
}

if(hasTriple){ //找到了3张相同的牌
if(PlayHand(seqTripleArray[0]).getCardPoint()<Card::Card_A){ //如果找到的最小的3张相同牌是小于A的,才执行下面
for(Card::CardPoint point=Card::Card_3; point<=Card::Card_A; point=Card::CardPoint(point+1)){
int pointCount = backup.pointCount(point); //得到较小点数的牌数
if(pointCount == 1){ //如果是1张,就打出3带1
Cards single = Strategy(m_player, backup).findSamePointCards(point,1); //取出点数为point的单牌
Cards tmp = seqTripleArray[0]; //取出找的3张相同点数的牌集合中,取出最小的3张相同的牌
tmp.add(single); //将找到的单牌放入
return tmp;
}else if(pointCount==2){ //如果找到的较小点数的牌数是2
Cards pair = Strategy(m_player, backup).findSamePointCards(point,2); //取出点数为point的对子
Cards tmp = seqTripleArray[0]; //取出找的3张相同点数的牌集合中,取出最小的3张相同的牌
tmp.add(pair); //将找到的单牌放入
return tmp;
}
}
}
//如果找不到满足条件的单排和对子,就不带副牌
return seqTripleArray[0];
}
//如果上面的牌型都没有得到打出,就考虑打单牌或对牌
Player* nextPlayer = m_player->getNextPlayer();
//如果下一家的手牌数只有1张了,且和下一个玩家不是相同身份
if(nextPlayer->getCards().cardCount()==1 && m_player->getRole()!=nextPlayer->getRole()){
for(Card::CardPoint point = Card::CardPoint(Card::Card_End-1);
point>=Card::Card_3; point=Card::CardPoint(point-1)){ //从大到小遍历
int pointCount = backup.pointCount(point); //得到指定点数牌的数量
if(pointCount == 1){ //如果只有1张,就取出来打出
Cards single = Strategy(m_player,backup).findSamePointCards(point,1);
return single;
}else if(pointCount == 2){ //如果有2张牌就,取出来打出
Cards pair = Strategy(m_player,backup).findSamePointCards(point,2);
return pair;
}
}
}else{ //如果下一家的手牌数为1且和它是相同身份;如果下一家手牌数大于1且与它不是相同身份;如果下一家手牌数大于1且与它是相同身份
for(Card::CardPoint point = Card::Card_3; point<Card::Card_End; point=Card::CardPoint(point+1)){ //从小到大遍历
int pointCount = backup.pointCount(point); //得到指定点数牌的数量
if(pointCount == 1){ //如果只有1张,就取出来打出
Cards single = Strategy(m_player,backup).findSamePointCards(point,1);
return single;
}else if(pointCount == 2){ //如果有2张牌就,取出来打出
Cards pair = Strategy(m_player,backup).findSamePointCards(point,2);
return pair;
}
}
}
return Cards(); //上面所有情况呕没有打出牌,就说明牌打完了,直接返回空对象
}


//从自己手牌中找到能击败已出牌的牌
Cards Strategy::getGreaterCards(PlayHand type) //参数是要被击败出牌的类型
{
//1 出牌玩家和当前玩家不是一伙的
Player* pendPlayer = m_player->getPendPlayer();
//上一个出牌玩家出了牌,与它不是一伙,且出牌玩家的手牌已经小于3--->pendPlayer != nullptr是防止第一次出牌的情况(地主自由出)
if(pendPlayer != nullptr && pendPlayer->getRole()!=m_player->getRole() && pendPlayer->getCards().cardCount()<=3){
QVector<Cards> bombs = findCardsByCount(4); //通过传入参数4,找出手牌中的所有炸弹,且是从小到大排的
for(int i=0; i<bombs.size(); i++){ //从最小开始遍历炸弹比较
if(PlayHand(bombs[i]).canBeat(type)){ //如果我的炸弹类型比出的牌类型大,就会发挥true
return bombs[i]; //说明找到了击败已出牌的的最小炸弹,就直接返回
}
}
//如果没有返回,说明没有找到合适的炸弹,就搜索当前玩家有没有王炸
Cards sj = findSamePointCards(Card::Card_SJ,1);
Cards bj = findSamePointCards(Card::Card_BJ,1);
if(!sj.isEmpty() && !bj.isEmpty()){ //如果有王炸,就返回王炸
Cards jokers;
jokers << sj << bj;
return jokers;
}
}
//2. 当前玩家和下一个玩家不是一伙的
Player* nextPlayer = m_player->getNextPlayer(); //得到下一个玩家对象
//将玩家手中的顺子剔除出去,万不得已还是不拆除(这里的顺子是指较大的顺子)
Cards remain = m_cards;
remain.remove(Strategy(m_player,remain).pickOptimalSeqSingles()); //去除掉手牌中较大的顺子

//去除冗余的思路:就是将相同的代码放到匿名函数里面,然后通过可调用对象绑定器进行绑定,最后可得到一个可调用对象,要用到的时候,直接使用该可调用对象即可
auto beatCard = std::bind([=](Cards &cards){ //参入的参数是一些牌,返回的还是Cards类型
QVector<Cards>beatCardsArray = Strategy(m_player,cards).findCardType(type, true); //找出已出牌的类型,且要求比它更大一点(true)
if(!beatCardsArray.isEmpty()){ //如果找出的不为空,说明找到了比已出牌更大的类型牌
//如果我与下一个玩家不是相同身份,且下一个玩家的手牌小于等于2了,这时需要出更大的牌,防止下一个玩家能出牌
if(m_player->getRole()!=nextPlayer->getRole() && nextPlayer->getCards().cardCount()<=2){
return beatCardsArray.back(); //已经找出的牌型是从小到大排的,所以取出最大的牌返回
}else{ //下一个不是相同玩家但牌数小于等于2;下一个是不同玩家,但牌数大于2;下一个是相同玩家,但牌数大于2
return beatCardsArray.front(); //否则的话,就直接返回最小的能压制已出牌的相同牌型
}
}
return Cards(); //不满足上面,就返回空对象
}, std::placeholders::_1);
Cards cs;
if(!(cs=beatCard(remain)).isEmpty()){ //这是从剔除顺子的手牌中找
return cs; //如果得到的cs不为空,就返回给调用者
}else{ //如果从剔除顺子的手牌中没有找到,就从所有手牌中找
if(!(cs=beatCard(m_cards)).isEmpty()){
return cs; //如果得到的cs不为空,就返回给调用者
}
}
return Cards(); //没有找到合适出牌类型,就返回空对象
}


bool Strategy::whetherToBeat(Cards &cs) //参数是通过其它函数得到的一个更大的牌,或为空
{
//没有找到能够击败对方的牌,即cs为空
if(cs.isEmpty()){
return false;
}
//得到出牌玩家的对象
Player* pendPlayer = m_player->getPendPlayer(); //得到上次出牌的玩家身份
if(m_player->getRole() == pendPlayer->getRole()){ //如果身份相同,即农民
//手里的牌所剩无几并且是一个完整的牌型,就可以打
Cards left = m_cards; //先复制手牌
left.remove(cs); //从手牌移出要打的牌
if(PlayHand(left).getHandType()!=PlayHand::Hand_Unknown){ //如果手牌剩的类型不是不确定的(即就是可以一下子打完的那中牌类型)
return true;
}
//如果cs对象中的牌的最小点数是2,大小王就不出牌
Card::CardPoint basePoint = PlayHand(cs).getCardPoint(); //得出要打牌的点数
if(basePoint==Card::Card_2 || basePoint==Card::Card_SJ || basePoint==Card::Card_BJ){
return false; //如果过大就不出
}
}else{ //如果与出牌的玩家不是相同身份
PlayHand myHand(cs); //先判断要出牌的类型
//如果是三个2带1,或者带1对,不出牌(保存实力)
if((myHand.getHandType()==PlayHand::Hand_Triple_Single || myHand.getHandType()==PlayHand::Hand_Triple_Pair)
&& myHand.getCardPoint()==Card::Card_2){
return false;
}
//如果cs是对2,并且出牌玩家手中的牌数量大于等于10 && 自己的牌数量大于等于5,暂时放弃出牌
if(myHand.getHandType()==PlayHand::Hand_Pair && myHand.getCardPoint()==Card::Card_2 && pendPlayer->getCards().cardCount()>=10 && m_player->getCards().cardCount()>=5){
return false;
}
}
return true; //上面情况都不是,就可以出牌
}

Cards Strategy::findSamePointCards(Card::CardPoint point, int count) //指定点数和指定数量的牌
{
if(count<1 || count>4){ //如果要找的牌的数量小于1或大于4,都是不合法的,返回空对象
return Cards();
}
//大小王
if(point == Card::Card_SJ || point == Card::Card_BJ){
if(count>1){ //非法的
return Cards(); //返回空对象
}
Card card;
card.setPoint(point); //存放点数(大王或小王)
card.setSuit(Card::Suit_Begin); //存放花色
if(m_cards.contains(card)){ //如果该牌在m_cards是真的存在
Cards cards;
cards.add(card);
return cards; //就将该牌添加到cards并返回
}
return Cards(); //没有找到大小王就返回空对象
}
//不是大小王
int findCount = 0; //这是用来记录指定点数的牌的数量
Cards findCards;
for(int suit=Card::Suit_Begin+1; suit<Card::Suit_End; suit++){ //遍历花色
Card card; //创建一个card,记录的是指定的点数,和该遍历的花色
card.setPoint(point);
card.setSuit((Card::CardSuit)suit);
if(m_cards.contains(card)){ //如果该card在m_cards,说明有该张牌
findCount++;
findCards.add(card); //将该张牌添加到findCards
if(findCount == count){
return findCards; //如果指定点数的牌的数量到达了指定的点数,就返回这些牌
}
}
}
return Cards(); //如果遍历完所有的花色,都没有找到指定点数、指定数量的牌,就返回空对象
}

QVector<Cards> Strategy::findCardsByCount(int count) //指定数量的牌
{
if(count<1 || count>4){
return QVector<Cards>(); //如果指定的数量不合法,就直接返回空
}
QVector<Cards>cardsArray;
for(Card::CardPoint point=Card::Card_3; point<Card::Card_End; point=(Card::CardPoint)(point+1)){
if(m_cards.pointCount(point)==count){ //pointCount()函数是指定点数的牌数量
//如果该点数的牌数量和要求的牌数量相等,就取出该牌(通过上面刚写的函数)
Cards cs;
cs << findSamePointCards(point,count); //取出指定点数、指定数量的牌
cardsArray << cs; //最后返回的容器cardsArray中,一个cards存放的是相同点数的牌
}
}
return cardsArray; //然后容器,里面存的是指定数量的牌(可能有多中不同点数的牌)
}

Cards Strategy::getRangeCards(Card::CardPoint begin, Card::CardPoint end) //指定范围的所有牌
{
Cards rangeCards;
for(Card::CardPoint point=begin; point<end; point=(Card::CardPoint)(point+1)){ //遍历这个范围点数的牌
int count = m_cards.pointCount(point); //先获得该点数的牌数量
Cards cs = findSamePointCards(point, count); //通过点数、牌数量得到这些牌
rangeCards << cs;
}
return rangeCards; //这里面存的都是在指定范围的牌
}

QVector<Cards> Strategy::findCardType(PlayHand hand, bool beat) //hand是要求牌的类型,beat是否需要点数更大
{
PlayHand::HandType type = hand.getHandType(); //取出要求牌的类型
Card::CardPoint point = hand.getCardPoint(); //取出要求牌的点数
int extra = hand.getExtra();

//确定起始点数(如果beat为true,就在原来基础上+1;为false,就直接从最小3开始找)
Card::CardPoint beginPoint = beat?Card::CardPoint(point+1):Card::Card_3;

switch(type){
case PlayHand::Hand_Single:
return getCards(beginPoint, 1); //单牌
case PlayHand::Hand_Pair:
return getCards(beginPoint, 2); //对
case PlayHand::Hand_Triple:
return getCards(beginPoint, 3); //三个一样
case PlayHand::Hand_Triple_Single:
return getTripleSingleOrPair(beginPoint, PlayHand::Hand_Single); //3带1
case PlayHand::Hand_Triple_Pair:
return getTripleSingleOrPair(beginPoint, PlayHand::Hand_Pair); //3带一对
case PlayHand::Hand_Plane:
return getPlane(beginPoint);
case::PlayHand::Hand_Plane_Two_Single:
return getPlane2SingleOr2Pair(beginPoint, PlayHand::Hand_Single);
case::PlayHand::Hand_Plane_Two_Pair:
return getPlane2SingleOr2Pair(beginPoint, PlayHand::Hand_Pair);
case PlayHand::Hand_Seq_Pair: //连对
{
CardInfo info;
info.begin = beginPoint;
info.end = Card::Card_Q; //最大点数到Q即可
info.number = 2;
info.base = 3; //基础连对至少是3对
info.extra = extra;
info.beat = beat;
info.getSeq = &Strategy::getBaseSeqPair;
return getSepPairOrSeqSingle(info);
}
case PlayHand::Hand_Seq_Single: //顺子
{
CardInfo info;
info.begin = beginPoint;
info.end = Card::Card_10; //最大点数到10即可
info.number = 1;
info.base = 5; //基础顺子至少是5个
info.extra = extra;
info.beat = beat;
info.getSeq = &Strategy::getBaseSeqSingle;
return getSepPairOrSeqSingle(info);
}
case PlayHand::Hand_Bomb: //炸弹
return getBomb(beginPoint);
default:
return QVector<Cards>(); //如果都不是,就返回空对象
}
}

//从指定的Cards对象中挑选出顺子(参1和参2都是传出参数,参3是传入参数)
void Strategy::pickSeqSingles(QVector<QVector<Cards> > &allSeqRecord, const QVector<Cards> &seqSingle, const Cards &cards)
{
//1.得到传入牌cards的所有顺子的组合
QVector<Cards> allSeq = Strategy(m_player,cards).findCardType(PlayHand(PlayHand::Hand_Seq_Single,Card::Card_Begin,0),false);
if(allSeq.isEmpty()){
//如果为空了,就结束递归,将满足条件的顺子传入allSeqRecord
allSeqRecord << seqSingle;
}else{ //2.对顺子进行筛选
Cards saveCards = cards; //先复制手牌,备份使用,马上进入for循环了
//遍历得到的所有的顺子
for(int i=0; i<allSeq.size(); i++){
Cards aScheme = allSeq.at(i); //将顺子取出
Cards temp = saveCards;
temp.remove(aScheme); //将顺子从用户手中剔除

QVector<Cards> seqArray = seqSingle;
seqArray << aScheme; //将得到的顺子放入seqArray
//检测还没有其它的顺子
//seqArray:存储一轮for循环中多轮递归得到的所有的可用的顺子
//allSeqRecord:存储多轮for循环中多轮递归得到的所有的可用的顺子
//temp:是剔除顺子的手牌
pickSeqSingles(allSeqRecord,seqArray,temp);
//allSeqRecord里面的存的,比如说第1个元素存的是剔除34567后,手牌中还满足的顺子(包括34567);第2个元素是剔除345678后还满足的顺子(包括345678)
}
}
}

//筛选最优的顺子
QVector<Cards> Strategy::pickOptimalSeqSingles()
{
QVector<QVector<Cards>>seqRecord;
QVector<Cards> seqSingles;
//找顺子之前,先把一些更重要的牌型剔除,防止被拆
Cards save = m_cards; //先复制手牌
save.remove(findCardsByCount(4)); //剔除炸弹
save.remove(findCardsByCount(3)); //剔除一些3带1或飞机类型的牌
pickSeqSingles(seqRecord, seqSingles, save); //对一些重要的牌进行剔除后,基于剩下的牌save来进行查找顺子
if(seqRecord.isEmpty()){ //如果找出的集合为空,说明没有找到顺子,就返回空
return QVector<Cards>();
}
//遍历容器
QMap<int,int>seqMarks; //存储对应关系的
for(int i=0; i<seqRecord.size();i++){
Cards backupCards = m_cards; //先复制手牌到backupCards
QVector<Cards>seqArray = seqRecord[i]; //复制此刻要处理的容器(里面都是顺子)
backupCards.remove(seqArray); //从复制的手牌backupCards中移除此刻容器里面的所有顺子

//判断剩下的单牌数量,数量越少,顺子的组合就越合理
QVector<Cards> singleArray = Strategy(m_player, backupCards).findCardsByCount(1); //将移除顺子容器后的手牌传入
//因为传入的数量是1,所以返回的singleArray中每个元素cards里面都是存的不同点数的牌,但每个点数的牌数量都为1
CardList cardList;
for(int j=0; j<singleArray.size(); j++){
cardList << singleArray[j].toCardList(); //将所有的单牌取出转换后存入到cardList
}
//这里选择点数相对较大一点的顺子,则在两种组合中留下的单牌数量相同的情况下,单牌点数就可能小。所以下面就选择单牌点数小的组合来返回
int mark = 0;
for(int j=0; j<cardList.size(); j++){
mark += cardList[j].point() + 15; //加15的原因,是防止两组组合留下的单牌数不一样情况下,类似3、4和k这两种,应该选择k对应的组合
}
seqMarks.insert(i,mark); //将对应的mark值和其最大容器组合下标i的对应关系存到seqMarks
}
//遍历map
int value = 0;
int comMark = 1000; //先初始化一个较大值
auto it = seqMarks.constBegin(); //常量迭代器
for(; it!=seqMarks.constEnd(); it++){
if(it.value() < comMark){ //寻找最小的key值对应的value
comMark = it.value();
value = it.key();
}
}
return seqRecord[value]; //返回最优的顺子(较大的顺子),是QVector<Cards>类型
}

QVector<Cards> Strategy::getCards(Card::CardPoint point, int number) //起点点数point,number张
{
QVector<Cards> findCardsArray;
for(Card::CardPoint pt=point; pt<Card::Card_End; pt=(Card::CardPoint)(pt+1)){
//目的是尽量不拆分别的牌型
if(m_cards.pointCount(pt)==number){ //这里是确保找的点数的牌数量与想找的牌数量一样,就执行下面,防止本来有个炸弹,但因为找但牌,就拆开了
Cards cs = findSamePointCards(pt, number);
findCardsArray << cs;
}
}
return findCardsArray;
}

QVector<Cards> Strategy::getTripleSingleOrPair(Card::CardPoint begin, PlayHand::HandType type)
{
//找到点数相同的三张牌
QVector<Cards>findCardArray = getCards(begin,3); //找出有3张相同点数的一些牌
if(!findCardArray.isEmpty()){
//将找到的牌从用户手中删除
Cards remainCards = m_cards; //复制用户手牌
remainCards.remove(findCardArray); //删除3张相同的牌
//在剩余牌中继续搜索(搜索单牌或成对的牌)
Strategy st(m_player, remainCards);
QVector<Cards>cardsArray = st.findCardType(PlayHand(type,Card::Card_Begin,0),false);
if(!cardsArray.isEmpty()){
//将找到的牌和三张点数相同的牌进行组合
for(int i=0; i<findCardArray.size();i++){
findCardArray[i].add(cardsArray.at(i));
}
}else{
findCardArray.clear();
}
}
//将最终结果返回给函数调用者
return findCardArray;
}

QVector<Cards> Strategy::getPlane(Card::CardPoint begin) //飞机不带牌
{
QVector<Cards> findCardArray;
for(Card::CardPoint point=begin; point<=Card::Card_K; point=(Card::CardPoint)(point+1)){
//根据点数和数量进行搜索
Cards prevCards = findSamePointCards(point, 3); //找出飞机小
Cards nextCards = findSamePointCards((Card::CardPoint)(point+1),3); //找出飞机大
if(!prevCards.isEmpty() && !nextCards.isEmpty()){
Cards tmp;
tmp << prevCards << nextCards; //将飞机小和大存到一个Cards里面,再存入到findCardArray
findCardArray << tmp;
}
}
return findCardArray;
}

QVector<Cards> Strategy::getPlane2SingleOr2Pair(Card::CardPoint begin, PlayHand::HandType type) //飞机带2单还是2对
{
//找飞机
QVector<Cards>findCardArray = getPlane(begin); //找出有3张相同点数的一些牌
if(!findCardArray.isEmpty()){
//将找到的牌从用户手中删除
Cards remainCards = m_cards; //复制用户手牌
remainCards.remove(findCardArray); //删除3张相同的牌
//在剩余牌中继续搜索(搜索单牌或成对的牌)
Strategy st(m_player, remainCards);
QVector<Cards>cardsArray = st.findCardType(PlayHand(type, Card::Card_Begin, 0), false);
if(cardsArray.size()>=2){
//将找到的牌就和飞机进行组合
for(int i=0; i<findCardArray.size();i++){
Cards tmp;
tmp << cardsArray[0] << cardsArray[1];
findCardArray[i].add(tmp);
}
}else{
findCardArray.clear(); //没有找到,说明不满足这种飞机,就直接清空
}
}
//将最终结果返回给函数调用者(可能为空)
return findCardArray;
}
//连对或顺子(begin是起始点;extra是要找的牌数;beat指是否比原基础上至少大1点)
QVector<Cards> Strategy::getSepPairOrSeqSingle(CardInfo &info) //连对和顺子提供的数据是不一样的
{
QVector<Cards>findCardsArray;
if(info.beat){ //为true,表示要找能压制对方的牌
//最少3个,最大A(连对)
for(Card::CardPoint point=info.begin; point<=info.end; point=(Card::CardPoint)(point+1)){
bool found = true; //记录是否查找成功
Cards seqCards;
for(int i=0; i<info.extra; i++){
//基于点数和数量进行牌的搜索
Cards cards = findSamePointCards((Card::CardPoint)(point+i),info.number); //连续的2张2张的找连对
if(cards.isEmpty() || (point+info.extra>=Card::Card_2)){ //这种情况就是没有找到
found = false;
seqCards.clear();
break; //说明以此刻的point为起点,遍历extra个是找不到的,直接跳出循环,继续下一个point为起点开始查找
}else{
seqCards << cards; //如果找到了就加入seqCards
}
}
if(found){ //如果以此刻的point为起点找到了满足条件的,就没有必要继续找了,因为压制对方的最小的连对已经找到
findCardsArray << seqCards;
return findCardsArray; //直接返回即可
}
}
}else{ //bet为false,意味着要找的连对对点数和数量是没有要求的(不受extra约束)
for(Card::CardPoint point=info.begin; point<=info.end; point=(Card::CardPoint)(point+1)){
//将找到的这个基础连对存储起来
Cards baseSeq = (this->*info.getSeq)(point); //使用函数指针,得到一个基础连对或基础顺子
if(baseSeq.isEmpty()){ //如果是空,就不执行下面了,重新循环
continue;
}
findCardsArray << baseSeq; //将基础连对存储到最终容器中

int followed = info.base; //因为之前已经找到了3对,接下来是再其基础上找连续对子
Cards alreadyFollowedCards; //存储后序找到的满足条件的连对
while(true){
Card::CardPoint followedPoint = Card::CardPoint(point+followed); //新的起始点数
//判断是否超出了上限
if(followedPoint >= Card::Card_2){ //如果点数比2大,是不合理的,直接退出循环
break;
}
Cards follwedCards = findSamePointCards(followedPoint, info.number); //如果点数合理,就寻找该点数的对子
if(follwedCards.isEmpty()){ //为空表示没有找到,退出循环(说明以此刻的point为起点的后序对子段了或没有了)
break;
}else{
//比如说345是基础连对,在此基础上,3456、34567都会作为一个独立的cards存储到最终容器中,当没有8连对时,以345为基础的也结束查找了
alreadyFollowedCards << follwedCards; //在后面基础每找到一对,就将其存入到alreadyFollowedCards
Cards newSeq = baseSeq; //通过拷贝赋值得到newSeq,它里面存的是此刻的基础对子
newSeq << alreadyFollowedCards; //将这一轮找到的对子加入到基础对子,又得到一种新的连对
findCardsArray << newSeq; //将这一轮得到的新连对存入到最终容器中
followed++; //++操作,在此刻的基础连对上,继续找下一个对子
}
}
}
}
return findCardsArray; //如果遍历完了还没有找到,返回的就是一个空对象,否则里面就有很多连对cards对象
}

Cards Strategy::getBaseSeqPair(Card::CardPoint point) //生成基础连对函数(参数是起始点)
{
Cards cards0 = findSamePointCards(point, 2);
Cards cards1 = findSamePointCards((Card::CardPoint)(point+1),2);
Cards cards2 = findSamePointCards((Card::CardPoint)(point+2),2);
Cards baseSeq;
if(!cards0.isEmpty() && !cards1.isEmpty() && !cards2.isEmpty()){
baseSeq << cards0 << cards1 << cards2;
}
return baseSeq;
}

Cards Strategy::getBaseSeqSingle(Card::CardPoint point) //生成基础顺子函数
{
Cards cards0 = findSamePointCards(point, 1);
Cards cards1 = findSamePointCards((Card::CardPoint)(point+1),1);
Cards cards2 = findSamePointCards((Card::CardPoint)(point+2),1);
Cards cards3 = findSamePointCards((Card::CardPoint)(point+3),1);
Cards cards4 = findSamePointCards((Card::CardPoint)(point+4),1);
Cards baseSeq;
if(!cards0.isEmpty() && !cards1.isEmpty() && !cards2.isEmpty() && !cards3.isEmpty() && !cards4.isEmpty()){
baseSeq << cards0 << cards1 << cards2 << cards3 << cards4;
}
return baseSeq;
}

QVector<Cards> Strategy::getBomb(Card::CardPoint begin) //找炸弹
{
QVector<Cards> findcardsArray;
for(Card::CardPoint point=begin; point<Card::Card_End; point=(Card::CardPoint)(point+1)){
Cards cs = findSamePointCards(point, 4);
if(!cs.isEmpty()){
findcardsArray << cs;
}
}
return findcardsArray;
}