最近閒來無事想做一個質量高一點的進階版的迷宮小遊戲,首先就要先生成最基礎的迷宮地圖,因此學習並整理了一下迷宮生成算法。由於python更容易實現這些算法,因此首先使用pyhon將各種生成算法整理一遍,之後再用Qt或者javascript重寫一遍,再加上可視化。
目前已經使用python實現了一個簡單的可玩版本,可在迷宮遊戲python實現查看。
大概瞭解了一下,生成迷宮的算法主要有三種思路,其中最小生成樹算法又可以分爲選點法(prim)和選邊法(kruskal):
- 隨機深度優先算法
- 遞歸分割算法(TODO)
- 隨機prim最小生成樹算法
- *kruskal最小生成樹算法(使用並查集實現)
❓ BrainStorm:能否編程生成圓形的迷宮?如下圖所示,如果有讀者有思路,歡迎留言討論。
話不多說,進入正文。
1 隨機深度優先算法
之所以叫做隨機深度優先算法,是因爲傳統的深度優先算法中已經確定了遍歷上下左右四個方向的順序,因此每次生成的地圖都是一樣的,而且特別簡單。爲了增加地圖的隨機性,需要在每次遍歷上下左右四個方向的時候先將四個方向隨機打亂再進行遍歷。先看看隨機深度優先算法生成的地圖,如果dfs起點隨機選擇(我這選擇的是終點旁邊的點作爲dfs的起點)效果還是不錯的,結果如下圖:
在這裏總結幾個我在編程實現的過程中遇到的小問題:
- 如果設置了迷宮的起點start(我設置的是左上角)和終點dest(我設置的是右下角),建議將深度優先遍歷函數的起點設置爲dest,將遞歸結束的條件設置爲到達start。這樣的好處是:能夠增加遊戲的難度,具體原因自行體會。
- 將迷宮的長寬(包括邊框)設置爲奇數,每次往前遍歷前進的距離爲2而不是1,這樣能確保生成的地圖更加美觀。如果設置每次前進的步長爲1,生成的地圖可能如下圖所示:
python3的源代碼爲:
import numpy as np
import time
import random
import copy
class Maze(object):
def __init__(self, width = 11, height = 11):
# 迷宮最小長寬爲5
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
def print_matrix(self):
for i in range(self.height):
for j in range(self.width):
if self.matrix[i][j] == -1:
print('□', end = '')
elif self.matrix[i][j] == 0:
print(' ', end = '')
elif self.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
# 這裏的長和寬設置的是50,但是實際生成的迷宮長寬會是51
maze = Maze(50, 50)
maze.generate_matrix_dfs()
maze.print_matrix()
2 遞歸分割算法
// TODO
3 隨機prim最小生成樹算法
隨機prim最小生成樹算法就是使用最小生成樹的思想,將所有的奇數點(橫縱座標均爲奇數的點)連成一個連通分量,而且每相鄰的兩個點之間的距離都爲1,因此這個方法也可以看作是一種特殊的隨機廣度優先算法。
初始情況下,將除了四個邊界之外的所有的位置初始化爲0(空),然後從奇數點開始使用隨機prim算法生成迷宮地圖。由於思想類似廣度優先,因此從起點到終點之間的路徑比較簡單(相比於使用隨機深度優先算法生成的地圖),很容易找到答案。運行結果如下圖:
python3的源代碼爲:
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
def print_matrix(self):
for i in range(self.height):
for j in range(self.width):
if self.matrix[i][j] == -1:
print('□', end = '')
elif self.matrix[i][j] == 0:
print(' ', end = '')
elif self.matrix[i][j] == 1:
print('■', end = '')
print('')
# 雖然說是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
maze = Maze(51, 51)
maze.generate_matrix_prim()
maze.print_matrix()
4 *kruskal最小生成樹算法
kruskal最小生成樹算法和prim類似,由於相鄰的兩個點之間的距離都是1,因此退化成隨機選邊法。初始化的時候首先將所有的奇數點的值設置爲-1(牆壁),對於共有個奇數點的圖而言,共需要選擇個可連接的邊。在需要判斷連通圖的個數的時候,並查集是最好用的,因此這裏使用並查集實現。
python3代碼
import numpy as np
import time
import random
import copy
class UnionSet(object):
"""
並查集
"""
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
def print_matrix(self):
for i in range(self.height):
for j in range(self.width):
if self.matrix[i][j] == -1:
print('□', end = '')
elif self.matrix[i][j] == 0:
print(' ', end = '')
elif self.matrix[i][j] == 1:
print('■', end = '')
print('')
# 最小生成樹算法-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
if __name__ == '__main__':
maze = Maze(51, 51)
maze.generate_matrix_kruskal()
maze.print_matrix()
*迷宮自動尋路-dfs
使用深度優先搜索尋路,我知道這肯定是最爛的迷宮尋路算法,但是性能可以接受,因此就先用這個吧~
# 迷宮尋路算法dfs
def find_path_dfs(self):
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] == self.destination[0] and path[-1][1] == self.destination[1] - 1:
for pos in path:
self.matrix[pos[0]][pos[1]] = 1
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 - 1 and visited[row_][col_] == 0 and self.matrix[row_][col_] == 0:
dfs(path + [[row_, col_]])
dfs([[self.start[0], self.start[1]]])
測試結果如下圖:
萬事俱備,只欠東風!接下來就做可視化界面了,本篇完。