今天我看到有人fork了我的这个项目,其实怪不好意思的,因为现在看来,这个项目写的实在不咋地。我后来又用java swing写了一个人工智能五子棋,并对算法进行了优化,如果想fork的话,还是fork这个新的项目吧
Github链接:gobang-ver4.0-JavaSwing
这次的五子棋AI是我写的第三版,前两版因为自己的开发项目不充足,导致最后写出的程序Bug众多,无法修改,第三版重新设计了算法,并使用了多文件结构,最后顺利完成了。
根据之前查询的资料我了解到,要想写出无可匹敌的五子棋AI,要用到博弈树、AlphaBeta减枝法、机器学习等知识,但是因为个人水平的原因,这些都无法实现,不过我的算法起码能保证玩家稍不留神,就会被打败,这和五子棋新手博弈、OJ霸榜,都足够了。等到日后自己的知识量增加了,再来对这个程序进行完善吧。
这里先展示一下最后的效果
程序下棋的水平还是可以的,日常拿来消遣一下也没什么问题。
在设计五子棋算法之前,要对五子棋的算法有基本的了解,这里我借用别人的文章来介绍。原文链接
最常见的基本棋型大体有以下几种:连五,活四,冲四,活三,眠三,活二,眠二。
①连五:顾名思义,五颗同色棋子连在一起,不需要多讲。
②活四:有两个连五点(即有两个点可以形成五),图中白点即为连五点。
稍微思考一下就能发现活四出现的时候,如果对方单纯过来防守的话,是已经无法阻止自己连五了。
③冲四:有一个连五点,如下面三图,均为冲四棋型。图中白点为连五点。
相对比活四来说,冲四的威胁性就小了很多,因为这个时候,对方只要跟着防守在那个唯一的连五点上,冲四就没法形成连五。
![]() 图3-1 |
![]() 图3-2 |
![]() 图3-3 |
④活三:可以形成活四的三,如下图,代表两种最基本的活三棋型。图中白点为活四点。
活三棋型是我们进攻中最常见的一种,因为活三之后,如果对方不以理会,将可以下一手将活三变成活四,而我们知道活四是已经无法单纯防守住了。所以,当我们面对活三的时候,需要非常谨慎对待。在自己没有更好的进攻手段的情况下,需要对其进行防守,以防止其形成可怕的活四棋型。
![]() 图4-1 |
![]() 图4-2 |
⑤眠三:只能够形成冲四的三,如下各图,分别代表最基础的六种眠三形状。图中白点代表冲四点。眠三的棋型与活三的棋型相比,危险系数下降不少,因为眠三棋型即使不去防守,下一手它也只能形成冲四,而对于单纯的冲四棋型,我们知道,是可以防守住的。
![]() 图5-1 |
图5-2 |
![]() 图5-3 |
![]() 图5-4 |
![]() 图5-5 |
![]() 图5-6 |
如上所示,眠三的形状是很丰富的。对于初学者,在下棋过程中,很容易忽略不常见的眠三形状,例如图5-6所示的眠三。
有新手学了活三眠三后,会提出疑问,说活三也可以形成冲四啊,那岂不是也可以叫眠三?
会提出这个问题,说明对眠三定义看得不够仔细:眠三的的定义是,只能够形成冲四的三。而活三可以形成眠三,但也能够形成活四。
此外,在五子棋中,活四棋型比冲四棋型具有更大的优势,所以,我们在既能够形成活四又能够形成冲四时,会选择形成活四。
温馨提示:学会判断一个三到底是活三还是眠三是非常重要的。所以,需要好好体会。
后边禁手判断的时候也会有所应用。
⑥活二:能够形成活三的二,如下图,是三种基本的活二棋型。图中白点为活三点。
活二棋型看起来似乎很无害,因为他下一手棋才能形成活三,等形成活三,我们再防守也不迟。但其实活二棋型是非常重要的,尤其是在开局阶段,我们形成较多的活二棋型的话,当我们将活二变成活三时,才能够令自己的活三绵绵不绝微风里,让对手防不胜防。
![]() 图6-1 |
![]() 图6-2 |
![]() 图6-3 |
⑦眠二:能够形成眠三的二。图中四个为最基本的眠二棋型,细心且喜欢思考的同学会根据眠三介绍中的图2-13找到与下列四个基本眠二棋型都不一样的眠二。图中白点为眠三点。
![]() 图7-1 |
![]() 图7-2 |
![]() 图7-3 |
![]() 图7-4 |
在介绍算法之前,要将程序里的一些基本概念介绍一下。
标准五子棋棋盘一般是15X15的大小(当然程序里可以随意改)。
我后面会将连五,活四,冲四,活三,眠三,活二,眠二称为特殊棋型,这样可以节约博客字数。
对于棋盘上的排成直线的一串棋子,有横、竖、左上到右下,左下到右上四种方向,我将其命名为 横(LAY)、竖(STAND)、主对角(MAIN)、副对角(VICE)。
对于某一个方向上的排成一条直线的棋子,我称其为棋链,如下方的图就是一个棋链
这个棋链是竖向(STAND)的,当然,棋链也可以是横向(LAY)、主对角向(MAIN)的和副对角向(VICE)的。
程序中有当前棋子上一个、下一个的概念,LAY的后一个是右一个;STAND的后一个是往下一个;MAIN的下一个是往右下一个;VICE的下一个是往右上一个。
程序的算法概括起来讲,是找到棋盘上的空位,首先假设该空位上放上棋子,然后计算这个位置四个方向上的连五,活四,冲四,活三,眠三,活二,眠二的个数,并根据这些棋型由高到低进行赋分。
当为所有空位赋完分后,遍历所有空位,找到分值最大的空位,并在该空位上放上棋子。
下面对算法的细节进行更为详细的介绍:
在获取空位四个方向的棋子时不但要注意获得的棋子长度,还要注意该空位在棋链中的位置,拿一个长度为5的连5棋链来说,该空位可能位于第1或2或…第5个位置,也就是说,判断一个空位在四个方向、五个位置是否为连5,要取得4X5共20个棋链,,而一条棋链最长长度为7,也就是说,获取一个空位的棋型数,最坏一共要取得4X7=28条棋链。下面我通过代码形象地展示全过程。
getChessChain()返回空位棋子ce、LAY,STAND,MAIN,VICE四个方向、长度为n,棋链位置为loc的四条棋链
std::vector<std::vector<Chess>> getChessChain(Chess ce,int n,int loc);
下面四个函数分别返回长度为four,five,six,seven;空位棋子为ce、方向为dir、位置为loc的一条棋链
std::vector<Chess> fourChain(Chess ce, int dir, int loc);
std::vector<Chess> fiveChain(Chess ce, int dir, int loc);
std::vector<Chess> sixChain(Chess ce, int dir, int loc);
std::vector<Chess> sevenChain(Chess ce, int dir, int loc);
下面7个函数分别返回长度为one,two,three,four,five,six,seven;空位棋子为ce、长度为n,方向为dir的的一条棋链
std::vector<Chess> inDirOne(Chess ce,int n,int dir);
std::vector<Chess> inDirTwo(Chess ce,int n,int dir);
std::vector<Chess> inDirThree(Chess ce,int n,int dir);
std::vector<Chess> inDirFour(Chess ce,int n,int dir);
std::vector<Chess> inDirFive(Chess ce,int n,int dir);
std::vector<Chess> inDirSix(Chess ce,int n,int dir);
std::vector<Chess> inDirSeven(Chess ce,int n,int dir);
上面的函数,是由下到上,层层递进的,我的一位老师之前问我,为什么我的程序中的成员函数多有相同,那是因为这些函数看起来相似,但每个都有其特殊性,这些特殊性是没法通过一个函数封装的。
获取一条棋链后,该怎么判断它是不是特殊棋型呢?这里我那其中一个函数来举例可能更直观一点。
int linkFive(Chess ce1,Chess ce2,Chess ce3,Chess ce4,Chess ce5);
上面的代码以连5为例,将获得的所有长度为5棋链中的其中一条拿出来,将其中每个元素依序放入函数进行判断,如果满足连五的要求,即五个棋子颜色相同,返回1,否则返回0。
算法中还有一部分是关于怎么对棋子赋分的,这些部分我会在下面的赋值和边界判断中进行介绍
这里我先展示一颗棋子的属性:
struct Chess
{
int color;
int atkValue;
int defValue;
int row;
int col;
bool isBoundry;
}
棋子属性包括棋子颜色,攻击分数(atkValue),防守分数(defValue),棋子坐标 以及棋子是不是处于边界的判断。
程序在下一步棋之前,会先对所有空位赋攻击分数和防守分数,然后寻找拥有最大攻击分数和防守分数的棋子,具体流程如下图:
注意流程中最后一步,当经过前面的筛选后选的棋子还不唯一,要随机选择棋子,否则程序的下棋套路会变得固定,容易被别人找到必胜的策略。
接下来,为了保证逻辑性和博客的简洁性,我会自上而下,展示部分重要代码。
AI()函数,首先对所有空位赋值(giveAllValue),然后找寻值最大的空位(getBestChess),最后在该空位上下子(putChess)。程序会记录棋盘上棋子的个数,如果一开始棋盘上没有子,就将子落在棋盘中央,这样是为了节省一开始的判断时间。
void ChessRobot::AI(Map mp)
{//程序先给每个空位更新权值
//然后再获得权值最大的棋子
//最后将权值最大的棋子放置在棋盘中
if(mp.getNum()==0)
{//当棋盘全空的时候,机器人在中间下子
putChess(mp,Chess((mp.getSize()+1)/2,(mp.getSize()+1)/2));
return;
}
giveAllValue(mp);
Chess ce=getBestChess(mp);
putChess(mp,ce);
}
下面会对AI()函数中的giveAllvalue,getBestChess,putChess函数分别介绍。
giveAllValue分别设置atkValue和defValue
void ChessRobot::giveAllValue(Map mp)
{
for(int i=1;i<=mp.getSize();i++)
{
for(int j=1;j<=mp.getSize();j++)
{
Chess tmpCe=mp.getChess(i,j);
if(tmpCe.color==NONE)
{
mp.setAtkValue(tmpCe,getAtkValue(mp,tmpCe));
mp.setDefValue(tmpCe,getDefValue(mp,tmpCe));
}
}
}
return;
}
setDefValue和giveDefValue因为与atk类似所以不再展示
计算每个空位的atkValue和defValue,然后通过setAtkValue将值赋给空位
int ChessRobot::getAtkValue(Map mp, Chess ce)
{
int allValue[7];
//将七种棋型的个数存入数组
allValue[0]=mp.countLinkFive(ce);
allValue[1]=mp.countLiveFour(ce);
allValue[2]=mp.countRushFour(ce);
allValue[3]=mp.countLiveThree(ce);
allValue[4]=mp.countSleepThree(ce);
allValue[5]=mp.countLiveTwo(ce);
allValue[6]=mp.countSleepTwo(ce);
int atkValue=0;
atkValue+=allValue[0]*TWO_VAL;
atkValue+=allValue[1]*FIFTEEN_VAL;
atkValue+=getFibHand(allValue)*SIX_VAL;
atkValue+=allValue[2]*EIGHT_VAL;
atkValue+=allValue[3]*TEN_VAL;
atkValue+=allValue[4]*TWELVE_VAL;
atkValue+=allValue[5]*FOURTEEN_VAL;
atkValue+=allValue[6]*FIFTEEN_VAL;
return atkValue;
}
getAtkValue中要取得七种特殊棋型的个数,这里暂且只展示展示countLinkFive()
int Map::countLinkFive(Chess ce)
{
using std::vector;
int count=0;
for (int i=1;i<=5;i++)
{
vector<vector<Chess>> vce=getChessChain(ce,5,i);
if(!chainIsOK(vce)) continue;
count+=linkFive(vce,5);
}
return count;
}
在往下级的函数(及从getChessChain)开始,在算法介绍部分已经有过介绍,这里不再展示详细代码
void Map::setAtkValue(Chess ce,int atkValue)
{
map[ce.row][ce.col].atkValue=atkValue;
return;
}
getBestChess中取最佳棋子的过程在上面的赋值中已有流程图加以介绍,这里不在赘述
Chess ChessRobot::getBestChess(Map mp)
{
//if(mp.getNum()==0) return Chess((mp.getSize()+1)/2,(mp.getSize()+1)/2);
//这里判断没有用
using std::vector;
vector<Chess> chessChain;
for (int i=1;i<=mp.getSize();i++)
{
for (int j=1;j<=mp.getSize();j++)
{
if(mp.getChess(i,j).color==NONE)
{
chessChain.push_back(mp.getChess(i,j));
}
}
}
Chess maxAtk;
Chess maxDef;
vector<Chess>::iterator it;
int countNone=0;
int countLen=0;
it=chessChain.begin();
maxAtk=*it;
maxDef=*it;
while(it!=chessChain.end())
{
if((*it).atkValue>maxAtk.atkValue) maxAtk=*it;
if((*it).defValue>maxDef.defValue) maxDef=*it;
it++;
}//将找最大和找最小合并,减少判断时间
/*it=chessChain.begin();
maxDef=*it;
while(it!=chessChain.end())
{
if((*it).defValue>maxDef.defValue) maxDef=*it;
it++;
}*/
vector<Chess> secondWeight;//如果maxDef>maxAtk,存maxDef;反之亦然
int flag=0;//记录是maxAtk大还是maxDef大
if(maxAtk.atkValue>=maxDef.defValue)
{
flag=1;
it=chessChain.begin();
while(it!=chessChain.end())
{
if((*it).atkValue==maxAtk.atkValue) secondWeight.push_back(*it);
it++;
}
}
else
{
flag=2;
it=chessChain.begin();
while(it!=chessChain.end())
{
if((*it).defValue==maxDef.defValue) secondWeight.push_back(*it);
it++;
}
}
//是因为其在OJ中会显示超时
it=secondWeight.begin();
Chess tmpMax;
if(flag==1)//如果maxAtk大
{
tmpMax=*it;//找次位权的最大值
while(it!=secondWeight.end())
{
if((*it).defValue>=tmpMax.defValue) tmpMax=*it;
it++;
}
}
else//如果maxDef大
{
tmpMax=*it;//找次位权的最大值
while(it!=secondWeight.end())
{
if((*it).atkValue>=tmpMax.atkValue) tmpMax=*it;
it++;
}
}
int n=0;//记录次位权值最大值个数
vector<Chess> secondWeightBest;//存放第二位权的最大值链<---如果不止一个还要随机取一个
it=secondWeight.begin();
if(flag==1)//如果maxValue大
{
while(it!=secondWeight.end())
{
if((*it).defValue==tmpMax.defValue)
{
secondWeightBest.push_back(*it);
n++;
}
it++;
}
}
else
{
while(it!=secondWeight.end())
{
if((*it).atkValue==tmpMax.atkValue)
{
secondWeightBest.push_back(*it);
n++;
}
it++;
}
}
if(n==1) return secondWeightBest[0];
int loc=getRandomLocation(n);//生成0~n-1 的随机数,所谓所选择的棋子;
return secondWeightBest[loc];
/*if(maxAtk.atkValue>maxDef.defValue) return maxAtk;
else return maxDef;*/
}
void ChessRobot::putChess(Map mp, Chess ce)
{
mp.putAIChess(ce);
return;
}
putChess就是修改棋盘中的颜色值,没什么好说的
void Map::putAIChess(Chess ce)
{//不整体赋值,是因为没有必要,只赋color可以节省时间
numPlus();
map[ce.row][ce.col].color=friendColor;
return;
}
代码托管于Github
Github链接:https://github.com/wcgzgj/GobangVer3.0-BC-2020-4-29-.git
对于不会用Github的同学,我也提供了百度网盘下载链接
百度网盘链接:https://pan.baidu.com/s/14aONH57YB_NCcax9FFqGBA
提取码:mfo0
5.20日更新
今天在复盘的时候发现了一个BUG
这里在给连四赋值的时候赋错了,把五号权值赋成了十五号权值,会导致程序不会想去下连四的棋型,把这里改成FIVE_VALUE就好了。
链接里的内容就不改了,给得读者自己去修改吧 主要还是自己懒