編輯:陳萍
國際象棋是一種在棋盤上玩的雙人戰略棋盤遊戲,棋盤格式為 64 格,排列在 8×8 網格中。有人無聊的時候會找電腦下國際象棋,但也有人無聊了會教電腦下棋。
國際象棋可以說是最棒的棋盤遊戲之一,它是戰略戰術和純技術的完美融合。每位玩家開局時各有 16 枚棋子:一王、一後、兩車、兩馬、兩象和八兵,各具不同功能與走法。真人對弈可以憑借玩家的經驗,步步為營。那麼,對于一個機器——計算機,你該如何教會它下棋?近日,有人在 medium 上發表了一篇文章,詳細解釋了如何教計算機玩國際象棋。
本文将從 5 個方面進行介紹:
在開始之前,你隻需要提前安裝 Python3。
Board 表示
首先,你需要對棋子背後的邏輯進行編碼,即為每個棋子分配每一次可能的合法移動。
python-chess 庫為我們提供了棋子的移動生成和驗證,簡化了工作,安裝方式如下:
!pipinstall python-chess
python-chess 庫安裝好後,導入 chess 模塊并進行初始化:
import chess
board = chess.Board()
board
在 notebook 中的輸出如下所示:
board 對象是一個完整的 board 表示,該對象為我們提供了一些重要的函數,例如,board.is_checkmate() 函數檢查是否存在将殺(checkmate),board.push() 函數附加一個移動,board.pop() 函數撤銷最後一次移動等。閱讀完整的文檔請參閱:https://python-chess.readthedocs.io/en/latest/
Board 評估
為了對 board 進行初步評估,必須考慮一位大師在各自比賽中的想法。
我們應該想到的一些要點是:
将上述要點以方程形式進行表達:
通過化簡上述方程,可以得到:象 > 馬 > 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 -9999
else:
return 9999
if board.is_stalemate():
return 0
if board.is_insufficient_material():
return 0
第二步,計算總的棋子數,并把棋子總數傳遞給 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)]
for i 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)]
for i 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)]
for i 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)]
for i 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)]
for i 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)]
for i in board.pieces(chess.KING, chess.BLACK)])
第四步,計算評價函數,此時将會返回白棋的 material 得分和各棋子單獨得分之和。
eval = material pawnsq knightsq bishopsq rooksq queensq kingsq
if board.turn:
return eval
else:
return -eval
評價函數流程圖
移動選擇
算法的最後一步是用 Minimax 算法中的 Negamax 實現進行移動選擇,Minimax 算法是雙人遊戲(如跳棋等)中的常用算法。之後使用 Alpha-Beta 剪枝進行優化,這樣可以減少執行的時間。
現在讓我們深入研究一下 minimax 算法。該算法被廣泛應用在棋類遊戲中,用來找出失敗的最大可能性中的最小值。該算法廣泛應用于人工智能、決策論、博弈論、統計和哲學,力圖在最壞的情況下将損失降到最低。簡單來說,在遊戲的每一步,假設玩家 A 試圖最大化獲勝幾率,而在下一步中,玩家 B 試圖最小化玩家 A 獲勝的幾率。
為了更好地理解 minimax 算法,請看下圖:
維基百科中 minimax 樹舉例
為了得到更好的結果,使用 minimax 變體 negamax,因為我們隻需要一個最大化兩位玩家效用的函數。不同點在于,一個玩家的損失等于另一個玩家的收獲,反之亦然。
就遊戲而言,給第一個玩家的位置值和給第二個玩家的位置值符号是相反的。
negamax 示例
首先,我們将 alpha 設為負無窮大,beta 設為正無窮大,這樣兩位玩家都能以盡可能差的分數開始比賽,代碼如下:
except:
bestMove = chess.Move.null()
bestValue = -99999
alpha = -100000
beta = 100000
for move in board.legal_moves:
board.push(move)
boardValue = -alphabeta(-beta, -alpha, depth - 1)
if boardValue > bestValue:
bestValue = boardValue
bestMove = move
if (boardValue > alpha):
alpha = boardValue
board.pop()
return bestMove
下面讓我們以流程圖的方式來解釋:
search 函數的流程圖
下一步是進行 alpha-beta 的剪枝來優化執行速度。
來自維基百科的 alpha-beta 剪枝說明
代碼如下:
def alphabeta(alpha, beta, depthleft): bestscore = -9999 if (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): return score if (score > bestscore): bestscore = score if (score > alpha): alpha = score return bestscore
現在,讓我們用下面給出的流程圖來調整 alphabeta 函數:
現在是靜态搜索,這種搜索旨在僅評估靜态位置,即不存在緻勝戰術移動的位置。該搜索需要避免由搜索算法的深度限制所引起的水平線效應(horizon effect)。
代碼如下:
def quiesce(alpha, beta):
stand_pat = evaluate_board()
if (stand_pat >= beta):
return beta
if (alpha < stand_pat):
alpha = stand_pat
for move in board.legal_moves:
if board.is_capture(move):
board.push(move)
score = -quiesce(-beta, -alpha)
board.pop()
if (score >= beta):
return beta
if (score > alpha):
alpha = score
return alpha
簡單總結一下 quiesce 函數:
quiesce 函數流程圖。
測試 AI
開始測試前,需要導入一些庫:
測試有 3 項:
1. AI 對弈人類:
AI 選擇從 g1 到 f3,這是一個很明智的選擇。
2. AI 對弈 AI:
3. AI 對弈 Stockfish:
可以得出:AI 還不夠智能,不足以打敗 stockfish 12,但仍然堅持走了 20 步。
接口測試
上述測試方式看起來代碼很多,你也可以寫一個接口測試 AI。
然後執行:
最終輸出
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!