基于Python的2048游戏研究与开发

agluo 发布于 2024-11-23 33 次阅读


AI 摘要

这篇文章探讨了如何使用Python开发经典的2048游戏。通过结合Tkinter、Numpy和Pygame等技术栈,作者详细介绍了游戏的核心逻辑、用户界面设计、音效管理以及扩展功能。文章不仅展示了如何实现数字块的移动与合并,还引入了技能系统和里程碑特效,提升了游戏的策略性和趣味性。无论是初学者还是有经验的开发者,都能从中获得启发,了解如何将Python应用于游戏开发。

游戏规则

当没有合法移动可以执行时,游戏结束。此时所有数字块之和即为最终得分。

游戏在一个 4x4 的网格上进行(本实现允许用户自定义网格大小)。

初始时,网格中随机出现几个数字块(通常是2或4),每次移动后会在空格处随机生成新的数字块。

玩家可以通过上下左右四个方向滑动屏幕上的数字块,相同数值的相邻块会合并成一个新的块,其值为两者之和。

技术栈

  • Python:作为主要编程语言。
  • Tkinter:用于构建图形用户界面。
  • Numpy:处理二维数组(游戏地图)。
  • Pygame:管理音效播放。

技术实现

1. 游戏逻辑

  • 初始化: 使用numpy创建一个4x4的零矩阵,代表游戏棋盘。MapClass类负责管理游戏状态,包括棋盘、分数、技能使用次数等。
class MapClass:
    def __init__(self, n):
        self.n = n
        self.Map = np.zeros((n, n), dtype=int)
        ...
  • 移动和合并: 通过旋转棋盘来简化移动逻辑。move方法根据方向旋转棋盘,然后调用_move_up方法处理向上移动和合并。
def move(self, direction):
    rotations = {"up": 0, "right": 3, "down": 2, "left": 1}
    rotation_count = rotations[direction]
    rotated_map = np.rot90(self.Map, -rotation_count)
    new_map = self._move_up(rotated_map)
    ...
  • 添加新块: 在每次有效移动后,随机选择一个空白格子生成2或4。
def _add_new(self, count=1):
    zero_list = list(zip(*np.where(self.Map == 0)))
    if not zero_list:
        return
    for _ in range(count):
        i, j = rd.choice(zero_list)
        self.Map[i, j] = rd.choice([2] * 10 + [4])
        ...

2. 用户界面

  • tkinter的使用: tkinter用于创建游戏的图形用户界面,包括棋盘的绘制、分数显示、技能按钮等。
class UI(Frame):
    def __init__(self, parent, game, n, UI_size):
        ...
        self.canvas = Canvas(self, width=self.WIDTH, height=self.HEIGHT, bg="#bbada0")
        ...
  • 动态效果: 游戏中使用了粒子特效和颜色渐变来增强用户体验。例如,当玩家达到某个里程碑时,会触发庆祝特效。
def particle_effect(self, x, y):
    particles = []
    for _ in range(100):
        dx, dy = rd.uniform(-10, 10), rd.uniform(-10, 10)
        ...

3. 音效与交互

  • 音效管理: 通过pygame库来播放游戏中的音效,如移动、合并、游戏结束等。
class AudioManager:
    def __init__(self, sound_paths):
        pygame.mixer.init()
        self.sounds = {name: pygame.mixer.Sound(path) for name, path in sound_paths.items()}
    ...
  • 技能系统: 玩家可以使用技能来消除棋盘上的一个数字块,增加了游戏的策略性。
def activate_skill(self):
    if self.game.skill_uses > 0:
        self.skill_active = True
        ...

4. 扩展功能

  • 里程碑系统: 当玩家达到特定的数字时,会触发特效和音效增强游戏的趣味性。
  • 自适应棋盘大小: 通过一个对话框让用户输入棋盘大小,使游戏具有更大的灵活性。
def get_board_size(parent):
    dialog = InputDialog(parent)
    parent.wait_window(dialog)
    return dialog.value

核心功能解析

  • 数字块移动与合并: move 函数结合 np.rot90 实现任意方向的移动,简化了代码逻辑。_move_up 函数处理向上移动和合并的核心逻辑。
  • 消除方块技能: remove_tile 函数实现消除指定位置方块的功能,并限制使用次数。
  • 里程碑与特效: 游戏达到特定分数 (32, 64, 128...) 会触发庆祝特效,使用 canvas 绘制粒子效果,并播放音效。
  • 音效: 使用 pygame 库播放音效,增强游戏反馈。
  • 自定义棋盘大小: 允许用户通过输入框自定义棋盘大小。

完整代码

import random
import random as rd
import tkinter as tk
from tkinter import Canvas, Frame, Button, Label
from tkinter import messagebox

import numpy as np
import pygame


class AudioManager:
    def __init__(self, sound_paths):
        pygame.mixer.init()
        # 初始化音效字典
        self.sounds = {name: pygame.mixer.Sound(path) for name, path in sound_paths.items()}

    def play_sound(self, sound_name):
        """播放指定名称的音效"""
        if sound_name in self.sounds:
            self.sounds[sound_name].play()
        else:
            print(f"未找到音效:{sound_name}")


class MapClass:
    def __init__(self, n):
        self.n = n
        self.Map = np.zeros((n, n), dtype=int)
        self.score = 0
        self.result = 'playing'
        self.new_tile_pos = None
        self.skill_uses = 1  # 初始技能使用次数
        self.next_skill_score = 1000  # 下一次增加消除机会的目标分数
        self.milestones = {32: False, 64: False, 128: False, 256: False, 512: False, 1024: False}  # 里程碑状态
        self._add_new(2)

    def _add_new(self, count=1):
        zero_list = list(zip(*np.where(self.Map == 0)))
        if not zero_list:
            return
        for _ in range(count):
            i, j = rd.choice(zero_list)
            self.Map[i, j] = rd.choice([2] * 10 + [4])
            self.new_tile_pos = (i, j)
            zero_list.remove((i, j))

    def remove_tile(self, x, y):
        """消除指定位置的数字块"""
        if self.skill_uses > 0 and self.Map[x, y] != 0:
            self.Map[x, y] = 0
            self.skill_uses -= 1
            return True
        return False

    def move(self, direction):
        audio_manager.play_sound("move")  # 添加播放移动音效
        """移动逻辑"""
        rotations = {"up": 0, "right": 3, "down": 2, "left": 1}
        rotation_count = rotations[direction]
        rotated_map = np.rot90(self.Map, -rotation_count)

        new_map = self._move_up(rotated_map)

        self.Map = np.rot90(new_map, rotation_count)
        self._add_new(1)

        if self.is_game_over():
            self.result = "lost"

        # 检测是否达成里程碑
        return [value for value in np.unique(self.Map) if value in self.milestones and not self.milestones[value]]

    def _move_up(self, matrix):
        """实现向上的数字块移动和合并"""
        new_matrix = np.zeros_like(matrix)
        for col in range(self.n):
            non_zero = matrix[:, col][matrix[:, col] != 0]
            merged = []
            skip = False
            for i in range(len(non_zero)):
                if skip:
                    skip = False
                    continue
                if i < len(non_zero) - 1 and non_zero[i] == non_zero[i + 1]:
                    merged_value = non_zero[i] * 2
                    merged.append(merged_value)
                    self.score += merged_value
                    if merged_value == 2048:
                        self.result = "won"
                    if self.score >= self.next_skill_score:
                        self.skill_uses += 1
                        self.next_skill_score += 1000
                    skip = True
                else:
                    merged.append(non_zero[i])
            new_matrix[:len(merged), col] = merged
        return new_matrix

    def is_game_over(self):
        """检查游戏是否结束"""
        # 如果没有空位,则游戏结束
        if np.all(self.Map != 0):
            return True
        return False


class UI(Frame):
    def __init__(self, parent, game, n, UI_size):
        Frame.__init__(self, parent)
        self.game = game
        self.n = n
        self.parent = parent
        self.skill_active = False  # 表示技能是否处于激活状态
        (self.MARGIN, self.WIDTH, self.HEIGHT, self.SIDE) = UI_size
        self.colors = {
            0: "#cdc1b4", 2: "#eee4da", 4: "#ede0c8", 8: "#f2b179",
            16: "#f59563", 32: "#f67c5f", 64: "#f65e3b", 128: "#edcf72",
            256: "#edcc61", 512: "#edc850", 1024: "#edc53f", 2048: "#edc22e"
        }
        self.font_colors = {2: "#776e65", 4: "#776e65", 8: "#f9f6f2", 16: "#f9f6f2", 32: "#f9f6f2",
                            64: "#f9f6f2", 128: "#f9f6f2", 256: "#f9f6f2", 512: "#f9f6f2",
                            1024: "#f9f6f2", 2048: "#f9f6f2"}
        self._initUI()

    def _initUI(self):
        self.parent.title("2048")
        self.pack(fill="both", expand=True)

        self.canvas = Canvas(self, width=self.WIDTH, height=self.HEIGHT, bg="#bbada0")
        self.canvas.pack(fill="both", expand=True)

        self.show_score = Label(self, text=f"Score: {self.game.score}", font=("Verdana", 20), bg="#bbada0", fg="white")
        self.show_score.pack(side="top", pady=10)

        self.skill_button = Button(self, text=f"技能:消除方块 ({self.game.skill_uses})",
                                   command=self.activate_skill, font=("Verdana", 12))
        self.skill_button.pack(side="top", pady=10)

        self._draw_grid()
        self.parent.bind("<Key>", self.key_press)

    def start_skill_effect(self):
        """技能模式特效,多彩渐变背景"""
        colors = ["#ff5733", "#ffa500", "#ffd700", "#ff4500", "#ff6347"]  # 渐变颜色列表
        step = 0

        def change_color():
            nonlocal step
            self.canvas.config(bg=colors[step % len(colors)])  # 循环切换颜色
            step += 1
            if self.skill_active:  # 仅在技能模式下循环
                self.parent.after(100, change_color)
            else:
                self.canvas.config(bg="#bbada0")  # 恢复默认背景

        change_color()

    def _hex_to_rgb(self, hex_color):
        """将十六进制颜色转换为RGB"""
        hex_color = hex_color.lstrip('#')
        return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))

    def activate_skill(self):
        if self.game.skill_uses > 0:
            self.skill_active = True
            self.skill_button.config(text="点击棋盘以消除方块", state="disabled")
            self.canvas.bind("<Button-1>", self.select_tile)
            self.start_skill_effect()
        else:
            messagebox.showinfo("技能已用尽", "你已经用完了所有的技能机会!")

    def reset_skill_active(self, event):
        self.skill_active = False
        self.skill_button.config(text=f"技能:消除方块 ({self.game.skill_uses})", state="normal")
        self.canvas.config(bg="#bbada0")  # 恢复原背景
        self.canvas.unbind("<Button-1>")

    def select_tile(self, event):
        """选择要消除的方块"""
        x = (event.x - self.MARGIN) // self.SIDE
        y = (event.y - self.MARGIN) // self.SIDE
        if 0 <= x < self.n and 0 <= y < self.n:
            if self.game.remove_tile(y, x):
                self._draw_grid()
                self.reset_skill_active(event)
        audio_manager.play_sound("milestone")  # 添加消除音效

    def _draw_grid(self):
        self.canvas.delete("grid")
        for i in range(self.n):
            for j in range(self.n):
                x1 = self.MARGIN + j * self.SIDE
                y1 = self.MARGIN + i * self.SIDE
                x2 = x1 + self.SIDE
                y2 = y1 + self.SIDE
                value = self.game.Map[i, j]
                color = self.colors.get(value, "#3c3a32")
                self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline="#bbada0", tags="grid", width=5)
                if value:
                    font_color = "blue" if (i, j) == self.game.new_tile_pos else self.font_colors.get(value, "white")
                    font_size = max(self.SIDE // 4, 14)
                    self.canvas.create_text((x1 + x2) // 2, (y1 + y2) // 2, text=str(value),
                                            fill=font_color, font=("Verdana", font_size, "bold"), tags="grid")

    def trigger_effect(self, milestone):
        audio_manager.play_sound("milestone")  # 添加播放里程碑音效
        """触发动态庆祝特效。"""
        x = self.WIDTH // 2
        y = self.HEIGHT // 2
        size = 30
        steps = 15
        color = ["gold", "orange", "red"]
        text_id = self.canvas.create_text(x, y, text=f"{milestone} Achieved!", fill="gold",
                                          font=("Verdana", size, "bold"), tags="effect")
        self.particle_effect(x, y)

        def animate():
            self.canvas.delete(text_id)

        self.parent.after(2000, animate)

    def particle_effect(self, x, y):
        """改进版粒子特效。"""
        particles = []
        for _ in range(100):
            dx, dy = rd.uniform(-10, 10), rd.uniform(-10, 10)
            color = rd.choice(["red", "yellow", "orange"])
            particle = self.canvas.create_oval(x, y, x + 5, y + 5, fill=color, tags="effect")
            particles.append((particle, dx, dy, color))

        def move_particles(step):
            if step <= 0:
                for particle, _, _, _ in particles:
                    self.canvas.delete(particle)
                return
            for particle, dx, dy, color in particles:
                self.canvas.move(particle, dx, dy)
                alpha = max(0, step / 20)  # 粒子的透明度
                new_color = self._adjust_color_alpha(color, alpha)
                self.canvas.itemconfig(particle, fill=new_color)
            self.canvas.update()
            self.parent.after(50, lambda: move_particles(step - 1))

        move_particles(20)

    def _adjust_color_alpha(self, color, alpha):
        """随机生成颜色并调整透明度。"""
        r = random.randint(80, 255)
        g = random.randint(80, 255)
        b = random.randint(80, 255)

        # 根据 alpha 值调整颜色的透明度
        r = int(r * alpha)
        g = int(g * alpha)
        b = int(b * alpha)

        # 返回调整后的颜色
        return f'#{r:02x}{g:02x}{b:02x}'

    def reset_game(self):
        """重置游戏状态"""
        self.game = MapClass(self.n)  # 重新初始化游戏对象
        self.show_score.config(text=f"Score: {self.game.score}")
        self.skill_button.config(text=f"技能:消除方块 ({self.game.skill_uses})")
        self._draw_grid()  # 刷新界面

    def _game_over(self, message):
        """游戏结束后的处理"""
        # final_message 中不重复插入 Final Score, 而是直接显示分数
        final_message = f"{message}\nDo you want to play again?"

        # 使用 messagebox.askyesno 显示结束消息框
        response = messagebox.askyesno("Game Over", final_message)

        if response:  # 如果用户选择“是”,重新开始游戏
            self.reset_game()
        else:  # 如果用户选择“否”,退出游戏
            self.parent.quit()

    def key_press(self, event):
        key_map = {"Up": "up", "Down": "down", "Left": "left", "Right": "right"}
        if event.keysym in key_map:
            if self.skill_active:
                self.reset_skill_active(event)
            else:
                milestones = self.game.move(key_map[event.keysym])
                self.show_score.config(text=f"Score: {self.game.score}")
                self.skill_button.config(text=f"技能:消除方块 ({self.game.skill_uses})")
                self._draw_grid()

                for milestone in milestones:
                    self.game.milestones[milestone] = True
                    self.trigger_effect(milestone)

                if self.game.result == 'lost':
                    audio_manager.play_sound("game_over")  # 添加播放失败音效
                    self._game_over(f"You lost ! \nFinal Score: {self.game.score}")
                elif self.game.result == 'won':
                    audio_manager.play_sound("milestone")  # 胜利音效复用
                    self._game_over(f"Congratulations ! You won ! \nFinal Score: {self.game.score}")


class InputDialog(tk.Toplevel):
    def __init__(self, parent, title="输入棋盘大小"):
        super().__init__(parent)
        self.parent = parent
        self.title(title)
        self.geometry("300x150")
        self.configure(bg="#bbada0")
        self.resizable(False, False)

        self.value = None

        # 添加输入界面元素
        tk.Label(self, text="请输入棋盘大小 (至少 3):", bg="#bbada0", fg="white", font=("Verdana", 12)).pack(pady=10)
        self.entry = tk.Entry(self, font=("Verdana", 14), justify="center")
        self.entry.pack(pady=5)
        self.entry.bind("<Return>", self.submit)

        # 提交按钮
        tk.Button(self, text="确认", command=self.submit, font=("Verdana", 12),
                  bg="#8f7a66", fg="white", activebackground="#a67c52").pack(pady=10)

    def submit(self, event=None):
        try:
            value = int(self.entry.get())
            if value >= 3:
                self.value = value
                self.destroy()
            else:
                raise ValueError
        except ValueError:
            messagebox.showerror("无效输入", "请输入一个大于等于3的整数!")


def get_board_size(parent):
    dialog = InputDialog(parent)
    parent.wait_window(dialog)
    return dialog.value


if __name__ == "__main__":
    pygame.init()

    # 音效文件路径字典
    sound_paths = {
        "move": "sounds/deplacement.mp3",  # 移动音效
        "merge": "sounds/fusion.mp3",  # 合并音效
        "milestone": "sounds/gagner.mp3",  # 达成里程碑音效
        "game_over": "sounds/perdu.mp3"  # 游戏失败音效
    }

    # 初始化音效管理器
    audio_manager = AudioManager(sound_paths)

    root = tk.Tk()
    root.withdraw()  # 隐藏主窗口,等待用户输入棋盘大小

    # 获取棋盘大小
    n = get_board_size(root)

    if n:  # 用户输入有效数字后,启动游戏
        root.deiconify()  # 显示主窗口
        MARGIN = 20
        WIDTH = HEIGHT = 500
        SIDE = (WIDTH - 2 * MARGIN) // n

        # 初始化游戏和界面
        game = MapClass(n)
        UI(root, game, n, (MARGIN, WIDTH, HEIGHT, SIDE))
        root.mainloop()
    else:  # 用户未输入有效数字时退出程序
        print("用户取消了输入")
        root.destroy()

总结

通过本文的介绍,我们不仅实现了一个基本的2048游戏,还加入了音效、动态效果、技能系统等增强用户体验的功能。使用Python的tkinternumpy不仅简化了开发过程,还提供了良好的性能支持。这个项目展示了如何利用Python进行游戏开发的可能性,并为进一步的扩展和改进提供了基础,如AI玩家、多人模式等。