zoukankan      html  css  js  c++  java
  • [深度学习]实现一个博弈型的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?

    小伙伴:那我还能怎样……

  • 相关阅读:
    Java实现 蓝桥杯 算法训练 画图(暴力)
    Java实现 蓝桥杯 算法训练 画图(暴力)
    Java实现 蓝桥杯 算法训练 相邻数对(暴力)
    Java实现 蓝桥杯 算法训练 相邻数对(暴力)
    Java实现 蓝桥杯 算法训练 相邻数对(暴力)
    Java实现 蓝桥杯 算法训练 Cowboys
    Java实现 蓝桥杯 算法训练 Cowboys
    55. Jump Game
    54. Spiral Matrix
    50. Pow(x, n)
  • 原文地址:https://www.cnblogs.com/erwin/p/7835191.html
Copyright © 2011-2022 走看看