commit a4df31dd244c931161530a3490ef4c87da3ece29 Author: laowang Date: Wed Nov 5 13:37:16 2025 +0800 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd15ec4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,181 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore \ No newline at end of file diff --git a/2046.ico b/2046.ico new file mode 100644 index 0000000..88cc330 Binary files /dev/null and b/2046.ico differ diff --git a/assets/SourceHanSansCN-Regular.otf b/assets/SourceHanSansCN-Regular.otf new file mode 100644 index 0000000..56832dd Binary files /dev/null and b/assets/SourceHanSansCN-Regular.otf differ diff --git a/assets/chain reaction.mp3 b/assets/chain reaction.mp3 new file mode 100644 index 0000000..c8bbbf5 Binary files /dev/null and b/assets/chain reaction.mp3 differ diff --git a/assets/click.mp3 b/assets/click.mp3 new file mode 100644 index 0000000..f8a0b3e Binary files /dev/null and b/assets/click.mp3 differ diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..5b028af Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/game over.mp3 b/assets/game over.mp3 new file mode 100644 index 0000000..1fd5b58 Binary files /dev/null and b/assets/game over.mp3 differ diff --git a/assets/game-music-loop.mp3 b/assets/game-music-loop.mp3 new file mode 100644 index 0000000..aa23091 Binary files /dev/null and b/assets/game-music-loop.mp3 differ diff --git a/assets/merge.mp3 b/assets/merge.mp3 new file mode 100644 index 0000000..8d87ff8 Binary files /dev/null and b/assets/merge.mp3 differ diff --git a/assets/press-start-2p.ttf b/assets/press-start-2p.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/assets/press-start-2p.ttf differ diff --git a/font.otf b/font.otf new file mode 100644 index 0000000..56832dd Binary files /dev/null and b/font.otf differ diff --git a/game_v2.py b/game_v2.py new file mode 100644 index 0000000..6360c5b --- /dev/null +++ b/game_v2.py @@ -0,0 +1,625 @@ +# 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() \ No newline at end of file diff --git a/文档.md b/文档.md new file mode 100644 index 0000000..292c6c3 --- /dev/null +++ b/文档.md @@ -0,0 +1,119 @@ +# 《数字合成》游戏脚本说明文档 (v3.8) + + + +## 1. 如何运行游戏 + +1. **安装 Pygame**: 如果您尚未安装,请打开您电脑的命令行工具(在Windows上是 **CMD** 或 **PowerShell**)并输入以下命令,然后按回车: + + ``` + pip install pygame + ``` + +2. **准备文件**: + + - 将我们最终确定的Python代码保存为一个文件,例如 `game.py`。 + - 在与 `game.py` **完全相同的目录**下,创建一个名为 `assets` 的文件夹。 + - 将所有的资源文件(`.mp3` 音效, `press-start-2p.ttf` 字体, `.ico` 图标)全部放入这个 `assets` 文件夹。 + + 您的文件结构应该如下所示,这是一个正确的示例: + + ``` + - /您的游戏文件夹/ + |- game.py + |- /assets/ + |- press-start-2p.ttf + |- favicon.ico + |- click.mp3 + |- merge.mp3 + |- chain reaction.mp3 + |- game over.mp3 + |- game-music-loop.mp3 + ``` + +3. **运行**: 打开命令行工具,进入到 `game.py` 所在的文件夹,然后运行: + + ``` + python game.py + ``` + +## 2. 如何将游戏打包成 .exe 文件 + +将游戏打包成一个`.exe`文件,就可以让没有安装Python的朋友也能直接在Windows电脑上玩。最常用的工具是 `pyinstaller`。 + +#### **第一步:安装 PyInstaller** + +和安装Pygame一样,在命令行工具中输入以下命令并按回车: + +``` +pip install pyinstaller +``` + +#### **第二步:执行打包命令** + +1. 打开命令行工具。 + +2. 使用 `cd` 命令进入到您的游戏文件夹(也就是`game.py`和`assets`文件夹所在的位置)。例如: + + ``` + cd C:\Users\WT\Videos\2046 + ``` + +3. 输入以下打包命令,然后按回车。请**完整复制**这一整行命令: + + ``` + pyinstaller --onefile --windowed --add-data "assets;assets" --icon="assets/favicon.ico" game.py + ``` + +#### **命令解释:** + +- `--onefile`: 将所有东西打包进一个单独的`.exe`文件中。 +- `--windowed`: 运行`.exe`时,不会出现一个黑色的命令行窗口。 +- `--add-data "assets;assets"`: **【最重要的一步】** 这会告诉打包工具,把`assets`文件夹和里面的所有内容(字体、音效)一起打包进`.exe`文件中。第一个`assets`是源文件夹,第二个`assets`是打包后在程序内部的路径。 +- `--icon="assets/favicon.ico"`: 为您的`.exe`文件设置一个自定义的图标。 +- `game.py`: 您要打包的主程序文件名。 + +#### **第三步:找到您的 .exe 文件** + +打包过程需要一些时间。完成后,您的游戏文件夹里会出现几个新文件夹,其中一个是 `dist`。 + +**您的`.exe`文件就在 `dist` 文件夹里!** 您可以把它复制出来,发送给朋友,他们双击就可以玩了。 + +## 3. 如何自定义游戏外观(配置区详解) + +现在,所有的配置项都集中在代码顶部的 **`UI_SETTINGS`** 字典中。您可以非常安全地修改它们。 + +### **主菜单界面 (Main Menu)** + +- `"MENU_TITLE_POS": (X, Y)`: **主标题的中心点坐标**。修改第二个值(Y)可以整体上下移动标题。 +- `"MENU_SUBTITLE_POS": (X, Y)`: **副标题的中心点坐标**。 +- `"MENU_START_BUTTON_POS": (X, Y)`: **“开始游戏”按钮的中心点坐标**。 + +### **游戏结束菜单 (Game Over Menu)** + +- `"GAMEOVER_TITLE_POS": (X, Y)`: **"Game Over"标题的中心坐标**。 +- `"GAMEOVER_SCORE_TEXT_BASE_POS": (X, Y)`: 这是“最终分数”和“最高数字”这两行文字的**基准中心点**。 +- `"GAMEOVER_SCORE_LINE_SPACING"`: **【重要】** 控制“最终分数”和“最高数字”之间的**行间距**。 + +### **游戏内方块 (In-Game Cells)** + +- `"CELL_NUMBER_FONT_SIZE"`: **【重要】** 此项专门控制棋盘上**方块内部数字的字体大小**,与信息栏的字体完全独立。 + +### **顶部信息栏 (In-Game HUD)** + +- `"HUD_LABEL_VALUE_SPACING"`: **【重要】** 这是您最关心的**行间距**。它控制着“标签”(如"Score")与其下方“数值”(如"100")之间的垂直距离。**增大此值会让它们的距离变远**。 + +#### **分数、机会、最高分模块** + +您可以独立定制每一块: + +- `"HUD_SCORE_LABEL_POS": (X, Y)`: “Score”**标签**的中心点坐标。 +- `"HUD_SCORE_VALUE_FONT_SIZE"`: **分数数值**的字体大小。 +- (`TRIES` 和 `HIGHEST` 模块的配置项与此类似) + +### **游戏内顶部按钮颜色** + +- `"BUTTON_..._BG"`: 控制各个按钮的背景色。 +- `"BUTTON_..._HOVER"`: 控制鼠标悬停在按钮上时的颜色。 + +希望这份最终的文档能够帮到您。再次为之前给您带来的所有麻烦,致以最深的歉意。 \ No newline at end of file