记录贴:从头开始写五子棋AI
c4droid吧
全部回复
仅看楼主
level 7
之前写过一个五子棋AI,后来优化的时候遇到了困难,维护不下去了,就弃坑了。。。
今天我又打算从头开始重新写一个五子棋AI,每天完成一部分,同时以这个贴子纪录每天的成果。
为了使主题连贯,建议大家尽量在楼中楼回复。
2015年12月17日 05点12分 1
level 13
插。
2015年12月17日 05点12分 2
level 11

2015年12月17日 05点12分 3
level 10

2015年12月17日 05点12分 4
level 7
先介绍一下这个AI的大体框架:
1.局势评价函数
局势评价函数的任务就是对于给定的局面,计算出这个局面的分值。局面越有利,分值越高,局面越不利,分值越低。
显然,五子棋可能的局面数非常多,是不可能穷举的,要构造出一个好的局面评价函数并不容易。
我的方案如上图所示:先统计出棋盘上黑白方各棋型的数量,再根据统计结果给出评分。
2.搜索策略
有了局面评价函数,怎么得到最佳的落子点呢?
最简单的方法可以这样:对于当前棋盘上所有的空点,分别计算在各点处落子后的局面的评分值,找出落之后评分最高的那个点,就是AI下一步要落子的点。
上述方法是一种贪婪算法,它只考虑了一步以后的局势,十分短视。
(理论上,只要局面评价函数足够理想,贪婪法也可以是无懈可击的。而实际上,要构造一个理想的评价函数几乎不可能,贪婪法的效果并不好)
为了解决贪婪法短视的问题,应该往后考虑多步,使用minmax搜索算法。可是这又带来一个大问题:搜索的局面数关于搜索的步数指数增长。在minmax搜索的基础上使用alphabeta剪枝可以显著减少不必要的搜索数量。(具体有多显著?以我之前写的那个AI为例:不使用alphabeta剪枝时大概只能搜索6步,使用alphabeta剪枝后可以搜索12步)
搜索12步就够了吗?并不够。进一步压榨AI性能的方法有:迭代加深,hash表,等等
这里就不一一介绍了
2015年12月17日 06点12分 5
level 7
棋盘局面以什么样的结构来存储?
这里的棋盘大小为15*15,那么使用一个15*15的数组不就搞定了?
棋盘上每个点有三种可能:空,黑,白 , 分别用0,1,2表示, 棋盘上的点和数组元素一一对应。
是否有更好的表示方法?
很显然表示空,黑,白 3种状态中的一种,只需要2个bit就够了:
空:00
黑:01
白:10
棋盘上每一行有15个点,所以只需要 15 * 2 = 30 个bit就能表示了。
大部分机器上,int的长度为32bit,一个int就可以表示棋盘上的一行,15个int可以表示一个完整的棋盘:
这样表示不仅节省空间,更重要的是能够加速后面的一些函数,比如对棋型的统计,对棋盘局面求hash等。
2015年12月17日 07点12分 6
这里的int row[15]中的row是什么呢?没听说过这种数据类型啊。
2016年10月20日 01点10分
@贼法法 这只是个数组的名称[喷]
2016年10月25日 11点10分
赞同你的这种用法,默默看下去
2016年12月24日 00点12分
level 14

2015年12月17日 07点12分 7
level 11
我就要插
2015年12月17日 08点12分 8
level 7
到现在为止,这个棋型统计器使用了 “查表大法” 和 “位运算大法” 加速,性能已经做到极致了
吗?
还是图样,速度还能再快10倍:
下五子棋的时候,每一步只落一个子,落子前和落子后的棋盘仅仅是落子点所在的行,列,斜列发生了变化,而其它行列完全没变。
只要记下棋盘每一行每一列的棋型,落子后只需要重新扫描落子点所在的行,列,斜列,而不用扫描整个棋盘,工作量一下子就降到了之前的1/15
于是棋盘的存储结构就变成了这样:
Ntype是棋型的种类数,我一共定义了16种不同的棋型,所以它等于16
现在,这个棋盘结构看起来有点臃肿了。
我试图把它压缩,写成这样:
能进一步压缩吗?
计算一下,每一行或列或斜列上最多可能有多少个棋型?由于一行的长度是15,扫描框的长度是6,所以扫描框在一行上可以扫10个不同的位置,最多产生10个棋型,表示0到10至少需要4个bit,一共16种棋型,需要64个bit。 所以,一个64位的int就能表示某一行或列或斜列上各棋型的数量了。
2015年12月17日 11点12分 12
level 10
厉害(ง •̀_•́)ง!!!!
2015年12月17日 12点12分 13
level 5
如果你有那个Yixin源代码就好了
据说是非常吊的五子棋软件
2015年12月17日 13点12分 14
弈心确实厉害,我之前那个AI和弈心下过几局,只有无禁先手才能赢,平衡开局都输得很快。不过我这次的AI定位是轻量级,不准备搞得过于复杂,在不超过2000行代码的前提下追求最高棋力。
2015年12月17日 13点12分
[勉强]
2015年12月19日 04点12分
level 5
我一般恒星 或者峡月开局
2015年12月17日 13点12分 15
level 7
今天发现 __int64 的移位操作在有的编译器下会出错,妥协于兼容性,还是采用折中的 unsigned char 数组来表示某一条线上的棋型。
在棋盘A上的(x, y)坐标落子的函数,n取值范围为0到2
2015年12月18日 12点12分 16
level 7
几张图理清结构体中 row[15], column[15], right[19], left[19], 与棋盘位图之间的关系:
给出棋子的坐标,通过变换即可求出该棋子在 row[15], column[15], right[19], left[19],
中的位置:
假设棋子坐标为 ( x, y )
则其在 row[15] 中的位置为 ( row[x] >> 2*y ) & 3
在 column[15] 中的位置为 ( column[y] >> 2*x ) & 3
令 int p = x + y - 5;
若 p>=0 && p<19
则其在 right[15] 中的位置为 ( right[p] << ( p<10 ? 2*y : 28-2*x ) ) & 3
令 int p = x - y + 9;
若 p>=0 && p<19
则其在 left[19] 中的位置为 ( left[p] << ( p<10 ? 2*x : 2*y ) ) & 3
之前的落子函数先将要落子的位置清空,再对其赋值,而大多数情况下要落子的点是空点。
为了提高运行效率,将之前的落子函数拆分成撤子和落子两个函数:
2015年12月18日 14点12分 17
level 7
完整版的棋型 扫描与统计 函数。
对于棋盘A,扫描 坐标( x, y ) 所在行,列,斜列,并更新结构体A中的棋型数据
我已经尽量让它简短了,可是它的长度还是超过了屏幕的高度。有没有更简洁的写法?
2015年12月18日 16点12分 18
两个1~19的循环合并?
2015年12月19日 06点12分
@DXKite 函数并没有扫描整个棋盘,没有1~19的循环。
2015年12月19日 07点12分
@牲口才开电车 额。0~19
2015年12月19日 07点12分
萌新帮顶
2016年12月26日 10点12分
1 2 3 4 尾页