C++五子棋人机对战分析与总结
写在前面的话:
本人学代码才半年多点,智商有限,代码漏洞百出,还望大佬指教。这是我在csdn上的第一篇文章,分享下做这个项目的心得和过程。如有用语不当,还望大家指出,并多多包容。
说真的,一开始听到这个题目的时候没想到要做这么难的。两个月没敲代码了,编程水平赶不上假期,连一月份的社团叫做的贪吃蛇代码自己看着都赞叹之前的聪明了——因为看不懂了。
大一下第一次C++大作业老师抛了个五子棋的题目,并且要带有一定智能。智能!智能?要怎么才有智能呢?老师说遍历赋分在加和,主要就是写循环。这只是简单的逻辑,甚至过于简单了,但却让我有了思路。不过确实好难!之前做的贪吃蛇相当于只是一个开环的,没有反馈的(姑且让我这么说)。只要建立起数据结构与界面的映射就行。把蛇头,蛇身这两个数据搞明白,流程理顺,剩下的就只是调库和优雅的翻译了。(当时翻译的也不优雅hhh)
但这个五子棋的对数据结构和算法的要求就上了一个档次。算法还是第一次加入,听听就头大。不过还是挑战下自己吧,上次做那个贪吃蛇也是把我搞了要死不活的,最后还不是做出来了,而且只用了8天!那就不多说,开工!
先还是理流程,找思路,看看需要写哪些方面的类与对象。我初步写了下,暂时把算法skip掉。基本流程写出来了,就是游戏初始化–>画图–>玩家落子–>画图–>电脑落子–>画图然后反复。电脑要怎么落子呢?显然是找价值最大的点。那要找子就必须要有个范围。如果每一个点都去计算价值,就会大大影响效率。需要计算的点周围肯定是要有棋子的。那我要搞得高级一点,就定在5*5范围了。显然,遍历这个可以下棋的点比遍历所有点要快很多。为了后续遍历方便,这个容器中的点一定要按从左到右,从上到下的顺序排序。开始上手了,发现oop忘记了好多,被迫重新点开黑马程序员的视频,也看了下之前的贪吃蛇的代码。这次代码要吸取上次写的有点混乱的教训,争取写得优雅一些,能让别人比较清晰的看懂。先是发现找子范围的那个容器很不好写,但后来发现可以用插入排序的思想。然后就这样磕了一周,框架差不多出来了,终于要磕算法了。我上网扫描了下,发现有个程序员给的教学代码的算法只是遍历了连着的情况,这样肯定会错过一些好棋。那我就把空位写上去。确实,我自己下五子棋的时候还是经常下跳三,跳四那些棋的。算法和那个容器一样,感觉好难写。我不想让上下斜这四个方向产生的分数进行简单叠加。于是我联想下五子棋时我自己对棋子的价值的评判。我下了一颗子A,A既成4,又形成一个活2;另一颗子B,成了两个活2。A的价值增益比起只成4的价值增益应该不大,但是B的价值增益比起只成一个2的价值增益就大了好多。从数学上可以抽象出:两个数叠加的时候,如果相近,它们的和可以比原来的较大数大好多;反之则不比较大数大多少。例如:
f
(
20
,
20
)
=
100
;
f
(
100
,
10
)
=
120.
f(20,20) = 100; f(100,10) = 120.
f(20,20)=100;f(100,10)=120. 前者比较大数大五倍,后者只大1.2倍。这就有了后面的公式:
f
(
M
,
m
)
=
M
+
m
∗
M
I
N
(
m
,
5
)
f(M,m) = M + m*MIN(\sqrt{m},5)
f(M,m)=M+m∗MIN(m,5)
现在长吁一口气,做完了!用时差不多加起来就10天,虽然比起一会儿写出来的大佬来说很菜,但也还算满意的了。之前去学stm32做机器人,又拖队友后腿又郁闷又没有进步,这次做这个项目才10天,重拾被遗忘的编程能力,代码也规范了些,最主要的是没学数据结构与算法就摸索出一些东西了!不过管他的,学习本来就是在不断探索中,不断了解自己中进步的啦。
效果展示
C++五子棋效果展示(QQ录屏原因造成准心偏移)
思路分析
0. 说明:
画图库:opencv,鼠标操作进行落子,调用了opencv提供的回调函数
void Chess::my_mouse_callback(int event, int x, int y, int flag, void* param);
setMouseCallback("io_img", Chess::my_mouse_callback, void*);
1. 流程:
注意:在这个代码中,流程与画图在回调函数与主函数中都有体现,需要结合着看。
2. OOP设计:
这里只简述部分重要的,体现整体思路的,函数大多没给出完整的声明
点类Mypoint:存放点的坐标,提供operator<, operator==, operator>
大小关系:规定y小的小,y相同时x小的小;反之则大
class MyPoint
{
friend ostream& operator<< (MyPoint chess, ostream& cout);
public:
int x;
int y;
//运算符重载,比较两个点谁更小,方便排序
//优先比较纵坐标,纵坐标相同的情况下横坐标小的点更小,等于,大于依此类推
bool operator<(MyPoint p);
bool operator==(MyPoint p);
bool operator>(MyPoint p);
MyPoint();
MyPoint(int x, int y);
};
棋子类Pieces:存放棋子位置(点),棋子的颜色status(黑与白)
//棋子的类
class Pieces
{
friend class Chess;
protected:
bool p_color; //每个棋子的颜色。定义黑棋为1,白棋为0
MyPoint p_point; //棋子的位置
public:
Pieces();
Pieces(bool color, MyPoint p);
~Pieces();
MyPoint getPoint();
bool getColor();
};
空类Vacancy:存放空的位置(点),空的分数,优先级(后面介绍)
简单说下,这里的相同方,不同方是为了后面赋分的时候统一为两种情况:
- 相同方:黑棋下的时候评估黑棋,白棋下的时候评估白棋
- 不同方:黑棋下的时候评估白棋,白棋下的时候评估黑棋
class Vacancy
{
friend class Chess;
protected:
MyPoint c_point; //可能落子的位置
double value = 0; //这个位置的分数,默认值为0
int d_priority = 0; //不同方棋子的优先级
int m_priority = 0; //相同方棋子的优先级
public:
Vacancy(MyPoint p,int v);
~Vacancy();
MyPoint get_point();
};
棋局类chess:Pieces容器chessman,用于存放所有棋子;
Vacancy容器choices,用于存放所有可能下棋的空 位,由chessman直接确定;
游戏模式game_mode,人机还是人人;
游戏界面Mat::gamewindow;
对战结果result;
刚下的一颗棋子last_piece
接口函数有更新choices的upgrade_vacancy();
切换轮 次toggle_turn(),;
绘制图样函数draw_game(Mat:: &io_img);
计算最优落子点 get_each_dir()和get_all() ;
嵌套结构体AI
结构AI: 分数,优先级,结构Each_dir,存放每一边(上下、左右、左上右下,右上左下4边)的相关信息。
提供AI& operator+= (Each_dir element)用于相加每一边的信息(当然不是简单的线性相加)
const int LENGTH = 14;
const int WIDTH = 14; //棋盘大小为15*15
class Chess
{
friend class Pieces;
friend class Vacancy;
friend int main(); //main函数做友元
friend void game_init(Chess& this_chess);
private:
bool status; //玩家身份(黑棋为1,白棋为0)
bool my_turn; //轮次。玩家轮次为1,电脑轮次为0
vector<Pieces> chessman; //存放所有的棋子
vector<Vacancy> choices; //存放可能的落子空位以及每个位置的价值。
//可能落点定义为所有棋子周围5*5区域(不包含任何棋子)与板子大小的交集
Pieces last_pieces; //刚刚落下的棋子
bool result = 0; //出现胜负置1,未出现胜负保持0(默认值)
bool isIwin = 1; //人机模式下辅助判断输赢。玩家赢置位1,输置位0
vector<Vacancy>::iterator get_lastpoint_ite(vector<Vacancy>& m_chessman);
static const vector<Scalar> board_color; //棋盘的颜色
static const vector<Scalar> general_color; //黑,白,红
void draw_pieces(Mat &io_img);
void draw_last_piece(Mat &io_img);
public:
bool game_mode; //对战模式。0为对弈,1为人机
static bool game_pause;
Chess();
Mat gameWindow;
//Chess::AI s;
//在chessman中插入新的棋子,并将棋盘上的棋子按照坐标排序,便于管理。
//返回1为成功插入
bool insert_pieces(Pieces new_pieces);
//更新空位,同样空位也按顺序排列好
void upgrade_vacancy();
void set_mode(bool mode); //游戏开始时设置对战模式,人机,人人
void win_or_lost(); //出现输赢后打印结果,结束游戏,等待玩家重新开始或退出
bool is_my_turn();
void set_status(bool my_status); //设置玩家执黑棋(1),白棋(0)
int get_status(MyPoint pos); //输入一个坐标,返回-1(空),0(白棋),1(黑棋)
void toggle_turn(); //改变轮次,切换身份
void draw_game(Mat &io_img); //画图的函数
static void draw_line(Mat& io_img);
static void draw_board(Mat& io_img);
static void my_mouse_callback(int event, int x, int y, int flag, void* param); //opencv提供的鼠标操作的回调函数
struct AI
{
public:
struct Each_side //每一边(一定要初始化)
{
int m_s = 0; //我方棋子数(AI方)
int e_s = 0; //空格数
int d_s = 0; //敌方棋子数(玩家方)
};
struct Each_dir //每一个方向(两边值的合计)
{
int m_d = 0;
int e_d = 0;
int d_d = 0;
int m_sub_priority = 0; //子优先级
int d_sub_priority = 0;
double each_dir_val = 0; //子分数
bool m_flag3 = false; //相同视角方是否有活3
bool m_flag4 = false; //相同视角方是否有活4
bool d_flag3 = false; //不同视角方是否有活3
bool d_flag4 = false; //不同视角方是否有活4
};
Each_dir all_dir_val[4]; //四个方向的值
bool m_flag3 = false; //相同视角方是否有活3
bool m_flag4 = false; //相同视角方是否有活4
bool d_flag3 = false; //不同视角方是否有活3
bool d_flag4 = false; //不同视角方是否有活4
double all_val = 0; //总分数
int m_priority = 0; //相同视角方总优先级
int d_priority = 0; //不同视角方总优先级,两个优先级一样时相同视角方视为更大(先手优势)
static double my_max(double a, double b);
static int my_max(int a, int b);
static double my_min(double a, double b); //最小值就只提供double型的了
AI& operator+=(Each_dir element); //核心算法函数之一,用运算符重载去实现会漂亮一点
};
//核心算法函数,对空位的价值进行评估,智能程度取决于这个函数及其AI& operator+=(Each_dir element)
//myview为1表示对友方(AI)进行评估,反之对敌方进行评估
//为了减少代码量,新增参数myview
AI::Each_dir get_each_dir(int x, int y, vector<Vacancy>::iterator it, bool myview);
//调用上面的函数,实现对一个点所有方向进行评估
AI get_all(vector<Vacancy>::iterator it);
//调用上面的两个函数及其其他函数,用于下棋
void AI_plays();
};
3. 数据:
-
存放所有棋子的
vector<Pieces> chessman
数据插入:先检查新的数据是否合法。鼠标在格点上,且该点与之前的所有棋子的点不重合。若合法,用 operator<,operator>比较,按顺序插入,这样就像插入排序那样,chessman中的棋子按位置排列好了。
bool Chess::insert_pieces(Pieces new_pieces)
{
//棋局开始容器为空,直接添加元素
if (chessman.empty())
{
chessman.push_back(new_pieces);
return true;
}
else if( new_pieces.getPoint() < chessman[0].getPoint()) //首插
{
chessman.insert(chessman.begin(), new_pieces);
return true;
}
else if (new_pieces.getPoint() > chessman[chessman.size() - 1].getPoint()) //尾插
{
chessman.push_back(new_pieces);
return true;
}
//如果新的元素大于上一个值,小于下一个值(或者没有下一个值),添加新元素
else
{
for (vector<Pieces>::iterator it = chessman.begin(); it != (chessman.end() - 1); it++)
{
if (it->getPoint() < new_pieces.getPoint() && ((it+1)->getPoint() > new_pieces.getPoint())) //为什么这里调用了Pieces的析构函数
{
chessman.insert(it+1, new_pieces);
return true;
}
}
}
//程序如果执行到这里说明前面有问题
cerr << "ERROR in function Chess::insert_pieces" << endl;
return false;
}
-
存放所有可下棋空位的
vector<Vacancy> choices
数据更新:每一次落子完成,我们只需要看新落的子周围的空是否在choices中,如果不在则按位置的顺序插 入(包括该棋子本身所在的空),然后从choices中erase掉这个空(下了棋的地方不能落子)。我们先把这个新的点当作空位判断是否插入,可以保证在后面一定需要erase,就省了很多麻烦。
代码如下:
//更新完棋子的容器后,更新空的容器。先更新所有的空位,再更新这些空位的值
//采用插入排序的思想
void Chess::upgrade_vacancy()
{
//当上一个Choice为空,无法遍历先前的Choice作为插入排序的指标,因此直接更新空位
if (chessman.size() == 1)
{
for (int r = -2; r <= 2; r++)
{
if (r + chessman[0].getPoint().y < 0 || r + chessman[0].getPoint().y > WIDTH) { continue; } //如果遍历的行超出棋盘边界
for (int c = -2; c <= 2; c++)
{
if (c + chessman[0].getPoint().x < 0 || c + chessman[0].getPoint().x > LENGTH) { continue; }
else
{
Vacancy temp(MyPoint(c + chessman[0].getPoint().x, r + chessman[0].getPoint().y),0); //value先设为默认值0
choices.push_back(temp);
}
}
}
choices.erase(get_lastpoint_ite(choices));
}
else
{
for (int r = -2; r <= 2; r++)
{
if (r + last_pieces.getPoint().y < 0 || r + last_pieces.getPoint().y > WIDTH) { continue; } //如果遍历的行超出棋盘边界
for (int c = -2; c <= 2; c++)
{
if (c + last_pieces.getPoint().x < 0 || c + last_pieces.getPoint().x > LENGTH) { continue; }
else
{
MyPoint temp_point = MyPoint(c + last_pieces.getPoint().x, r + last_pieces.getPoint().y);
if (temp_point < choices[0].c_point) //首插
{
choices.insert(choices.begin(), Vacancy(temp_point, 0));
}
else if (temp_point > choices[choices.size() - 1].c_point) //尾插
{
choices.push_back(Vacancy(temp_point, 0));
}
else //此时temp_point在choices表的中间
{
for (vector<Vacancy>::iterator it = choices.begin(); it != choices.end(); it++)
{
if (it->c_point == temp_point)
{ break; }
else
//尾插操作已经完成,it->c_point一定不超过temp_point,执行到这里可保证数组不越界
{
bool j1 = (it->c_point < temp_point);
bool j2 = ((it + 1)->c_point > temp_point);
bool j3 = ((it + 1)->c_point < temp_point);
bool j4 = ((it + 1)->c_point == temp_point);
if (it->c_point < temp_point && (it + 1)->c_point > temp_point)
{
choices.insert(it+1, Vacancy(temp_point, 0));
break;
}
}
}
}
}
}
}
//下面算法实现找出并删除choices中与chessman重合的元素,时间复杂度为O(n)。
//(没学过数据结构,我摸索的写的,可能不对)
vector<Vacancy>::iterator itc = choices.begin();
for (vector<Pieces>::iterator itp = chessman.begin(); itp!= chessman.end();itp++)
{
while (itc != choices.end()&&itp->p_point > itc->c_point )
{ itc++; }
if (itc == choices.end()) { break; } //此时已经删完了,为了避免数组越界,可以返回
if (itp->p_point == itc->c_point)
{
itc = choices.erase(itc); //erase后迭代器必须返回当前删除元素的后继元素!!!
continue;
}
else if (itp->p_point < itc->c_point) { continue; }
}
}
}
4. 算法:
简述:遍历找子,赋分
对于每个choices中的元素,遍历左右,上下,左上右下,右上左下这四个方向,每个方向找出友方棋子,敌方棋子,空的个数。当遇到敌方棋子时d进行自加直接返回。(因为后面的棋子是你的还是敌方的不再产生影响,因为连不起来了)注意到跳2,跳3等这些棋的空位数只是1,所以当空位超过1时,直接返回。然后遇到友方棋子进行自加。在每个方向的统计过程中,我把这个方向分成两个单方向进行计数,计数范围为[i+1,i+4] (i为这个需要评估价值的点)每种需要赋值的情况及其价值,优先级如下表
敌我赋分的统一:
相同的棋(例如先手的活三与后手的活三),先手的价值肯定更大(你肯定要进攻啊),为了简便,我考虑让评分函数的评估视角作为一个参数,而不是两个函数(一个从己方视角,一个从对面视角)。这就有了函数 Chess::AI::Each_dir Chess::get_each_dir(int x, int y,vector<Vacancy>::iterator it,bool myview)
,myview就是表示视角。x,y表示每次遍历时坐标变化的量。比如上下就取(0,1)或者(0,-1),右上左下就取(1,1)或(-1,-1)。
优先级的加入:
有些棋是必杀或者必须应的。好比三三,三四,活四。遇到这些情况时,则需要优先行棋,无论 分数高低。显然,成5的优先级高于成4高于三三,这样,优先级需要排个序。(例如对面要成5了,你可以成三四,你肯定得去堵对面)又注意到相同优先级时也有差异:比如到我下,我可以成双四,对面也可以成双四,虽然这两个点优先级时一样的,但是我是先手,我比对面快,肯定先成双四。因此,优先级得分成敌我双方两种(或者设定更细的优先级,其实这个更简单),优先级大的先下(敌我双方优先级的最大值),如果优先级相同,看我方优先级是否更高,如果满足则先下,否则才比较分数。事实证明,这样处理后ai完全不会错过绝杀棋或必应棋子,这种处理就由于只采用分数进行判断!
代码如下:
Chess::AI::Each_dir Chess::get_each_dir(int x, int y, vector<Vacancy>::iterator it,bool myview)
{
MyPoint temp = it->c_point;
MyPoint copy = temp;
AI::Each_side both_sides_val[2]; //每个方向的两边
//bool my_chess = bool(this->status == (bool)get_status(temp)); //为友方棋子
//bool flag = bool(my_chess == myview); //flag为1时表示友方棋子友方视角评估或敌方棋子敌方视角评估
int a = x; int b = y; //从而可以统一两种情况
for (int i = 0; i < 4; i++) //至多遍历4个点
{
temp.x += x;
temp.y += y;
bool my_chess = bool(this->status == (bool)get_status(temp)); //为友方棋子
bool flag = bool(my_chess == myview); //flag为1时表示友方棋子友方视角评估或敌方棋子敌方视角评估
if (temp.x < 0 || temp.x > LENGTH || temp.y < 0 || temp.y > WIDTH) { both_sides_val[0].d_s++; break; } //越界
else if (get_status(temp) == -1) //为空
{
bool add_flag = 0;
if (both_sides_val[0].e_s == 1) { add_flag = 0; }
else if (temp.x + x < 0 || temp.x + x > LENGTH || temp.y + y < 0 || temp.y + y > WIDTH) { add_flag = 0; }
else if (get_status(MyPoint(temp.x + x, temp.y + y)) == -1) { add_flag = 0; }
else if (myview == (bool(this->status == (bool)get_status(MyPoint(temp.x + x, temp.y + y))))) { add_flag = 1; }
if (add_flag) { both_sides_val[0].e_s++; }
else { break; }
}
//当myview为1时,对电脑棋子进行评估
//this->status为玩家身份,取反后为电脑身份(1为白,0为黑)。 get_status返回0(白棋),1(黑棋)
//else if (this->status == (bool)get_status(temp)) { both_sides_val[0].d_s++; break; } //为敌方(玩家)棋子
//else if (this->status != (bool)get_status(temp)) { both_sides_val[0].m_s++; } //为友方(AI)棋子
else if (!flag) { both_sides_val[0].d_s++; break; } //为status和myside不同的棋子
else if (flag) { both_sides_val[0].m_s++; } //为status和myside相同的棋子
else //运行到这里说明前面有问题!
{
cerr << "ERROR IN get_each_dir(int x, int y, vector<Vacancy>::iterator it)1111" << endl;
}
}
both_sides_val[1].e_s = both_sides_val[0].e_s; //两边的空的总和之多为1,故保留前一个的empty值
temp = copy;
for (int i = 0; i < 4; i++) //至多遍历4个点
{
temp.x -= x;
temp.y -= y;
bool my_chess = bool(this->status == (bool)get_status(temp)); //为友方棋子
bool flag = bool(my_chess == myview); //flag为1时表示友方棋子友方视角评估或敌方棋子敌方视角评估
if (temp.x < 0 || temp.x > LENGTH || temp.y < 0 || temp.y > WIDTH) { both_sides_val[1].d_s++; break; } //越界
else if (get_status(temp) == -1) //为空
{
bool add_flag = 0;
if (both_sides_val[1].e_s == 1) { add_flag = 0; }
else if (temp.x-x < 0 || temp.x-x > LENGTH || temp.y-y < 0 || temp.y-y > WIDTH) { add_flag = 0; }
else if (get_status(MyPoint(temp.x - x, temp.y - y)) == -1) { add_flag = 0; }
else if (myview == (bool(this->status == (bool)get_status(MyPoint(temp.x - x, temp.y - y))))) { add_flag = 1; }
if (add_flag) { both_sides_val[1].e_s++; }
else { break; }
}
else if (!flag) { both_sides_val[1].d_s++; break; } //为status和myside不同的棋子
else if (flag) { both_sides_val[1].m_s++; } //为status和myside相同的棋子
else //运行到这里说明前面有问题!
{
cerr << "ERROR IN get_each_dir(int x, int y, vector<Vacancy>::iterator it)1112" << endl;
}
}
it->c_point;
AI::Each_dir this_dir;
this_dir.m_d = both_sides_val[0].m_s + both_sides_val[1].m_s;
this_dir.d_d = both_sides_val[0].d_s + both_sides_val[1].d_s;
this_dir.e_d = both_sides_val[1].e_s; //空位至多为1,而且both_sides_val[1].e_s是二者中最大的
//下面根据对每种情况的棋子的价值进行评估,对val进行赋值
//用于双三、双四、三四设定优先级
if (myview) //对相同视角进行评估
{
if (this_dir.m_d == 1 && this_dir.e_d == 0 && this_dir.d_d == 0)
{ this_dir.each_dir_val = 10; } //连2
else if (this_dir.m_d == 1 && this_dir.e_d == 1 && this_dir.d_d == 0)
{ this_dir.each_dir_val = 8; } //跳2
else if (this_dir.m_d == 2 && this_dir.e_d == 0 && this_dir.d_d == 1)
{ this_dir.each_dir_val = 25; } //死3
else if (this_dir.m_d == 2 && this_dir.e_d == 1 && this_dir.d_d == 1)
{ this_dir.each_dir_val = 25; } //死跳3
else if (this_dir.m_d == 2 && this_dir.e_d == 0 && this_dir.d_d == 0)
{ this_dir.each_dir_val = 100; this_dir.m_flag3++; } //连3
else if (this_dir.m_d == 2 && this_dir.e_d == 1 && this_dir.d_d == 0)
{ this_dir.each_dir_val = 80; this_dir.m_flag3++; } //跳3
else if (this_dir.m_d == 3 && this_dir.e_d == 0 && this_dir.d_d == 1)
{ this_dir.each_dir_val = 120; this_dir.m_flag4++; } //死4
else if (this_dir.m_d == 3 && this_dir.e_d == 1 && this_dir.d_d == 1)
{ this_dir.each_dir_val = 90; this_dir.m_flag4++; } //死跳4
else if (this_dir.m_d == 3 && this_dir.e_d == 0 && this_dir.d_d == 0)
{ this_dir.each_dir_val = 5000; this_dir.m_sub_priority = 2; } //连4
else if (this_dir.m_d == 3 && this_dir.e_d == 1 && this_dir.d_d == 0)
{ this_dir.each_dir_val = 90; this_dir.m_flag4++; } //跳4
else if (this_dir.m_d >= 4 && this_dir.e_d == 0)
{ this_dir.each_dir_val = 100000; this_dir.m_sub_priority = 3;} //绝杀5 (包括长连)
else if (this_dir.m_d >= 4 && this_dir.e_d == 1)
{ this_dir.each_dir_val = 300; this_dir.m_flag4++;} //长4
return this_dir;
}
else //对不同视角方进行评估
{
if (this_dir.m_d == 1 && this_dir.e_d == 0 && this_dir.d_d == 0)
{ this_dir.each_dir_val = 8; } //连2
else if (this_dir.m_d == 1 && this_dir.e_d == 1 && this_dir.d_d == 0)
{ this_dir.each_dir_val = 5; } //跳2
else if (this_dir.m_d == 2 && this_dir.e_d == 0 && this_dir.d_d == 1)
{ this_dir.each_dir_val = 15; } //死3
else if (this_dir.m_d == 2 && this_dir.e_d == 1 && this_dir.d_d == 1)
{ this_dir.each_dir_val = 15; } //死跳3
else if (this_dir.m_d == 2 && this_dir.e_d == 0 && this_dir.d_d == 0)
{this_dir.each_dir_val = 50; this_dir.d_flag3++;} //连3
else if (this_dir.m_d == 2 && this_dir.e_d == 1 && this_dir.d_d == 0)
{this_dir.each_dir_val = 40; this_dir.d_flag3++;} //跳3
else if (this_dir.m_d == 3 && this_dir.e_d == 0 && this_dir.d_d == 1)
{this_dir.each_dir_val = 60; this_dir.d_flag4++;} //死4
else if (this_dir.m_d == 3 && this_dir.e_d == 1 && this_dir.d_d == 1)
{this_dir.each_dir_val = 45; this_dir.d_flag4++;} //死跳4
else if (this_dir.m_d == 3 && this_dir.e_d == 0 && this_dir.d_d == 0)
{this_dir.each_dir_val = 2000; this_dir.d_sub_priority = 2;} //连4
else if (this_dir.m_d == 3 && this_dir.e_d == 1 && this_dir.d_d == 0)
{this_dir.each_dir_val = 45; this_dir.d_flag4++;} //跳4
else if (this_dir.m_d >= 4 && this_dir.e_d == 0)
{this_dir.each_dir_val = 30000; this_dir.d_sub_priority = 3;} //绝杀5 (包括长连)
else if (this_dir.m_d >= 4 && this_dir.e_d == 1)
{this_dir.each_dir_val = 200; this_dir.d_flag4++;} //长4
return this_dir;
}
}
下面是运用上述函数对四个方向进行评估
Chess::AI Chess::get_all(vector<Vacancy>::iterator it) //唯一向外提供的接口
{
AI temp_ai;
for (int turn = 0; turn < 2; turn++)
{
temp_ai += get_each_dir(-1,0, it, (bool)turn);
temp_ai += get_each_dir(-1,1, it, (bool)turn);
temp_ai += get_each_dir(0, 1, it, (bool)turn);
temp_ai += get_each_dir(1 ,1, it, (bool)turn);
}
it->value = temp_ai.all_val;
it->m_priority = temp_ai.m_priority;
it->d_priority = temp_ai.d_priority;
return temp_ai;
}
分数表
情形 | 我方(先手) | 后手(敌方) | 优先级 | 所有情况及其实现 |
---|---|---|---|---|
连2 | 10 | 8 | 0 | m1 e0 d0 |
跳2 | 8 | 5 | 0 | m1 e1 d0 |
死3 | 25 | 15 | 0 | m2 e0 d1 |
死跳3 | 25 | 15 | 0 | m2 e1 d1 |
连3 | 100 | 50 | 0 | m2 e0 d0 |
跳3 | 80 | 40 | 0 | m2 e1 d0 |
死4 | 120 | 60 | 0 | m3 e0 d1 |
死跳4 | 90 | 45 | 0 | m3 e1 d1 |
连4 | 5000 | 2000 | 2 | m3 e0 d0 |
跳4 | 250 | 150 | 0 | m3 e1 d0 |
长4 | 300 | 200 | 0 | mn e1 d0 |
绝杀5 | 100000 | 30000 | 3 | m4 e0 |
双三 | 这些计算得到 | 1 | ||
三四 | 2 | |||
双四 | 2 |
这里我为了简单就没有设置黑棋的双三,双四,长连禁手了。其实也不难实现,一个思路是可以设置个-1的优先级。
每两边信息的叠加
函数实现: AI& operator+=(Each_dir element);
返回AI类的对象
敌我优先级叠加时取最大值,遇到活三,死四时给flag_3,flag_4自加,如果flag_3>2,说明双三;
flag_3>1,flag_4>1说明三四;
flag_4>2说明双四。
最后根据双三,双四,三四去赋值优先级(如果原来的优先级更大则保持不变)
分数叠加时正如“写在前面的话”中分析的:
我下了一颗子A,A既成4,又形成一个活2;另一颗子B,成了两个活2。A的价值增益比起只成4的价值增益应该不大,但是B的价值增益比起只成一个2的价值增益就大了好多。
从数学上可以抽象出:两个数叠加的时候,如果相近,它们的和可以比原来的较大数大好多,表现得像乘法;反之则不比较大数大多少,表现得像加法。
其实还有一点,如果两个数都很大时又相近时公式1得到的值会很大,由于函数经过不断测试,得出以下公式(M为较大数,m为较小数):
公式(第一代)
f ( M , m ) = M + m ∗ m f(M,m) = M + m*\sqrt{m} f(M,m)=M+m∗m
公式(第二代)
f ( M , m ) = M + m ∗ M I N ( m , 5 ) f(M,m) = M + m*MIN(\sqrt{m},5) f(M,m)=M+m∗MIN(m,5)
可能有人要问没出现乘法还怎么表现为两数相乘?其实也不好回答,这个函数我用了两数的比的算术平方根作为加权和乘法拟合后就消去max*min这一项了。第二代公式虽然简洁,但是实践证明挺有用的,希望大佬们可以给出更有效的叠加的公式
代码实现:
Chess::AI& Chess::AI::operator+=(Each_dir element)
{
//先更新优先级
//双三,双四,三四优先级设定
if (this->d_flag3 && element.d_flag3) { this->d_priority = my_max(this->d_priority, 1); }
if (this->d_flag3 && element.d_flag4) { this->d_priority = my_max(this->d_priority, 2); }
if (this->d_flag4 && element.d_flag4) { this->d_priority = my_max(this->d_priority, 2); }
if (this->m_flag3 && element.m_flag3) { this->m_priority = my_max(this->m_priority, 1); }
if (this->m_flag3 && element.m_flag4) { this->d_priority = my_max(this->m_priority, 2); }
if (this->m_flag4 && element.m_flag4) { this->m_priority = my_max(this->m_priority, 2); }
this->m_priority = my_max(this->m_priority, element.m_sub_priority);
this->d_priority = my_max(this->d_priority, element.d_sub_priority);
this->d_flag3 |= element.d_flag3; //简洁明了
this->d_flag4 |= element.d_flag4;
this->m_flag3 |= element.m_flag3;
this->m_flag4 |= element.m_flag4;
//再更新分数
double temp_max = my_max(this->all_val, element.each_dir_val);
double temp_min = my_min(this->all_val, element.each_dir_val);
this->all_val = temp_max + temp_min*my_min(pow(temp_min, 0.5),5.0); //核心赋分函数
return *this;
}
5. 改进:
- 黑棋禁手(一个方法:直接把优先级置-1)
- 提供更多游戏操作功能,比如说悔棋,提示,计分,计时,难度、音乐、音效、绝杀特效
- 算法上可以实现多轮下棋计算,对一个子能计算好几步(这方面我还完全不懂,应该要学了数据结构与算法吧)
- 联网对局
6. 参考资料:
评分数据参考自 程序员Rock的 C++五子棋人机对战
小萌新的代码在大佬面前直打哆嗦