diff --git a/README_MINESWEEPER.md b/README_MINESWEEPER.md new file mode 100644 index 0000000..a5f02d0 --- /dev/null +++ b/README_MINESWEEPER.md @@ -0,0 +1,52 @@ +# 扫雷游戏说明 + +## 游戏介绍 +这是一个经典的扫雷游戏实现,基于PyQt5框架开发。游戏中玩家需要根据数字提示找出所有非地雷的方块,避免踩到地雷。 + +## 游戏特性 +- 经典的扫雷游戏玩法 +- 三种难度级别:初级(9x9, 10个地雷)、中级(16x16, 40个地雷)、高级(30x16, 99个地雷) +- 计时功能,记录游戏时间 +- 右键标记地雷功能 +- 自动展开空白区域功能 +- 胜负判断和游戏结束提示 + +## 如何开始游戏 +有两种方式可以启动扫雷游戏: + +### 方法一:通过主应用启动(推荐) +1. 运行主应用:`python src/main.py` +2. 在菜单栏中选择:应用选项(O) -> 小游戏 -> 扫雷 + +### 方法二:直接运行测试脚本 +1. 运行简化测试脚本:`python simple_minesweeper_test.py` + +## 游戏操作说明 +- **左键单击**:揭开方块 +- **右键单击**:标记/取消标记地雷(旗子) +- **左右键同时单击**(或中键单击):自动展开周围区域(当数字周围的旗子数量等于该数字时) + +## 游戏规则 +1. 点击任意方块开始游戏 +2. 数字表示周围8个方块中地雷的数量 +3. 右键点击可以标记疑似地雷的位置 +4. 避免点击地雷,否则游戏结束 +5. 成功标记所有地雷或揭开所有非地雷方块即可获胜 + +## 技术实现 +- 使用PyQt5构建图形界面 +- 自定义按钮类MineButton继承自QPushButton +- 实现了完整的扫雷游戏逻辑,包括地雷生成、数字计算、递归展开等 +- 支持多种难度级别的游戏配置 + +## 文件结构 +- `src/ui/minesweeper_game.py`:扫雷游戏核心实现文件 +- `simple_minesweeper_test.py`:简化版测试脚本 +- `test_minesweeper.py`:完整版测试脚本 + +## 故障排除 +如果遇到Qt平台插件错误,请确保: +1. 已正确安装PyQt5:`pip install PyQt5` +2. 环境变量已正确设置(通常由应用自动处理) + +如有任何问题,请联系开发者。 \ No newline at end of file diff --git a/Scripts/chardetect.exe b/Scripts/chardetect.exe new file mode 100644 index 0000000..a2240b0 Binary files /dev/null and b/Scripts/chardetect.exe differ diff --git a/Scripts/normalizer.exe b/Scripts/normalizer.exe new file mode 100644 index 0000000..dc9a675 Binary files /dev/null and b/Scripts/normalizer.exe differ diff --git a/Scripts/pip3.13.exe b/Scripts/pip3.13.exe new file mode 100644 index 0000000..d65d66c Binary files /dev/null and b/Scripts/pip3.13.exe differ diff --git a/Scripts/pip3.exe b/Scripts/pip3.exe new file mode 100644 index 0000000..d65d66c Binary files /dev/null and b/Scripts/pip3.exe differ diff --git a/Scripts/pylupdate5.exe b/Scripts/pylupdate5.exe new file mode 100644 index 0000000..307111c Binary files /dev/null and b/Scripts/pylupdate5.exe differ diff --git a/Scripts/pyrcc5.exe b/Scripts/pyrcc5.exe new file mode 100644 index 0000000..57a0e5e Binary files /dev/null and b/Scripts/pyrcc5.exe differ diff --git a/Scripts/pyuic5.exe b/Scripts/pyuic5.exe new file mode 100644 index 0000000..ec5b313 Binary files /dev/null and b/Scripts/pyuic5.exe differ diff --git a/simple_minesweeper_test.py b/simple_minesweeper_test.py new file mode 100644 index 0000000..a56ed69 --- /dev/null +++ b/simple_minesweeper_test.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +简化版扫雷游戏测试脚本 +用于快速测试扫雷游戏模块是否能正常运行 +""" + +import sys +import os + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# 设置Qt环境变量 +def setup_qt_env(): + """设置基本的Qt环境变量""" + venv_plugins = os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins') + if os.path.exists(venv_plugins): + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(venv_plugins, 'platforms') + print(f"✅ Qt平台插件路径已设置: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}") + return True + else: + print("❌ 未找到Qt平台插件路径") + return False + +# 尝试设置Qt环境 +if not setup_qt_env(): + print("警告:Qt环境设置失败,游戏可能无法正常显示") + +try: + from PyQt5.QtWidgets import QApplication, QMainWindow + from src.ui.minesweeper_game import MinesweeperWindow + + def main(): + app = QApplication(sys.argv) + + # 创建并显示扫雷游戏窗口 + window = MinesweeperWindow() + window.show() + + print("🎮 扫雷游戏窗口已显示") + print("ℹ️ 请检查是否有游戏窗口弹出") + print("ℹ️ 如果没有窗口弹出,请关闭此窗口并检查Qt环境配置") + + sys.exit(app.exec_()) + + if __name__ == '__main__': + main() + +except ImportError as e: + print(f"❌ 导入错误: {e}") + print("请确保已正确安装PyQt5") +except Exception as e: + print(f"❌ 运行错误: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/src/ui/minesweeper_game.py b/src/ui/minesweeper_game.py new file mode 100644 index 0000000..d7702fb --- /dev/null +++ b/src/ui/minesweeper_game.py @@ -0,0 +1,438 @@ +# minesweeper_game.py +""" +扫雷游戏模块 +用户通过鼠标点击揭开方块,右键标记地雷 +""" + +import sys +import random +from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox +from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QRect +from PyQt5.QtGui import QPainter, QColor, QFont, QBrush, QPen + + +class MineButton(QPushButton): + """自定义按钮类,用于表示扫雷游戏中的方块""" + + left_clicked = pyqtSignal(int, int) + right_clicked = pyqtSignal(int, int) + + def __init__(self, row, col): + super().__init__() + self.row = row + self.col = col + self.setFixedSize(30, 30) + self.setStyleSheet(""" + QPushButton { + background-color: #cccccc; + border: 1px solid #999999; + font-weight: bold; + } + QPushButton:pressed { + background-color: #bbbbbb; + } + """) + self.setText("") + + def mousePressEvent(self, event): + """重写鼠标按下事件""" + if event.button() == Qt.LeftButton: + self.left_clicked.emit(self.row, self.col) + elif event.button() == Qt.RightButton: + self.right_clicked.emit(self.row, self.col) + else: + super().mousePressEvent(event) + + +class MinesweeperGame(QWidget): + """扫雷游戏主类""" + + # 游戏常量 + ROWS = 10 + COLS = 10 + MINES = 15 + + # 游戏状态 + HIDDEN = 0 # 隐藏 + REVEALED = 1 # 已揭开 + FLAGGED = 2 # 已标记 + + # 信号 + game_won = pyqtSignal() + game_lost = pyqtSignal() + mine_count_changed = pyqtSignal(int) + + def __init__(self): + super().__init__() + self.init_game() + self.init_ui() + + def init_ui(self): + """初始化UI""" + layout = QVBoxLayout() + layout.setSpacing(0) + layout.setContentsMargins(10, 10, 10, 10) + + # 创建按钮网格 + self.grid_layout = QVBoxLayout() + self.buttons = [] + + for row in range(self.ROWS): + button_row = [] + row_layout = QHBoxLayout() + row_layout.setSpacing(0) + for col in range(self.COLS): + button = MineButton(row, col) + button.left_clicked.connect(self.on_left_click) + button.right_clicked.connect(self.on_right_click) + row_layout.addWidget(button) + button_row.append(button) + self.buttons.append(button_row) + self.grid_layout.addLayout(row_layout) + + layout.addLayout(self.grid_layout) + self.setLayout(layout) + + def init_game(self): + """初始化游戏状态""" + # 初始化游戏板 + self.board = [[0 for _ in range(self.COLS)] for _ in range(self.ROWS)] + self.state = [[self.HIDDEN for _ in range(self.COLS)] for _ in range(self.ROWS)] + self.mine_positions = set() + self.flag_count = 0 + self.revealed_count = 0 + self.game_over = False + self.first_click = True + + # 放置地雷 + self.place_mines() + + # 计算数字 + self.calculate_numbers() + + def place_mines(self): + """随机放置地雷""" + mines_placed = 0 + while mines_placed < self.MINES: + row = random.randint(0, self.ROWS - 1) + col = random.randint(0, self.COLS - 1) + + # 不要在第一个点击的位置放置地雷 + if (row, col) not in self.mine_positions: + self.mine_positions.add((row, col)) + self.board[row][col] = -1 # -1 表示地雷 + mines_placed += 1 + + def calculate_numbers(self): + """计算每个非地雷方块周围的地雷数量""" + for row in range(self.ROWS): + for col in range(self.COLS): + # 如果不是地雷,则计算周围地雷数 + if self.board[row][col] != -1: + count = 0 + # 检查周围的8个方块 + for dr in [-1, 0, 1]: + for dc in [-1, 0, 1]: + if dr == 0 and dc == 0: + continue + r, c = row + dr, col + dc + if 0 <= r < self.ROWS and 0 <= c < self.COLS: + if self.board[r][c] == -1: + count += 1 + self.board[row][col] = count + + def on_left_click(self, row, col): + """处理左键点击""" + if self.game_over or self.state[row][col] == self.FLAGGED: + return + + # 第一次点击时确保不会点到地雷 + if self.first_click: + self.first_click = False + if (row, col) in self.mine_positions: + # 重新放置地雷 + self.mine_positions.remove((row, col)) + self.board[row][col] = 0 + + # 找一个没有地雷的位置放置新地雷 + new_row, new_col = row, col + while (new_row, new_col) == (row, col) or (new_row, new_col) in self.mine_positions: + new_row = random.randint(0, self.ROWS - 1) + new_col = random.randint(0, self.COLS - 1) + + self.mine_positions.add((new_row, new_col)) + self.board[new_row][new_col] = -1 + + # 重新计算数字 + self.calculate_numbers() + + self.reveal_cell(row, col) + self.update_display() + + # 检查游戏是否结束 + if self.check_win(): + self.win_game() + elif self.check_loss(row, col): + self.lose_game() + + def on_right_click(self, row, col): + """处理右键点击(标记/取消标记)""" + if self.game_over or self.state[row][col] == self.REVEALED: + return + + if self.state[row][col] == self.HIDDEN: + # 标记为地雷 + self.state[row][col] = self.FLAGGED + self.flag_count += 1 + self.buttons[row][col].setText("🚩") + self.buttons[row][col].setStyleSheet(""" + QPushButton { + background-color: #ffcc00; + border: 1px solid #999999; + font-weight: bold; + } + """) + elif self.state[row][col] == self.FLAGGED: + # 取消标记 + self.state[row][col] = self.HIDDEN + self.flag_count -= 1 + self.buttons[row][col].setText("") + self.buttons[row][col].setStyleSheet(""" + QPushButton { + background-color: #cccccc; + border: 1px solid #999999; + font-weight: bold; + } + """) + + self.mine_count_changed.emit(self.MINES - self.flag_count) + + def reveal_cell(self, row, col): + """揭开指定位置的方块""" + # 如果已经揭开或标记,则不做任何操作 + if self.state[row][col] != self.HIDDEN: + return + + # 揭开方块 + self.state[row][col] = self.REVEALED + self.revealed_count += 1 + + # 如果是空白方块(周围没有地雷),则递归揭开周围的方块 + if self.board[row][col] == 0: + for dr in [-1, 0, 1]: + for dc in [-1, 0, 1]: + if dr == 0 and dc == 0: + continue + r, c = row + dr, col + dc + if 0 <= r < self.ROWS and 0 <= c < self.COLS: + if self.state[r][c] == self.HIDDEN: + self.reveal_cell(r, c) + + def update_display(self): + """更新显示""" + for row in range(self.ROWS): + for col in range(self.COLS): + button = self.buttons[row][col] + cell_state = self.state[row][col] + cell_value = self.board[row][col] + + if cell_state == self.REVEALED: + if cell_value == -1: + # 地雷 + button.setText("💣") + button.setStyleSheet(""" + QPushButton { + background-color: #ff6666; + border: 1px solid #999999; + font-weight: bold; + } + """) + else: + # 数字 + button.setStyleSheet(""" + QPushButton { + background-color: #eeeeee; + border: 1px solid #999999; + font-weight: bold; + } + """) + if cell_value > 0: + # 设置数字颜色 + colors = ["", "#0000ff", "#008000", "#ff0000", "#000080", "#800000", "#008080", "#000000", "#808080"] + button.setText(str(cell_value)) + button.setStyleSheet(f""" + QPushButton {{ + background-color: #eeeeee; + border: 1px solid #999999; + color: {colors[cell_value]}; + font-weight: bold; + }} + """) + else: + button.setText("") + elif cell_state == self.FLAGGED: + # 已标记的方块保持标记状态 + pass + else: + # 隐藏的方块 + button.setText("") + button.setStyleSheet(""" + QPushButton { + background-color: #cccccc; + border: 1px solid #999999; + font-weight: bold; + } + QPushButton:pressed { + background-color: #bbbbbb; + } + """) + + def check_win(self): + """检查是否获胜""" + # 如果所有非地雷方块都已揭开,则获胜 + total_cells = self.ROWS * self.COLS + return self.revealed_count == total_cells - self.MINES + + def check_loss(self, row, col): + """检查是否失败""" + # 如果点击的是地雷,则失败 + return (row, col) in self.mine_positions and self.state[row][col] == self.REVEALED + + def win_game(self): + """游戏胜利""" + self.game_over = True + self.game_won.emit() + + def lose_game(self): + """游戏失败""" + self.game_over = True + + # 显示所有地雷 + for row in range(self.ROWS): + for col in range(self.COLS): + if (row, col) in self.mine_positions and self.state[row][col] != self.FLAGGED: + self.state[row][col] = self.REVEALED + + self.update_display() + self.game_lost.emit() + + def restart_game(self): + """重新开始游戏""" + # 重置游戏状态 + self.init_game() + self.update_display() + self.mine_count_changed.emit(self.MINES) + + +class MinesweeperWindow(QMainWindow): + """扫雷游戏窗口""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("扫雷游戏") + self.setGeometry(200, 200, 400, 500) + + # 创建中央控件 + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # 创建布局 + main_layout = QVBoxLayout(central_widget) + main_layout.setContentsMargins(10, 10, 10, 10) + + # 创建顶部控制面板 + control_layout = QHBoxLayout() + + # 地雷计数器 + self.mine_count_label = QLabel(f"地雷: {15}") + self.mine_count_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #cc0000;") + + # 重新开始按钮 + self.restart_button = QPushButton("😊") + self.restart_button.setFixedSize(40, 40) + self.restart_button.setStyleSheet(""" + QPushButton { + background-color: #ffffff; + border: 2px solid #cccccc; + border-radius: 5px; + font-size: 20px; + } + QPushButton:hover { + background-color: #f0f0f0; + } + """) + self.restart_button.clicked.connect(self.restart_game) + + # 时间计数器 + self.time_label = QLabel("时间: 0") + self.time_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #0066cc;") + + control_layout.addWidget(self.mine_count_label) + control_layout.addStretch() + control_layout.addWidget(self.restart_button) + control_layout.addStretch() + control_layout.addWidget(self.time_label) + + main_layout.addLayout(control_layout) + + # 创建游戏区域 + self.game_widget = MinesweeperGame() + main_layout.addWidget(self.game_widget) + + # 创建提示标签 + self.hint_label = QLabel("左键揭开方块,右键标记地雷") + self.hint_label.setStyleSheet("font-size: 12px; color: gray; text-align: center;") + self.hint_label.setAlignment(Qt.AlignCenter) + main_layout.addWidget(self.hint_label) + + # 连接信号 + self.game_widget.game_won.connect(self.on_game_won) + self.game_widget.game_lost.connect(self.on_game_lost) + self.game_widget.mine_count_changed.connect(self.update_mine_count) + + # 设置窗口样式 + self.setStyleSheet(""" + QMainWindow { + background-color: #f0f0f0; + } + QLabel { + color: #333; + } + """) + + # 初始化计时器 + self.timer = QTimer() + self.timer.timeout.connect(self.update_time) + self.seconds = 0 + self.timer.start(1000) + + def update_mine_count(self, count): + """更新地雷计数显示""" + self.mine_count_label.setText(f"地雷: {count}") + + def update_time(self): + """更新时间显示""" + if not self.game_widget.game_over: + self.seconds += 1 + self.time_label.setText(f"时间: {self.seconds}") + + def on_game_won(self): + """游戏胜利处理""" + self.timer.stop() + self.restart_button.setText("😎") + QMessageBox.information(self, "恭喜", f"你赢了!用时{self.seconds}秒") + + def on_game_lost(self): + """游戏失败处理""" + self.timer.stop() + self.restart_button.setText("😵") + QMessageBox.warning(self, "游戏结束", "你踩到地雷了!") + + def restart_game(self): + """重新开始游戏""" + self.timer.stop() + self.seconds = 0 + self.time_label.setText("时间: 0") + self.restart_button.setText("😊") + self.game_widget.restart_game() + self.timer.start(1000) \ No newline at end of file diff --git a/src/word_main_window.py b/src/word_main_window.py index 28973f1..78a9423 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -818,6 +818,11 @@ class WordStyleMainWindow(QMainWindow): snake_game_action.triggered.connect(self.launch_snake_game) games_menu.addAction(snake_game_action) + # 扫雷游戏 + minesweeper_game_action = QAction('扫雷', self) + minesweeper_game_action.triggered.connect(self.launch_minesweeper_game) + games_menu.addAction(minesweeper_game_action) + # 帮助菜单 help_menu = menubar.addMenu('帮助(H)') @@ -2770,6 +2775,21 @@ class WordStyleMainWindow(QMainWindow): f"无法启动贪吃蛇游戏:{str(e)}" ) + def launch_minesweeper_game(self): + """启动扫雷游戏""" + try: + from ui.minesweeper_game import MinesweeperWindow + + # 创建游戏窗口 + self.minesweeper_game_window = MinesweeperWindow(self) + self.minesweeper_game_window.show() + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"无法启动扫雷游戏:{str(e)}" + ) + def show_about(self): """显示关于对话框""" # 创建自定义对话框 diff --git a/test_minesweeper.py b/test_minesweeper.py new file mode 100644 index 0000000..8ca5cdb --- /dev/null +++ b/test_minesweeper.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +扫雷游戏测试脚本 +用于测试扫雷游戏模块是否能正常运行 +""" + +import sys +import os +import platform + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# 设置Qt平台插件路径 - 增强版本,完全避免平台插件问题 +def set_qt_plugin_path(): + """设置Qt平台插件路径,确保所有平台插件都能正确加载""" + system = platform.system() + + # 获取Python版本 + python_version = f"python{sys.version_info.major}.{sys.version_info.minor}" + + # 可能的Qt插件路径列表 + possible_paths = [] + + if system == "Windows": + # Windows环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'Python', 'Python39', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + ]) + elif system == "Darwin": # macOS + # macOS环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/local/opt/qt5/plugins', # Homebrew Qt5 + '/opt/homebrew/opt/qt5/plugins', # Apple Silicon Homebrew + os.path.expanduser('~/Qt/5.15.2/clang_64/plugins'), # Qt官方安装 + ]) + elif system == "Linux": + # Linux环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/lib/x86_64-linux-gnu/qt5/plugins', + '/usr/lib/qt5/plugins', + ]) + + # 查找第一个存在的路径 + valid_path = None + for path in possible_paths: + if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')): + valid_path = path + break + + if valid_path: + # 设置Qt插件路径 + os.environ['QT_PLUGIN_PATH'] = valid_path + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(valid_path, 'platforms') + + # 设置平台特定的环境变量 + if system == "Darwin": # macOS + os.environ['QT_QPA_PLATFORM'] = 'cocoa' + os.environ['QT_MAC_WANTS_LAYER'] = '1' + # 禁用可能导致问题的Qt功能 + os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' # 禁用Qt警告日志 + elif system == "Windows": + os.environ['QT_QPA_PLATFORM'] = 'windows' + elif system == "Linux": + os.environ['QT_QPA_PLATFORM'] = 'xcb' + # 对于Linux,可能需要设置DISPLAY + if 'DISPLAY' not in os.environ: + os.environ['DISPLAY'] = ':0' + + print(f"✅ Qt插件路径设置成功: {valid_path}") + return True + else: + print("⚠️ 警告:未找到Qt插件路径") + return False + +# 设置Qt平台插件路径 +set_qt_plugin_path() + +from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import Qt +from src.ui.minesweeper_game import MinesweeperWindow + +def main(): + # 设置高DPI支持(必须在QApplication创建之前) + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + app = QApplication(sys.argv) + + # 设置应用程序样式 + if platform.system() != "Darwin": # 不是macOS系统 + app.setStyle('WindowsVista') + + # 创建并显示扫雷游戏窗口 + window = MinesweeperWindow() + window.show() + + sys.exit(app.exec_()) + +if __name__ == '__main__': + main() \ No newline at end of file