C++类和对象项目:斗地主残局(做为程序员怎么能被人机虐)

news2024/12/27 14:59:45

斗地主残局解析项目

项目工程化 

由于这是一个项目,所以我们需要按照标准的项目工程化来进行设计。

分析大体框架

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++ 中的一个类型转换运算符,用于在继承层次结构中进行安全的向下转型(从基类指针或引用转换为派生类指针或引用)。它的作用包括:

  1. 安全的类型转换: dynamic_cast 在运行时进行类型检查,确保转换是安全的。如果转换是合法的,它返回指向目标类型的指针或引用;如果转换不合法,它返回空指针(对于指针)或抛出 std::bad_cast 异常(对于引用)。

  2. 多态类型的安全转换: 当基类指针或引用指向实际派生类对象时,dynamic_cast 可以将其转换为相应的派生类指针或引用,以便访问派生类特有的成员和方法。

  3. 向下转型检查: dynamic_cast 在进行向下转型时会检查目标类型是否与实际对象的类型兼容。如果目标类型是不兼容的,转换将失败。

  4. 多态类的运行时类型识别(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关键字

  1. 通用类型转换: 将一种数据类型转换为另一种数据类型,如将整数转换为浮点数、将指针转换为整数等。

  2. 向上转型: 将派生类指针或引用转换为基类指针或引用。这种转换是安全的,因为派生类对象包含基类的部分,所以可以通过基类指针或引用来访问。

  3. 显式类型转换: 将一个类型转换为另一个类型,例如将整数转换为枚举类型、将指针转换为不同类型的指针等。

  4. 解除常量性: 将常量对象的常量性转换为非常量,以便对其进行修改。

        需要注意的是,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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1660687.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一文了解spring事务特性

推荐工具 objectlog 对于重要的一些数据&#xff0c;我们需要记录一条记录的所有版本变化过程&#xff0c;做到持续追踪&#xff0c;为后续问题追踪提供思路。objectlog工具是一个记录单个对象属性变化的日志工具,工具采用spring切面和mybatis拦截器相关技术编写了api依赖包&a…

【Java】HOT100+代码随想录 动态规划(上)背包问题

目录 理论基础 一、基础题目 LeetCode509&#xff1a;斐波那契数 LeetCode70&#xff1a;爬楼梯 LeetCode746&#xff1a;使用最小花费爬楼梯 LeetCode62&#xff1a;不同路径 LeetCode63&#xff1a;不同路径ii LeetCode343&#xff1a;整数拆分 LeetCode96&#xff1a;不…

P8802 [蓝桥杯 2022 国 B] 出差

P8802 [蓝桥杯 2022 国 B] 出差 分析 很明显&#xff1a;单源最短路径 没有负权边 dijkstra 1.存图 2.准备两个数组 dis[]&#xff1a;更新源点到各个点的距离 vis[]&#xff1a;标记是否访问 3.从源点开始&#xff0c;更新源点到与其邻接的点的距离&#xff0c;每次选…

探索 Joomla! CMS:打造个性化网站的利器

上周我们的Hostease客户咨询建站服务。他想要用Joomla建站。Hostease提供免费安装Joomla CMS服务。这可以让客户搭建网站变得更加简单和高效。下面是针对Joomla建站的一些使用心得。 Joomla CMS是一款开放自由的软件&#xff0c;为用户提供了创建和维护网站的自由度。它经过全…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-15.4讲--ARM异常中断返回

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

【已解决】QT C++中QLineEdit不可粘贴输入

本博文源于生产实际&#xff0c;主要解决LineEdit不可粘贴输入的情况。下面将进行具体分析 问题来源 输入框只能一个个输入&#xff0c;不可复制粘贴。 分析 给QLineEdit装一个监听事件&#xff0c;监听它的事件即可。 问题解决步骤 问题一共分为三步&#xff1a; 书写监…

认养小游戏功能介绍

认养小游戏通常模拟了真实的农业生产过程&#xff0c;让玩家能够在线上体验种植、养殖的乐趣。以下是一些常见的认养小游戏功能介绍&#xff1a; 选择认养的农产品&#xff1a;首先&#xff0c;玩家可以从游戏中提供的多种农产品中选择自己想要认养的种类&#xff0c;如蔬菜、…

深入了解模拟和存根:提高单元测试质量的关键技术

一、引言 在进行单元测试时&#xff0c;我们经常会遇到对外部资源的依赖&#xff0c;如数据库、网络接口等。模拟&#xff08;Mocking&#xff09;和存根&#xff08;Stubbing&#xff09;是两种帮助我们模拟这些外部资源&#xff0c;使我们能够在隔离环境中测试单元的方法。在…

PyQt6--Python桌面开发(6.QLineEdit单行文本框)

QLineEdit单行文本框 import sys import time from PyQt6.QtGui import QValidator,QIntValidator from PyQt6.QtWidgets import QApplication,QLabel,QLineEdit from PyQt6 import uicif __name__ __main__:appQApplication(sys.argv)uiuic.loadUi("./QLine单行文本框.u…

618值得入手的平价好物清单,看完再买不吃亏!

即将到来的618年中购物狂欢节&#xff0c;无疑是一年一度的购物盛宴。为了让大家的购物体验更加愉悦和充实&#xff0c;我特地为大家精选了一系列好物。如果你也打算在618尽情购物&#xff0c;那就赶紧收藏这份清单吧&#xff01; 一、舒适佩戴不伤耳——南卡骨传导耳机Runner…

MySQL数据库实验三

本文承接前面的俩次实验基础上完成&#xff0c;不过实现的都是基础操作的练习 目录 目录 前言 实验目的 实验要求 实验内容及步骤 updata操作 delete操作 alter操作 添加列 删除列 修改列的数据类型 要求实现 实验结果 代码结果 注意事项 思考题 总结 前言 本文是MySQL数据库…

有了Supervisor,再也不用担心程序撂挑子了!

目录 1. 概述 2. 问题场景 3. Supervisor 简介 4.部署流程 4.1. 安装 Supervisor 4.2. 自定义服务配置文件 4.3. 自定义应用配置文件 4.4. 启动 supervisord 服务进程 4.5. 启动 supervisorctl 客户端进程 4.6. 验证 supervisor 的监控重启特性 5. 高级特性 5.1. 进…

大模型微调之 在亚马逊AWS上实战LlaMA案例(七)

大模型微调之 在亚马逊AWS上实战LlaMA案例&#xff08;七&#xff09; 微调SageMaker JumpStart上的LLaMA 2模型 这是在us-west-2的测试结果。 展示了如何使用SageMaker Python SDK部署预训练的Llama 2模型&#xff0c;并将其微调到你的数据集&#xff0c;用于领域适应或指令…

Python注意事项【自我维护版】

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 本篇博客在之前的博客上进行的维护 创建Python…

Java转Kotlin调用JNI方法异常

一、背景 Java调用JNI方法时没有任何问题&#xff0c;但是使用Java转Kotlin以后出现了崩溃异常&#xff1a;A java_vm_ext.cc:597] JNI DETECTED ERROR IN APPLICATION: jclass has wrong type: 校验参数后没有任何变化&#xff0c;经过分析验证找到解决方案 二、原因…

Python中的多进程、多线程、协程

Python中的多线程、多进程、协程 一、概述 1. 多线程Thread &#xff08;threading&#xff09;&#xff1a; 优点&#xff1a;同一个进程中可以启动多个线程&#xff0c;充分利用IO时&#xff0c;cpu进行等待的时间缺点&#xff1a;相对于进程&#xff0c;多线程只能并发执…

Windows:管理用户账户,密码策略和安全配置

在Windows操作系统中&#xff0c;管理用户账户和密码策略是确保系统安全的关键步骤。本文将探讨如何通过PowerShell和其他Windows工具管理用户账户&#xff0c;包括查看和设置密码策略、检查用户状态&#xff0c;以及导出和导入安全策略。这些管理任务对于系统管理员尤其重要&a…

STM32学习和实践笔记(25):USART(通用同步、异步收发器)

一&#xff0c;STM32的USART简介 USART即通用同步、异步收发器&#xff0c;它能够灵活地与外部设备进行全双工数据交换&#xff0c;满足外部设备对工业标准 NRZ 异步串行数据格式的要求。 UART即通用异步收发器&#xff0c;它是在USART基础上裁剪掉了同步通信功能。 开发板上…

Star-CCM+分配零部件至区域2-根据零部件的特性分组分配零部件至区域

前言 前文已经讲解了将零部件分配至区域的方法。其中有一种方法是"将所有部件分配到一个区域"。在工程应用中&#xff0c;有时会把同一种类型的部件分配到一个区域&#xff0c;因此在一个项目中有可能需要多次进行"将所有部件分配到一个区域"。如在电机温…

主机通过带光发端和ops接收端控制屏串口调试记录

场景就是主机电脑使用cutecom通过光纤口再到ops接收端从而控制屏过程 光纤口有个发送端波特率&#xff0c;Ops有接收端波特率&#xff0c;屏有自己的波特率&#xff0c;主机电脑可以通过发串口指令去设置发送端波特率和ops接收端波特率。因为主机只有一个&#xff0c;屏有多种…