“JavaScript中國象棋程序” 這一系列教程將帶你從頭使用JavaScript編寫一個中國象棋程序。這是教程的第8節。
程序的最終效果點擊這里查看。
這一系列共有9個部分:
0、JavaScript中國象棋程序(0)- 前言
在這最后一節,我們的主要工作是使用開局庫、對根節點的搜索分離出來、以及引入PVS(Principal Variation Search)主要變例搜索。
8.1、開局庫
這一節我們引入book.js文件。該文件中定義了一個二維數組BOOK_DAT。這個數組就是開局庫,保存的數據格式如下:
[lock, mv, vl]
其中,lock = zobristLock >>> 1(無符號右移1位,高位補0)
mv是步驟
vl是權重(隨機選擇走法的幾率,僅當兩個相同的lock有不同的mv時,vl的值才有意義,這是為了實現走棋的隨機性)。
開局庫是按照lock排序的,因此可以用二分查找。找到一項以后,把它前后lock相同的所有項都取出,從中隨機選擇一個mv。
壓縮開局庫的容量,所有對稱的局面只用一項,所以當一個局面在BOOK_DAT中找不到時,還應該試一下它的對稱局面是否在BOOK_DAT中。
8.2、對根節點的特殊處理
現在由於開局庫的加入,開局程序的走法具有了一定的隨機性。但如果是開局庫中沒有的局面,程序的走法依然是固定不變的。我們在根節點處,對搜索得到的分值,做小范圍的浮動,以此實現走法的隨機性。
vlBest += Math.floor(Math.random() * RANDOMNESS) - Math.floor(Math.random() * RANDOMNESS);
其中,RANDOMNESS = 8。新的分值會在區間(vlBest - 8, vlBest + 8 )浮動。
我們把對根節點的搜索分離出來,這有以下好處:
1、更方便實現走法的隨機性
2、沒有必要嘗試Beta截斷(根節點處Beta正無窮);
3、省略了檢查重復局面、獲取置換表、空步裁剪等步驟。
8.3、PVS主要變例搜索
經過前面的工作,走法已經得到了很好的排序,好的走法會先被搜索。這是PVS的基礎。
圖a 圖b
假設第一個走法是最好的走法,沒有引發剪枝,A點的搜索區間為(0, 100),走法1得到估值30。由於30 > 0,所以A點的alpha變為30,以后的搜索區間變為(30, 100),所以B2點的搜索區間為(-100, -30)。
可以進一步大膽地考慮,假設第1個走法就是最好的走法,那么后面走法得到的估值不會落在區間(30, 100)。所以從A點的第2個走法開始,要做的就是驗證這種假設,搜索區間為(30, 31)。由於搜索區間很小,搜索速度會很快。返回值vl有3種情況。
(1)、vl <= 30。說明走法不比第1個走法好,假設成立。
(2)、vl >= 100。返回值比A點的原有搜索邊界beta還大,應該剪枝,假設成立。
(3)、30 < vl < 100。走法比第1個走法好,假設不成立。
第3種情況時,走法不成立,應該對該走法重新以(30, 100)區間進行搜索。如果得到40,則該走法就是最好的走法,后續搜索又對該走法進行假設驗證,區間為(40, 41)。
8.4、長將判負策略
程序會調用repStatus函數,判斷局面是否出現重復,以及長將,這在第6節已經介紹。如果出現長將,會得到-BAN_VALUE(其中,BAN_VALUE = MATE_VALUE - 100),再根據殺棋步數做調整。但是由於長將判負並不是對某個單純局面的評分,而是跟路線有關的,把這個分值直接存入置換表就不太合適了。
我們的解決辦法就是:獲取置換表時把“利用長將判負策略搜索到的局面”過濾掉。如果某個局面分值在WIN_VALUE(MATE_VALUE - 200)和BAN_VALUE之間,那么這個局面就是“利用長將判負策略搜索到的局面”。如果是通過repStatus函數得到了和棋的分值,會同樣被過濾掉。
我們仍舊把“利用長將判負策略搜索到的局面”記錄到置換表,因為這些局面提供的最佳走法是有啟發價值的。反過來說,如果“利用長將判負策略搜索到的局面”沒有最佳走法,那么這種局面就沒有必要記錄到置換表了。
8.5、核心代碼說明
本節的代碼可以在 Github 下載,也可以直接clone
git clone -b step-8 https://github.com/Royhoo/write-a-chinesechess-program
Position中新增或修改的主要屬性和方法:
(1)、bookMove()
獲取開局庫中的走法。
Search中新增或修改的主要屬性和方法:
(1)、searchRoot(depth)
對根節點的搜索。