使用Pygame实现数字益智小游戏

使用Pygame实现数字益智小游戏

简介

这款小游戏将经典的迷宫寻路与数学运算相结合,通过算法确保可玩性,是一款兼具趣味性和教育意义的数字益智游戏。使用 Pygame 绘制界面。

运行截图:

游戏核心机制

玩家 在迷宫格子上移动,每走一步会消耗 步数。

数字格(绿色)存储一个整数值,当玩家 持有运算符 时踏上数字格,会对玩家当前数字执行运算。

运算符格(黄/红/紫/青)分别代表 +、−、×、÷,踏上后 拾取 该运算符(替换旧运算符),但 不立即运算。

目标格(橙色)显示目标数字,玩家抵达时若当前数字与目标一致则 过关,否则 失败。

障碍格(深灰)不可通过,增加路径选择难度。

计分规则:通关奖励 = max(0, 1000 - 步数 × 10),鼓励用最少步数完成。

迷宫生成与可解性保证

1 随机生成

使用二维列表 maze 表示迷宫,每个格子记录类型和数值(若有)。

起点(左上角)和终点(右下角)固定为数字格,其余格子按概率随机分配类型(含障碍),并随等级动态调整数值范围。

2 通路检查(is_reachable)

利用 广度优先搜索(BFS) 检查是否至少有一条物理通路(无视数字运算)从起点到终点,若无则重新生成。

3 可解性验证(check_solvable)

在迷宫生成后,再次使用 BFS 搜索 状态空间。每个状态为 (x, y, 当前数字, 持有的运算符)。

模拟所有可能的移动和运算,检查是否存在一条路径使玩家到达终点时数字等于目标。

若不存在,则抛弃该迷宫并重新生成,直到有解为止。这保证了每一关都有解法(但玩家仍需自己寻找)。

玩家移动与运算逻辑

玩家移动时,代码通过 方向向量(上下左右)计算新坐标,并检查越界和障碍。

若目标格子是普通数字格且玩家持有运算符,则执行对应运算(除法仅当整除时生效,否则保留运算符)。

若目标格子是运算符格,则替换玩家持有的运算符。

每次移动都会记录步数,并存储路径(用于可能的回溯/显示)。

自动演示功能

利用 BFS 搜索完整解路径(find_solution),不仅验证可解,还记录每一步的动作(方向)。启动演示后,程序按一定帧延迟逐步执行该动作序列,模拟玩家自动通关。

演示期间键盘输入被屏蔽,按 ESC 可终止并复位玩家到起点。

依赖库及安装说明

|---------------------|-------------------------------------------|--------------------|
| 库名 | 功能简介 | 是否需要安装 |
| pygame | 提供游戏窗口、图形绘制、事件处理(键盘/退出)、字体渲染、音频(预留)等核心功能。 | 需要安装(非标准库) |
| random | 生成随机数,用于迷宫格子类型和数字的随机分配。 | 标准库,无需额外安装 |
| sys | 提供 sys.exit() 用于退出程序。 | 标准库,无需额外安装 |
| enum | 提供 Enum 基类,用于定义游戏状态和格子类型枚举。 | 标准库,无需额外安装 |
| collections | 提供 deque(双端队列),用于 BFS 搜索中的队列。 | 标准库,无需额外安装 |

安装 pygame 的命令

在命令行中运行:

pip install pygame

验证安装,在shell中运行:

import pygame

print(pygame.version.ver)

关于Python中的pygame游戏模块的介绍,可参见 https://blog.csdn.net/cnds123/article/details/119514520

源码

(由DeepSeek辅助生成)

python 复制代码
# =============================================================================
# 数字迷宫冒险 (Number Maze Adventure)
# 一款结合迷宫探索与数字运算的益智游戏。
# 玩家在迷宫中移动,拾取运算符并与数字格运算,最终使到达终点时的数字等于目标值。
# 使用 Pygame 实现,自动生成有解的迷宫,并支持自动演示功能。
# =============================================================================

import pygame
import random
import sys
from enum import Enum
from collections import deque

# ----------------------------- Pygame 初始化 ----------------------------------
pygame.init()
pygame.mixer.init()          # 初始化音频(预留)

# ------------------------------ 屏幕设置 -------------------------------------
WIDTH, HEIGHT = 1000, 700
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("数字迷宫冒险")

# ------------------------------ 颜色定义 -------------------------------------
# 使用 RGB 元组定义游戏中使用的颜色,便于统一调整主题
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 50, 50)
GREEN = (50, 255, 100)
BLUE = (80, 180, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 165, 0)
PURPLE = (180, 80, 220)
CYAN = (80, 220, 220)
GRAY = (150, 150, 150)
DARK_GRAY = (80, 80, 80)

# ------------------------------ 字体设置 -------------------------------------
# 使用系统宋体以支持中文显示,若系统无此字体可替换为其他中文字体
font = pygame.font.SysFont("simsun", 30)          # 普通文本
big_font = pygame.font.SysFont("simsun", 60)      # 大标题/弹窗
small_font = pygame.font.SysFont("simsun", 22)    # 格子内数字/运算符
info_font = pygame.font.SysFont("simsun", 26)     # 顶部信息栏
rule_font = pygame.font.SysFont("simsun", 20)     # 右侧规则面板

# ------------------------------ 游戏状态枚举 --------------------------------
class GameState(Enum):
    """游戏当前所处的状态"""
    PLAYING = 1          # 正常进行中
    GAME_OVER = 2        # 失败
    LEVEL_COMPLETE = 3   # 关卡通过
    MENU = 4             # 菜单(预留)

# ------------------------------ 格子类型枚举 --------------------------------
class NumberType(Enum):
    """迷宫格子类型"""
    NORMAL = 1      # 普通数字格:包含一个整数值,可参与运算
    PLUS = 2        # 加法运算符格
    MINUS = 3       # 减法运算符格
    MULTIPLY = 4    # 乘法运算符格
    DIVIDE = 5      # 除法运算符格
    GOAL = 6        # 目标格(终点),显示目标数字
    OBSTACLE = 7    # 障碍格,不可通行

# =============================================================================
# 玩家类
# =============================================================================
class Player:
    """表示玩家角色,包含位置、当前数字、持有的运算符、移动步数等状态"""

    def __init__(self, x, y, start_number):
        """
        初始化玩家
        :param x: 列索引(水平位置)
        :param y: 行索引(垂直位置)
        :param start_number: 起点的初始数字
        """
        self.x = x
        self.y = y
        self.number = start_number      # 当前数字
        self.operator = None            # 当前持有的运算符,为 None 或 NumberType 中的 PLUS/MINUS/MULTIPLY/DIVIDE
        self.moves = 0                  # 累计移动次数
        self.path = [(x, y)]            # 走过的路径(坐标列表)

    def move(self, dx, dy, maze):
        """
        尝试向 (dx, dy) 方向移动,并处理格子效果。
        :param dx: 水平偏移量(-1,0,1)
        :param dy: 垂直偏移量(-1,0,1)
        :param maze: 迷宫二维列表
        :return: 是否成功移动(True/False)
        """
        new_x, new_y = self.x + dx, self.y + dy
        # 边界检查
        if not (0 <= new_x < len(maze[0]) and 0 <= new_y < len(maze)):
            return False
        cell = maze[new_y][new_x]
        # 障碍物检查
        if cell['type'] == NumberType.OBSTACLE:
            return False

        # 执行移动,更新位置和路径
        self.x, self.y = new_x, new_y
        self.path.append((self.x, self.y))
        self.moves += 1

        # 根据目标格子类型处理
        if cell['type'] == NumberType.NORMAL:
            # 普通数字格:若持有运算符,则进行运算,然后清除运算符
            if self.operator is not None:
                num = cell['value']
                if self.operator == NumberType.PLUS:
                    self.number += num
                    self.operator = None
                elif self.operator == NumberType.MINUS:
                    self.number -= num
                    self.operator = None
                elif self.operator == NumberType.MULTIPLY:
                    self.number *= num
                    self.operator = None
                elif self.operator == NumberType.DIVIDE:
                    # 除法仅当能整除时才执行,否则保留运算符不变
                    if num != 0 and self.number % num == 0:
                        self.number //= num
                        self.operator = None
                    # 不能整除则保留运算符,不改变数字
        elif cell['type'] in [NumberType.PLUS, NumberType.MINUS,
                              NumberType.MULTIPLY, NumberType.DIVIDE]:
            # 运算符格:拾取运算符(覆盖旧运算符)
            self.operator = cell['type']
        # 目标格(GOAL)不做任何处理,由游戏主逻辑判断胜负
        return True

    def reset_to_start(self, start_number):
        """
        重置玩家到起点,用于重新开始或演示结束。
        :param start_number: 起点的数字
        """
        self.x = 0
        self.y = 0
        self.number = start_number
        self.operator = None
        self.moves = 0
        self.path = [(0, 0)]

    def draw(self, surface, cell_size, maze_start_x, maze_start_y):
        """
        在屏幕上绘制玩家。
        :param surface: pygame 表面
        :param cell_size: 格子像素大小
        :param maze_start_x: 迷宫区域左上角 x 偏移
        :param maze_start_y: 迷宫区域左上角 y 偏移
        """
        # 计算玩家中心坐标
        x_pos = maze_start_x + self.x * cell_size + cell_size // 2
        y_pos = maze_start_y + self.y * cell_size + cell_size // 2

        # 绘制蓝色圆形(玩家本体)
        pygame.draw.circle(surface, BLUE, (x_pos, y_pos), cell_size // 3)
        pygame.draw.circle(surface, WHITE, (x_pos, y_pos), cell_size // 3, 2)

        # 显示当前数字
        text = small_font.render(str(self.number), True, WHITE)
        text_rect = text.get_rect(center=(x_pos, y_pos))
        surface.blit(text, text_rect)

        # 若持有运算符,在玩家右上角显示运算符符号
        if self.operator:
            op_symbols = {
                NumberType.PLUS: "+",
                NumberType.MINUS: "-",
                NumberType.MULTIPLY: "×",
                NumberType.DIVIDE: "÷"
            }
            op_text = small_font.render(op_symbols[self.operator], True, YELLOW)
            op_rect = op_text.get_rect(center=(x_pos + cell_size // 4, y_pos - cell_size // 4))
            surface.blit(op_text, op_rect)


# =============================================================================
# 游戏主类
# =============================================================================
class NumberMazeGame:
    """游戏核心控制类,管理迷宫生成、玩家状态、游戏循环等"""

    def __init__(self):
        """初始化游戏参数,并开始第一关"""
        # 迷宫显示参数
        self.cell_size = 60                # 每个格子的像素尺寸
        self.maze_width = 10               # 迷宫列数
        self.maze_height = 8               # 迷宫行数
        self.maze_start_x = 10             # 迷宫区域左上角 x 偏移(留边距)
        self.maze_start_y = 100            # 迷宫区域左上角 y 偏移(为标题留空间)

        # 自动演示相关
        self.demo_mode = False             # 是否正在演示
        self.demo_actions = []             # 演示动作序列 [(dx, dy), ...]
        self.demo_index = 0                # 当前执行到的动作索引
        self.demo_timer = 0                # 帧计数器
        self.demo_delay = 15               # 每多少帧执行一步(值越大演示越慢)

        self.reset()                       # 开始游戏(等级1,分数0)

    def reset(self):
        """完全重置游戏(从第一关开始)"""
        self.level = 1
        self.score = 0
        self.reset_level()

    def reset_level(self):
        """重置当前关卡(保留等级和分数),生成新迷宫并重置玩家"""
        self.state = GameState.PLAYING
        self.generate_maze()
        self.demo_mode = False
        self.demo_actions = []
        self.demo_index = 0

    # ---------------------- 迷宫生成与可解性检查 --------------------------

    def is_reachable(self, maze):
        """
        使用 BFS 检查迷宫是否存在物理通路(忽略数字运算)。
        :param maze: 迷宫数据
        :return: True/False
        """
        start = (0, 0)
        end = (self.maze_width - 1, self.maze_height - 1)
        visited = set()
        queue = deque([start])
        visited.add(start)
        directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        while queue:
            x, y = queue.popleft()
            if (x, y) == end:
                return True
            for dx, dy in directions:
                nx, ny = x + dx, y + dy
                if (0 <= nx < self.maze_width and 0 <= ny < self.maze_height and
                        (nx, ny) not in visited and
                        maze[ny][nx]['type'] != NumberType.OBSTACLE):
                    visited.add((nx, ny))
                    queue.append((nx, ny))
        return False

    def check_solvable(self, start_num, target_num):
        """
        使用 BFS 搜索状态空间,检查是否存在一条路径可使玩家到达终点时数字等于目标。
        状态为 (x, y, 当前数字, 持有的运算符)。
        :param start_num: 起点数字
        :param target_num: 目标数字
        :return: True 表示有解,False 表示无解
        """
        start_state = (0, 0, start_num, None)
        visited = set([start_state])
        q = deque([start_state])
        directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        while q:
            x, y, num, op = q.popleft()
            # 到达终点且数字匹配
            if x == self.maze_width - 1 and y == self.maze_height - 1:
                if num == target_num:
                    return True
            # 限制数字范围,防止无限增长
            if abs(num) > 1000:
                continue

            for dx, dy in directions:
                nx, ny = x + dx, y + dy
                if not (0 <= nx < self.maze_width and 0 <= ny < self.maze_height):
                    continue
                cell = self.maze[ny][nx]
                if cell['type'] == NumberType.OBSTACLE:
                    continue

                new_num = num
                new_op = op

                # 模拟移动到该格子的效果
                if cell['type'] == NumberType.NORMAL:
                    if new_op is not None:
                        val = cell['value']
                        if new_op == NumberType.PLUS:
                            new_num += val
                            new_op = None
                        elif new_op == NumberType.MINUS:
                            new_num -= val
                            new_op = None
                        elif new_op == NumberType.MULTIPLY:
                            new_num *= val
                            new_op = None
                        elif new_op == NumberType.DIVIDE:
                            if val != 0 and new_num % val == 0:
                                new_num //= val
                                new_op = None
                elif cell['type'] in [NumberType.PLUS, NumberType.MINUS,
                                      NumberType.MULTIPLY, NumberType.DIVIDE]:
                    new_op = cell['type']

                state = (nx, ny, new_num, new_op)
                if state not in visited:
                    visited.add(state)
                    q.append(state)
        return False

    def find_solution(self, start_num, target_num):
        """
        使用 BFS 搜索并返回一条可行路径的动作序列。
        与 check_solvable 类似,但额外记录父状态以回溯路径。
        :param start_num: 起点数字
        :param target_num: 目标数字
        :return: 动作列表 [(dx, dy), ...] 或 None(若未找到)
        """
        start_state = (0, 0, start_num, None)
        parent = {start_state: (None, None)}   # state -> (前驱state, 动作)
        q = deque([start_state])
        directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        end_state = None

        while q:
            x, y, num, op = q.popleft()
            if x == self.maze_width - 1 and y == self.maze_height - 1:
                if num == target_num:
                    end_state = (x, y, num, op)
                    break
            if abs(num) > 1000:
                continue

            for dx, dy in directions:
                nx, ny = x + dx, y + dy
                if not (0 <= nx < self.maze_width and 0 <= ny < self.maze_height):
                    continue
                cell = self.maze[ny][nx]
                if cell['type'] == NumberType.OBSTACLE:
                    continue

                new_num = num
                new_op = op

                if cell['type'] == NumberType.NORMAL:
                    if new_op is not None:
                        val = cell['value']
                        if new_op == NumberType.PLUS:
                            new_num += val
                            new_op = None
                        elif new_op == NumberType.MINUS:
                            new_num -= val
                            new_op = None
                        elif new_op == NumberType.MULTIPLY:
                            new_num *= val
                            new_op = None
                        elif new_op == NumberType.DIVIDE:
                            if val != 0 and new_num % val == 0:
                                new_num //= val
                                new_op = None
                elif cell['type'] in [NumberType.PLUS, NumberType.MINUS,
                                      NumberType.MULTIPLY, NumberType.DIVIDE]:
                    new_op = cell['type']

                new_state = (nx, ny, new_num, new_op)
                if new_state not in parent:
                    parent[new_state] = ((x, y, num, op), (dx, dy))
                    q.append(new_state)

        if end_state is None:
            return None

        # 回溯路径,从终点反向到起点
        actions = []
        state = end_state
        while parent[state][0] is not None:
            prev_state, action = parent[state]
            actions.append(action)   # action 是 (dx, dy)
            state = prev_state
        actions.reverse()
        return actions

    def generate_maze(self):
        """
        生成迷宫,要求:
        1. 有物理通路(is_reachable)
        2. 有数字解(check_solvable)
        若不满足则重新生成,直至成功。
        生成后初始化玩家。
        """
        while True:
            # 构建迷宫二维列表
            self.maze = []
            for y in range(self.maze_height):
                row = []
                for x in range(self.maze_width):
                    if x == 0 and y == 0:
                        # 起点:普通数字格,随机生成起点数字,范围随等级提升
                        start_num = random.randint(1, 3 + self.level)
                        cell = {'type': NumberType.NORMAL, 'value': start_num}
                    elif x == self.maze_width - 1 and y == self.maze_height - 1:
                        # 终点(目标格):目标数字基于起点数字生成,确保有挑战性
                        min_target = max(1, start_num - 5 * self.level)
                        max_target = start_num + 10 * self.level
                        target_num = random.randint(min_target, max_target)
                        cell = {'type': NumberType.GOAL, 'value': target_num}
                    else:
                        # 其他格子:按概率随机分配类型
                        r = random.random()
                        if r < 0.10:
                            cell = {'type': NumberType.OBSTACLE, 'value': None}
                        elif r < 0.45:
                            cell = {'type': NumberType.NORMAL, 'value': random.randint(1, 4 + self.level // 2)}
                        elif r < 0.65:
                            cell = {'type': NumberType.PLUS, 'value': None}
                        elif r < 0.80:
                            cell = {'type': NumberType.MINUS, 'value': None}
                        elif r < 0.92:
                            cell = {'type': NumberType.MULTIPLY, 'value': None}
                        else:
                            cell = {'type': NumberType.DIVIDE, 'value': None}
                    row.append(cell)
                self.maze.append(row)

            # 检查物理通路
            if not self.is_reachable(self.maze):
                continue

            # 提取起点和终点的数字
            start_num = self.maze[0][0]['value']
            target_num = self.maze[self.maze_height - 1][self.maze_width - 1]['value']

            # 检查数字可解性
            if self.check_solvable(start_num, target_num):
                self.start_number = start_num
                self.target_number = target_num
                self.player = Player(0, 0, start_num)
                break   # 生成成功,退出循环

    # -------------------------- 自动演示 ----------------------------------

    def start_demo(self):
        """启动自动演示:寻找一条解法路径,并逐步执行"""
        if self.demo_mode or self.state != GameState.PLAYING:
            return
        actions = self.find_solution(self.start_number, self.target_number)
        if actions is None:
            return   # 理论上不会发生(因为生成时保证了有解)
        self.demo_actions = actions
        self.demo_index = 0
        self.demo_timer = self.demo_delay
        self.demo_mode = True
        self.player.reset_to_start(self.start_number)

    def stop_demo(self):
        """停止演示并重置玩家到起点"""
        self.demo_mode = False
        self.demo_actions = []
        self.demo_index = 0
        self.player.reset_to_start(self.start_number)

    # -------------------------- 事件处理 ----------------------------------

    def handle_events(self):
        """处理所有 pygame 事件(键盘、退出等)"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            if event.type == pygame.KEYDOWN:
                # 演示模式下,只允许 ESC 取消演示
                if self.demo_mode:
                    if event.key == pygame.K_ESCAPE:
                        self.stop_demo()
                    continue

                # 游戏进行中:方向键控制移动,A 启动演示
                if self.state == GameState.PLAYING:
                    if event.key == pygame.K_UP:
                        self.player.move(0, -1, self.maze)
                    elif event.key == pygame.K_DOWN:
                        self.player.move(0, 1, self.maze)
                    elif event.key == pygame.K_LEFT:
                        self.player.move(-1, 0, self.maze)
                    elif event.key == pygame.K_RIGHT:
                        self.player.move(1, 0, self.maze)
                    elif event.key == pygame.K_a:
                        self.start_demo()

                # R 键:在结束状态(失败/通关)时重置当前关卡
                if event.key == pygame.K_r:
                    if self.state in [GameState.GAME_OVER, GameState.LEVEL_COMPLETE]:
                        self.reset_level()

                # N 键:通关后进入下一关
                if event.key == pygame.K_n and self.state == GameState.LEVEL_COMPLETE:
                    # 计算奖励分数并累加,等级+1
                    self.score += max(0, 1000 - self.player.moves * 10)
                    self.level += 1
                    self.reset_level()

    # -------------------------- 更新逻辑 ----------------------------------

    def update(self):
        """
        更新游戏逻辑:
        - 演示模式下逐步执行动作
        - 非演示模式检查玩家是否到达终点,判定胜负
        """
        # 演示模式更新
        if self.demo_mode:
            if self.demo_index < len(self.demo_actions):
                self.demo_timer -= 1
                if self.demo_timer <= 0:
                    dx, dy = self.demo_actions[self.demo_index]
                    self.player.move(dx, dy, self.maze)
                    self.demo_index += 1
                    self.demo_timer = self.demo_delay
            else:
                self.stop_demo()   # 演示完成
            return

        # 非演示模式
        if self.state != GameState.PLAYING:
            return

        # 检查是否到达终点
        target_x, target_y = self.maze_width - 1, self.maze_height - 1
        if self.player.x == target_x and self.player.y == target_y:
            if self.player.number == self.target_number:
                self.state = GameState.LEVEL_COMPLETE
            else:
                self.state = GameState.GAME_OVER

    # -------------------------- 绘制界面 ----------------------------------

    def draw(self):
        """绘制所有图形界面元素"""
        screen.fill(BLACK)

        # ---------- 标题 ----------
        title_text = big_font.render("数字迷宫冒险", True, YELLOW)
        screen.blit(title_text, (WIDTH // 2 - title_text.get_width() // 2, 3))

        # ---------- 顶部信息栏 ----------
        info_bg = pygame.Rect(0, 62, WIDTH, 30)
        pygame.draw.rect(screen, DARK_GRAY, info_bg)

        score_text = info_font.render(f"分数:{self.score}", True, WHITE)
        level_text = info_font.render(f"关卡:{self.level}", True, YELLOW)
        moves_text = info_font.render(f"步数:{self.player.moves}", True, GREEN)

        current_str = f"当前:{self.player.number}"
        if self.player.operator:
            op_sym = {NumberType.PLUS: "+", NumberType.MINUS: "-",
                      NumberType.MULTIPLY: "×", NumberType.DIVIDE: "÷"}
            current_str += f" {op_sym[self.player.operator]}"
        current_text = info_font.render(current_str, True, CYAN)
        target_text = info_font.render(f"目标:{self.target_number}", True, ORANGE)

        # 调整各信息项的水平位置
        screen.blit(score_text, (10, 60))
        screen.blit(level_text, (180, 60))
        screen.blit(moves_text, (300, 60))
        screen.blit(current_text, (450, 60))
        screen.blit(target_text, (680, 60))

        # ---------- 绘制迷宫网格 ----------
        for y, row in enumerate(self.maze):
            for x, cell in enumerate(row):
                rect_x = self.maze_start_x + x * self.cell_size
                rect_y = self.maze_start_y + y * self.cell_size

                # 根据格子类型选择颜色和边框
                if cell['type'] == NumberType.NORMAL:
                    color = GREEN
                    border = WHITE
                elif cell['type'] == NumberType.PLUS:
                    color = YELLOW
                    border = ORANGE
                elif cell['type'] == NumberType.MINUS:
                    color = RED
                    border = DARK_GRAY
                elif cell['type'] == NumberType.MULTIPLY:
                    color = PURPLE
                    border = CYAN
                elif cell['type'] == NumberType.DIVIDE:
                    color = CYAN
                    border = BLUE
                elif cell['type'] == NumberType.GOAL:
                    color = ORANGE
                    border = YELLOW
                elif cell['type'] == NumberType.OBSTACLE:
                    color = DARK_GRAY
                    border = GRAY
                else:
                    color = GRAY
                    border = DARK_GRAY

                # 绘制底色和边框
                pygame.draw.rect(screen, color, (rect_x, rect_y, self.cell_size, self.cell_size))
                pygame.draw.rect(screen, border, (rect_x, rect_y, self.cell_size, self.cell_size), 2)

                # 绘制格子内容(文字或符号)
                if cell['type'] != NumberType.OBSTACLE:
                    if cell['type'] == NumberType.PLUS:
                        text = "+"
                    elif cell['type'] == NumberType.MINUS:
                        text = "-"
                    elif cell['type'] == NumberType.MULTIPLY:
                        text = "×"
                    elif cell['type'] == NumberType.DIVIDE:
                        text = "÷"
                    elif cell['type'] == NumberType.GOAL:
                        # 目标格显示"目标"和数字两行
                        lines = ["目标", str(cell['value'])]
                        for i, line in enumerate(lines):
                            t = small_font.render(line, True, BLACK)
                            t_rect = t.get_rect(center=(rect_x + self.cell_size // 2,
                                                        rect_y + self.cell_size // 2 - 10 + i * 22))
                            screen.blit(t, t_rect)
                        continue
                    else:  # NORMAL
                        text = str(cell['value'])

                    t = small_font.render(text, True, BLACK)
                    t_rect = t.get_rect(center=(rect_x + self.cell_size // 2, rect_y + self.cell_size // 2))
                    screen.blit(t, t_rect)

        # ---------- 绘制玩家 ----------
        self.player.draw(screen, self.cell_size, self.maze_start_x, self.maze_start_y)

        # ---------- 右侧规则面板 ----------
        panel_x = self.maze_start_x + self.maze_width * self.cell_size + 15
        panel_y = self.maze_start_y
        panel_width = WIDTH - panel_x - 10
        panel_height = HEIGHT - self.maze_start_y - 120  # 底部留出控制栏空间

        # 半透明深色背景
        panel_bg = pygame.Rect(panel_x, panel_y, panel_width, panel_height)
        pygame.draw.rect(screen, (20, 20, 30, 200), panel_bg)   # 半透明
        pygame.draw.rect(screen, GRAY, panel_bg, 2)              # 边框

        # 规则标题
        rule_title = rule_font.render("--- 游戏规则 ---", True, YELLOW)
        screen.blit(rule_title, (panel_x + 10, panel_y + 10))

        # 规则内容列表
        rules = [
            "目标:到达终点时数字等于目标值",
            "运算符格:拾取运算符(覆盖旧)",
            "数字格:使用拾取的运算符进行运算",
            "除法:仅当整除时才生效,否则保留",
            "运算符",
            "障碍:不可通过",
            "方向键:移动玩家",
            "R:重置当前关卡(等级/分数不变)",
            "N:通关后进入下一关",
            "A:自动演示,此时按ESC取消"
        ]

        # 逐行渲染
        y_offset = panel_y + 50
        for line in rules:
            text = rule_font.render(line, True, WHITE)
            screen.blit(text, (panel_x + 10, y_offset))
            y_offset += 30

        # ---------- 底部控制栏 ----------
        ctrl_bg = pygame.Rect(0, HEIGHT - 40, WIDTH, 40)
        pygame.draw.rect(screen, DARK_GRAY, ctrl_bg)

        tips = [
            "方向键:移动",
            "R:重新开始本关",
            "N:下一关",
            "A:自动演示"
        ]
        for i, tip in enumerate(tips):
            t = info_font.render(tip, True, GRAY)
            screen.blit(t, (WIDTH // 2 - 400 + i * 220, HEIGHT - 35))

        # 演示模式提示
        if self.demo_mode:
            demo_info = info_font.render("演示中... (按 ESC 取消)", True, RED)
            screen.blit(demo_info, (WIDTH // 2 - demo_info.get_width() // 2, 45))

        # ---------- 游戏状态弹窗(遮罩 + 文字) ----------
        if self.state == GameState.GAME_OVER:
            # 半透明黑色遮罩
            overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
            overlay.fill((0, 0, 0, 180))
            screen.blit(overlay, (0, 0))

            over_text = big_font.render("任务失败!", True, (255, 200, 200))
            over_rect = over_text.get_rect(center=(WIDTH//2, HEIGHT//2 - 80))
            screen.blit(over_text, over_rect)

            msg_text = font.render(f"你的数字是 {self.player.number},需要 {self.target_number}", True, WHITE)
            msg_rect = msg_text.get_rect(center=(WIDTH//2, HEIGHT//2 - 10))
            screen.blit(msg_text, msg_rect)

            restart_text = font.render("按 R 重新开始本关", True, (255, 255, 100))
            restart_rect = restart_text.get_rect(center=(WIDTH//2, HEIGHT//2 + 60))
            screen.blit(restart_text, restart_rect)

        elif self.state == GameState.LEVEL_COMPLETE:
            overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
            overlay.fill((0, 0, 0, 160))
            screen.blit(overlay, (0, 0))

            complete_text = big_font.render("关卡完成!", True, (200, 255, 200))
            complete_rect = complete_text.get_rect(center=(WIDTH//2, HEIGHT//2 - 80))
            screen.blit(complete_text, complete_rect)

            bonus = max(0, 1000 - self.player.moves * 10)
            bonus_text = font.render(f"奖励分数:{bonus}", True, YELLOW)
            bonus_rect = bonus_text.get_rect(center=(WIDTH//2, HEIGHT//2 - 10))
            screen.blit(bonus_text, bonus_rect)

            next_text = font.render("按 N 进入下一关", True, (100, 255, 255))
            next_rect = next_text.get_rect(center=(WIDTH//2, HEIGHT//2 + 60))
            screen.blit(next_text, next_rect)

        # 更新显示
        pygame.display.flip()

    # -------------------------- 主循环 ----------------------------------

    def run(self):
        """游戏主循环,以 60 FPS 运行"""
        clock = pygame.time.Clock()
        while True:
            self.handle_events()
            self.update()
            self.draw()
            clock.tick(60)


# =============================================================================
# 程序入口
# =============================================================================
if __name__ == "__main__":
    game = NumberMazeGame()
    game.run()