项目概述
Pygame 是 Python 中一个功能强大的 2D 游戏开发库,它提供了处理图形、声音、输入和事件循环的完整工具集。本文通过 Pygame 实现一个经典 2048 数字合并游戏。
在这个游戏中,玩家在一个 4×4 的网格上滑动所有方块,相同数字的方块相撞后会合并为它们的和,目标是合成出数值为 2048 的方块。其中:
- 移动:使用方向键(↑ ↓ ← →)或 WASD 键控制所有方块同时向该方向滑动;同一行/列中数值相同的相邻方块会合并,每次移动后在随机空位生成一个新方块(90% 概率为 2,10% 概率为 4)。
- 得分:每次合并两个数值为 N 的方块,得分增加 2N;分数实时更新并记录本局最高分。
- 键盘操作:R:重新开始游戏。
- 胜利条件:棋盘上出现数值为 2048 的方块 = 游戏胜利(胜利后可继续挑战更高数值)。
- 失败条件:棋盘已满且没有任何可合并的相邻方块 = 游戏失败。
游戏实现

初始化与基础设置
在游戏启动前,需要初始化 Pygame 并定义棋盘布局、颜色方案等基础参数。
pygame.init()
SIZE = 520 # 窗口宽度(正方形)
GRID_OFF = 70 # 顶部 HUD 区域高度
COLS = 4 # 列数(同时也是行数)
GAP = 12 # 方块间距
TILE_W = (SIZE - GAP * (COLS + 1)) // COLS # 单个方块宽度
TILE_H = TILE_W
screen = pygame.display.set_mode((SIZE, SIZE + GRID_OFF))
pygame.display.set_caption("2048")
clock = pygame.time.Clock()
颜色与方块样式配置
不同数值的方块使用独立的背景色(bg)、前景文字色(fg)和字号(fs),数值越大颜色越深,视觉层次分明:
BG_COLOR = (18, 14, 6) # 全局背景(深褐黑)
GRID_COLOR = (26, 20, 6) # 网格背景
EMPTY_COLOR = (40, 30, 8) # 空格颜色
TILE_STYLES = {
0: {"bg": (40, 30, 8), "fg": (40, 30, 8), "fs": 36},
2: {"bg": (250,199, 117), "fg": (99, 56, 6), "fs": 36},
4: {"bg": (239,159, 39), "fg": (65, 36, 2), "fs": 36},
8: {"bg": (186,117, 23), "fg": (255, 230, 180), "fs": 36},
16: {"bg": (133, 79, 11), "fg": (255, 220, 150), "fs": 36},
32: {"bg": ( 99, 56, 6), "fg": (250, 199, 117), "fs": 36},
64: {"bg": ( 65, 36, 2), "fg": (250, 199, 117), "fs": 36},
128: {"bg": ( 83, 74, 183), "fg": (238, 237, 254), "fs": 30},
256: {"bg": ( 60, 52, 137), "fg": (206, 203, 246), "fs": 30},
512: {"bg": ( 38, 33, 92), "fg": (175, 169, 236), "fs": 30},
1024: {"bg": ( 24, 95, 165), "fg": (230, 241, 251), "fs": 24},
2048: {"bg": (226, 75, 74), "fg": (255, 255, 255), "fs": 24},
}
字体加载函数
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"
font_hud = pygame.font.Font(CHINESE_FONT_PATH, 22)
font_big = pygame.font.Font(CHINESE_FONT_PATH, 48)
def get_font(size):
return pygame.font.Font(CHINESE_FONT_PATH, size)
通过直接引用系统字体文件路径来支持中文显示。如需跨平台兼容,可替换为 pygame.font.SysFont(None, size) 使用系统默认字体。
核心函数设计
1. 网格操作
创建空网格 new_grid:
def new_grid():
return [[0] * COLS for _ in range(COLS)]
用二维列表表示棋盘,0 代表空格。
随机生成新方块 add_random:
def add_random(grid):
empty = [(r, c) for r in range(COLS) for c in range(COLS) if grid[r][c] == 0]
if not empty:
return
r, c = random.choice(empty)
grid[r][c] = 4 if random.random() < 0.1 else 2
收集所有空格位置后随机选择一个,以 10% 的概率生成 4,其余情况生成 2。
2. 核心滑动算法
单行滑动 slide_row:
def slide_row(row):
tiles = [v for v in row if v != 0] # 去除空格,压缩到一侧
gained = 0
i = 0
while i < len(tiles) - 1:
if tiles[i] == tiles[i + 1]: # 相邻相同则合并
tiles[i] *= 2
gained += tiles[i]
tiles.pop(i + 1)
i += 1
while len(tiles) < COLS: # 末尾补零
tiles.append(0)
return tiles, gained
该算法是 2048 的核心:
- 压缩:先过滤掉所有 0,把非零数字左移紧靠。
- 合并:从左到右扫描,相邻相等则合并(每个方块每次移动只能参与一次合并)。
- 补零:将合并后的结果右侧填充 0 至原长度。
四方向移动 move:
def move(grid, direction):
gained = 0
changed = False
g = [row[:] for row in grid] # 深拷贝,不修改原始网格
if direction == "left":
for r in range(COLS):
new_row, pts = slide_row(g[r])
if new_row != g[r]:
changed = True
g[r] = new_row
gained += pts
elif direction == "right":
for r in range(COLS):
rev, pts = slide_row(g[r][::-1]) # 翻转后滑动,再翻转回来
new_row = rev[::-1]
if new_row != g[r]:
changed = True
g[r] = new_row
gained += pts
elif direction == "up":
for c in range(COLS):
col = [g[r][c] for r in range(COLS)] # 提取列
new_col, pts = slide_row(col)
for r in range(COLS):
if g[r][c] != new_col[r]:
changed = True
g[r][c] = new_col[r]
gained += pts
elif direction == "down":
for c in range(COLS):
col = [g[r][c] for r in range(COLS)]
new_col, pts = slide_row(col[::-1]) # 翻转列后滑动,再翻转回来
new_col = new_col[::-1]
for r in range(COLS):
if g[r][c] != new_col[r]:
changed = True
g[r][c] = new_col[r]
gained += pts
return g, gained, changed
四个方向的实现统一复用 slide_row,利用翻转技巧避免重复逻辑:
- 向右 = 翻转每行 → 向左滑动 → 再翻转。
- 向上 = 转置为列 → 向左滑动 → 写回。
- 向下 = 转置为列 → 翻转 → 向左滑动 → 翻转 → 写回。
3. 游戏状态检测
检测是否还有合法移动 can_move:
def can_move(grid):
for r in range(COLS):
for c in range(COLS):
if grid[r][c] == 0: # 存在空格
return True
if c + 1 < COLS and grid[r][c] == grid[r][c + 1]: # 水平相邻相等
return True
if r + 1 < COLS and grid[r][c] == grid[r + 1][c]: # 垂直相邻相等
return True
return False
检测是否胜利 has_won:
def has_won(grid):
return any(grid[r][c] == 2048 for r in range(COLS) for c in range(COLS))
核心类设计
1. 动画类(AnimTile)
AnimTile 负责处理方块出现和合并时的缩放弹出动画:
class AnimTile:
def __init__(self, value, scale=0.0):
self.value = value
self.scale = scale # 当前缩放比例,0.0 = 不可见,1.0 = 完整大小
def update(self):
if self.scale < 1.0:
self.scale = min(1.0, self.scale + 0.12) # 每帧递增,约 8 帧完成动画
新方块初始 scale=0.0,每帧调用 update() 逐步放大到 1.0,实现平滑的弹出效果。
2. 游戏主类(Game)
Game 类是整个游戏的控制中心,负责状态管理、事件处理、逻辑更新和渲染。
构造函数 __init__:
def __init__(self):
self.best = 0 # 最高分(跨局保留)
self.reset()
重置游戏 reset:
def reset(self):
self.grid = new_grid()
self.score = 0
self.game_over = False
self.won = False
self.anims = [[AnimTile(0, 1.0)] * COLS for _ in range(COLS)]
add_random(self.grid)
add_random(self.grid)
self._sync_anims(set())
初始放置两个随机方块,并同步动画状态。
同步动画状态 _sync_anims:
def _sync_anims(self, new_cells):
for r in range(COLS):
for c in range(COLS):
v = self.grid[r][c]
if (r, c) in new_cells:
self.anims[r][c] = AnimTile(v, 0.0) # 新出现的方块从 0 开始动画
else:
self.anims[r][c] = AnimTile(v, 1.0) # 已有方块直接显示完整大小
执行移动 do_move:
def do_move(self, direction):
if self.game_over:
return
new_grid_, gained, changed = move(self.grid, direction)
if not changed: # 移动无效,直接返回
return
self.grid = new_grid_
self.score += gained
if self.score > self.best:
self.best = self.score
add_random(self.grid)
self._sync_anims(set())
if not can_move(self.grid):
self.game_over = True
if has_won(self.grid) and not self.won:
self.won = True
事件处理 handle_events:
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit(); sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r:
self.reset()
key_dir = {
pygame.K_LEFT: "left", pygame.K_a: "left",
pygame.K_RIGHT: "right", pygame.K_d: "right",
pygame.K_UP: "up", pygame.K_w: "up",
pygame.K_DOWN: "down", pygame.K_s: "down",
}
if event.key in key_dir:
self.do_move(key_dir[event.key])
支持方向键和 WASD 两套操作方案,R 键重新开始。
绘制方法 draw:
def draw(self):
screen.fill(BG_COLOR)
# 绘制 HUD 信息栏
pygame.draw.rect(screen, (22, 16, 5), (0, 0, SIZE, GRID_OFF))
pygame.draw.line(screen, (80, 60, 20), (0, GRID_OFF), (SIZE, GRID_OFF), 1)
screen.blit(font_hud.render("2048", True, (250, 199, 117)), (14, 14))
screen.blit(font_hud.render(f"得分 {self.score}", True, WHITE), (14, 38))
screen.blit(font_hud.render(f"最高 {self.best}", True, GRAY), (200, 38))
screen.blit(font_hud.render("R=重来", True, GRAY), (SIZE - 100, 38))
# 绘制网格背景
grid_rect = pygame.Rect(GAP - 2, GRID_OFF + GAP - 2,
SIZE - 2 * GAP + 4,
COLS * (TILE_H + GAP) + 4)
pygame.draw.rect(screen, GRID_COLOR, grid_rect, border_radius=10)
# 绘制每个方块(含缩放动画)
for r in range(COLS):
for c in range(COLS):
rect = tile_rect(r, c)
anim = self.anims[r][c]
v = self.grid[r][c]
style = TILE_STYLES.get(v, TILE_STYLES[2048])
pygame.draw.rect(screen, EMPTY_COLOR, rect, border_radius=6)
if v == 0:
continue
sc = anim.scale
sw, sh = int(TILE_W * sc), int(TILE_H * sc)
sx = rect.centerx - sw // 2
sy = rect.centery - sh // 2
tile_surf = pygame.Surface((sw, sh), pygame.SRCALPHA)
pygame.draw.rect(tile_surf, style["bg"], (0, 0, sw, sh),
border_radius=int(6 * sc))
screen.blit(tile_surf, (sx, sy))
if sc > 0.5:
fnt = get_font(style["fs"])
text = fnt.render(str(v), True, style["fg"])
screen.blit(text, (rect.centerx - text.get_width() // 2,
rect.centery - text.get_height() // 2))
# 游戏结束/胜利遮罩
if self.game_over or self.won:
overlay = pygame.Surface((SIZE, SIZE), pygame.SRCALPHA)
overlay.fill((18, 14, 6, 185))
screen.blit(overlay, (0, GRID_OFF))
t1 = font_big.render("YOU WIN!" if self.won else "GAME OVER", True,
(250, 199, 117) if self.won else (226, 75, 74))
t2 = font_hud.render(f"得分 {self.score} 最高 {self.best}", True, WHITE)
t3 = font_hud.render("按 R 重新开始", True, GRAY)
cx = SIZE // 2
cy = GRID_OFF + (SIZE - GRID_OFF) // 2
screen.blit(t1, (cx - t1.get_width() // 2, cy - 60))
screen.blit(t2, (cx - t2.get_width() // 2, cy + 10))
screen.blit(t3, (cx - t3.get_width() // 2, cy + 46))
pygame.display.flip()
辅助函数 tile_rect:
def tile_rect(row, col):
x = GAP + col * (TILE_W + GAP)
y = GRID_OFF + GAP + row * (TILE_H + GAP)
return pygame.Rect(x, y, TILE_W, TILE_H)
根据行列索引计算方块的屏幕坐标,统一由 GAP 控制间距。
主循环 run:
def run(self):
while True:
self.handle_events()
self.update()
self.draw()
clock.tick(60)
以 60 FPS 运行,保证动画流畅。
全部代码
import pygame
import sys
import random
import copy
pygame.init()
SIZE = 520
GRID_OFF = 70 # top HUD height
COLS = 4
GAP = 12
TILE_W = (SIZE - GAP * (COLS + 1)) // COLS
TILE_H = TILE_W
screen = pygame.display.set_mode((SIZE, SIZE + GRID_OFF))
pygame.display.set_caption("2048")
clock = pygame.time.Clock()
BG_COLOR = (18, 14, 6)
GRID_COLOR = (26, 20, 6)
EMPTY_COLOR= (40, 30, 8)
WHITE = (230, 225, 210)
GRAY = (130, 120, 100)
TILE_STYLES = {
0: {"bg": (40, 30, 8), "fg": (40, 30, 8), "fs": 36},
2: {"bg": (250,199, 117), "fg": (99, 56, 6), "fs": 36},
4: {"bg": (239,159, 39), "fg": (65, 36, 2), "fs": 36},
8: {"bg": (186,117, 23), "fg": (255, 230, 180), "fs": 36},
16: {"bg": (133, 79, 11), "fg": (255, 220, 150), "fs": 36},
32: {"bg": ( 99, 56, 6), "fg": (250, 199, 117), "fs": 36},
64: {"bg": ( 65, 36, 2), "fg": (250, 199, 117), "fs": 36},
128: {"bg": ( 83, 74, 183), "fg": (238, 237, 254), "fs": 30},
256: {"bg": ( 60, 52, 137), "fg": (206, 203, 246), "fs": 30},
512: {"bg": ( 38, 33, 92), "fg": (175, 169, 236), "fs": 30},
1024: {"bg": ( 24, 95, 165), "fg": (230, 241, 251), "fs": 24},
2048: {"bg": (226, 75, 74), "fg": (255, 255, 255), "fs": 24},
}
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"
# 使用具体字体文件(如果系统有):
font_hud = pygame.font.Font(CHINESE_FONT_PATH, 22)
font_big = pygame.font.Font(CHINESE_FONT_PATH, 48)
def get_font(size):
# return pygame.font.SysFont("Courier New", size, bold=True)
return pygame.font.Font(CHINESE_FONT_PATH, size) # 回退到默认字体
def tile_rect(row, col):
x = GAP + col * (TILE_W + GAP)
y = GRID_OFF + GAP + row * (TILE_H + GAP)
return pygame.Rect(x, y, TILE_W, TILE_H)
def new_grid():
return [[0] * COLS for _ in range(COLS)]
def add_random(grid):
empty = [(r, c) for r in range(COLS) for c in range(COLS) if grid[r][c] == 0]
if not empty:
return
r, c = random.choice(empty)
grid[r][c] = 4 if random.random() < 0.1 else 2
def slide_row(row):
tiles = [v for v in row if v != 0]
gained = 0
i = 0
while i < len(tiles) - 1:
if tiles[i] == tiles[i + 1]:
tiles[i] *= 2
gained += tiles[i]
tiles.pop(i + 1)
i += 1
while len(tiles) < COLS:
tiles.append(0)
return tiles, gained
def move(grid, direction):
gained = 0
changed = False
g = [row[:] for row in grid]
if direction == "left":
for r in range(COLS):
new_row, pts = slide_row(g[r])
if new_row != g[r]:
changed = True
g[r] = new_row
gained += pts
elif direction == "right":
for r in range(COLS):
rev, pts = slide_row(g[r][::-1])
new_row = rev[::-1]
if new_row != g[r]:
changed = True
g[r] = new_row
gained += pts
elif direction == "up":
for c in range(COLS):
col = [g[r][c] for r in range(COLS)]
new_col, pts = slide_row(col)
for r in range(COLS):
if g[r][c] != new_col[r]:
changed = True
g[r][c] = new_col[r]
gained += pts
elif direction == "down":
for c in range(COLS):
col = [g[r][c] for r in range(COLS)]
new_col, pts = slide_row(col[::-1])
new_col = new_col[::-1]
for r in range(COLS):
if g[r][c] != new_col[r]:
changed = True
g[r][c] = new_col[r]
gained += pts
return g, gained, changed
def can_move(grid):
for r in range(COLS):
for c in range(COLS):
if grid[r][c] == 0:
return True
if c + 1 < COLS and grid[r][c] == grid[r][c + 1]:
return True
if r + 1 < COLS and grid[r][c] == grid[r + 1][c]:
return True
return False
def has_won(grid):
return any(grid[r][c] == 2048 for r in range(COLS) for c in range(COLS))
class AnimTile:
"""Handles scale-pop animation when a tile appears / merges."""
def __init__(self, value, scale=0.0):
self.value = value
self.scale = scale # 0..1
def update(self):
if self.scale < 1.0:
self.scale = min(1.0, self.scale + 0.12)
class Game:
def __init__(self):
self.best = 0
self.reset()
def reset(self):
self.grid = new_grid()
self.score = 0
self.game_over = False
self.won = False
self.anims = [[AnimTile(0, 1.0)] * COLS for _ in range(COLS)]
add_random(self.grid); add_random(self.grid)
self._sync_anims(set())
def _sync_anims(self, new_cells):
for r in range(COLS):
for c in range(COLS):
v = self.grid[r][c]
if (r, c) in new_cells:
self.anims[r][c] = AnimTile(v, 0.0)
else:
self.anims[r][c] = AnimTile(v, 1.0)
def do_move(self, direction):
if self.game_over:
return
new_grid_, gained, changed = move(self.grid, direction)
if not changed:
return
self.grid = new_grid_
self.score += gained
if self.score > self.best:
self.best = self.score
new_cells = set()
add_random(self.grid)
# mark newly appeared cell for animation
for r in range(COLS):
for c in range(COLS):
if self.grid[r][c] != 0 and (gained > 0 or self.anims[r][c].value == 0):
pass
# simple: animate all cells
self._sync_anims(set())
if not can_move(self.grid):
self.game_over = True
if has_won(self.grid) and not self.won:
self.won = True
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit(); sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r:
self.reset()
key_dir = {
pygame.K_LEFT: "left", pygame.K_a: "left",
pygame.K_RIGHT: "right", pygame.K_d: "right",
pygame.K_UP: "up", pygame.K_w: "up",
pygame.K_DOWN: "down", pygame.K_s: "down",
}
if event.key in key_dir:
self.do_move(key_dir[event.key])
def update(self):
for r in range(COLS):
for c in range(COLS):
self.anims[r][c].update()
def draw(self):
screen.fill(BG_COLOR)
# --- HUD ---
pygame.draw.rect(screen, (22, 16, 5), (0, 0, SIZE, GRID_OFF))
pygame.draw.line(screen, (80, 60, 20), (0, GRID_OFF), (SIZE, GRID_OFF), 1)
screen.blit(font_hud.render("2048", True, (250, 199, 117)), (14, 14))
screen.blit(font_hud.render(f"得分 {self.score}", True, WHITE), (14, 38))
screen.blit(font_hud.render(f"最高 {self.best}", True, GRAY), (200, 38))
screen.blit(font_hud.render("R=重来", True, GRAY), (SIZE - 100, 38))
# --- Grid background ---
grid_rect = pygame.Rect(GAP - 2, GRID_OFF + GAP - 2,
SIZE - 2 * GAP + 4,
COLS * (TILE_H + GAP) + 4)
pygame.draw.rect(screen, GRID_COLOR, grid_rect, border_radius=10)
# --- Tiles ---
for r in range(COLS):
for c in range(COLS):
rect = tile_rect(r, c)
anim = self.anims[r][c]
v = self.grid[r][c]
style = TILE_STYLES.get(v, TILE_STYLES[2048])
# empty slot
pygame.draw.rect(screen, EMPTY_COLOR, rect, border_radius=6)
if v == 0:
continue
# scale-pop effect
sc = anim.scale
sw = int(TILE_W * sc)
sh = int(TILE_H * sc)
sx = rect.centerx - sw // 2
sy = rect.centery - sh // 2
tile_surf = pygame.Surface((sw, sh), pygame.SRCALPHA)
pygame.draw.rect(tile_surf, style["bg"],
(0, 0, sw, sh), border_radius=int(6 * sc))
screen.blit(tile_surf, (sx, sy))
# number
if sc > 0.5:
fs = style["fs"]
fnt = get_font(fs)
text = fnt.render(str(v), True, style["fg"])
screen.blit(text, (rect.centerx - text.get_width() // 2,
rect.centery - text.get_height() // 2))
# --- Game over / Win overlay ---
if self.game_over or self.won:
overlay = pygame.Surface((SIZE, SIZE + GRID_OFF - GRID_OFF), pygame.SRCALPHA)
overlay.fill((18, 14, 6, 185))
screen.blit(overlay, (0, GRID_OFF))
if self.won:
t1 = font_big.render("YOU WIN!", True, (250, 199, 117))
else:
t1 = font_big.render("GAME OVER", True, (226, 75, 74))
t2 = font_hud.render(f"得分 {self.score} 最高 {self.best}", True, WHITE)
t3 = font_hud.render("按 R 重新开始", True, GRAY)
cx = SIZE // 2
cy = GRID_OFF + (SIZE - GRID_OFF) // 2
screen.blit(t1, (cx - t1.get_width() // 2, cy - 60))
screen.blit(t2, (cx - t2.get_width() // 2, cy + 10))
screen.blit(t3, (cx - t3.get_width() // 2, cy + 46))
pygame.display.flip()
def run(self):
while True:
self.handle_events()
self.update()
self.draw()
clock.tick(60)
if __name__ == "__main__":
Game().run()












