這幾天更新了一些內容,在現在發布的程序當中存在若干處錯誤,都被修復了。其中包括模型評價、局面評價、置換表提取等關鍵部分的錯誤。程序的基本框架沒有太大變化,增加了PV路徑記錄,從而可以得到除了最佳招法之外的走棋路線,修改了模板當中的沖棋點部分,准備實現VCn搜索、回溯搜索,但是由於思路上還有一點問題所以還沒有真正付諸實施。在修復錯誤並增加了幾條知識之后進行了一定的測試,現在和連珠妙手(fiver6、豬八戒級別)進行對戰測試,勝率大概在30%-40%,但是棋局下的比較少,數據可能不很准確。不過我不想和這些知名軟件做比較,只是為了給程序排查錯誤和增加一些知識,畢竟我本人的五子棋水平非常低。當實現VCn搜索和回溯搜索之后,會把根節點的搜索單獨列出,之后會發布一個版本,這個版本將會作為這一階段的最終版本,也可能以后就不更新了;當然,如果時間太少,可能暫時放棄VCn和回溯的開發,那么會發布現在的這個版本。2012年8月1日。
今天更新這一版本的程序,主要做了以下修改:
1、將常量放在一個單獨類
2、用向量類代替棋盤類並更改記錄方式
3、新的評價方法
4、根據棋型生成key
5、下子時只更新被改變的向量
6、實現雙置換表
7、使用:歷史表、alpha-beta剪裁、空步剪裁、沖棋延伸、主要變例搜索、迭代加深技術。不使用靜態搜索等技術。
8、統計相關信息,以便計算置換表命中率、每秒搜索節點數、等等信息。
遺留問題:
1、棋型提取和評價函數。雖然找到了更優的棋型提取方法和評價函數,並且理論上速度可以達到與象棋引擎接近甚至更快的速度,但是代碼還沒寫。
2、更好的剪裁方式。雖然現有的剪裁方式已經不錯了,但是只要挖掘就還能找到更好的方法。就像代碼中的棋盤剪裁更新一樣。
接下來解釋一下置換表和更新的這些部分的代碼,以便下載源程序后更快的看完它。
‘============================以下更新前內容保留==================================
1、什么是置換表
它記錄了一個局面、局面評分等相關信息,用來在搜索過程中用來將評價得分這一系列的運算”置換“為查表得到結果。它的目標是減少運算量,提高速度。
2、置換表都記錄什么,如何處理
因為搜索時,很多情況下能遇到相同的局面已經搜索過的現象。所以,如果我們能記錄下一個局面、評分、類型、深度,那么,當我們再遇到這個棋型時,只需要知道,若當前深度小於等於記錄深度,那么就返回評價,當然還要看記錄的局面的節點類型,稍微處理一下。
3、如何實現置換表
我們最好用一個數(key)來記錄一個局面,然后,根據這個數,就能找到評分、類型、深度等信息。怎么看都是使用key-value的東西,但是我測試了一下,哈希表速度要比前輩們的方法慢很多。他們把這個key處理了一下:變成下標(key mod len),那么好吧,這樣做的速度被證實非常快。而這同時也涉及到一些問題,其中最嚴重的就是,如果我把長度設置的較小例如10,那么它就無法起到記錄局面的作用了(因為它的內容不斷的被更新,而我們查找的時候根本找不到過去的局面),可多大行呢,這不好說,除非你設置的置換表和你能夠經歷的局面相等,呵呵,整個硬盤作為虛擬內存都未必夠用,估計初始化置換表就要很久很久……所以這個”適當“的值,很難說,我的做法是:設置一個盡量大的值,這可以減少重復提高效率,而前提是,初始化過程,不超過1秒。我的計算機可以初始化1<<24這么多,而不會超過一秒。所以我就設置了這么大一個置換表。
那么,思路已經很清晰了,你可以先去閱讀象棋小巫師的源代碼,看看他是如何實現置換表的,當然,如果你感覺它的RC2改成VB.NET讓你頭疼,那么建議你使用RC4,因為VB.NET已經提供了這種算法,你只需要稍微修改一下原來的代碼,因為RC4是一個64位的結果。但是需要注意的是,我們得到局面的dwkey沒有那么麻煩,你只需要去修改mbitboard類,在set函數中對dwkey進行更新就可以了。因為局面的變化在這里最容易記錄。
’==============================以上更新前內容保留=================================
好了,現在我們介紹一下新程序,當然,它還會被更新幾次。幸好今天我測試的時候感覺棋力還可以了,雖然還會出現明顯的失誤,但可以肯定的是,這些失誤是由於分值設置引起的,而沒有大量對局參考的情況下,這些數據很難做到較為准確。
1、更新的常量類
我把常量都放在一個類里面,這樣修改時可以避免很多麻煩。這沒有什么好介紹的,並且源碼中注釋非常明確。
2、模板類
這個類負責評價一個向量上是否存在這個模板。它有一些成員變量需要解釋一下:
Public len As Integer
' 模板含有棋子數
Private pipecount As Integer
' 模板
Private infow As Integer
Private infob As Integer
' 模板返回值
Public value As Integer
' 適用於本模板的信息截斷
Private make As Integer
例如,在模板 New mMod({0, 1, 1, 1, 1, 0}, 42)中,
模板長度:6,也就是說,這個模板將檢測向量中連續6位。
模板含有棋子數:4
infow和infob這是白棋和黑棋模板,它們被new函數根據傳入數組初始化。
value是模板對應的棋型,也就是上面new函數中的42。
make是掩碼信息,它用於把向量的棋型當中無用的(前面)部分去掉。
new函數是這樣的:
len = bs.Length
pipecount = val \ 10
value = val
For i = 0 To bs.Length - 1
infow = infow << 2
infob = infob << 2
If bs(i) = 1 Then
infow = infow Or CInt( 1)
infob = infob Or CInt( 2)
End If
make = make Or ( CInt( 1) << (i * 2)) ' 遮蔽,把模板中用到的位都置1。所以只需要對信息進行AND操作,就可以去掉信息中無用部分。
make = make Or ( CInt( 1) << (i * 2 + 1))
Next
End Sub
其中pipecount的初始化時根據val這可能要解釋一下,因為在程序中定義:長連為60,連5為50,活4為42,沖4為41……所以,分值整除10就是含有的棋子個數。
在循環中,分別記錄黑棋和白棋的模板,因為如果記錄到同一模板,那么判斷黑棋時非常麻煩!並不是>>1就能解決的問題(會遺漏白棋,使得判斷結果不准確)。
New mMod({0, 1, 1, 1, 1, 0}, 42) 這個模板的二進制表示看起來是這樣的:
infow=00 01 01 01 01 00
infob=00 10 10 10 10 00
make=11 11 11 11 11 11
可以看出,白棋和黑棋他們分別占據每2位中的后一位和前一位,而在向量中記錄棋型時,也是這樣:
好了,這些代碼我們不做更多的解釋,在vector類中再詳細說明。現在我們只需要知道,在向量和模板中,我們從最右面開始用連續2位記錄棋盤上的一個位置,其中后一位為白棋,前一位為黑棋,若沒有棋子則這兩位均為0,但絕不可能出現兩位都為1的情況。
於是,我們的比較函數簡單了一些——我們進行了“塊”比較,但這不是完整的塊,只是integer的比較速度要快於byte()的比較速度,所以比較函數速度比原來快。后來我想到了不用遍歷整個向量信息的方法,而且不是比較整個30位(可以想象,比較全部30位將有多少模板……那是難以完成的工作),當然這是以后我們要討論的。所以,現在的比較函數還存在for循環:
inf0 = inf0 And make ' 將無用信息去掉
If inf0 = infow Then ' 符合模板
vector.value( 0) = value ' 記錄模板值
vector.update( 0) = False ' 已符合,無需繼續掃描
End If
首先,使用位移運算從向量信息取出一部分,然后用and運算把信息中前面的多余部分去掉,直接和模板比較數值是否相等就可以了。當然,如果相同,我們還需要更新一下向量的相關值和更新標志。
3、模板管理類
這個類負責初始化全部模板,並且,負責比較一個向量符合哪一個模板,它的初始化函數非常簡單,我們只介紹一下評價函數:
If vector.info <> 0 Then
Dim i, ilen As Integer
For i = 0 To AllMod.Length - 1
ilen = AllMod(i).len
If vector.len >= ilen Then ' 若向量長度不小於模板長度
AllMod(i).CompareMod(vector)
If vector.update( 0) = False AndAlso vector.update( 1) = False Then
Return
End If
End If
Next
End If
If vector.update( 0) Then
vector.value( 0) = 0
vector.update( 0) = False
End If
If vector.update( 1) Then
vector.value( 1) = 0
vector.update( 1) = False
End If
End Sub
在這段代碼中我們統一評價白棋和黑棋:
首先我們檢查向量是否為空,若為空,則設置值為0並且更新標志為空;
而后,比較向量長度和模板長度,得出相應的評價;
應該注意的是,vector.update(0) = False AndAlso vector.update(1)這一條件,僅有這個條件是不夠的,因為我們的模板不是全部情況都概括了,所以,當評價混合棋型時、棋型變為空時,可能不會更新某些向量的棋型標志和更新標志,這是非常嚴重的。
4、向量類
Public update( 1) As Boolean
' 轉換表
Private table( 224) As Byte
' 向量上的棋型
Public info As Integer
' 向量長度
Public len As Integer
' 向量上白棋、黑棋的個數
Public pipecount( 1) As Byte
' 向量上白棋、黑棋的棋型
Public value( 1) As Byte
' 向量方向
Private Direction As Integer
' 坐標表
Public points() As Byte
Sub New(ps As Byte(), dir As Integer)
points = ps
Direction = dir
len = ps.Length
For i = 0 To ps.Length - 1
table(ps(i)) = i ' 轉換表的下標對應着在points的值,而值對應下標在points中的位置。
Next
End Sub
轉換表:這個表在new函數中被初始化,可以看出,它的值記錄了i,這個i實際上就是它的下標對應的ps當中的位置,簡單地說,通過它,我們可以快速的得到一個坐標在ps數組當中的下標。
向量方向:這個值就是用來記錄向量是4個方向中的那個方向,只用於更新相應方向上的key。所以,一個局面有4個key。簡單的來說,這4個key就是棋型在該方向垂直方向上的投影。
get函數非常簡單,我們不做介紹了,解釋一下set函數當中的關鍵代碼(以刪除一個子為例,下一個子和刪除的情況基本一致):
info = info And Not ( CInt( 1) << table(point) * 2) ' 刪除白棋
info = info And Not ( CInt( 1) << table(point) * 2 + 1) ' 刪除黑棋
mVectorManeger.keys(Direction) = mVectorManeger.keys(Direction) Xor info ' 記錄新的記錄
pipecount(bp) -= 1 ' 記錄白棋或黑棋個數
update( 0) = True ' 記錄需要更新,無論白棋變了還是黑棋變了,棋型都變化,所以同時需要更新。
update( 1) = True
mVectorManeger.shapes( 0)(value( 0)) -= 1
mVectorManeger.shapes( 1)(value( 1)) -= 1
mModManager.Evaluate( Me)
mVectorManeger.shapes( 0)(value( 0)) += 1
mVectorManeger.shapes( 1)(value( 1)) += 1
key的更新:
首先,在key中去掉當前棋型
然后,刪除白棋和黑棋(當然,bp值=0就是白棋,=1就是黑棋)。刪除的方法很簡單,就是把1移動到指定位置,然后not,這樣其他位都是1,而指定位置為0,接下來and。
最后,在key中記錄更新后的棋型
中間的代碼更新了棋子個數、更新標志。最后:
評價前:從棋型記錄中減去原來的棋型(注意,雖然棋型確實更新了,但是沒有經過評價,向量的value是不變的)
評價:按照update來更新棋型信息
評價后:在棋型記錄中加上現在的棋型
5、向量管理
這個類的代碼非常少。也很容易看懂。其中set函數中 Dim a = mVectorManeger.shapes沒有什么實際意義,只是用來在這里檢查棋型匯總是否正確,這是我在調試過程中遺留的。
6、局面類
局面類進行了較多更新,例如分離了禁手判斷等函數,但基本上還是很容易看懂的。需要介紹的是原來在bitboard類中的GenerateMoves函數:
Function GenerateMoves( ByRef mvs() As Byte, mv As Integer) As Integer ' 生成所有走法
' 臨時變量,存儲當前局面下每個子周圍三格以內的空位
Dim tmp As BitArray = GetGeneratePoints()
Dim offset As Integer = 0
If mv > - 1 Then
tmp.Set(mv, False)
offset = 1
End If
' 統計全部空位
Dim i As Integer = 0, nGenMoves As Integer = offset
For i = 0 To 224
If tmp(i) Then
mvs(offset + nGenMoves) = i
nGenMoves += 1
End If
Next
Return nGenMoves - 1
End Function
函數中的mv是置換表招法,所以函數所做的改動是為了把置換表招法放在mvs(0)這個位置,並且,tmp.set(mv,false)保證了后面的搜索中不會再次找到這個招法。
然后是置換表的覆蓋和提取,因為我們使用了2個置換表(橫向和縱向,兩個斜向沒有使用),所以查找和保存邏輯稍微復雜一些。尤其是保存邏輯:
Sub RecordHash(nFlag As Integer, vl As Integer, nDepth As Integer, mv As Integer)
' 被替換的置換表
Dim hshtindex As Integer = - 1
Dim hsh, hsh0, hsh1 As mHashItem
' ===============================置換表覆蓋策略=================================
' 0、查找空的,直接覆蓋。若沒有空的:
' 分別找到置換表0、1當中對應的元素,
' 1、若元素完全符合某一個,則
' 1.1、若深度更深,則直接覆蓋
' 1.2、否則,退出
' 2、若不完全符合任何一個,則
' 2.1、若深度比置換表中深度更淺的深,則覆蓋這個
' 2.2、否則,退出
hsh0 = hstb0(mVectorManeger.keys( 0) And mConstValue.HASH_SIZE_S1) ' 提取
If hsh0.dwLock_a = 0 Then ' 若為空,直接覆蓋
hshtindex = 0
Else ' 不為空
' 若一致
If (hsh0.dwLock_a = mVectorManeger.keys( 1)) AndAlso (hsh0.dwLock_b = mVectorManeger.keys( 2)) AndAlso (hsh0.dwLock_c = mVectorManeger.keys( 3)) Then
If hsh0.ucDepth < nDepth Then ' 若深度更大則更新
hshtindex = 0
Else ' 若深度更小則返回
Return
End If
End If
' 若不一致,查找下一個
End If
If hshtindex = - 1 Then
hsh1 = hstb1(mVectorManeger.keys( 1) And mConstValue.HASH_SIZE_S1)
If hsh1.dwLock_a = 0 Then
hshtindex = 1
Else
If (hsh1.dwLock_a = mVectorManeger.keys( 0)) AndAlso (hsh1.dwLock_b = mVectorManeger.keys( 2)) AndAlso (hsh1.dwLock_c = mVectorManeger.keys( 3)) Then
If hsh1.ucDepth < nDepth Then
hshtindex = 1
Else
Return
End If
End If
End If
End If
' 若沒有找到空的、完全符合的,則覆蓋深度更小的。
If hshtindex = - 1 Then
If hsh0.ucDepth < hsh1.ucDepth Then
If hsh0.ucDepth < nDepth Then hshtindex = 0
Else
If hsh1.ucDepth < nDepth Then hshtindex = 1
End If
End If
' 若沒有深度更小的,那么不記錄。
If hshtindex = - 1 Then
Return
End If
If hshtindex = 0 Then hsh = hsh0 Else hsh = hsh1
hsh.ucFlag = nFlag
hsh.ucDepth = nDepth
If vl > mConstValue.WIN_VALUE Then
hsh.svl = vl + nDistance
ElseIf vl < -mConstValue.WIN_VALUE Then
hsh.svl = vl - nDistance
Else
hsh.svl = vl
End If
hsh.wmv = mv
' 保存到置換表
hsh.dwLock_b = mVectorManeger.keys( 2)
hsh.dwLock_c = mVectorManeger.keys( 3)
If hshtindex = 0 Then
hsh.dwLock_a = mVectorManeger.keys( 1)
hstb0(mVectorManeger.keys( 0) And mConstValue.HASH_SIZE_S1) = hsh
Else
hsh.dwLock_a = mVectorManeger.keys( 0)
hstb0(mVectorManeger.keys( 1) And mConstValue.HASH_SIZE_S1) = hsh
End If
c += 1
End Sub
不用關心c這個變量,它是用來記錄置換表中一共存儲了多少局面的。
這個邏輯是這樣的:
A、找到兩個置換表中與當前局面一致的局面,能覆蓋則覆蓋,不能就不記錄了
B、如果沒有一致局面,則找到空的,記錄當前局面
C、如果沒有一致局面,也沒空的,那就覆蓋深度更淺的。
D、如果還沒記錄下來,那算了吧……
7、掃描類
這個類就是核心了,其中包括各種剪裁技術。但是前面我們已經做過非常多的介紹了,至少,原理是沒有什么問題(之所以這么說,是因為我懷疑前面的代碼中有一處甚至若干處難以查找的“手誤”),所以應該很容易能夠看懂這些代碼。
下集預告:
下次更新的時候,我希望迭代加深能夠超過現在的6——通過優化棋型評價和局面評價(雖然現在是按需更新,但也許我們可以不更新整個局面)。當然,你如果使用更嚴格的棋盤剪裁,現在就可以達到8。所以,歷史表和階梯式棋盤剪裁結合起來也許效果更好,但是這還沒有進入日程。
本集源碼:
全部文章和源碼整理完成,以后更新也會在下面地址: