剛剛寫完了第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行,但是如果不充分理解遞歸設計方法和運行特性,對后面的空步剪裁、沖棋延伸代碼的編寫會造成很大的麻煩。建議先自己寫一個簡單的遞歸函數,理解一下遞歸語句前面一直進棧后面一直出棧和單次運行合理即可的設計思想。總而言之一句話,程序能不能真正下起來棋,就看這一哆嗦了!好了,不寫了,去看看我的靜態搜索應該改幾句代碼,然后就是構思重構置換表!代碼是不寫了,累了……
轉載請注明出處:
原文地址
全部文章和源碼整理完成,以后更新也會在下面地址: