BNB AI

BNB AI设计

概述
在本次python的项目中,我们小组顺利的完成了游戏泡泡堂的基本功能,总代码量接近5k行,并在游戏泡泡堂的基本功能上实现了创新和拓展。选择泡泡堂作为python课程的项目题目是因为泡泡堂这一游戏,本身被多数玩家青睐,自2003年泡泡堂创建至今,泡泡堂就一直流行于中国甚至世界。因此,在学习了python基础之后,我们小组选择实现泡泡堂这个游戏,在实现过程中能帮助我们更好地掌握python,了解python相关的工具和环境,理解python的特性。在实现方面,泡泡堂有很大的扩展空间,在各个功能模块上面也容易区分开来,整个项目能够由简到繁,使得小组内的每个成员能够很好的完成自己的工作。

功能
这里写图片描述

游戏截图
这里写图片描述
这里写图片描述
这里写图片描述

AI设计
这篇博客将与大家分享BNB项目过程中的AI设计,AI模块设计代码量394行。所分享的内容仅为此次BNB项目AI设计的重点部分,要想设计出一个较为完整的AI还需要许多的功能和长时间的Debug。下面将依次介绍设计过程中的重点部分,非常欢迎各位博友一起讨论。
a) 设计思路
b) 计算安全区域
c) 判断当前位置放泡泡是否安全
d) 寻找路径
e) 按照路径移动

AI设计流程图
这里写图片描述

设计思路
我们希望游戏中的AI能够存活足够长的时间,并且能够在此基础上开辟通到玩家的路径,离玩家越来越近,当与玩家足够接近时尝试放泡泡将玩家炸死。游戏中的AI能够首先根据周围的地形、整体的局势来决定下一步该怎么做。AI将会根据环境来计算出一个合适的路径,接着根据路径移动,在每一步移动中都会刷新路径,达到AI实时反应的效果。
为达到这个目标,AI在每一步行动中会采取如下策略。
a) 计算安全区域和危险区域。
b) 计算玩家位置。
c) 寻找AI到达玩家的路径,同时判定AI与玩家是否连通。
d) 判断AI当前的位置是否安全。
e) 若AI当前位置危险,规避泡泡,计算路径。
否则若AI与玩家不连通,炸箱子,计算路径。
否则若AI与玩家连通,攻击玩家,计算路径。
f) 判断AI是否需要推箱子。若AI需要推箱子,则推箱子,计算路径。
g) 根据路径移动

    def control(self):
        """运行AI"""
        self.compute_safe_region()
        self.compute_player_pos()
        self.find_path("JudgeReachable")

        self.judge_evade()
        if self.evade:
            self.find_path("EvadeBubble")
        elif not self.attack:
            self.find_path("FindBox")
        else:
            self.find_path("FindPlayer")

        self.judge_push()
        if self.push:
            self.find_path("PushBox")

        self.move()
        self.kill()

计算安全区域
AI运行的第一步就是计算安全区域。先说明在此函数中定义的数据结构,record是一个二维列表,表示当前的地图情况,列表中存储布尔类型,True为可走,False为不可走。那么哪些地方是可走的、哪些地方是不可走的呢?在我们的地图中,如果某个格子有障碍物或超出了边界,那么这个格子当然是不可走的,为False。还有一种情况的格子也是不可走的,就是有水柱的格子和即将出现水柱的格子。除此之外其它格子为安全格子。

    def compute_safe_region(self):
        """计算安全区域与危险区域,存储在record中"""
        delay = 10
        for i in range(1, self.screen_x + 1):
            for j in range(1, self.screen_y + 1):
                if self.plat.f1[i][j] == None or isinstance(self.plat.f1[i][j], Prop):
                    if self.record_count[i][j] >= delay:
                        self.record[i][j] = True
                        self.record_count[i][j] = 0
                    else:
                        self.record_count[i][j] += 1
                else:
                    self.record[i][j] = False
                    self.record_count[i][j] = 0

        for i in range(1, self.screen_x + 1):
            for j in range(1, self.screen_y + 1):
                if type(self.plat.f1[i][j]) == Bubble or type(self.plat.f1[i][j]) == TimingBubble:
                # 对泡泡四个方向的水柱区域
                    for k in range(0, self.plat.f1[i][j].field + 1):
                        if j - k >= 1:
                            self.record[i][j - k] = False
                            self.record_count[i][j - k] = 0
                        if j + k <= self.screen_y:
                            self.record[i][j + k] = False
                            self.record_count[i][j + k] = 0
                        if i - k >= 1:
                            self.record[i - k][j] = False
                            self.record_count[i - k][j] = 0
                        if i + k <= self.screen_x:
                            self.record[i + k][j] = False
                            self.record_count[i + k][j] = 0

判断当前位置放泡泡是否安全
这个函数是为了让AI不炸死自己而设计的。函数的功能为判断当前位置放泡泡是否安全。在该函数中,我定义了两种数据,一个是若放泡泡,泡泡波及的范围,另一个是若放泡泡,AI的逃脱范围。在逃脱范围的十字中,若存在一个拐角,那么AI是能够逃脱的。此外,若在泡泡波及范围之外有安全的区域,则AI也是能够逃脱的。其它情况都是不能逃脱的。

    def try_bubble(self, x, y):
        """
        判断当前位置放泡泡是否安全
        spout_range 表示若放泡泡,该泡泡波及的范围
        escape_range 表示若放泡泡,逃脱的范围
        """
        # 左右上下
        spout_range = [0, 0, 0, 0]
        escape_range = [0, 0, 0, 0]

        # 计算若放泡泡,泡泡波及的范围
        for i in range(0, 4):
            for j in range(1, self.power + 2):
                if i == 0 and not self.check_f1(x - j, y):
                    break
                elif i == 1 and not self.check_f1(x + j, y):
                    break
                elif i == 2 and not self.check_f1(x, y - j):
                    break
                elif i == 3 and not self.check_f1(x, y + j):
                    break
            if i == 0:   spout_range[i] = x - (j - 1)
            elif i == 1: spout_range[i] = x + (j - 1)
            elif i == 2: spout_range[i] = y - (j - 1)
            elif i == 3: spout_range[i] = y + (j - 1)

        # 计算若放泡泡,AI的逃脱范围
        for i in range(0, 4):
            for j in range(1, self.power + 2):
                if i == 0 and not self.check_record(x - j, y):
                    break
                elif i == 1 and not self.check_record(x + j, y):
                    break
                elif i == 2 and not self.check_record(x, y - j):
                    break
                elif i == 3 and not self.check_record(x, y + j):
                    break
            if i == 0:   escape_range[i] = x - (j - 1)
            elif i == 1: escape_range[i] = x + (j - 1)
            elif i == 2: escape_range[i] = y - (j - 1)
            elif i == 3: escape_range[i] = y + (j - 1)

        # 沿着可通行的十字,人物只要可以在任意一个地方中途跳出 (-1 / +1)就说明这个泡泡不会困住人的所有逃生路径
        for i in range(1, x - escape_range[0] + 1):
            if self.check_record(x - i, y + 1) or self.check_record(x - i, y - 1):
                return True

        for i in range(1, escape_range[1] - x + 1):
            if self.check_record(x + i, y + 1) or self.check_record(x + i, y - 1):
                return True

        for i in range(1, y - escape_range[2] + 1):
            if self.check_record(x - 1, y - i) or self.check_record(x + 1, y - i):
                return True

        for i in range(1, escape_range[3] - y + 1):
            if self.check_record(x - 1, y + i) or self.check_record(x + 1, y + i):
                return True

        # 检查泡泡威力之外有没有可通行的地方
        for i in range(0, 4):
            if spout_range[i] != escape_range[i]:
                return False
        if self.check_record(spout_range[0] - 1, y) or self.check_record(spout_range[1] + 1, y) or self.check_record(
                x, spout_range[2] - 1) or self.check_record(x, spout_range[3] + 1):
            return True
        return False

寻找路径
算法思路:AI依据参数option计算路径时,应用了BFS的思路。即判断在AI当前位置的上下左右四个方向的网格中,是否存在符合要求的网格,不同的option要求不同。若在AI的四周存在这样的网格,则将该网格坐标加入temp_grid[i][j]中,即从AI位置到达该位置(i, j)的路径。在遍历到目标位置时,结束BFS,将AI到该目标位置的路径赋值到self.path中,这样就完成了一次寻找路径。

为什么要选择BFS算法来搜索路径?第一点是基于性能的考虑,我们在图论中知道计算路径的算法有BFS、DFS、Floyd、Dijkstra这些算法,然而更加适用于网格地图的算法是BFS和DFS,从平均性能的角度出发,BFS性能是优于DFS的。第二点是基于Debug方面的考虑,使用BFS能更加清楚的了解算法的执行过程和数据结构的内容,便于测试和修改。

在遍历过程中,将可走的网格加入路径。当option为判断与玩家的连通性或者规避泡泡时,只要网格是空地或者是道具类型,那么该网格就是可走的。当option为寻找玩家或寻找箱子或推箱子时,如果网格是安全的,那么该网格才是可走的。

遍历的终止条件。由于option的不同,在计算路径的时候遍历的终止条件也将不同。当判断与玩家的连通性或寻找玩家时,若遍历到玩家位置,则终止。当规避泡泡时,若遍历到一个安全位置,则终止。当寻找箱子时,若遍历到一个周围有箱子的位置,并且在这个位置放泡泡不会将自己炸死,则终止。当推箱子时,若遍历到一个合适的推箱子位置,则终止。

    def find_path(self, option):
        """
        BFS
        option = JudgeReachable 判断AI是否与玩家连通
        option = EvadeBubble 规避所有泡泡
        option = FindPlayer 寻找最近玩家
        option = FindBox 寻找最近箱子
        option = PushBox 计算推箱子路径
        """
        # temp_grid[i][j] = [(), (), (), ...], 存储到达该位置的路径
        temp_grid = [[[] for j in range(self.screen_y + 1)] for i in range(self.screen_x + 1)]

        # queue = [(), (), (), ...], 存储单点位置
        queue = [(self.grid_x, self.grid_y)]

        # temp_grid[i][j] = [(), (), (), ...], 存储到达该位置的路径
        temp_grid[self.grid_x][self.grid_y].append((self.grid_x, self.grid_y))

        # visited[i][j] = bool, 存储某位置是否已遍历过
        visited = [[False for j in range(self.screen_y + 1)] for i in range(self.screen_x + 1)]

        # 记录搜索层数
        search_count = 0

        if option == "JudgeReachable":
            self.reachable = False

        while queue:
            search_count += 1
            if search_count > self.max_search_range:
                return

            cur = queue.pop(0)
            visited[cur[0]][cur[1]] = True

            # 遍历顺序 方向朝玩家
            offset = [self.player_pos[0] - self.grid_x, self.player_pos[1] - self.grid_y]
            # 右下左上
            if offset[0] >= 0 and offset[1] >= 0:
                next = [(cur[0] + 1, cur[1]), (cur[0], cur[1] + 1), (cur[0] - 1, cur[1]), (cur[0], cur[1] - 1)]
            # 右上左下
            elif offset[0] >= 0 and offset[1] < 0:
                next = [(cur[0] + 1, cur[1]), (cur[0], cur[1] - 1), (cur[0] - 1, cur[1]), (cur[0], cur[1] + 1)]
            # 左下右上
            elif offset[0] < 0 and offset[1] >= 0:
                next = [(cur[0] - 1, cur[1]), (cur[0], cur[1] + 1), (cur[0] + 1, cur[1]), (cur[0], cur[1] - 1)]
            # 左上右下
            elif offset[0] < 0 and offset[1] < 0:
                next = [(cur[0] - 1, cur[1]), (cur[0], cur[1] - 1), (cur[0] + 1, cur[1]), (cur[0], cur[1] + 1)]

            # 当遍历到目标位置时,结束
            if option == "JudgeReachable":
                if cur == self.player_pos:
                    self.reachable = True
                    return
            elif option == "EvadeBubble":
                if self.record[cur[0]][cur[1]]:
                    self.path = temp_grid[cur[0]][cur[1]]
                    return
            elif option == "FindPlayer":
                if cur == self.player_pos:
                    self.path = temp_grid[cur[0]][cur[1]]
                    return
            elif option == "FindBox":
                for ne in next:
                    if not (1 <= ne[0] <= self.screen_x and 1 <= ne[1] <= self.screen_y):
                        continue
                    if (type(self.plat.f1[ne[0]][ne[1]]) == plats.Box or type(self.plat.f1[ne[0]][ne[1]]) == plats.Wall) and type(self.plat.g[cur[0]][cur[1]]) != plats.Spine:
                        if not self.try_bubble(cur[0], cur[1]):
                            continue
                        self.path = temp_grid[cur[0]][cur[1]]
                        self.box_pos = cur
                        return
            elif option == "PushBox":
                for ne in next:
                    if not (1 <= ne[0] <= self.screen_x and 1 <= ne[1] <= self.screen_y):
                        continue
                    if type(self.plat.f1[ne[0]][ne[1]]) == plats.Box:
                        # c_记录cur与ne的改变量,用于指出ne在cur的哪个方向, (cur_x + 2*c_x, cur_y + 2*c_y)即为目标位置
                        c_x, c_y = ne[0] - cur[0], ne[1] - cur[1]
                        des_pos = (cur[0] + 2 * c_x, cur[1] + 2 * c_y)
                        if not (1 <= des_pos[0] <= self.screen_x and 1 <= des_pos[1] <= self.screen_y):
                            continue
                        if self.plat.f1[des_pos[0]][des_pos[1]] == None or isinstance(self.plat.f1[des_pos[0]][des_pos[1]], Prop):
                            temp_grid[ne[0]][ne[1]] = deepcopy(temp_grid[cur[0]][cur[1]])
                            temp_grid[ne[0]][ne[1]].append(ne)
                            self.path = temp_grid[ne[0]][ne[1]]
                            return

            # 遍历优先顺序 下左上右
            for ne in next:
                if not (1 <= ne[0] <= self.screen_x and 1 <= ne[1] <= self.screen_y):
                    continue
                if visited[ne[0]][ne[1]]:
                    continue

                if option == "JudgeReachable" or option == "EvadeBubble":
                    if self.plat.f1[ne[0]][ne[1]] == None or isinstance(self.plat.f1[ne[0]][ne[1]], Prop):
                        queue.append(ne)
                        # 需要采用深复制策略
                        temp_grid[ne[0]][ne[1]] = deepcopy(temp_grid[cur[0]][cur[1]])
                        temp_grid[ne[0]][ne[1]].append(ne)
                elif (option == "FindPlayer" and self.attack) or option == "FindBox" or option == "PushBox":
                    if self.record[ne[0]][ne[1]]:
                        queue.append(ne)
                        # 需要采用深复制策略
                        temp_grid[ne[0]][ne[1]] = deepcopy(temp_grid[cur[0]][cur[1]])
                        temp_grid[ne[0]][ne[1]].append(ne)

按照路径移动
让AI根据前面计算好的路径运动起来,在这个函数中,通过路径来设置AI的移动参数。在这里要注意的是只能取路径的开头部分来设置移动参数,否则会出现AI愣住的问题。

    def move(self):
        """按照路径移动"""
        path_count = 0
        for des in self.path:
            # 只需取path开头部分移动
            path_count += 1
            if path_count >= 3: break

            if (self.grid_x, self.grid_y) == des:
                self.moving_left  = False
                self.moving_right = False
                self.moving_down  = False
                self.moving_up    = False
            else:
                move_x, move_y = des[0] - self.grid_x, des[1] - self.grid_y
                if move_x == 1:         self.moving_right = True
                if move_x == -1:        self.moving_left  = True
                if move_y == 1:         self.moving_down  = True
                if move_y == -1:        self.moving_up    = True

        # 攻击玩家
        if self.attack:
            if (self.grid_x, self.grid_y) == self.player_pos:
                if self.try_bubble(self.grid_x, self.grid_y) == True:
                    self.space = True
                    self.make_bubble()
                    self.space = False
        # 炸箱子
        else:
            if (self.grid_x, self.grid_y) == self.box_pos:
                self.space = True
                self.make_bubble()
                self.space = False

个人感悟
我们希望游戏中的AI能够存活足够长的时间,并且能够在此基础上开辟通到玩家的路径,离玩家越来越近,当与玩家足够接近时尝试放泡泡将玩家炸死。在AI的设计初期,为使AI能达到比较理想的状态,需要首先明确AI的决策。在经过了较长时间的思考和观察后,我最终决定使用先算路径后移动、先自保后攻击的基本思路。在这个思路下的AI,能够存活最长的时间,给玩家更好的体验。在实现过程中,最大的难点在于如何找路,在思考了许多地图路径算法后,基于性能、Debug方面的考虑,最终我使用了BFS的算法,也取得了不错的效果。在AI基本成型后,由于地图因素的扩展,出现了许多待解决的问题,并且由于AI始终处于运动的状态,关于AI的变量时刻在变化着,Debug也是件不容易的事情。通过长时间的观察和思考,我添加了一些限制AI活动的功能,使得AI在寻找路径和移动的过程中更加“谨慎”,最终大大延长了AI的存活时间,取得了不错的效果。

猜你喜欢

转载自blog.csdn.net/AbyssalSeaa/article/details/81179129
AI