嗯,今天接着來搞五子棋,從五子棋開始給小伙伴們聊AI。
昨天晚上我們已經實現了一個五子棋的邏輯部分,其實講道理,有個規則在,可以開始搞AI了,但是考慮到不夠直觀,我們還是順帶先把五子棋的UI也先搞出來。所以今天咱們搞UI。
邏輯部分在這里:[深度學習]實現一個博弈型的AI,從五子棋開始(1)
小伙伴:啥?再次省去吐槽一萬字,說好的講深度學習在哪兒,說好的強化學習在哪兒,今天又是五子棋……
我:是五子棋,AI不能缺場景啊,沒有場景談AI就是空談,是得先有個棋啊。再說了,雖說之前搞了個邏輯,至少搞個界面出來測一下嘛,萬一場景的邏輯都沒對,還AI個錘子!
老羅:又關我什么事?
好了,不扯了,回正題,我們一開始設計就是邏輯和UI分離,上一篇我們實現了邏輯部分,今天來實現UI部分,給咱的五子棋搞個UI。
(2)五子棋下棋UI的實現
Python做五子棋UI的話,咱們這里就用 PyGame 來搞,當然也有別的庫,說老實話Python做UI我真沒搞過多少,PyGame 的基礎用法和各種知識我就不展開了,畢竟這不是重點,有興趣的小伙伴可以自行Google,我也是邊學邊用呢,哈哈!
既然是做UI,得有素材,我在網上找了一個棋盤:
以及黑白兩顆棋子:
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?
小伙伴:那我還能怎樣……