383 lines
15 KiB
Python
383 lines
15 KiB
Python
import random
|
|
import csv
|
|
import json
|
|
import os
|
|
from datetime import datetime
|
|
|
|
|
|
class MazeGenerator:
|
|
def __init__(self, size, filename, name="Default Maze"):
|
|
# 迷宫基础元素
|
|
self.ROUTE = '0'
|
|
self.WALL = '1'
|
|
# 特殊元素
|
|
self.BOSS = 'b'
|
|
self.START = 's'
|
|
self.END = 'e'
|
|
self.TRAP = 't'
|
|
self.MECHANISM = 'l'
|
|
self.GOLD = 'g'
|
|
|
|
self.size = size
|
|
self.maze = []
|
|
self.filename = filename
|
|
self.name = name # 迷宫名称
|
|
self.special_elements = [] # 存储特殊元素的位置和值
|
|
self.history_mazes = []
|
|
def initialize_maze(self):
|
|
"""初始化迷宫,全部填充为墙"""
|
|
self.maze = [[self.WALL for _ in range(self.size)] for _ in range(self.size)]
|
|
|
|
def create_maze(self, x1, y1, x2, y2):
|
|
"""递归分割法生成迷宫,墙壁始终在偶数坐标"""
|
|
def getWallIndex(start, length):
|
|
assert length >= 3
|
|
wall_index = random.randint(start + 1, start + length - 2)
|
|
if wall_index % 2 == 1:
|
|
wall_index -= 1
|
|
return wall_index
|
|
|
|
def isValid(x, y):
|
|
return 0 <= x < self.size and 0 <= y < self.size
|
|
|
|
def isMovable(x, y):
|
|
return self.maze[y][x] != self.WALL
|
|
|
|
def generateHoles(x, y, width, height, wall_x, wall_y):
|
|
holes = []
|
|
hole_entrys = [
|
|
(random.randint(x, wall_x - 1), wall_y),
|
|
(random.randint(wall_x + 1, x + width - 1), wall_y),
|
|
(wall_x, random.randint(y, wall_y - 1)),
|
|
(wall_x, random.randint(wall_y + 1, y + height - 1))
|
|
]
|
|
margin_entrys = [
|
|
(x, wall_y), (x + width - 1, wall_y),
|
|
(wall_x, y), (wall_x, y + height - 1)
|
|
]
|
|
adjacent_entrys = [
|
|
(x - 1, wall_y), (x + width, wall_y),
|
|
(wall_x, y - 1), (wall_x, y + height)
|
|
]
|
|
for i in range(4):
|
|
adj_x, adj_y = adjacent_entrys[i]
|
|
if isValid(adj_x, adj_y) and isMovable(adj_x, adj_y):
|
|
mx, my = margin_entrys[i]
|
|
self.maze[my][mx] = self.ROUTE
|
|
else:
|
|
holes.append(hole_entrys[i])
|
|
ignore_hole = random.randint(0, len(holes) - 1)
|
|
for i in range(len(holes)):
|
|
if i != ignore_hole:
|
|
hx, hy = holes[i]
|
|
self.maze[hy][hx] = self.ROUTE
|
|
|
|
def recursiveDivision(x, y, width, height):
|
|
if width < 3 or height < 3:
|
|
return
|
|
wall_x = getWallIndex(x, width)
|
|
wall_y = getWallIndex(y, height)
|
|
for i in range(x, x + width):
|
|
self.maze[wall_y][i] = self.WALL
|
|
for i in range(y, y + height):
|
|
self.maze[i][wall_x] = self.WALL
|
|
generateHoles(x, y, width, height, wall_x, wall_y)
|
|
recursiveDivision(x, y, wall_x - x, wall_y - y)
|
|
recursiveDivision(x, wall_y + 1, wall_x - x, y + height - wall_y - 1)
|
|
recursiveDivision(wall_x + 1, y, x + width - wall_x - 1, wall_y - y)
|
|
recursiveDivision(wall_x + 1, wall_y + 1, x + width - wall_x - 1, y + height - wall_y - 1)
|
|
self.history_mazes.append([[__ for __ in _] for _ in self.maze])
|
|
|
|
# 先全部通路
|
|
self.maze = [[self.ROUTE for _ in range(self.size)] for _ in range(self.size)]
|
|
# 四周加墙
|
|
for x in range(self.size):
|
|
self.maze[0][x] = self.WALL
|
|
self.maze[self.size - 1][x] = self.WALL
|
|
for y in range(self.size):
|
|
self.maze[y][0] = self.WALL
|
|
self.maze[y][self.size - 1] = self.WALL
|
|
# 递归分割
|
|
try:
|
|
recursiveDivision(1, 1, self.size - 2, self.size - 2)
|
|
except:
|
|
self.create_maze(x1, y1, x2, y2) # 如果递归失败,重新尝试
|
|
def set_random_exits(self):
|
|
"""随机设置迷宫入口和出口"""
|
|
available = self.get_available_cells()
|
|
if len(available) < 2:
|
|
raise ValueError("迷宫空间不足,无法设置随机出入口")
|
|
start, end = random.sample(available, 2)
|
|
self.maze[start[0]][start[1]] = self.START
|
|
self.maze[end[0]][end[1]] = self.END
|
|
self.special_elements.extend([(start[0], start[1], self.START), (end[0], end[1], self.END)])
|
|
def get_history_mazes(self):
|
|
for idx,i in enumerate(self.history_mazes):
|
|
if i == self.history_mazes[idx-1]:
|
|
del self.history_mazes[idx]
|
|
|
|
return self.history_mazes
|
|
|
|
def get_available_cells(self):
|
|
"""获取所有可通行单元格"""
|
|
cells = []
|
|
for i in range(1, self.size - 1):
|
|
for j in range(1, self.size - 1):
|
|
if self.maze[i][j] == self.ROUTE:
|
|
cells.append((i, j))
|
|
return cells
|
|
|
|
def place_special_elements(self, boss_count=1, traps_range=(3, 8),
|
|
mechanisms_range=(2, 6), skill_traps=5,gold_range=(3,8)):
|
|
"""放置特殊元素(支持技能触发陷阱)"""
|
|
available = self.get_available_cells()
|
|
random.shuffle(available)
|
|
|
|
# 计算所需单元格数量
|
|
required = 2 + boss_count + random.randint(*traps_range) + random.randint(*mechanisms_range) + skill_traps + random.randint(*gold_range)
|
|
if len(available) < required:
|
|
raise ValueError(f"空间不足,需要{required}个单元格,实际可用{len(available)}")
|
|
|
|
# 放置出入口
|
|
|
|
start, end = available.pop(), available.pop()
|
|
self.special_elements.extend([(start[0], start[1], self.START), (end[0], end[1], self.END)])
|
|
self.maze[start[0]][start[1]] = self.START
|
|
self.maze[end[0]][end[1]] = self.END
|
|
|
|
# 放置BOSS
|
|
for _ in range(boss_count):
|
|
pos = available.pop()
|
|
val = random.randint(50, 100)
|
|
self.special_elements.append((pos[0], pos[1], f"{self.BOSS}{val}"))
|
|
self.maze[pos[0]][pos[1]] = f"{self.BOSS}{val}"
|
|
|
|
# 放置普通陷阱
|
|
traps = random.randint(*traps_range)
|
|
for _ in range(traps):
|
|
pos = available.pop()
|
|
val = random.randint(5, 20)
|
|
self.special_elements.append((pos[0], pos[1], f"{self.TRAP}{val}"))
|
|
self.maze[pos[0]][pos[1]] = f"{self.TRAP}{val}"
|
|
|
|
# 放置机关
|
|
mechanisms = random.randint(*mechanisms_range)
|
|
for _ in range(mechanisms):
|
|
pos = available.pop()
|
|
val = random.randint(10, 30)
|
|
self.special_elements.append((pos[0], pos[1], f"{self.MECHANISM}{val}"))
|
|
self.maze[pos[0]][pos[1]] = f"{self.MECHANISM}{val}"
|
|
# 放置金币
|
|
mechanisms = random.randint(*gold_range)
|
|
for _ in range(mechanisms):
|
|
pos = available.pop()
|
|
val = random.randint(10, 30)
|
|
self.special_elements.append((pos[0], pos[1], f"{self.GOLD}{val}"))
|
|
self.maze[pos[0]][pos[1]] = f"{self.GOLD}{val}"
|
|
|
|
def generate(self, seed=None, boss_count=1, traps_range=(3, 8),
|
|
mechanisms_range=(2, 6), skill_traps=5):
|
|
"""生成迷宫主方法"""
|
|
random.seed(seed or random.randint(0, 1000))
|
|
self.initialize_maze()
|
|
self.create_maze(1, 1, self.size - 2, self.size - 2)
|
|
self.patch_maze_edges() # 自动修正边界
|
|
self.place_special_elements(boss_count, traps_range, mechanisms_range, skill_traps)
|
|
print(f"成功生成迷宫: {self.name}")
|
|
|
|
def export_to_csv(self, filename):
|
|
"""导出迷宫到CSV文件"""
|
|
try:
|
|
with open(filename, 'w', newline='') as f:
|
|
writer = csv.writer(f)
|
|
for row in self.maze:
|
|
writer.writerow(row)
|
|
print(f"迷宫已导出至: {os.path.abspath(filename)}")
|
|
except Exception as e:
|
|
print(f"导出失败: {str(e)}")
|
|
|
|
def read_from_csv(self):
|
|
"""从CSV读取迷宫数据"""
|
|
try:
|
|
with open(self.filename, 'r', newline='') as f:
|
|
reader = csv.reader(f)
|
|
self.maze = [list(map(self._parse_element, row)) for row in reader]
|
|
self.size = len(self.maze)
|
|
print(f"成功从{os.path.abspath(self.filename)}读取迷宫")
|
|
return True
|
|
except Exception as e:
|
|
print(f"读取失败: {str(e)}")
|
|
return False
|
|
|
|
def _parse_element(self, elem):
|
|
"""解析CSV中的元素类型"""
|
|
if elem == 's': return self.START
|
|
if elem == 'e': return self.END
|
|
if elem.startswith('b'): return self.BOSS
|
|
if elem.startswith('t'): return self.TRAP
|
|
if elem.startswith('l'): return self.MECHANISM
|
|
if elem.startswith('g'): return self.GOLD
|
|
|
|
return int(elem)
|
|
|
|
def print_maze(self):
|
|
"""打印迷宫到控制台(带元素标识)"""
|
|
symbols = {
|
|
self.WALL: '■', self.ROUTE: '·', self.START: 'S',
|
|
self.END: 'E', self.BOSS: 'B', self.TRAP: 'T',
|
|
self.GOLD: 'G','|':'|','-':'—',
|
|
}
|
|
for row in self.maze:
|
|
display = []
|
|
for cell in row:
|
|
symbol = symbols.get(((str)(cell))[0], ((str)(cell))[0])
|
|
display.append(str(symbol))
|
|
print(' '.join(display))
|
|
def read_csv(self):
|
|
l = []
|
|
with open(f'{self.filename}', 'r', newline='') as f:
|
|
reader = csv.reader(f)
|
|
for row in reader:
|
|
l.append(row)
|
|
return l
|
|
def patch_maze_edges(self):
|
|
|
|
"""只在不破坏联通性的前提下,修正右侧和下侧边界的多余通路(加墙)"""
|
|
n = self.size
|
|
if n %2==1:
|
|
return
|
|
candidates = []
|
|
# 倒数第二列(右侧)
|
|
x = n - 2
|
|
for y in range(1, n-1):
|
|
if self.maze[y][x] == self.ROUTE:
|
|
right = self.maze[y][x+1] if x+1 < n else self.WALL
|
|
down = self.maze[y+1][x] if y+1 < n else self.WALL
|
|
if right == self.ROUTE or down == self.ROUTE:
|
|
candidates.append((y, x))
|
|
# 倒数第
|
|
# 二行(下侧)
|
|
y = n - 2
|
|
for x in range(1, n-1):
|
|
if self.maze[y][x] == self.ROUTE:
|
|
right = self.maze[y][x+1] if x+1 < n else self.WALL
|
|
down = self.maze[y+1][x] if y+1 < n else self.WALL
|
|
if right == self.ROUTE or down == self.ROUTE:
|
|
candidates.append((y, x))
|
|
# 逐个尝试加墙,确保联通
|
|
for y, x in candidates:
|
|
old = self.maze[y][x]
|
|
self.maze[y][x] = self.WALL
|
|
if not self.is_maze_connected():
|
|
self.maze[y][x] = old # 恢复
|
|
self.history_mazes.append([[__ for __ in _] for _ in self.maze])
|
|
|
|
def is_maze_connected(self):
|
|
"""检查迷宫是否连通(深度优先搜索)"""
|
|
visited = [[False] * self.size for _ in range(self.size)]
|
|
|
|
def dfs(x, y):
|
|
if x < 0 or x >= self.size or y < 0 or y >= self.size:
|
|
return
|
|
if visited[y][x] or self.maze[y][x] == self.WALL:
|
|
return
|
|
visited[y][x] = True
|
|
dfs(x + 1, y)
|
|
dfs(x - 1, y)
|
|
dfs(x, y + 1)
|
|
dfs(x, y - 1)
|
|
|
|
# 从左上角开始搜索
|
|
dfs(1, 1)
|
|
|
|
# 检查是否所有通路都被访问
|
|
for y in range(1, self.size - 1):
|
|
for x in range(1, self.size - 1):
|
|
if self.maze[y][x] == self.ROUTE and not visited[y][x]:
|
|
return False
|
|
return True
|
|
|
|
def export_to_json(self, filename):
|
|
"""导出迷宫到JSON文件"""
|
|
try:
|
|
import json
|
|
from datetime import datetime
|
|
|
|
# 转换迷宫格式
|
|
json_maze = self._convert_to_json_format()
|
|
|
|
# 创建JSON数据结构
|
|
json_data = {
|
|
"maze": json_maze,
|
|
"metadata": {
|
|
"name": self.name,
|
|
"size": self.size,
|
|
"export_time": datetime.now().isoformat() if 'datetime' in globals() else None,
|
|
"special_elements_count": len(self.special_elements)
|
|
}
|
|
}
|
|
|
|
with open(filename, 'w', encoding='utf-8') as f:
|
|
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
|
print(f"迷宫已导出至: {os.path.abspath(filename)}")
|
|
except Exception as e:
|
|
print(f"JSON导出失败: {str(e)}")
|
|
|
|
def _convert_to_json_format(self):
|
|
"""
|
|
将内部迷宫格式转换为JSON格式
|
|
'1' -> '#', '0' -> ' ', 's' -> 'S', 'e' -> 'E'
|
|
'g数字' -> 'G', 't数字' -> 'T', 'l数字' -> 'L', 'b数字' -> 'B'
|
|
"""
|
|
json_maze = []
|
|
for row in self.maze:
|
|
json_row = []
|
|
for cell in row:
|
|
cell_str = str(cell)
|
|
if cell_str == '1':
|
|
json_row.append('#')
|
|
elif cell_str == '0':
|
|
json_row.append(' ')
|
|
elif cell_str == 's':
|
|
json_row.append('S')
|
|
elif cell_str == 'e':
|
|
json_row.append('E')
|
|
elif cell_str.startswith('g'):
|
|
json_row.append('G')
|
|
elif cell_str.startswith('t'):
|
|
json_row.append('T')
|
|
elif cell_str.startswith('l'):
|
|
json_row.append('L')
|
|
elif cell_str.startswith('b'):
|
|
json_row.append('B')
|
|
else:
|
|
json_row.append(' ') # 默认为通路
|
|
json_maze.append(json_row)
|
|
return json_maze
|
|
|
|
def main():
|
|
# 示例1: 生成带技能陷阱的迷宫
|
|
generator = MazeGenerator(
|
|
size=20,
|
|
filename="dungeon_maze.csv"
|
|
)
|
|
generator.generate(
|
|
seed=666
|
|
)
|
|
generator.print_maze()
|
|
generator.export_to_csv("d.csv")
|
|
for i in generator.history_mazes:
|
|
print(i[3:])
|
|
|
|
reader = MazeGenerator(size=1, filename="d.csv")
|
|
if reader.read_from_csv():
|
|
print("\n读取的迷宫:")
|
|
reader.print_maze()
|
|
for i in generator.get_history_mazes():
|
|
for j in i:
|
|
print(j)
|
|
print()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|