編輯:陳萍
西洋棋是一種在棋盤上玩的雙人戰略棋盤遊戲,棋盤格式為 64 格,排列在 8×8 網格中。有人無聊的時候會找電腦下西洋棋,但也有人無聊了會教電腦下棋。
西洋棋可以說是最棒的棋盤遊戲之一,它是戰略戰術和純技術的完美融合。每位玩家開局時各有 16 枚棋子:一王、一後、兩車、兩馬、兩象和八兵,各具不同功能與走法。真人對弈可以憑藉玩家的經驗,步步為營。那麼,對於一個機器——計算機,你該如何教會它下棋?近日,有人在 medium 上發表了一篇文章,詳細解釋了如何教計算機玩西洋棋。
本文將從 5 個方面進行介紹:
Board 表示;Board 評估;移動選擇;測試 AI;接口測試。在開始之前,你只需要提前安裝 Python3。
Board 表示
首先,你需要對棋子背後的邏輯進行編碼,即為每個棋子分配每一次可能的合法移動。
python-chess 庫為我們提供了棋子的移動生成和驗證,簡化了工作,安裝方式如下:
!pip install python-chess
python-chess 庫安裝好後,導入 chess 模塊並進行初始化:
importchessboard = chess.Board()board
在 notebook 中的輸出如下所示:
board 對象是一個完整的 board 表示,該對象為我們提供了一些重要的函數,例如,board.is_checkmate() 函數檢查是否存在將殺(checkmate),board.push() 函數附加一個移動,board.pop() 函數撤銷最後一次移動等。閱讀完整的文檔請參閱:https://python-chess.readthedocs.io/en/latest/
Board 評估
為了對 board 進行初步評估,必須考慮一位大師在各自比賽中的想法。
我們應該想到的一些要點是:
避免用一個小棋子換三個兵;象總是成對出現;避免用兩個小棋子換一輛車和一個兵。將上述要點以方程形式進行表達:
象 > 3 個兵 & 馬 > 3 個兵;象 > 馬;象 + 馬 > 車 + 兵。通過化簡上述方程,可以得到:象 > 馬 > 3 個兵。同樣,第三個方程可以改寫成:象 + 馬 = 車 + 1.5 個兵,因為兩個小棋子相當於一個車和兩個兵。
使用 piece square table 來評估棋子,在 8x8 的矩陣中設置值,例如在西洋棋中,在有利的位置設置較高的值,在不利的位置設置較低的值。
例如,白色國王越過中線的概率將小於 20%,因此我們將在該矩陣中將數值設置為負值。
再舉一個例子,假設皇后希望自己被放在中間位置,因為這樣可以控制更多的位置,因此我們將在中心設置更高的值,其他棋子也一樣,因為西洋棋都是為了保衛國王和控制中心。
理論就講這些,現在我們來初始化 piece square table:
pawntable=[0,0,0,0,0,0,0,0,5,10,10,-20,-20,10,10,5,5,-5,-10,0,0,-10,-5,5,0,0,0,20,20,0,0,0,5,5,10,25,25,10,5,5,10,10,20,30,30,20,10,10,50,50,50,50,50,50,50,50,0,0,0,0,0,0,0,0]knightstable=[-50,-40,-30,-30,-30,-30,-40,-50,-40,-20,0,5,5,0,-20,-40,-30,5,10,15,15,10,5,-30,-30,0,15,20,20,15,0,-30,-30,5,15,20,20,15,5,-30,-30,0,10,15,15,10,0,-30,-40,-20,0,0,0,0,-20,-40,-50,-40,-30,-30,-30,-30,-40,-50]bishopstable=[-20,-10,-10,-10,-10,-10,-10,-20,-10,5,0,0,0,0,5,-10,-10,10,10,10,10,10,10,-10,-10,0,10,10,10,10,0,-10,-10,5,5,10,10,5,5,-10,-10,0,5,10,10,5,0,-10,-10,0,0,0,0,0,0,-10,-20,-10,-10,-10,-10,-10,-10,-20]rookstable=[0,0,0,5,5,0,0,0,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,5,10,10,10,10,10,10,5,0,0,0,0,0,0,0,0]queenstable=[-20,-10,-10,-5,-5,-10,-10,-20,-10,0,0,0,0,0,0,-10,-10,5,5,5,5,5,0,-10,0,0,5,5,5,5,0,-5,-5,0,5,5,5,5,0,-5,-10,0,5,5,5,5,0,-10,-10,0,0,0,0,0,0,-10,-20,-10,-10,-5,-5,-10,-10,-20]kingstable=[20,30,10,0,0,10,30,20,20,20,0,0,0,0,20,20,-10,-20,-20,-20,-20,-20,-20,-10,-20,-30,-30,-40,-40,-30,-30,-20,-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30]
通過以下四種方法得到評估函數:
第一步檢查遊戲是否還在繼續。
這個階段的背後編碼邏輯是:如果它在 checkmate 時返回 true,程序將會檢查輪到哪方移動。如果當前輪到白方移動,返回值為 - 9999,即上次一定是黑方移動,黑色獲勝;否則返回值為 + 9999,表示白色獲勝。對於僵局或比賽材料不足,返回值為 0 以表示平局。
代碼實現方式:
if board.is_checkmate(): if board.turn: return -9999else: return9999if board.is_stalemate(): return0if board.is_insufficient_material(): return0
第二步,計算總的棋子數,並把棋子總數傳遞給 material 函數。
wp = len(board.pieces(chess.PAWN, chess.WHITE))bp = len(board.pieces(chess.PAWN, chess.BLACK))wn = len(board.pieces(chess.KNIGHT, chess.WHITE))bn = len(board.pieces(chess.KNIGHT, chess.BLACK))wb = len(board.pieces(chess.BISHOP, chess.WHITE))bb = len(board.pieces(chess.BISHOP, chess.BLACK))wr = len(board.pieces(chess.ROOK, chess.WHITE))br = len(board.pieces(chess.ROOK, chess.BLACK))wq = len(board.pieces(chess.QUEEN, chess.WHITE))bq = len(board.pieces(chess.QUEEN, chess.BLACK))
第三步,計算得分。material 函數得分的計算方法是:用各種棋子的權重乘以該棋子黑白兩方個數之差,然後求這些結果之和。而每種棋子的得分計算方法是:該棋子在該遊戲實例中所處位置的 piece-square 值的總和。
material = 100 * (wp - bp) + 320 * (wn - bn) + 330 * (wb - bb) + 500 * (wr - br) + 900 * (wq - bq)pawnsq = sum([pawntable[i] for i in board.pieces(chess.PAWN, chess.WHITE)])pawnsq = pawnsq + sum([-pawntable[chess.square_mirror(i)]fori in board.pieces(chess.PAWN, chess.BLACK)])knightsq = sum([knightstable[i] for i in board.pieces(chess.KNIGHT, chess.WHITE)])knightsq = knightsq + sum([-knightstable[chess.square_mirror(i)]fori in board.pieces(chess.KNIGHT, chess.BLACK)])bishopsq = sum([bishopstable[i] for i in board.pieces(chess.BISHOP, chess.WHITE)])bishopsq = bishopsq + sum([-bishopstable[chess.square_mirror(i)]fori in board.pieces(chess.BISHOP, chess.BLACK)])rooksq = sum([rookstable[i] for i in board.pieces(chess.ROOK, chess.WHITE)])rooksq = rooksq + sum([-rookstable[chess.square_mirror(i)]fori in board.pieces(chess.ROOK, chess.BLACK)])queensq = sum([queenstable[i] for i in board.pieces(chess.QUEEN, chess.WHITE)])queensq = queensq + sum([-queenstable[chess.square_mirror(i)]fori in board.pieces(chess.QUEEN, chess.BLACK)])kingsq = sum([kingstable[i] for i in board.pieces(chess.KING, chess.WHITE)])kingsq = kingsq + sum([-kingstable[chess.square_mirror(i)]fori in board.pieces(chess.KING, chess.BLACK)])
第四步,計算評價函數,此時將會返回白棋的 material 得分和各棋子單獨得分之和。
eval = material + pawnsq + knightsq + bishopsq + rooksq + queensq + kingsqif board.turn: returnevalelse: return -eval
評價函數流程圖
移動選擇
算法的最後一步是用 Minimax 算法中的 Negamax 實現進行移動選擇,Minimax 算法是雙人遊戲(如跳棋等)中的常用算法。之後使用 Alpha-Beta 剪枝進行優化,這樣可以減少執行的時間。
現在讓我們深入研究一下 minimax 算法。該算法被廣泛應用在棋類遊戲中,用來找出失敗的最大可能性中的最小值。該算法廣泛應用於人工智慧、決策論、博弈論、統計和哲學,力圖在最壞的情況下將損失降到最低。簡單來說,在遊戲的每一步,假設玩家 A 試圖最大化獲勝機率,而在下一步中,玩家 B 試圖最小化玩家 A 獲勝的機率。
為了更好地理解 minimax 算法,請看下圖:
維基百科中 minimax 樹舉例
為了得到更好的結果,使用 minimax 變體 negamax,因為我們只需要一個最大化兩位玩家效用的函數。不同點在於,一個玩家的損失等於另一個玩家的收穫,反之亦然。
就遊戲而言,給第一個玩家的位置值和給第二個玩家的位置值符號是相反的。
negamax 示例
首先,我們將 alpha 設為負無窮大,beta 設為正無窮大,這樣兩位玩家都能以儘可能差的分數開始比賽,代碼如下:
except:bestMove = chess.Move.null()bestValue = -99999alpha = -100000beta = 100000formove in board.legal_moves:board.push(move)boardValue = -alphabeta(-beta, -alpha, depth - 1)ifboardValue > bestValue:bestValue = boardValuebestMove = moveif(boardValue > alpha):alpha = boardValueboard.pop()returnbestMove
下面讓我們以流程圖的方式來解釋:
search 函數的流程圖
下一步是進行 alpha-beta 的剪枝來優化執行速度。
來自維基百科的 alpha-beta 剪枝說明
代碼如下:
def alphabeta(alpha, beta, depthleft): bestscore = -9999if (depthleft == 0): return quiesce(alpha, beta) for move in board.legal_moves: board.push(move) score = -alphabeta(-beta, -alpha, depthleft - 1) board.pop() if (score >= beta): returnscore if (score > bestscore): bestscore = score if (score > alpha): alpha = score return bestscore
現在,讓我們用下面給出的流程圖來調整 alphabeta 函數:
現在是靜態搜索,這種搜索旨在僅評估靜態位置,即不存在致勝戰術移動的位置。該搜索需要避免由搜索算法的深度限制所引起的水平線效應(horizon effect)。
代碼如下:
defquiesce(alpha, beta):stand_pat = evaluate_board()if(stand_pat >= beta):returnbetaif(alpha < stand_pat):alpha = stand_patformove in board.legal_moves:ifboard.is_capture(move):board.push(move)score = -quiesce(-beta, -alpha)board.pop()if(score >= beta):returnbetaif(score > alpha):alpha = scorereturnalpha
簡單總結一下 quiesce 函數:
quiesce 函數流程圖。
測試 AI
開始測試前,需要導入一些庫:
測試有 3 項:
AI 對弈人類;AI 對弈 AI;AI 對弈 Stockfish。1. AI 對弈人類:
AI 選擇從 g1 到 f3,這是一個很明智的選擇。
2. AI 對弈 AI:
3. AI 對弈 Stockfish:
可以得出:AI 還不夠智能,不足以打敗 stockfish 12,但仍然堅持走了 20 步。
接口測試
上述測試方式看起來代碼很多,你也可以寫一個接口測試 AI。
然後執行:
最終輸出