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. 环境配置
程序中会用到的库:
python">import tkinter as tk
import random
import json
import os
其中os、json和random是python的内置库,不需要安装,tkinter是标准库之一,通常也不需要单独安装。
4. 代码实现
变量说明
python"># 设置游戏参数
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 # 判断游戏是否结束的标志
游戏界面
python">"""创建游戏界面"""
# 组件部分
# 上层:构建框架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)
棋盘绘制
python">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有设置配色,后面更大的数就默认了,不过为了颜色不太杂乱,设置的颜色也少。
块的数字值设置
python"># 绘制每个方块
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")
分数显示部分
python">self.high_score = self.load_highest_score()
self.high_score_label.config(text=f"Highest Score: {self.high_score}")
存在一个问题, 就是第一次运行时, 文件不存在, 然后获取最高分数是从文档获取的,所以这个时候会是0。
python">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 此时应从文档中取。
python">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)
python">def spawn_tile(self):empty_tiles = [(x, y) for x in range(self.grid_size) for y in range(self.grid_size) ifself.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这种比较小的数值
上下左右
python">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()
合并&积分
python">def merge_row_left(self, row):"""将给定行向左合并"""new_row = [num for num in row if num != 0] # 去除零merged_row = []index = 0while index < len(new_row):# 如果相邻两个相同,则合并if index + 1 < len(new_row) and new_row[index] == new_row[index + 1]:merged_value = new_row[index] * 2merged_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
数据存储
游戏进度保存
python">def save_game(self):"""保存当前游戏状态 游戏实时存档"""data = {'tiles': self.tile_values, 'score': self.score}with open("save_game.txt", "w") as f:json.dump(data, f)
每移动一步都会被记录下来,所以中断游戏的时候,用户下次再玩,开始新游戏时,加载游戏时,会先检测是否有历史记录,有的话可以继续未完成的游戏,也可以重新开始。
游戏加载
python">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()
高分保存
将用户排名信息进行保存,存至文件永久保存。
python">def save_high_scores(self, scores):"""保存高分记录"""with open("high_scores.json", "w") as f:json.dump(scores, f)
重来一局
game over判断
python">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
python">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就要清掉,但又不能影响下一局的判断,所以加了判断:
python">if not hasattr(self, 'game_over_label'):
游戏重来
python">def restart_game(self):"""重启游戏"""self.tile_values = [[0] * self.grid_size for _ in range(self.grid_size)]self.score = 0self.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()
分数排行
排行榜的分数增删( 增 包含改,同一个用户可以多次等级分数,会自动择高分)
python">def add_high_score(self, score_window):"""添加新的高分记录"""def submit_score():name = entry.get()if name:score_list = self.load_high_scores()exists = Falsefor i, (n, sc) in enumerate(score_list):if n == name: # 如果名称已存在则更新分数score_list[i][1] = max(sc, self.score)exists = Truebreakif 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()
完整代码
python">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)] # 数字块初始值为 0self.game_over = False # 判断游戏是否结束的标志self.create_ui() # 创建uiself.load_game() # 加载游戏self.spawn_tile() # 随机新增数字块self.draw_board() # 绘制游戏局root.bind("<Key>", self.key_pressed) # 绑定键盘事件到root窗口def create_ui(self):"""创建游戏界面"""# 组件部分# 上层:构建框架frame 包含2个文本标签:当前分数score和最高分数Highest Scoreself.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 0def 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) ifself.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:returnif 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 = Trueself.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 = 0while index < len(new_row):# 如果相邻两个相同,则合并if index + 1 < len(new_row) and new_row[index] == new_row[index + 1]:merged_value = new_row[index] * 2merged_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_rowdef restart_game(self):"""重启游戏"""self.tile_values = [[0] * self.grid_size for _ in range(self.grid_size)]self.score = 0self.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 = Falsefor i, (n, sc) in enumerate(score_list):if n == name: # 如果名称已存在则更新分数score_list[i][1] = max(sc, self.score)exists = Truebreakif 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()