刚刚写完了第4个程序,实现了迭代加深、空步剪裁、冲棋延伸。(棋盘剪裁已经在第3个程序里面实现了)。本来准备写第5个程序,不过有点累了,就没有继续写。后面几篇更新的速度会慢一些,主要是写完之后我还需要仔细检查一下,这样一个程序尤其是偌大一个递归函数里面搞来搞去的,难免出现一些问题,尤其是手误,逻辑怎么看都没问题,又处于递归当中很难调试查找。为了示例程序尽可能少的出现问题,我会进行一定量的测试,当然测试量不可能很大,我还没有写和其他程序下棋的接口,所以无法从大量对局中查找问题和评价程序的真实棋力。
引擎下棋的最基础的东西,就是评价局面。这往往需要大量的对局和校正参数,而我没有做这些工作,原因前面已经说过了。但是不代表我们无法粗略的设置一些分值,尤其是像五子棋这样的程序,评分不是子力,而应该是从“形”“势”角度出发,所以没有固定的子力分值评价和位置分值评价。在我的程序中,是使用模板来进行棋型评价(这和我最初发布的文章当中有所不同,因为经过测试,发现那种方法不仅难于编码和调试,而且速度并不比模板匹配快多少,所以综合起来还是采用了模板评价)的,然后综合这72个评价结果并与对方棋72个结果进行比较来得到分值。原文地址http://www.cnblogs.com/zcsor/例如,对方有一个冲4,而我们双活3,我们理所应当的去封堵,但是我们能认为双活3的棋力小于冲4吗?当然不。所以我采用了上述的评价方式。具体实现是这样的:
1、为什么是72个?
72这个数,是五子棋上的“成棋向量”,只有72个方向能成5,而其他方向不够长度……这么说吧,横向15,纵向15,左下右上21,左上右下21,一共72。
2、如何得到这72个向量?如何记录它们?
得到这些向量很容易,可以硬编码,也可以循环遍历。我才用了循环遍历的方式,因为为了速度更快,我需要记录向量上的每个点,而不是计算它们。——当然,这里我没有进行测试,我感觉,一个14个元素的byte数组寻址速度不会很慢。
代码就像这样:
Dim x, y As Integer
' 横向
For y = 0 To 14
all.Add(GetVector( 0, y, 14, y, 1, 0))
Next
' 纵向
For x = 0 To 14
all.Add(GetVector(x, 0, x, 14, 0, 1))
Next
' 右上
For y = 4 To 14
all.Add(GetVector( 0, y, y, 0, 1, - 1))
Next
For x = 1 To 10
all.Add(GetVector(x, 14, 14, x, 1, - 1))
Next
' 左上
For x = 4 To 14
all.Add(GetVector(x, 14, 0, 14 - x, - 1, - 1))
Next
For y = 13 To 4 Step - 1
all.Add(GetVector( 14, y, 14 - y, 0, - 1, - 1))
Next
' 分配到点记录表
Dim i As Integer
For x = 0 To 14
For y = 0 To 14
Dim ls As New List( Of mVector)
' 遍历全部向量,将点所在的向量保存到ls。
For i = 0 To 71
If InLine(i, y * 15 + x) <> - 1 Then ls.Add(all(i))
Next
' 以点坐标为键,加入表中。
hs.Add(y * 15 + x, ls)
Next
Next
其中的hs是一个哈希表,为什么需要这个表呢?因为我们需要知道一个点所在的全部向量,这样我们放一个棋子或删除一个棋子之后,就可以知道哪些向量需要更新评价值,而不是更新全部的向量评价,要知道模板匹配即使使用了内存比较API也非常慢,何况我们是从一个长度可能达到14的信息中遍历若干个模板的位置。当然,现在的做法完全可以进一步优化,但是我准备在发完这些连载之后,再抽时间进行细致的优化或许到时候能够找到更有效更快速的方式。
3、得到向量之后,如何评价?
我们使用一个数组,来记录当前向量上的全部子和空位,而后和模板数组进行比较,从而得到棋型信息。我们不会得到棋型的具体分值,最终综合棋型才得到得分。
实际的评价函数,是一个非常长的函数:
Sub Evaluate(ucpc As mBitBoard)
' 向量点数组最大下标
Dim infend As Integer = ps.Length - 1
' 循环变量
Dim i As Integer
' 本方、对方在向量上的子分布信息
Dim inf1(infend) As Byte
Dim inf2(infend) As Byte
' 循环访问向量上指向的棋盘点
For i = 0 To infend
If ucpc.Get(ps(i)) = 1 Then ' 黑子
inf1(i) = 1
inf2(i) = 2
ElseIf ucpc.Get(ps(i)) = 0 Then ' 白子
inf1(i) = 2
inf2(i) = 1
Else ' 无子
inf1(i) = 0
inf2(i) = 0
End If
Next
' 白子棋型
linkInfs( 1) = EvaluateShape(inf1, 0, infend)
' 更新冲棋点坐标为棋盘坐标
For i = 0 To linkInfs( 1).cqpend
linkInfs( 1).cqp(i) = ps(linkInfs( 1).cqp(i))
Next
' 黑子棋型
linkInfs( 0) = EvaluateShape(inf2, 0, infend)
For i = 0 To linkInfs( 0).cqpend
linkInfs( 0).cqp(i) = ps(linkInfs( 0).cqp(i))
Next
End Sub
看起来可能非常短,但是实际上……这是因为提取了EvaluateShape函数,这个函数看起来甚至让人憎恶,因为它长得如此丑陋:
Private Shared Function EvaluateShape(inf As Byte(), infstart As Integer, infend As Integer) As LinkInfo
Dim count As Integer
Dim lnkinf As New LinkInfo
' 长连
If CompareArray(inf, infstart, infend, mll1, count) Then
lnkinf.lnk = 60
Return lnkinf
End If
If CompareArray(inf, infstart, infend, mll2, count) Then
lnkinf.lnk = 60
Return lnkinf
End If
If CompareArray(inf, infstart, infend, mll3, count) Then
lnkinf.lnk = 60
Return lnkinf
End If
If CompareArray(inf, infstart, infend, mll4, count) Then
lnkinf.lnk = 60
Return lnkinf
End If
' 成5
If CompareArray(inf, infstart, infend, ml5, count) Then
lnkinf.lnk = 50
Return lnkinf
End If
' 活4
If CompareArray(inf, infstart, infend, ml42, count) Then
lnkinf.lnk = 42
' 在这里记录的冲棋坐标是inf的下标,也是ps的下标,而不是实际棋盘坐标。
' 虽然可以用循环来找到模板当中的0,但是比硬编码要慢很多。
lnkinf.cqp( 0) = count
lnkinf.cqp( 1) = count + 5
lnkinf.cqpend = 1
Return lnkinf
End If
…………
此处省略一万句
…………
事实上,没有一万句那么多,但是加起来模板和模板匹配函数,有几百行了。具体情况还是请各位看代码吧。
4、得到向量的棋型评价和冲棋信息之后,是如何评价局面的?
实际上,局面评价函数也是面目狰狞的……不过幸好线条很清晰:
首先,我们要遍历每个向量,如果需要更新,那么就运行向量的评价函数。
If all(j).update Then ' 若需要,则更新棋型评价。
all(j).Evaluate(ucpc) ' 更新函数同时更新黑白两色。
all(j).update = False
End If
然后,我们分别统计己方、对方的72向量上的长连、成5、活4、冲4、活3、冲3、活2,,,,停停停,后面的就没有了!
' 本方
Select Case all(j).linkInfs(player).lnk
Case 60 ' 长连
l60_1.Add(all(j).linkInfs(player))
Case 50 ' 成5
l50_1.Add(all(j).linkInfs(player))
Case 42 ' 活4
l42_1.Add(all(j).linkInfs(player))
Case 41 ' 冲4
l41_1.Add(all(j).linkInfs(player))
Case 32 ' 活3
l32_1.Add(all(j).linkInfs(player))
Case 31 ' 冲3
l31_1.Add(all(j).linkInfs(player))
Case 22 ' 活2
l22_1.Add(all(j).linkInfs(player))
End Select
最后,我们根据不同的情况,给出不同的得分,例如死棋——对方死棋,那么判断对方是否禁手,如果禁手并且走了禁手那我们胜利了(好像是捡的)……如果我们称5,那我们理所当然的胜利了(当然如果是对方没看见我们偷偷的下上的,也捡到了……但是这种情况是幻觉…………)。当然还有很多情况,我们需要一一评价。总体来说,冲棋部分处理起来代码最多,但是却得到了额外的好处——可以直接生成冲棋点,也就是说不用生成全部招法,而得到非常准确的招法点。
' 1、被杀死
' 1.1、对方成5
If l50_2.Count > 0 Then Return - 10000
' 1.2、对方不禁手
If RestrictedMove <> 1 - player Then
If l60_2.Count > 0 Then Return - 10000
End If
' 1.3、被禁手
If RestrictedMove = player Then
If l60_1.Count > 0 AndAlso l50_1.Count = 0 Then Return - 10000 ' 长连,但是同时成5不为禁手。
If l32_1.Count > 1 Then Return - 10000 ' 双活三禁手
If l42_1.Count > 1 Then Return - 10000 ' 双活四禁手
If l41_1.Count > 1 Then Return - 10000 ' 双冲四禁手
End If
这就是整个评价过程的全貌了。
实际上,评价结果的值只有这么几个:10000、-10000、5000、-5000和一些比较小的数(冲3活2只是根据计数进行了简单评分)。当然,冲3活2的评分过程中,应该考虑先行权分值问题(程序中这个值暂时置0),但是其他的评价里面个人认为不需要。
程序完整源码如下:
下集预告:
3、基石——超出边界的alpha-beta剪裁
下一集的程序就可以称为一个真正的五子棋程序了,虽然水平不高,但是毕竟可以走棋了。努力有了回报,总是非常高兴的事情。强烈建议在我更新下一集之前(今天我不会再更新了,至少要等我写完第6个程序——置换表并经过一定量的测试和优化之后,才会发出来。因为冲棋延伸的代码并不让我满意,也许短短的1行并不能解决全部问题。),仔细阅读关于alpha-beta的原理,最好是自己实现这个递归函数,当然看懂也不错。因为这是整个程序最核心的内容之一,虽然这一集也是,代码也很多,但只要耐心看,很容易能弄懂;可alpha-beta剪裁不是那么回事,代码加上注释和空行才不到50行,但是如果不充分理解递归设计方法和运行特性,对后面的空步剪裁、冲棋延伸代码的编写会造成很大的麻烦。建议先自己写一个简单的递归函数,理解一下递归语句前面一直进栈后面一直出栈和单次运行合理即可的设计思想。总而言之一句话,程序能不能真正下起来棋,就看这一哆嗦了!好了,不写了,去看看我的静态搜索应该改几句代码,然后就是构思重构置换表!代码是不写了,累了……
转载请注明出处:
原文地址
全部文章和源码整理完成,以后更新也会在下面地址: