【五子棋AI循序渐进】关于VCT,VCF的思考和核心代码
前面几篇发布了一些有关五子棋的基本算法,其中有一些BUG也有很多值得再次思考的问题,在框架和效果上基本达到了一个简单的AI的水平,当然,我也是初学并没有掌握太多的高级技术。对于这个程序现在还在优化当中,主要是完善所使用的启发方式、编写多线程搜索代码、加入开局库等一些工作。开局库和多线程还没有实现,仅在棋盘表示、模板表示上修改较多,而启发方式上有一些改进,主要是修改了置换表为双置换表(深度和实时),内部迭代加深启发和我们接下来要说明的VCT和VCF代码。其中棋盘表示采用了一组15个32位表示,每一个32位当中用30位表示一行,模板拆分成了单方模板,所以穷举了15个点上都是某一方的全部情况并加以评价。这导致了冲棋表表示和更新的一些变化,从而使得评价和走法生成器更准确和容易工作。这些可能会在下一次发布代码时能够看到。下面来说一下VCT,VCF的主要代码和其中的一些思考(因为代码没有整合到程序中,所以只说明一下思路和核心代码):
对于冲棋的情况,前面的代码使用的解决方式是冲棋延伸(就是象棋AI当中的将军延伸),但这并不是一个解决我们问题的好方法。当然,如果我们的程序够快,NPS高达几万几十万K,那这应该没什么问题。
1、我们的程序还很蹩脚,NPS只有几十K,搜索深度少的可怜,如果进行ALPHA BETA FULL SEARCH只能搜索三四层…单纯的延伸不能找到足够深的深度上的杀棋
2、虽然延伸时走法生成器依然把冲棋值更高的点放在前面,但剪裁函数依然要遍历本层才能知道后面的结果,不能快速找到杀棋从而结束搜索
也就是说,单纯的延伸和搜索速度之间相互影响,导致找不到很多VCT\VCF解,而这些解往往很浅(十几二十步),在现有水平下(NPS=60K左右),我们如果能预见最大搜索深度(程序中为16)左右或者从当前深度一直延伸到最大深度,那么我们就应该非常满意了。那么,如何实现这些要求呢,当然,应该是很容易的,因为我们每一步只搜素一部分冲棋招法,所以自然会比完全搜索快很多,也就是说,我们将会写一个类似于“静态搜索”的函数,在遇到合适情况时,调用它来找到VCT,VCF解,并且它也可以用在叶节点评价上。我们有以下需求:
1、评价一个局面、生成需要扫描的招法
2、能够对胜利局面进行截断,判定胜利者,从而找出VCT,VCF解
3、能够返回胜利走法路线
这要比我们的完全搜索函数少很多内容,现在我们逐一实现它:
1、评价和生成招法
我把他们放在一个循环里实现,因为我还在遍历冲棋值表来完成这些工作而不是随着setplayer函数来完成这些工作。评价很简单,按照评分规则给出胜利时的分值即可。而生成招法,需要我们考虑这样的问题:当本方与对方冲棋类型相同(已经形成42或41*2例外),例如都有32时,我们的走法是只生成自己的冲棋,还是堵截一起生成,还是只生成堵截?可能你有疑惑,为什么还涉及到堵截?因为我们使用A-B剪裁,所以如果自己是冲棋,那么对方就应该是堵截。现在假设,我们是冲棋方但走了一个堵截招法,那么对方要冲棋还是堵截?我们堵截之后还能不能继续冲棋…………很多问题随之而来,而且,我们的速度会被拖的非常慢,我们既然要搜索VCT,VCF为什么要去堵截,如果我们堵截了,那么就不是VC了……所以我的想法是在本方有冲棋的时候,去堵截对方这不科学,但同样,如果对方有更快完成的冲棋自己不去堵截,那也不科学(注意我们只搜索冲棋的子树或者冲棋的子树的一部分),于是招法生成器生成的招法,只是本方大于等于对方的冲棋点或者对方大于本方的冲棋点。注意,是两种情况,如果对方的冲棋点更大,那么我们就只生成对方的,换句话说是堵截,否则,就生成本方大于等于对方的,因为轮到我们走么,等于的冲一下就大于了。
所以,评价只给出胜利时的值,因为我们只要进行截断;招法生成只生成堵截或只生成冲棋。
2、对局面进行截断,找出VCT,VCF解
这与A-B剪裁不同,A-B剪裁要从具体分值上知道是否进行剪裁,如何剪裁。而我们需要依据:
A、找到某一方胜利(实际上,我们的搜索并不只是能找到本方胜利的情况,也可以找到对方胜利的情况)
B、达到平静局面,即双方都没有冲棋的局面(其实这种局面并不多,多数情况下会一直冲下去很多步,可见庞大的树庞大的复杂度)
当满足A或B时,我们就要返回了,但是根据B的叙述,很少能找到平静局面,那么怎么做呢,我们采用限定搜索深度的方法就可以了,另外,我们的搜索是对冲棋进行全面搜索,所以可能在没有达到最深深度时,就有某个胜利局面(必胜),于是我们采用迭代加深的方法来调用这个函数。
所以,局面是否截断要看是否达到胜利局面、最大搜索深度、平静局面。
3、返回VCT,VCF路线
这个应该非常简单,只是有别于A-B剪裁,A-B剪裁记录PV路线是在PV招法那里,而我们在截断时,因为需求不同,A-B要知道分值较好的路线即PV路线,而我们要知道胜利路线。
至此,我们的思考也就差不多了,差的发现再改……呃,写代码:
Public Function AlphaBeta_VCTF(pLine As mPVLine, nDepth As Integer, winval As Integer) As Integer Dim line As New mPVLine 'pvs走法 Dim nGenMove As Integer '子节点数 Dim vl As Integer '评价分值
Dim mvs(225) As Byte '子节点走法缓存 Dim mv As Integer '当前走法 '1.生成全部走法和评价值 nGenMove = pos.NextGenerateMove_VCF(mvs, vl) '当nGenMove为-1时,说明没有走法了。 If nDepth = 0 OrElse nGenMove = -1 Then Return vl '2.逐一走这些走法,并进行递归 For i As Integer = 0 To nGenMove mv = mvs(i) pos.SetPlayer(mv, pos.sdPlayer) vl = -AlphaBeta_VCTF(line, nDepth - 1, -winval) pos.SetPlayer(mvs(i), 2) '3.进行截断 If vl = winval Then '找到一个Beta走法 pLine.argmove(0) = mv '记录最佳走法路径 Array.Copy(line.argmove, 0, pLine.argmove, 1, line.cmove + 1) '加入后续走法 pLine.cmove = line.cmove + 1 '更新走法总数 Exit For 'Beta截断 End If Next '4.返回评价 Return vl End Function
简要的分析一下,函数有三个参数,走法路线、深度、截断值。走法路线和以前代码的基本一样,截断值其实就是MATE_VALUE只是层数不一样时正负不同。
1、生成走法和评价。这个函数后面说说明一下。
2、遍历走法。很简单的代码,下一个子,然后撤销。只要注意正负号和深度变化就可以了。
3、截断。当己方胜利时,要记录走法路线,这是需要注意的。
因为走法生成部分涉及更多代码,离我们的VCT,VCF很远,所以这里只给出一些说明:
Function NextGenerateMove_VCF(ByRef nextMove() As Byte, ByRef Eval As Integer) As Integer
1、遍历冲棋值表,统计双方胜利情况、冲棋情况,并将冲棋情况按照:42+32、42、41*2、41+32、41,32+32,32的分组进行排序
2、若对方胜利,则评价值为-Mate_Value,返回值为-1。
3、设置评价返回值为1。
4、若本方存在成5点,则nextMove(0)=成五点,返回值0。
5、若对方存在成5点,则nextMove(0)=成五点,返回值0。
6、按照1所述顺序遍历,按前述返回己方大于等于或对方大于全部点。
End Function
以上这个思路,有一个致命漏洞,它导致冲棋方有“胜利可能”就返回,也就是说没有遍历守棋方的全部守棋就返回了。也就是说我的VCF又要重写了%>_<% 。可以区分-winval和走棋者是否是冲棋方来修正这个错误,也可以对算法再次嵌套来解决,但是不便于理解和维护。因为攻守双方走法生成器往往不同,所以改用Attack function defense function的交叉递归来实现。这次实现的思路明确定位在VCF或VCT胜利是一种“逼迫性”的胜利,即从攻方来看是找到必胜路线,而守方来看就悲催了找不到不输的路线……图示如下:
就是这样……所以明天继续,还是先写VCF,因为VCF要考虑的东西比较少,走法生成器那里又可以直接从“历史表”里面直接读取各种棋型:胜利、44、43、42、4141、33、32等等,大概就是理顺一下上图的思路然后码出来测试一下,然后考虑VCT的问题,已经好几次了,最好这次不要再发现设计的算法有漏洞,不然真的黔驴技穷的时候就要去写IF ....IF ....IF ....IF ....IF ....嵌个十来层?要了亲命了………会死人的。。。。。。。。。。
刚刚把这部分的代码实现完,其中遇到了很多问题,例如咳咳。。复制粘贴代码的时候把对守方的调用多复制了一次,包括下子和提子,要了命了,一个24招的VCF竟然用600ms才收敛完毕,后来发现了,从600ms降低到16……暴汗,又因为用的早期的棋型库,其中有一部分错误,在用VCF习题测试时怎么也通不过,后来才发现,这部分代码开始就不是在没有已知BUG的代码上改的,,又暴汗,改用代码生成模板的代码之后问题解决。。。还没有更多测试,不过看着应该可以了,额,其实以前的也是看着可以了。。。。。把核心代码写一写,这个代码是一个类似于极大极小值算法的一对函数,我会详细注释它们:
Function AttackVCF(nDepth As Integer, pLine As mPVLine) As Integer
1、达到深度返回局面评价
If nDepth <= 0 Then Return mvsort.Evaluate(pos.sdPlayer)
因为分开算的原因么,所以无论哪方评价的时候要传入攻击方,详细的可以参考极大极小值的原理。实际上评价非常简单,只是当攻击方胜利时返回mate_value,防守方胜利返回-mate_value,其他时候都返回1(这个数没有什么实际意义)
2、生成走法
nGenMove = mvsort.NextVCFAttackMove(mvs)
这里的走法生成器是攻方的,它的代码和守方基本相同,攻方只生成41及更高棋型,并判断进攻达到胜利局面还是失败局面(无进攻点也算进攻失败)。所以这个函数的返回值也许用nGenMove来保存不太适合了:
3、截断
If nGenMove = MovesSort.Attack_Failed Then Return -ConstValue.MATE_VALUE
If nGenMove = MovesSort.Attack_Succeeded Then Return ConstValue.MATE_VALUE
4、遍历全部走法并在进攻成功处记录走法
For i As Integer = 0 To nGenMove
mv = mvs(i)
pos.SetPlayer(mv, pos.sdPlayer)
vl = DefenseVCF(nDepth - 1, line)
pos.SetPlayer(mvs(i), 2)
If vl = ConstValue.MATE_VALUE Then
Attack_Succeeded_Index += 1 '记录进攻成功个数
pLine.argmove(0) = mv '记录最佳走法路径
Array.Copy(line.argmove, 0, pLine.argmove, 1, line.cmove + 1) '加入后续走法
pLine.cmove = line.cmove + 1 '更新走法总数
'Exit For
End If
Next
函数中被我注释掉了一个exit for。因为这种截断导致排在后面的走法不被探测,虽然我测试的几个VCF(因为它们多具有反攻,并要使用全局VCF求解,当然你局部VCF代码循环检测也行)没有发现无法找到解的问题,但是我觉得应该去掉它以免某些解被无缘无故的干掉。
5、若存在进攻成功,则说明该路是成功路线,否则继续搜索。
If Attack_Succeeded_Index >= 0 Then Return ConstValue.MATE_VALUE Else Return 1
这就是除了参数声明之外的整个VCF进攻函数,而DefenseVCF函数与这个函数的代码除在防守成功、失败时返回的值与上面相反外,就是Defense_Failed_Index用相同方法统计出来之后,要区分的不是有木有,而是是不是全部。至于防守方的走法生成器,它除了判别谁胜利之外,就是要生成攻方的进攻点,看起来和攻击方走法生成器一样,但实际上它只生成攻击方43、42的棋型,而不生成41。
全部文章和源码整理完成,以后更新也会在下面地址: