嗯,今天接着來搞五子棋,從五子棋開始給小夥伴們聊AI。html
昨天晚上咱們已經實現了一個五子棋的邏輯部分,其實講道理,有個規則在,能夠開始搞AI了,可是考慮到不夠直觀,咱們仍是順帶先把五子棋的UI也先搞出來。因此今天我們搞UI。數組
邏輯部分在這裏:[深度學習]實現一個博弈型的AI,從五子棋開始(1)框架
小夥伴:啥?再次省去吐槽一萬字,說好的講深度學習在哪兒,說好的強化學習在哪兒,今天又是五子棋……函數
我:是五子棋,AI不能缺場景啊,沒有場景談AI就是空談,是得先有個棋啊。再說了,雖然說以前搞了個邏輯,至少搞個界面出來測一下嘛,萬一場景的邏輯都沒對,還AI個錘子!學習
老羅:又關我什麼事?ui
好了,不扯了,回正題,咱們一開始設計就是邏輯和UI分離,上一篇咱們實現了邏輯部分,今天來實現UI部分,給咱的五子棋搞個UI。spa
(2)五子棋下棋UI的實現設計
Python作五子棋UI的話,我們這裏就用 PyGame 來搞,固然也有別的庫,說老實話Python作UI我真沒搞過多少,PyGame 的基礎用法和各類知識我就不展開了,畢竟這不是重點,有興趣的小夥伴能夠自行Google,我也是邊學邊用呢,哈哈!code
既然是作UI,得有素材,我在網上找了一個棋盤:orm
以及黑白兩顆棋子:
PS:爲了UI上面好看,棋子由於是圓形的,最好是處理成PNG格式,帶Alpha通道,外面透明。另外這幾張圖不知道上傳了會不會被壓縮成別的格式,我打了個包放在文章末尾了。
在我們以前的工程裏建個目錄「UI」,棋盤取名 chessboard.jpg 放在目錄下,兩顆棋子分別取名 piece_black.png、piece_white.png 也放到目錄下。
看看屬性,棋盤是540*540像素的,棋子是32*32像素,數字記下來,而後我們找的這個棋盤是有邊緣的,量一下,邊緣離第一根線大約是22像素。要作render,得用到這些數字。
橫豎各15根線這個不用說,15根線中間有14個格子,因此線和線的距離是總寬度減去兩個邊緣再除以格子數: (540 - 22 * 2) / 14 貌似除不盡,那就先這樣子。
好了,建一個文件 render.py ,我們先把剛剛那些數字放進去,順便該import的也import了,好比pygame、好比我們昨天的定義和昨天的五棋子邏輯:
#coding:utf-8 import pygame from pygame.locals import * from consts import * from gobang import GoBang #IMAGE_PATH = '/Users/phantom/Projects/AI/gobang/UI/' IMAGE_PATH = 'UI/' WIDTH = 540 HEIGHT = 540 MARGIN = 22 GRID = (WIDTH - 2 * MARGIN) / (N - 1) PIECE = 32
而後咱們定義一個新的類,GameRender,render初始化的時候咱們綁定一個邏輯類,而後初始化pygame,把窗體大小設置一下,該加載的資源先加載了,代碼比較簡單,沒有什麼爲何,pygame就是這麼用的,pygame有興趣的小夥伴本身Google。
render咱們仍然考慮定義了一個current表示當前步,黑棋先下,因此current定義成黑色。
class GameRender(object): def __init__(self, gobang): # 綁定邏輯類 self.__gobang = gobang # 黑棋開局 self.__currentPieceState = ChessboardState.BLACK # 初始化 pygame pygame.init() # pygame.display.set_mode((width, height), flags, depth) self.__screen = pygame.display.set_mode((WIDTH, HEIGHT), 0, 32) pygame.display.set_caption('五子棋AI') # UI 資源 self.__ui_chessboard = pygame.image.load(IMAGE_PATH + 'chessboard.jpg').convert() self.__ui_piece_black = pygame.image.load(IMAGE_PATH + 'piece_black.png').convert_alpha() self.__ui_piece_white = pygame.image.load(IMAGE_PATH + 'piece_white.png').convert_alpha()
render類嘛,各類draw了,對不對,確實是。不過這裏有一個問題。
以前的邏輯類裏咱們定義了一個二維數組chessMap還記得嗎?看看邏輯類GoBang的定義:
class GoBang(object): def __init__(self): self.__chessMap = [[ChessboardState.EMPTY for j in range(N)] for i in range(N)] self.__currentI = -1 self.__currentJ = -1 self.__currentState = ChessboardState.EMPTY
咱們先思考一個問題,chessMap裏的座標,和我們棋盤的座標怎麼對應呢,chessMap裏 i,j 就是0到14,0到14;我們棋盤上,render的時候,那但是按像素來的啊,棋盤但是0到540像素呢,嚴格的說,是540減去兩個邊緣,22到518像素,得先對應吧。好,作個座標變換,把棋子下標 i,j 變成像素 x,y。從邊緣開始計算,每相鄰一個棋子,加一個格子的大小GRID,那若是咱們的棋子要擺上去的話,要擺到棋子中間,因此 x,y 分別再減去半個棋子的大小,代碼就2行,比較清晰了:
def coordinate_transform_map2pixel(self, i, j): # 從 chessMap 裏的邏輯座標到 UI 上的繪製座標的轉換 return MARGIN + j * GRID - PIECE / 2, MARGIN + i * GRID - PIECE / 2
好,這下咱們能夠從邏輯類裏讀狀態出來繪製了,再考慮一下,座標變換嘛,還需不須要反過來變。是,確實須要,下棋落子的時候,實際是在UI上給獲得 x,y 對吧,咱們得去set一下邏輯類裏的狀態吧,因此這時候又須要把 x,y 座標變換成 i,j 的,怎麼算就不詳細展開了,類似的邏輯。
這裏我們偷個懶的話,前一個映射函數不是有式子了麼:
x = MARGIN + j * GRID - PIECE / 2
y = MARGIN + i * GRID - PIECE / 2
作個位移,推導一下等式的兩邊, 把 j 用 x 來表達一下, i 用 y 來表達一下,就能夠了:
i = (y - MARGIN + PIECE / 2) / GRID
j = (x - MARGIN + PIECE / 2) / GRID
這裏細心的小夥伴們發現了,i 和 j 可能不是整數哦,首先得到的座標固然是經過鼠標來,這個原本就有誤差,不會那麼剛恰好,而且GRID好像也不是整數,除一下,都不知道是多少了,OK,那我們Round一下咯。
又有小夥伴說了,不是棋盤有邊緣麼,那個MARGIN就時刻提醒咱們,有個邊緣,要是我在邊緣上點擊,會不會出現負值,或者大於N的值。對,考慮得很好,得判斷一下邊界,這下應該差很少了,能夠寫代碼了:
def coordinate_transform_pixel2map(self, x, y): # 從 UI 上的繪製座標到 chessMap 裏的邏輯座標的轉換 i , j = int(round((y - MARGIN + PIECE / 2) / GRID)), int(round((x - MARGIN + PIECE / 2) / GRID)) # 有MAGIN, 排除邊緣位置致使 i,j 越界 if i < 0 or i >= N or j < 0 or j >= N: return None, None else: return i, j
好了,到如今,座標的映射也搞定了,終於能夠draw、draw、draw了,好吧,那就draw,先畫棋盤再畫棋子,棋子是啥顏色就畫啥顏色,空白的就跳過:
def draw_chess(self): # 棋盤 self.__screen.blit(self.__ui_chessboard, (0,0)) # 棋子 for i in range(0, N): for j in range(0, N): x,y = self.coordinate_transform_map2pixel(i,j) state = self.__gobang.get_chessboard_state(i,j) if state == ChessboardState.BLACK: self.__screen.blit(self.__ui_piece_black, (x,y)) elif state == ChessboardState.WHITE: self.__screen.blit(self.__ui_piece_white, (x,y)) else: # ChessboardState.EMPTY pass
爲了下棋的時候體驗稍微好一點呢,咱們在鼠標上是否是最好也畫一個棋子,這樣感受點上去就能落子,好像會好一點:
def draw_mouse(self): # 鼠標的座標 x, y = pygame.mouse.get_pos() # 棋子跟隨鼠標移動 if self.__currentPieceState == ChessboardState.BLACK: self.__screen.blit(self.__ui_piece_black, (x - PIECE / 2, y - PIECE / 2)) else: self.__screen.blit(self.__ui_piece_white, (x - PIECE / 2, y - PIECE / 2))
若是出現連續的5顆同色棋子,要顯示贏棋的結果,那就再來個draw:
def draw_result(self, result): font = pygame.font.Font('/Library/Fonts/Songti.ttc', 50) tips = u"本局結束:" if result == ChessboardState.BLACK : tips = tips + u"黑棋勝利" elif result == ChessboardState.WHITE: tips = tips + u"白棋勝利" else: tips = tips + u"平局" text = font.render(tips, True, (255, 0, 0)) self.__screen.blit(text, (WIDTH / 2 - 200, HEIGHT / 2 - 50))
想一想還差啥?
對,下棋的邏輯還沒作吧,鼠標點擊,在棋盤上放顆棋子,咱們剛剛draw棋子的時候實際上是讀取的邏輯類裏的chessMap,那下棋的時候,去set對應的狀態:
def one_step(self): i, j = None, None # 鼠標點擊 mouse_button = pygame.mouse.get_pressed() # 左鍵 if mouse_button[0]: x, y = pygame.mouse.get_pos() i, j = self.coordinate_transform_pixel2map(x, y) if not i is None and not j is None: # 格子上已經有棋子 if self.__gobang.get_chessboard_state(i, j) != ChessboardState.EMPTY: return False else: self.__gobang.set_chessboard_state(i, j, self.__currentPieceState) return True return False
如今不是還沒AI嘛,咱們一不作二不休,先搞一我的人對弈,那就再加一個切換顏色的函數:
def change_state(self): if self.__currentPieceState == ChessboardState.BLACK: self.__currentPieceState = ChessboardState.WHITE else: self.__currentPieceState = ChessboardState.BLACK
好了,還差啥?好像做爲render的話,感受差很少,那就來個main函數溜一溜代碼試試,新建一個 game.py,這裏咱們 main 函數裏先給AI留個接口,至少留個框架咯 :
import pygame from pygame.locals import * from sys import exit from consts import * from gobang import GoBang from render import GameRender #from gobang_ai import GobangAI if __name__ == '__main__': gobang = GoBang() render = GameRender(gobang) #先給AI留個接口 #ai = GobangAI(gobang, ChessboardState.WHITE) result = ChessboardState.EMPTY enable_ai = False while True: # 捕捉pygame事件 for event in pygame.event.get(): # 退出程序 if event.type == QUIT: exit() elif event.type == MOUSEBUTTONDOWN: # 成功着棋 if render.one_step(): result = gobang.get_chess_result() else: continue if result != ChessboardState.EMPTY: break if enable_ai: #ai.one_step() result = gobang.get_chess_result() else: render.change_state() # 繪製 render.draw_chess() render.draw_mouse() if result != ChessboardState.EMPTY: render.draw_result(result) # 刷新 pygame.display.update()
好了,跑一下試試,沒有AI就拉兩個小夥伴來對弈,實在不行先左手和右手來一把,好像還行,邏輯沒問題:
整理一下完整版的 render.py :
#coding:utf-8 import pygame from pygame.locals import * from consts import * from gobang import GoBang #IMAGE_PATH = '/Users/phantom/Projects/AI/gobang/UI/' IMAGE_PATH = 'UI/' WIDTH = 540 HEIGHT = 540 MARGIN = 22 GRID = (WIDTH - 2 * MARGIN) / (N - 1) PIECE = 32 class GameRender(object): def __init__(self, gobang): # 綁定邏輯類 self.__gobang = gobang # 黑棋開局 self.__currentPieceState = ChessboardState.BLACK # 初始化 pygame pygame.init() # pygame.display.set_mode((width, height), flags, depth) self.__screen = pygame.display.set_mode((WIDTH, HEIGHT), 0, 32) pygame.display.set_caption('五子棋AI') # UI 資源 self.__ui_chessboard = pygame.image.load(IMAGE_PATH + 'chessboard.jpg').convert() self.__ui_piece_black = pygame.image.load(IMAGE_PATH + 'piece_black.png').convert_alpha() self.__ui_piece_white = pygame.image.load(IMAGE_PATH + 'piece_white.png').convert_alpha() def coordinate_transform_map2pixel(self, i, j): # 從 chessMap 裏的邏輯座標到 UI 上的繪製座標的轉換 return MARGIN + j * GRID - PIECE / 2, MARGIN + i * GRID - PIECE / 2 def coordinate_transform_pixel2map(self, x, y): # 從 UI 上的繪製座標到 chessMap 裏的邏輯座標的轉換 i , j = int(round((y - MARGIN + PIECE / 2) / GRID)), int(round((x - MARGIN + PIECE / 2) / GRID)) # 有MAGIN, 排除邊緣位置致使 i,j 越界 if i < 0 or i >= N or j < 0 or j >= N: return None, None else: return i, j def draw_chess(self): # 棋盤 self.__screen.blit(self.__ui_chessboard, (0,0)) # 棋子 for i in range(0, N): for j in range(0, N): x,y = self.coordinate_transform_map2pixel(i,j) state = self.__gobang.get_chessboard_state(i,j) if state == ChessboardState.BLACK: self.__screen.blit(self.__ui_piece_black, (x,y)) elif state == ChessboardState.WHITE: self.__screen.blit(self.__ui_piece_white, (x,y)) else: # ChessboardState.EMPTY pass def draw_mouse(self): # 鼠標的座標 x, y = pygame.mouse.get_pos() # 棋子跟隨鼠標移動 if self.__currentPieceState == ChessboardState.BLACK: self.__screen.blit(self.__ui_piece_black, (x - PIECE / 2, y - PIECE / 2)) else: self.__screen.blit(self.__ui_piece_white, (x - PIECE / 2, y - PIECE / 2)) def draw_result(self, result): font = pygame.font.Font('/Library/Fonts/Songti.ttc', 50) tips = u"本局結束:" if result == ChessboardState.BLACK : tips = tips + u"黑棋勝利" elif result == ChessboardState.WHITE: tips = tips + u"白棋勝利" else: tips = tips + u"平局" text = font.render(tips, True, (255, 0, 0)) self.__screen.blit(text, (WIDTH / 2 - 200, HEIGHT / 2 - 50)) def one_step(self): i, j = None, None # 鼠標點擊 mouse_button = pygame.mouse.get_pressed() # 左鍵 if mouse_button[0]: x, y = pygame.mouse.get_pos() i, j = self.coordinate_transform_pixel2map(x, y) if not i is None and not j is None: # 格子上已經有棋子 if self.__gobang.get_chessboard_state(i, j) != ChessboardState.EMPTY: return False else: self.__gobang.set_chessboard_state(i, j, self.__currentPieceState) return True return False def change_state(self): if self.__currentPieceState == ChessboardState.BLACK: self.__currentPieceState = ChessboardState.WHITE else: self.__currentPieceState = ChessboardState.BLACK
好了,就這樣~
UI素材我打個包放這兒了:
…………後記…………
我:小夥伴們別吐槽了,明天必定開始搞AI了,由於五子棋我們有啦~
小夥伴:好吧,終於。
我:等一下,明天?NO,我口誤,下一篇必定開始搞AI了,明天不必定有時間來寫博客呢 - -
小夥伴:再再次省去吐槽一萬字!
我:反正每週至少寫兩篇嘛,OK?
小夥伴:那我還能怎樣……