2048是一个单人益智游戏,目标是移动和合并数字,以达到2048。
1. 实现效果
Python实现2048小游戏
2. 游戏规则
简单地理解一下规则
基本规则:
- 4x4棋盘,每个格可包含一个2的倍数的数字,初始时为空,表示0。
- 游戏开始时,格中会随机生成两个数,数字通常为2或4。
- 可通过键盘的上下左右方向来驱使所有的块向同一方向移,直至无法移动。
- 移动过程,途遇相同数字,二者合并后的数字为原来的两倍,且仅合并一对为1个数字。
- 移动后,随机生成一个新的2或4方块。
- 每次合并方块时,得分会增加,得分等于合并后新方块的数字。
- 游戏结束:网格中没有空位置且没有可以合并的方块时game over
- 重启游戏:点击“Restart”按钮可以重启游戏。
- 高分记录:可以查看之前的高分,并可以输入姓名记录自己的得分。
其他规则:
- 实现游戏规则。
- 使用图形函数生成界面等。 【分数+游戏局+重玩+分数排行】
- 用文件存储用户的进度。【游戏进度实时记录于save_game.txt便于继续残局】
- 用户开始新游戏时,先检测是否有历史记录,有的话可以继续未完成的游戏,也可以重新开始。
- 实现用户排名功能,要求能够将排名信息进行保存,存至文件永久保存。【存至high_scores.json文件】
- 当新用户的成绩需要插入排名列表时,要能够修改原列表信息:如果是同一用户需要更新成绩,则覆盖原成绩。【Score按钮】
- 可以插入、修改、删除排名信息。【Score按钮,点击显示弹窗,当中的排名信息可以增删改】
3. 环境配置
程序中会用到的库:
import tkinter as tk
import random
import json
import os
其中os、json和random是python的内置库,不需要安装,tkinter是标准库之一,通常也不需要单独安装。
4. 代码实现
变量说明
# 设置游戏参数
self.grid_size = 4 # 4×4格
self.score = 0 # 当前局的分数
self.high_score = self.load_highest_score() # 最高分记录
self.tile_values = [[0] * self.grid_size for _ in range(self.grid_size)] # 数字块初始值为 0
self.game_over = False # 判断游戏是否结束的标志
游戏界面
"""创建游戏界面"""
# 组件部分
# 上层:构建框架frame 包含2个文本标签:当前分数score和最高分数Highest Score
self.frame = tk.Frame(self.root, bg="#faf8ef", padx=10, pady=10)
self.frame.pack(pady=20)
# 分数标签 在frame中加score标签 显示当前游戏分数
self.score_label = tk.Label(self.frame, text=f"Score: {self.score}", font=("Arial", 18), bg="#faf8ef")
self.score_label.grid(row=0, column=0,padx=10,)
# 最高分标签 在frame中加high_score标签 显示最高游戏分数
self.high_score_label = tk.Label(self.frame, text=f"Highest Score: {self.high_score}", font=("Arial", 18),
bg="#faf8ef")
self.high_score_label.grid(row=0, column=1,padx=10,)
# 中层:创建画布 画布用于绘制游戏块
self.canvas = tk.Canvas(self.root, width=400, height=400, highlightthickness=0)
self.canvas.pack()
# 下层:构建框架frame1 包含2个按钮, 1个标签:重来按钮restart+信息按钮Scores+游戏结束标签
self.frame1 = tk.Frame(self.root, bg="#faf8ef")
self.frame1.pack(pady=20)
self.restart_button = tk.Button(self.frame1, text="Restart", command=self.restart_game, font=("Arial", 16),
bg="#8f7a66", fg="white", relief=tk.FLAT)
self.high_scores_button = tk.Button(self.frame1, text="Scores", command=self.show_high_scores,
font=("Arial", 16), bg="#8f7a66", fg="white", relief=tk.FLAT)
# 布局按钮
self.restart_button.grid(row=0, column=0, padx=20)
self.high_scores_button.grid(row=0, column=1, padx=20)
棋盘绘制
tile_colors = {0: "#D9D5D1", 2: "#eee4da", 4: "#ede0c8", 8: "#f2b179",
16: "#f59563", 32: "#f67c5f", 64: "#f67c5f", 128: "#f9f6f2",
256: "#f9f6f2", 512: "#f9f6f2", 1024: "#f9f6f2", 2048: "#f9f6f2",} # 数字砖块颜色渐变
数字块背景色渐变
不同的数字格子背景颜色不同
假设值,看一下配色
{"tiles": [[2, 4, 6, 16], [32, 64, 128, 256], [512, 1024, 2048, 4096], [8192, 16384, 32768, 65536]], "score": 11223243436}
2 ~ 2048有设置配色,后面更大的数就默认了,不过为了颜色不太杂乱,设置的颜色也少。
块的数字值设置
# 绘制每个方块
for y in range(self.grid_size):
for x in range(self.grid_size):
value = self.tile_values[y][x] # 块的数字值
color = tile_colors.get(value, "#cdc1b4") # 默认颜色
self.canvas.create_rectangle(x * 100 + 5, y * 100 + 5, (x + 1) * 100 - 5, (y + 1) * 100 - 5, fill=color, outline="#bfb3a0", width=8, tags='tile') # 画布绘制正方形块 初始块 就是0的状态, 此时砖块无数值
if value != 0: # 当 value ≥ 0 给砖块加入数值文字
self.canvas.create_text(x * 100 + 50, y * 100 + 50,text=str(value), font=("Arial", 24), fill="#776e65")
分数显示部分
self.high_score = self.load_highest_score()
self.high_score_label.config(text=f"Highest Score: {self.high_score}")
存在一个问题, 就是第一次运行时, 文件不存在, 然后获取最高分数是从文档获取的,所以这个时候会是0。
def load_highest_score(self):
"""加载最高分"""
if os.path.exists("high_scores.json"):
with open("high_scores.json", "r") as f:
high_scores = json.load(f)
if high_scores:
return high_scores[0][1] # 返回最高分
return 0
因此,预防第一局时, 还没有历史记录, 最高分数睡懒觉 设置为当没有最高分数时让最高分数跟随当前分数score。
几个特殊情况的最高分数值显示:一个是初始时没有文档最高值记录,此时应跟随score。然后我存入一最高分,① 游戏未结束,并不restart继续玩,此时应仍旧跟随;② 此时若restart 应从文档中取;③ 若此时我继续玩一会又restart 此时应从文档中取。
self.high_score = self.load_highest_score()
if self.high_score==0 or self.score > self.high_score:
self.high_score_label.config(text=f"Highest Score: {self.score}")
else:
self.high_score_label.config(text=f"Highest Score: {self.high_score}")
动作部分
随机新数
在空白方块上生成一个新的数字方块(2或4)
def spawn_tile(self):
empty_tiles = [(x, y) for x in range(self.grid_size) for y in range(self.grid_size) if
self.tile_values[y][x] == 0]
if empty_tiles:
x, y = random.choice(empty_tiles)
self.tile_values[y][x] = random.choice([2, 4]) # 一般是2或者4这种比较小的数值
上下左右
def move(self, direction):
"""根据方向移动方块并合并"""
original_tiles = [row[:] for row in self.tile_values] # 记录原始状态
merged = [[False] * self.grid_size for _ in range(self.grid_size)]
if direction == "Left":
for x in range(self.grid_size):
self.tile_values[x] = self.merge_row_left(self.tile_values[x])
elif direction == "Right":
for i in range(self.grid_size):
self.tile_values[i] = self.merge_row_left(self.tile_values[i][::-1])[::-1]
elif direction == "Up":
for j in range(self.grid_size):
col = [self.tile_values[i][j] for i in range(self.grid_size)]
merged_col = self.merge_row_left(col)
for i in range(self.grid_size):
self.tile_values[i][j] = merged_col[i]
elif direction == "Down":
for j in range(self.grid_size):
col = [self.tile_values[i][j] for i in range(self.grid_size)]
merged_col = self.merge_row_left(col[::-1])[::-1]
for i in range(self.grid_size):
self.tile_values[i][j] = merged_col[i]
# 如果发生了移动或合并,则生成新的方块
if original_tiles != self.tile_values:
self.spawn_tile()
合并&积分
def merge_row_left(self, row):
"""将给定行向左合并"""
new_row = [num for num in row if num != 0] # 去除零
merged_row = []
index = 0
while index < len(new_row):
# 如果相邻两个相同,则合并
if index + 1 < len(new_row) and new_row[index] == new_row[index + 1]:
merged_value = new_row[index] * 2
merged_row.append(merged_value) # 添加合并后的值
self.score += merged_value # 更新分数
index += 2 # 跳过下一个值
else:
merged_row.append(new_row[index])
index += 1
# 填充剩余的零
while len(merged_row) < self.grid_size:
merged_row.append(0)
return merged_row
数据存储
游戏进度保存
def save_game(self):
"""保存当前游戏状态 游戏实时存档"""
data = {'tiles': self.tile_values, 'score': self.score}
with open("save_game.txt", "w") as f:
json.dump(data, f)
每移动一步都会被记录下来,所以中断游戏的时候,用户下次再玩,开始新游戏时,加载游戏时,会先检测是否有历史记录,有的话可以继续未完成的游戏,也可以重新开始。
游戏加载
def load_game(self):
"""加载游戏状态 先查看是否存在残局, 有则恢复, 无则重开一局"""
if os.path.exists("save_game.txt"):
with open("save_game.txt", "r") as f:
data = json.load(f)
self.tile_values = data['tiles']
self.score = data['score']
else:
self.restart_game()
高分保存
将用户排名信息进行保存,存至文件永久保存。
def save_high_scores(self, scores):
"""保存高分记录"""
with open("high_scores.json", "w") as f:
json.dump(scores, f)
重来一局
game over判断
def check_game_over(self):
"""检查游戏是否结束"""
if any(0 in row for row in self.tile_values):
return False # 如果还有空方块,游戏未结束
for i in range(self.grid_size):
for j in range(self.grid_size):
# 检查相邻方块是否可以合并
if (i < self.grid_size - 1 and self.tile_values[i][j] == self.tile_values[i + 1][j]) or \
(j < self.grid_size - 1 and self.tile_values[i][j] == self.tile_values[i][j + 1]):
return False # 还有可以合并的方块
return True # 无法再移动或合并,游戏结束
显示game over
def show_game_over(self):
"""显示游戏结束的信息"""
if not hasattr(self, 'game_over_label'):
self.game_over_label = tk.Label(self.frame1, text="Game Over!", font=('Arial', 24), fg="red", bg="#faf8ef")
self.game_over_label.grid(row=0, column=2, padx=20)
restart的话,这个game over的label就要清掉,但又不能影响下一局的判断,所以加了判断:
if not hasattr(self, 'game_over_label'):
游戏重来
def restart_game(self):
"""重启游戏"""
self.tile_values = [[0] * self.grid_size for _ in range(self.grid_size)]
self.score = 0
self.game_over = False
# 清除游戏结束的标签(如果有的话)
if hasattr(self, 'game_over_label'):
self.game_over_label.destroy() # 移除游戏结束的文本
del self.game_over_label # 删除引用
self.spawn_tile()
self.spawn_tile() # 在两个随机位置生成方块
self.draw_board()
self.save_game()
分数排行
排行榜的分数增删( 增 包含改,同一个用户可以多次等级分数,会自动择高分)
def add_high_score(self, score_window):
"""添加新的高分记录"""
def submit_score():
name = entry.get()
if name:
score_list = self.load_high_scores()
exists = False
for i, (n, sc) in enumerate(score_list):
if n == name: # 如果名称已存在则更新分数
score_list[i][1] = max(sc, self.score)
exists = True
break
if not exists:
score_list.append([name, self.score]) # 新增高分
score_list.sort(key=lambda x: x[1], reverse=True) # 根据分数排序
score_list = score_list[:10] # 只保留前10名
self.save_high_scores(score_list)
score_window.destroy() # 关闭添加窗口
self.show_high_scores() # 更新高分显示
def delete_high_score(self, name):
"""删除某个玩家的高分记录"""
score_list = self.load_high_scores()
score_list = [item for item in score_list if item[0] != name]
self.save_high_scores(score_list)
self.show_high_scores()
完整代码
import tkinter as tk
import random
import json
import os
class Game2048:
def __init__(self, root):
self.root = root # 主窗口
root.title("2048 Game") # 窗口标题
# 设置游戏参数
self.grid_size = 4 # 4×4格
self.score = 0 # 当前局的分数
self.high_score = self.load_highest_score() # 最高分记录
self.tile_values = [[0] * self.grid_size for _ in range(self.grid_size)] # 数字块初始值为 0
self.game_over = False # 判断游戏是否结束的标志
self.create_ui() # 创建ui
self.load_game() # 加载游戏
self.spawn_tile() # 随机新增数字块
self.draw_board() # 绘制游戏局
root.bind("<Key>", self.key_pressed) # 绑定键盘事件到root窗口
def create_ui(self):
"""创建游戏界面"""
# 组件部分
# 上层:构建框架frame 包含2个文本标签:当前分数score和最高分数Highest Score
self.frame = tk.Frame(self.root, bg="#faf8ef", padx=10, pady=10)
self.frame.pack(pady=20)
# 分数标签 在frame中加score标签 显示当前游戏分数
self.score_label = tk.Label(self.frame, text=f"Score: {self.score}", font=("Arial", 18), bg="#faf8ef")
self.score_label.grid(row=0, column=0,padx=10,)
# 最高分标签 在frame中加high_score标签 显示最高游戏分数
self.high_score_label = tk.Label(self.frame, text=f"Highest Score: {self.high_score}", font=("Arial", 18),
bg="#faf8ef")
self.high_score_label.grid(row=0, column=1,padx=10,)
# 中层:创建画布 画布用于绘制游戏块
self.canvas = tk.Canvas(self.root, width=400, height=400, highlightthickness=0)
self.canvas.pack()
# 下层:构建框架frame1 包含2个按钮, 1个标签:重来按钮restart+信息按钮Scores+游戏结束标签
self.frame1 = tk.Frame(self.root, bg="#faf8ef")
self.frame1.pack(pady=20)
self.restart_button = tk.Button(self.frame1, text="Restart", command=self.restart_game, font=("Arial", 16),
bg="#8f7a66", fg="white", relief=tk.FLAT)
self.high_scores_button = tk.Button(self.frame1, text="Scores", command=self.show_high_scores,
font=("Arial", 16), bg="#8f7a66", fg="white", relief=tk.FLAT)
# 布局按钮
self.restart_button.grid(row=0, column=0, padx=20)
self.high_scores_button.grid(row=0, column=1, padx=20)
def draw_board(self):
"""绘制棋盘"""
self.canvas.delete("all") # 清空画布
tile_colors = {0: "#D9D5D1", 2: "#eee4da", 4: "#ede0c8", 8: "#f2b179",
16: "#f59563", 32: "#f67c5f", 64: "#f67c5f", 128: "#f9f6f2",
256: "#f9f6f2", 512: "#f9f6f2", 1024: "#f9f6f2", 2048: "#f9f6f2",} # 数字砖块颜色渐变
# 绘制每个方块
for y in range(self.grid_size):
for x in range(self.grid_size):
value = self.tile_values[y][x] # 块的数字值
color = tile_colors.get(value, "#cdc1b4") # 默认颜色
self.canvas.create_rectangle(x * 100 + 5, y * 100 + 5, (x + 1) * 100 - 5, (y + 1) * 100 - 5, fill=color, outline="#bfb3a0", width=8, tags='tile') # 画布绘制正方形块 初始块 就是0的状态, 此时砖块无数值
if value != 0: # 当 value ≥ 0 给砖块加入数值文字
self.canvas.create_text(x * 100 + 50, y * 100 + 50,text=str(value), font=("Arial", 24), fill="#776e65")
self.score_label.config(text=f"Score: {self.score}")
self.high_score = self.load_highest_score()
if self.high_score==0 or self.score > self.high_score:
self.high_score_label.config(text=f"Highest Score: {self.score}")
else:
self.high_score_label.config(text=f"Highest Score: {self.high_score}")
def load_game(self):
"""加载游戏状态 先查看是否存在残局, 有则恢复, 无则重开一局"""
if os.path.exists("save_game.txt"):
with open("save_game.txt", "r") as f:
data = json.load(f)
self.tile_values = data['tiles']
self.score = data['score']
else:
self.restart_game()
def load_highest_score(self):
"""加载最高分"""
if os.path.exists("high_scores.json"):
with open("high_scores.json", "r") as f:
high_scores = json.load(f)
if high_scores:
return high_scores[0][1] # 返回最高分
return 0
def save_game(self):
"""保存当前游戏状态 游戏实时存档"""
data = {'tiles': self.tile_values, 'score': self.score}
with open("save_game.txt", "w") as f:
json.dump(data, f)
def spawn_tile(self):
"""在空白方块上生成一个新的数字方块(2或4) 新增数字砖块 add new_tile"""
empty_tiles = [(x, y) for x in range(self.grid_size) for y in range(self.grid_size) if
self.tile_values[y][x] == 0]
if empty_tiles:
x, y = random.choice(empty_tiles)
self.tile_values[y][x] = random.choice([2, 4]) # 一般是2或者4这种比较小的数值
def key_pressed(self, event):
"""处理键盘按键事件"""
if self.game_over:
return
if event.keysym in ['Up', 'Down', 'Left', 'Right']:
self.move(event.keysym)
self.save_game()
self.draw_board()
if self.check_game_over():
self.game_over = True
self.show_game_over()
def check_game_over(self):
"""检查游戏是否结束"""
if any(0 in row for row in self.tile_values):
return False # 如果还有空方块,游戏未结束
for i in range(self.grid_size):
for j in range(self.grid_size):
# 检查相邻方块是否可以合并
if (i < self.grid_size - 1 and self.tile_values[i][j] == self.tile_values[i + 1][j]) or \
(j < self.grid_size - 1 and self.tile_values[i][j] == self.tile_values[i][j + 1]):
return False # 还有可以合并的方块
return True # 无法再移动或合并,游戏结束
def show_game_over(self):
"""显示游戏结束的信息"""
if not hasattr(self, 'game_over_label'):
self.game_over_label = tk.Label(self.frame1, text="Game Over!", font=('Arial', 24), fg="red", bg="#faf8ef")
self.game_over_label.grid(row=0, column=2, padx=20)
def move(self, direction):
"""根据方向移动方块并合并"""
original_tiles = [row[:] for row in self.tile_values] # 记录原始状态
merged = [[False] * self.grid_size for _ in range(self.grid_size)]
if direction == "Left":
for x in range(self.grid_size):
self.tile_values[x] = self.merge_row_left(self.tile_values[x])
elif direction == "Right":
for i in range(self.grid_size):
self.tile_values[i] = self.merge_row_left(self.tile_values[i][::-1])[::-1]
elif direction == "Up":
for j in range(self.grid_size):
col = [self.tile_values[i][j] for i in range(self.grid_size)]
merged_col = self.merge_row_left(col)
for i in range(self.grid_size):
self.tile_values[i][j] = merged_col[i]
elif direction == "Down":
for j in range(self.grid_size):
col = [self.tile_values[i][j] for i in range(self.grid_size)]
merged_col = self.merge_row_left(col[::-1])[::-1]
for i in range(self.grid_size):
self.tile_values[i][j] = merged_col[i]
# 如果发生了移动或合并,则生成新的方块
if original_tiles != self.tile_values:
self.spawn_tile()
def merge_row_left(self, row):
"""将给定行向左合并"""
new_row = [num for num in row if num != 0] # 去除零
merged_row = []
index = 0
while index < len(new_row):
# 如果相邻两个相同,则合并
if index + 1 < len(new_row) and new_row[index] == new_row[index + 1]:
merged_value = new_row[index] * 2
merged_row.append(merged_value) # 添加合并后的值
self.score += merged_value # 更新分数
index += 2 # 跳过下一个值
else:
merged_row.append(new_row[index])
index += 1
# 填充剩余的零
while len(merged_row) < self.grid_size:
merged_row.append(0)
return merged_row
def restart_game(self):
"""重启游戏"""
self.tile_values = [[0] * self.grid_size for _ in range(self.grid_size)]
self.score = 0
self.game_over = False
# 清除游戏结束的标签(如果有的话)
if hasattr(self, 'game_over_label'):
self.game_over_label.destroy() # 移除游戏结束的文本
del self.game_over_label # 删除引用
self.spawn_tile()
self.spawn_tile() # 在两个随机位置生成方块
self.draw_board()
self.save_game()
def show_high_scores(self):
"""显示高分记录"""
score_window = tk.Toplevel(self.root)
score_window.title("Rank")
score_window.configure(bg="#faf8ef")
score_list = self.load_high_scores()
for index, (name, score) in enumerate(score_list):
frame = tk.Frame(score_window, bg="#faf8ef")
frame.pack(pady=5)
tk.Label(frame, text=f"{index + 1}. {name}: {score}", font=("Arial", 16), bg="#faf8ef", fg="#776e65").pack(
side=tk.LEFT)
delete_button = tk.Button(frame, text="Delete", command=lambda name=name: self.delete_high_score(name),
font=("Arial", 12), bg="#f44336", fg="white", relief=tk.FLAT)
delete_button.pack(side=tk.RIGHT)
tk.Button(score_window, text="Add Score", command=lambda: self.add_high_score(score_window), font=("Arial", 16),
bg="#8f7a66", fg="white", activebackground="#d6ccc6").pack(pady=10)
def delete_high_score(self, name):
"""删除某个玩家的高分记录"""
score_list = self.load_high_scores()
score_list = [item for item in score_list if item[0] != name]
self.save_high_scores(score_list)
self.show_high_scores()
def load_high_scores(self):
"""加载高分记录"""
if os.path.exists("high_scores.json"):
with open("high_scores.json", "r") as f:
return json.load(f)
return []
def save_high_scores(self, scores):
"""保存高分记录"""
with open("high_scores.json", "w") as f:
json.dump(scores, f)
def add_high_score(self, score_window):
"""添加新的高分记录"""
def submit_score():
name = entry.get()
if name:
score_list = self.load_high_scores()
exists = False
for i, (n, sc) in enumerate(score_list):
if n == name: # 如果名称已存在则更新分数
score_list[i][1] = max(sc, self.score)
exists = True
break
if not exists:
score_list.append([name, self.score]) # 新增高分
score_list.sort(key=lambda x: x[1], reverse=True) # 根据分数排序
score_list = score_list[:10] # 只保留前10名
self.save_high_scores(score_list)
score_window.destroy() # 关闭添加窗口
self.show_high_scores() # 更新高分显示
# 弹出输入窗口
entry_window = tk.Toplevel(score_window)
entry_window.configure(bg="#faf8ef")
tk.Label(entry_window, text="Enter Your Name:", bg="#faf8ef", fg="#776e65", font=("Arial", 16)).pack()
entry = tk.Entry(entry_window, font=("Arial", 16))
entry.pack(padx=10, pady=10)
tk.Button(entry_window, text="Submit", command=submit_score, font=("Arial", 16),
bg="#8f7a66", fg="white", activebackground="#d6ccc6").pack()
if __name__ == "__main__":
root = tk.Tk()
game = Game2048(root)
root.mainloop()