基于A*搜索算法迷宫游戏开发
一、前言
迷宫游戏是非常经典的游戏。要求:
- 在该题中随机生成一个迷宫,并求解迷宫;
- 游戏支持玩家走迷宫,和系统走迷宫路径两种模式。玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫路径要求基于A*算法实现,输出走迷宫的最优路径并显示。
- 设计交互友好的游戏图形界面。
二、基本流程
思路梳理
三、迷宫随机生成
随机生成迷宫可以用到多种算法,包括深度优先遍历、随机prim算法、递归回溯算法等等。这里我选用了随机prim算法。
原始版本的随机Prim算法是维护一个墙的列表。
- 随机选择一个迷宫单元作为起点,加入检查列表并标记为已访问
- 当检查列表非空时,随机从列表中取出一个迷宫单元,进行循环
①如果当前迷宫单元有未被访问过的相邻迷宫单元
a. 随机选择一个未访问的相邻迷宫单元
b. 去掉当前迷宫单元与相邻迷宫单元之间的墙
c. 标记相邻迷宫单元为已访问,并将它加入检查列表
②否则,当前迷宫单元没有未访问的相邻迷宫单元,则从检查列表删除当前迷宫单元
设置基本参数
def setMap(self, x, y, value):
if value == MAP_ENTRY_TYPE.MAP_EMPTY:
self.map[y][x] = 0
elif value == MAP_ENTRY_TYPE.MAP_BLOCK:
self.map[y][x] = 1
elif value == MAP_ENTRY_TYPE.MAP_START:
self.map[y][x] = 2
elif value == MAP_ENTRY_TYPE.MAP_END:
self.map[y][x] = 3
判断是否已经访问过(访问过则代表不设定为墙
def isVisited(self, x, y):
return self.map[y][x] != 1
如果当前位置四周的相邻结点有墙
if len(directions):
# 随机选择一个方向的墙
direction = choice(directions)
if direction == WALL_DIRECTION.WALL_LEFT:
map.setMap(2 * (x - 1) + 1, 2 * y + 1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2 * x, 2 * y + 1, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x - 1, y))
elif direction == WALL_DIRECTION.WALL_UP:
map.setMap(2 * x + 1, 2 * (y - 1) + 1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2 * x + 1, 2 * y, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x, y - 1))
elif direction == WALL_DIRECTION.WALL_RIGHT:
map.setMap(2 * (x + 1) + 1, 2 * y + 1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2 * x + 2, 2 * y + 1, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x + 1, y))
elif direction == WALL_DIRECTION.WALL_DOWN:
map.setMap(2 * x + 1, 2 * (y + 1) + 1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2 * x + 1, 2 * y + 2, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x, y + 1))
return True
else:
# 找不到四周有未经访问的墙(即周边的墙都已经访问过
return False
随机prim算法
def randomPrim(map, width, height):
# 起始点的设置
startX, startY = (0, 0)
map.setMap(2*startX+0, 2*startY+1, MAP_ENTRY_TYPE.MAP_START)
# 终点的设置
endX, endY = (width, height)
map.setMap(2*endX-0, 2*endY-1, MAP_ENTRY_TYPE.MAP_END)
checklist = []
checklist.append((startX, startY))
while len(checklist):
# 从checklist中选择一个随机方向的墙
entry = choice(checklist)
if not checkAdjacentPos(map, entry[0], entry[1], width, height, checklist):
# 如果四周的墙都已访问过,则把该结点位置从checklist中删除
checklist.remove(entry)
考虑到后续键盘移动迷宫和自动寻路迷宫都需要地图,我将此算法生成的地图以“ . ”、“ # ”分别标记为通路和墙壁,以及S作为起始点而E作为终点,并导出生成一个txt文件。如下
四、移动迷宫与尾迹生成
首先复制txt文件中的内容,赋给map数组。
由于要用到键盘控制移动,使用了turtle库进行绘图,并使用三张图片绘制迷宫的基本元素,wall是迷宫的墙体,pr作为当前移动的位置,la为迷宫移动的尾迹。
1. 绘制迷宫
def make_maze(self):
level = levels[current_level - 1]
for i in range(len(level)):
# 取出某一行
row = level[i]
# 获取到某一元素的坐标
for j in range(len(row)):
screen_x = -244 + 12 * j
screen_y = 185 - 12 * i
char = row[j]
#如果元素为X,则画出迷宫
if char == '#':
self.goto(screen_x,screen_y)
self.stamp()
walls.append((screen_x,screen_y))
elif char == 'S':
player.goto(screen_x,screen_y)
player.st()
2. 键盘响应
mz.listen()
mz.onkey(player.go_right, 'Right')
mz.onkey(player.go_left, 'Left')
mz.onkey(player.go_up, 'Up')
mz.onkey(player.go_down, 'Down')
3. 当前位置的移动以及尾迹生成
class Player(t.Turtle):
#当前位置的移动
def __init__(self):
super().__init__()
# 先隐藏起来,隐秘进行运动
self.ht()
self.shape('images/pr.gif')
self.speed(0)
self.penup()
# 右移
def go_right(self):
# print('going right')
self.shape('images/la.gif')
self.stamp()
go_x = self.xcor() + 12
go_y = self.ycor()
self.shape('images/pr.gif')
self.move(go_x, go_y)
# 左移
def go_left(self):
# print('going left')
self.shape('images/la.gif')
self.stamp()
go_x = self.xcor() - 12
go_y = self.ycor()
self.shape('images/pr.gif')
self.move(go_x,go_y)
# 上移
def go_up(self):
# print('going up')
self.shape('images/la.gif')
self.stamp()
go_x = self.xcor()
go_y = self.ycor() + 12
self.shape('images/pr.gif')
self.move(go_x, go_y)
# 下移
def go_down(self):
# print('going down')
self.shape('images/la.gif')
self.stamp()
go_x = self.xcor()
go_y = self.ycor() - 12
self.shape('images/pr.gif')
self.move(go_x, go_y)
# 运动时的共同行为
def move(self, go_x, go_y):
if (go_x, go_y) not in walls:
self.goto(go_x, go_y)
else:
print('撞墙了')
4. 运行效果
五、A*迷宫自动寻路
1. 关键名词解释
- open集:包含已搜索到的待检测结点
- close集:包含已检测的结点
- G:从开始结点到当前结点的移动量
- H:当前结点到目标结点的移动量估计值
4邻域的曼哈顿距离
8邻域的对角线距离 - F:G + H
- 父节点指针:每个结点都有,确定追踪关系
2. 主循环
- 从open集中取一个最优结点n(F最小)来检测
- 将结点n从open集中移除,加入close集
- n若是目标结点,则算法结束
- 否则尝试添加n结点的所有邻结点n‘
①邻结点在close集中,已检测过则无需添加
②邻结点在open集中
a. 如果重新计算的G值比邻结点保存的G值小,则要更新这个邻结点的G值和F值,以及父节点
b. 否则不做操作
③否则将邻结点加入open集,并设置其父结点为n,以及设置G和F的值
3. 初始化地图
for i in range(self.width):
for j in range(self.height):
# 生成墙壁
if test_map[j][i] == '#':
node = self.canvas.create_rectangle(i * 10 + 50 - self.__r,
j * 10 + 50 - self.__r, i * 10 + 50 + self.__r,
j * 10 + 50 + self.__r,
fill="#000000", # 填充黑色
outline="#ffffff", # 轮廓白色
tags="node",
)
# 生成通路
if test_map[j][i] == '*':
node = self.canvas.create_rectangle(i * 10 + 50 - self.__r,
j * 10 + 50 - self.__r, i * 10 + 50 + self.__r,
j * 10 + 50 + self.__r,
fill="#00ff00", # 填充绿色
outline="#ffffff", # 轮廓白色
tags="node",
)
4. 核心代码
# 查找路径的入口函数
def find_path(self):
# 构建开始节点
p = Node_Elem(None, self.s_x, self.s_y, 0.0)
while True:
# 扩展节点
self.extend_round(p)
# 如果open表为空,则不存在路径,返回
if not self.open:
return
# 取F值最小的节点
idx, p = self.get_best()
# 到达终点,生成路径,返回
if self.is_target(p):
self.make_path(p)
return
# 把此节点加入close表,并从open表里删除
self.close.append(p)
del self.open[idx]
# 取F值最小的节点
def get_best(self):
best = None
bv = 10000000 # MAX值
bi = -1
for idx, i in enumerate(self.open):
value = self.get_dist(i)
if value < bv:
best = i
bv = value
bi = idx
return bi, best
# 求距离
def get_dist(self, i):
# F = G + H
# G 为当前路径长度,H为估计终点长度
return i.dist + math.sqrt((self.e_x - i.x) * (self.e_x - i.x)) + math.sqrt((self.e_y - i.y) * (self.e_y - i.y))
# 扩展节点
def extend_round(self, p):
# 上下左右四个方向移动
xs = (0, -1, 1, 0)
ys = (-1, 0, 0, 1)
for x, y in zip(xs, ys):
new_x, new_y = x + p.x, y + p.y
# 检查位置是否合法
if not self.is_valid_coord(new_x, new_y):
continue
# 构造新的节点,计算距离
node = Node_Elem(p, new_x, new_y, p.dist + self.get_cost(
p.x, p.y, new_x, new_y))
# 新节点在关闭列表,则忽略
if self.node_in_close(node):
continue
i = self.node_in_open(node)
# 新节点在open表
if i != -1:
# 当前路径距离更短
if self.open[i].dist > node.dist:
# 更新距离
self.open[i].parent = p
self.open[i].dist = node.dist
continue
# 否则加入open表
self.open.append(node)
5. 运行结果
按下enter键后自动寻路成功