迷宫游戏开发
一、实现目标
1.随机生成一个迷宫,并且求解迷宫。
2.要求游戏支持玩家走迷宫,和系统走迷宫路径两种模式。玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫路径要求基于A*算法实现,输出走迷宫的最优路径并显示。
3.设计交互友好的游戏图形界面。
二、实现方式
1.使用python和threading模块。我们进行程序开发的时候,肯定避免不了要处理并发的情况。一般并发的手段有采用多进程和多线程。但线程比进程更轻量化,系统开销一般也更低,所以大家更倾向于用多线程的方式处理并发的情况。Python 提供多线程编程的方式。threading 模块中最核心的内容是 Thread 这个类。我们要创建 Thread 对象,然后让它们运行,每个 Thread 对象代表一个线程,在每个线程中我们可以让程序处理不同的任务,这就是多线程编程。
参考:Python多线程编程(一):threading 模块 Thread 类的用法详解
2.使用随机Prim算法。
原始版本的随机Prim算法是维护一个墙的列表。
首先随机选择一个迷宫单元,设置为已访问,然后把它的所有邻墙放入列表。
当列表里还有墙时,重复下面循环
a)从列表里随机选择一面墙。
b)如果这面墙相邻的两个迷宫单元只有一个被访问过,先把这面墙设置为打通,并从列表里删除这面墙,然后把未访问的迷宫单元设为已访问,并把这个迷宫单元的邻墙添加到列表。
c)如果这面墙相邻的两个单元单元都已经被访问过,那就从列表里删除这面墙。
但是在实现时要同时记录墙和迷宫单元的信息,会比较复杂,所以使用改进版本,只维护一个迷宫单元的列表。地图信息和之前的递推回溯算法是一样的。
在迷宫生成算法中会用到这个表示方式(2 * x+1, 2 * y+1)。同时迷宫的长度和宽度必须为奇数。
算法主循环,重复下面步骤2直到检查列表为空:
①随机选择一个迷宫单元作为起点,加入检查列表并标记为已访问
②当检查列表非空时,随机从列表中取出一个迷宫单元,进行循环
a)如果当前迷宫单元有未被访问过的相邻迷宫单元
b)随机选择一个未访问的相邻迷宫单元
c)去掉当前迷宫单元与相邻迷宫单元之间的墙
d)标记相邻迷宫单元为已访问,并将它加入检查列表
e)否则,当前迷宫单元没有未访问的相邻迷宫单元
f)则从检查列表删除当前迷宫单元
参考:Python 四大迷宫生成算法实现(2): 随机Prim算法
三、主要函数
1.随机Prim算法
代码如下(示例):
from random import randint, choice
# 单元格类型
#0为路,1为墙
class CellType:
ROAD = 0
WALL = 1
# 墙的方向
class Direction:
LEFT = 0,
UP = 1,
RIGHT = 2,
DOWN = 3,
# 迷宫地图
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.maze = [[0 for x in range(self.width)] for y in range(self.height)]
def reset_maze(self, value):
for y in range(self.height):
for x in range(self.width):
self.set_maze(x, y, value)
def set_maze(self, x, y, value):
self.maze[y][x] = CellType.ROAD if value == CellType.ROAD else CellType.WALL
def visited(self, x, y):
return self.maze[y][x] != 1
def check_neighbors(maze, x, y, width, height, checklist):
directions = []
if x > 0:
if not maze.visited(2 * (x - 1) + 1, 2 * y + 1):
directions.append(Direction.LEFT)
if y > 0:
if not maze.visited(2 * x + 1, 2 * (y - 1) + 1):
directions.append(Direction.UP)
if x < width - 1:
if not maze.visited(2 * (x + 1) + 1, 2 * y + 1):
directions.append(Direction.RIGHT)
if y < height - 1:
if not maze.visited(2 * x + 1, 2 * (y + 1) + 1):
directions.append(Direction.DOWN)
if len(directions):
direction = choice(directions)
if direction == Direction.LEFT:
maze.set_maze(2 * (x - 1) + 1, 2 * y + 1, CellType.ROAD)
maze.set_maze(2 * x, 2 * y + 1, CellType.ROAD)
checklist.append((x - 1, y))
elif direction == Direction.UP:
maze.set_maze(2 * x + 1, 2 * (y - 1) + 1, CellType.ROAD)
maze.set_maze(2 * x + 1, 2 * y, CellType.ROAD)
checklist.append((x, y - 1))
elif direction == Direction.RIGHT:
maze.set_maze(2 * (x + 1) + 1, 2 * y + 1, CellType.ROAD)
maze.set_maze(2 * x + 2, 2 * y + 1, CellType.ROAD)
checklist.append((x + 1, y))
elif direction == Direction.DOWN:
maze.set_maze(2 * x + 1, 2 * (y + 1) + 1, CellType.ROAD)
maze.set_maze(2 * x + 1, 2 * y + 2, CellType.ROAD)
checklist.append((x, y + 1))
return True
return False
def random_prime(map, width, height):
start_x, start_y = (randint(0, width - 1), randint(0, height - 1))
map.set_maze(2 * start_x + 1, 2 * start_y + 1, CellType.ROAD)
checklist = [(start_x, start_y)]
while len(checklist):
entry = choice(checklist)
if not check_neighbors(map, entry[0], entry[1], width, height, checklist):
checklist.remove(entry)
def do_random_prime(map):
map.reset_maze(CellType.WALL)
random_prime(map, (map.width - 1) // 2, (map.height - 1) // 2)
def set_entrance_exit(maze):
entrance = []
for i in range(maze.height):
if maze.maze[i][1] == 0:
maze.set_maze(0, i, 0)
entrance = [0, i]
break
exit = []
for i in range(maze.height - 1, 0, -1):
if maze.maze[i][maze.width - 2] == 0:
maze.set_maze(maze.width - 1, i, 0)
exit = [maze.width - 1, i]
break
return entrance, exit
def generate_maze(width=21, height=21):
# 初始化迷宫
maze = Maze(width, height)
# 生成地图
do_random_prime(maze)
# 选择起点和终点
entrance, exit = set_entrance_exit(maze)
# 返回地图
return maze.maze, entrance, exit
2.迷宫入口
代码如下(示例):
if __name__ == '__main__':
# 生成迷宫与入口
size = random_maze_size()
maze, entrance, exit = generate_maze(size, size)
solve_thread = threading.Thread(target=solve_maze, args=(maze, entrance, exit, draw_maze))
solve_thread.start()
while True:
CLOCK.tick(FPS)
for event in pygame.event.get():
# 检查是否关闭窗口
if event.type == pygame.QUIT:
if solve_thread is not None and solve_thread.is_alive():
stop_thread(solve_thread)
solve_thread = None
exit(0)
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
dispatcher_click(mouse_pos)
3.迷宫地图
do_random_prime 函数 先调用reset_maze函数将地图都设置为墙,迷宫单元所在位置为墙表示未访问。有个注意点是地图的长宽和迷宫单元的位置取值范围的对应关系。
假如地图的宽度是21,长度是21,对应的迷宫单元的位置取值范围是 x(0,10), y(0,10), 因为迷宫单元(x,y)对应到地图上的位置是(2 * x+1, 2 * y+1)。
random_prime 函数就是上面算法主循环的实现。
代码如下(示例):
def random_prime(map, width, height):
start_x, start_y = (randint(0, width - 1), randint(0, height - 1))
map.set_maze(2 * start_x + 1, 2 * start_y + 1, CellType.ROAD)
checklist = [(start_x, start_y)]
while len(checklist):
entry = choice(checklist)
if not check_neighbors(map, entry[0], entry[1], width, height, checklist):
checklist.remove(entry)
def do_random_prime(map):
map.reset_maze(CellType.WALL)
random_prime(map, (map.width - 1) // 2, (map.height - 1) // 2)
def generate_maze(width=21, height=21):
# 初始化迷宫,注意长度和宽度为奇数
maze = Maze(width, height)
# 生成地图
do_random_prime(maze)
# 选择起点和终点
entrance, exit = set_entrance_exit(maze)
# 返回地图
return maze.maze, entrance, exit
4.迷宫绘制
代码如下(示例):
WHITE = (255, 255, 255) #道路
BLACK = (0, 0, 0) #障碍
RED = (255, 0, 0) #走过的路
GREEN = (0, 255, 0) #出口
YELLOW = (255, 255, 0) #最终的通路
获取玩家上下左右四个相邻位置的值和位置。
# 单元格类型
#0为路,1为墙,2是走过的路,3是死胡同,不在迷宫里或是已经走过又回退回来,说明此路不通
class CellType:
ROAD = 0
WALL = 1
WALKED = 2
DEAD = 3
# 墙的方向
class Direction:
LEFT = 0,
UP = 1,
RIGHT = 2,
DOWN = 3,
def valid(maze, x, y):
if x < 0 or y < 0:
return False
if x >= len(maze) or y >= len(maze):
return False
val = maze[y][x]
if val == CellType.WALL or val == CellType.DEAD:
return False
return val, x, y
def neighbors(maze, pos):
x, y = pos
t, r, d, l = valid(maze, x, y - 1), valid(maze, x + 1, y), valid(maze, x, y + 1), valid(maze, x - 1, y)
return t, r, d, l
def mark_walked(maze, pos):
maze[pos[1]][pos[0]] = CellType.WALKED
def mark_dead(maze, pos):
maze[pos[1]][pos[0]] = CellType.DEAD
对获取到的相邻四个位置进行处理,返回最推荐走的位置与值。
def suggest_pos(cells):
arr = []
# 遍历相邻4个位置
for cell in cells:
# 如果不是False,说明是路,可能是没走过的路,也可能是走过的路,添加到数组中
if cell:
arr.append(cell[0])
# 如果是False,说明是墙或者死胡同,不再考虑
else:
arr.append(CellType.DEAD)
# 优先推荐没走过的路,根据我们CellType中的定义,没走过的路值最小,其次是走过的路
return cells[arr.index(min(arr))]
对推荐的位置进行处理。
a)如果是没走过的路,直接走,并把当前的位置标记为已走过
b)如果是走过的路,也走,并把当前的位置标记为死胡同,此路不通
c)如果没有推荐的路,游戏结束,迷宫有问题,无解
d)走 这个动作,就用到了递归,继续调用solve_maze方法,位置参数改为推e)荐的下一步的位置
f)重复以上步骤直到当前位置与出口位置重叠,迷宫走完,游戏结束
next_pos = suggest_pos((t, r, d, l))
if next_pos:
if next_pos[0] == CellType.WALKED:
mark_dead(maze, pos)
else:
mark_walked(maze, pos)
callback(maze, next_pos)
return solve_maze(maze, (next_pos[1], next_pos[2]), end, callback)
else:
mark_dead(maze, pos)
callback(maze, next_pos)
return False
5.强行关闭线程
因为在迷宫中一直走找不到出口而使程序无法结束,这时候就需要一个强行关闭程序来中止。
参考:python强制关闭的一种办法
代码如下(示例):
import ctypes #用于调用动态链接库函数的功能模块
import inspect
def _async_raise(tid, exctype):
"""raises the exception, performs cleanup if needed""" #强行关闭线程
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def stop_thread(thread):
_async_raise(thread.ident, SystemExit)
四、示意图展示
随机生成的两个地图并自动寻找出口。