c++ 五子棋AI 算法及实现 详细解析

2020.11.17日更新

今天我看到有人fork了我的这个项目,其实怪不好意思的,因为现在看来,这个项目写的实在不咋地。我后来又用java swing写了一个人工智能五子棋,并对算法进行了优化,如果想fork的话,还是fork这个新的项目吧
Github链接:gobang-ver4.0-JavaSwing




C++ 五子棋AI

  • 2020.11.17日更新
  • 前言
  • 效果展示
  • 五子棋棋型介绍
  • 算法解析
    • 基本概念
    • 算法
      • 简略介绍
      • 详细介绍
    • 赋分
  • 代码实现及介绍
    • AI()
      • giveAllValue()
        • getAtkValue()
          • countLinkFive()
        • setAtkValue()
      • getBestChess()
      • putChess()
  • 源代码Github链接

前言

  这次的五子棋AI是我写的第三版,前两版因为自己的开发项目不充足,导致最后写出的程序Bug众多,无法修改,第三版重新设计了算法,并使用了多文件结构,最后顺利完成了。
  根据之前查询的资料我了解到,要想写出无可匹敌的五子棋AI,要用到博弈树、AlphaBeta减枝法、机器学习等知识,但是因为个人水平的原因,这些都无法实现,不过我的算法起码能保证玩家稍不留神,就会被打败,这和五子棋新手博弈、OJ霸榜,都足够了。等到日后自己的知识量增加了,再来对这个程序进行完善吧。

效果展示

这里先展示一下最后的效果
c++ 五子棋AI 算法及实现 详细解析_第1张图片
程序下棋的水平还是可以的,日常拿来消遣一下也没什么问题。

五子棋棋型介绍

  在设计五子棋算法之前,要对五子棋的算法有基本的了解,这里我借用别人的文章来介绍。原文链接

  最常见的基本棋型大体有以下几种:连五,活四,冲四,活三,眠三,活二,眠二。

连五:顾名思义,五颗同色棋子连在一起,不需要多讲。

c++ 五子棋AI 算法及实现 详细解析_第2张图片
图1-1

活四:有两个连五点(即有两个点可以形成五),图中白点即为连五点。
稍微思考一下就能发现活四出现的时候,如果对方单纯过来防守的话,是已经无法阻止自己连五了。

c++ 五子棋AI 算法及实现 详细解析_第3张图片
图2-1

冲四:有一个连五点,如下面三图,均为冲四棋型。图中白点为连五点。
相对比活四来说,冲四的威胁性就小了很多,因为这个时候,对方只要跟着防守在那个唯一的连五点上,冲四就没法形成连五。

c++ 五子棋AI 算法及实现 详细解析_第4张图片
图3-1
c++ 五子棋AI 算法及实现 详细解析_第5张图片
图3-2
c++ 五子棋AI 算法及实现 详细解析_第6张图片
图3-3

活三:可以形成活四的三,如下图,代表两种最基本的活三棋型。图中白点为活四点。
活三棋型是我们进攻中最常见的一种,因为活三之后,如果对方不以理会,将可以下一手将活三变成活四,而我们知道活四是已经无法单纯防守住了。所以,当我们面对活三的时候,需要非常谨慎对待。在自己没有更好的进攻手段的情况下,需要对其进行防守,以防止其形成可怕的活四棋型。

c++ 五子棋AI 算法及实现 详细解析_第7张图片
图4-1
c++ 五子棋AI 算法及实现 详细解析_第8张图片
图4-2

眠三:只能够形成冲四的三,如下各图,分别代表最基础的六种眠三形状。图中白点代表冲四点。眠三的棋型与活三的棋型相比,危险系数下降不少,因为眠三棋型即使不去防守,下一手它也只能形成冲四,而对于单纯的冲四棋型,我们知道,是可以防守住的。

c++ 五子棋AI 算法及实现 详细解析_第9张图片
图5-1

图5-2
c++ 五子棋AI 算法及实现 详细解析_第10张图片
图5-3
c++ 五子棋AI 算法及实现 详细解析_第11张图片
图5-4
c++ 五子棋AI 算法及实现 详细解析_第12张图片
图5-5
c++ 五子棋AI 算法及实现 详细解析_第13张图片
图5-6

如上所示,眠三的形状是很丰富的。对于初学者,在下棋过程中,很容易忽略不常见的眠三形状,例如图5-6所示的眠三。

有新手学了活三眠三后,会提出疑问,说活三也可以形成冲四啊,那岂不是也可以叫眠三?
会提出这个问题,说明对眠三定义看得不够仔细:眠三的的定义是,只能够形成冲四的三。而活三可以形成眠三,但也能够形成活四。

此外,在五子棋中,活四棋型比冲四棋型具有更大的优势,所以,我们在既能够形成活四又能够形成冲四时,会选择形成活四。

温馨提示:学会判断一个三到底是活三还是眠三是非常重要的。所以,需要好好体会。
后边禁手判断的时候也会有所应用。

活二:能够形成活三的二,如下图,是三种基本的活二棋型。图中白点为活三点。
活二棋型看起来似乎很无害,因为他下一手棋才能形成活三,等形成活三,我们再防守也不迟。但其实活二棋型是非常重要的,尤其是在开局阶段,我们形成较多的活二棋型的话,当我们将活二变成活三时,才能够令自己的活三绵绵不绝微风里,让对手防不胜防。

c++ 五子棋AI 算法及实现 详细解析_第14张图片
图6-1
c++ 五子棋AI 算法及实现 详细解析_第15张图片
图6-2
c++ 五子棋AI 算法及实现 详细解析_第16张图片
图6-3

眠二:能够形成眠三的二。图中四个为最基本的眠二棋型,细心且喜欢思考的同学会根据眠三介绍中的图2-13找到与下列四个基本眠二棋型都不一样的眠二。图中白点为眠三点。

c++ 五子棋AI 算法及实现 详细解析_第17张图片
图7-1
c++ 五子棋AI 算法及实现 详细解析_第18张图片
图7-2
c++ 五子棋AI 算法及实现 详细解析_第19张图片
图7-3
c++ 五子棋AI 算法及实现 详细解析_第20张图片
图7-4

算法解析

基本概念

  在介绍算法之前,要将程序里的一些基本概念介绍一下。
  标准五子棋棋盘一般是15X15的大小(当然程序里可以随意改)。
  我后面会将连五,活四,冲四,活三,眠三,活二,眠二称为特殊棋型,这样可以节约博客字数。
  对于棋盘上的排成直线的一串棋子,有横、竖、左上到右下,左下到右上四种方向,我将其命名为 横(LAY)、竖(STAND)、主对角(MAIN)、副对角(VICE)
  对于某一个方向上的排成一条直线的棋子,我称其为棋链,如下方的图就是一个棋链
c++ 五子棋AI 算法及实现 详细解析_第21张图片
这个棋链是竖向(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)棋子坐标 以及棋子是不是处于边界的判断。
  程序在下一步棋之前,会先对所有空位赋攻击分数防守分数,然后寻找拥有最大攻击分数和防守分数的棋子,具体流程如下图:
c++ 五子棋AI 算法及实现 详细解析_第22张图片
注意流程中最后一步,当经过前面的筛选后选的棋子还不唯一,要随机选择棋子,否则程序的下棋套路会变得固定,容易被别人找到必胜的策略。

代码实现及介绍

请按照目录下的代码层次结构观看代码

  • 2020.11.17日更新
  • 前言
  • 效果展示
  • 五子棋棋型介绍
  • 算法解析
    • 基本概念
    • 算法
      • 简略介绍
      • 详细介绍
    • 赋分
  • 代码实现及介绍
    • AI()
      • giveAllValue()
        • getAtkValue()
          • countLinkFive()
        • setAtkValue()
      • getBestChess()
      • putChess()
  • 源代码Github链接

  接下来,为了保证逻辑性和博客的简洁性,我会自上而下,展示部分重要代码。

AI()

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()

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;
}

setDefValuegiveDefValue因为与atk类似所以不再展示

getAtkValue()

计算每个空位的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()

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)开始,在算法介绍部分已经有过介绍,这里不再展示详细代码

setAtkValue()

void Map::setAtkValue(Chess ce,int atkValue)
{
	map[ce.row][ce.col].atkValue=atkValue;
	return;
}

getBestChess()

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()

putChess就是修改棋盘中的颜色值,没什么好说的

void Map::putAIChess(Chess ce)
{//不整体赋值,是因为没有必要,只赋color可以节省时间
	numPlus();
	map[ce.row][ce.col].color=friendColor;
	return;
}



建议按照目录中的程序结构进行观看。
c++ 五子棋AI 算法及实现 详细解析_第23张图片

源代码Github链接

代码托管于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
c++ 五子棋AI 算法及实现 详细解析_第24张图片
这里在给连四赋值的时候赋错了,把五号权值赋成了十五号权值,会导致程序不会想去下连四的棋型,把这里改成FIVE_VALUE就好了。
链接里的内容就不改了,给得读者自己去修改吧 主要还是自己懒

你可能感兴趣的:(算法,c++,ai,人工智能)