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