文章目录
序言
此博客仅记录个人程序编写思路
未经允许严禁转载
用面向对象的方式编写能在Linux的命令行下能游玩的德州扑克游戏。Gitee仓库
抽象思路
对现实世界的单局游戏来说:
- 一组玩家
- 牌桌
- 荷官
其中荷官可以看成是整局的游戏的管理者,将会对整局游戏进行调度。
第一次抽象:
- 一组玩家
- 玩家:
- 手牌
- 筹码
- 牌力
- 玩家:
- 牌桌
- 牌堆
- 公共牌
- 奖池
这当中, 筹码 和 奖池 可以一同泛化为 筹码币 。
筹码币 的行为主要是 获得 和 使用 。
这部分内容属于搭建游戏靠后的内容,或者说可以单独列出来。
因为很多基于喊注的游戏来说,都有着和德州类似的下注流程。
其次就是 手牌 、 牌堆 、 公共牌 都可以看成是 一组牌 。
而 一组牌 又是由很多张单独的卡牌组合而成。
可以顺利成章的泛化出 牌 这个基类。
最后就是 玩家 和 牌力 ,其中玩家可以派生出 人类玩家 和 电脑玩家。
电脑玩家也可以接着派生出初级、中级、高级电脑玩家。
而对于 牌力 ,又或者在德州扑克里叫做 牌型。
本质上就是对 5张牌 的类型 判断 和 比较 。
牌力的判断和比较,可以说是第一个核心部分。
另一部分就是电脑玩家的 下注逻辑 编写。
类的抽象
可以说是从易到难,依次是
- 牌类
- 筹码币类
- 玩家类
- 牌力算法
牌类(Card)
对于现实世界来说,每一张扑克牌都有两个属性
- 点数
- 花色
这里只针对德州扑克的52(4x13)张,不考虑大小王或者癞子的加入。
因为牌的数目很小,其实可以就用从0-51的数字表示每一张牌。
自然而然用一个 char 型就能保存了。
using CardType=char;
class Card
{
protected:
CardType _card;
public:
//默认构造
Card():_card(0){}
~Card(){}
那么问题来了,如何获取点数和花色呢?
很简单,通过对 _card 的运算可以获得。
- 点数
0-51可以分成4组0-12,13-25,26-38,39-51。 - 花色
每一组自然对应四种花色♠、♥、♣、♦
具体如何映射看个人,笔者这里就随意了。
可以简单用一个枚举或者宏定义。如下
using ColourType=int;
using NumType=int;
//0,1,2,3
//♠,♥,♣,♦
enum Suits:ColourType{
SUITS_SPADE,//♠
SUITS_HEARTS,//♥
SUITS_DIAMONDS,//♣
SUITS_CLUBS//♦
};
//点数2,3,4,5,6,7,8,9,10,J ,Q ,K ,A
//对应2,3,4,5,6,7,8,9,10,11,12,13,14
enum Nums : NumType
{
NUM_2 = 2,
NUM_3,
NUM_4,
NUM_5,
NUM_6,
NUM_7,
NUM_8,
NUM_9,
NUM_10,
NUM_J,
NUM_Q,
NUM_K,
NUM_A
};
通过对 _cards 取余 和 做除 获得 点数 和 花色
using CardType=char;
class Card
{
protected:
CardType _card;
//用char一个字节来表示卡牌的点数和花色
//0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10,11,12,
//13,14,15,16,17,18,19,20,21,22,23,24,25,
//_card%13+2=点数
//_card/13=花色
public:
Card()
:_card(0){}
Card(NumType num,ColourType colour)
:_card(colour*13+(num-2)){}
NumType GetNum()const{return (_card%13)+2;}
ColourType GetColour()const{return _card/13;}
为什么没有去定义一个可以修改_card的成员函数?
或者可以改变点数或者花色的成员函数?
- 因为从现实世界来说,每张牌生成之后,它的点数和花色就应该是固定的,无法被修改,我们只能查看它。
那为什么不考虑在堆上声请堆空间保存_card呢?
- 没必要,申请堆空间同样需要时间,而且纵观整个游戏流程来说:
1.存在大量卡牌的交换。
2.牌力算法上可能会涉及大量的运算和比较。
3.就一个char,很小。
!还没设计好
既然提到了大量的使用,必然存在很多 复制、调用和比较 。
所以需要将其 比较函数 和 复制函数 全部补全。
(这里就不做展示了,详见仓库)Card.hpp
最后还得补充上每张牌的可视化,数号和花色如何打印。
同时还得考虑到后面的一组牌的可视化。
以下代码仅供参考:
//公共函数
namespace CARD{
inline void PrintNum(const NumType&);
inline void PrintColour(const ColourType&);
}//end of CARD
namespace CARD{
inline void PrintColour(const ColourType&colour){
if(colour==SUITS_SPADE){
cout<<"\033[34m♠\033[0m";
}else if(colour==SUITS_CLUBS){
cout<<"\033[31m♦\033[0m";
}else if(colour==SUITS_HEARTS){
cout<<"\033[31m♥\033[0m";
}else if(colour==SUITS_DIAMONDS){
cout<<"\033[34m♣\033[0m";
}
}
inline void PrintNum(const NumType&num){
if(num==NUM_J){cout<<"J";}
else if(num==NUM_Q){cout<<"Q";}
else if(num==NUM_K){cout<<"K";}
else if(num==NUM_A){cout<<"A";}
else {cout<<static_cast<int>(num);}
}
}//end of CARD
之后便是在基础Card的基础上去设计一组卡牌的类了。
一组牌(GroupCards)
首先最好想到的一点就是用一个容器去存储Card,
那不如就用 vector 吧。
其次一组牌还得有一个 牌的数目 的数据成员,毕竟对于其派生类来说,最重要的区别就是在于牌的数目。
当然,可以把这个类设计为虚类,虽然表面看,不同一组牌直接没有明显的“多态的形式”。虽然它们都可以泛化为一组牌。
参考代码如下:
using Cards=vector<Card>;
using NumsType=unsigned int;
class GroupCards
{
public:
GroupCards(NumsType nums)
:_nums(nums)
,_cards()
{
_cards.reserve(_nums);
}
virtual~GroupCards() {}//没啥用,毕竟vector很懂事
NumsType GetNums()const{return _nums;}
size_t GetSize()const{return _cards.size();}
const Card*GetFirstCards()const{return &_cards.front();}
Cards& ReturnCards(){return _cards;}
Cards ReturnCards()const{return _cards;}
protected:
Cards _cards;
NumsType _nums;
};
之后就是对一组牌的一些可能会用到的操作
- 排序 ,根据数号,根据花色(升序降序要结合之后的牌力算法考虑)
- 图像化,最好写一个万能接口,能根据想要的形式打印。还得安全。
- 可能需要一组牌与另一组牌的 相加 ,返回更长的一组牌。(或者别的什么运算,综合牌力算法考虑)
namespace GROUP_CARDS{
//指定几行几列打印卡牌
//row:行 cow:列
void PrintCards(const GroupCards &cards, const int row=1,const int cow=5);
void SortCardsNum(Cards& cards);
void SortCardsColour(Cards& cards);
Cards CardsAddCards(Cards& Lcards,Cards& Rcards);
}//end of CARDS
方法具体实现:(仅展示排序和相加,毕竟图像化可以多种多样)
void GROUP_CARDS::SortCardsNum(Cards&cards){
sort(cards.begin(),cards.end(),[](Card a,Card b){
if(a.GetNum()==b.GetNum()){
return a.GetColour()<b.GetColour();
}
return a.GetNum()<b.GetNum();
});
}
void GROUP_CARDS::SortCardsColour(Cards&cards){
sort(cards.begin(),cards.end(),[](Card a,Card b){
if(a.GetColour()==b.GetColour()){
return a.GetNum()<b.GetNum();
}
return a.GetColour()<b.GetColour();
});
}
Cards GROUP_CARDS::CardsAddCards(Cards& Lcards,Cards& Rcards){
Cards tmp;
tmp.reserve(Lcards.size()+Rcards.size());
for(auto &rc:Lcards){
tmp.push_back(rc);
}
for(auto &rc:Rcards){
tmp.push_back(rc);
}
return tmp;
}
写排序的时候,可以练习到 Lambda表达式 ,当然这是C++11的特性。
在写这一部分的时候,考虑到视觉上的比较效果。
我对基于数号的排序设置为,从左到右的升序。
同时考虑到下标运算符对 Cards 也就 vector<Card> 的越界访问,
我相当于重载了 Cards 的下标访问运算符。(个人用法)
//!!!!记得放到.c文件中,不然会重定义。
template<>
Card& Cards::operator[](size_t x){
x=x%size();//对长度取余
return *(begin()+x);
}
时间记录
2022.11.18
在一组牌的基础上去派生其他类
牌堆(DeckCards)
牌堆类设计
直接上代码:
#ifndef _CTL_DECKCARDS_HPP_
#define _CTL_DECKCARDS_HPP_
#include "GroupCards.hpp"
#include <random>
using UseType=unsigned int;
class DeckCards
:public GroupCards
{
public:
//对于德州来说,卡组就只有52张
DeckCards(NumsType nums=52)
:GroupCards(nums)
,_used(0)
{}
~DeckCards() {}
void PrintDeckCards(int row=13,int cow=4)const;
void SetDeckCards();//初始化卡组
Card SendCard(){
_used=_used%_nums;//防止发牌越界
return _cards[_used++];}
void ResetSend(){_used=0;}
void ShuffleDeck(int times=1);//洗牌
private:
//记录发牌的位置
UseType _used;
};
#endif
对于DeckCards来说
- 增加的数据成员
- 发牌的标记位 _used
- 增加的方法
- 初始化卡组 SetDeckCards()
- 洗牌 ShuffleDeck()
- 发牌 SendCard()
- 重置发牌位 ResetSend()
初始化卡牌
先看初始化卡牌 :
//初始化卡组
void DeckCards::SetDeckCards(){
for(int i=0;i<13;++i){
for(int j=0;j<4;++j){
//i代表点数,j代表花色
//注意对于Card的属性的映射
//这里i+2对应实际牌点数的大小
_cards.push_back(Card(i+2,j));
}
}
}
- 可能会觉得这样利用 vector 的 push_back 将 Card 加进去,在结束的时候应该加上 shrink_to_fit() 。
- 其实没必要,因为在 GroupCards 的构造函数 里就已经根据张数给容器预留空间了。
洗牌
关于洗牌的实现,笔者纠结了很久,因为如果不是随机的洗牌,可能会导致每个玩家的获胜概率不均匀。尽可能要在洗牌的时候,让每张牌在每个位置出现的概率相同。
//洗牌,默认1次
void DeckCards::ShuffleDeck(int times){
::srand(time(NULL));
Card tmp;
for(int j=0;j<times;++j){
for(int i=52-1;i>=0;--i){
tmp=_cards[i];
int ch=::rand()%(i+1);
_cards[i]=_cards[ch];
_cards[ch]=tmp;
}
}
}
- 洗牌的设计思路:
- 遍历卡组
- 将当前卡牌暂存
- 生成一个随机数,范围是未洗牌的下标范围
- 将当前的卡牌 与 随机数下标的卡牌交换
- 换个角度想,就是手里拿着一副牌,每次随机的,从剩余的牌里选一张出来,将其放置。将所有的牌放置完以后,得到的就是一副每张牌在每个位置出现概率相同的卡组。
- 出于习惯,笔者在C++里用到的所有 C的头文件,均采用C++提供的,例如此处用到的是<cstdlib>
这里卡牌的交换可以优化。
与手牌公共牌的关系
- 现实游戏里,无论是手牌的还是公共牌都是从牌堆获得卡牌。
- 自然,手牌 和 公共牌 都是依赖于 牌堆。
- 或者说, 手牌 与 公共牌 的 获取 都需要通过 牌堆的 SendCard() 得到。
SendCard()函数详见DeckCards的头文件
类图如下:
手牌(HandCards)
代码如下:
#ifndef _CTL_HANDCARDS_HPP_
#define _CTL_HANDCARDS_HPP_
#include "DeckCards.hpp"
class HandCards
:public GroupCards//公有继承
{
public:
HandCards(DeckCards& deck)//构造时必须绑定Deck,语义上没有卡组,哪来的手牌。
:GroupCards(2)
,_deck(&deck)
{}
~HandCards() {}
void show()const;
void AutoGetSafe();//获取卡牌
void AutoGet();
void CleanCards(){_cards.clear();}//清空手牌
//仅提供一个换绑接口,没啥用,或许存在让手牌再去绑定另一个卡组的情况?
void RebindDeck(DeckCards& deck){_deck=&deck;}
private:
DeckCards* _deck;
};
//德州手牌获取
inline void HandCards::AutoGetSafe(){
if(GetSize()==0||GetSize()==1){
_cards.push_back((*_deck).SendCard());
}else{}
}
inline void HandCards::AutoGet(){
_cards.push_back((*_deck).SendCard());
}
#endif
- 需要注意的点
- 确保获取卡牌时,不会让手牌的数目超过上限。安全的获得卡牌。
- 记得提供 清空手牌 的接口。
公共牌(PublicCards)
公共牌的设计和手牌类似
代码如下:
#ifndef _CTL_PUBLICCARDS_HPP_
#define _CTL_PUBLICCARDS_HPP_
#include "DeckCards.hpp"
class PublicCards
:public GroupCards
{
public:
PublicCards(DeckCards& deck)
:GroupCards(5)
,_deck(&deck)
{}
~PublicCards() {}
void show()const;
void AutoGetSafe();
void AutoGet();
void CleanCards(){_cards.clear();}
void RebindDeck(DeckCards & deck){_deck=&deck;}
private:
DeckCards* _deck;
};
inline void PublicCards::AutoGetSafe(){
if(GetSize()==0){//发牌圈,发三张弃一张
_cards.push_back((*_deck).SendCard());
_cards.push_back((*_deck).SendCard());
_cards.push_back((*_deck).SendCard());
(*_deck).SendCard();
return;
}else if (GetSize()==3){//转牌圈,发一张弃一张
_cards.push_back((*_deck).SendCard());
(*_deck).SendCard();
return;
}else if(GetSize()==4){//河牌圈,发一张弃一张
_cards.push_back((*_deck).SendCard());
return;
}else{}
}
inline void PublicCards::AutoGet(){
_cards.push_back((*_deck).SendCard());
}
#endif
公共牌和手牌基本没有什么差别。
在 AutoGetSafe() 里可以再增加错误情况的打印信息。
至此,所有和牌有关的实体类设计完成。
算法
整个游戏最核心的无非就两点
- 牌型 判断
- 牌型 比较
首先我们先了解德州扑克的所有牌型
注意在顺子中会多出一个最小的顺子A2345
设计思路
- 其实想实现牌型的判断并不难,无非就是循环判断。但怎么写才能精简,直取牌型判断的关键。
- 以下是笔者个人理解:
- 德州的牌型分为两类
- 花色类
- 点数类
其中点数类,可以细分为
- 4Kind,1Kind (四条)
- 3Kind,2Kind (葫芦)
- 3Kind,1Kind,1Kind (三条)
- 2Kind,2Kind,1Kind (两对)
- 2Kind,1Kind,1Kind,1Kind (一对)
- 1Kind,1Kind,1Kind,1Kind,1Kind (高牌)
这六种情况,其中高牌包含了9+1种顺子的情况。
如此分析下来,如果仅对5张牌来说。
至少需要:
- 统计:相同点数的牌的次数
- 出现4次的
- 出现3次的
- 出现2次的
- 出现1次的
- 统计:相同花色的牌的次数
- 四种花色分别出现了多少次
- 排序:按照数号升序排序,如果数号相同就按照花色大小。(降序也可以,但一定要确定排序后是固定的)
//当然也完全可以给每种牌型单独设立一个判断函数,最后组合起来看什么类型。
方案一
获取到5张牌的这些信息后,基本就可以很快的判断出它是什么牌型了。
笔者这里展示自己的代码屎山 (这个版本在后面被舍弃了)
TypeType TYPE::GetType(const Cards&cards5){
//暂存,排序
Cards tmp=cards5;
GROUP_CARDS::SortCardsNum(tmp);
//统计相同点数的出现次数
map<NumType,int> kind_map;
//统计相同花色的出现次数
map<ColourType,int> colour_map;
for(auto &rc:tmp){
++kind_map[rc.GetNum()];
++colour_map[rc.GetColour()];
}
int arrkind[4]={0};
bool colour5=false;
for(auto &rc:kind_map){
if(rc.second==1){++arrkind[0];}
else if(rc.second==2){++arrkind[1];}
else if(rc.second==3){++arrkind[2];}
else if(rc.second==4){++arrkind[3];}
}
for(auto &rc:colour_map){
if(rc.second==5){colour5=true;}
}
bool straight5=false;
if(arrkind[0]==5){straight5=TYPE::IsStraight(tmp);}
bool RoyalFlush5=false;
if(colour5&&straight5){RoyalFlush5=TYPE::IsRoyalFlush(tmp);}
TypeType typeTmp=0;
if(RoyalFlush5){typeTmp=ROYAL_FLUSH;}
else if(colour5&&straight5){typeTmp=STRAIGHT_FLUSH;}
else if(arrkind[3]==1){typeTmp=FOUR_OF_A_KIND;}
else if(arrkind[2]==1&&arrkind[1]==1){typeTmp=FULL_HOUSE;}
else if(colour5){typeTmp=FLUSH;}
else if(arrkind[0]==5&&straight5){typeTmp=STRAIGHT;}
else if(arrkind[2]==1&&arrkind[0]==2){typeTmp=THREE_OF_A_KIND;}
else if(arrkind[1]==2&&arrkind[0]==1){typeTmp=TWO_PAIRS;}
else if(arrkind[1]==1&&arrkind[0]==3){typeTmp=ONE_PAIR;}
else {typeTmp=HIGH_CARD;}
return typeTmp;
}
#endif
当然,既然是C++那自然牌型也得设计成一个类。
- 这个类要有自己的卡牌容器,用于保存当前判断的牌型具体
- 这个类会被作为玩家的属性,当进入翻牌圈之后,玩家就有自己的牌型了(公共牌+手牌)
- 这个类需要实现自身的比较函数
因为笔者最后选择的将所有牌组合生成一个哈希表。所有这里不展示过去版本的代码。
可以参考该头文件Type.hpp
这个头文件是真正的屎山 ,笔者一开始打算用基于对象的方式,为每种类型设置比较函数。
然后,这个类会根据自身的类型值,自动选择绑定比较函数,实现相同牌型的自比较。
第一个版本就是基于这种算法实现的。可以说思路简单粗暴,而且对于德州的游戏进程来说,也称得上高效。
代码如下:
bool GREADER::KindCard(const Type & Left,const Type & Right){
map<NumType,int,std::greater<NumType> > numLMap;//左操作数的卡牌相同点数出现次数统计图
map<NumType,int,std::greater<NumType> > numRMap;//右操作数的卡牌相同点数出现次数统计图
auto Cleft=Left.ReturnCards();
auto Cright=Right.ReturnCards();
for(int i=0;i<5;++i){
++numLMap[Cleft[i].GetNum()];
++numRMap[Cright[i].GetNum()];
}
int kind4[2]={0};//记录出现4次的卡牌点数
int kind3[2]={0};//记录出现3次的卡牌点数
int Lkind2[2]={0};//记录左操作数出现2次的卡牌点数
int Rkind2[2]={0};//记录右操作数出现2次的卡牌点数
int Lkind1[5]={0};//记录左操作数出现1次的卡牌点数
int Rkind1[5]={0};//记录右操作数出现1次的卡牌点数
int pos2l,pos2r,pos1l,pos1r;
pos2l=pos2r=pos1l=pos1r=0;
//统计
for(auto &rc:numLMap){
if(rc.second==4){kind4[0]=rc.first;}
if(rc.second==3){kind3[0]=rc.first;}
if(rc.second==2){Lkind2[pos2l++]=rc.first;}
if(rc.second==1){Lkind1[pos1l++]=rc.first;}
}
for(auto &rc:numRMap){
if(rc.second==4){kind4[1]=rc.first;}
if(rc.second==3){kind3[1]=rc.first;}
if(rc.second==2){Rkind2[pos2r++]=rc.first;}
if(rc.second==1){Rkind1[pos1r++]=rc.first;}
}
if(kind4[0]!=0&&kind4[1]!=0){//四条的情况
if(kind4[0]!=kind4[1]){
return kind4[0]>kind4[1];
}else{return Lkind1[0]>Rkind1[0];}
}
else if(kind3[0]!=0&&kind3[1]!=0){
if(Lkind2[0]!=0&&Rkind2[0]!=0){//葫芦的情况
if(kind3[0]!=kind3[1]){return kind3[0]>kind3[1];}
else{return Lkind2[0]>Rkind2[0];}
}else{//三条的情况
if(kind3[0]!=kind3[1]){return kind3[0]>kind3[1];}
else{
for(int i=0;i<3;++i){if(Lkind1[i]!=Rkind1[i]){return Lkind1[i]>Rkind1[i];}}
return false;
}
}
}
else if(Lkind2[0]!=0&&Lkind2[1]!=0&&Rkind2[0]!=0&&Rkind2[1]!=0){//两对的情况
for(int i=0;i<2;++i){
if(Lkind2[i]!=Rkind2[i]){return Lkind2[i]>Rkind2[i];}
}
return Lkind1[0]>Rkind1[0];
}
else if(Lkind2[0]!=0&&Rkind2[0]!=0){//一对的情况
if(Lkind2[0]!=Rkind2[0]){return Lkind2[0]>Rkind2[0];}
for(int i=0;i<3;++i){
if(Lkind1[i]!=Rkind1[i]){return Lkind1[i]>Rkind1[i];}
}
return false;
}
else{//高牌
for(int i=0;i<5;++i){
if(Lkind1[i]!=Rkind1[i]){return Lkind1[i]>Rkind1[i];}
}
return false;
}
}
这个函数基本上把所有情况都考虑进去了,以至于笔者后面根本不需要去注册每种类型的比较函数,直接都用这个就好,毕竟相同点数的牌必须统计。逃不掉遍历卡牌。
只要能判断5张牌的类型,同时实现类型的自比较,那自然5+2张牌的问题就很好解决了。
- 无非就是从7张牌中选取5张的组合。
- 在遍历的时候,每次保存最大的那个,最后自然就是玩家的最大牌力。
从7张牌中选5张
Type TYPE::GainType(Cards cards){
int Cnum=cards.size();
if(Cnum<5){
cerr<<"TYPE::GainType() Cnum<5 \n";
exit(1);
}
Type Tret;
if(Cnum==5){
Tret.SetCards(cards);
return Tret;
}else{
//从N张牌中选5张
Cards Ctmp;
Type Ttmp;
const int Five=5;
Ctmp.resize(Five);
int Pick1,Pick2,Pick3,Pick4,Pick5;
for(Pick1=0;Pick1<=Cnum-Five;++Pick1){
for(Pick2=Pick1+1;Pick2<=Cnum-Five+1;++Pick2){
for(Pick3=Pick2+1;Pick3<=Cnum-Five+2;++Pick3){
for(Pick4=Pick3+1;Pick4<=Cnum-Five+3;++Pick4){
for(Pick5=Pick4+1;Pick5<=Cnum-Five+4;++Pick5){
Ctmp[0]=cards[Pick1];
Ctmp[1]=cards[Pick2];
Ctmp[2]=cards[Pick3];
Ctmp[3]=cards[Pick4];
Ctmp[4]=cards[Pick5];
Ttmp.SetCards(Ctmp);
if(Ttmp>Tret){//这里就会体现比较函数
Tret.SetCards(Ctmp);
}
}
}
}
}
}
return Tret;
}
}
方案二
还有更好的做法吗?
其实笔者迭代了几个版本后发现
- 判断出牌型:
- 其实也没必要从7张中选出5张,如果把所有属性抽出来,也可以判断出。
- 但是这样需要保存每张牌的标记位,因为最后要获取最大的牌型具体。
- 对于德州来说需要判断最多的情况也就是2+5,也就判断牌型了21次,比较了21次。
综合考虑下,对于一个玩家的一局游戏而已,每次5张牌的去判断,也只需要判断1+6+21=28次。
那不如从另一方面入手。
- 比较不同的牌型:
- 其实 每种,经过 确定的排序方式 的排序 后,它的牌力,战斗力是确定的。
那不如就直接生成一个所有5张组合-牌力的哈希表吧。
(这个数量还是蛮大的,从52张牌中选出5张,一共有:2598960 种情况)
因为笔者在设立 Card 的时候,它的数据成员就是一个 char 。
所有可以利用 unordered_map 生成一个哈希表。
主键为 经过排序后的 char[5], 值就是对应的牌力 short。
虽然一个只有7字节,算下来7*2598960也有17MB的大小。
但是如果做出来了,判断牌型和牌型之间的比较就会变得很快。
因为判断牌型,只需要从 unordered_map 里去 find()。
众所周知,哈希查找的时间复杂度为常数,最坏情况和容器大小成线性。
那就先从最大的开始吧(因为逻辑上好思考)
皇家同花顺
void Level::CreatMapRoyalFlush()
{
int lv = 1;
lv = 1;
//对皇家同花顺
//只有一种等级
for (int i = 0; i < 4; ++i)
{
char ch4 = i * 13 + 12; // A
char ch5 = i * 13 + 8; // 10
char ch6 = i * 13 + 9; // J
char ch7 = i * 13 + 10; // Q
char ch8 = i * 13 + 11; // K
string tmp({ch5, ch6, ch7, ch8, ch4});
_cardsmap.insert({tmp, lv});
}
}
- 这里可能会有人问,为什么主键是 string 而不是 char[5]。
- 因为一开始打算将主键设计的可读。
- 因为 char[5] 不是一个类型,不能作为 unordered_map 的参数
- 因为默认哈希算法用 string 更方便。
*诚然,虽然磁盘化的时候是17MB,但加载到内存上时,因为string是3个指针的大小,大了很多。
但,目前版本就开发到这里,暂不考虑内存上的空间优化(都是堆上管干嘛)
接着是同花顺的代码
void Level::CreatMapStraightFlush()
{
int lv = 1;
//对同花顺;
for (int i = 0; i < 4; ++i)
{
char colour = i * 13;
//8种同花顺子
//顶顺为皇家同花顺
for (int j = 11; j >= 4; --j)
{
char numS = j;
char card0 = colour + numS, card1 = card0 - 4,
card2 = card0 - 3, card3 = card0 - 2, card4 = card0 - 1;
string tmp({card1, card2, card3, card4, card0});
_cardsmap.insert({tmp, ++lv});
}
lv = 1;
}
lv = 1 + 8;
for (int i = 0; i < 4; ++i)
{
char colour = i * 13;
char card = colour + 0, card1 = colour + 1,
card2 = colour + 2, card3 = colour + 3;
char cardA = colour + 12;
//最小的同花顺
string tmp({card, card1, card2, card3, cardA});
_cardsmap.insert({tmp, lv});
}
}
同花顺没什么好说的,就是要注意有一个特例2345A的 同花顺
四条
void Level::CreatMapFourOfAKind()
{
// lv=1+8+1;
int lv = 10;
//四条
for (int i = 12; i >= 0; --i)
{
char nums = i, nums1 = nums + 13, nums2 = nums1 + 13, nums3 = nums2 + 13;
string tmp;
tmp.resize(4);
tmp[0] = nums;
tmp[1] = nums1;
tmp[2] = nums2;
tmp[3] = nums3;
for (int j = 0; j < 4; ++j)
{
char colour = j;
for (int k = 12; k >= 0; --k)
{
if (k == i){continue;}//避免取到四条的点数
char card = colour * 13 + k;
string use;
if (i > k){use = card + tmp;}//保障点数的升序
else{use = tmp + card;}
_cardsmap.insert({use, ++lv});
}
lv -= 12;
}
lv += 12;
}
}
对于四条而言
- 先选出 4kind 的牌,再选出 1kind 的牌 。
- 合计13*12=156种等级情况。
- 别忘了加上花色,而且也得符合升序。
之后是葫芦
void Level::CreatMapFullHouse()
{
// lv=1+8+1+12*13;
int lv = 166;
//葫芦
char colourarr[4] = {0, 13, 26, 39};
for (int i = 12; i >= 0; --i)
{
char num0 = i;
for (int c1 = 0; c1 <= 4 - 3; ++c1)
{
for (int c2 = c1 + 1; c2 <= 4 - 3 + 1; ++c2)
{
for (int c3 = c2 + 1; c3 <= 4 - 3 + 2; ++c3)
{
char num1 = num0 + colourarr[c1];
char num2 = num0 + colourarr[c2];
char num3 = num0 + colourarr[c3];
for (int s1 = 0; s1 <= 4 - 2; ++s1)
{
for (int s2 = s1 + 1; s2 <= 4 - 2 + 1; ++s2)
{
for (int k = 12; k >= 0; --k)
{
if (k == i)//避免点数相同
{
continue;
}
char num4 = k + colourarr[s1];
char num5 = k + colourarr[s2];
if (k > i)//比较点数,确保键是从小到大(或者说固定的牌型方式)
{
string use({num1, num2, num3, num4, num5});
_cardsmap.insert({use, ++lv});
}
else
{
string use({num4, num5, num1, num2, num3});
_cardsmap.insert({use, ++lv});
}
}
lv -= 12;
}
}
}
}
}
lv += 12;
}
}
- 其实看到这里基本处理思路就基本大致相同了
- 先挑选出牌型比较时,优先级高的部分,例如葫芦就是先3kind的那部分。
- 然后去选出剩下的部分,注意已经选择的点数,是不能选的。
- 因为花色是不会影响比较的,所有再插入 lv值 的时候,要去除花色的影响。
- 最后要注意顺序的问题,对于葫芦来说,就两个点数,但是两个点数没有明显的大小关系。所有必须判断后调整位置。
之后是同花,这一个牌型可以说是高牌中的特例,能写出这个,那高牌无法就是跳过花色相同的情况。
同花代码:
void Level::CreatMapFlush()
{
// lv=10+156+156;
int lv = 322;
//同花
for (int co = 0; co < 4; ++co)
{
char colour = co * 13;
int timetmp = 0;
for (int n1 = 12; n1 >= 4; --n1)
{
for (int n2 = n1 - 1; n2 >= 3; --n2)
{
for (int n3 = n2 - 1; n3 >= 2; --n3)
{
for (int n4 = n3 - 1; n4 >= 1; --n4)
{
for (int n5 = n4 - 1; n5 >= 0; --n5)
{
if (n5 + 1 == n4 && n4 + 1 == n3 && n3 + 1 == n2 && n2 + 1 == n1)//跳过顺子
{
continue;
}
if (n5 == 0 &&n5 + 1 ==n4 &&n4 + 1 == n3 &&n3 + 1 == n2 &&n1 == 12)//跳过顺子
{
continue;
}
char card0 = n5 + colour;
char card1 = n4 + colour;
char card2 = n3 + colour;
char card3 = n2 + colour;
char card4 = n1 + colour;
//隐含n1>n2>n3>n4>n5;
string tmp({card0, card1, card2, card3, card4});
// tmp需要排除顺子的可能
_cardsmap.insert({tmp, ++lv});
++timetmp;
}
}
}
}
}
lv -= timetmp;
}
}
- 同花说白了就是,C(13,5) 的组合情况1287种情况,当然得减去10种顺子的情况,因为成顺就是同花顺了。
顺子
void Level::CreatMapStraight()
{
// lv=322+1287-10;
int lv = 1599;
//顺子
for (int num4 = 12; num4 >= 4; --num4)
{
++lv;
for (int colour4 = 0; colour4 < 4; ++colour4)
{
char card4 = num4 + colour4 * 13; //第五张牌
for (int colour3 = 0; colour3 < 4; ++colour3)
{
char card3 = num4 - 1 + colour3 * 13; //第四张牌
for (int colour2 = 0; colour2 < 4; ++colour2)
{
char card2 = num4 - 2 + colour2 * 13; //第三张牌
for (int colour1 = 0; colour1 < 4; ++colour1)
{
char card1 = num4 - 3 + colour1 * 13; //第二张牌
for (int colour0 = 0; colour0 < 4; ++colour0)
{
if (colour0 == colour1 && colour1 == colour2 &&
colour2 == colour3 && colour3 == colour4)//避免同花
{
continue;
}
char card0 = num4 - 4 + colour0 * 13;
string tmp({card0, card1, card2, card3, card4});
_cardsmap.insert({tmp, lv});
}
}
}
}
}
}
++lv;
for (int colour4 = 0; colour4 < 4; ++colour4)
{
char card4 = 12 + colour4 * 13; //第五张牌 A
for (int colour3 = 0; colour3 < 4; ++colour3)
{
char card3 = 3 + colour3 * 13; //第四张牌 5
for (int colour2 = 0; colour2 < 4; ++colour2)
{
char card2 = 2 + colour2 * 13; //第三张牌 4
for (int colour1 = 0; colour1 < 4; ++colour1)
{
char card1 = 1 + colour1 * 13; //第二张牌 3
for (int colour0 = 0; colour0 < 4; ++colour0)
{
if (colour0 == colour1 && colour1 == colour2 &&
colour2 == colour3 && colour3 == colour4)//避免同花
{
continue;
}
char card0 = 0 + colour0 * 13; //第一张牌 2
string tmp({card0, card1, card2, card3, card4});
_cardsmap.insert({tmp, lv});
}
}
}
}
}
}
- 顺子和写同花顺相比,就是多了花色的选择,同时需要将同花的情况排除。
- 需要注意,笔者这里取花色的时候从0开始的,正好满足点数相同时,花色也是升序的。(后面的牌型也是)所有不用特别对花色进行比较。
- 反复强调,要保障这里最后给哈希表的键一定要是固定排序的。
之后的三条、两对、一对,思路和做法都相同,而对于高牌,无法就是排除同花和顺子的情况,所有就一并放在这里。
void Level::CreatMapThreeOfAKind()
{
// lv=1599+10;
int lv = 1609;
//三条YX1X2
for (int numY = 12; numY >= 0; --numY)
{ //确定三张点数一样的牌的点数
for (int co1 = 0; co1 <= 4 - 3; ++co1)
{
for (int co2 = co1 + 1; co2 <= 4 - 3 + 1; ++co2)
{
for (int co3 = co2 + 1; co3 <= 4 - 3 + 2; ++co3)
{
char card0 = numY + co1 * 13;
char card1 = numY + co2 * 13;
char card2 = numY + co3 * 13;
string tmpY({card0, card1, card2}); //一组三张点数一样的牌
//从剩余的12中选两个点数出来
for (int num1 = 12; num1 >= 1; --num1)
{
if (num1 == numY)
{
continue;
}
for (int num2 = num1 - 1; num2 >= 0; --num2)
{
if (num2 == numY){continue;}//避免点数相同
++lv;
for (int colour1 = 0; colour1 < 4; ++colour1)
{
for (int colour2 = 0; colour2 < 4; ++colour2)
{
char cardX1 = num1 + colour1 * 13;
char cardX2 = num2 + colour2 * 13;
string tmpX1({cardX1});
string tmpX2({cardX2});
string tmp;
//隐含num2<num1;
if (numY < num2)
{
tmp = tmpY + tmpX2 + tmpX1;
}
else if (numY < num1)
{
tmp = tmpX2 + tmpY + tmpX1;
}
else
{
tmp = tmpX2 + tmpX1 + tmpY;
}
_cardsmap.insert({tmp, lv});
}
}
}
}
lv -= 66;
}
}
}
lv += 66;
}
}
void Level::CreatMapTwoPair()
{
// lv=1609+66*13;
int lv = 2467;
//两对 从13种数号中选2种
for (int numY1 = 12; numY1 >= 1; --numY1) //大的对子
{
for (int numY2 = numY1 - 1; numY2 >= 0; --numY2) //小的对子
{
for (int colour1 = 0; colour1 <= 4 - 2; ++colour1)
{
for (int colour2 = colour1 + 1; colour2 <= 4 - 2 + 1; ++colour2)
{
for (int colour3 = 0; colour3 <= 4 - 2; ++colour3)
{
for (int colour4 = colour3 + 1; colour4 <= 4 - 2 + 1; ++colour4)
{
//确定两对每张牌的花色
char card0 = numY1 + colour1 * 13;
char card1 = numY1 + colour2 * 13;
//隐含colour2>colour1
string tmpY1({card0, card1});
char card2 = numY2 + colour3 * 13;
char card3 = numY2 + colour4 * 13;
string tmpY2({card2, card3});
for (int numX = 12; numX >= 0; --numX)
{
if (numX == numY1 || numX == numY2)
{
continue;
}
++lv;
for (int colour5 = 0; colour5 < 4; ++colour5)
{
char card4 = numX + colour5 * 13;
string tmpX({card4});
//隐含numY1>numY2;
string tmp;
if (numX > numY1)
{
tmp = tmpY2 + tmpY1 + tmpX;
}
else if (numX > numY2)
{
tmp = tmpY2 + tmpX + tmpY1;
}
else
{
tmp = tmpX + tmpY2 + tmpY1;
}
_cardsmap.insert({tmp, lv});
}
}
lv -= 11;
}
}
}
}
lv += 11;
}
}
}
void Level::CreatMapOnePair()
{
// lv=2467+78*11;
int lv = 3325;
//一对
for (int numY = 12; numY >= 0; --numY) //对子的数号
{
for (int colour1 = 0; colour1 <= 4 - 2; ++colour1)
{
for (int colour2 = colour1 + 1; colour2 <= 4 - 2 + 1; ++colour2)
{
//确定对子的花色组合
char card0 = numY + colour1 * 13;
char card1 = numY + colour2 * 13;
string tmpY({card0, card1});
//从剩余的12中选出3个点数出来
for (int num1 = 12; num1 >= 2; --num1)
{
for (int num2 = num1 - 1; num2 >= 1; --num2)
{
for (int num3 = num2 - 1; num3 >= 0; --num3)
{
if (num1 == numY || num2 == numY || num3 == numY)
{
continue;
}
++lv;
for (int colour3 = 0; colour3 < 4; ++colour3)
{
for (int colour4 = 0; colour4 < 4; ++colour4)
{
for (int colour5 = 0; colour5 < 4; ++colour5)
{
char card2 = num1 + colour3 * 13;
string tmpX1({card2});
char card3 = num2 + colour4 * 13;
string tmpX2({card3});
char card4 = num3 + colour5 * 13;
string tmpX3({card4});
string tmp;
//隐含num3<num2<num1
if (numY < num3)
{
tmp = tmpY + tmpX3 + tmpX2 + tmpX1;
}
else if (numY < num2)
{
tmp = tmpX3 + tmpY + tmpX2 + tmpX1;
}
else if (numY < num1)
{
tmp = tmpX3 + tmpX2 + tmpY + tmpX1;
}
else
{
tmp = tmpX3 + tmpX2 + tmpX1 + tmpY;
}
_cardsmap.insert({tmp, lv});
}
}
}
}
}
}
lv -= 220;
}
}
lv += 220;
}
}
void Level::CreatMapHighCard()
{
// lv=3325+220*13;
int lv = 6185;
//高牌
int times = 0;
for (int numX1 = 12; numX1 >= 4; --numX1)
{
for (int numX2 = numX1 - 1; numX2 >= 3; --numX2)
{
for (int numX3 = numX2 - 1; numX3 >= 2; --numX3)
{
for (int numX4 = numX3 - 1; numX4 >= 1; --numX4)
{
for (int numX5 = numX4 - 1; numX5 >= 0; --numX5)
{
if (numX5 + 1 == numX4 && numX4 + 1 == numX3 &&
numX3 + 1 == numX2 && numX2 + 1 == numX1)
{
continue;
}
if (numX5 == 0 && numX5 + 1 == numX4 && numX4 + 1 == numX3 &&
numX3 + 1 == numX2 && numX1 == 12)
{
continue;
}
++lv;
times = 0;
for (int colour1 = 0; colour1 < 4; ++colour1)
{
for (int colour2 = 0; colour2 < 4; ++colour2)
{
for (int colour3 = 0; colour3 < 4; ++colour3)
{
for (int colour4 = 0; colour4 < 4; ++colour4)
{
for (int colour5 = 0; colour5 < 4; ++colour5)
{
if (colour1 == colour2 && colour2 == colour3 &&
colour3 == colour4 && colour4 == colour5)
{
continue;
}
char card0 = numX1 + colour1 * 13;
char card1 = numX2 + colour2 * 13;
char card2 = numX3 + colour3 * 13;
char card3 = numX4 + colour4 * 13;
char card4 = numX5 + colour5 * 13;
//隐含numX5<numX4<numX3<numX2<numX1
string tmp({card4, card3, card2, card1, card0});
_cardsmap.insert({tmp, lv});
}
}
}
}
}
//会跳过同花顺,同花,顺子
}
}
}
}
}
}
- 行数有点超标,
VS的自动对齐 - 一套
屎山代码下来,我们 C(52,5) 的所有情况就全部插入到哈希表里了。 - 之后只需要用find()就可以获取到这种牌组合下,牌力是多少了。
显然每次打开游戏都取生成这个哈希表是很不合理的。
其次,7个字节乘以C(52,5)=2598960,也是13MB多的大小。这只是键值对所占的空间。
还记得吗,一开始笔者为了直接用C++自带的对string的哈希,把 unordered_map<key,val> 种的key定义的是string。
这样做确实省事了,但是有几个问题。
- string会占3个指针的大小,表里有2598960个元素,啧啧,直接用了45MB的堆空间。
- 虽然可以将键值对序列化(磁盘化),但是读出来也不可能直接读到 unordered_map 的哈希表里,只能一个一个插入。
除非再去把每个哈希值保存下来,序列化,然后每次读取的时候直接加载到 unordered_map 的哈希表里。- unordered_map 提供的string的哈希函数的边界很大很大,260万条数据太少了,浪费。
分析下来,目前的问题: C++提供的哈希表对于德州扑克来说,性能过剩了。而且不方便序列化存储和读取。
接下来就是有趣的部分了,如何去优化这个哈希表?
或者说,如何去优化这个哈希函数?
毕竟只要有哈希函数,我们可以自己实现哈希搜索。也可以很好的序列化(磁盘化),已经读取加载了。
优化哈希表
因为本篇文章真的写的太长了,所有具体哈希表的优化我会重开一篇。
文章链接如下:
优化哈希表的方法
时间记录
2022.11.22
尚未结束···