# game.py

# ==============================================================================
# --- 1. 导入所有需要的库 (IMPORT LIBRARIES) ---
# ==============================================================================
# 导入库就像是为你的项目准备工具箱，每个库都有特定的功能。

import pygame  # Pygame库，是制作2D游戏的核心，负责处理图形、声音、用户输入等。
import sys     # 系统库，在这里主要用于安全地退出程序 (sys.exit())。
import os      # 操作系统库，用于处理文件和文件夹路径，能确保在Windows、Mac、Linux上路径格式都正确。
import random  # 随机库，用于生成随机数，比如随机生成方块的数字。
import time    # 时间库，用于处理时间相关的操作，比如实现动画的延迟效果。
from typing import List, Tuple, Dict, Any, Optional # 类型提示库，它不影响程序运行，但能让代码更清晰、更易读，方便开发者理解变量应该是什么类型。

# ==============================================================================
# --- 2. 游戏配置 (GAME CONFIGURATION) ---
# 您可以在此区域内，自由修改游戏的所有参数，无需触碰下方的核心代码。
# 这里就像是游戏的“控制面板”，调整数值就能改变游戏玩法和外观。
# ==============================================================================

# --- 2.1 核心逻辑配置 (Core Logic) ---
# ------------------------------------------------------------------------------
BOARD_SIZE = 6                  # 棋盘尺寸。例如 6 就是 6x6 的棋盘。
INITIAL_OPPORTUNITIES = 10       # 【重要】初始机会次数。这是游戏开始时玩家拥有的点击次数。
MAX_OPPORTUNITIES = 10           # 允许通过消除方块累计的最大机会次数。
MAX_INITIAL_NUMBER = 6         # 游戏开始或补充新方块时，生成的数字的最大值。例如设为10，则会随机生成1到10的整数。

# --- 2.2 屏幕与布局 (Screen & Layout) ---
# ------------------------------------------------------------------------------
SCREEN_WIDTH = 900              # 游戏窗口的宽度（单位：像素）。
SCREEN_HEIGHT = 1000            # 游戏窗口的高度（单位：像素）。
FPS = 60                        # 游戏的目标帧率 (Frames Per Second)。越高画面越流畅，但对电脑性能要求也越高。60是理想值。
CELL_SIZE = 100                 # 棋盘上每个方块的边长（单位：像素）。
GRID_GAP = 12                   # 棋盘上每个方块之间的间隙（单位：像素）。
BOARD_BG_COLOR = (44, 62, 80)   # 棋盘的背景颜色 (RGB格式)。
EMPTY_CELL_COLOR = (52, 73, 94) # 棋盘上空格子（等待方块填充时）的颜色 (RGB格式)。

# --- 2.3 UI 可视化配置 (UI VISUAL SETTINGS) ---
# ------------------------------------------------------------------------------
# 【重要】这里是您定制游戏“颜值”的核心区域。
UI_SETTINGS = {
    # --- 主菜单界面 (Main Menu) ---
    "MENU_TITLE_LINE1": "Number",                # 菜单标题第一行
    "MENU_TITLE_LINE2": "Evolve",                # 菜单标题第二行
    "MENU_TITLE_POS": (SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 150), # 标题的中心坐标 (X, Y)。修改第二个值可上下移动标题。
    "MENU_TITLE_LINE_SPACING": 70,               # 标题有多行时，行与行之间的垂直距离。
    "MENU_TITLE_FONT_SIZE": 40,                  # 标题的字体大小。
    "MENU_SUBTITLE_POS": (SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 40), # 副标题的中心坐标 (X, Y)。
    "MENU_SUBTITLE_FONT_SIZE": 14,               # 副标题的字体大小。
    "MENU_START_BUTTON_POS": (SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 80), # “开始游戏”按钮的中心坐标 (X, Y)。
    "MENU_BUTTON_SIZE": (360, 80),               # 菜单上按钮的尺寸 (宽度, 高度)。
    "MENU_BUTTON_FONT_SIZE": 32,                 # 菜单上按钮的字体大小。

    # --- 游戏内方块 (In-Game Cells) ---
    "CELL_NUMBER_FONT_SIZE": 45,                 # 【重要】棋盘方块内数字的字体大小。

    # --- 顶部信息栏 - 通用设置 (In-Game HUD - General) ---
    "HUD_HEIGHT": 200,                           # 顶部信息栏的整体高度。
    "HUD_BG_COLOR": (52, 73, 94),                # 顶部信息栏的背景颜色。
    "HUD_LABEL_VALUE_SPACING": 40,               # 【重要】信息栏中“标签(如Score)”和其下方“数值(如100)”之间的【行间距】。

    # --- 顶部信息栏 - “分数”模块 (HUD - Score Module) ---
    "HUD_SCORE_LABEL_POS": (225, 140),           # “Score”标签的 (X, Y) 坐标。
    "HUD_SCORE_LABEL_FONT_SIZE": 22,             # “Score”标签的字体大小。
    "HUD_SCORE_VALUE_FONT_SIZE": 36,             # 分数数值的字体大小。
    "HUD_SCORE_VALUE_COLOR": (255, 215, 0),      # 分数数值的颜色 (金色)。

    # --- 顶部信息栏 - “机会”模块 (HUD - Tries Module) ---
    "HUD_TRIES_LABEL_POS": (450, 140),           # “Tries”标签的 (X, Y) 坐标。
    "HUD_TRIES_LABEL_FONT_SIZE": 22,             # “Tries”标签的字体大小。
    "HUD_TRIES_STARS_FONT_SIZE": 36,             # 机会星星的字体大小。
    "HUD_TRIES_STARS_COLOR": (255, 215, 0),      # 机会星星的颜色 (金色)。

    # --- 顶部信息栏 - “最高分”模块 (HUD - Highest Module) ---
    "HUD_HIGHEST_LABEL_POS": (675, 140),         # “Highest”标签的 (X, Y) 坐标。
    "HUD_HIGHEST_LABEL_FONT_SIZE": 22,           # “Highest”标签的字体大小。
    "HUD_HIGHEST_VALUE_FONT_SIZE": 36,           # 最高分数值的字体大小。
    "HUD_HIGHEST_VALUE_COLOR": (255, 215, 0),    # 最高分数值的颜色 (金色)。
    
    # --- 游戏内顶部按钮 (In-Game Top Buttons) ---
    "INGAME_BUTTON_SIZE": (150, 50),             # 游戏内顶部按钮的尺寸。
    "INGAME_BUTTON_FONT_SIZE": 16,               # 游戏内顶部按钮的字体大小。
    "INGAME_BUTTON_Y": 35,                       # 游戏内顶部按钮的Y坐标。
    "INGAME_BUTTON_SPACING": 20,                 # 游戏内顶部按钮之间的水平间距。
    
    # --- 游戏结束菜单 (Game Over Menu) ---
    "GAMEOVER_TITLE_POS": (SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 180), # "Game Over"标题的中心坐标。
    "GAMEOVER_TITLE_FONT_SIZE": 70,              # "Game Over"标题的字体大小。
    "GAMEOVER_SCORE_TEXT_BASE_POS": (SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 60), # 最终分数文本的【基准】中心坐标。
    "GAMEOVER_SCORE_TEXT_FONT_SIZE": 18,         # 最终分数文本的字体大小。
    "GAMEOVER_SCORE_LINE_SPACING": 50,           # 【重要】最终分数和最高数字之间的【行间距】。
    "GAMEOVER_RESTART_BUTTON_POS": (SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 80), # “再玩一局”按钮的中心坐标。

    # --- 游戏内按钮颜色 (In-Game Button Colors) ---
    "BUTTON_RESTART_BG": (26, 188, 156),         # 重玩按钮的背景色
    "BUTTON_RESTART_HOVER": (22, 160, 133),      # 重玩按钮悬停时的颜色
    "BUTTON_MUTE_BG": (230, 126, 34),            # 静音按钮的背景色
    "BUTTON_MUTE_HOVER": (211, 84, 0),           # 静音按钮悬停时的颜色
    "BUTTON_EXIT_BG": (231, 76, 60),             # 退出按钮的背景色
    "BUTTON_EXIT_HOVER": (192, 57, 43),          # 退出按钮悬停时的颜色
    "BUTTON_MENU_START_BG": (46, 204, 113),      # 菜单“开始”按钮的背景色
    "BUTTON_MENU_START_HOVER": (39, 174, 96),    # 菜单“开始”按钮悬停时的颜色
}

# --- 2.4 单元格颜色 (Cell Colors) ---
# ------------------------------------------------------------------------------
# 此颜色表定义了不同数字的方块的背景色和文字颜色。
# 字典的键是数字，值是包含背景('bg')和文字('text')颜色的另一个字典。
# 'default' 用于处理所有未明确定义的更高数字的颜色。
COLOR_MAP = {
    1:  {'bg': (236, 240, 241), 'text': (52, 73, 94)},
    2:  {'bg': (52, 152, 219),  'text': (255, 255, 255)},
    3:  {'bg': (46, 204, 113),  'text': (255, 255, 255)},
    4:  {'bg': (241, 196, 15),  'text': (255, 255, 255)},
    5:  {'bg': (230, 126, 34),  'text': (255, 255, 255)},
    6:  {'bg': (231, 76, 60),   'text': (255, 255, 255)},
    7:  {'bg': (155, 89, 182),  'text': (255, 255, 255)},
    8:  {'bg': (26, 188, 156),  'text': (255, 255, 255)},
    9:  {'bg': (211, 84, 0),    'text': (255, 255, 255)},
    10: {'bg': (192, 57, 43),   'text': (255, 255, 255)},
    11: {'bg': (142, 68, 173),  'text': (255, 255, 255)},
    12: {'bg': (39, 174, 96),   'text': (255, 255, 255)},
    'default': {'bg': (127, 140, 141), 'text': (255, 255, 255)}
}

# --- 2.5 资源文件路径 (Asset Paths) ---
# ------------------------------------------------------------------------------
# 【打包EXE修复】下面的函数和路径定义是为了解决打包成exe后闪退的问题。
def resource_path(relative_path: str) -> str:
    """
    获取资源的绝对路径。这是解决PyInstaller打包后找不到资源文件问题的关键。
    当程序被打包成一个文件(.exe)时, 资源文件会被解压到一个临时的系统文件夹中。
    这个函数能正确地找到那个临时文件夹的路径。
    """
    try:
        # PyInstaller 会创建一个名为 _MEIPASS 的临时文件夹，并把路径存储在 sys 模块中
        base_path = sys._MEIPASS
    except Exception:
        # 如果不是在打包环境下运行（即直接运行.py文件），则基路径就是当前工作目录
        base_path = os.path.abspath(".")

    # 将基础路径和相对路径拼接成一个完整的绝对路径
    return os.path.join(base_path, relative_path)

# 【打包EXE修复】使用 resource_path 函数来重新定义所有资源文件的路径
# 这样做可以确保无论是在开发环境还是在打包后的exe中，程序都能找到这些文件。
ASSETS_DIR_NAME = 'assets' # 资源文件夹的名称保持不变
FONT_PATH = resource_path(os.path.join(ASSETS_DIR_NAME, 'press-start-2p.ttf'))
ICON_PATH = resource_path(os.path.join(ASSETS_DIR_NAME, 'favicon.ico'))
CLICK_SOUND_PATH = resource_path(os.path.join(ASSETS_DIR_NAME, 'click.mp3'))
MERGE_SOUND_PATH = resource_path(os.path.join(ASSETS_DIR_NAME, 'merge.mp3'))
CHAIN_SOUND_PATH = resource_path(os.path.join(ASSETS_DIR_NAME, 'chain reaction.mp3'))
GAMEOVER_SOUND_PATH = resource_path(os.path.join(ASSETS_DIR_NAME, 'game over.mp3'))
MUSIC_PATH = resource_path(os.path.join(ASSETS_DIR_NAME, 'game-music-loop.mp3'))

# 定义音量
MUSIC_VOLUME, SFX_VOLUME = 0.3, 0.5

# --- 2.6 其他配置 (Other Settings) ---
# ------------------------------------------------------------------------------
ANIMATION_SPEED = 0.2  # 动画速度。值越大，方块移动和缩放越快。
SCORE_POPUP_DURATION = 60 # 分数弹出动画的持续时间（以帧为单位）。60帧大约是1秒。
SCORE_POPUP_SPEED = 2     # 分数向上漂浮的速度（每帧移动的像素数）。
WHITE = (255, 255, 255)   # 预定义白色，方便使用
GOLD = (255, 215, 0)      # 预定义金色，方便使用
DARK_OVERLAY = (0, 0, 0, 180) # 菜单背景的半透明黑色遮罩。第四个值是透明度(0-255)。

# ==============================================================================
# --- 3. 游戏核心代码 (CORE GAME CODE) ---
# 下方为游戏的核心实现，通常无需修改。这里是游戏“引擎”的部分。
# ==============================================================================

def draw_text(surface: pygame.Surface, text: str, font: pygame.font.Font, color: Tuple, center: Tuple):
    """一个通用的函数，用于在指定位置绘制【居中】的文本。"""
    # 1. 使用字体和颜色渲染文本，生成一个"文本表面"
    text_surface = font.render(str(text), True, color)
    # 2. 获取这个文本表面的矩形区域，并将其中心点设置为指定的center坐标
    text_rect = text_surface.get_rect(center=center)
    # 3. 将文本表面绘制到主屏幕(surface)上
    surface.blit(text_surface, text_rect)

def draw_rounded_rect(surface: pygame.Surface, rect: pygame.Rect, color: Tuple, radius: int):
    """一个通用的函数，用于绘制圆角矩形。"""
    # Pygame内置的draw.rect函数支持border_radius参数，可以直接画出圆角
    pygame.draw.rect(surface, color, rect, border_radius=radius)

class Cell:
    """此类代表棋盘上的一个单元格（方块）。它负责管理自己的所有信息和行为。"""
    def __init__(self, value: int, row: int, col: int, board_top_left: Tuple):
        """初始化一个单元格对象。"""
        self.value = value  # 方块上的数字
        self.row, self.col = row, col # 在棋盘网格中的行和列
        self.board_top_left = board_top_left # 棋盘左上角的屏幕坐标，用于计算自身位置

        # 动画相关的属性
        self.x, self.y = self.get_target_pos() # 当前的像素坐标，用于平滑移动
        self.scale = 0.0  # 初始缩放比例为0，实现“凭空出现”的动画效果
        self.is_merging = False # 标记此方块是否正在被合并掉

    def get_target_pos(self) -> Tuple[float, float]:
        """根据单元格的行列位置，计算它在屏幕上的【目标】像素坐标。"""
        board_x, board_y = self.board_top_left
        # 公式：棋盘左上角坐标 + 间隙 + 自身所在行列 * (方块大小 + 间隙)
        target_x = board_x + GRID_GAP + self.col * (CELL_SIZE + GRID_GAP)
        target_y = board_y + GRID_GAP + self.row * (CELL_SIZE + GRID_GAP)
        return (target_x, target_y)

    def update(self):
        """每帧更新单元格的状态，用于实现平滑的动画效果（缓动动画）。"""
        # --- 位置动画 ---
        target_x, target_y = self.get_target_pos()
        # 核心公式: new_pos = current_pos + (target_pos - current_pos) * speed
        # 这会让方块每帧都靠近目标位置一段距离，速度会越来越慢，看起来很自然。
        self.x += (target_x - self.x) * ANIMATION_SPEED
        self.y += (target_y - self.y) * ANIMATION_SPEED
        
        # --- 缩放动画 ---
        # 如果方块正在被合并，目标缩放为0 (消失)；否则为1 (正常大小)。
        target_scale = 0.0 if self.is_merging else 1.0
        self.scale += (target_scale - self.scale) * ANIMATION_SPEED

    def draw(self, surface: pygame.Surface):
        """将单元格绘制到屏幕上。"""
        if self.scale < 0.01: return # 如果方块太小，不绘制以提高性能

        colors = COLOR_MAP.get(self.value, COLOR_MAP['default'])
        scaled_size = CELL_SIZE * self.scale
        rect = pygame.Rect(self.x + (CELL_SIZE - scaled_size) / 2,
                           self.y + (CELL_SIZE - scaled_size) / 2,
                           scaled_size, scaled_size)
        
        draw_rounded_rect(surface, rect, colors['bg'], 15)
        
        # 当方块放大到一定程度时才绘制数字
        if self.scale > 0.8:
            font_size = int(UI_SETTINGS["CELL_NUMBER_FONT_SIZE"] * self.scale)
            cell_font = pygame.font.Font(FONT_PATH, font_size)
            draw_text(surface, str(self.value), cell_font, colors['text'], rect.center)

class ScorePopup:
    """此类代表合并时弹出的得分数字，它会向上漂浮并逐渐消失。"""
    def __init__(self, value: int, pos: Tuple, font: pygame.font.Font):
        self.value = value
        self.pos = list(pos) # 转换为列表，因为元组tuple不能修改
        self.font = font
        self.lifetime = SCORE_POPUP_DURATION # 存活时间（帧数）
        self.alpha = 255 # 初始透明度 (255为完全不透明)

    def update(self):
        """每帧更新弹出分数的位置和透明度。"""
        self.pos[1] -= SCORE_POPUP_SPEED # Y坐标减小，实现向上漂浮
        self.lifetime -= 1
        self.alpha = max(0, 255 * (self.lifetime / SCORE_POPUP_DURATION))

    def draw(self, surface: pygame.Surface):
        """将弹出分数绘制到屏幕上。"""
        text_surface = self.font.render(f"+{self.value}", True, WHITE)
        text_surface.set_alpha(self.alpha)
        surface.blit(text_surface, self.pos)

class Button:
    """一个通用的按钮类，处理绘制、鼠标悬停和点击事件。"""
    def __init__(self, rect: Tuple, text: str, font: pygame.font.Font, text_color: Tuple, bg_color: Tuple, hover_color: Tuple):
        self.rect = pygame.Rect(rect)
        self.text = text
        self.font = font
        self.text_color = text_color
        self.colors = {'normal': bg_color, 'hover': hover_color}
        self.is_hovered = False

    def set_text(self, text: str): 
        """允许在游戏过程中改变按钮的文字，比如"Mute" -> "Unmute"."""
        self.text = text

    def handle_event(self, event: pygame.event.Event) -> bool:
        """处理单个事件，判断按钮是否被点击。"""
        if event.type == pygame.MOUSEMOTION:
            self.is_hovered = self.rect.collidepoint(event.pos)
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and self.is_hovered:
            return True # 返回True，表示按钮被点击了
        return False

    def draw(self, surface: pygame.Surface):
        """将按钮绘制到屏幕上。"""
        color = self.colors['hover'] if self.is_hovered else self.colors['normal']
        draw_rounded_rect(surface, self.rect, color, 15)
        draw_text(surface, self.text, self.font, self.text_color, self.rect.center)

class Game:
    """游戏的主控制类，像一个“总司令”，管理着游戏的所有方面。"""
    def __init__(self):
        """初始化整个游戏。这个函数在游戏开始时只运行一次。"""
        pygame.init(); pygame.mixer.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("Number Evolve")
        # 【打包EXE修复】原版代码没有设置图标，这里也保持一致，仅在资源路径定义中保留ICON_PATH变量
        # 如果需要设置图标，可以取消下面这行代码的注释
        # try: pygame.display.set_icon(pygame.image.load(ICON_PATH))
        # except: print("Warning: Icon file not found or could not be loaded.")
            
        self.clock = pygame.time.Clock()
        self.calculate_layout()
        self.load_assets()
        self.create_ui()
        self.reset_game_state()
        
        # 游戏状态管理
        self.flow_state = "START_MENU" # 'START_MENU', 'PLAYING', 'GAME_OVER'
        self.turn_state = "IDLE"       # 'IDLE', 'CHECK_MATCHES', 'MERGING', 'DROPPING', 'REFILLING'
        self.animation_end_time = 0
        self.is_muted = False

    def calculate_layout(self):
        """计算棋盘在屏幕上的位置，使其居中。"""
        board_width = BOARD_SIZE * CELL_SIZE + (BOARD_SIZE + 1) * GRID_GAP
        self.board_rect = pygame.Rect(
            (SCREEN_WIDTH - board_width) / 2,
            UI_SETTINGS["HUD_HEIGHT"] + (SCREEN_HEIGHT - UI_SETTINGS["HUD_HEIGHT"] - board_width) / 2,
            board_width, board_width
        )

    def load_assets(self):
        """加载所有外部资源文件，如字体和声音。"""
        try:
            # 【打包EXE修复】这里的 FONT_PATH 等变量现在由 resource_path 函数生成，路径总是正确的
            if not os.path.exists(FONT_PATH): raise FileNotFoundError(f"Font not found: {FONT_PATH}")
            self.fonts = {
                'title': pygame.font.Font(FONT_PATH, UI_SETTINGS["MENU_TITLE_FONT_SIZE"]),
                'subtitle': pygame.font.Font(FONT_PATH, UI_SETTINGS["MENU_SUBTITLE_FONT_SIZE"]),
                'popup': pygame.font.Font(FONT_PATH, 36),
                'button': pygame.font.Font(FONT_PATH, UI_SETTINGS["MENU_BUTTON_FONT_SIZE"]),
                'ingame_button': pygame.font.Font(FONT_PATH, UI_SETTINGS["INGAME_BUTTON_FONT_SIZE"]),
                'score_label': pygame.font.Font(FONT_PATH, UI_SETTINGS["HUD_SCORE_LABEL_FONT_SIZE"]),
                'score_value': pygame.font.Font(FONT_PATH, UI_SETTINGS["HUD_SCORE_VALUE_FONT_SIZE"]),
                'tries_label': pygame.font.Font(FONT_PATH, UI_SETTINGS["HUD_TRIES_LABEL_FONT_SIZE"]),
                'tries_stars': pygame.font.Font(FONT_PATH, UI_SETTINGS["HUD_TRIES_STARS_FONT_SIZE"]),
                'highest_label': pygame.font.Font(FONT_PATH, UI_SETTINGS["HUD_HIGHEST_LABEL_FONT_SIZE"]),
                'highest_value': pygame.font.Font(FONT_PATH, UI_SETTINGS["HUD_HIGHEST_VALUE_FONT_SIZE"]),
                'gameover_title': pygame.font.Font(FONT_PATH, UI_SETTINGS["GAMEOVER_TITLE_FONT_SIZE"]),
                'gameover_score': pygame.font.Font(FONT_PATH, UI_SETTINGS["GAMEOVER_SCORE_TEXT_FONT_SIZE"]),
            }
            self.sounds = {'click': pygame.mixer.Sound(CLICK_SOUND_PATH), 'merge': pygame.mixer.Sound(MERGE_SOUND_PATH),
                           'chain': pygame.mixer.Sound(CHAIN_SOUND_PATH), 'over': pygame.mixer.Sound(GAMEOVER_SOUND_PATH)}
            for s in self.sounds.values(): s.set_volume(SFX_VOLUME)
            pygame.mixer.music.load(MUSIC_PATH)
            pygame.mixer.music.set_volume(MUSIC_VOLUME)
        except Exception as e:
            # 如果任何资源加载失败，打印错误信息并退出。
            print(f"FATAL ERROR loading assets: {e}"); sys.exit()

    def create_ui(self):
        """根据配置信息，创建游戏中所有的按钮实例。"""
        w, h = UI_SETTINGS["MENU_BUTTON_SIZE"]; cx, start_y = UI_SETTINGS["MENU_START_BUTTON_POS"]
        self.start_button = Button((cx - w/2, start_y, w, h), "Start Game", self.fonts['button'], WHITE, UI_SETTINGS["BUTTON_MENU_START_BG"], UI_SETTINGS["BUTTON_MENU_START_HOVER"])
        
        restart_cx, restart_y = UI_SETTINGS["GAMEOVER_RESTART_BUTTON_POS"]
        self.restart_button = Button((restart_cx - w/2, restart_y, w, h), "Play Again", self.fonts['button'], WHITE, UI_SETTINGS["BUTTON_MENU_START_BG"], UI_SETTINGS["BUTTON_MENU_START_HOVER"])
        
        w_ingame, h_ingame = UI_SETTINGS["INGAME_BUTTON_SIZE"]; y_ingame = UI_SETTINGS["INGAME_BUTTON_Y"]
        spacing = UI_SETTINGS["INGAME_BUTTON_SPACING"]; total_width = 3 * w_ingame + 2 * spacing
        start_x = (SCREEN_WIDTH - total_width) / 2
        
        self.ingame_restart_button = Button((start_x, y_ingame, w_ingame, h_ingame), "Restart", self.fonts['ingame_button'], WHITE, UI_SETTINGS["BUTTON_RESTART_BG"], UI_SETTINGS["BUTTON_RESTART_HOVER"])
        self.mute_button = Button((start_x + w_ingame + spacing, y_ingame, w_ingame, h_ingame), "Mute", self.fonts['ingame_button'], WHITE, UI_SETTINGS["BUTTON_MUTE_BG"], UI_SETTINGS["BUTTON_MUTE_HOVER"])
        self.exit_button = Button((start_x + 2 * (w_ingame + spacing), y_ingame, w_ingame, h_ingame), "Exit", self.fonts['ingame_button'], WHITE, UI_SETTINGS["BUTTON_EXIT_BG"], UI_SETTINGS["BUTTON_EXIT_HOVER"])
        
        self.score_popups = []

    def reset_game_state(self):
        """重置所有与一局游戏相关的变量，用于开始新游戏或重玩。"""
        self.board = [[None for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
        self.score, self.opportunities, self.highest_number, self.combo_counter = 0, INITIAL_OPPORTUNITIES, 0, 0
        self.last_clicked_pos = None

    def start_new_game(self):
        """开始一局全新的游戏。"""
        self.reset_game_state()
        values = [[random.randint(1, MAX_INITIAL_NUMBER) for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
        # 循环生成，直到初始棋盘上没有任何可以消除的组合，确保开局公平
        while self.check_for_matches(values)[0]:
            values = [[random.randint(1, MAX_INITIAL_NUMBER) for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
        for r in range(BOARD_SIZE):
            for c in range(BOARD_SIZE):
                self.board[r][c] = Cell(values[r][c], r, c, self.board_rect.topleft)
                if values[r][c] > self.highest_number: self.highest_number = values[r][c]
        self.flow_state, self.turn_state = "PLAYING", "IDLE"
        if not self.is_muted: pygame.mixer.music.play(-1) # -1表示无限循环播放

    def play_sound(self, sound_key: str):
        """播放一个指定的音效，但会检查静音状态。"""
        if not self.is_muted: self.sounds[sound_key].play()

    def run(self):
        """游戏的主循环。这个循环会一直运行，直到玩家关闭窗口。"""
        while True:
            # 1. 处理用户输入 (键盘、鼠标)
            self.handle_events()
            # 2. 更新游戏状态 (移动方块、计算分数、检查逻辑)
            self.update()
            # 3. 将所有东西绘制到屏幕上
            self.draw()
            # 4. 控制游戏速度，确保在所有电脑上运行速度一致
            self.clock.tick(FPS)

    def handle_events(self):
        """处理所有来自用户的事件。"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT: pygame.quit(); sys.exit()
            
            # 根据当前的游戏流程状态，将事件分发给对应的处理器
            if self.flow_state == "START_MENU":
                if self.start_button.handle_event(event): self.play_sound('click'); self.start_new_game()
            elif self.flow_state == "GAME_OVER":
                if self.restart_button.handle_event(event): self.play_sound('click'); self.start_new_game()
            elif self.flow_state == "PLAYING":
                if self.ingame_restart_button.handle_event(event): self.play_sound('click'); self.start_new_game()
                if self.exit_button.handle_event(event): pygame.quit(); sys.exit()
                if self.mute_button.handle_event(event):
                    self.is_muted = not self.is_muted; self.mute_button.set_text("Unmute" if self.is_muted else "Mute")
                    if self.is_muted: pygame.mixer.music.pause()
                    else: pygame.mixer.music.unpause()
                
                # 只有在等待玩家操作(IDLE)时，才处理棋盘点击
                if self.turn_state == "IDLE" and event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                    self.handle_board_click(event.pos)

    def update(self):
        """每帧更新游戏世界中的所有动态对象。"""
        for r in range(BOARD_SIZE):
            for c in range(BOARD_SIZE):
                if self.board[r][c]: self.board[r][c].update()
        for p in self.score_popups[:]:
            p.update()
            if p.lifetime <= 0: self.score_popups.remove(p)

        # 持续更新按钮的悬停状态
        mouse_pos = pygame.mouse.get_pos()
        # 伪造一个MOUSEMOTION事件传递给按钮，让它们能实时响应悬停
        fake_event = pygame.event.Event(pygame.MOUSEMOTION, pos=mouse_pos)
        if self.flow_state == "START_MENU":
            self.start_button.handle_event(fake_event)
        elif self.flow_state == "GAME_OVER":
            self.restart_button.handle_event(fake_event)
        elif self.flow_state == "PLAYING":
            self.ingame_restart_button.handle_event(fake_event)
            self.mute_button.handle_event(fake_event)
            self.exit_button.handle_event(fake_event)

        # 如果不在游戏进行中，或者动画正在播放，则不执行后续的游戏逻辑更新
        if self.flow_state != "PLAYING" or time.time() < self.animation_end_time: return

        # --- 游戏逻辑状态机：定义了消除、下落、填充的自动流程 ---
        if self.turn_state == "CHECK_MATCHES":
            values = [[cell.value if cell else 0 for cell in row] for row in self.board]
            has_matches, matches_list = self.check_for_matches(values)
            if not has_matches:
                self.turn_state = "IDLE"; self.combo_counter = 0
                if self.opportunities <= 0:
                    self.play_sound('over'); self.flow_state = "GAME_OVER"; pygame.mixer.music.stop()
            else:
                self.combo_counter += 1; self.play_sound('chain' if self.combo_counter > 1 else 'merge')
                if self.opportunities < MAX_OPPORTUNITIES: self.opportunities += 1
                self.process_merges(matches_list)
                self.turn_state = "MERGING"; self.animation_end_time = time.time() + 0.3
        elif self.turn_state == "MERGING":
            self.turn_state = "DROPPING"; self.process_gravity(); self.animation_end_time = time.time() + 0.3
        elif self.turn_state == "DROPPING":
            self.turn_state = "REFILLING"; self.process_refill(); self.animation_end_time = time.time() + 0.3
        elif self.turn_state == "REFILLING":
            self.turn_state = "CHECK_MATCHES"; self.last_clicked_pos = None

    def draw(self):
        """将所有游戏元素绘制到屏幕上。绘制顺序很重要，像画油画一样，一层一层往上画。"""
        self.screen.fill(UI_SETTINGS["HUD_BG_COLOR"]) # 1. 绘制最底层的背景
        self.draw_stats_bar() # 2. 绘制信息栏
        self.draw_board_bg() # 3. 绘制棋盘背景

        for r in range(BOARD_SIZE): # 4. 绘制所有方块
            for c in range(BOARD_SIZE):
                if self.board[r][c]: self.board[r][c].draw(self.screen)
        for p in self.score_popups: p.draw(self.screen) # 5. 绘制分数弹出效果
        
        # 6. 根据游戏流程，绘制最上层的UI（菜单或按钮）
        if self.flow_state == "PLAYING": self.draw_ingame_ui()
        elif self.flow_state == "START_MENU": self.draw_start_menu()
        elif self.flow_state == "GAME_OVER": self.draw_gameover_menu()
        
        pygame.display.flip() # 7. 将内存中画好的一切，更新到屏幕上

    def draw_board_bg(self):
        """绘制棋盘的深色背景和浅色网格。"""
        draw_rounded_rect(self.screen, self.board_rect, BOARD_BG_COLOR, 20)
        for r in range(BOARD_SIZE):
            for c in range(BOARD_SIZE):
                rect = pygame.Rect(self.board_rect.left + GRID_GAP + c * (CELL_SIZE + GRID_GAP),
                                  self.board_rect.top + GRID_GAP + r * (CELL_SIZE + GRID_GAP), CELL_SIZE, CELL_SIZE)
                draw_rounded_rect(self.screen, rect, EMPTY_CELL_COLOR, 15)

    def draw_stats_bar(self):
        """绘制顶部的分数、机会、最高数字等信息。"""
        spacing = UI_SETTINGS["HUD_LABEL_VALUE_SPACING"]
        label_pos = UI_SETTINGS["HUD_SCORE_LABEL_POS"]; value_pos = (label_pos[0], label_pos[1] + spacing)
        draw_text(self.screen, "Score", self.fonts['score_label'], WHITE, label_pos)
        draw_text(self.screen, self.score, self.fonts['score_value'], UI_SETTINGS["HUD_SCORE_VALUE_COLOR"], value_pos)
        
        label_pos = UI_SETTINGS["HUD_TRIES_LABEL_POS"]; value_pos = (label_pos[0], label_pos[1] + spacing)
        draw_text(self.screen, "Tries", self.fonts['tries_label'], WHITE, label_pos)
        draw_text(self.screen, '★' * self.opportunities, self.fonts['tries_stars'], UI_SETTINGS["HUD_TRIES_STARS_COLOR"], value_pos)
        
        label_pos = UI_SETTINGS["HUD_HIGHEST_LABEL_POS"]; value_pos = (label_pos[0], label_pos[1] + spacing)
        draw_text(self.screen, "Highest", self.fonts['highest_label'], WHITE, label_pos)
        draw_text(self.screen, self.highest_number, self.fonts['highest_value'], UI_SETTINGS["HUD_HIGHEST_VALUE_COLOR"], value_pos)
        
    def draw_start_menu(self):
        """绘制开始菜单界面。"""
        overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA); overlay.fill(DARK_OVERLAY)
        self.screen.blit(overlay, (0, 0))
        title_x, title_y = UI_SETTINGS["MENU_TITLE_POS"]; line_spacing = UI_SETTINGS["MENU_TITLE_LINE_SPACING"]
        draw_text(self.screen, UI_SETTINGS["MENU_TITLE_LINE1"], self.fonts['title'], GOLD, (title_x, title_y - line_spacing/2))
        draw_text(self.screen, UI_SETTINGS["MENU_TITLE_LINE2"], self.fonts['title'], GOLD, (title_x, title_y + line_spacing/2))
        draw_text(self.screen, "Click to upgrade, Match 3 to merge!", self.fonts['subtitle'], WHITE, UI_SETTINGS["MENU_SUBTITLE_POS"])
        self.start_button.draw(self.screen)
        
    def draw_gameover_menu(self):
        """绘制游戏结束界面。"""
        overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA); overlay.fill(DARK_OVERLAY)
        self.screen.blit(overlay, (0, 0))
        draw_text(self.screen, "Game Over", self.fonts['gameover_title'], GOLD, UI_SETTINGS["GAMEOVER_TITLE_POS"])
        
        base_x, base_y = UI_SETTINGS["GAMEOVER_SCORE_TEXT_BASE_POS"]; line_spacing = UI_SETTINGS["GAMEOVER_SCORE_LINE_SPACING"]
        score_text = f"Final Score: {self.score}"; highest_text = f"Highest Tile: {self.highest_number}"
        
        draw_text(self.screen, score_text, self.fonts['gameover_score'], WHITE, (base_x, base_y - line_spacing/2))
        draw_text(self.screen, highest_text, self.fonts['gameover_score'], WHITE, (base_x, base_y + line_spacing/2))
        
        self.restart_button.draw(self.screen)
        
    def draw_ingame_ui(self):
        """绘制游戏进行时顶部的几个功能按钮。"""
        self.ingame_restart_button.draw(self.screen); self.mute_button.draw(self.screen); self.exit_button.draw(self.screen)

    def handle_board_click(self, mouse_pos: Tuple):
        """处理玩家在棋盘区域的点击。"""
        if self.opportunities <= 0: return
        for r in range(BOARD_SIZE):
            for c in range(BOARD_SIZE):
                rect = pygame.Rect(self.board_rect.left + GRID_GAP + c * (CELL_SIZE + GRID_GAP),
                                  self.board_rect.top + GRID_GAP + r * (CELL_SIZE + GRID_GAP), CELL_SIZE, CELL_SIZE)
                if rect.collidepoint(mouse_pos) and self.board[r][c]:
                    self.play_sound('click'); self.opportunities -= 1
                    cell = self.board[r][c]; cell.value += 1
                    if cell.value > self.highest_number: self.highest_number = cell.value
                    self.last_clicked_pos, self.turn_state, self.combo_counter = (r, c), "CHECK_MATCHES", 0
                    return

    def check_for_matches(self, board_values: List[List[int]]) -> Tuple[bool, List]:
        """检查整个棋盘，找出所有可消除的组合（使用广度优先搜索BFS）。"""
        matches, matches_list = set(), []
        for r in range(BOARD_SIZE):
            for c in range(BOARD_SIZE):
                if board_values[r][c] == 0 or (r,c) in matches: continue
                q, group = [(r, c)], set([(r, c)])
                while q:
                    curr_r, curr_c = q.pop(0)
                    for dr, dc in [(0,1), (0,-1), (1,0), (-1,0)]:
                        nr, nc = curr_r + dr, curr_c + dc
                        if 0 <= nr < BOARD_SIZE and 0 <= nc < BOARD_SIZE and \
                           board_values[nr][nc] == board_values[r][c] and (nr, nc) not in group:
                            group.add((nr, nc)); q.append((nr, nc))
                if len(group) >= 3: matches.update(group); matches_list.append(list(group))
        return len(matches) > 0, matches_list

    def process_merges(self, matches_list: List[List[Tuple]]):
        """处理所有找到的匹配组合，计算分数并更新方块数值。"""
        for group in matches_list:
            value = self.board[group[0][0]][group[0][1]].value
            if value is None: continue
            new_value = value + 1
            target_pos = group[-1]
            if self.last_clicked_pos and self.last_clicked_pos in group: target_pos = self.last_clicked_pos
            score = new_value + (new_value - 1)**2
            total_points = round(score * (1 + (self.combo_counter - 1) * 0.5))
            self.score += total_points
            if new_value > self.highest_number: self.highest_number = new_value
            
            target_cell = self.board[target_pos[0]][target_pos[1]]
            if target_cell:
                popup_pos = target_cell.get_target_pos()
                self.score_popups.append(ScorePopup(total_points, popup_pos, self.fonts['popup']))

            for r, c in group:
                if (r, c) == target_pos:
                    if self.board[r][c]: self.board[r][c].value = new_value
                elif self.board[r][c]: 
                    self.board[r][c].is_merging = True

    def process_gravity(self):
        """处理重力效果，让上方的方块掉落以填补空位。"""
        for r in range(BOARD_SIZE):
            for c in range(BOARD_SIZE):
                if self.board[r][c] and self.board[r][c].is_merging: self.board[r][c] = None
        for c in range(BOARD_SIZE):
            empty_row = BOARD_SIZE - 1
            for r in range(BOARD_SIZE - 1, -1, -1):
                if self.board[r][c]:
                    if r != empty_row:
                        cell = self.board[r][c]; self.board[empty_row][c] = cell
                        self.board[r][c] = None; cell.row = empty_row
                    empty_row -= 1

    def process_refill(self):
        """在所有空位上生成新的随机方块。"""
        for r in range(BOARD_SIZE):
            for c in range(BOARD_SIZE):
                if not self.board[r][c]: self.board[r][c] = Cell(random.randint(1, MAX_INITIAL_NUMBER), r, c, self.board_rect.topleft)

# ==============================================================================
# --- 4. 游戏入口 (GAME ENTRY POINT) ---
# ==============================================================================
if __name__ == '__main__':
    # 这是Python程序的标准入口。
    # 当你直接运行 `python game.py` 时，这部分代码会被执行。
    # 1. 创建一个Game类的实例 (一个游戏对象)
    # 2. 调用它的run()方法，启动游戏主循环
    Game().run()