代码实现(超级玛丽闯迷宫)
上一篇博客对这个游戏的整体框架以及算法的流程进行了比较详细的设计及分析;对必要的类和类的成员变量、类的方法进行了声明以及声明。这一篇博客主要来呈现相关代码。
目录
- 代码实现(超级玛丽闯迷宫)
- 一、迷宫定义(Map类)
- 1. 定义枚举类
- 2. 成员变量
- 3. 成员方法
- 二、随机生成迷宫(Prim算法)
- 1. prim算法生成迷宫流程
- 三、迷宫自动寻路(A*算法)
- 1. 定义结点
- 2. 子方法
- 3. 算法流程
- 四、游戏界面(PyQt5)
- 1. 成员变量
- 2. 构造函数
- 3. 子方法
一、迷宫定义(Map类)
要实现随机生成迷宫第一步当然是定义迷宫,定义迷宫的大小以及对迷宫进行获取、设置、重置、判断、打印等操作的方法。
# 定义地图类
class Map():
1. 定义枚举类
用于规范结点类型和墙的方位。
(1)NODE_TYPE(Enum): 结点的类型。
class NODE_TYPE(Enum):
NODE_EMPTY = 0,
NODE_BLOCK = 1,
NODE_START = 2,
NODE_END = 3,
(2)WALL_DIRECTION(Enum): 墙壁在当前结点的方位。
class WALL_DIRECTION(Enum):
WALL_LEFT = 0,
WALL_UP = 1,
WALL_RIGHT = 2,
WALL_DOWN = 3,
2. 成员变量
(1)width,height:定义迷宫的行数和列数。
(2)map:二维列表用来存放迷宫地图,坐标定义与数组索引相似,x为纵向,y为横向(与一般坐标系不同)。
二维列表中每一个元素的值代表不同的含义,分别如下:
① map[i][j] == 0:当前格为空格。
② map[i][j] == 1:当前格为墙壁。
③ map[i][j] == 2:当前格为起点。
④ map[i][j] == 3:当前格为终点。
⑤ map[i][j] == 4:当前格为自动寻路路径上的结点。
def __init__(self, width, height):
self.width = width
self.height = height
# 嵌套使用列表解析快速生成地图
self.map = [[0 for x in range(self.width)] for y in range(self.height)]
3. 成员方法
(1)getMap(self):返回当前对象的map[n][m]二位迷宫列表。
def getMap(self):
return self.map
(2)resetNode(self, value):设置map[n][m]二位迷宫列表的每一个元素为value值。
def resetNode(self, value):
for x in range(self.height):
for y in range(self.width):
self.setNode(x, y, value)
(3)setNode(self, x, y, value):设置map[x][y] = value。
def setNode(self, x, y, value):
if value == NODE_TYPE.NODE_EMPTY:
self.map[x][y] = 0
elif value == NODE_TYPE.NODE_BLOCK:
self.map[x][y] = 1
elif value == NODE_TYPE.NODE_START:
self.map[x][y] = 2
elif value == NODE_TYPE.NODE_END:
self.map[x][y] = 3
(4)isWall(self, x, y):判断map[x][y] = 1 ?也就是判断是否是墙壁。
def isWall(self, x, y):
return self.map[x][y] == 1
(5)showMap(self):打印map[n][m]整个迷宫。
def showMap(self):
for row in self.map:
s = ''
for col in row:
if col == 0:
s += ' '
elif col == 1:
s += ' #'
elif col == 2:
s += ' S'
elif col == 3:
s += ' E'
print(s)
print(self.map)
二、随机生成迷宫(Prim算法)
1. prim算法生成迷宫流程
def randomPrim(map, width, height):
预处理部分:
(1)定义一个检查列表checklist存放为划分顶点集。
checklist = []
(2)把初始起点加入checklist列表中。
checklist.append((startX, startY)) # 把初始结点加入队列
主循环部分:
(3)重复下面几个步骤直到检查列表checklist为空:
# 如果列表不为空
while len(checklist):
① 随机取出checklist列表中的一个结点node1。
# 每次随机选择一个空结点
entry = choice(checklist)
② 检查这个结点node1周围有没有墙wall。
if checkAdjacentPos(map, entry[0], entry[1], width, height, checklist):
③ 有墙:从这个结点node1周围的墙中随机选一个,假设是wall_left,把这个墙置为空;并且把这个墙与node1之间的结点也置为空;最后把这个墙的位置坐标加入checklist中。
directions = []
if x > 0:
if map.isWall(2 * (x - 1) + 1, 2 * y + 1):
directions.append(WALL_DIRECTION.WALL_UP)
if y > 0:
if map.isWall(2 * x + 1, 2 * (y - 1) + 1):
directions.append(WALL_DIRECTION.WALL_LEFT)
if x < height - 1:
if map.isWall(2 * (x + 1) + 1, 2 * y + 1):
directions.append(WALL_DIRECTION.WALL_DOWN)
if y < width - 1:
if map.isWall(2 * x + 1, 2 * (y + 1) + 1):
directions.append(WALL_DIRECTION.WALL_RIGHT)
# 如果当前结点四周的相邻结点有墙
if len(directions):
# 随机选一个墙
direction = choice(directions)
if direction == WALL_DIRECTION.WALL_LEFT:
map.setNode(2 * x + 1, 2 * (y - 1) + 1, NODE_TYPE.NODE_EMPTY)
map.setNode(2 * x + 1, 2 * y, NODE_TYPE.NODE_EMPTY)
checklist.append((x, y - 1))
elif direction == WALL_DIRECTION.WALL_UP:
map.setNode(2 * (x - 1) + 1, 2 * y + 1, NODE_TYPE.NODE_EMPTY)
map.setNode(2 * x, 2 * y + 1, NODE_TYPE.NODE_EMPTY)
checklist.append((x - 1, y))
elif direction == WALL_DIRECTION.WALL_RIGHT:
map.setNode(2 * x + 1, 2 * (y + 1) + 1, NODE_TYPE.NODE_EMPTY)
map.setNode(2 * x + 1, 2 * (y + 1), NODE_TYPE.NODE_EMPTY)
checklist.append((x, y + 1))
elif direction == WALL_DIRECTION.WALL_DOWN:
map.setNode(2 * (x + 1) + 1, 2 * y + 1, NODE_TYPE.NODE_EMPTY)
map.setNode(2 * (x + 1), 2 * y + 1, NODE_TYPE.NODE_EMPTY)
checklist.append((x + 1, y))
④ 无墙:把node1从checklist中删除。
checklist.remove(entry)
(4)通过上述步骤后,迷宫即可随机生成。
三、迷宫自动寻路(A*算法)
1. 定义结点
为什么要定义结点:因为最后输出路径的时候需要从最后一个目标结点不断访问父节点来进行路径追溯,最后才能输出从源节点到目标结点的一条完整路径。
主要变量有:
- x、y坐标
- 沿当前路径到map[x][y]的路径长度(G值)
- 当前路径的F值(F = G + H)
- 父节点(用于路径回溯)
# 定义结点
class Node():
x = 0
y = 0
long = 0
score = 0
parent = []
2. 子方法
(1)pathEvaluate(val, fx, fy, ex, ey) :
计算当前路径代价(F = G + H)。
# 当前路径代价 = 当前路径长度 + 曼哈顿距离
def pathEvaluate(val, fx, fy, ex, ey):
return val + abs(ey - fy) + abs(ex - fx)
(2)findMin(open) :
返回open表中代价最小的结点。
# 返回open表中代价最小的结点
def findMin(open):
min_val = open[0]
for temp in open:
if temp.score < min_val.score:
min_val = temp
return min_val
(3)find_node(node, list):
判断某个结点在不在某个表中。
# 判断某个结点在不在某个表中
def find_node(node, list):
for i in range(0,list.__len__()):
if list[i].x == node.x and list[i].y == node.y:
return i
return -1
(4)AStar(map, row, col, sx, sy, ex, ey)方法:
完成A* 算法的主逻辑,执行过程中调用前面三个子方法。
# A*算法
def AStar(map, row, col, sx, sy, ex, ey):
3. 算法流程
定义与预处理部分:
(1)定义一个open开放列表,这里面的结点可能更新可能移除。
(2)定义一个close封闭列表 ,这里面的结点不需要处理。
(3)往open表中加入起点。
open = []
close = []
turn = [[0,1],[1,0],[0,-1],[-1,0]]
open.append(Node(sx, sy, 0, pathEvaluate(0, sx, sy, ex, ey), -1))
主循环部分:
(4)重复下面几个步骤直到检查列表checklist为空:
# 只要open表不为空就继续搜索
while len(open):
① 找到open表中代价最小的结点,把它从open表中取出并放入close表中。
# 找到open表中代价最小的结点
node = findMin(open)
# 删除open表中刚才找到的最小结点
open.remove(node)
# 在close表中加入刚才找到的最小结点
close.append(node)
② 判断这个结点是否是目标结点
# 如果目标结点是终点,返回path
if node.x == ex and node.y == ey:
③ 是:通过这个结点不断回溯到源节点生成一条路径,返回这个路径并退出整个AStar函数。
path = []
while node.parent != -1:
path.append((node.x, node.y))
node = node.parent
# path.append([node.x, node.y])
del(path[0])
path.reverse()
return path
④ 否:往相邻结点nx拓展搜索。
# 往相邻结点拓展搜索
for add in turn:
dx = node.x + add[0] # 目的结点横坐标
dy = node.y + add[1] # 目的结点纵坐标
dl = node.long + 1 # 目的路径长度
ds = pathEvaluate(dl, dx, dy, ex, ey) # 计算目标结点路径代价
d_node = Node(dx, dy, dl, ds, node)
⑤ 如果相邻结点nx:1.在close表里。2.是墙壁。3. 超出地图边界。就跳过这个相邻结点nx,直到这个结点的4个相邻结(n1、n2、n3、n4)点都判断过。
# 如果相邻结点1.在close表里。2.是墙壁。3. 超出地图边界。就不拓展。
if find_node(d_node, close) != -1 or map[dx][dy] == 1 or dx < 0 or dy < 0 or dx >= row or dy >= col:
continue
⑥ 如果相邻结点nx在open表中(注意只要 (x,y) 坐标相同就算相同),比较相邻结点nx和open表中结点的G移动代价,如果相邻结点n的G值比open表中相同结点的G值小,则更新open表中的相同结点的G值和F值,并且把open表中的相同结点的父节点设置为当前结点。
# 如果目标结点在open表中
pos = find_node(d_node, open)
if pos != -1:
t_node = open[pos]
if d_node.long < t_node.long:
open[pos].long = d_node
open[pos].score = pathEvaluate(open[pos].long, open[pos].x, open[pos].y, ex, ey)
open[pos].parent = node
⑦ 如果相邻结点nx不在open表中,直接把相邻结点nx加入open表中。
# 如果不在open表中,将当前结点加入open表
else:
open.append(d_node)
(4)通过上述步骤后,从源结点到目标结点的路径就生成了。
四、游戏界面(PyQt5)
1. 成员变量
① 窗口变量:设置窗口大小
# 窗口变量
width = 900
height = 900
② 地图变量:设置地图行列数、起点、终点、地图列表
# 地图变量
row = 7
col = 7
start_x = 1
start_y = 0
end_x = row - 2
end_y = col - 1
map = [] # 随机生成的迷宫
③ 玩家变量:定义玩家坐标
# 玩家变量
player_x = 1
player_y = 0
④ 游戏标志:定义各种游戏标志,如到第几关、是否通关、人物左右。
# 游戏标志
player_direction = 1 # 0代表人朝左,1代表人朝右
key_flag = 1 # 标志是否可以按键
level = 1 # 游戏关卡
paint_label = 1 # 画关卡
win_flag = 0 # 标记胜利
⑤ 音乐播放器:音乐播放器的初始化以及音乐的导入。
# 音乐播放器
pygame.mixer.init()
pygame.mixer.music.load(r"Music/Level 1.mp3") # 背景音乐
sound_grow = pygame.mixer.Sound(r"Music/grow.mp3") # 升级音效
sound_pass = pygame.mixer.Sound(r"Music/Pass.mp3") # 过关音效
sound_gold = pygame.mixer.Sound(r"Music/gold.mp3") # 金币音效
sound_gold.set_volume(0.2)
2. 构造函数
- 设置窗口标题
- 设置图标
- 设定游戏背景颜色
- 游戏窗口大小
- 让游戏窗口大小固定
- 初始化游戏
def __init__(self):
QWidget.__init__(self) # 调用父类的构造函数
self.setWindowTitle('Super Mario v1.0') # 设置窗口标题
self.setWindowIcon(QIcon("Picture/Logo.ico"))
self.setStyleSheet('background-color:#622C0D') # 设定游戏背景颜色
self.resize(self.width, self.height) # 游戏窗口大小
self.setFixedSize(self.width, self.height) # 让游戏窗口大小固定
# 初始化游戏
self.init()
3. 子方法
- init(self): 初始化游戏
# 初始化游戏
def init(self):
self.player_x = 1
self.player_y = 0
self.update_level() # 更新地图尺寸
self.map = Maze.run(self.row, self.col) # 更新地图
self.paint_label = 1
self.win_flag = 0
self.update()
self.key_flag = 1 # 标志是否可以按键
self.update_bgm()
- update_bgm(self): 更新背景音乐
# 更新背景音乐
def update_bgm(self):
pygame.mixer.music.stop()
if self.level == 1:
pygame.mixer.music.load(r"Music/Level 1.mp3") # 背景音乐
pygame.mixer.music.play(-1)
if self.level == 2:
pygame.mixer.music.load(r"Music/Level 2.mp3") # 背景音乐
pygame.mixer.music.play(-1)
if self.level == 3:
pygame.mixer.music.load(r"Music/Level 3.mp3") # 背景音乐
pygame.mixer.music.play(-1)
- update_level(self): 更新游戏关卡
# 更新游戏关卡
def update_level(self):
if self.level == 1:
self.row = 7
self.col = 7
self.setStyleSheet('background-color:#552408')
elif self.level == 2:
self.row = 15
self.col = 15
self.setStyleSheet('background-color:#034B4B')
elif self.level == 3:
self.row = 31
self.col = 31
self.setStyleSheet('background-color:#3F2706')
self.end_x = self.row - 2
self.end_y = self.col - 1
- showMap(self): 打印地图
# 打印地图
def showMap(self):
print("start(%d, %d)" % (self.start_x, self.start_y))
print("end(%d, %d)" % (self.end_x, self.end_y))
for row in self.map:
s = ''
for col in row:
if col == 0:
s += ' '
elif col == 1:
s += ' #'
elif col == 2:
s += ' S'
elif col == 3:
s += ' E'
elif col == 4:
s += ' G'
print(s)
- paintEvent(self, event): 绘图事件
# 绘图事件
def paintEvent(self, event):
QWidget.paintEvent(self, event)
painter = QPainter(self)
col_space = self.width / self.col
row_space = self.height / self.row
for i in range(0, self.col):
for j in range(0, self.row):
node = self.map[j][i]
if node == 1:
if self.level == 1:
painter.drawImage(QRectF(i * col_space, j * row_space, col_space, row_space),
QImage('Picture/wall.png'))
if self.level == 2:
painter.drawImage(QRectF(i * col_space, j * row_space, col_space, row_space),
QImage('Picture/wall_2.png'))
if self.level == 3:
painter.drawImage(QRectF(i * col_space, j * row_space, col_space, row_space),
QImage('Picture/wall_3.png'))
if node == 2:
pic = ''
if self.level == 1:
if self.player_direction == 1:
pic = 'Picture/player1_right.png'
else:
pic = 'Picture/player1_left.png'
else:
if self.player_direction == 1:
pic = 'Picture/player2_right.png'
else:
pic = 'Picture/player2_left.png'
painter.drawImage(QRectF(self.player_y * col_space, self.player_x * row_space, col_space, row_space),
QImage(pic))
if node == 3:
pic = ''
if self.level == 1:
pic = 'Picture/mushroom'
elif self.level == 2:
pic = 'Picture/star'
elif self.level == 3:
pic = 'Picture/water_pipe'
painter.drawImage(QRectF(self.end_y * col_space, self.end_x * row_space, col_space, row_space),
QImage(pic))
if node == 4:
painter.drawImage(QRectF(i * col_space, j * row_space, col_space, row_space),
QImage('Picture/gold.png'))
# 画关卡
if self.paint_label == 1:
painter.setPen(QColor(255, 255, 255))
painter.setFont(QFont('Apple ][', 40))
if self.level == 1:
painter.drawText(self.width / 2 - 220, self.height / 2, "Level 1")
elif self.level == 2:
painter.drawText(self.width / 2 - 220, self.height / 2, "Level 2")
elif self.level == 3:
painter.drawText(self.width / 2 - 220, self.height / 2, "Level 3")
# 画胜利标志
if self.win_flag == 1:
painter.setPen(QColor(255, 255, 255))
painter.setFont(QFont('Apple ][', 60))
painter.drawText(self.width / 2 - 180, self.height / 2, "WIN!")
painter.setFont(QFont('Apple ][', 12))
painter.drawText(self.width / 2 - 350, self.height / 2 + 80, "Please press 'R' to restart the game")
- sound(self): 播放音效
# 播放音效
def sound(self):
pygame.mixer.music.stop()
if self.level == 1 or self.level == 2:
self.sound_grow.play()
self.key_flag = 0
time.sleep(1)
elif self.level == 3:
self.sound_pass.play()
self.key_flag = 0
self.map[self.player_x][self.player_y] = 3
self.win_flag = 1
self.repaint()
time.sleep(6)
- checkPass(self): 判断通关
# 判断通关
def checkPass(self):
if self.player_x == self.row - 2 and self.player_y == self.col - 1:
self.sound()
if self.level != 3:
self.level += 1
self.init()
- findpath(self): 自动寻路
# 自动寻路
def findpath(self):
path = AStar(self.map, self.row, self.col, self.player_x, self.player_y, self.end_x, self.end_y)
for temp in path:
self.map[temp[0]][temp[1]] = 4
self.update()
- keyPressEvent(self, event): 键盘事件
# 键盘事件
def keyPressEvent(self, event):
QWidget.keyPressEvent(self, event)
key = event.key()
if key == Qt.Key_F and self.win_flag != 1:
self.findpath()
# 重新生成地图
if key == Qt.Key_R:
self.map = Maze.run(self.row, self.col)
self.level = 1
self.init()
# 退出游戏
if key == Qt.Key_Escape:
self.close()
# 判断是否能移动
if self.key_flag == 0:
return
# 上移
if key == Qt.Key_Up or key == Qt.Key_W:
dest_up = self.player_x - 1
if dest_up >= 0 and self.map[dest_up][self.player_y] != 1:
if self.map[dest_up][self.player_y] == 4:
self.sound_gold.play()
self.map[self.player_x][self.player_y] = 0
self.player_x = dest_up
self.map[self.player_x][self.player_y] = 2
self.update()
# 下移
if key == Qt.Key_Down or key == Qt.Key_S:
dest_down = self.player_x + 1
if dest_down < self.row and self.map[dest_down][self.player_y] != 1:
if self.map[dest_down][self.player_y] == 4:
self.sound_gold.play()
self.map[self.player_x][self.player_y] = 0
self.player_x = dest_down
self.map[self.player_x][self.player_y] = 2
self.update()
# 左移
if key == Qt.Key_Left or key == Qt.Key_A:
self.player_direction = 0
dest_left = self.player_y - 1
if dest_left >= 0 and self.map[self.player_x][dest_left] != 1:
if self.map[self.player_x][dest_left] == 4:
self.sound_gold.play()
self.map[self.player_x][self.player_y] = 0
self.player_y = dest_left
self.map[self.player_x][self.player_y] = 2
self.update()
# 右移
if key == Qt.Key_Right or key == Qt.Key_D:
self.player_direction = 1
self.paint_label = 0
dest_right = self.player_y + 1
if dest_right < self.col and self.map[self.player_x][dest_right] != 1:
if self.map[self.player_x][dest_right] == 4:
self.sound_gold.play()
self.map[self.player_x][self.player_y] = 0
self.player_y = dest_right
self.map[self.player_x][self.player_y] = 2
if self.level == 3:
self.update()
else:
self.repaint()
self.checkPass()
从理解Prim算法和A* 算法到自己实现这个算法需要不断地尝试、调试,在这个过程中又会进一步加深对这些算法的理解。想法往往是简单的,而实现是复杂的,想得到的不一定实现的了。
———2020.12.21(罗涵)
THE END