六子棋AI程序---核心讲解

1.前言

笔者这里想说一句:六子棋终于写完了,啊~
不说了,先看战绩:
在这里插入图片描述这里是没有分出胜负的,但是,下棋时间先超过3分钟的判输。
在这里插入图片描述这里是交换先后手,又下了一局,你可以认为是对面棋艺不精,但看到最后你就明白了。
最近一个星期都在忙于升级自己的AI代码,所以博客更新的比较慢,不过近期会加快更新的。下面我们进入正题。

2.游戏规则

六子棋的规则与五子棋非常相似,玩家有黑白两方,各持黑子与白子,黑方先行。采用19*19的棋盘。具体玩法:除了第一次黑方下一子外,之后白黑双方轮流每次各下两子(即第一步黑方下一子、然后白方下两子、黑方下两子、白方下两子…)。直的、横的、斜的连成6子(或以上)者获胜。若全部棋盘填满仍未分出胜负,且双方各自下棋时间,则为和局。否则若有一方时间先超过3分钟,则直接判输。

3.核心策略:贪心+博弈树

(1)贪心策略

这也是笔者最早的一个版本,用的是贪心思想,即将子落在当前局面最佳落子点。思想很简单,但问题来了,当前局面最佳落子点是哪一个?怎么判断?
笔者这里做出详细解答:我们假定己方为白子,如果轮到白方落子时,发现棋盘上有五子相连,并且两头为空(无子),那这两头是不是当前的最该落子的位置呢。如果没有五子相连,四子相连,并且两头为空,也是当前最该落子的位置(可落两子,所以下四子相连的位置,可以成六子相连)。如果有两对四子相连的白子相交于同一空位,那这个空位也是最该落子的位置。同理…
五子棋的话,想必绝杀赢得概率最大了,就是有两对四子相连,对方根本堵不过来,那就是必赢了。六子棋也同样如此,可是绝杀的棋型太多,怎么可能全都列出,而且就算全都列出(感觉不太现实),我又怎么判断当前棋盘里有没有这种棋型呢?如果真想硬刚这方向的话,呃,只能说劝你善良,对自己好一点吧。
这里我们换个思维,我们知道各式各样的绝杀棋型,无非是一些小的模型平凑而成,比如几个四子相连的模型,随机的排列,那么有些点必然就是必赢点。现在就是将问题简化了,我们只要能找出一些小模型就行了(四子相连,五子相连,三子+一子…),这些小模型无非就是几个棋子在六个连续的格子里排列组合而已,要穷举也是挺容易的。下面给出部分模型,其它模型就靠自己补充了。 在这里插入图片描述 这样模型就写出来了,在棋盘遍历寻找模型时,在横,纵,左斜,右斜四个方向上都要遍历。可是如果找到了,又怎么办呢?我们前面部分说了,找最适合落子的点,既然是“最”,那么它就一定有比较,有比较,就会有东西来衡量大小。这里我们给每个格子赋上分值,挑选分值最高的就是最适合落子的位置了。那么格子的分值怎么来呢?对,既然我们已经有了模型,能匹配上模型的格子,如果是空格的话,都应该赋上分值,给上图添加分值如下。
在这里插入图片描述
我们在建立模型的同时,给特定位置附上分值,如果匹配成功,则会在棋盘上赋上相应的分值。讲到这里,我们就可以给出模型代码了。

struct Point { //点结构
	int x, y;
};
struct Model
{
	int status[6];//记录每个格子的状态,0为黑,1为白,2为空
	int probability_p[6];//记录各个格子的概率
};
//eg.对所有模型各个格子的分值赋值为0,color代表某一方的颜色值,模型有很多个,所以定义模型数组model[n]
/*model[0].status[0] = 2;
	model[0].status[1] = color;
	model[0].status[2] = color;
	model[0].status[3] = color;
	model[0].status[4] = color;
	model[0].status[5] = color;
	model[0].probability_p[0] = 30;*/

留心的朋友可能注意到,有时候两子相连会是三子相连的子集,而我们的分值还叠加了,这显然是不对的。这里做出的调整是,给每个格子加上“方向”,如果该格子在同一方向上匹配到多个模型,则挑选最大的分值,如果是不同方向,分值叠加。部分实现代码如下:
在这里插入图片描述 意思是:先竖直方向上遍历棋盘时,如果当前格子的方向不为0,则给格子加上模型上的分值,如果当前格子的方向为0,则比较原来分值,将较大的分值替换原来分值。最后将该格子的方向赋值为当前竖直方向,其他方向同理。这样,遍历整个棋盘,找寻分值最高的点就是当前最适点了。
注意事项:计算分值一定要考虑双方棋子,即己方四子相连落子概率很高,对面四子相连落子概率也很高,这也就是为什么建立模型时用的是color而不是具体颜色值。

(2)博弈树

博弈树是笔者后来加上的,正当笔者信誓旦旦的拿着自己的“贪心”六子棋AI程序找朋友挑战时,结果那是个惨不忍睹…(如果你认为是笔者输的话,那你还真就猜对了…)。想想都可得知,只考虑当前最优解,对手稍微拐个弯子不就输了么。想用一句话形容当晚找朋友solo的自己,但想归想。因为早期的贪心策略是找当前最优解,如果没有找到,则随机落点,这棋技显然是低得不行。当时输了后是真想放弃贪心的那套代码了,虽然花了三天时间,有点舍不得,但确实是好垃圾呀。迫于无奈,只能暂时将它搁置一边,打算重写一套。这就是接下来要讲的重点,博弈树了。
简单来说,博弈树的主要思想是,假设我走这,假设你走那,假设我走这,假设你走那…。就这么循环往复,获取未来棋盘的分值(是棋盘的分值,和贪心里格子的分值不同),选出分值最高的走法,返回给当前。但是举个例子,如果是20乘20的棋盘的话,如果深入四层,那么可能的走法就有400乘399乘398乘397,也就256亿种假设的走法吧。如果你对自己的计算机很自信的话,你可以试试,下面一段代码需要多久才能运行完。

int i,j,k,m;
for(i=0;i<400;i++)
{
   for(j=0;j<400;j++)
   {
      for(k=0;k<400;k++)
      {
         for(m=0;m<400;m++)
         {
            cout<<""<<endl;
         }
      }
   }
}
cout<<"谢谢你走进我的世界"<<endl;

针对上面的问题,可以用“剪枝”来解决,但当时由于时间比较紧,笔者对剪枝还理解的不到位,又为了减少运行时间,只用了两层的博弈树。其核心思想为深度优先搜索,极大极小值搜索,递归。用图来描述为:
在这里插入图片描述 这里我们一步步进行讲解
首先:棋盘分值怎么定,如果贪心策略明白的话,那么棋盘分值也很好得出。同样的模型,只是把每个点的分值改为模型的分值,如果匹配成功则给当前棋盘加上模型的分值就可以了。
第二:我们博弈的起始点无需从棋盘的所有位置开始,比如说,如果棋盘上只有中心有一个棋子,轮到你下的时候,你会下在棋盘的四个顶角吗,显然不会。所以这里我们只需要将棋盘上所有点周围的点作为可能的起始点就可以了,如下图,周围黄色的点即为第一层可落子的点,通过大致缩小范围可大大减小运行时间, 在这里插入图片描述第三:假设我们落在绿色点,那么重复步骤二,确定下一步可能落子的点,如黄色区域所示。 在这里插入图片描述第四:假设我们落在红点,则结束(如果想多深入几层,也可再次重复步骤二),计算当前棋盘分值。 在这里插入图片描述讲到这里相信大家思路都比较清晰了,代码也不难实现,感觉上就两层for循环,一个计算当前棋盘分值的函数,和一个获取可下点(黄色区域)的函数,每次获取子结点分值最高的那个就行了。(貌似两层和深度优先搜索没啥关系,自己代码还写复杂了…)。

4.总结

前言的两张图片是用笔者的贪心+两层博弈树和六层博弈树的对局(对面是正版,用了剪枝,所以运行时间7分钟下完整个棋盘算挺快的了)。我们这两者结合的好处就是,如果你对局的是用博弈树的,可以耗它时间,让它超时(如果对面剪枝不行的话)。如果你对局的是普通的贪心,那么几乎开头几句就绝杀了(两层博弈树的功劳),原来就有一局是和普通贪心比的,开局三步定胜负了,对面一直堵我,最后堵不完了,绝杀。如果想让AI程序棋艺更好的话,可以上github好好学学剪枝,剪枝剪得好加上贪心节省时间,真的可以很秀的。最后,不管怎样,终于不用被六子棋折磨了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章