Python-2048小游戏修订后(进阶版本) 之前做的那个2048游戏有些问题这是重新修订的版本对程序内容进行了优化和升级游戏可以正常运行。一、主要功能功能模块说明核心玩法经典 2048 游戏使用方向键移动方块相同数字合并目标拼出 2048。动画特效移动时方块独立缩放淡入淡出棋盘整体不晃动合并时产生彩色粒子飞溅效果快速爆发并消失。撞击音效每次合并数字时播放短促上升音效由程序实时合成无需外部音频文件。多套皮肤内置 5 套配色方案经典、暗黑、清新、暖阳、海洋可通过菜单栏“皮肤”随时切换当前皮肤带勾选标记。游戏说明点击菜单栏“帮助”→“游戏说明”在游戏区域显示半透明覆盖层介绍操作方法和快捷键点击任意处或按 ESC 关闭。分数记录实时显示当前分数和最高分最高分自动保存到本地highscore.txt文件。菜单栏集成所有功能游戏、皮肤、帮助集成在窗口顶部菜单栏无弹窗分离操作便捷。二、安装依赖必需依赖Python 3.6PySide6pip install pyside6可选依赖音效支持pygamepip install pygame若未安装 pygame程序会禁用音效其他功能完全正常。完整安装命令一次性安装所有依赖pip install pyside6 pygame三、如何游玩启动程序直接运行代码窗口弹出后即可开始。移动方块使用键盘方向键↑ ↓ ← →移动所有方块。游戏目标不断合并数字直到拼出2048方块即为胜利。游戏结束当棋盘填满且无可合并相邻方块时显示“游戏结束”覆盖层。重新开始按键盘R 键或点击菜单“游戏”→“新游戏”/“重新开始”。切换皮肤按键盘S 键循环切换或点击菜单“皮肤”选择具体皮肤。查看帮助点击菜单“帮助”→“游戏说明”或按ESC 键关闭帮助层。退出游戏点击菜单“游戏”→“退出”或关闭窗口。四、附加说明最高分保存最高分自动保存在程序同目录下的highscore.txt中下次启动自动加载。音效仅在合并发生时播放音量为适中不影响游戏流畅度。性能使用 PySide6 原生绘图动画由 QTimer 驱动占用资源低运行流畅。五、源代码import sys import math import random import time import struct import os from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QMenuBar, QStatusBar, QLabel, QMenu) from PySide6.QtCore import Qt, QTimer, QRectF, QPointF from PySide6.QtGui import QPainter, QColor, QFont, QPaintEvent # ---------- 尝试初始化 pygame 混音器用于音效 ---------- try: import pygame pygame.mixer.init(frequency22050, size-16, channels1) SOUND_ENABLED True except Exception: SOUND_ENABLED False print(音效不可用请安装 pygame: pip install pygame) # 游戏逻辑类 class Game2048: def __init__(self): self.board [[0]*4 for _ in range(4)] self.score 0 self.highscore self.load_highscore() self.game_over False self.win False self.merge_positions [] self.spawn_number() self.spawn_number() def load_highscore(self): if os.path.exists(highscore.txt): with open(highscore.txt, r) as f: try: return int(f.read()) except: return 0 return 0 def save_highscore(self): with open(highscore.txt, w) as f: f.write(str(self.highscore)) def spawn_number(self): empty [(i,j) for i in range(4) for j in range(4) if self.board[i][j]0] if empty: i,j random.choice(empty) self.board[i][j] 2 if random.random() 0.9 else 4 def can_move(self): if sum(row.count(0) for row in self.board) 0: return True for i in range(4): for j in range(4): val self.board[i][j] if (i14 and self.board[i1][j]val) or (j14 and self.board[i][j1]val): return True return False def merge_row(self, row): new_row [num for num in row if num ! 0] merged [] merge_info [] i 0 while i len(new_row): if i1 len(new_row) and new_row[i] new_row[i1]: merged_val new_row[i] * 2 merged.append(merged_val) self.score merged_val merge_info.append((len(merged)-1, merged_val)) i 2 else: merged.append(new_row[i]) i 1 merged [0]*(4-len(merged)) return merged, merge_info def move(self, direction): old_board [row[:] for row in self.board] self.merge_positions [] moved False if direction up: for col in range(4): col_vals [self.board[row][col] for row in range(4)] new_col, infos self.merge_row(col_vals) for idx, val in infos: self.merge_positions.append((idx, col, val)) for row in range(4): self.board[row][col] new_col[row] elif direction down: for col in range(4): col_vals [self.board[row][col] for row in range(3,-1,-1)] new_col, infos self.merge_row(col_vals) for idx, val in infos: self.merge_positions.append((3-idx, col, val)) for row in range(4): self.board[row][col] new_col[3-row] elif direction left: for row in range(4): new_row, infos self.merge_row(self.board[row]) for idx, val in infos: self.merge_positions.append((row, idx, val)) self.board[row] new_row elif direction right: for row in range(4): reversed_row self.board[row][::-1] new_row, infos self.merge_row(reversed_row) for idx, val in infos: self.merge_positions.append((row, 3-idx, val)) self.board[row] new_row[::-1] if self.board ! old_board: moved True if self.score self.highscore: self.highscore self.score self.save_highscore() if not self.win: for row in self.board: if 2048 in row: self.win True return True, old_board return False, None def reset(self): self.board [[0]*4 for _ in range(4)] self.score 0 self.game_over False self.win False self.merge_positions [] self.spawn_number() self.spawn_number() self.highscore self.load_highscore() # 内置皮肤 SKINS [ { name: 经典, bg: (187, 173, 160), empty: (205, 193, 180), tiles: { 2: (238, 228, 218), 4: (237, 224, 200), 8: (242, 177, 121), 16: (245, 149, 99), 32: (246, 124, 95), 64: (246, 94, 59), 128: (237, 207, 114), 256: (237, 204, 97), 512: (237, 200, 80), 1024: (237, 197, 63), 2048: (237, 194, 46) }, text: (0, 0, 0), border_radius: 8 }, { name: 暗黑, bg: (30, 30, 30), empty: (60, 60, 60), tiles: { 2: (100, 100, 100), 4: (130, 130, 130), 8: (180, 130, 80), 16: (200, 100, 60), 32: (220, 80, 50), 64: (230, 60, 30), 128: (200, 180, 80), 256: (200, 170, 60), 512: (200, 160, 40), 1024: (200, 150, 30), 2048: (200, 140, 20) }, text: (255, 255, 255), border_radius: 12 }, { name: 清新, bg: (200, 230, 200), empty: (220, 240, 220), tiles: { 2: (170, 210, 170), 4: (150, 190, 150), 8: (130, 170, 130), 16: (110, 150, 110), 32: (90, 130, 90), 64: (70, 110, 70), 128: (200, 200, 100), 256: (180, 180, 80), 512: (160, 160, 60), 1024: (140, 140, 40), 2048: (120, 120, 20) }, text: (0, 0, 0), border_radius: 0 }, { name: 暖阳, bg: (255, 200, 180), empty: (255, 220, 200), tiles: { 2: (255, 180, 160), 4: (255, 160, 140), 8: (255, 140, 120), 16: (255, 120, 100), 32: (255, 100, 80), 64: (255, 80, 60), 128: (255, 210, 100), 256: (255, 190, 80), 512: (255, 170, 60), 1024: (255, 150, 40), 2048: (255, 130, 20) }, text: (0, 0, 0), border_radius: 10 }, { name: 海洋, bg: (50, 80, 120), empty: (80, 120, 160), tiles: { 2: (100, 160, 200), 4: (80, 140, 180), 8: (60, 120, 160), 16: (40, 100, 140), 32: (30, 80, 120), 64: (20, 60, 100), 128: (200, 200, 80), 256: (180, 180, 60), 512: (160, 160, 40), 1024: (140, 140, 30), 2048: (120, 120, 20) }, text: (255, 255, 255), border_radius: 6 } ] # 音效生成函数 def play_merge_sound(): 生成并播放一个短促的上升音效无需外部文件 if not SOUND_ENABLED: return try: freq_start 440 freq_end 880 duration 0.08 sample_rate 22050 num_samples int(sample_rate * duration) samples [] for i in range(num_samples): t i / sample_rate freq freq_start (freq_end - freq_start) * (t / duration) value int(0.4 * 32767 * math.sin(2 * math.pi * freq * t)) samples.append(value) byte_data b.join(struct.pack(h, v) for v in samples) sound pygame.mixer.Sound(bufferbyte_data) sound.play() except: pass # 粒子系统 class Particle: def __init__(self, pos, color, velocity, size, lifetime): self.pos pos self.color color self.velocity velocity self.size size self.lifetime lifetime self.age 0.0 self.alive True def update(self, dt): self.age dt self.pos (self.pos[0] self.velocity[0]*dt, self.pos[1] self.velocity[1]*dt) if self.age self.lifetime: self.alive False return self.velocity (self.velocity[0]*0.95, self.velocity[1]*0.95) def draw(self, painter): if not self.alive: return progress self.age / self.lifetime alpha int(255 * (1 - progress)) size int(self.size * (1 - progress * 0.3)) color QColor(*self.color) color.setAlpha(alpha) painter.setBrush(color) painter.setPen(Qt.NoPen) x, y self.pos painter.drawEllipse(QPointF(x, y), size, size) # 游戏绘图组件 class GameWidget(QWidget): def __init__(self, parentNone): super().__init__(parent) self.game Game2048() self.skins SKINS self.skin_index 0 self.current_skin self.skins[self.skin_index] # 动画状态 self.animating False self.anim_progress 0.0 self.anim_duration 280 self.anim_timer QTimer() self.anim_timer.timeout.connect(self.update_animation) self.anim_timer.setInterval(16) self.anim_old_board None self.anim_new_board None self.anim_start_time 0 self.anim_merge_positions [] self.particles [] self.help_overlay None self.setFocusPolicy(Qt.StrongFocus) self.setMinimumSize(400, 500) def keyPressEvent(self, event): key event.key() if key Qt.Key_R: self.game.reset() self.particles.clear() self.update() self.update_status() return if key Qt.Key_S: self.skin_index (self.skin_index 1) % len(self.skins) self.current_skin self.skins[self.skin_index] self.update() return if self.game.game_over or self.animating: return direction None if key Qt.Key_Up: direction up elif key Qt.Key_Down: direction down elif key Qt.Key_Left: direction left elif key Qt.Key_Right: direction right if direction: moved, old_board self.game.move(direction) if moved: self.anim_merge_positions self.game.merge_positions[:] self.anim_old_board old_board self.anim_new_board [row[:] for row in self.game.board] self.animating True self.anim_progress 0.0 self.anim_start_time int(time.time() * 1000) self.anim_timer.start() # 如果有合并播放音效并生成粒子 if self.anim_merge_positions: play_merge_sound() self.create_merge_particles() self.update_status() self.update() def create_merge_particles(self): 生成快速爆发的粒子生命周期极短 if not self.anim_merge_positions: return margin 12 grid_size 105 board_width 4 * grid_size 3 * margin board_height board_width ox (self.width() - board_width) // 2 oy (self.height() - board_height) // 2 for (row, col, val) in self.anim_merge_positions: cx ox margin col * (grid_size margin) grid_size//2 cy oy margin row * (grid_size margin) grid_size//2 base self.current_skin[tiles].get(val, (255, 200, 100)) # 25个粒子快速爆发 for _ in range(25): angle random.uniform(0, 2*math.pi) speed random.uniform(150, 350) vx speed * math.cos(angle) vy speed * math.sin(angle) size random.randint(6, 16) lifetime random.uniform(0.2, 0.4) # 极短 # 颜色微调 r min(255, max(0, base[0] random.randint(-40, 40))) g min(255, max(0, base[1] random.randint(-40, 40))) b min(255, max(0, base[2] random.randint(-40, 40))) p Particle((cx, cy), (r, g, b), (vx, vy), size, lifetime) self.particles.append(p) def update_animation(self): now int(time.time() * 1000) elapsed now - self.anim_start_time self.anim_progress min(elapsed / self.anim_duration, 1.0) if self.anim_progress 1.0: self.anim_timer.stop() self.animating False self.game.spawn_number() if not self.game.can_move(): self.game.game_over True self.update_status() # 更新粒子 dt 0.016 for p in self.particles: p.update(dt) self.particles [p for p in self.particles if p.alive] self.update() def paintEvent(self, event): painter QPainter(self) painter.setRenderHint(QPainter.Antialiasing) bg QColor(*self.current_skin[bg]) painter.fillRect(self.rect(), bg) margin 12 grid_size 105 board_width 4 * grid_size 3 * margin board_height board_width ox (self.width() - board_width) // 2 oy (self.height() - board_height) // 2 if self.animating: p self.anim_progress old_alpha int(max(0, (1 - p * 1.3)) * 255) old_scale 1.0 - 0.1 * p if old_alpha 0: self.draw_board_static(painter, self.anim_old_board, self.current_skin, ox, oy, grid_size, margin, old_alpha, old_scale) new_alpha int(min(1.0, (p - 0.1) * 1.5) * 255) if new_alpha 0: new_scale 0.9 0.1 * p self.draw_board_static(painter, self.anim_new_board, self.current_skin, ox, oy, grid_size, margin, new_alpha, new_scale) else: self.draw_board_static(painter, self.game.board, self.current_skin, ox, oy, grid_size, margin) for p in self.particles: p.draw(painter) if self.game.game_over: painter.fillRect(self.rect(), QColor(0,0,0,150)) painter.setPen(QColor(255,255,255)) painter.setFont(QFont(Arial, 30, QFont.Bold)) painter.drawText(self.rect(), Qt.AlignCenter, 游戏结束!) painter.setFont(QFont(Arial, 16)) painter.drawText(self.rect().adjusted(0, 60, 0, 0), Qt.AlignCenter, 按 R 重新开始) elif self.game.win: painter.fillRect(self.rect(), QColor(0,255,0,80)) painter.setPen(QColor(0,0,0)) painter.setFont(QFont(Arial, 30, QFont.Bold)) painter.drawText(self.rect(), Qt.AlignCenter, 你赢了!) painter.setFont(QFont(Arial, 16)) painter.drawText(self.rect().adjusted(0, 60, 0, 0), Qt.AlignCenter, 按 R 重新开始) def draw_board_static(self, painter, board, skin, ox, oy, grid_size, margin, alpha255, scale1.0): for row in range(4): for col in range(4): val board[row][col] cx ox margin col * (grid_size margin) grid_size//2 cy oy margin row * (grid_size margin) grid_size//2 size int(grid_size * scale) x cx - size//2 y cy - size//2 if val 0: color skin[empty] else: color skin[tiles].get(val, skin[empty]) if isinstance(color, tuple): color QColor(*color) if alpha 255: color.setAlpha(alpha) painter.setBrush(color) painter.setPen(Qt.NoPen) border_radius skin.get(border_radius, 0) painter.drawRoundedRect(x, y, size, size, border_radius, border_radius) if val ! 0: font_size int(44 * scale) font QFont(Arial, font_size, QFont.Bold) painter.setFont(font) text_color QColor(*skin[text]) if alpha 255: text_color.setAlpha(alpha) painter.setPen(text_color) painter.drawText(QRectF(x, y, size, size), Qt.AlignCenter, str(val)) def update_status(self): if self.parent(): main self.parent() if hasattr(main, update_status_bar): main.update_status_bar(self.game.score, self.game.highscore) def show_help(self): if self.help_overlay is None: self.help_overlay HelpOverlay(self) self.help_overlay.show() self.help_overlay.raise_() def hide_help(self): if self.help_overlay: self.help_overlay.hide() def set_skin_index(self, index): self.skin_index index % len(self.skins) self.current_skin self.skins[self.skin_index] self.particles.clear() self.update() def get_skin_names(self): return [s[name] for s in self.skins] # 帮助覆盖层 class HelpOverlay(QWidget): def __init__(self, parent): super().__init__(parent) self.setGeometry(parent.rect()) self.setAttribute(Qt.WA_TransparentForMouseEvents, False) self.setFocusPolicy(Qt.StrongFocus) self.hide() def paintEvent(self, event): painter QPainter(self) painter.fillRect(self.rect(), QColor(0,0,0,180)) rect self.rect().adjusted(60, 80, -60, -80) painter.setBrush(QColor(240,240,240)) painter.setPen(QColor(200,200,200)) painter.drawRoundedRect(rect, 15, 15) painter.setPen(QColor(0,0,0)) painter.setFont(QFont(Arial, 26, QFont.Bold)) painter.drawText(rect, Qt.AlignTop | Qt.AlignHCenter, 游戏说明) lines [ 使用方向键 (↑ ↓ ← →) 移动方块, 相同数字的方块相遇会合并, 目标拼出 2048 的方块, , 快捷键:, R - 重新开始, S - 切换皮肤 (循环), ESC - 关闭帮助, , 点击任意位置或按 ESC 关闭 ] painter.setFont(QFont(Arial, 16)) y rect.top() 70 for line in lines: painter.drawText(rect.left()30, y, line) y 32 def mousePressEvent(self, event): self.hide() self.parent().setFocus() def keyPressEvent(self, event): if event.key() Qt.Key_Escape: self.hide() self.parent().setFocus() # 主窗口 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(2048 豪华版) self.setMinimumSize(560, 640) self.game_widget GameWidget() self.setCentralWidget(self.game_widget) self.create_menu_bar() self.status_bar QStatusBar() self.setStatusBar(self.status_bar) self.score_label QLabel(分数: 0 最高: 0) self.status_bar.addPermanentWidget(self.score_label) self.status_bar.showMessage(方向键移动 | R 重开 | S 换肤 | 帮助查看说明) self.game_widget.update_status() def create_menu_bar(self): menubar self.menuBar() # 游戏 game_menu menubar.addMenu(游戏) new_action game_menu.addAction(新游戏) new_action.triggered.connect(self.reset_game) restart_action game_menu.addAction(重新开始) restart_action.triggered.connect(self.reset_game) game_menu.addSeparator() quit_action game_menu.addAction(退出) quit_action.triggered.connect(self.close) # 皮肤动态菜单 self.skin_menu menubar.addMenu(皮肤) self.update_skin_menu() # 帮助 help_menu menubar.addMenu(帮助) help_action help_menu.addAction(游戏说明) help_action.triggered.connect(self.game_widget.show_help) def reset_game(self): self.game_widget.game.reset() self.game_widget.particles.clear() self.game_widget.update() self.game_widget.update_status() def update_skin_menu(self): self.skin_menu.clear() skins self.game_widget.get_skin_names() for i, name in enumerate(skins): action self.skin_menu.addAction(name) action.setCheckable(True) if i self.game_widget.skin_index: action.setChecked(True) action.triggered.connect(lambda checked, idxi: self.change_skin(idx)) def change_skin(self, index): self.game_widget.set_skin_index(index) self.update_skin_menu() def update_status_bar(self, score, highscore): self.score_label.setText(f分数: {score} 最高: {highscore}) # 启动 if __name__ __main__: app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec())