繼去年3月人機大戰引起全球矚目以來,圍棋AI(人工智能)再度引起跨領域的關注:一個叫Master的圍棋AI,幾天時間,面對中日韓頂尖職業圍棋選手,已取得60勝0敗的恐怖戰績,展示出的圍棋技藝已經到了人類理解不了的程度。這能夠視爲人工智能在圍棋領域的一次「大征服」,而在此以外的意義則是,告訴了咱們人工智能在征服一項領域或職業時,究竟速度有多快。理解這一點,對於人類,乃至每個人,都很是重要。經過本實驗的學習,能夠對人機對戰有初步瞭解。html
實驗利用Python模擬AI和玩家進行四子棋遊戲,利用遊戲實驗Pygame庫,爲遊戲提供界面和操做支持。AI算法借用蒙特卡洛搜索樹思想。經過設置AI的難度係數,即AI所能考慮到的將來棋子的可能走向,從而選擇出最佳的方案和玩家對抗。難度係數越大,AI搜索範圍越廣,它所能作出的決定越明智。python
遊戲最終效果截圖:算法
本課程難度通常,屬於初級課程,適合具備Python基礎並對Pygame有所瞭解的用戶學習。bash
你能夠經過下面命令將代碼下載到實驗樓環境中,做爲參照對比進行學習。app
$ wget http://labfile.oss.aliyuncs.com/courses/746/fourinrow.py
四子棋遊戲是在7*6的格子中。輪流從格子最上方落下棋子。棋子會落在該列格子中最下面的空格子裏。先將四個棋子連成一條線(水平直線,豎直直線,或傾斜直線)者獲勝,遊戲結束。dom
在Code目錄下進行建立工程文件Fourinrow,在終端執行命令ide
cd Code && mkdir Fourinrow
下載本次實驗所需的圖片資源到Fourinrow文件下函數
$ cd Fourinrow $ wget http://labfile.oss.aliyuncs.com/courses/746/images.zip $ unzip images.zip
安裝依賴包post
$ sudo apt-get update $ sudo apt-get install python-pygame
用到的變量包括,棋盤的寬度,長度(能夠修改,設計不一樣規格的棋盤),難度係數,棋子大小以及一些設計座標變量的設定。學習
在FourinRow.py
文件中輸入以下代碼:
import random, copy, sys, pygame from pygame.locals import * BOARDWIDTH = 7 # 棋子盤的寬度欄數 BOARDHEIGHT = 6 # 棋子盤的高度欄數 assert BOARDWIDTH >= 4 and BOARDHEIGHT >= 4, 'Board must be at least 4x4.' #python assert斷言是聲明其布爾值必須爲真的斷定,若是發生異常就說明表達示爲假。 #能夠理解assert斷言語句爲raise-if-not,用來測試表示式,其返回值爲假,就會觸發異常。 DIFFICULTY = 2 # 難度係數,計算機可以考慮的移動級別 #這裏2表示,考慮對手走棋的7種可能性及如何應對對手的7種走法 SPACESIZE = 50 # 棋子的大小 FPS = 30 # 屏幕的更新頻率,即30/s WINDOWWIDTH = 640 # 遊戲屏幕的寬度像素 WINDOWHEIGHT = 480 # 遊戲屏幕的高度像素 XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * SPACESIZE) / 2)#X邊緣座標量,即格子欄的最左邊 YMARGIN = int((WINDOWHEIGHT - BOARDHEIGHT * SPACESIZE) / 2)#Y邊緣座標量,即格子欄的最上邊 BRIGHTBLUE = (0, 50, 255)#藍色 WHITE = (255, 255, 255)#白色 BGCOLOR = BRIGHTBLUE TEXTCOLOR = WHITE RED = 'red' BLACK = 'black' EMPTY = None HUMAN = 'human' COMPUTER = 'computer'
除此以外咱們還須要定義一些pygame的全局變量。這些全局變量在以後的各個模塊中會被屢次調用。其中不少是存儲載入圖片的變量,準備工做有點長,請你們耐心一點哦。
#初始化pygame的各個模塊 pygame.init() #初始化了一個Clock對象 FPSCLOCK = pygame.time.Clock() #建立遊戲窗口 DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) #遊戲窗口標題 pygame.display.set_caption(u'four in row') #Rect(left,top,width,height)用來定義位置和寬高 REDPILERECT = pygame.Rect(int(SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE) #這裏建立的是窗口中左下角和右下角的棋子 BLACKPILERECT = pygame.Rect(WINDOWWIDTH - int(3 * SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE) #載入紅色棋子圖片 REDTOKENIMG = pygame.image.load('4row_red.png') #將紅色棋子圖片縮放爲SPACESIZE REDTOKENIMG = pygame.transform.smoothscale(REDTOKENIMG, (SPACESIZE, SPACESIZE)) #載入黑色棋子圖片 BLACKTOKENIMG = pygame.image.load('4row_black.png') #將黑色棋子圖片縮放爲SPACESIZE BLACKTOKENIMG = pygame.transform.smoothscale(BLACKTOKENIMG, (SPACESIZE, SPACESIZE)) #載入棋子面板圖片 BOARDIMG = pygame.image.load('4row_board.png') #將棋子面板圖片縮放爲SPACESIZE BOARDIMG = pygame.transform.smoothscale(BOARDIMG, (SPACESIZE, SPACESIZE)) #載入人勝利時圖片 HUMANWINNERIMG = pygame.image.load('4row_humanwinner.png') #載入AI勝時圖片 COMPUTERWINNERIMG = pygame.image.load('4row_computerwinner.png') #載入平局提示圖片 TIEWINNERIMG = pygame.image.load('4row_tie.png') #返回一個Rect實例 WINNERRECT = HUMANWINNERIMG.get_rect() #遊戲窗口中間位置座標 WINNERRECT.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2)) #載入操做提示圖片 ARROWIMG = pygame.image.load('4row_arrow.png') #返回一個Rect實例 ARROWRECT = ARROWIMG.get_rect() #操做提示的左位置 ARROWRECT.left = REDPILERECT.right + 10 #將操做提示與下方紅色棋子實例在縱向對齊 ARROWRECT.centery = REDPILERECT.centery
至此咱們完成了前期的準備工做。
初始時,將棋盤二維列表清空,而後根據玩家和AI的走法將棋盤相應位置設定顏色。
def drawBoard(board, extraToken=None): #DISPLAYSURF 是咱們的界面,在初始化變量模塊中有定義 DISPLAYSURF.fill(BGCOLOR)#將遊戲窗口背景色填充爲藍色 spaceRect = pygame.Rect(0, 0, SPACESIZE, SPACESIZE)#建立Rect實例 for x in range(BOARDWIDTH): #肯定每一列中每一行中的格子的左上角的位置座標 for y in range(BOARDHEIGHT): spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE)) #x =0,y =0時,即第一列第一行的格子。 if board[x][y] == RED:#若是格子值爲紅色 #則在在遊戲窗口的spaceRect中畫紅色棋子 DISPLAYSURF.blit(REDTOKENIMG, spaceRect) elif board[x][y] == BLACK: #不然畫黑色棋子 DISPLAYSURF.blit(BLACKTOKENIMG, spaceRect) # extraToken 是包含了位置信息和顏色信息的變量 # 用來顯示指定的棋子 if extraToken != None: if extraToken['color'] == RED: DISPLAYSURF.blit(REDTOKENIMG,(extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE)) elif extraToken['color'] == BLACK: DISPLAYSURF.blit(BLACKTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE)) # 畫棋子面板 for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE)) DISPLAYSURF.blit(BOARDIMG, spaceRect) # 畫遊戲窗口中左下角和右下角的棋子 DISPLAYSURF.blit(REDTOKENIMG, REDPILERECT) # 左邊的紅色棋子 DISPLAYSURF.blit(BLACKTOKENIMG, BLACKPILERECT) # 右邊的黑色棋子 def getNewBoard(): board = [] for x in range(BOARDWIDTH): board.append([EMPTY] * BOARDHEIGHT) return board #返回board列表,其值爲BOARDHEIGHT數量的None
簡單介紹一下蒙特卡洛搜索樹的思想:
利用一維中的擲點法完成對圍棋盤面的評估。具體來說,當咱們給定某一個棋盤局面時,程序在當前局面的全部可下點中隨機選擇一個點擺上棋子,並不斷重複這個隨機選擇可下點(擲點)的過程,直到雙方都沒有可下點(即對弈結束),再把這個最終狀態的勝負結果反饋回去,做爲評估當前局面的依據。
在該實驗當中AI經過不斷選擇不一樣的欄,而後考慮雙方的獲勝結果進行評估,AI最終會選擇評估較高的策略。
在瀏覽下面圖片和文字以前請先看一下後面的代碼,而後在對應講解內容。
觀察下面圖示中AI和player的對決
實驗中有些變量能夠直觀反映了AI棋子操做的過程:
PotentialMoves:返回一個列表,表示AI將棋子移動到列表中任一欄時獲勝的可能性大小,其數值爲-7~0的隨機數,數值爲負數時表示AI將棋子移動到這一欄時,玩家可能會在接下來兩步取勝,數值越小表示玩家獲勝可能性越大。爲0,表示玩家不會獲勝,而且AI也不可能獲勝,爲1表示AI能夠獲勝。
bestMoveFitness:適應度是選取PotentialMoves中最大的數值
bestMoves:若是PotentialMoves中有多個最大值,則表示AI將棋子移動到這些值所在的欄時,玩家獲勝的概率都是最小的。因此將這些欄從新添加到列表bestMoves中。
column:當bestMoves爲多個值時隨機選擇bestMoves中的一欄做爲AI的移動。如果惟一值,則column爲這個惟一值。
實驗中經過打印這些 bestMoveFitness ,bestMoves , column ,potentialMoves得出在上圖中AI的每一步參數:
steps | potentialMoves | bestMoveFitness | bestMoves | column |
---|---|---|---|---|
1 | [0, 0, 0, 0, 0, 0, 0] | 0 | [0, 1, 2, 3, 4, 5, 6] | 0 |
2 | [0, 0, 0, 0, 0, 0, 0] | 0 | [0, 1, 2, 3, 4, 5, 6] | 6 |
3 | [-1, -1, -1, 0, -1, -1, -1] | 0 | [3] | 3 |
4 | [-3, -2, 0, -3, -3, -2, -3] | 0 | [2] | 2 |
經過第三步AI的選擇,更加細緻地瞭解算法的原理:
下圖是部分AI走法示意圖,該圖顯示了若是AI將棋子落在第一格中,Player的可能選擇,以及AI接下來的一步對player獲勝產生的影響,正是經過這種搜索,迭代AI能夠斷定在接下來兩步中對手和本身的獲勝狀況,從而作出抉擇。
下圖是計算AI適應度值的流程圖,實驗中難度係數爲2,需考慮7 ^ 4=2041次:
經過以上流程圖,不難發現。AI的第一步棋子,若爲0,1,2,4,5,6。則Player總有可能將剩下的兩個棋子所有放在3,從而獲勝。以AI=0爲例,若player =0,即紅色的第1枚棋子不在3,第2枚紅色棋子不論在哪,都不可能獲勝,爲方便表述,用序列表示各類組合,序列第一個表是AI第一步,第二個數表示Player的迴應,第三個數表示AI的迴應。X表示任意有效移動。 因此[0,0,x]=0,推理得,當序列爲[0,x<>3,x],player都不能獲勝。只有當player的第2枚棋子爲3時,AI的第二枚在不爲3的狀況下都能獲勝,因此[0,x=3,x<>3] =-1。共有6種狀況。最終的結果爲(0+0+…(43個)-1*6)/7 =-1 同理對於其餘的四種,結果都爲-1。當AI第一步就在3的話,Player就不可能獲勝,而且AI也不能獲勝,因此爲0。AI會選擇最高適應度值來走,即會在第3列落下棋子。
同理能夠分析接下來AI的選擇。概括起來即是,若是AI的一步使得Player獲勝的可能性越大,AI的適應度值越低,AI也選擇適應度較高的,即按照阻止Player獲勝的走法進行。固然,若是它本身可以獲勝,它會優先將本身獲勝的走法設置最高適應度。
def getPotentialMoves(board, tile, lookAhead): if lookAhead == 0 or isBoardFull(board): ''' 若是難度係數爲0,或格子已滿 則返回列表值全爲0,即此時 適應度值和列的潛在移動值相等。 此時AI將隨機降落棋子,失去智能 ''' return [0] * BOARDWIDTH #肯定對手棋子顏色 if tile == RED: enemyTile = BLACK else: enemyTile = RED potentialMoves = [0] * BOARDWIDTH #初始一個潛在的移動列表,其數值所有爲0 for firstMove in range(BOARDWIDTH): #對每一欄進行遍歷,將雙方中的任一方的移動稱爲firstMove #則另一方的移動就稱爲對手,counterMove。 #這裏咱們的firstMove爲AI,對手爲玩家。 dupeBoard = copy.deepcopy(board) #這裏用深複製是爲了讓board和dupeBoard不互相影響 if not isValidMove(dupeBoard, firstMove): #若是在dupeBoard中黑色棋子移到firstMove欄無效 continue #則繼續下一個firstMove makeMove(dupeBoard, tile, firstMove) #若是是有效移動,則設置相應的格子顏色 if isWinner(dupeBoard, tile): #若是獲勝 potentialMoves[firstMove] = 1 #獲勝的棋子自動得到一個很高的數值來表示其獲勝的概率 #數值越大,獲勝可能性越大,對手獲勝可能性越小。 break #不要干擾計算其餘的移動 else: if isBoardFull(dupeBoard): #若是dupeBoard中沒有空格 potentialMoves[firstMove] = 0 #沒法移動 else: for counterMove in range(BOARDWIDTH): #考慮對手的移動 dupeBoard2 = copy.deepcopy(dupeBoard) if not isValidMove(dupeBoard2, counterMove): continue makeMove(dupeBoard2, enemyTile, counterMove) if isWinner(dupeBoard2, enemyTile): potentialMoves[firstMove] = -1 #若是玩家獲勝,則AI的在此欄值最低 break else: # 遞歸調用getPotentialMoves results = getPotentialMoves(dupeBoard2, tile, lookAhead - 1) potentialMoves[firstMove] += (sum(results) / BOARDWIDTH) / BOARDWIDTH return potentialMoves
拖拽棋子,判斷棋子所在位置的格子,驗證棋子的有效性,調用棋子下落函數,完成操做。
def getHumanMove(board, isFirstMove): draggingToken = False tokenx, tokeny = None, None while True: # pygame.event.get()來處理全部的事件 for event in pygame.event.get(): if event.type == QUIT:#中止,退出 pygame.quit() sys.exit() elif event.type == MOUSEBUTTONDOWN and not draggingToken and REDPILERECT.collidepoint(event.pos): #若是事件類型爲鼠標按下,notdraggingToken爲True,鼠標點擊的位置在REDPILERECT裏面 draggingToken = True tokenx, tokeny = event.pos elif event.type == MOUSEMOTION and draggingToken:#若是開始拖動了紅色棋子 tokenx, tokeny = event.pos #更新被拖拽的棋子的位置 elif event.type == MOUSEBUTTONUP and draggingToken: #若是鼠標鬆開,而且棋子被拖拽 #若是棋子被拖拽在board的正上方 if tokeny < YMARGIN and tokenx > XMARGIN and tokenx < WINDOWWIDTH - XMARGIN: column = int((tokenx - XMARGIN) / SPACESIZE)#根據棋子的x座標肯定棋子會落的列(0,1...6) if isValidMove(board, column):#若是棋子移動有效 """ 掉落在相應的空格子中, 這裏只是顯示一個掉落的效果 不用這個函數也能經過下面的代碼實現棋子填充空格 """ animateDroppingToken(board, column, RED) #將空格中最下面的格子設爲紅色 board[column][getLowestEmptySpace(board, column)] = RED drawBoard(board)#在落入的格子中畫紅色棋子 pygame.display.update()#窗口更新 return tokenx, tokeny = None, None draggingToken = False if tokenx != None and tokeny != None:#若是拖動了棋子,則顯示拖動的棋子 drawBoard(board, {'x':tokenx - int(SPACESIZE / 2), 'y':tokeny - int(SPACESIZE / 2), 'color':RED}) #而且經過調整x,y的座標使拖動時,鼠標始終位於棋子的中心位置。 else: drawBoard(board)#當爲無效移動時,鼠標鬆開後,由於此時board中全部格子的值均爲none #調用drawBoard時,進行的操做是顯示下面的兩個棋子,至關於棋子回到到開始拖動的地方。 if isFirstMove: DISPLAYSURF.blit(ARROWIMG, ARROWRECT)#AI先走,顯示提示操做圖片 pygame.display.update() FPSCLOCK.tick()
實現AI棋子自動移動並降落到相應位置的函數。
def animateComputerMoving(board, column): x = BLACKPILERECT.left#下面黑色棋子的左座標 y = BLACKPILERECT.top #下面黑色棋子的上座標 speed = 1.0 while y > (YMARGIN - SPACESIZE):#當y的值較大,即棋子位於窗口下方時 y -= int(speed)#y不斷減少,即棋子不斷上移 speed += 0.5#減少的速度增長 drawBoard(board, {'x':x, 'y':y, 'color':BLACK}) #y不斷變化,不斷繪製紅色棋子,造成不斷上升的效果 pygame.display.update() FPSCLOCK.tick() #當棋子上升到borad頂端時 y = YMARGIN - SPACESIZE#y從新賦值,此時棋子的最下邊和board的最上邊相切 speed = 1.0 while x > (XMARGIN + column * SPACESIZE):#當x值大於須要移到的列的x座標時 x -= int(speed)#x值不斷減少,即左移 speed += 0.5 drawBoard(board, {'x':x, 'y':y, 'color':BLACK}) #此時y座標已經不變,即從board上端向左平移到所在列。 pygame.display.update() FPSCLOCK.tick() #黑色棋子降落到計算獲得的空格 animateDroppingToken(board, column, BLACK)
經過返回的potentialMoves,選擇其列表中最高的數字做爲適應度值,並從這些適應度高欄中隨機選擇做爲最後的移動目標。
def getComputerMove(board): potentialMoves = getPotentialMoves(board, BLACK, DIFFICULTY)#潛在的移動,是一個含BOARDWIDTH個值的列表。。 print potentialMoves #列表值與設置的難度係數有關 bestMoves = [] bestMoveFitness =max(potentialMoves) print bestMoveFitness #創建bestMoves空列表 for i in range(len(potentialMoves)): if potentialMoves[i] == bestMoveFitness and isValidMove(board, i): bestMoves.append(i) #列出全部能夠移動到的列,該列表可能爲空,可能只有一個值,也可能有多個值 print bestMoves return random.choice(bestMoves)#從能夠移動到的列中,隨機選擇一個做爲移動到的目標
經過不斷改變棋子的相應座標,實現下落的動畫效果。
def getLowestEmptySpace(board, column): # 返回最一列中最下面的空格 for y in range(BOARDHEIGHT-1, -1, -1): if board[column][y] == EMPTY: return y return -1 def makeMove(board, player, column): lowest = getLowestEmptySpace(board, column)#返回一欄中 if lowest != -1:#若是格子中的有空格 board[column][lowest] = player '''則將player(red/black)賦值給一欄中的最low的一個空格 由於,棋子是落在一欄當中全部空格的最下面一個空格 即認定這個格子中的顏色 ''' def animateDroppingToken(board, column, color): x = XMARGIN + column * SPACESIZE #x座標 y = YMARGIN - SPACESIZE #y座標 dropSpeed = 1.0#棋子降落的速度 lowestEmptySpace = getLowestEmptySpace(board, column)#一列的空格當中最下面的一個空格 while True: y += int(dropSpeed)#y的座標以dropSpeed疊加 dropSpeed += 0.5#dropSpeed也在加速,即棋子下落的加速度爲0.5 if int((y - YMARGIN) / SPACESIZE) >= lowestEmptySpace:#判斷到達最下面的空格 return drawBoard(board, {'x':x, 'y':y, 'color':color})#y不斷變化,不斷繪製紅色棋子,造成不斷降落的效果 pygame.display.update() FPSCLOCK.tick()
判斷棋子的移動是否有效,判斷棋盤是否還有空格
def isValidMove(board, column): #判斷棋子移動有效性 if column < 0 or column >= (BOARDWIDTH) or board[column][0] != EMPTY: #若是列<0,或>BOARDWIDTH,或列中沒有空格子 return False #則爲無效的移動,不然有效 return True def isBoardFull(board): #若是格子中沒有空餘,則返回True for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): if board[x][y] == EMPTY: return False return True
幾張示意圖方便了解,獲勝的四種狀況。圖中所示是x,y取極值時所對應的位置。
def isWinner(board, tile): # 檢查水平方向棋子狀況 for x in range(BOARDWIDTH - 3):#x的取值爲0,1,2,3 for y in range(BOARDHEIGHT):#遍歷全部行 #若是x=0,則看第y行前4個棋子否都是相同的棋子,以此類推能夠遍歷全部的水平棋子四子相連狀況.只要有一個x,y成立就能夠斷定獲勝 if board[x][y] == tile and board[x+1][y] == tile and board[x+2][y] == tile and board[x+3][y] == tile: return True # 檢查豎直方向棋子狀況,與水平狀況相似 for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT - 3): if board[x][y] == tile and board[x][y+1] == tile and board[x][y+2] == tile and board[x][y+3] == tile: return True # 檢查左傾斜方向棋子狀況 for x in range(BOARDWIDTH - 3):#x取值0,1,2,3 for y in range(3, BOARDHEIGHT):#由於左傾斜連成四子時,最坐下面的棋子至少爲列中距離最上面四個格子,即y>=3 if board[x][y] == tile and board[x+1][y-1] == tile and board[x+2][y-2] == tile and board[x+3][y-3] == tile:#斷定左傾斜四子同色 return True # 檢查右傾斜方向棋子狀況,與左傾斜相似 for x in range(BOARDWIDTH - 3): for y in range(BOARDHEIGHT - 3): if board[x][y] == tile and board[x+1][y+1] == tile and board[x+2][y+2] == tile and board[x+3][y+3] == tile: return True return False
def main(): isFirstGame = True #初始isFirstGame while True: #使遊戲一直可以運行下去 runGame(isFirstGame) isFirstGame = False def runGame(isFirstGame): if isFirstGame: # 剛剛啓動遊戲第一局時 #讓AI先走第一步棋子,以便玩家能夠觀察到遊戲是怎麼玩的 turn = COMPUTER showHelp = True else: # 從第二劇開始,隨機分配 if random.randint(0, 1) == 0: turn = COMPUTER else: turn = HUMAN showHelp = False
基於蒙特卡洛搜索樹算法,利用Pygame模塊使用Python代碼實現了,人工自由選擇棋子,AI經過算法智能跳到的人機大戰效果。整個實驗,讓咱們熟悉了pygame建立實例和移動的基礎知識,也初步瞭解了蒙特卡洛算法的具體應用。
http://www.mamicode.com/info-detail-1261189.html
蒙特卡洛搜索樹介紹:http://jeffbradberry.com/posts/2015/09/intro-to-monte-carlo-tree-search/