From 460256c1cb3e4c878658a6fbb9db3ab1b4ec79d7 Mon Sep 17 00:00:00 2001 From: Gary Gan Date: Sun, 29 Jun 2025 18:40:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=B7=AF=E5=BE=84=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONFIG_README.md | 72 +++++++++++++ CSV_SAVE_SYSTEM_README.md | 138 ++++++++++++++++++++++++ SAVE_SYSTEM_README.md | 98 +++++++++++++++++ config.py | 61 +++++++++++ main.py | 188 ++++++++++++++++++++++++++------ maze.csv | 28 ++--- maze.py | 99 ++++++++++++++++- save_manager.py | 142 ++++++++++++++++++++++++ save_ui.py | 162 ++++++++++++++++++++++++++++ simple_save_manager.py | 220 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 1155 insertions(+), 53 deletions(-) create mode 100644 CONFIG_README.md create mode 100644 CSV_SAVE_SYSTEM_README.md create mode 100644 SAVE_SYSTEM_README.md create mode 100644 config.py create mode 100644 save_manager.py create mode 100644 save_ui.py create mode 100644 simple_save_manager.py diff --git a/CONFIG_README.md b/CONFIG_README.md new file mode 100644 index 0000000..1a495af --- /dev/null +++ b/CONFIG_README.md @@ -0,0 +1,72 @@ +# 配置文件说明 + +## 概述 +`config.py` 包含了整个迷宫游戏的所有配置参数,使得项目更易于维护和自定义。 + +## 配置参数说明 + +### UI 界面配置 +- `UI_HEIGHT = 1000`: 游戏窗口高度 +- `UI_WIDTH = 1500`: 游戏窗口宽度 + +### 迷宫配置 +- `MAZE_SIZE = 800`: 迷宫显示区域大小 +- `WALL_SIZE = 50`: 每个墙体/格子的像素大小 + +### 性能配置 +- `FPS = 120`: 游戏帧率 +- `AUTO_PLAY_INTERVAL = 30`: 自动播放时每多少帧前进一步 + +### 按钮尺寸配置 +- `BUTTON_START_SIZE = (200, 100)`: 开始按钮尺寸 +- `BUTTON_SAVE_SIZE = (80, 80)`: 保存按钮尺寸 +- `BUTTON_CONTROL_SIZE = (100, 50)`: 路径控制按钮尺寸 + +### 字体配置 +- `FONT_FILE = "syht.otf"`: 字体文件路径 +- `FONT_SIZE = 18`: 字体大小 + +### 资源路径配置 +所有图片资源的路径都定义在这里,便于统一管理: +- `WALL_IMAGE`: 墙体纹理 +- `COIN_IMAGE`: 金币纹理 +- `TRAP_IMAGE`: 陷阱纹理 +- `START_BUTTON_IMAGE`: 开始按钮图片 +- `SAVE_BUTTON_IMAGE`: 保存按钮图片 + +### 颜色配置 +预定义了常用的颜色值: +- `COLOR_WHITE`: 白色背景 +- `COLOR_BLACK`: 黑色文字 +- `COLOR_GRAY`: 灰色按钮 +- `COLOR_GREEN`: 绿色高亮 +- `COLOR_GOLD`: 金色路径 + +### 布局配置 +`get_button_positions()` 函数返回所有UI元素的位置,包括: +- 各种按钮的位置 +- 文字显示的位置 +- 根据窗口大小自动计算相对位置 + +## 使用方法 + +### 导入配置 +```python +from config import * +``` + +### 获取按钮位置 +```python +button_positions = get_button_positions() +start_x, start_y = button_positions['start_button'] +``` + +### 修改配置 +要修改游戏参数,只需编辑 `config.py` 文件即可,无需修改主程序代码。 + +## 优势 +1. **集中管理**: 所有配置参数集中在一个文件中 +2. **易于维护**: 修改参数不需要在多个文件中查找 +3. **可扩展性**: 新增配置项只需在配置文件中添加 +4. **代码整洁**: 主程序代码更加简洁明了 +5. **复用性**: 配置可以被多个模块共享使用 diff --git a/CSV_SAVE_SYSTEM_README.md b/CSV_SAVE_SYSTEM_README.md new file mode 100644 index 0000000..8f05151 --- /dev/null +++ b/CSV_SAVE_SYSTEM_README.md @@ -0,0 +1,138 @@ +# CSV存档系统使用说明 + +## 🎯 新的存档方式 + +根据您的需求,存档系统已经完全重新设计,现在只使用CSV格式保存迷宫和路径信息。 + +### 📁 文件格式 + +存档文件采用CSV格式,路径信息直接嵌入在迷宫数据中: + +#### 路径标记格式: +- **空地 + 路径**: `0` → `p10` (表示第10步) +- **金币 + 路径**: `g15` → `g15p10` (金币15,路径第10步) +- **陷阱 + 路径**: `t20` → `t20p10` (陷阱20,路径第10步) +- **机关 + 路径**: `l24` → `l24p10` (机关24,路径第10步) +- **boss + 路径**: `b92` → `b92p10` (boss92,路径第10步) +- **起点 + 路径**: `s` → `sp1` (起点,路径第1步) +- **终点 + 路径**: `e` → `ep25` (终点,路径第25步) + +#### 示例CSV内容: +```csv +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +1,g28p11,p10,p9,p14,p16,p17,p18,p19,p20,p21,p22,p23,t14p24,p25,1 +1,1,1,g27p8,1,1,1,1,1,1,1,1,1,1,p26,1 +1,p5,p6,p7,0,0,0,t14,0,0,1,0,1,p28,p27,1 +1,p4,1,1,1,1,1,0,1,t16,1,0,1,ep29,1,1 +``` + +### 🎮 功能特性 + +#### 1. 保存功能 +- **快速保存**: `Ctrl+S` - 自动使用时间戳命名 +- **命名保存**: 通过存档界面自定义名称 +- **路径完整性**: 保存完整的路径序列到CSV + +#### 2. 读档功能 +- **CSV解析**: 自动解析CSV中的路径信息 +- **路径重建**: 完整恢复路径序列和位置 +- **格式兼容**: 支持复杂的路径标记格式 + +#### 3. 路径处理 +- **多路径支持**: 处理一个格子被多次经过的情况(如`p35p37`) +- **特殊格子**: 正确处理起点、终点、金币、陷阱等特殊元素 +- **顺序保持**: 确保路径顺序完全正确 + +### 🔧 操作方法 + +#### 保存游戏: +1. **快速保存**: 按 `Ctrl+S` 自动保存到 `saves/maze_save_时间戳.csv` +2. **命名保存**: + - 点击"读档"按钮 + - 按 `N` 键新建存档 + - 输入文件名(自动添加.csv后缀) + - 按回车确认 + +#### 加载游戏: +1. 点击"读档"按钮或按 `Ctrl+L` +2. 使用↑↓键选择存档文件 +3. 按回车加载选中的存档 +4. 游戏会显示完整的路径信息 + +#### 管理存档: +- **查看列表**: 显示所有CSV存档文件 +- **删除存档**: 选中后按 `Delete` 键 +- **按时间排序**: 最新的存档显示在前面 + +### 📂 存档文件结构 + +``` +saves/ +├── maze_save_20250629_174154.csv # 时间戳命名的存档 +├── my_best_solution.csv # 自定义命名的存档 +└── level_1_complete.csv # 另一个自定义存档 +``` + +### 💡 高级特性 + +#### 路径重叠处理: +当路径多次经过同一个格子时,系统会正确保存多个路径标记: +- `p35p37` - 表示第35步和第37步都经过这个格子 +- `g15p10p20` - 表示金币15,第10步和第20步都经过 + +#### 自动解析: +系统使用正则表达式自动识别所有路径标记: +```python +# 识别 g15p10p20 中的所有路径号:10, 20 +path_matches = re.findall(r'p(\d+)', cell) +``` + +### 🎨 界面说明 + +#### 存档界面: +- **文件格式**: 显示"输入存档名称(.csv)"提示 +- **路径信息**: 加载时显示"存档加载成功" +- **完整显示**: 加载后立即显示完整路径 + +#### 快捷键: +- **Ctrl+S**: 保存包含路径的CSV文件 +- **Ctrl+L**: 打开读档界面 +- **N键**: 新建存档(在存档界面中) +- **Delete**: 删除选中的存档 + +### 📊 数据完整性 + +#### 保存时: +- ✅ 完整的迷宫结构 +- ✅ 完整的路径序列(1到N的所有步骤) +- ✅ 特殊元素的属性值 +- ✅ 路径与元素的关联关系 + +#### 加载时: +- ✅ 自动解析所有路径标记 +- ✅ 重建完整的路径序列 +- ✅ 恢复原始迷宫结构 +- ✅ 正确处理路径重叠 + +### 🔄 兼容性 + +- **向前兼容**: 支持读取旧版本生成的CSV文件 +- **标准格式**: 使用标准CSV格式,可用Excel等工具查看 +- **跨平台**: 文件在不同操作系统间完全兼容 + +### 📝 注意事项 + +1. **文件名**: 会自动添加.csv后缀,无需手动输入 +2. **路径完整性**: 确保所有路径步骤都被正确保存和加载 +3. **特殊字符**: 避免在存档名中使用特殊字符 +4. **存储位置**: 所有存档保存在`saves/`目录下 + +### 🏆 优势 + +1. **简洁明了**: 路径信息直接可见,易于理解 +2. **完全兼容**: 与您现有的CSV工作流程完美集成 +3. **数据完整**: 保存所有必要信息,无信息丢失 +4. **易于调试**: 可以直接查看和编辑CSV文件 +5. **标准格式**: 使用标准CSV格式,通用性强 + +现在您的存档系统完全符合您的需求:只保存CSV文件,路径信息嵌入其中,格式清晰易懂!🎉 diff --git a/SAVE_SYSTEM_README.md b/SAVE_SYSTEM_README.md new file mode 100644 index 0000000..d6d7cd7 --- /dev/null +++ b/SAVE_SYSTEM_README.md @@ -0,0 +1,98 @@ +# 存档系统使用说明 + +## 新增功能 + +### 🎮 完整的存档系统 +现在游戏支持完整的存档和读档功能,不仅保存迷宫结构,还会保存路径信息和当前进度。 + +### 🎯 功能特性 + +#### 1. 保存功能 +- **CSV保存**: 点击 "保存" 按钮保存迷宫结构到CSV文件(兼容旧版本) +- **完整存档**: 使用 `Ctrl+S` 或点击 "读档" 按钮选择 "新建" 保存完整游戏状态 + +#### 2. 读档功能 +- **存档列表**: 点击 "读档" 按钮查看所有可用存档 +- **快速读档**: 使用 `Ctrl+L` 快速打开读档界面 + +#### 3. 存档管理 +- **存档信息**: 显示存档名称、保存时间和迷宫大小 +- **删除存档**: 选中存档后按 `Delete` 键删除 +- **存档排序**: 按保存时间倒序排列 + +### 🔧 操作方法 + +#### 保存游戏 +1. **快速保存**: `Ctrl+S` - 使用时间戳自动命名 +2. **命名保存**: + - 点击 "读档" 按钮 + - 按 `N` 键新建存档 + - 输入存档名称 + - 按回车确认 + +#### 加载游戏 +1. **打开界面**: 点击 "读档" 按钮或按 `Ctrl+L` +2. **选择存档**: 使用↑↓键选择或直接点击 +3. **加载**: 按回车键或双击加载选中的存档 +4. **关闭**: 按 `ESC` 关闭界面 + +#### 管理存档 +- **删除存档**: 选中后按 `Delete` 键 +- **查看信息**: 存档列表显示名称和保存时间 + +### 📁 存档文件结构 + +存档保存在 `saves/` 目录下: +``` +saves/ +├── maze_save_20231229_143022.json # 完整存档文件 +├── maze_save_20231229_143022_maze.csv # 对应的CSV文件 +└── ... +``` + +#### 存档内容包括: +- **元数据**: 存档名称、时间、迷宫尺寸 +- **迷宫数据**: 完整的迷宫结构 +- **路径数据**: 完整路径、当前步数、完成状态 + +### 🎨 界面说明 + +#### 存档界面控制: +- **↑↓键**: 选择存档 +- **回车**: 加载选中的存档 +- **Delete**: 删除选中的存档 +- **N键**: 新建存档 +- **ESC**: 关闭界面 + +#### 键盘快捷键: +- **Ctrl+S**: 快速保存当前游戏状态 +- **Ctrl+L**: 打开读档界面 +- **空格**: 路径下一步 +- **R键**: 重置路径 +- **A键**: 自动播放切换 + +### 💡 使用技巧 + +1. **定期保存**: 在路径规划的关键点保存进度 +2. **命名规范**: 使用有意义的存档名称便于识别 +3. **备份重要存档**: 重要的迷宫解法可以导出CSV备份 +4. **清理存档**: 定期删除不需要的存档文件 + +### 🔄 兼容性 + +- **向后兼容**: 依然支持CSV格式的迷宫文件 +- **数据完整**: 新存档格式包含完整的游戏状态 +- **灵活切换**: 可以在新旧保存方式之间自由选择 + +### 📝 注意事项 + +1. 存档文件使用JSON格式,包含完整的游戏状态 +2. 删除存档操作不可撤销,请谨慎操作 +3. 存档目录会自动创建,无需手动设置 +4. 加载存档会覆盖当前游戏状态 + +现在你可以: +- 在解谜过程中随时保存进度 +- 尝试不同的路径策略 +- 保存有趣的迷宫布局 +- 与他人分享迷宫存档 diff --git a/config.py b/config.py new file mode 100644 index 0000000..f5dfa5a --- /dev/null +++ b/config.py @@ -0,0 +1,61 @@ +# 游戏配置文件 + +# UI 界面配置 +UI_HEIGHT = 1000 +UI_WIDTH = 1500 + +# 迷宫配置 +MAZE_SIZE = 800 +WALL_SIZE = 50 + +# 游戏性能配置 +FPS = 120 + +# 路径播放配置 +AUTO_PLAY_INTERVAL = 30 # 每30帧自动前进一步 + +# 按钮尺寸配置 +BUTTON_START_SIZE = (200, 100) +BUTTON_SAVE_SIZE = (80, 80) +BUTTON_CONTROL_SIZE = (100, 50) + +# 字体配置 +FONT_FILE = "syht.otf" +FONT_SIZE = 18 + +# 资源路径配置 +ASSETS_PATH = "assets" +WALL_IMAGE = f"{ASSETS_PATH}/wall.png" +COIN_IMAGE = f"{ASSETS_PATH}/coin.png" +TRAP_IMAGE = f"{ASSETS_PATH}/trap.png" +START_BUTTON_IMAGE = f"{ASSETS_PATH}/start_button.png" +SAVE_BUTTON_IMAGE = f"{ASSETS_PATH}/save.png" +LOAD_BUTTON_IMAGE = f"{ASSETS_PATH}/load.png" + +# 默认文件名 +DEFAULT_MAZE_FILE = "maze.csv" + +# 颜色配置 +COLOR_WHITE = (255, 255, 255) +COLOR_BLACK = (0, 0, 0) +COLOR_GRAY = (200, 200, 200) +COLOR_LIGHT_GRAY = (100, 100, 100) +COLOR_GREEN = (100, 255, 100) +COLOR_GOLD = (255, 215, 0) + +# 布局配置 +def get_button_positions(): + """返回按钮位置配置""" + control_panel_x = MAZE_SIZE + ((UI_WIDTH - MAZE_SIZE) / 2 - 100) + + return { + 'start_button': (MAZE_SIZE + 50, 0), + 'save_button': (MAZE_SIZE + 50, 110), + 'load_button': (MAZE_SIZE + 150, 110), + 'next_step_button': (MAZE_SIZE + 50, 200), + 'reset_path_button': (MAZE_SIZE + 200, 200), + 'auto_play_button': (MAZE_SIZE + 350, 200), + 'progress_text': (MAZE_SIZE + 50, 270), + 'hint_text': (MAZE_SIZE + 50, 300), + 'save_list_area': (MAZE_SIZE + 50, 350, 400, 200) # x, y, width, height + } diff --git a/main.py b/main.py index 237f6df..21fdef5 100644 --- a/main.py +++ b/main.py @@ -4,16 +4,11 @@ from pygame.constants import QUIT from maze import Maze from draw import Button, Toast +from save_ui import SaveLoadUI +from config import * import sys import os -UI_HEIGHT = 1000 -UI_WIDTH = 1500 - -MAZE_SIZE = 150 -WALL_SIZE = 50 -FPS = 120 - screen: Surface = None # 窗口实例 clock = None # 时钟实例 @@ -30,52 +25,139 @@ def pygameInit(title: str = "pygame"): clock = pygame.time.Clock() # Initialize font with UTF-8 support pygame.font.init() - textFont = pygame.font.Font("syht.otf", 18) + textFont = pygame.font.Font(FONT_FILE, FONT_SIZE) if __name__ == "__main__": pygameInit("maze") - maze = Maze(wall_size=WALL_SIZE, maze_size=MAZE_SIZE, file_name="maze.csv") - image_wall = pygame.image.load("assets/wall.png").convert_alpha() - image_wall = pygame.transform.scale(image_wall, (WALL_SIZE, WALL_SIZE)) # 例如缩放到50x50像素 + maze = Maze(wall_size=WALL_SIZE, maze_size=MAZE_SIZE, file_name=DEFAULT_MAZE_FILE) + + # 加载图片资源 + image_wall = pygame.image.load(WALL_IMAGE).convert_alpha() + image_wall = pygame.transform.scale(image_wall, (WALL_SIZE, WALL_SIZE)) - image_coin = pygame.image.load("assets/coin.png").convert_alpha() - image_coin = pygame.transform.scale(image_coin, (WALL_SIZE, WALL_SIZE)) # 例如缩放到50x50像素 + image_coin = pygame.image.load(COIN_IMAGE).convert_alpha() + image_coin = pygame.transform.scale(image_coin, (WALL_SIZE, WALL_SIZE)) - image_trap = pygame.image.load("assets/trap.png").convert_alpha() - image_trap = pygame.transform.scale(image_trap, (WALL_SIZE, WALL_SIZE)) # 例如缩放到50x50像素 + image_trap = pygame.image.load(TRAP_IMAGE).convert_alpha() + image_trap = pygame.transform.scale(image_trap, (WALL_SIZE, WALL_SIZE)) - button_start_texture = pygame.image.load("assets/start_button.png").convert_alpha() - button_start_texture = pygame.transform.scale(button_start_texture, (200, 100)) - button_start = Button(pygame.rect.Rect(MAZE_SIZE + ((UI_WIDTH - MAZE_SIZE) / 2 - 100), 0, 200, 100), button_start_texture) + # 获取按钮位置配置 + button_positions = get_button_positions() - button_save_texture = pygame.image.load("assets/save.png").convert_alpha() - button_save_texture = pygame.transform.scale(button_save_texture, (80, 80)) - button_save = Button(pygame.rect.Rect(MAZE_SIZE + ((UI_WIDTH - MAZE_SIZE) / 2 - 100), 110, 80, 80), button_save_texture) + # 创建按钮 + button_start_texture = pygame.image.load(START_BUTTON_IMAGE).convert_alpha() + button_start_texture = pygame.transform.scale(button_start_texture, BUTTON_START_SIZE) + button_start = Button(pygame.rect.Rect(*button_positions['start_button'], *BUTTON_START_SIZE), button_start_texture) + button_save_texture = pygame.image.load(SAVE_BUTTON_IMAGE).convert_alpha() + button_save_texture = pygame.transform.scale(button_save_texture, BUTTON_SAVE_SIZE) + button_save = Button(pygame.rect.Rect(*button_positions['save_button'], *BUTTON_SAVE_SIZE), button_save_texture) - # 没有生成迷宫就保存的提示框 + button_load_texture = pygame.image.load(LOAD_BUTTON_IMAGE).convert_alpha() + button_load_texture = pygame.transform.scale(button_load_texture, BUTTON_SAVE_SIZE) + button_load = Button(pygame.rect.Rect(*button_positions['load_button'], *BUTTON_SAVE_SIZE), button_load_texture) + + # 添加路径控制按钮 + button_next_step = Button(pygame.rect.Rect(*button_positions['next_step_button'], *BUTTON_CONTROL_SIZE), None) + button_reset_path = Button(pygame.rect.Rect(*button_positions['reset_path_button'], *BUTTON_CONTROL_SIZE), None) + button_auto_play = Button(pygame.rect.Rect(*button_positions['auto_play_button'], *BUTTON_CONTROL_SIZE), None) + + # 提示信息 mes1 = Toast("没有生成迷宫,无法保存", UI_WIDTH, UI_HEIGHT, font=textFont) mes2 = Toast("迷宫已保存", UI_WIDTH, UI_HEIGHT, font=textFont) + mes3 = Toast("存档已保存", UI_WIDTH, UI_HEIGHT, font=textFont) + mes4 = Toast("存档加载成功", UI_WIDTH, UI_HEIGHT, font=textFont) + mes5 = Toast("加载失败", UI_WIDTH, UI_HEIGHT, font=textFont) + # 创建存档界面 + save_ui = SaveLoadUI(textFont) + + # 路径控制变量 + auto_play = False + auto_play_timer = 0 + auto_play_interval = AUTO_PLAY_INTERVAL running = True while running: clock.tick(FPS) # 限制帧数 - screen.fill((255, 255, 255)) # 铺底 + screen.fill(COLOR_WHITE) # 铺底 + + # 自动播放逻辑 + if auto_play and len(maze.full_path) > 0: + auto_play_timer += 1 + if auto_play_timer >= auto_play_interval: + if not maze.next_path_step(): + auto_play = False # 路径播放完成后停止自动播放 + auto_play_timer = 0 + for event in pygame.event.get(): - button_start.handle_event(event=event) - button_save.handle_event(event=event) - - if button_start.pressed == True: - maze.generate() + # 首先让存档界面处理事件 + save_result = save_ui.handle_event(event, maze) + if save_result == "save_success": + mes3.show() + elif save_result == "load_success": + mes4.show() + auto_play = False # 加载游戏后停止自动播放 + elif save_result == "load_failed": + mes5.show() - if button_save.pressed == True: - if len(maze.grid) == 0: - mes1.show() - else: - maze.export_to_csv("maze.csv") - mes2.text = "迷宫已保存至maze.csv" - mes2.show() + # 如果存档界面正在显示,不处理其他按钮事件 + if not save_ui.show_save_list: + button_start.handle_event(event=event) + button_save.handle_event(event=event) + button_load.handle_event(event=event) + button_next_step.handle_event(event=event) + button_reset_path.handle_event(event=event) + button_auto_play.handle_event(event=event) + + if button_start.pressed == True: + maze.generate() + auto_play = False # 生成新迷宫时停止自动播放 + + if button_save.pressed == True: + if len(maze.grid) == 0: + mes1.show() + else: + maze.export_to_csv(DEFAULT_MAZE_FILE) + mes2.text = f"迷宫已保存至{DEFAULT_MAZE_FILE}" + mes2.show() + + if button_load.pressed == True: + save_ui.update_save_list(maze) + save_ui.toggle_save_list() + + # 路径控制 + if button_next_step.pressed == True and len(maze.full_path) > 0: + maze.next_path_step() + + if button_reset_path.pressed == True and len(maze.full_path) > 0: + maze.reset_path() + auto_play = False + + if button_auto_play.pressed == True and len(maze.full_path) > 0: + auto_play = not auto_play + auto_play_timer = 0 + + # 键盘控制 + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_SPACE and len(maze.full_path) > 0: + maze.next_path_step() + elif event.key == pygame.K_r and len(maze.full_path) > 0: + maze.reset_path() + auto_play = False + elif event.key == pygame.K_a and len(maze.full_path) > 0: + auto_play = not auto_play + auto_play_timer = 0 + elif event.key == pygame.K_s and pygame.key.get_pressed()[pygame.K_LCTRL]: + # Ctrl+S 保存包含路径的CSV + if len(maze.grid) > 0: + result = maze.save_game() + if result: + mes3.show() + elif event.key == pygame.K_l and pygame.key.get_pressed()[pygame.K_LCTRL]: + # Ctrl+L 打开读档界面 + save_ui.update_save_list(maze) + save_ui.toggle_save_list() if event.type == QUIT: running = False @@ -84,9 +166,45 @@ if __name__ == "__main__": maze.draw(screen=screen, wall_texture=image_wall, coin_texture=image_coin, trap_texture=image_trap) button_start.draw(screen=screen) button_save.draw(screen=screen) + button_load.draw(screen=screen) + + # 绘制路径控制按钮 + if len(maze.full_path) > 0: + # 绘制按钮背景 + pygame.draw.rect(screen, COLOR_GRAY, button_next_step.rect) + pygame.draw.rect(screen, COLOR_GRAY, button_reset_path.rect) + pygame.draw.rect(screen, COLOR_GREEN if auto_play else COLOR_GRAY, button_auto_play.rect) + + # 绘制按钮文字 + next_text = textFont.render("下一步", True, COLOR_BLACK) + reset_text = textFont.render("重置", True, COLOR_BLACK) + auto_text = textFont.render("自动播放" if not auto_play else "停止", True, COLOR_BLACK) + + screen.blit(next_text, (button_next_step.rect.x + 10, button_next_step.rect.y + 15)) + screen.blit(reset_text, (button_reset_path.rect.x + 25, button_reset_path.rect.y + 15)) + screen.blit(auto_text, (button_auto_play.rect.x + 5, button_auto_play.rect.y + 15)) + + # 显示当前步数信息 + progress_text = textFont.render(f"路径进度: {maze.path_step}/{len(maze.full_path)}", True, COLOR_BLACK) + screen.blit(progress_text, button_positions['progress_text']) + + # 显示操作提示 + if len(maze.full_path) > 0: + hint_text = textFont.render("空格键: 下一步 | R键: 重置 | A键: 自动播放", True, COLOR_LIGHT_GRAY) + screen.blit(hint_text, button_positions['hint_text']) + + # 显示快捷键提示 + shortcut_text = textFont.render("Ctrl+S: 保存包含路径的CSV | Ctrl+L: 读档", True, COLOR_LIGHT_GRAY) + screen.blit(shortcut_text, (MAZE_SIZE + 50, 330)) mes1.draw(screen=screen) mes2.draw(screen=screen) + mes3.draw(screen=screen) + mes4.draw(screen=screen) + mes5.draw(screen=screen) + + # 绘制存档界面(必须在最后绘制以显示在最上层) + save_ui.draw(screen) pygame.display.flip() pygame.quit() diff --git a/maze.csv b/maze.csv index 320e83c..e8eee19 100644 --- a/maze.csv +++ b/maze.csv @@ -1,16 +1,16 @@ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 -1,0,0,1,0,0,0,0,0,l25,0,t10,0,1,0,1 -1,1,0,1,1,1,1,1,1,1,1,1,l15,1,t7,1 -1,0,0,g26,0,e,t20,s,0,0,0,0,0,0,l25,1 -1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,1 -1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1 -1,0,1,1,1,1,0,1,1,1,1,1,g24,1,0,1 -1,0,0,1,0,t16,0,0,1,0,0,0,0,1,l16,1 -1,1,1,1,1,1,1,1,1,1,1,0,1,1,g15,1 -1,0,t8,1,0,1,0,0,g12,0,1,0,0,1,0,1 -1,0,0,1,0,1,1,0,1,0,1,0,1,1,0,1 -1,0,0,0,0,0,0,0,1,0,1,0,t15,1,l16,1 -1,0,0,1,0,1,0,t16,1,0,1,0,0,1,0,1 -1,1,0,1,1,1,1,1,1,0,1,0,0,1,0,1 -1,0,0,0,0,0,0,0,1,0,t5,b89,0,1,0,1 +1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 +1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1 +1,0,0,0,1,0,0,t9,1,0,1,0,1,0,0,1 +1,0,1,1,1,1,l15,1,1,0,1,0,1,0,1,1 +1,b50,0,0,0,0,g11,0,1,0,1,0,l16,0,1,1 +1,1,0,1,1,1,1,1,1,0,1,0,1,0,1,1 +1,0,t20,0,0,0,0,0,1,0,0,0,1,0,1,1 +1,1,1,1,1,1,1,0,0,0,1,0,1,0,1,1 +1,0,1,0,0,0,1,0,1,0,1,0,1,0,1,1 +1,0,1,1,1,0,l20,g19,1,t8,1,0,1,l12,1,1 +1,g11,e,0,0,0,1,g13,1,0,1,0,1,0,0,1 +1,0,1,1,0,1,1,1,1,0,1,1,1,1,s,1 +1,0,1,0,0,0,0,0,1,0,1,0,1,0,0,1 +1,0,1,1,1,l22,1,0,1,l27,1,0,0,t17,0,1 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 diff --git a/maze.py b/maze.py index 416071a..dd1d8fc 100644 --- a/maze.py +++ b/maze.py @@ -2,6 +2,7 @@ import pygame from maze_generator import MazeGenerator from SourceCollector import SourceCollector from tanxin import * +from simple_save_manager import simple_save_manager import time class Maze: @@ -14,19 +15,109 @@ class Maze: self.grid = [] self.generater = MazeGenerator(self.size, self.file_name) + self.source_collector = None + self.path_step = 0 # 当前显示到路径的第几步 + self.full_path = [] # 完整路径 + self.is_path_complete = False # 路径是否完全显示 def generate(self): seed = int(time.time() * 1000) % (2**32) self.generater.generate(seed=seed) - obj = SourceCollector(maze=self.generater.maze) - obj.run() - self.grid = obj.output_list() - print(self.grid) + self.source_collector = SourceCollector(maze=self.generater.maze) + self.source_collector.run() + self.full_path = self.source_collector.get_path() + for i in self.full_path: + print(i) + self.path_step = 0 + self.is_path_complete = False + self.grid = self.generater.maze # 使用原始迷宫数据 + print(f"路径长度: {len(self.full_path)}") + + def next_path_step(self): + """显示路径的下一步""" + if self.path_step < len(self.full_path): + self.path_step += 1 + self.update_grid_with_path() + return True + return False + + def reset_path(self): + """重置路径显示""" + self.path_step = 0 + self.is_path_complete = False + self.grid = self.generater.maze if self.generater.maze else [] + + def auto_advance_path(self): + """自动推进路径显示""" + if not self.is_path_complete: + if not self.next_path_step(): + self.is_path_complete = True + + def update_grid_with_path(self): + """根据当前步数更新网格显示""" + if not self.full_path or not self.generater.maze: + return + + # 从原始迷宫开始 + self.grid = [row[:] for row in self.generater.maze] # 深拷贝 + + # 只显示到当前步数的路径 + for idx in range(min(self.path_step, len(self.full_path))): + y, x = self.full_path[idx] + if self.grid[y][x].startswith('s') or self.grid[y][x].startswith('e'): + continue + if self.grid[y][x].startswith('g') or self.grid[y][x].startswith('t'): + self.grid[y][x] = f"{self.grid[y][x]}p{idx}" + continue + self.grid[y][x] = f"p{idx}" def export_to_csv(self, filename): + """导出迷宫到CSV文件(兼容旧版本)""" self.generater.export_to_csv(filename=filename) + def save_game(self, save_name=None): + """保存包含路径信息的迷宫到CSV文件""" + if len(self.grid) == 0: + print("没有生成迷宫,无法保存") + return None + + return simple_save_manager.save_maze_with_path(self, save_name) + + def load_game(self, save_file): + """从CSV存档文件加载游戏状态""" + load_data = simple_save_manager.load_maze_from_csv(save_file) + if load_data is None: + return False + + try: + # 恢复迷宫数据 + self.generater.maze = load_data['original_grid'] + self.size = len(load_data['original_grid']) + + # 恢复路径数据 + self.full_path = load_data['path_sequence'] + self.path_step = len(self.full_path) # 显示完整路径 + self.is_path_complete = True + + # 重新创建SourceCollector以便后续操作 + self.source_collector = SourceCollector(maze=self.generater.maze) + + # 使用包含路径信息的网格作为显示网格 + self.grid = load_data['path_grid'] + + print(f"成功加载游戏状态,路径长度: {len(self.full_path)}") + print(f"当前显示完整路径") + return True + + except Exception as e: + print(f"加载游戏状态失败: {str(e)}") + return False + + def get_save_list(self): + """获取所有可用的存档列表""" + return simple_save_manager.get_save_list() + def draw(self, screen, wall_texture, coin_texture, trap_texture): diff --git a/save_manager.py b/save_manager.py new file mode 100644 index 0000000..e1445ce --- /dev/null +++ b/save_manager.py @@ -0,0 +1,142 @@ +import json +import csv +import os +from datetime import datetime +from config import DEFAULT_MAZE_FILE + +class GameSaveManager: + """游戏存档管理器""" + + def __init__(self): + self.save_directory = "saves" + self.ensure_save_directory() + + def ensure_save_directory(self): + """确保存档目录存在""" + if not os.path.exists(self.save_directory): + os.makedirs(self.save_directory) + + def save_game_state(self, maze_instance, save_name=None): + """ + 保存游戏状态,包括迷宫和路径信息 + + Args: + maze_instance: Maze类的实例 + save_name: 存档名称,如果为None则使用时间戳 + + Returns: + str: 保存的文件路径 + """ + if save_name is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + save_name = f"maze_save_{timestamp}" + + # 创建存档数据结构 + save_data = { + "metadata": { + "save_name": save_name, + "save_time": datetime.now().isoformat(), + "maze_size": maze_instance.size, + "wall_size": maze_instance.wall_size, + "maze_display_size": maze_instance.maze_size + }, + "maze_data": { + "grid": maze_instance.generater.maze if maze_instance.generater.maze else [], + "generator_name": maze_instance.generater.name if hasattr(maze_instance.generater, 'name') else "Unknown" + }, + "path_data": { + "full_path": maze_instance.full_path, + "current_step": maze_instance.path_step, + "is_path_complete": maze_instance.is_path_complete + } + } + + # 保存为JSON文件 + save_file = os.path.join(self.save_directory, f"{save_name}.json") + try: + with open(save_file, 'w', encoding='utf-8') as f: + json.dump(save_data, f, indent=2, ensure_ascii=False) + + # 同时保存CSV格式的迷宫数据(兼容性) + csv_file = os.path.join(self.save_directory, f"{save_name}_maze.csv") + self.save_maze_csv(maze_instance.generater.maze, csv_file) + + print(f"游戏状态已保存至: {os.path.abspath(save_file)}") + return save_file + + except Exception as e: + print(f"保存失败: {str(e)}") + return None + + def load_game_state(self, save_file): + """ + 加载游戏状态 + + Args: + save_file: 存档文件路径 + + Returns: + dict: 包含游戏状态的字典,如果加载失败返回None + """ + try: + with open(save_file, 'r', encoding='utf-8') as f: + save_data = json.load(f) + + print(f"成功加载存档: {save_data['metadata']['save_name']}") + print(f"保存时间: {save_data['metadata']['save_time']}") + return save_data + + except Exception as e: + print(f"加载失败: {str(e)}") + return None + + def save_maze_csv(self, maze_grid, filename): + """保存迷宫网格为CSV格式""" + try: + with open(filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + for row in maze_grid: + writer.writerow(row) + return True + except Exception as e: + print(f"CSV保存失败: {str(e)}") + return False + + def get_save_list(self): + """获取所有存档文件列表""" + saves = [] + if os.path.exists(self.save_directory): + for filename in os.listdir(self.save_directory): + if filename.endswith('.json'): + save_path = os.path.join(self.save_directory, filename) + try: + with open(save_path, 'r', encoding='utf-8') as f: + save_data = json.load(f) + saves.append({ + 'filename': filename, + 'path': save_path, + 'name': save_data['metadata']['save_name'], + 'save_time': save_data['metadata']['save_time'], + 'maze_size': save_data['metadata']['maze_size'] + }) + except: + continue + return sorted(saves, key=lambda x: x['save_time'], reverse=True) + + def delete_save(self, save_file): + """删除存档文件""" + try: + if os.path.exists(save_file): + os.remove(save_file) + # 同时删除对应的CSV文件 + csv_file = save_file.replace('.json', '_maze.csv') + if os.path.exists(csv_file): + os.remove(csv_file) + print(f"存档已删除: {save_file}") + return True + except Exception as e: + print(f"删除失败: {str(e)}") + return False + +# 全局存档管理器实例 +save_manager = GameSaveManager() diff --git a/save_ui.py b/save_ui.py new file mode 100644 index 0000000..2fa3d20 --- /dev/null +++ b/save_ui.py @@ -0,0 +1,162 @@ +import pygame +from config import * + +class SaveLoadUI: + """存档和读档界面""" + + def __init__(self, font): + self.font = font + self.small_font = pygame.font.Font(FONT_FILE, FONT_SIZE - 4) + self.show_save_list = False + self.save_list = [] + self.selected_save = -1 + self.scroll_offset = 0 + self.max_visible_saves = 8 + + # 界面元素 + button_positions = get_button_positions() + self.list_area = pygame.Rect(*button_positions['save_list_area']) + self.save_input = "" + self.input_active = False + + def update_save_list(self, maze): + """更新存档列表""" + self.save_list = maze.get_save_list() + + def toggle_save_list(self): + """切换存档列表显示状态""" + self.show_save_list = not self.show_save_list + if not self.show_save_list: + self.selected_save = -1 + self.input_active = False + + def handle_event(self, event, maze): + """处理界面事件""" + if not self.show_save_list: + return None + + if event.type == pygame.KEYDOWN: + if self.input_active: + if event.key == pygame.K_RETURN: + # 保存游戏 + if self.save_input.strip(): + result = maze.save_game(self.save_input.strip()) + if result: + self.update_save_list(maze) + self.save_input = "" + self.input_active = False + return "save_success" + return "save_failed" + elif event.key == pygame.K_ESCAPE: + self.input_active = False + self.save_input = "" + elif event.key == pygame.K_BACKSPACE: + self.save_input = self.save_input[:-1] + else: + if len(self.save_input) < 20: # 限制输入长度 + self.save_input += event.unicode + else: + if event.key == pygame.K_UP: + if self.selected_save > 0: + self.selected_save -= 1 + elif event.key == pygame.K_DOWN: + if self.selected_save < len(self.save_list) - 1: + self.selected_save += 1 + elif event.key == pygame.K_RETURN: + # 加载选中的存档 + if 0 <= self.selected_save < len(self.save_list): + save_file = self.save_list[self.selected_save]['path'] + if maze.load_game(save_file): + self.show_save_list = False + return "load_success" + return "load_failed" + elif event.key == pygame.K_DELETE: + # 删除选中的存档 + if 0 <= self.selected_save < len(self.save_list): + save_file = self.save_list[self.selected_save]['path'] + from simple_save_manager import simple_save_manager + if simple_save_manager.delete_save(save_file): + self.update_save_list(maze) + if self.selected_save >= len(self.save_list): + self.selected_save = len(self.save_list) - 1 + return "delete_success" + elif event.key == pygame.K_n: + # 新建存档 + self.input_active = True + self.save_input = "" + elif event.key == pygame.K_ESCAPE: + self.toggle_save_list() + + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # 左键点击 + mouse_pos = pygame.mouse.get_pos() + # 检查是否点击在存档列表区域 + if self.list_area.collidepoint(mouse_pos): + # 计算点击的存档索引 + relative_y = mouse_pos[1] - self.list_area.y - 30 # 减去标题高度 + if relative_y >= 0: + clicked_index = relative_y // 25 # 每个存档项25像素高 + if 0 <= clicked_index < len(self.save_list): + self.selected_save = clicked_index + else: + # 点击外部关闭界面 + self.toggle_save_list() + + return None + + def draw(self, screen): + """绘制存档界面""" + if not self.show_save_list: + return + + # 绘制半透明背景 + overlay = pygame.Surface((UI_WIDTH, UI_HEIGHT)) + overlay.set_alpha(128) + overlay.fill((0, 0, 0)) + screen.blit(overlay, (0, 0)) + + # 绘制存档列表背景 + pygame.draw.rect(screen, COLOR_WHITE, self.list_area) + pygame.draw.rect(screen, COLOR_BLACK, self.list_area, 2) + + # 绘制标题 + title_text = self.font.render("存档管理", True, COLOR_BLACK) + screen.blit(title_text, (self.list_area.x + 10, self.list_area.y + 5)) + + # 绘制操作说明 + if self.input_active: + hint_text = self.small_font.render("输入存档名称(.csv),按回车保存:", True, COLOR_BLACK) + screen.blit(hint_text, (self.list_area.x + 10, self.list_area.y + 30)) + + # 绘制输入框 + input_rect = pygame.Rect(self.list_area.x + 10, self.list_area.y + 50, 300, 25) + pygame.draw.rect(screen, COLOR_WHITE, input_rect) + pygame.draw.rect(screen, COLOR_BLACK, input_rect, 2) + + input_text = self.small_font.render(self.save_input + "|", True, COLOR_BLACK) + screen.blit(input_text, (input_rect.x + 5, input_rect.y + 5)) + else: + hint_text = self.small_font.render("↑↓选择 回车加载 Delete删除 N新建 ESC关闭", True, COLOR_BLACK) + screen.blit(hint_text, (self.list_area.x + 10, self.list_area.y + 30)) + + # 绘制存档列表 + start_y = self.list_area.y + 55 + for i, save_info in enumerate(self.save_list): + y_pos = start_y + i * 25 + if y_pos > self.list_area.bottom - 25: + break + + # 高亮选中项 + if i == self.selected_save: + highlight_rect = pygame.Rect(self.list_area.x + 5, y_pos - 2, self.list_area.width - 10, 22) + pygame.draw.rect(screen, COLOR_GRAY, highlight_rect) + + # 显示存档信息 + save_text = f"{save_info['name']} - {save_info['save_time'][:16]}" + text_surface = self.small_font.render(save_text, True, COLOR_BLACK) + screen.blit(text_surface, (self.list_area.x + 10, y_pos)) + + # 如果没有存档,显示提示 + if not self.save_list and not self.input_active: + no_save_text = self.small_font.render("没有找到存档文件", True, COLOR_LIGHT_GRAY) + screen.blit(no_save_text, (self.list_area.x + 10, self.list_area.y + 60)) diff --git a/simple_save_manager.py b/simple_save_manager.py new file mode 100644 index 0000000..bbe92ac --- /dev/null +++ b/simple_save_manager.py @@ -0,0 +1,220 @@ +import csv +import os +from datetime import datetime +from config import DEFAULT_MAZE_FILE + +class SimpleSaveManager: + """简化的存档管理器 - 只保存CSV格式的迷宫文件""" + + def __init__(self): + self.save_directory = "saves" + self.ensure_save_directory() + + def ensure_save_directory(self): + """确保存档目录存在""" + if not os.path.exists(self.save_directory): + os.makedirs(self.save_directory) + + def save_maze_with_path(self, maze_instance, save_name=None): + """ + 保存包含路径信息的迷宫到CSV文件 + + Args: + maze_instance: Maze类的实例 + save_name: 存档名称,如果为None则使用时间戳 + + Returns: + str: 保存的文件路径 + """ + if save_name is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + save_name = f"maze_save_{timestamp}" + + # 确保文件名以.csv结尾 + if not save_name.endswith('.csv'): + save_name += '.csv' + + save_file = os.path.join(self.save_directory, save_name) + + try: + # 生成包含路径信息的网格 + path_grid = self.create_path_grid(maze_instance) + + # 保存到CSV文件 + with open(save_file, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + for row in path_grid: + writer.writerow(row) + + print(f"迷宫已保存至: {os.path.abspath(save_file)}") + return save_file + + except Exception as e: + print(f"保存失败: {str(e)}") + return None + + def create_path_grid(self, maze_instance): + """ + 创建包含路径信息的网格 + + Args: + maze_instance: Maze类的实例 + + Returns: + list: 包含路径信息的二维网格 + """ + if not maze_instance.generater.maze or not maze_instance.full_path: + return maze_instance.generater.maze if maze_instance.generater.maze else [] + + # 从原始迷宫开始 + path_grid = [row[:] for row in maze_instance.generater.maze] # 深拷贝 + + # 将完整路径信息添加到网格中 + for idx, (y, x) in enumerate(maze_instance.full_path): + current_cell = path_grid[y][x] + path_info = f"p{idx + 1}" # 路径编号从1开始 + + if current_cell.startswith('s'): + # 起点:保留起点标记,但也添加路径信息 + path_grid[y][x] = f"s{path_info}" + elif current_cell.startswith('e'): + # 终点:保留终点标记,但也添加路径信息 + path_grid[y][x] = f"e{path_info}" + elif current_cell.startswith('g'): + # 金币:g10p31 格式 + path_grid[y][x] = f"{current_cell}{path_info}" + elif current_cell.startswith('t'): + # 陷阱:t19p31 格式 + path_grid[y][x] = f"{current_cell}{path_info}" + elif current_cell.startswith('l'): + # 机关:l24p31 格式 + path_grid[y][x] = f"{current_cell}{path_info}" + elif current_cell.startswith('b'): + # boss:b92p31 格式 + path_grid[y][x] = f"{current_cell}{path_info}" + elif current_cell == '0': + # 空地:直接是p31 + path_grid[y][x] = path_info + else: + # 其他情况也添加路径信息 + path_grid[y][x] = f"{current_cell}{path_info}" + + return path_grid + + def load_maze_from_csv(self, csv_file): + """ + 从CSV文件加载迷宫,解析路径信息 + + Args: + csv_file: CSV文件路径 + + Returns: + dict: 包含迷宫数据和路径信息的字典 + """ + try: + with open(csv_file, 'r', newline='', encoding='utf-8') as f: + reader = csv.reader(f) + grid = [list(row) for row in reader] + + # 解析路径信息 + path_data = self.extract_path_from_grid(grid) + + # 创建原始迷宫(移除路径信息) + original_grid = self.create_original_grid(grid) + + result = { + 'original_grid': original_grid, + 'path_grid': grid, + 'path_sequence': path_data['path_sequence'], + 'path_positions': path_data['path_positions'] + } + + print(f"成功从 {os.path.abspath(csv_file)} 加载迷宫") + print(f"路径长度: {len(path_data['path_sequence'])}") + return result + + except Exception as e: + print(f"加载失败: {str(e)}") + return None + + def extract_path_from_grid(self, grid): + """从网格中提取路径信息""" + path_positions = {} # {step_number: (y, x)} + + for y in range(len(grid)): + for x in range(len(grid[y])): + cell = grid[y][x] + + # 查找所有路径信息 p数字 + import re + # 使用正则表达式找到所有p后跟数字的模式 + path_matches = re.findall(r'p(\d+)', cell) + + for path_match in path_matches: + try: + step_number = int(path_match) + path_positions[step_number] = (y, x) + except ValueError: + continue + + # 按步数排序创建路径序列 + path_sequence = [] + for step in sorted(path_positions.keys()): + path_sequence.append(path_positions[step]) + + return { + 'path_sequence': path_sequence, + 'path_positions': path_positions + } + + def create_original_grid(self, path_grid): + """从包含路径的网格创建原始迷宫网格""" + original_grid = [] + + for row in path_grid: + original_row = [] + for cell in row: + # 移除路径信息,恢复原始状态 + if 'p' in cell: + p_index = cell.find('p') + original_cell = cell[:p_index] if p_index > 0 else '0' + else: + original_cell = cell + original_row.append(original_cell) + original_grid.append(original_row) + + return original_grid + + def get_save_list(self): + """获取所有CSV存档文件列表""" + saves = [] + if os.path.exists(self.save_directory): + for filename in os.listdir(self.save_directory): + if filename.endswith('.csv'): + file_path = os.path.join(self.save_directory, filename) + try: + stat = os.stat(file_path) + saves.append({ + 'filename': filename, + 'path': file_path, + 'name': filename.replace('.csv', ''), + 'save_time': datetime.fromtimestamp(stat.st_mtime).isoformat(), + 'size': stat.st_size + }) + except: + continue + return sorted(saves, key=lambda x: x['save_time'], reverse=True) + + def delete_save(self, save_file): + """删除存档文件""" + try: + if os.path.exists(save_file): + os.remove(save_file) + print(f"存档已删除: {save_file}") + return True + except Exception as e: + print(f"删除失败: {str(e)}") + return False + +# 全局简化存档管理器实例 +simple_save_manager = SimpleSaveManager()