扫雷 #122

Merged
p9o3yklam merged 3 commits from xinglin-shi into main 3 months ago

@ -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. 环境变量已正确设置(通常由应用自动处理)
如有任何问题,请联系开发者。

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -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()

@ -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)

@ -813,6 +813,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('帮助')
@ -2894,6 +2899,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):
"""显示关于对话框"""
# 创建自定义对话框

@ -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()
Loading…
Cancel
Save