游戏规则
当没有合法移动可以执行时,游戏结束。此时所有数字块之和即为最终得分。
游戏在一个 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的tkinter
和numpy
不仅简化了开发过程,还提供了良好的性能支持。这个项目展示了如何利用Python进行游戏开发的可能性,并为进一步的扩展和改进提供了基础,如AI玩家、多人模式等。
Comments 2 条评论
能给我发一份源码看看吗?
@123 这不是有完整代码吗?猪吧