泡泡堂游戏开发 (Python Project)

源码链接
–> 泡泡堂游戏GitHub地址,记得随手给个star~

概述

在本次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 = 10for 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] = Trueself.record_count[i][j] = 0else:self.record_count[i][j] += 1else:self.record[i][j] = Falseself.record_count[i][j] = 0for 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] = Falseself.record_count[i][j - k] = 0if j + k <= self.screen_y:self.record[i][j + k] = Falseself.record_count[i][j + k] = 0if i - k >= 1:self.record[i - k][j] = Falseself.record_count[i - k][j] = 0if i + k <= self.screen_x:self.record[i + k][j] = Falseself.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):breakelif i == 1 and not self.check_f1(x + j, y):breakelif i == 2 and not self.check_f1(x, y - j):breakelif i == 3 and not self.check_f1(x, y + j):breakif 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):breakelif i == 1 and not self.check_record(x + j, y):breakelif i == 2 and not self.check_record(x, y - j):breakelif i == 3 and not self.check_record(x, y + j):breakif 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 Truefor 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 Truefor 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 Truefor 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 Falseif 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 Truereturn 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):"""BFSoption = 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 = 0if option == "JudgeReachable":self.reachable = Falsewhile queue:search_count += 1if search_count > self.max_search_range:returncur = 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 = Truereturnelif option == "EvadeBubble":if self.record[cur[0]][cur[1]]:self.path = temp_grid[cur[0]][cur[1]]returnelif option == "FindPlayer":if cur == self.player_pos:self.path = temp_grid[cur[0]][cur[1]]returnelif option == "FindBox":for ne in next:if not (1 <= ne[0] <= self.screen_x and 1 <= ne[1] <= self.screen_y):continueif (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]):continueself.path = temp_grid[cur[0]][cur[1]]self.box_pos = curreturnelif option == "PushBox":for ne in next:if not (1 <= ne[0] <= self.screen_x and 1 <= ne[1] <= self.screen_y):continueif 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):continueif 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):continueif visited[ne[0]][ne[1]]:continueif 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 = 0for des in self.path:# 只需取path开头部分移动path_count += 1if path_count >= 3:    breakif (self.grid_x, self.grid_y) == des:self.moving_left  = Falseself.moving_right = Falseself.moving_down  = Falseself.moving_up      = Falseelse:move_x, move_y = des[0] - self.grid_x, des[1] - self.grid_yif move_x == 1:            self.moving_right = Trueif move_x == -1:     self.moving_left  = Trueif move_y == 1:          self.moving_down  = Trueif 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 = Trueself.make_bubble()self.space = False# 炸箱子else:if (self.grid_x, self.grid_y) == self.box_pos:self.space = Trueself.make_bubble()self.space = False

个人感悟

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

泡泡堂游戏开发 (Python Project)相关推荐

  1. python新冠病毒泡泡堂游戏

    python新冠病毒泡泡堂游戏,音乐,素材,可执行文件 等下载网址: 链接:https://pan.baidu.com/s/1F359-kUuGHYznrjXPfZoxw 提取码:fne9 pytho ...

  2. 小伙熬夜用C++开发泡泡堂游戏,网友直呼:666!

    VC++ 基于ASL引擎开发的泡泡堂游戏,单机版游戏,演示了ASL引擎的各个主要模块的使用方法.模拟了盛大公司的同名游戏,实现其大部分功能.支持大物体.动画物体,支持小龟.猫头鹰.飞碟等坐骑,支持草丛 ...

  3. 小伙利用VC++打造泡泡堂游戏,遭粉丝疯狂打call

    VC++ 基于ASL引擎开发的泡泡堂游戏,单机版游戏,演示了ASL引擎的各个主要模块的使用方法.模拟了盛大公司的同名游戏,实现其大部分功能.支持大物体.动画物体,支持小龟.猫头鹰.飞碟等坐骑,支持草丛 ...

  4. 基于Java+Swing+Socket实现泡泡堂游戏

    基于Java+Swing+Socket实现泡泡堂游戏 一.功能展示 1.游戏登陆 2.房间 3.对战 二.代码展示 三.其他系统 前言 <泡泡堂>是由韩国游戏公司Nexon开发的一款休闲游 ...

  5. 基于C++的泡泡堂游戏设计与实现

    资源下载地址:https://download.csdn.net/download/sheziqiong/85631048 本游戏选题参考: 泡泡堂 游戏运行前准备: Mac平台环境搭建: 如果您已经 ...

  6. 泡泡堂段王一进去服务器不稳定,为什么泡泡堂游戏进入不了,只是点了选择了三区,然后就什么也没有了,过半天出来一个服务器连接中断...

    为什么泡泡堂游戏进入不了,只是点了选择了三区,然后就什么也没有了,过半天出来一个服务器连接中断 来源:互联网  宽屏版  评论 2009-03-24 03:38:20 分类: 电脑/网络 >&g ...

  7. 制作一个能够自动和人一起玩泡泡堂游戏的软件

    制作一个能够自动和人一起玩泡泡堂游戏的软件 1.允许软件犯错,因为人也会犯错,这样,设计起来才容易 2.必须有学习功能,就是玩过一段时间后,明显感觉到技能提高 3.对于印象程度深的事件,综合出能够遵照 ...

  8. [游戏开发]Python打表工具系列 [第二篇] [打表流程描简述]

    [上一篇链接] [游戏开发]Python打表工具系列 [第一篇][IDE开发环境部署] VSCode Python环境调试_Little丶Seven的博客-CSDN博客 [前言] 第二篇文章是对流程的 ...

  9. 泡泡堂AI,泡泡堂游戏录制与播放开发心得

    经过了一个月的有间断的研究和开发,我的泡泡堂从只有漏洞百出的AI版,到可以编辑地图的1.1版,最后到修正和提升了AI和改善了泡泡堂显示界面,并且加入泡泡堂录制和播放功能的泡泡堂1.2版终于面世了.也许 ...

最新文章

  1. 高一升学计算机,(有答案)2016年上学期高一年级对口升学第一次月考计算机应用试题资料讲解(9页)-原创力文档...
  2. 将shp文件导入到GeoDatabase中
  3. python有哪些方向、应该怎么学-终于找到深圳学Python,有几个方向?怎么从一个方向学到底...
  4. 36. Valid Sudoku数独判断
  5. STM32单片机,禁止系统启动时的变量初始化
  6. UNWAVERING SPIRIT AND VALUES
  7. 洛谷 - P2761 软件补丁问题(spfa+状压)
  8. oracle中多个数据库连接池,数据库连接池为什么要建立多个连接
  9. 【干货】借助用户画像解决电商业务问题.pdf(附下载链接)
  10. 一键刷入twrp工具_OPPORealme X 刷入原生lineage16-AOSP纯净系统完美ROOT 刷机必备
  11. Centos 6.5下NIS服务安装配置
  12. Core Java-多线程-线程的生命周期
  13. js产生两个数字之间的随机数
  14. API函数大全(转载)
  15. OriginPro 2021 设置成中文(软件自带)
  16. 使用Python开发游戏运行脚本(三)图片查找
  17. 【Zoomit】的安装及使用方法
  18. 语义分割--(DFN)Learning a Discriminative Feature Network for Semantic Segmentation
  19. 【技巧】Excel序号设置自动更新
  20. 一文读懂MES系统生产调度管理功能

热门文章

  1. SpringBean的常见面试题
  2. 《云集的全链路压测之路》
  3. 第三方调用NC外部交换平台 业务单据保存即审批 无审批人
  4. 游戏感:虚拟感觉的游戏设计师指南——第五章 不再靠直觉:游戏感的测量方法
  5. 三星的芯片代工业务增速最快,成为台积电的强劲对手
  6. 使用dom4j解析xml工具类
  7. 纪录MAC adb链接网易mumu模拟器
  8. 在mysql中显示数据库数据类型_MySQL(二) 数据库数据类型详解
  9. Flutter 垂直居中
  10. 2021李宏毅机器学习笔记--22 Generative Adversarial Network 01