(声明:转载于网络,仅供个人学习使用)
一、F函数
F函数是对ai(白子)走了一步之后的局面进行评分,棋型的权重的设计是有技巧的。
规则一:己方棋型权重为正,对方棋型权重为负,且相同棋型时,对方权重的绝对值要大于己方(可以设置为2倍或者3倍关系)。这是因为要考虑到进攻和防守,现在是己方(ai白子)下,例如:如果己方走了一步棋形成了活2,而对方已经有一个活2,那么显然是对方占优一些,因为下一步是对方走,对方是可以形成更高等级的活3的,所以己方活2就没有对方活2等级高。
规则2:等级:连5>活4>冲4=活3>眠3=活2>眠2=活1,相邻等级的权重设置为相差20倍(也可以30,40倍)。这是因为会重复计算等级比较低的棋型,为了不影响总体判断,比如一开始放一个子,活1的权重会计算16次,我设置为20倍,那么活2的权重刚好比16倍活1还要大一些。
规则3:对方连5、对方活4、对方冲4、对方活3的绝对值要设置大一点,这一点非常重要!!如果此时对方已经连五,说明己方已经输了。如果此时对方有活4和冲4,那么如果己方没有连5的话,己方必须要去阻止对方的活4和冲4。像这样可以分析出其他棋型权重。
二、极大极小搜索
1、基础
什么是极大极小搜索?我首先要介绍博弈树的概念。博弈树就是己方和敌方进行决策时形成的树状结构,每一个节点的分支表示当前节点可以走的各种可能的位置,每一个叶结点表示一个局面。比如说,从空棋盘开始(根节点),我进行落子,我有15x15=255种落子选择,我落子之后,形成了一颗有255个节点的博弈树,博弈树的叶结点就是一个局面。如果此时对手进行落子,对手有254种选择,那么新形成的博弈树就会有255x254个叶结点。
可以看到,博弈树是指数级别的,如果平均分支为b,博弈树层数为d(根节点为第0层),那么叶结点的总数约为b^d。
有了博弈树的概念,我们就可以实现能看几步的“聪明”ai了。那么怎么“看几步”呢?简单来说,就是遍历博弈树的所有叶结点,寻找到对ai最有利的局面,然后进行落子。在博弈树中,当ai走棋时选择对自己最有利的位置节点走,而当玩家走棋时,是ai模拟玩家选择对玩家最有利的位置节点走。由于评估函数F对局势的评分是对于ai白子来说的,所以ai走棋时选择F最大的节点,模拟玩家走棋时选择F最小的节点(F越小,对ai越不利,对玩家越有利,ai模拟玩家时是认为玩家是“聪明”的),这就是称为极大极小搜索的缘故。
2、优化:α-β剪枝算法
这个算法的名字听起来很高大上,但是实际内涵并不难理解。
举一个很简单的例子。
max层表示这一层节点的值应该选它的子节点的最大值,min层表示这一层节点的值应该选它的子节点的最小值。
对于这样一颗博弈树,已知了叶结点d、f、g、h的值,怎么求a的值呢?首先由d、f可以知道b的值为-1,然后a搜完了b节点,搜索另一边,搜索到a-c-g,此时可以知道c的值必然是<=-2的,由于a要选最大值节点,b的值已经大于c了,没有必要再搜完c节点,那么a-c-h这一个分支就被“剪掉了”。
α-β剪枝算法中每一个节点对应有一个α和一个β,α表示目前该节点的最好下界,β表示目前该节点的最好上界。在最开始时,α为负无穷,β为正无穷。然后进行搜索,max层节点每搜索它的一个子节点,就要更新自己的α(下界),而min层节点每搜索它的一个子节点,就要更新自己的β(上界)。如果更新之后发现α>=β了,说明后面的子节点已经不需要进行搜索了,直接break剪枝掉。这就是α-β剪枝算法的全部含义。
seekPoints()是用来找目前局面时最佳的几个落子点的位置以及落子之后的分数,这里用了局部搜索和静态评价启发来提高效率,后面再讲。
有几点非常容易犯错(我改bug花了半周),需要注意:
1.博弈树搜索深度depth必须为偶数,因为叶结点的局面评估函数F是对于白子走了一步的局面评估的,如果depth为奇数,会导致叶结点的F估值错误。
2.递归调用analyse时,不能用原来的board棋盘数组,因为analyse会模拟落子,如果简单的在board上面模拟落子会改变board的信息,后面的递归调用会不停的修改board,这样就不会得到正确结果。好的做法是复制一个新棋盘数组,然后模拟落子,再作为参数传给递归的analyse。
把醒目的部分去掉,剩下的就是最小-最大函数。可以看出现在的算法没有太多的改变。
这个函数需要传递的参数有:需要搜索的深度,负无穷大即Alpha,以及正无穷大即Beta: val = AlphaBeta(5, -INFINITY, INFINITY);
这样就完成了5层的搜索。我在写最小-最大函数时,用了一个诀窍来避免用了“Min”还用“Max”函数。在那个算法中,我从递归中返回时简单地对返回值取了负数。这样就使函数值在每一次递归中改变评价的角度,以反映双方棋手的交替着子,并且它们的目标是对立的。
在Alpha-Beta函数中我们做了同样的处理。唯一使算法感到复杂的是,Alpha和Beta是不断互换的。当函数递归时,Alpha和Beta不但取负数而且位置交换了,这就使得情况比口袋的例子复杂,但是可以证明它只是比最小-最大算法更好而已。
最终出现的情况是,在搜索树的很多地方,Beta是很容易超过的,因此很多工作都免去了。
3、局部搜索和静态评价启发
由于博弈树是指数级别的,如果能想办法减小平均分支数b,就能极大减少叶结点的数量。
局部搜索是说,只考虑那些能和棋子产生关系的空位置,而不用考虑所有空位置,这样能极大减小b的值。我的局部搜索的考虑棋盘上每一个有子点周围8个方向上延申3个深度,只有这些地方会与现有棋子产生联系。
静态评价启发是对于α-β剪枝算法而言的,意思是说,如果越早搜索到较优走法,剪枝就会越早发生。如果对可走节点的评估分数进行简单排序,就可以提高剪枝速度。我将局部搜索得到的所有可走节点的分数进行排序,放到POINTS类的一个对象中,[0]分数最高,[9]分数最低。
seekPoints()是综合了这两种优化方式的生成最佳落子位置函数,一般来说,搜索10个最优走法就已经可以满足需求了,可以极大提升搜索速度,但如果过多减小有可能剪掉有利分支。那么,如果是搜索深度为4,最多只需要搜索10^5=100000个叶结点,并且静态评价启发和α-β剪枝算法进一步减少,实际程序运行只需要搜索5000个叶结点,减少得非常多了!