斗地主残局解析项目
项目工程化
由于这是一个项目,所以我们需要按照标准的项目工程化来进行设计。
分析大体框架
1. 如何读取牌
如何进行文件读取,可以参看我这篇博客:C语言笔记:文件操作
//创建两个数组进行存储自己和对手的牌
int a[MAX_N + 5] = {0};
int b[MAX_N + 5] = {0};
void read(FILE *f, int *arr) {
int x;
while (fscanf(f, "%d", &x) != EOF) {
//当x等于0时表示读取完成当前一个人的手牌
if (x == 0) break;
arr[x] += 1;
}
return ;
}
void get_input_data() {
//通过fopen以只读方式打开文件
FILE *f = fopen("input", "r");
//通过f文件指针将手牌读取进自己的对手的手牌
read(f, a);
read(f, b);
//打开了文件,需要进行关闭
fclose(f);
return ;
}
通过文件读取的方式进行读取手牌。
2.通过目前手牌来获取可出牌牌型
这里用到的知识:条件编译
先进行头文件进行声明,我们需要的类:
这里的头文件,那么就应该放在include文件夹中。
在这里可能会看到很多不同的方法,在文章的下面,我会分别讲述里面对应函数的作用。
#ifndef _PAIXIN_H
#define _PAIXIN_H
#include <iostream>
#include <vector>
using namespace std;
#define MAX_N 18
//牌型作为派生类
class Card_Type {
public :
static vector<Card_Type *> get_PaiXin(int *);
virtual ostream& output() = 0;
virtual ~Card_Type() = default;
};
//子类单张
class OneCard : public Card_Type {
public :
OneCard(int x);
ostream& output() override;
static vector<Card_Type *> get(int *);
private :
//x表示什么单牌
int x;
};
//子类对子
class TwoCard : public Card_Type {
public :
TwoCard(int x);
ostream& output() override;
static vector<Card_Type *> get(int *);
private :
//x表示什么对子
int x;
};
//子类三带
class ThreeToCard : public Card_Type {
public :
ThreeToCard(int x, Card_Type *p);
ostream& output() override;
static vector<Card_Type *> get(int *);
private :
//x表示3带是什么
//p指向带的牌型
int x;
Card_Type *p;
};
//子类飞机
class AirPlane : public Card_Type {
public :
AirPlane(int x, int y);
AirPlane(int x, int y, Card_Type *, Card_Type *);
static vector<Card_Type *> To_One(int *, int, int);
static vector<Card_Type *> To_Two(int *, int, int);
static vector<Card_Type *> get(int *);
private :
//飞机的两个三带是什么
//p1和p2指向带的什么
int x, y;
Card_Type *p1, *p2;
};
//子类炸弹
class Bomb : public Card_Type {
public :
Bomb(int x);
ostream& output() override;
static vector<Card_Type *> get(int *);
private :
//x表示是什么炸弹
int x;
};
//子类四带二
class BombToCard : public Card_Type {
public :
BombToCard(int x);
BombToCard(int x, Card_Type *, Card_Type *);
static vector<Card_Type *> get(int *);
static vector<Card_Type *> To_One(int *, int);
static vector<Card_Type *> To_Two(int *, int);
ostream& output() override;
private :
//x表示当前4带的牌是多少
int x;
//p1,p2用来指向带的牌型
Card_Type *p1, *p2;
};
//子类顺子
class ShunZi : public Card_Type {
public :
ShunZi(int , int );
static vector<Card_Type *> get(int *);
ostream& output() override;
private :
//x表示起始牌
//len表示顺子长度
int x, len;
};
//子类火箭
class Rocket : public Card_Type {
public :
Rocket();
static vector<Card_Type *> get(int *);
ostream& output() override;
};
//子类不要
class Pass : public Card_Type {
public :
Pass();
static vector<Card_Type *> get(int *);
ostream& output() override;
};
//子类连对
class DoubleShunZi : public Card_Type {
public :
DoubleShunZi(int , int);
static vector<Card_Type *> get(int *);
ostream &output() override;
private :
//x表示连对的起始位置
//len表示连对的长度
int x, len;
};
#endif
声明:
1.创建基类和派生类
创建基类Card_Type,然后通过上图进行创建派生类去继承基类(Card_Type)。
2.声明获取牌型的函数
每个类中(派生类和基类中)都有一个类方法,用来获取牌型。
基类中名称:vector<Card_Type *> get_PaiXin(int *),该函数的作用就是通过之前获得的数组,来获取当前手牌所有可出牌的策略,而返回值是一个vector容器,存储的是基类指针类型。我们可以知道父类的指针可以指向子类的对象的地址的。
派生类中的名称:vector<Card_Type *> get(int *),该函数的作用就是通过数组来获取每个派生类对应的牌型,及对应牌型可以出牌的策略,那么基类中的get_PaiXin函数就是用来总和这些派生类get函数获取的出牌策略。
3.我们获取了牌型,那么就需要测试
在基类中声明一个纯虚函数:void output() ,用来对每种牌型的出牌方式输出。
定义:
在声明完成这些函数后我们就需要去定义这些函数:
那么定义的源文件就应放在src文件夹中:get_PaiXin.cc
下面对于get_PaiXin.cc源文件进行写代码,来实现刚刚我们声明的函数:
分别去实现基类和派生类中的刚刚声明的方法。
name数组用来存储每种牌的名字,分别从小到大3-JOKER。
string name[MAX_N + 5] = {
"", "", "", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "Ace", "2", "joker", "JOKER"
};
1.不要
vector<Card_Type *> Pass::get(int *) {
vector<Card_Type *> temp;
temp.push_back(new Pass());
return temp;
}
ostream &Pass::output() {
cout << "Pass";
return cout;
}
get:
直接通过new创建一个Pass对象,放入vector容器进行返回.
2.获取单牌
ostream& OneCard::output() {
cout << "DanZhang "<< name[x] ;
return cout;
}
vector<Card_Type *> OneCard::get(int *arr) {
vector<Card_Type *> temp;
for (int i = 3; i < MAX_N; i++) {
if (arr[i] == 0) continue;
temp.push_back(new OneCard(i));
}
return temp;
}
get:
去判断当前数组3-17,是否有牌,如果有那么就有这个位置对应的单牌出牌方式,通过new关键字对当前对象获取一块内存区域(这里调用的是有One_Card的有参构造),就需要声明这个有参构造并且定义.
声明,在PaiXin.h的OneCard类中
OneCard(int x);
定义,在src种创建一个源文件contructor_PaiXin.cc,这个文件用来存放对构造函数的定义.
OneCard::OneCard(int x) : x(x) {}
然后进行push_back到vector容器中。那么这里用到了一个参数,就需要对One_Card类中创建一个成员变量,用来存储当前单牌类型的对应大小(int x)
3.获取对子
vector<Card_Type *> TwoCard::get(int *arr) {
vector<Card_Type *> temp;
for (int i = 3; i < MAX_N; i++) {
if (arr[i] < 2) continue;
temp.push_back(new TwoCard(i));
}
return temp;
}
ostream &TwoCard::output() {
cout << "DuiZi " << name[x];
return cout;
}
get:
和单排获取方式一样,不过判断时,需要判断当前位置的牌量是否大于等于2。同理也要进行声明和定义构造函数;
TwoCard(int x);
TwoCard::TwoCard(int x) : x(x) {}
4.获取三带
vector<Card_Type *> ThreeToCard::get(int *arr) {
vector<Card_Type *> temp;
for (int i = 3; i < MAX_N; i++) {
if (arr[i] < 3) continue;
//三不带
temp.push_back(new ThreeToCard(i, new Pass()));
for (int j = 3; j < MAX_N; j++) {
if (!arr[j] || j == i) continue;
//三带一
temp.push_back(new ThreeToCard(i, new OneCard(j)));
//三带二
if (arr[j] >= 2) temp.push_back(new ThreeToCard(i, new TwoCard(j)));
}
}
return temp;
}
ostream &ThreeToCard::output() {
cout << "Three " << name[x] << " Dai ";
this->p->output();
return cout;
}
get:
这里由于是三带,就需要多一种牌型,那么在ThreeToCard类中需要多一个成员变量,Card_Type * p,这个指针用来指向带的牌型;
对于三张牌,就判断当前位置的牌量是否大于等于3,那么3不带就是带Pass,那么先将这种策略放入vector容器;
这里会用到有参构造,那么我们就需要在声明中声明这种形式对应的有参构造函数.
ThreeToCard(int x, Card_Type *p);
ThreeToCard::ThreeToCard(int x, Card_Type *p) : x(x), p(p) {}
然后在循环,通过判断当前位置是否有牌并且和3张牌的牌不同的牌,如果有并且和三张牌的牌型不同,就可以形成3带1的牌型,那么就new一个对象,放入容器中,然后在进行判断当前牌型是否大于两张,大于那么就可以形成3带2的牌型,那么就new一个对象,放入容器中.
5.获取飞机
在我写博客时我才发现,没有实现完善这个功能,飞机不止只有两个3连续的3带,可以连续很多个,在后续我会把完善后的发出来.
vector<Card_Type *> AirPlane::To_One(int *arr, int x, int y) {
vector<Card_Type *> temp;
for (int i = 3; i < MAX_N; i++) {
if (!arr[i]) continue;
arr[i] -= 1;
for (int j = i; j < MAX_N; j++) {
if (!arr[j]) continue;
temp.push_back(new AirPlane(x, y, new OneCard(i), new OneCard(j)));
}
arr[i] += 1;
}
return temp;
}
vector<Card_Type *> AirPlane::To_Two(int *arr, int x, int y) {
vector<Card_Type *> temp;
for (int i = 3; i <= 15; i++) {
if (arr[i] < 2) continue;
arr[i] -= 2;
for (int j = i; j <= 15; j++) {
if (arr[j] < 2) continue;
temp.push_back(new AirPlane(x, y, new TwoCard(i), new TwoCard(j)));
}
arr[i] += 2;
}
return temp;
}
vector<Card_Type *> AirPlane::get(int *arr) {
vector<Card_Type *> temp;
int flag = 1;
for (int i = 3; i <= 14; i++) {
if (arr[i] < 3) continue;
if (i + 1 > 14 || arr[i + 1] < 3) continue;
arr[i] -= 3;
arr[i + 1] -= 3;
//飞机不带的情况
temp.push_back(new AirPlane(i, i + 1, new Pass(), new Pass()));
//飞机带俩单
vector<Card_Type *> one = To_One(arr, i, i + 1);
//飞机带俩对
vector<Card_Type *> two = To_Two(arr, i, i + 1);
temp.insert(temp.end(), one.begin(), one.end());
temp.insert(temp.end(), two.begin(), two.end());
arr[i] += 3;
arr[i + 1] += 3;
}
return temp;
}
ostream &AirPlane::output() {
cout << "Three " << name[x] << " And Three " << name[y] << " Dai ";
p1->output();
cout << " And ";
p2->output();
return cout;
}
get:
获取方式和三带差不多,判断当前循环位置的牌型牌量是否大于三,如果大于三再判断,他接着的牌型的牌量是否大于三,如果大于三他就可以形成飞机的牌型,由于他有需要两个变量进行存储对应的两个牌型,就需要两个成员变量(int x,int y)用来存储对应的牌.而带的牌就需要两个(Card_Type *)指针类型来指向带的牌型,分别是p1和p2
然后设计带的牌:
第一种不带:
那么带的牌就是两个Pass.
第二种第三种带两个单牌和两个对子:
我从新声明和定义了一个类方法用来获取这种牌型,那么就需要对于申明中进行添加,对于具体逻辑我就不做解释了,可以看代码定义处,函数分别是:
vector<Card_Type *> AirPlane::To_One(int *arr, int x, int y){}
vector<Card_Type *> AirPlane::To_Two(int *arr, int x, int y){}
对于为什么arr数组为什么分别要减-3后面又+3,这里的作用就是表示当前这6张牌被用来当作飞机了,不能被使用状态和回溯表示未使用状态.
分别把这3种出牌类型放入到容器中并返回.
6.获取炸弹
vector<Card_Type *> Bomb::get(int *arr) {
vector<Card_Type *> temp;
for (int i = 3; i < MAX_N; i++) {
if (arr[i] != 4) continue;
temp.push_back(new Bomb(i));
}
return temp;
}
ostream &Bomb::output() {
cout << "ZhaDan " << name[x];
return cout;
}
get:
同单牌
7.4带2
vector<Card_Type *> BombToCard::To_One(int *arr, int x) {
vector<Card_Type *> temp;
for (int i = 3; i < MAX_N; i++) {
if (!arr[i]) continue;
arr[i] -= 1;
for (int j = i; j < MAX_N; j++) {
if (!arr[j]) continue;
temp.push_back(new BombToCard(x, new OneCard(i), new OneCard(j)));
}
arr[i] += 1;
}
return temp;
}
vector<Card_Type *> BombToCard::To_Two(int *arr, int x) {
vector<Card_Type *> temp;
for (int i = 3; i < MAX_N; i++) {
if (arr[i] < 2) continue;
arr[i] -= 2;
for (int j = i; j < MAX_N; j++) {
if (arr[j] < 2) continue;
temp.push_back(new BombToCard(x, new TwoCard(i), new TwoCard(j)));
}
arr[i] += 2;
}
return temp;
}
vector<Card_Type *> BombToCard::get(int *arr) {
vector<Card_Type *> temp;
for (int i = 3; i < MAX_N; i++) {
if (arr[i] != 4) continue;
//对数组进行处理当前牌是使用状态
arr[i] -= 4;
//4带两张单牌
vector<Card_Type *> one = To_One(arr, i);
//4带两个对子
vector<Card_Type *> two = To_Two(arr, i);
temp.insert(temp.end(), one.begin(), one.end());
temp.insert(temp.end(), two.begin(), two.end());
//归还,表示当前牌是未使用状态
arr[i] += 4;
}
return temp;
}
ostream &BombToCard::output() {
cout << "Four " << name[x] << " Dai ";
p1->output();
cout << " And ";
p2->output();
return cout;
}
get:
处理方式同飞机
8.王炸
vector<Card_Type *> Rocket::get(int *arr) {
vector<Card_Type *> temp;
if (arr[16] && arr[17]) temp.push_back(new Rocket());
return temp;
}
ostream &Rocket::output() {
cout << "Rocket!!";
return cout;
}
get:代码可见,逻辑非常简单
9.顺子
vector<Card_Type *> ShunZi::get(int *arr) {
vector<Card_Type *> temp;
//顺子最小长度为5
//最大长度3-A 为12
for (int l = 5; l <= 12; l++) {
//从最小位置开始枚举
//顺子最大的初始位置为A - 目前枚举长度 + 1
for (int i = 3, I = 14 - l + 1; i <= I; i++) {
int flag = 1;
//枚举当前长度每个位置是否有牌
//如果有位置没牌就无法组成顺子
for (int j = i; j < i + l; j++) {
if (arr[j]) continue;
flag = 0;
break;
}
if (flag == 1) temp.push_back(new ShunZi(i, l));
}
}
return temp;
}
ostream &ShunZi::output() {
cout << "ShunZi ";
for (int i = x; i < x + len; i++) {
i != x && cout << " ";
cout << name[i];
}
return cout;
}
get:
通过循环枚举,顺子最小长度为5,最大长度为12(3-Ace)
然后枚举起始位置从3到Ace减去当前枚举长度在+1
定义一个标记,表示当前枚举起始位置和长度是否有顺子
在通过循环,去判断当前枚举的起始位置到枚举位置加上枚举长度-1的位置是否有牌.
如果有一个位置没有牌,那么就使标记表示没有顺子.
最后通过判断标记来是否创建顺子并放入到容器中
最后返回容器.
10.连对
vector<Card_Type *> DoubleShunZi::get(int *arr) {
vector<Card_Type *> temp;
for (int l = 3; l <= 12; l++) {
for (int i = 3, I = 14 - l + 1; i <= I; i++) {
int flag = 1;
for (int j = i; j < i + l; j++) {
if (arr[j] >= 2) continue;
flag = 0;
break;
}
if (flag) temp.push_back(new DoubleShunZi(i, l));
}
}
return temp;
}
ostream &DoubleShunZi::output() {
for (int i = x; i < x + len; i++) {
i != x && cout << " ";
cout << name[i] << name[i];
}
return cout;
}
get:
获取方式同顺子.
11.牌型中的类方法:
vector<Card_Type *> Card_Type::get_PaiXin(int *arr) {
vector<Card_Type *> temp;
vector<Card_Type *> Pass = Pass::get(arr);
vector<Card_Type *> One = OneCard::get(arr);
vector<Card_Type *> Two = TwoCard::get(arr);
vector<Card_Type *> Three = ThreeToCard::get(arr);
vector<Card_Type *> sz = ShunZi::get(arr);
vector<Card_Type *> ZhaDan = Bomb::get(arr);
vector<Card_Type *> WangZha = Rocket::get(arr);
vector<Card_Type *> Feiji = AirPlane::get(arr);
vector<Card_Type *> LianDui = DoubleShunZi::get(arr);
vector<Card_Type *> FourtoCard = BombToCard::get(arr);
for (auto x : One) temp.push_back(x);
for (auto x : Two) temp.push_back(x);
for (auto x : Three) temp.push_back(x);
for (auto x : sz) temp.push_back(x);
for (auto x : ZhaDan) temp.push_back(x);
for (auto x : WangZha) temp.push_back(x);
for (auto x : Feiji) temp.push_back(x);
for (auto x : LianDui) temp.push_back(x);
for (auto x : FourtoCard) temp.push_back(x);
for (auto x : Pass) temp.push_back(x);
return temp;
}
get_PaiXin:
通过每个派生类的get方法,获取到每种出牌策略,并放入总和放到容器中,最后返回.
测试获取出牌策略:'
在project文件夹路径下创建一个test.cpp
用来测试获取牌型的逻辑是否正确
#include <iostream>
#include <PaiXin.h>
#include <vector>
using namespace std;
int a[MAX_N + 5] = {0};
int b[MAX_N + 5] = {0};
void read(FILE *f, int *arr) {
int x;
while (fscanf(f, "%d", &x) != EOF) {
if (x == 0) break;
arr[x] += 1;
}
return ;
}
void get_input_data() {
FILE *f = fopen("input", "r");
read(f, a);
read(f, b);
fclose(f);
return ;
}
int main() {
get_input_data();
vector<Card_Type *> temp1 = Card_Type::get_PaiXin(a) ;
vector<Card_Type *> temp2 = Card_Type::get_PaiXin(b);
//对于这个方法,我在Card_Type 类中写了一个类方法,用来打印当前获取的手牌
//具体代码可在我源文件中
Card_Type::output_arr(a);
Card_Type::output_arr(b);
for (auto x : temp1) x->output() << endl;
cout << "======================" << endl;
for (auto x : temp2) x->output() << endl;
return 0;
}
通过make命令(博客待更新)执行,结果我就不展示了,太长了.
3.通过上家牌型获取到可以出牌策略
①.分析每种牌型对应的管牌方式
②.设计
设计如何判断牌型:
这里由于需要获取到上家出牌牌型,那么我设计的方式是在基类中定义一个枚举类型的成员变量:
先申明枚举类型,这是放在include/PaiXin.h文件中的
enum class PaiXin_Type {
OneCard_Type,
TwoCard_Type,
ThreeToCard_Type,
ShunZi_Type,
AirPlane_Type,
Bomb_Type,
BombToCard_Type,
Rocket_Type,
DoubleShunZi_Type,
Pass_Type
};
并在Card_Type类中创建一个成员属性
PaiXin_Type type;
这个用来表示当前对象表示的牌型.
那么有了这个成员变量,那么这个Card_Type类就需要一个有参构造:
Card_Type(PaiXin_Type type);
由于这是父类的成员属性,所以每个子类的构造函数需要显示的调用父类的构造函数,所以就需要重写每个子类的构造函数,不管是默认构造还是有参构造:
而这些构造函数我从新写了一个源文件并放在src文件中,名叫:constructor_PaiXin.cc
//牌型类中的构造函数
#include <iostream>
#include <PaiXin.h>
using namespace std;
//注意重写构造函数,不管是默认和有参都需要在申明中去申明这些构造函数
//牌型
Card_Type::Card_Type(PaiXin_Type type) : type(type) {}
//单张
OneCard::OneCard(int x) : Card_Type(PaiXin_Type::OneCard_Type), x(x) {}
//对子
TwoCard::TwoCard(int x) : Card_Type(PaiXin_Type::TwoCard_Type), x(x) {}
//三带
ThreeToCard::ThreeToCard(int x, Card_Type *p) : Card_Type(PaiXin_Type::ThreeToCard_Type), x(x), p(p) {}
//飞机
AirPlane::AirPlane(int x, int y) : Card_Type(PaiXin_Type::AirPlane_Type), x(x), y(y), p1(nullptr), p2(nullptr){}
AirPlane::AirPlane(int x, int y, Card_Type *p1, Card_Type *p2) : Card_Type(PaiXin_Type::AirPlane_Type), x(x), y(y), p1(p1), p2(p2){}
//炸弹
Bomb::Bomb(int x) : Card_Type(PaiXin_Type::Bomb_Type), x(x) {}
//4带2
BombToCard::BombToCard(int x, Card_Type *p1, Card_Type *p2) : Card_Type(PaiXin_Type::BombToCard_Type), x(x), p1(p1), p2(p2) {}
BombToCard::BombToCard(int x) : Card_Type(PaiXin_Type::BombToCard_Type), x(x), p1(nullptr), p2(nullptr){}
//王炸
Rocket::Rocket() : Card_Type(PaiXin_Type::Rocket_Type) {}
//连子
ShunZi::ShunZi(int x, int len) : Card_Type(PaiXin_Type::ShunZi_Type), x(x), len(len) {}
//连对
DoubleShunZi::DoubleShunZi(int x, int len) : Card_Type(PaiXin_Type::DoubleShunZi_Type), x(x), len(len) {}
//不要
Pass::Pass() : Card_Type(PaiXin_Type::Pass_Type) {}
设计通过上家出牌牌型,来获取可以管上家出牌的策略
对于每个类中设计一个虚函数,对于>符号的重载.
//父类
virtual bool operator>(Card_Type *) = 0;
//子类
//注意每个子类的声明中都需要添加
bool operator>(Card_Type *) override;
定义每个类的中的重载>符号:
而这些重载>符号我从新写了一个源文件并放在src文件中,名叫:operator_PaiXin.cc
//传入参数是上家出牌牌型
//bool operator>(Card_Type *pre) {}
//单张管牌方式
//只能管单张和Pass
bool OneCard::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch (op) {
case PaiXin_Type::Pass_Type : return true;
case PaiXin_Type::OneCard_Type : {
OneCard *_pre = dynamic_cast<OneCard *>(pre);
return this->x > _pre->x;
} break;
default : return false;
}
return false;
}
//对子管牌方式
//只能管对子和Pass
bool TwoCard::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch (op) {
case PaiXin_Type::Pass_Type : return true;
case PaiXin_Type::TwoCard_Type : {
TwoCard *_pre = dynamic_cast<TwoCard *>(pre);
return this->x > _pre->x;
} break;
default : return false;
}
return false;
}
//三代管牌方式
//只能管对应的三带方式和pass
bool ThreeToCard::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch (op) {
case PaiXin_Type::Pass_Type : return true;
case PaiXin_Type::ThreeToCard_Type : {
ThreeToCard *_pre = dynamic_cast<ThreeToCard *>(pre);
if (_pre->p->type != this->p->type) return false;
return this->x > _pre->x;
} break;
default : return false;
}
return false;
}
//飞机管牌方式
//飞机的飞机和大于上家的和就可以管
//并且只能管对应方式
bool AirPlane::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch (op) {
case PaiXin_Type::Pass_Type : return true;
case PaiXin_Type::AirPlane_Type : {
AirPlane *_pre = dynamic_cast<AirPlane *>(pre);
if (_pre->p1->type != this->p1->type) return false;
return ((this->x + this->y) > (_pre->x + _pre->y));
} break;
default : return false;
}
return false;
}
//炸弹管牌方式
//除了王炸和比他大的炸弹管不了都可以管
bool Bomb::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch (op) {
case PaiXin_Type::Rocket_Type : return false;
case PaiXin_Type::Bomb_Type : {
Bomb *_pre = dynamic_cast<Bomb *>(pre);
return this->x > _pre->x;
} break;
default : return true;
}
return true;
}
//4带2管牌方式
//4张牌比他大并且带的牌型一样
bool BombToCard::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch (op) {
case PaiXin_Type::Pass_Type : return true;
case PaiXin_Type::BombToCard_Type : {
BombToCard *_pre = dynamic_cast<BombToCard *>(pre);
if (_pre->p1->type != this->p1->type) return false;
return this->x > _pre->x;
} break;
default : return false;
}
return true;
}
//顺子管牌方式
//先判断长度是否相同
//相同再判断起始位置大小
bool ShunZi::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch (op) {
case PaiXin_Type::Pass_Type : return true;
case PaiXin_Type::ShunZi_Type : {
ShunZi *_pre = dynamic_cast<ShunZi *>(pre);
if (_pre->len != this->len) return false;
return this->x > _pre->x;
} break;
default : return false;
}
return false;
}
//连对管牌方式和顺子一样
bool DoubleShunZi::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch (op) {
case PaiXin_Type::Pass_Type : return true;
case PaiXin_Type::DoubleShunZi_Type : {
DoubleShunZi *_pre = dynamic_cast<DoubleShunZi *>(pre);
if (_pre->len != this->len) return false;
return this->x > _pre->x;
} break;
default : return false;
}
return false;
}
//只要上家不是Pass
//就可以不要
bool Pass::operator>(Card_Type *pre) {
if (pre->type == PaiXin_Type::Pass_Type) return false;
return true;
}
//火箭
//什么都可以管
bool Rocket::operator>(Card_Type *pre) {
PaiXin_Type op = pre->type;
switch(op) {
case PaiXin_Type::Rocket_Type : return false;
default : return true;
}
return false;
}
比较的逻辑可以见代码,逻辑比较简单,我就不做解释了.
dynamic_cast关键字
dynamic_cast
是 C++ 中的一个类型转换运算符,用于在继承层次结构中进行安全的向下转型(从基类指针或引用转换为派生类指针或引用)。它的作用包括:
安全的类型转换:
dynamic_cast
在运行时进行类型检查,确保转换是安全的。如果转换是合法的,它返回指向目标类型的指针或引用;如果转换不合法,它返回空指针(对于指针)或抛出std::bad_cast
异常(对于引用)。多态类型的安全转换: 当基类指针或引用指向实际派生类对象时,
dynamic_cast
可以将其转换为相应的派生类指针或引用,以便访问派生类特有的成员和方法。向下转型检查:
dynamic_cast
在进行向下转型时会检查目标类型是否与实际对象的类型兼容。如果目标类型是不兼容的,转换将失败。多态类的运行时类型识别(RTTI):
dynamic_cast
使用运行时类型信息(RTTI)来确定对象的实际类型,并在需要时执行转换换上面是chatgpt给的解释,简单来说就是:
举个例子:
Card_Type *q = new OneCard(3); OneCard *p = dynamic_cast<OneCard *>(q);
通过上面的代码,q是我基类的指针,指向的是我子类的OneCard类型的对象.但是我现在去访问这个OneCard对象里的成员属性x,如果是通过q去访问是访问不了的.现在就需要将这个指针类型进行转换为OneCard *类型,通过dynamic_cast关键字来转换,而<>里的内容就是需要转换的类型,而()就是需要转换的指针,最后返回的结果就是<>里的目标指针并且指向的就是刚刚new出来的对象,但是q指针类型是没有改变的.
并且他会进行检查如果转换的类型不是当前类型的派生类会返回一个空指针(nullptr).
然后现在的p指针就是指向刚刚new出来的OneCard(3)的对象的地址.就可以通过p指针去访问这个对象的成员变量x
扩展:static_cast关键字
通用类型转换: 将一种数据类型转换为另一种数据类型,如将整数转换为浮点数、将指针转换为整数等。
向上转型: 将派生类指针或引用转换为基类指针或引用。这种转换是安全的,因为派生类对象包含基类的部分,所以可以通过基类指针或引用来访问。
显式类型转换: 将一个类型转换为另一个类型,例如将整数转换为枚举类型、将指针转换为不同类型的指针等。
解除常量性: 将常量对象的常量性转换为非常量,以便对其进行修改。
需要注意的是,
static_cast
并不会执行动态类型检查,因此转换的安全性需要程序员来保证。如果执行的转换是不安全的,编译器可能会发出警告或错误信息。与dynamic_cast
不同,static_cast
不能用于将基类指针或引用转换为派生类指针或引用,因为它不执行运行时类型检查,可能导致不安全的转换。
设计获得管牌牌型
在基类中创建一个类方法,用来获取对于上家出牌牌型,来获取当前手牌中可以管牌的牌型
声明,放在include头文件,PaiXin.h的Card_Type基类中
vector<Card_Type *> get_PaiXinGuan(int *arr, Card_Type *pre){}
定义:放在src源文件,operator_PaiXin.cc中
vector<Card_Type *> Card_Type::get_PaiXinGuan(int *arr, Card_Type *pre) {
vector<Card_Type *> temp = Card_Type::get_PaiXin(arr);
vector<Card_Type *> suppress_temp;
for (auto x : temp) {
if (x->operator>(pre)) suppress_temp.push_back(x);
//这种出牌策略不行时直接删掉,不占内存
else delete x;
}
return suppress_temp;
}
先获取当前手牌所有可以出牌的出牌策略,
然后通过对于所有出牌策略和上家出牌的牌型进行比较,如果可以管上家出牌就将当前出牌方式放入容器中,不可以就删除当前出牌牌型,释放空间.
最后返回所有的管牌牌型.
测试:
测试方法和获取出牌策略差不多:
#include <iostream>
#include <PaiXin.h>
#include <vector>
using namespace std;
int arr[MAX_N + 5] = {0};
void usge(int op = 10) {
if (op >= 1) printf("1.DanZhang\n");
if (op >= 2) printf("2.DuiZi\n");
if (op >= 3) printf("3.SanDai\n");
if (op >= 5) printf("4.ShunZi\n");
if (op >= 4) printf("5.FeiJi\n");
if (op >= 6) printf("6.LianDui\n");
if (op >= 7) printf("7.ZhaDan\n");
if (op >= 8) printf("8.WangZha\n");
if (op >= 9) printf("9.4Dai2\n");
if (op >= 10) printf("10.Pass\n");
printf("intput(-1): quit\n");
printf("input : ");
return ;
}
Card_Type *func(int max_op = 10) {
Card_Type *p;
int x = 0;
while (1) {
usge(max_op);
cin >> x;
if (x < 1 || x > 10) {
printf("input error!!\n");
printf("plese input(1-10 or -1)\n");
continue;
}
int num;
switch (x) {
case 1: {
cin >> num;
p = new OneCard(num);
} break;
case 2: {
cin >> num;
p = new TwoCard(num);
} break;
case 3: {
cin >> num;
p = new ThreeToCard(num, func(2));
} break;
case 4: {
printf("起始其位置x :");
cin >> num;
int len;
printf("长度len :");
cin >> len;
p = new ShunZi(num, len);
} break;
case 5: {
while (1) {
int num2;
cin >> num2 >> num;
if (abs(num2 - num) != 1) {
printf("input error");
continue;
}
p = new AirPlane(num2, num, func(2), func(2));
break;
}
} break;
case 6: {
printf("起始其位置x :");
cin >> num;
int len;
printf("长度len :");
cin >> len;
p = new DoubleShunZi(num, len);
} break;
case 7: {
cin >> num;
p = new Bomb(num);
} break;
case 8: {
p = new Rocket();
} break;
case 9: {
cin >> num;
p = new BombToCard(num, func(2), func(2));
} break;
case 10: {
p = new Pass();
} break;
}
break;
}
return p;
}
int a[MAX_N + 5] = {0};
int b[MAX_N + 5] = {0};
void read(FILE *f, int *arr) {
int x;
while (fscanf(f, "%d", &x) != EOF) {
if (x == 0) break;
arr[x] += 1;
}
return ;
}
void get_input_data() {
FILE *f = fopen("input", "r");
read(f, a);
read(f, b);
fclose(f);
return ;
}
int main() {
get_input_data();
Card_Type::output_arr(a);
vector<Card_Type *> temp1 = Card_Type::get_PaiXinGuan(a, func());
for (auto x : temp1) x->output() << endl;
return 0;
}
4.创建博弈树
min_max算法
来理解一下:
从叶子节点往上推,也就是最后一层:
其中的0表示人机的状态,它是必输的状态,因为我的手牌出完了,它是必输的.
然后在往上推:
先从两个pass的节点来看,由于有一个子节点是必输的状态,那么对手的状态就是必应的,也就是我是必赢的状态,所以状态为1
再来分析两个出5的节点,由于人机出完牌了,那么我就是必输的状态,所以状态为0.
再往上:
可以每个节点的子节点都有我都必输的状态,那么每个节点对手就是必赢的,那么对应的状态都是1
最后
根节点就是0,因为没有一个子节点是必输的状态,所以根节点是必输的状态.而每个节点表示的是对手的状态,根节点表示人机出的pass,那么我们就是必输的.
了解了大概的min_max算法,我们可以通过这种思想来创建这个博弈树,来获取必胜的出牌策略.
设计博弈树:
声明:
用到树那么肯定就要用到节点
当然这是放在include头文件中,名tree.h
#ifndef _TREE_H
#define _TREE_H
#include <vector>
#include <PaiXin.h>
class Node {
public :
Node();
Node(Card_Type *, int);
int win_flag;
Card_Type *p;
vector<Node *> child;
};
void get_Game_Tree(Node *, int *, int *);
#endif
定义:
放在src源文件中,名tree.cc
#include <iostream>
#include <tree.h>
#include <PaiXin.h>
using namespace std;
Node::Node() : win_flag(0), p(new Pass()){}
Node::Node(Card_Type *p, int x = 0) : win_flag(x), p(p){}
static bool confirm(int *arr) {
for (int i = 3; i < MAX_N; i++) {
if (arr[i]) return false;
}
return true;
}
//由于a先出也就是我们人先出
//所以根节点为Pass
//root表示上手牌出牌
//a表示当前出牌人
//b表示下手出派人
void get_Game_Tree(Node *root, int *a, int *b) {
//确认对手是否还有手牌
if (confirm(b)) {
root->win_flag = 0;
return ;
}
vector<Card_Type *> temp = Card_Type::get_PaiXinGuan(a, root->p);
for (int i = 0; i < temp.size(); i++) {
//为当前可以管牌牌型创建一个新节点
Node *node = new Node(temp[i]);
//当前管牌牌型已经出了
//那么a数组中就应该少了出的牌
temp[i]->tack(a);
//然后该b出牌
get_Game_Tree(node, b, a);
//回到出牌之前,把这次出的牌返回到手牌中
temp[i]->back(a);
//将node作为当前节点的子孩子
root->child.push_back(node);
if (!node->win_flag) {
root->win_flag = 1;
//只要有了一个必胜策略就退出枚举牌型
break;
}
}
return ;
}
这个过程就是递归,这个过程可能会有点繁琐,在后续优化我会想办法来优化这种设计.
其中tack和back方法是牌型类中的方法并且我设计成为了虚函数,这两个的函数的作用就是,表示当前牌已经被用了和归还当前使用的牌.
具体逻辑可以见代码,如果不能理解就多学算法和数据结构,慢慢的就能理解了.
5.设计交互界面:
这里可以和我不同:
#include <iostream>
#include <PaiXin.h>
#include <tree.h>
#include <stack>
using namespace std;
//创建两个数组进行存储自己和对手的牌
int a[MAX_N + 5] = {0};
int b[MAX_N + 5] = {0};
void read(FILE *f, int *arr) {
int x;
while (fscanf(f, "%d", &x) != EOF) {
//当x等于0时表示读取完成当前一个人的手牌
if (x == 0) break;
arr[x] += 1;
}
return ;
}
void get_input_data() {
//通过fopen以只读方式打开文件
FILE *f = fopen("input", "r");
//通过f文件指针将手牌读取进自己的对手的手牌
read(f, a);
read(f, b);
//打开了文件,需要进行关闭
fclose(f);
return ;
}
void run(Node *root) {
printf("root is %s\n", root->win_flag ? "win" : "lose");
//通过博弈树来获取必胜出牌顺序
stack<Node *> s;
//将根节点压入栈中
s.push(root);
while (!s.empty()) {
printf("%s: ", s.size() % 2 ? "-->" : " ");
Card_Type::output_arr(a);
printf("%s: ", s.size() % 2 ? " " : "-->");
Card_Type::output_arr(b);
Node *node = s.top();
int op;
do {
printf("[%3d] : back\n", -1);
for (int i = 0; i < node->child.size(); i++) {
Node *n = node->child[i];
printf("[%3d] : %s --> ", i, n->win_flag ? "" : "win");
n->p->output();
printf("\n");
}
cout << "input : " << endl;
cin >> op;
if (op == -1 || op < node->child.size()) break;
else {
printf("%d is input error\n", op);
continue;
}
} while(1);
//后悔出牌收回出牌
if (op == -1) {
s.pop();
node->p->back(s.size() % 2 ? a : b);
}else {
node->child[op]->p->tack(s.size() % 2 ? a : b);
s.push(node->child[op]);
}
}
}
int main() {
//根节点作为不要
//那么就是我们先出牌
Node *root = new Node();
cout << "new node" << endl;
get_input_data();
cout << "get data" << endl;
//获取博弈树
Card_Type::output_arr(a);
Card_Type::output_arr(b);
get_Game_Tree(root, a, b);
cout << "get game tree" << endl;
run(root);
return 0;
}
6.测试
激动人心的时刻测试,能否打败人机,随机打开一个纸牌游戏:
将牌输入进我们读取的文件也就是fopen打开的文件:
然后运行:
然后我们,我们可以看到第30种出牌方式我们必赢,那就输入30,并且游戏按照这种出牌方式:
对手出王炸,我们输入0
我们只有不要
然后对手打出顺子
按照输入19
然后我们只能不要
然后对手出牌:
打出一张单2
然后我们还是不要:
然后对手打出对子j
程序叫我们出对子2:
出完对子2后,对手不要
对手不要:
最后程序叫我们打出对子7
小小人机拿下拿下.
总结:
这个程序,对于需要优化的地方还有许多,不够完善.我在测试一些比较复杂的牌型时,他会把内存占满,被我系统杀死了,说明算法处还需要优化,还有交互界面,以及对于整个程序的逻辑设计来说还不够严谨,也就是说如果面对用户来说完全不能拿来使用.在后续优化的版本我也会更新.
最后源码我放在了github上可以自取:
GitHub - MrsmallLi/My_home