[深度學習]實現一個博弈型的AI,從五子棋開始(2)


嗯,今天接着來搞五子棋,從五子棋開始給小伙伴們聊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素材我打個包放這兒了:

點擊這里下載UI素材

 

…………后記…………

我:小伙伴們別吐槽了,明天一定開始搞AI了,因為五子棋咱們有啦~

小伙伴:好吧,終於。

我:等一下,明天?NO,我口誤,下一篇一定開始搞AI了,明天不一定有時間來寫博客呢 - -

小伙伴:再再次省去吐槽一萬字!

我:反正每周至少寫兩篇嘛,OK?

小伙伴:那我還能怎樣……

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM