“逻辑是游戏的心脏” —— 理解 2048 的核心算法
class Game:
"""游戏控制器。"""
def __init__(self):
self.grid = Grid() # 棋盘
self.score = 0 # 分数
self.moves = 0 # 移动次数
self.state = PLAYING # 游戏状态
def move(self, direction) # 移动方块
def reset(self) # 重新开始
def get_stats(self) # 获取统计信息
核心职责:
用户按下 ← 键
↓
Game.move("left")
↓
┌─────────────────────────────┐
│ 1. 检查是否可移动 │
│ 2. 执行滑动合并 │
│ 3. 计算本次得分 │
│ 4. 生成新方块 │
│ 5. 更新游戏状态 │
└─────────────────────────────┘
↓
返回是否成功移动
这是 2048 最核心的算法,让我们逐步拆解:
def _slide_and_merge(self, line: list[int], ascending: bool):
"""
滑动并合并一行。
参数:
line: 一行数据,如 [2, 0, 2, 4]
ascending: True=向左/上,False=向右/下
返回:
(new_line, merge_score)
"""
# 步骤 1:移除零(空格)
non_zero = [x for x in line if x != 0]
# [2, 0, 2, 4] → [2, 2, 4]
# 步骤 2:根据方向决定是否反转
if not ascending: # 向右/下移动
non_zero = non_zero[::-1]
# 向右:[2, 2, 4] → [4, 2, 2]
# 步骤 3:合并相邻相同值
merged = []
merge_score = 0
skip_next = False
for i in range(len(non_zero)):
if skip_next:
skip_next = False
continue
# 检查是否能和下一个合并
if i + 1 < len(non_zero) and non_zero[i] == non_zero[i + 1]:
# 合并!
new_value = non_zero[i] * 2
merged.append(new_value)
merge_score += new_value # 得分 = 合并后的值
skip_next = True # 跳过下一个(已合并)
else:
merged.append(non_zero[i])
# 步骤 4:补齐零
merged.extend([0] * (len(line) - len(merged)))
# [4, 4] → [4, 4, 0, 0]
# 步骤 5:如果向右/下,再反转回来
if not ascending:
merged = merged[::-1]
# 向右:[4, 4, 0, 0] → [0, 0, 4, 4]
return merged, merge_score
原始: [2, 0, 2, 4]
步骤 1(去零):[2, 2, 4]
步骤 2(ascending=True,不反转):[2, 2, 4]
步骤 3(合并):
i=0: 2 == 2 → 合并为 4,skip_next=True
i=1: 跳过
i=2: 4 无相邻 → 保持 4
结果:[4, 4]
步骤 4(补齐):[4, 4, 0, 0]
步骤 5(不反转):[4, 4, 0, 0]
最终: [4, 4, 0, 0] 得分:4
原始: [2, 2, 4, 0]
步骤 1(去零):[2, 2, 4]
步骤 2(ascending=False,反转):[4, 2, 2]
步骤 3(合并):
i=0: 4 ≠ 2 → 保持 4
i=1: 2 == 2 → 合并为 4,skip_next=True
i=2: 跳过
结果:[4, 4]
步骤 4(补齐):[4, 4, 0, 0]
步骤 5(反转回来):[0, 0, 4, 4]
最终: [0, 0, 4, 4] 得分:4
原始: [2, 2, 2, 2]
步骤 1(去零):[2, 2, 2, 2]
步骤 3(合并):
i=0: 2 == 2 → 合并为 4,skip_next=True
i=1: 跳过
i=2: 2 == 2 → 合并为 4,skip_next=True
i=3: 跳过
结果:[4, 4]
最终: [4, 4, 0, 0] 得分:8
⚠️ 注意:一次移动中,每个方块只能合并一次!
问题: 为什么向右滑动要先反转,处理完再反转回来?
答案: 统一处理逻辑!
# ❌ 不反转的写法(需要两套逻辑)
def slide_left(line):
# 从左到右处理
for i in range(len(line)):
if line[i] == line[i+1]:
...
def slide_right(line):
# 从右到左处理(容易出错)
for i in range(len(line)-1, -1, -1):
if line[i] == line[i-1]:
...
# ✅ 反转的写法(复用同一逻辑)
def slide(line, ascending):
if not ascending:
line = line[::-1] # 反转
# 统一的从左到右处理
result = merge(line)
if not ascending:
result = result[::-1] # 反转回来
return result
from enum import Enum, auto
class GameState(Enum):
"""游戏状态。"""
PLAYING = auto() # 游戏中
WON = auto() # 胜利(达到 2048)
GAME_OVER = auto() # 失败(无步可走)
┌─────────────┐
│ PLAYING │
│ (游戏中) │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
达到 2048 继续游戏 无步可走
│ │ │
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ WON │ │ PLAYING │ │GAME_OVER │
│ (胜利) │ │ (继续) │ │ (失败) │
└──────────┘ └──────────┘ └──────────┘
def _update_game_state(self) -> None:
"""检查并更新游戏状态。"""
# 检查胜利条件
if not self._won and self.grid.get_max_tile() >= self.WINNING_VALUE:
self._won = True
self.state = GameState.WON
# 检查失败条件
if self.grid.is_full() and not self._can_merge():
self.state = GameState.GAME_OVER
def _can_merge(self) -> bool:
"""检查是否有可合并的相邻方块。"""
# 检查水平方向
for row in range(GRID_SIZE):
for col in range(GRID_SIZE - 1):
if self.grid.cells[row][col] == self.grid.cells[row][col + 1]:
return True
# 检查垂直方向
for col in range(GRID_SIZE):
for row in range(GRID_SIZE - 1):
if self.grid.cells[row][col] == self.grid.cells[row + 1][col]:
return True
return False
# 在 Game 类中
def __init__(self):
self._won = False # 内部标记
def _update_game_state(self):
if not self._won and self.grid.get_max_tile() >= 2048:
self._won = True
self.state = GameState.WON
# 注意:这里不阻止玩家继续移动!
def move(self, direction):
if self.state == GameState.GAME_OVER:
return False # 失败后不能移动
# 但胜利后可以继续!
设计考虑:
WINNING_VALUE = 2048
def check_win(self) -> bool:
"""检查是否胜利。"""
return self.grid.get_max_tile() >= self.WINNING_VALUE
def get_max_tile(self) -> int:
"""获取棋盘上最大的方块值。"""
return max(max(row) for row in self.cells)
实现细节:
def is_game_over(self) -> bool:
"""检查游戏是否结束。"""
# 条件 1:棋盘未满 → 还能移动
if self.grid.has_empty_cells():
return False
# 条件 2:有可合并的 → 还能移动
if self._can_merge():
return False
# 既满又不能合并 → 真的结束了
return True
失败条件图解:
┌─────────────────────────────────────┐
│ 游戏结束判定流程 │
├─────────────────────────────────────┤
│ │
│ 有空单元格? │
│ │ │
│ ├─ 是 ─────────→ 未结束 │
│ │ 否 │
│ ↓ │
│ 有相邻相同值? │
│ │ │
│ ├─ 是 ─────────→ 未结束 │
│ │ 否 │
│ ↓ │
│ → 游戏结束 (GAME_OVER) │
│ │
└─────────────────────────────────────┘
# 情况 1:棋盘未满
[2, 0, 4, 0]
[0, 8, 0, 0]
[0, 0, 2, 0]
[0, 0, 0, 4]
→ 有空格,未结束 ✓
# 情况 2:棋盘满但可合并
[2, 4, 8, 16]
[4, 8, 16, 32]
[8, 16, 32, 64]
[16, 32, 64, 128]
→ 有相邻相同值(如第一列的 2,4,8,16),未结束 ✓
# 情况 3:棋盘满且不可合并
[2, 4, 2, 4]
[4, 2, 4, 2]
[2, 4, 2, 4]
[4, 2, 4, 2]
→ 无空格且无相邻相同,游戏结束 ✓
def move(self, direction: str) -> bool:
"""
向指定方向移动所有方块。
参数:
direction: "up", "down", "left", "right"
返回:
True 如果有方块移动,False 否则
"""
# 步骤 1:验证方向
if direction not in DIRECTIONS:
raise ValueError(f"Invalid direction: {direction}")
# 步骤 2:检查游戏是否结束
if self.state == GameState.GAME_OVER:
return False
# 步骤 3:选择对应的移动函数
move_func = {
DIRECTION_UP: self._move_up,
DIRECTION_DOWN: self._move_down,
DIRECTION_LEFT: self._move_left,
DIRECTION_RIGHT: self._move_right,
}[direction]
# 步骤 4:执行移动
moved, merge_score = move_func()
# 步骤 5:如果有移动,更新状态
if moved:
self.moves += 1
self.score += merge_score
self.grid.spawn_tile() # 生成新方块
self._update_game_state() # 检查胜负
return moved
def _move_left(self) -> tuple[bool, int]:
"""向左移动所有行。"""
return self._move_row(ascending=True)
def _move_right(self) -> tuple[bool, int]:
"""向右移动所有行。"""
return self._move_row(ascending=False)
def _move_row(self, ascending: bool) -> tuple[bool, int]:
"""
水平移动所有行。
返回:
(是否移动,合并得分)
"""
total_moved = False
total_merge_score = 0
for row in range(GRID_SIZE):
# 提取当前行
line = [self.grid.cells[row][col] for col in range(GRID_SIZE)]
# 滑动合并
new_line, merge_score = self._slide_and_merge(line, ascending)
total_merge_score += merge_score
# 检查是否有变化
for col, value in enumerate(new_line):
if self.grid.cells[row][col] != value:
total_moved = True
self.grid.cells[row][col] = value
return total_moved, total_merge_score
def _move_up(self) -> tuple[bool, int]:
"""向上移动所有列。"""
return self._move_column(ascending=True)
def _move_down(self) -> tuple[bool, int]:
"""向下移动所有列。"""
return self._move_column(ascending=False)
def _move_column(self, ascending: bool) -> tuple[bool, int]:
"""
垂直移动所有列。
原理:提取列 → 滑动合并 → 写回
"""
total_moved = False
total_merge_score = 0
for col in range(GRID_SIZE):
# 提取当前列
line = [self.grid.cells[row][col] for row in range(GRID_SIZE)]
# 滑动合并
new_line, merge_score = self._slide_and_merge(line, ascending)
total_merge_score += merge_score
# 写回
for row, value in enumerate(new_line):
if self.grid.cells[row][col] != value:
total_moved = True
self.grid.cells[row][col] = value
return total_moved, total_merge_score
观察: 行移动和列移动的代码几乎一样!
优化思路: 可以统一为一个方法,通过参数决定遍历方式。
给定初始状态:
[2, 0, 2, 0]
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 0, 4]
向左滑动后是什么状态?得分多少?
[2, 2, 2, 2]
向右滑动后是什么状态?
以下状态游戏是否结束?
[16, 8, 4, 2]
[8, 4, 2, 16]
[4, 2, 16, 8]
[2, 16, 8, 4]
# 在 Game 类中
def move(self, direction: str) -> bool:
moved = self.game.move(direction)
if moved:
# TODO: 播放音效
play_sound("move.mp3")
if self.merge_score > 0:
play_sound("merge.mp3")
class Game:
def __init__(self):
self.combo = 0 # 连击数
self.max_combo = 0
def move(self, direction: str) -> bool:
moved = self._do_move(direction)
if moved and self.merge_score > 0:
self.combo += 1
self.max_combo = max(self.max_combo, self.combo)
# 连击加分
self.score += self.merge_score * (1 + self.combo * 0.1)
else:
self.combo = 0
return moved
class Game:
def __init__(self):
self.move_history: list[str] = []
def move(self, direction: str) -> bool:
moved = self._do_move(direction)
if moved:
self.move_history.append(direction)
print(f"Move {len(self.move_history)}: {direction}")
return moved
def get_move_sequence(self) -> str:
"""获取移动序列(用于复盘)。"""
return " → ".join(self.move_history)
import cProfile
# 分析 move() 性能
game = Game()
cProfile.run('game.move("left")')
# 输出示例:
# 100 function calls in 0.001 seconds
# Ordered by: cumulative time
#
# ncalls tottime percall cumtime percall filename:lineno(function)
# 1 0.000 0.000 0.001 0.001 game.py:50(move)
# 1 0.000 0.000 0.000 0.000 game.py:80(_move_left)
# ...
(moved, score) 元组?A: 调用者需要知道:
用元组返回多个值比修改外部状态更清晰。
A: 职责分离!
move() 负责移动逻辑A: 添加详细日志:
def _slide_and_merge(self, line, ascending):
print(f"Input: {line}, ascending={ascending}")
non_zero = [x for x in line if x != 0]
print(f"After remove zeros: {non_zero}")
# ... 每一步都打印
print(f"Result: {merged}, score: {merge_score}")
return merged, merge_score
下一章: 第 5 章 用户界面开发
🐧 理解算法,才能优化它!