對於棋類軟件的搜索算法經前人的努力已造成了較爲成熟的Alpha-Beta搜索算法以及其它一些輔助加強算法。因此小生在本身的程序中直接借鑑了Alpha-Beta搜索算法並輔以了歷史啓發。對此二者王小春的《PC 遊戲編程(人機博弈)》和ElephantBoard的主頁 http://www.elephantbase.net/上都有很是詳細的介紹,因此這裏我只簡單介紹一下算法的主體思想:算法
首先對於棋類遊戲存在一棵「博弈樹」——樹的每個結點表明棋盤上的一個局面,對每個局面(結點)根據不一樣的走法又產生不一樣的局面(生出新的結點),如此不斷直到再無可選擇的走法(棋局結束)。編程
如今假定甲乙兩方下棋,甲勝的局面是一個極大值(一個正數),那麼乙勝的局面則是一個極小值(極大值的負值),和棋的局面則是零值或是接近零的值。如此,則輪到甲走棋時他會選擇生成結點的值最大的走法,相反輪到乙走棋時他會選擇生成結點的值最小的走法(固然,搜索會向下推導多步後選擇一個分值對當前下棋方有利的着法)。這就是「最大-最小」的基本思想。這樣搜索函數能夠經過搜索以當前局面爲根結點、限定層數之內的整棵樹來得到一個最佳的着法。不幸的是,博弈樹至關龐大(它會成指數增加),於是搜索整棵樹是一件至關費時的工做。數據結構
然而,下棋是一個你來我往的交替進行而且相互「較勁」的過程,因爲每一方都會盡量將局面導向對本身有利而對對方不利的形勢。因此有些「暫時」看來很不錯的局面因爲可能會產生很糟糕的局面於是根本沒有考慮的價值。因此當你看到某個局面有可能產生很糟糕的局面時(確切地說這裏的「很糟糕」是與以前所分析的狀況相比較而言的),你應當馬上中止對其剩餘子結點的分析——不要對它再報任何幻想了,若是你選擇了它,則你必將獲得那個很糟糕的局面,甚至更糟……這樣一來即可以很大程度上減小搜索的工做量,提升搜索效率。這稱爲「樹的剪裁」。爲了便於你們理解,下面我援引ElephantBoard的主頁http://www.elephantbase.net/上所翻譯的《Alpha-Beta搜索》中的一個「口袋的例子」,原文做者是Bruce Moreland (brucemo@seanet.com)。
口袋的例子:函數
好比你的死敵面前有不少口袋,他和你打賭賭輸了,所以他必須從中給你同樣東西,而挑選規則卻很是奇怪:
每一個口袋裏有幾件物品,你能取其中的一件,你來挑這件物品所在的口袋,而他來挑這個口袋裏的物品。你要趕忙挑出口袋並離開,由於你不肯意一直作在那裏翻口袋而讓你的死敵盯着你。
假設你一次只能找一隻口袋,在找口袋時一次只能從裏面摸出同樣東西。
很顯然,當你挑出口袋時,你的死敵會把口袋裏最糟糕的物品給你,所以你的目標是挑出「諸多最糟的物品當中是最好的」那個口袋。
你很容易把最小-最大原理運用到這個問題上。你是最大一方棋手,你將挑出最好的口袋。而你的死敵是最小一方棋手,他將挑出最好的口袋裏儘量差的物品。運用最小-最大原理,你須要作的就是挑一個有「最好的最差的」物品的口袋。
假設你能夠估計口袋裏每一個物品的準確價值的話,最小-最大原理可讓你做出正確的選擇。咱們討論的話題中,準確評價並不重要,由於它同最小-最大或Alpha-Beta的工做原理沒有關係。如今咱們假設你能夠正確地評價物品。
最小-最大原理剛纔討論過,它的問題是效率過低。你必須看每一個口袋裏的每件物品,這就須要花不少時間。
那麼怎樣才能作得比最小-最大更高效呢?
咱們從第一個口袋開始,看每一件物品,並對口袋做出評價。比方說口袋裏有一隻花生黃油三明治和一輛新汽車的鑰匙。你知道三明治更糟,所以若是你挑了這隻口袋就會獲得三明治。事實上只要咱們假設對手也會跟咱們同樣正確評價物品,那麼口袋裏的汽車鑰匙就是可有可無的了。
如今你開始翻第二個口袋,此次你採起的方案就和最小-最大方案不一樣了。你每次看一件物品,並跟你能獲得的最好的那件物品(三明治)去比較。只要物品比三明治更好,那麼你就按照最小-最大方案來辦——去找最糟的,或許最糟的要比三明治更好,那麼你就能夠挑這個口袋,它比裝有三明治的那個口袋好。
比方這個口袋裏的第一件物品是一張20美圓的鈔票,它比三明治好。若是包裏其餘東西都沒比這個更糟了,那麼若是你選了這個口袋,它就是對手必須給你的物品,這個口袋就成了你的選擇。
這個口袋裏的下一件物品是六合裝的流行唱片。你認爲它比三明治好,但比20美圓差,那麼這個口袋仍舊能夠選擇。再下一件物品是一條爛魚,這回比三明治差了。因而你就說「不謝了」,把口袋放回去,再也不考慮它了。
不管口袋裏還有什麼東西,或許還有另外一輛汽車的鑰匙,也沒有用了,由於你會獲得那條爛魚。或許還有比爛魚更糟的東西(那麼你看着辦吧)。不管如何爛魚已經夠糟的了,而你知道挑那個有三明治的口袋確定會更好。post
「最大-最小」的思想再加上「對樹的剪裁」就是Alpha-Beta搜索算法的核心。
下面就是CChessSearch.h的代碼實現,其中涉及的「歷史啓發」和「着法排序」的具體實現會在下篇文章中給出。lua
// CChessSearch.h #include "HistoryHeuristic.h" #include "SortMove.h" #include "CChessEvaluate.h" /////////////////// Data Define /////////////////////////////////////////////// int nMaxSearchDepth; // 最大搜索深度 CCHESSMOVE cmBestMove; // 存儲最佳走法 /////////////////// Function Prototype //////////////////////////////////////// // 經過AlphaBeta搜索+歷史啓發搜索獲得一部最佳着法並執行之 CCHESSMOVE SearchAGoodMove(); // AlphaBeta搜索+歷史啓發,nDepth爲搜索深度, // alpha初始爲極小值,beta初始爲極大值 int AlphaBeta_HH( int nDepth, int alpha, int beta ); // 判斷遊戲是否結束,若結束則根據當前下棋方返回相應的極值,不然返回0 // 當前下棋方勝則返回極大值,當前下棋方敗則返回極小值(下棋方追求極大值) int IsGameOver( int fWhoseTurn ); // 執行着法,返回ptTo位置的棋子情況。即若吃掉子返回被吃掉的子,沒有吃子則返回0 BYTE DoMove( CCHESSMOVE * move ); // 撤銷執行着法,恢復原位置的棋子情況。nCChessID爲原ptTo位置的棋子情況 void UndoMove( CCHESSMOVE * move, BYTE nCChessID ); ////////////////// Programmer-Defined Function //////////////////////////////// CCHESSMOVE SearchAGoodMove() { ResetHistoryTable(); int i; i = AlphaBeta_HH( nMaxSearchDepth, -MaxValue, MaxValue ); DoMove( &cmBestMove ); return cmBestMove; } int AlphaBeta_HH( int nDepth, int alpha, int beta ) { int nScore; int nCount; BYTE nCChessID; int i; i = IsGameOver( ( nMaxSearchDepth - nDepth ) % 2 ); if( i != 0 ) // 遊戲結束 return i; if( nDepth == 0 ) // 葉子節點 return Eveluate( ( nMaxSearchDepth - nDepth ) % 2 ); nCount = GenerateMove( ( nMaxSearchDepth - nDepth ) % 2 , nDepth ); for( i = 0; i < nCount; i ++ ) // 取歷史表得分 { MoveList[nDepth][i].nScore = GetHistoryScore( & MoveList[nDepth][i] ); } MergeSort( MoveList[nDepth], nCount ); // 對着法進行降序排序 int iBestmove = -1; for( i = 0; i < nCount; i ++ ) { nCChessID = DoMove( & MoveList[nDepth][i] ); // 執行着法(生成新節點) nScore = - AlphaBeta_HH( nDepth - 1, -beta, -alpha );//遞歸調用AlphaBeta_HH UndoMove( & MoveList[nDepth][i], nCChessID ); // 撤銷執行(刪除節點) if( nScore > alpha ) { alpha = nScore; // 保留最大值 if( nDepth == nMaxSearchDepth ) cmBestMove = MoveList[nDepth][i]; iBestmove = i; // 保存最佳走法的序號 } if( alpha >= beta ) { iBestmove = i; // 保存最佳走法的序號 break; } } if( iBestmove != -1 ) EnterHistoryScore( & MoveList[nDepth][iBestmove], nDepth ); //記錄歷史得分 return alpha; } int IsGameOver( int fWhoseTurn ) { int x, y ; if( fWhoseTurn == RED ) // 輪到紅方下棋,只多是紅帥已經被吃 { // 紅方九宮 for( x = 3; x <= 5; x ++ ) for( y = 0; y <= 2; y ++ ) { if( CChessBoard[x][y] == RED_K ) { return 0; // 紅帥沒被吃,則說明遊戲還沒有結束 } } return -MaxValue ; // 返回失敗極值(已驗證應爲 -MaxValue ) } else // 輪到黑方下棋,只多是黑將已經被吃 { // 黑方九宮 for( x = 3; x <= 5; x ++ ) for( y = 9; y >= 7; y -- ) { if( CChessBoard[x][y] == BLACK_K ) { return 0; // 黑將沒被吃,則說明遊戲還沒有結束 } } return -MaxValue ; // 返回失敗極值(已驗證應爲 -MaxValue ) } } BYTE DoMove( CCHESSMOVE * move ) { BYTE nCChessID; //保留目標位置的棋子情況 nCChessID = CChessBoard[ move->ptTo.x ][ move->ptTo.y ] ; //移動子到目標位置 CChessBoard[ move->ptTo.x ][ move->ptTo.y ] = CChessBoard[ move->ptFrom.x ][ move->ptFrom.y ] ; CChessBoard[ move->ptFrom.x ][ move->ptFrom.y ] = 0 ; return nCChessID; } void UndoMove( CCHESSMOVE * move, BYTE nCChessID ) { //將子移動回原處 CChessBoard[ move->ptFrom.x ][ move->ptFrom.y ] = CChessBoard[ move->ptTo.x ][ move->ptTo.y ] ; //恢復目標位置的子 CChessBoard[ move->ptTo.x ][ move->ptTo.y ] = nCChessID ; } // end of CChessSearch.h