“JavaScript中國象棋程序” 這一系列教程將帶你從頭使用JavaScript編寫一個中國象棋程序。這是教程的第4節。
上一節的程序,電腦是在隨機走棋,這樣太沒勁了。這一節我們的程序中加入極大極小搜索算法,這樣程序會稍微有點智商。不過棋力不高,也就是普通小學生的水平吧。
4.1、局面評估
局面評估,就是判斷局面對紅方(或黑方)的優勢,並把優勢量化。棋子價值可用以下不等式表達:
帥 > 車 > 馬、炮 > 仕、相 > 兵
棋子價值可以簡單量化為:
兵 |
仕 |
相 |
炮 |
馬 |
車 |
帥 |
10 |
20 |
20 |
40 |
45 |
90 |
1000 |
但是棋子價值是跟位置有關系的,比如兵在過河前價值很小,過河后價值大漲。在我們的程序中,兵的位置價值數組如下:
[ // 兵 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 11, 13, 11, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 19, 24, 34, 42, 44, 42, 34, 24, 19, 0, 0, 0, 0, 0, 0, 0, 19, 24, 32, 37, 37, 37, 32, 24, 19, 0, 0, 0, 0, 0, 0, 0, 19, 23, 27, 29, 30, 29, 27, 23, 19, 0, 0, 0, 0, 0, 0, 0, 14, 18, 20, 27, 29, 27, 20, 18, 14, 0, 0, 0, 0, 0, 0, 0, 7, 0, 13, 0, 16, 0, 13, 0, 7, 0, 0, 0, 0, 0, 0, 0, 7, 0, 7, 0, 15, 0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]
在初始位置,中兵價值15,其他四個位置價值都是7。位於九宮的中心位置時,價值達到最高的44。這個數組肯定不是憑空想象出來的,應該是象棋百科全書網的前輩,經過無數次的試驗得到的。
帥的位置價值數組如下:
[ // 帥 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]
帥是無價的。沒有了帥,游戲是要結束的。數組里的1、2、11、15僅僅表示了帥的位置分值,並不是說帥只值這個數。由於兵、帥的位置沒有重合,這兩個數組可以合並。
如此一來,每種棋子就都會有一個與絕對位置相關的價值數組,因此我們的程序里有一個常量數組PIECE_VALUE[7][256]。該數組只提供了紅方的位置分值。想獲得黑方的位置分值,使用前面介紹過的函數SQUARE_FLIP(sq)翻轉一下位置即可。此外,我們在Position對象中定義vlWhite和vlBlack兩個屬性,分別表示紅方和黑方棋子價值。每次調用addPiece(sq, pc, bDel)增刪棋子時,都會更新vlWhite和vlBlack。獲取紅方優勢的局面評估函數:
Position.prototype.evaluate = function() { var vl = this.vlWhite - this.vlBlack; return vl; }
4.2、簡單的兩層搜索
圓形節點為紅方走棋的局面,方形節點為黑方走棋局面,紅色數字為局面估值(也就是紅方的優勢)。深度優先遍歷這個搜索樹。
(1)、初始局面為A,該紅方走棋。紅方有B1、B2、B3三種走法。
(2)、假設紅方選擇第一種走法,走到了局面B1。
(3)、在局面B1,該黑方走棋。黑方有C1、C2、C3三種走法。
(4)、C1、C2、C3是葉子節點,調用局面評估函數,算得局面估值(也就是紅方優勢)分別是8、10、15。
(5)、回到局面B1,此時該黑方走棋。黑方后完后,紅方優勢越小,對黑方越有利。黑方自然會走到對自己最有利的C1局面。此時我們認為,B1局面的估值是8。
(6)、同樣的方法,B2、B3的估值分別是5、10。
(7)、回到初始局面A,紅方走棋。現在已經知道,選擇第一種走法,最終估值為8;選擇第二種走法,最終估值為5;選擇第三種走法,最終估值為10。估值就是紅方的優勢,紅方自然會選擇對自己優勢最大的走法,也就是走到局面B3。
(8)、可知行棋路線為A -> B3 -> C7。
A點選擇估值最大的局面,稱為極大點;B1、B2、B3選擇估值最小的局面,稱為極小點。
4.3、極大點搜索算法
在程序中,定義一個全局變量,表示搜索深度:
var MINMAXDEPTH = 3;
也就是說,程序會搜索3層。當然你也可以改為4,這樣電腦的棋力會更強。當搜索深度超過4時,電腦會走得很慢很慢。根節點的層次是MINMAXDEPTH,每往下搜索一層,層次減1。當層次為0時,不再向下搜索,而是調用評估函數計算估值。
偽代碼如下:
// 極大點搜索 Search.prototype.maxSearch = function(depth) { 如果depth等於0,調用評估函數並返回分值 var vlBest = 負無窮; // 初始最優值 var mvs = 當前局面全部走法; // 生成當前局面的所有走法 var value = 0; for (var i = 0; i < mvs.length; i ++) { 執行招法mvs[i] 調用極小點搜索算法,深度設為depth-1,並將返回值賦給value 撤銷招法mvs[i] if (value > vlBest) { // 尋找最大估值 vlBest = value; if (depth == MINMAXDEPTH) { // 如果回到了根節點,需要記錄根節點的最佳走法 記錄根節點的最佳走法 } } } return vlBest; // 返回當前節點的最優值 }
4.4、極小點搜索算法
極小點搜索這與極大點搜索很相似,但有3處不同:
(1)、初始最優值為正無窮。
(2)、遞歸調用的是極大點搜索算法。(而極大點搜索算法會遞歸調用極小點搜索算法)
(3)、尋找的是最小估值。
4.5、極大極小搜索算法
if (該紅方走棋) {
調用極大點搜索算法,深度為MINMAXDEPTH
} else {
調用極小點搜索算法,深度為MINMAXDEPTH
}
4.6、核心代碼說明
本節的代碼可以在 Github 下載,也可以直接clone
git clone -b step-4 https://github.com/Royhoo/write-a-chinesechess-program
Board中新增或修改的主要屬性和方法:
(1)、result
對局結果,有4種狀態。
0表示結果未知(正在戰斗中,這有在這一狀態下,程序才響應用戶的點擊事件)
1表示你贏了(也就是電腦輸了)
2表示和棋
3表示你輸了(也就是電腦贏了)
Position中新增或修改的主要屬性和方法:
(1)、vlWhite
紅方所有棋子的價值
(2)、vlBlack
黑方所有棋子的價值
(3)、addPiece(sq, pc, bDel)
此方法增加了一項功能,就是在增減棋子時,更新vlWhite和vlBlack。
(4)、checked()
判斷老將是否被對方攻擊。具有攻擊性的棋子是車、馬、炮、兵。我們要判斷對方的這四類棋子是否攻擊到了己方老將,以及是否將帥對臉。算法如下:
1、假設帥(將)是車,判斷它是否能吃到對方的車和將(帥)。如果能吃到對方的車,說明己方帥(將)被對方車攻擊;如果能吃到將(帥),說明存在將帥對臉。
2、假設帥(將)是炮,判斷它是否能吃到對方的炮。
3、假設帥(將)是馬,判斷它是否能吃到對方的馬,需要注意的是,帥(將)的馬腿用的數組是ADVISOR_DELTA,而不是KING_DELTA。
4、假設帥(將)是過河的兵(卒),判斷它是否能吃到對方的卒(兵)。
(5)、makeMove()
改方法做了一些改進。如果移動棋子后,發現老將被對方攻擊,也就是說這步棋是去送死的,那么就要撤銷對棋子的移動,並返回false。
Search中新增或修改的主要屬性和方法:
(1)、mvResult
這是搜索算法找到的最佳走法,隨后電腦就會執行這步棋。
(2)、maxSearch()
極大點搜索算法
(3)、minSearch()
極小點搜索算法
(4)、maxMinSearch()
極大極小搜索算法