maze_python/maze_generator.py
2025-06-30 21:05:34 +08:00

492 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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=0, traps_range=(3, 8),
mechanisms_range=(2, 6), skill_traps=5,gold_range=(3,8)):
"""放置特殊元素(支持技能触发陷阱)- 不包括Boss和机关这些将在路径生成后放置"""
available = self.get_available_cells()
random.shuffle(available)
# 计算所需单元格数量不包括Boss和机关
required = 2 + random.randint(*traps_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放置Boss将在路径生成后放置
# # 放置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}"
# 技能触发陷阱
for _ in range(skill_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}"
# 放置金币
gold_count = random.randint(*gold_range)
for _ in range(gold_count):
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, traps_range=(3, 8),
mechanisms_range=(1, 1), skill_traps=5):
"""生成迷宫主方法 - Boss和机关将在路径生成后单独放置"""
# 清除之前的历史记录
self.history_mazes = []
self.special_elements = []
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(0, traps_range, mechanisms_range, skill_traps) # boss_count=0, mechanisms也不在这里放置
print(f"成功生成迷宫: {self.name} (Boss和机关将在路径生成后放置)")
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 place_boss_on_path(self, path, boss_count=1):
"""
在给定路径上随机放置Boss
Args:
path: 路径列表,格式为[(y1, x1), (y2, x2), ...]
boss_count: 要放置的Boss数量默认为1
Returns:
list: 放置Boss的位置列表
"""
if not path or len(path) < 3:
print("路径太短无法放置Boss")
return []
# 排除起点和终点只在中间路径上放置Boss
middle_path = path[1:-1] # 去掉第一个(起点)和最后一个(终点)
if len(middle_path) < boss_count:
print(f"路径中间位置不足,只能放置{len(middle_path)}个Boss")
boss_count = len(middle_path)
# 随机选择Boss放置位置
boss_positions = random.sample(middle_path, boss_count)
placed_bosses = []
for pos in boss_positions:
y, x = pos
# 只在通路位置放置Boss避免覆盖特殊元素
if self.maze[y][x] == self.ROUTE or str(self.maze[y][x]).startswith('p'):
val = random.randint(50, 100)
boss_element = f"{self.BOSS}{val}"
self.maze[y][x] = boss_element
self.special_elements.append((y, x, boss_element))
placed_bosses.append((y, x, val))
print(f"在路径位置 ({y}, {x}) 放置Boss血量: {val}")
return placed_bosses
def place_mechanisms_on_path(self, path, mechanisms_count=1, mechanisms_range=(1, 1)):
"""
在给定路径上随机放置机关
Args:
path: 路径列表,格式为[(y1, x1), (y2, x2), ...]
mechanisms_count: 要放置的机关数量默认为1
mechanisms_range: 机关数量范围当mechanisms_count为None时使用
Returns:
list: 放置机关的位置列表
"""
if not path or len(path) < 3:
print("路径太短,无法放置机关")
return []
# 如果没有指定机关数量使用默认值1
if mechanisms_count is None:
mechanisms_count = 1
# 排除起点和终点,只在中间路径上放置机关
middle_path = path[1:-1] # 去掉第一个(起点)和最后一个(终点)
# 过滤掉已经被Boss和其他特殊元素占据的位置
available_positions = []
for pos in middle_path:
y, x = pos
cell_str = str(self.maze[y][x])
# 只在通路位置或路径标记位置放置机关避免覆盖Boss和其他特殊元素
if (self.maze[y][x] == self.ROUTE or cell_str.startswith('p')) and not cell_str.startswith('b'):
available_positions.append(pos)
if len(available_positions) < mechanisms_count:
print(f"路径中可用位置不足,只能放置{len(available_positions)}个机关")
mechanisms_count = len(available_positions)
if mechanisms_count <= 0:
print("无可用位置放置机关")
return []
# 随机选择机关放置位置,确保不重复
mechanism_positions = random.sample(available_positions, mechanisms_count)
placed_mechanisms = []
for pos in mechanism_positions:
y, x = pos
# 再次检查位置是否可用(避免重复放置)
if self.maze[y][x] == self.ROUTE or str(self.maze[y][x]).startswith('p'):
val = random.randint(10, 30)
mechanism_element = f"{self.MECHANISM}{val}"
self.maze[y][x] = mechanism_element
self.special_elements.append((y, x, mechanism_element))
placed_mechanisms.append((y, x, val))
print(f"在路径位置 ({y}, {x}) 放置机关,奖励: {val}")
return placed_mechanisms
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()