在迷宮算法總結篇中我總結了生成迷宮的四種算法,在這一篇文章裏面我側重迷宮遊戲的實現以及可視化。
使用python3中的GUI繪圖庫tkinter實現了一個簡陋版的迷宮遊戲,地圖截圖如下圖所示。
爲了降低遊戲的難度,在尋找路徑的過程中你可以通過點擊圖中的空白點,生成從起點到你點擊位置的路徑。迷宮的難度主要由以下幾個參數決定:
- 迷宮的長和寬:尺寸越大,生成的地圖越難
- 迷宮生成算法:地圖難度:kruskal算法 > 隨機深度優先算法 > prim算法 > 遞歸分割算法。
目前已經開發到版本1.0.7,已經實現的功能有:
當前版本代碼在Maze-game-v1.0.7.
- 遊戲背景音樂
- 遊戲難度遞增
- 增加狀態欄顯示狀態信息
- 作弊(查看提示)增加懲罰分數(當前作弊一次懲罰20分)
- 保存讀取地圖
- 菜單欄,可用於設置地圖生成算法,地圖尺寸等
- 增加迷霧模式
- 增加生存模式,參考Roguelike Vision Algorithms豐富可玩性
- 顯示等級以及當前移動步數
- 隨機生成遊戲地圖
- 按方向鍵後自動前進倒退(到分岔路停止)
- 起點到任意位置輔助路徑顯示(鼠標左鍵單擊空白地方顯示路線) 移動次數計數
- 到達終點後通關,按任意鍵進入下一關(目前沒有難度設置,難度相同)
出現過的bug列表:
-
到達終點後上下左右鍵仍然可用且不會進入到下一關
已解決:修改進入下一關的邏輯 -
畫圖很慢,不知道是我代碼寫的垃圾還是這個python的tkinter庫本身的問題[狗頭]
已解決:刷新也面前調用Canvas.delete(“all”)清除之前繪製的所有內容再畫新的頁面即可。 -
通關後生成的地圖繪製的就更慢了,按一次方向鍵後要等一年[狗頭]
已解決:同上
最新版本截圖
1 簡單模式
2 迷霧模式
詳細的介紹以及說明後續補充。現在提供1.0.3版本源碼清單如下,爲了方便代碼管理,後續版本代碼將會直接上傳到Github,目前已經更新到v1.0.7,最新版本代碼Maze-game-v1.0.7.
- 迷宮類 Maze:
├ print_matrix
├ generate_matrix_dfs
├ generate_matrix_prim
├ generate_matrix_kruskal
├ generate_matrix_split
├ find_path_dfs
└ find_path_bfs (TODO) - 並查集類 UnionSet:
├ find
└ union - 可視化:
├ draw_cell
├ draw_path
├ draw_maze
├ check_reach
├ _eventHandler
├ _paint
├ _reset
└ update_maze
迷宮算法源碼 mazeGenerator.py:
import numpy as np
import time
import random
import copy
class UnionSet(object):
"""
並查集實現,構造函數中的matrix是一個numpy類型
"""
def __init__(self, arr):
self.parent = {pos: pos for pos in arr}
self.count = len(arr)
def find(self, root):
if root == self.parent[root]:
return root
return self.find(self.parent[root])
def union(self, root1, root2):
self.parent[self.find(root1)] = self.find(root2)
class Maze(object):
"""
迷宮生成類
"""
def __init__(self, width = 11, height = 11):
assert width >= 5 and height >= 5, "Length of width or height must be larger than 5."
self.width = (width // 2) * 2 + 1
self.height = (height // 2) * 2 + 1
self.start = [1, 0]
self.destination = [self.height - 2, self.width - 1]
self.matrix = None
self.path = []
def print_matrix(self):
matrix = copy.deepcopy(self.matrix)
for p in self.path:
matrix[p[0]][p[1]] = 1
for i in range(self.height):
for j in range(self.width):
if matrix[i][j] == -1:
print('□', end = '')
elif matrix[i][j] == 0:
print(' ', end = '')
elif matrix[i][j] == 1:
print('■', end = '')
print('')
def generate_matrix_dfs(self):
# 地圖初始化,並將出口和入口處的值設置爲0
self.matrix = -np.ones((self.height, self.width))
self.matrix[self.start[0], self.start[1]] = 0
self.matrix[self.destination[0], self.destination[1]] = 0
visit_flag = [[0 for i in range(self.width)] for j in range(self.height)]
def check(row, col, row_, col_):
temp_sum = 0
for d in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
temp_sum += self.matrix[row_ + d[0]][col_ + d[1]]
return temp_sum <= -3
def dfs(row, col):
visit_flag[row][col] = 1
self.matrix[row][col] = 0
if row == self.start[0] and col == self.start[1] + 1:
return
directions = [[0, 2], [0, -2], [2, 0], [-2, 0]]
random.shuffle(directions)
for d in directions:
row_, col_ = row + d[0], col + d[1]
if row_ > 0 and row_ < self.height - 1 and col_ > 0 and col_ < self.width - 1 and visit_flag[row_][col_] == 0 and check(row, col, row_, col_):
if row == row_:
visit_flag[row][min(col, col_) + 1] = 1
self.matrix[row][min(col, col_) + 1] = 0
else:
visit_flag[min(row, row_) + 1][col] = 1
self.matrix[min(row, row_) + 1][col] = 0
dfs(row_, col_)
dfs(self.destination[0], self.destination[1] - 1)
self.matrix[self.start[0], self.start[1] + 1] = 0
# 雖然說是prim算法,但是我感覺更像隨機廣度優先算法
def generate_matrix_prim(self):
# 地圖初始化,並將出口和入口處的值設置爲0
self.matrix = -np.ones((self.height, self.width))
def check(row, col):
temp_sum = 0
for d in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
temp_sum += self.matrix[row + d[0]][col + d[1]]
return temp_sum < -3
queue = []
row, col = (np.random.randint(1, self.height - 1) // 2) * 2 + 1, (np.random.randint(1, self.width - 1) // 2) * 2 + 1
queue.append((row, col, -1, -1))
while len(queue) != 0:
row, col, r_, c_ = queue.pop(np.random.randint(0, len(queue)))
if check(row, col):
self.matrix[row, col] = 0
if r_ != -1 and row == r_:
self.matrix[row][min(col, c_) + 1] = 0
elif r_ != -1 and col == c_:
self.matrix[min(row, r_) + 1][col] = 0
for d in [[0, 2], [0, -2], [2, 0], [-2, 0]]:
row_, col_ = row + d[0], col + d[1]
if row_ > 0 and row_ < self.height - 1 and col_ > 0 and col_ < self.width - 1 and self.matrix[row_][col_] == -1:
queue.append((row_, col_, row, col))
self.matrix[self.start[0], self.start[1]] = 0
self.matrix[self.destination[0], self.destination[1]] = 0
# 遞歸切分算法,還有問題,現在不可用
def generate_matrix_split(self):
# 地圖初始化,並將出口和入口處的值設置爲0
self.matrix = -np.zeros((self.height, self.width))
self.matrix[0, :] = -1
self.matrix[self.height - 1, :] = -1
self.matrix[:, 0] = -1
self.matrix[:, self.width - 1] = -1
# 隨機生成位於(start, end)之間的偶數
def get_random(start, end):
rand = np.random.randint(start, end)
if rand & 0x1 == 0:
return rand
return get_random(start, end)
# split函數的四個參數分別是左上角的行數、列數,右下角的行數、列數,牆壁只能在偶數行,偶數列
def split(lr, lc, rr, rc):
if rr - lr < 2 or rc - lc < 2:
return
# 生成牆壁,牆壁只能是偶數點
cur_row, cur_col = get_random(lr, rr), get_random(lc, rc)
for i in range(lc, rc + 1):
self.matrix[cur_row][i] = -1
for i in range(lr, rr + 1):
self.matrix[i][cur_col] = -1
# 挖穿三面牆得到連通圖,挖孔的點只能是偶數點
wall_list = [
("left", cur_row, [lc + 1, cur_col - 1]),
("right", cur_row, [cur_col + 1, rc - 1]),
("top", cur_col, [lr + 1, cur_row - 1]),
("down", cur_col, [cur_row + 1, rr - 1])
]
random.shuffle(wall_list)
for wall in wall_list[:-1]:
if wall[2][1] - wall[2][0] < 1:
continue
if wall[0] in ["left", "right"]:
self.matrix[wall[1], get_random(wall[2][0], wall[2][1] + 1) + 1] = 0
else:
self.matrix[get_random(wall[2][0], wall[2][1] + 1), wall[1] + 1] = 0
# self.print_matrix()
# time.sleep(1)
# 遞歸
split(lr + 2, lc + 2, cur_row - 2, cur_col - 2)
split(lr + 2, cur_col + 2, cur_row - 2, rc - 2)
split(cur_row + 2, lc + 2, rr - 2, cur_col - 2)
split(cur_row + 2, cur_col + 2, rr - 2, rc - 2)
self.matrix[self.start[0], self.start[1]] = 0
self.matrix[self.destination[0], self.destination[1]] = 0
split(0, 0, self.height - 1, self.width - 1)
# 最小生成樹算法-kruskal(選邊法)思想生成迷宮地圖,這種實現方法最複雜。
def generate_matrix_kruskal(self):
# 地圖初始化,並將出口和入口處的值設置爲0
self.matrix = -np.ones((self.height, self.width))
def check(row, col):
ans, counter = [], 0
for d in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
row_, col_ = row + d[0], col + d[1]
if row_ > 0 and row_ < self.height - 1 and col_ > 0 and col_ < self.width - 1 and self.matrix[row_, col_] == -1:
ans.append([d[0] * 2, d[1] * 2])
counter += 1
if counter <= 1:
return []
return ans
nodes = set()
row = 1
while row < self.height:
col = 1
while col < self.width:
self.matrix[row, col] = 0
nodes.add((row, col))
col += 2
row += 2
unionset = UnionSet(nodes)
while unionset.count > 1:
row, col = nodes.pop()
directions = check(row, col)
if len(directions):
random.shuffle(directions)
for d in directions:
row_, col_ = row + d[0], col + d[1]
if unionset.find((row, col)) == unionset.find((row_, col_)):
continue
nodes.add((row, col))
unionset.count -= 1
unionset.union((row, col), (row_, col_))
if row == row_:
self.matrix[row][min(col, col_) + 1] = 0
else:
self.matrix[min(row, row_) + 1][col] = 0
break
self.matrix[self.start[0], self.start[1]] = 0
self.matrix[self.destination[0], self.destination[1]] = 0
# 迷宮尋路算法dfs
def find_path_dfs(self, destination):
visited = [[0 for i in range(self.width)] for j in range(self.height)]
def dfs(path):
visited[path[-1][0]][path[-1][1]] = 1
if path[-1][0] == destination[0] and path[-1][1] == destination[1]:
self.path = path[:]
return
for d in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
row_, col_ = path[-1][0] + d[0], path[-1][1] + d[1]
if row_ > 0 and row_ < self.height - 1 and col_ > 0 and col_ < self.width and visited[row_][col_] == 0 and self.matrix[row_][col_] == 0:
dfs(path + [[row_, col_]])
dfs([[self.start[0], self.start[1]]])
if __name__ == '__main__':
maze = Maze(51, 51)
maze.generate_matrix_kruskal()
maze.print_matrix()
maze.find_path_dfs(maze.destination)
print("answer", maze.path)
maze.print_matrix()
迷宮可視化源碼 maze.py:
import tkinter as tk
from mazeGenerator import Maze
import time
import copy
import numpy as np
import math
import threading
def draw_cell(canvas, row, col, color="#F2F2F2"):
x0, y0 = col * cell_width, row * cell_width
x1, y1 = x0 + cell_width, y0 + cell_width
canvas.create_rectangle(x0, y0, x1, y1, fill = color, outline =color, width = 0)
def draw_path(canvas, matrix, row, col, color, line_color):
# 列
if row + 1 < rows and matrix[row - 1][col] >= 1 and matrix[row + 1][col] >= 1:
x0, y0 = col * cell_width + 2 * cell_width / 5, row * cell_width
x1, y1 = x0 + cell_width / 5, y0 + cell_width
# 行
elif col + 1 < cols and matrix[row][col - 1] >= 1 and matrix[row][col + 1] >= 1:
x0, y0 = col * cell_width, row * cell_width + 2 * cell_width / 5
x1, y1 = x0 + cell_width, y0 + cell_width / 5
# 左上角
elif col + 1 < cols and row + 1 < rows and matrix[row][col + 1] >= 1 and matrix[row + 1][col] >= 1:
x0, y0 = col * cell_width + 2 * cell_width / 5, row * cell_width + 2 * cell_width / 5
x1, y1 = x0 + 3 * cell_width / 5, y0 + cell_width / 5
canvas.create_rectangle(x0, y0, x1, y1, fill = color, outline = line_color, width = 0)
x0, y0 = col * cell_width + 2 * cell_width / 5, row * cell_width + 2 * cell_width / 5
x1, y1 = x0 + cell_width / 5, y0 + 3 * cell_width / 5
# 右上角
elif row + 1 < rows and matrix[row][col - 1] >= 1 and matrix[row + 1][col] >= 1:
x0, y0 = col * cell_width, row * cell_width + 2 * cell_width / 5
x1, y1 = x0 + 3 * cell_width / 5, y0 + cell_width / 5
canvas.create_rectangle(x0, y0, x1, y1, fill = color, outline = line_color, width = 0)
x0, y0 = col * cell_width + 2 * cell_width / 5, row * cell_width + 2 * cell_width / 5
x1, y1 = x0 + cell_width / 5, y0 + 3 * cell_width / 5
# 左下角
elif col + 1 < cols and matrix[row - 1][col] >= 1 and matrix[row][col + 1] >= 1:
x0, y0 = col * cell_width + 2 * cell_width / 5, row * cell_width
x1, y1 = x0 + cell_width / 5, y0 + 3 * cell_width / 5
canvas.create_rectangle(x0, y0, x1, y1, fill = color, outline = line_color, width = 0)
x0, y0 = col * cell_width + 2 * cell_width / 5, row * cell_width + 2 * cell_width / 5
x1, y1 = x0 + 3 * cell_width / 5, y0 + cell_width / 5
# 右下角
elif matrix[row - 1][col] >= 1 and matrix[row][col - 1] >= 1:
x0, y0 = col * cell_width, row * cell_width + 2 * cell_width / 5
x1, y1 = x0 + 3 * cell_width / 5, y0 + cell_width / 5
canvas.create_rectangle(x0, y0, x1, y1, fill = color, outline = line_color, width = 0)
x0, y0 = col * cell_width + 2 * cell_width / 5, row * cell_width
x1, y1 = x0 + cell_width / 5, y0 + 3 * cell_width / 5
else:
x0, y0 = col * cell_width + 2 * cell_width / 5, row * cell_width + 2 * cell_width / 5
x1, y1 = x0 + cell_width / 5, y0 + cell_width / 5
canvas.create_rectangle(x0, y0, x1, y1, fill = color, outline = line_color, width = 0)
def draw_maze(canvas, matrix, path, moves):
"""
根據matrix中每個位置的值繪圖:
-1: 牆壁
0: 空白
1: 參考路徑
2: 移動過的位置
"""
for r in range(rows):
for c in range(cols):
if matrix[r][c] == 0:
draw_cell(canvas, r, c)
elif matrix[r][c] == -1:
draw_cell(canvas, r, c, '#525288')
elif matrix[r][c] == 1:
draw_cell(canvas, r, c)
draw_path(canvas, matrix, r, c, '#bc84a8', '#bc84a8')
elif matrix[r][c] == 2:
draw_cell(canvas, r, c)
draw_path(canvas, matrix, r, c, '#ee3f4d', '#ee3f4d')
for p in path:
matrix[p[0]][p[1]] = 1
for move in moves:
matrix[move[0]][move[1]] = 2
def update_maze(canvas, matrix, path, moves):
canvas.delete("all")
matrix = copy.copy(matrix)
for p in path:
matrix[p[0]][p[1]] = 1
for move in moves:
matrix[move[0]][move[1]] = 2
row, col = movement_list[-1]
colors = ['#525288', '#F2F2F2', '#525288', '#F2F2F2', '#525288', '#F2F2F2', '#525288', '#F2F2F2']
if level > 2:
colors = ['#232323', '#252525', '#2a2a32', '#424242', '#434368', '#b4b4b4', '#525288', '#F2F2F2']
for r in range(rows):
for c in range(cols):
distance = (row - r) * (row - r) + (col - c) * (col - c)
if distance >= 100:
color = colors[0:2]
elif distance >= 60:
color = colors[2:4]
elif distance >= 30:
color = colors[4:6]
else:
color = colors[6:8]
if matrix[r][c] == 0:
draw_cell(canvas, r, c, color[1])
elif matrix[r][c] == -1:
draw_cell(canvas, r, c, color[0])
elif matrix[r][c] == 1:
draw_cell(canvas, r, c, color[1])
draw_path(canvas, matrix, r, c, '#bc84a8', '#bc84a8')
elif matrix[r][c] == 2:
draw_cell(canvas, r, c, color[1])
draw_path(canvas, matrix, r, c, '#ee3f4d', '#ee3f4d')
def check_reach():
global next_maze_flag
if movement_list[-1] == maze.destination:
print("Congratulations! You reach the goal! The step used: {}".format(click_counter))
x0, y0 = width / 2 - 200, 30
x1, y1 = x0 + 400, y0 + 40
canvas.create_rectangle(x0, y0, x1, y1, fill = '#F2F2F2', outline ='#525288', width = 3)
canvas.create_text(width / 2, y0 + 20, text = "Congratulations! You reach the goal! Steps used: {}".format(click_counter), fill = "#525288")
next_maze_flag = True
def _eventHandler(event):
global movement_list
global click_counter
global next_maze_flag
global level
if not next_maze_flag and event.keysym in ['Left', 'Right', 'Up', 'Down']:
click_counter += 1
windows.title("Maze Level-{} Steps-{}".format(level, click_counter))
cur_pos = movement_list[-1]
ops = {'Left': [0, -1], 'Right': [0, 1], 'Up': [-1, 0], 'Down': [1, 0]}
r_, c_ = cur_pos[0] + ops[event.keysym][0], cur_pos[1] + ops[event.keysym][1]
if len(movement_list) > 1 and [r_, c_] == movement_list[-2]:
movement_list.pop()
while True:
cur_pos = movement_list[-1]
counter = 0
for d in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
r_, c_ = cur_pos[0] + d[0], cur_pos[1] + d[1]
if c_ >= 0 and maze.matrix[r_][c_] == 0:
counter += 1
if counter != 2:
break
movement_list.pop()
elif r_ < maze.height and c_ < maze.width and maze.matrix[r_][c_] == 0:
while True:
movement_list.append([r_, c_])
temp_list = []
for d in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
r__, c__ = r_ + d[0], c_ + d[1]
if c__ < maze.width and maze.matrix[r__][c__] == 0 and [r__, c__] != cur_pos:
temp_list.append([r__, c__])
if len(temp_list) != 1:
break
cur_pos = [r_, c_]
r_, c_ = temp_list[0]
update_maze(canvas, maze.matrix, maze.path, movement_list)
check_reach()
elif next_maze_flag:
next_maze_flag = False
movement_list = [maze.start]
click_counter = 0
maze.generate_matrix_kruskal()
maze.path = []
draw_maze(canvas, maze.matrix, maze.path, movement_list)
level += 1
def _paint(event):
x, y = math.floor((event.y - 1) / cell_width), math.floor((event.x - 1) / cell_width)
if maze.matrix[x][y] == 0:
maze.find_path_dfs([x, y])
update_maze(canvas, maze.matrix, maze.path, movement_list)
def _reset(event):
maze.path = []
update_maze(canvas, maze.matrix, maze.path, movement_list)
if __name__ == '__main__':
# 基礎參數
cell_width = 20
rows = 37
cols = 51
height = cell_width * rows
width = cell_width * cols
level = 1
click_counter = 0
next_maze_flag = False
windows = tk.Tk()
windows.title("Maze")
canvas = tk.Canvas(windows, background="#F2F2F2", width = width, height = height)
canvas.pack()
maze = Maze(cols, rows)
movement_list = [maze.start]
maze.generate_matrix_kruskal()
draw_maze(canvas, maze.matrix, maze.path, movement_list)
canvas.bind("<Button-1>", _paint)
canvas.bind("<Button-3>", _reset)
canvas.bind_all("<KeyPress>", _eventHandler)
windows.mainloop()
將以上兩個代碼分別保存到mazeGenerator.py 和maze.py,確保兩個py文件在同一文件夾下,運行maze.py即可。
(暫時 完)