Python開發AI應用-國際象棋應用

      AI 部分總述node

    AI在作出決策前通過三個不一樣的步驟。首先,他找到全部規則容許的棋步(一般在開局時會有20-30種,隨後會下降到幾種)。其次,它生成一個棋步樹用來隨後決定最佳決策。雖然樹的大小隨深度指數增加,可是樹的深度能夠是任意的。假設每次決策有平均20個可選的棋步,那深度爲1對應20棋步,深度爲2對應400棋步,深度爲3對應8000棋步。最後,它遍歷這個樹,採起x步後結果最佳的那個棋步,x是咱們選擇的樹的深度。後面的文章爲了簡單起見,我會假設樹深爲2。python

       生成棋步樹算法

  棋步樹是這個AI的核心。構成這個樹的類是MoveNode.py文件中的MoveNode。他的初始化方法以下:數組

    

def __init__(self, move, children, parent) :
   self.move = move
   self.children = children
   self.parent = parent
   pointAdvantage = None
   depth = 1

  這個類有五個屬性。首先是move,即它包含的棋步,它是個Move類,在這不是很重要,只須要知道它是一個告訴一個起子往哪走的棋步,能夠吃什麼子,等等。而後是children,它也是個MoveNode類。第三個屬性是parent,因此經過它能夠知道上一層有哪些MoveNode。pointAdvantage屬性是AI用來決定這一棋步是好是壞用的。depth屬性指明這一結點在第幾層,也就是說該節點上面有多少節點。生成棋步樹的代碼以下:app

      

def generateMoveTree(self) :
    moveTree = []
    for move in self.board.getAllMovesLegal(self.side) :
        moveTree.append(MoveNode(move, [], None))
 
    for node in moveTree :
        self.board.makeMove(node.move)
        self.populateNodeChildren(node)
        self.board.undoLastMove()
    return moveTree

  變量moveTree一開始是個空list,隨後它裝入MoveNode類的實例。第一個循環後,它只是一個擁有沒有父結點、子結點的MoveNode的數組,也就是一些根節點。第二個循環遍歷moveTree,用populateNodeChildren函數給每一個節點添加子節點:dom

     

def populateNodeChildren(self, node) :
    node.pointAdvantage = self.board.getPointAdvantageOfSide(self.side)
    node.depth = node.getDepth()
    if node.depth == self.depth :
        return
 
    side = self.board.currentSide
 
    legalMoves = self.board.getAllMovesLegal(side)
    if not legalMoves :
        if self.board.isCheckmate() :
            node.move.checkmate = True
            return
        elif self.board.isStalemate() :
            node.move.stalemate = True
            node.pointAdvantage = 0
            return
 
    for move in legalMoves :
        node.children.append(MoveNode(move, [], node))
        self.board.makeMove(move)
        self.populateNodeChildren(node.children[-1])
        self.board.undoLastMove()

  這個函數是遞歸的,而且它有點難用圖像表達出來。一開始給它傳遞了個MoveNode對象。這個MoveNode對象會有爲1的深度,由於它沒有父節點。咱們仍是假設這個AI被設定爲深度爲2。所以率先傳給這個函數的結點會跳過第一個if語句。ide

      而後,決定出全部規則容許的棋步。不過這在這篇文章討論的範圍以外,若是你想看的話代碼都在Github上。下一個if語句檢查是否有符合規則的棋步。若是一個都沒有,要麼被將死了,要麼和棋了。若是是被將死了,因爲沒有其餘能夠走的棋步,把node.move.checkmate屬性設爲True並return。和棋也是類似的,不過因爲哪一方都沒有優點,咱們把node.pointAdvantage設爲0。函數

     若是不是將死或者和棋,那麼legalMoves變量中的全部棋步都被加入當前結點的子節點中做爲MoveNode,而後函數被調用來給這些子節點添加他們本身的MoveNode。優化

當結點的深度等於self.depth(這個例子中是2)時,什麼也不作,當前節點的子節點保留爲空數組。設計

    

       遍歷樹

    假設/咱們有了一個MoveNode的樹,咱們須要遍歷他,找到最佳棋步。這個邏輯有些微妙,須要花一點時間想明白它(在明白這是個很好的算法以前,我應該更多地去用Google)。因此我會盡量充分解釋它。比方說這是咱們的棋步樹:

    若是這個AI很笨,只有深度1,他會選擇拿「象」吃「車」,致使它獲得5分而且總優點爲+7。而後下一步「兵」會吃掉它的「後」,如今優點從+7變爲-2,由於它沒有提早想到下一步。

       

    

    在假設它的深度爲2。將會看到它用「後」吃「馬」致使分數-4,移動「後」致使分數+1,「象」吃「車」致使分數-2。所以,他選擇移動後。這是設計AI時的通用技巧,你能夠在這找到更多資料(極小化極大算法)。

   因此咱們輪到AI時讓它選擇最佳棋步,而且假設AI的對手會選擇對AI來講最不利的棋步。下面展現這一點是如何實現的:

   

def getOptimalPointAdvantageForNode(self, node) :
    if node.children:
        for child in node.children :
            child.pointAdvantage = self.getOptimalPointAdvantageForNode(child)
 
        #If the depth is divisible by 2, it's a move for the AI's side, so return max
        if node.children[0].depth % 2 == 1 :
            return(max(node.children).pointAdvantage)
        else :
            return(min(node.children).pointAdvantage)
    else :
        return node.pointAdvantage

   這也是個遞歸函數,因此一眼很難看出它在幹什麼。有兩種狀況:當前結點有子節點或者沒有子節點。假設棋步樹正好是前面圖中的樣子(實際中每一個樹枝上會有更多結點)。

第一種狀況中,當前節點有子節點。拿第一步舉例,Q吃掉N。它子節點的深度爲2,因此2除2取餘不是1。這意味着子節點包含對手的一步棋,因此返回最小步數(假設對手會走出對AI最不利的棋步)。

      該節點的子節點不會有他們本身的節點,由於咱們假設深度爲2。所以,他們但會他們真實的分值(-4和+5)。他們中最小的是-4,因此第一步,Q吃N,被給爲分值-4。

其餘兩步也重複這個步驟,移動「後」的分數給爲+1,「象」吃「車」的分數給爲-2。

       選擇最佳棋步

    最難的部分已經完成了,如今這個AI要作的事就是從最高分值的棋步中作選擇。

   

def bestMovesWithMoveTree(self, moveTree) :
    bestMoveNodes = []
    for moveNode in moveTree :
        moveNode.pointAdvantage = self.getOptimalPointAdvantageForNode(moveNode)
        if not bestMoveNodes :
            bestMoveNodes.append(moveNode)
        elif moveNode > bestMoveNodes[0] :
            bestMoveNodes = []
            bestMoveNodes.append(moveNode)
        elif moveNode == bestMoveNodes[0] :
            bestMoveNodes.append(moveNode)
 
    return [node.move for node in bestMoveNodes]

  此時有三種狀況。若是變量bestMoveNodes爲空,那麼moveNode的值是多少,都添加到這個list中。若是moveNode的值高於bestMoveNodes的第一個元素,清空這個list而後添加該moveNode。若是moveNode的值是同樣的,那麼添加到list中。

      最後一步是從最佳棋步中隨機選擇一個(AI能被預測是很糟糕的)

   

bestMoves = self.bestMovesWithMoveTree(moveTree)
randomBestMove = random.choice(bestMoves)

  這就是全部的內容。AI生成一個樹,用子節點填充到任意深度,遍歷這個樹找到每一個棋步的分值,而後隨機選擇最好的。這有各類能夠優化的地方,剪枝,剃刀,靜止搜索等等,可是但願這篇文章很好地解釋了基礎的暴力算法的象棋AI是如何工做的。

    注意:部門內容參考於互聯網

相關文章
相關標籤/搜索