2014年,Gabriele Cirulli 开发的《2048》席卷了全球。玩家在一个 4×44 \times 44×4 的网格中滑动数字,相同的数字碰撞合并,试图拼凑出 2048。
作为一个计算机科学的观察者,当我们剥离其鲜艳的 UI 外壳,会发现它的内核极其纯粹:这是一个状态机,其状态转换由矩阵操作定义。
今天,我们将不使用复杂的嵌套循环来分别处理"上下左右",而是利用线性代数的对称性,构建一个优雅的 Python 游戏引擎。
第一部分:数学建模------将游戏抽象为矩阵
游戏棋盘本质上是一个 4×44 \times 44×4 的矩阵 MMM。
M=[0202448000220000] M = \begin{bmatrix} 0 & 2 & 0 & 2 \\ 4 & 4 & 8 & 0 \\ 0 & 0 & 2 & 2 \\ 0 & 0 & 0 & 0 \end{bmatrix} M= 0400240008202020
1.1 核心操作的原子化
2048 的移动逻辑看起来很复杂,但其实可以分解为两个原子操作:
- 压缩 (Compress):将非零元素向一侧堆叠,挤掉中间的零。
- 合并 (Merge) :相邻且相同的元素结合,A+A→2AA + A \rightarrow 2AA+A→2A。
1.2 维度的降维打击:只写一个方向
初学者最容易犯的错误是写四个函数:move_left, move_right, move_up, move_down。这会导致代码重复且难以维护。
如果我们利用矩阵的几何变换 ,我们只需要写一个 move_left(左移)。
- 向右移 :等同于 Reverse(M)→Left→Reverse(M)\text{Reverse}(M) \rightarrow \text{Left} \rightarrow \text{Reverse}(M)Reverse(M)→Left→Reverse(M)
- 向上移 :等同于 Transpose(M)→Left→Transpose(M)\text{Transpose}(M) \rightarrow \text{Left} \rightarrow \text{Transpose}(M)Transpose(M)→Left→Transpose(M)
- 向下移 :等同于 Transpose(M)→Right→Transpose(M)\text{Transpose}(M) \rightarrow \text{Right} \rightarrow \text{Transpose}(M)Transpose(M)→Right→Transpose(M)
通过转置(行列互换)和镜像(左右翻转),我们将二维空间的四个方向问题,坍缩为单一方向的一维数组处理问题。
第二部分:Python 逻辑引擎实现
首先,我们实现不依赖于任何图形库的纯逻辑核心。
python
import random
class LogicEngine:
def __init__(self):
self.grid = [[0] * 4 for _ in range(4)]
self.score = 0
self.add_new_tile()
self.add_new_tile()
def add_new_tile(self):
"""在空白处随机生成一个 2 (90%) 或 4 (10%)"""
empty_cells = [(r, c) for r in range(4) for c in range(4) if self.grid[r][c] == 0]
if not empty_cells:
return
r, c = random.choice(empty_cells)
self.grid[r][c] = 2 if random.random() < 0.9 else 4
def compress(self, grid):
"""原子操作:压缩非零元素到左侧"""
new_grid = [[0] * 4 for _ in range(4)]
for r in range(4):
pos = 0
for c in range(4):
if grid[r][c] != 0:
new_grid[r][pos] = grid[r][c]
pos += 1
return new_grid
def merge(self, grid):
"""原子操作:合并相邻相同元素"""
for r in range(4):
for c in range(3):
if grid[r][c] != 0 and grid[r][c] == grid[r][c+1]:
grid[r][c] *= 2
grid[r][c+1] = 0
self.score += grid[r][c]
return grid
def reverse(self, grid):
"""矩阵镜像翻转"""
return [row[::-1] for row in grid]
def transpose(self, grid):
"""矩阵转置 (行列互换)"""
return [list(row) for row in zip(*grid)]
def move_left(self, grid):
"""核心逻辑:压缩 -> 合并 -> 再压缩"""
grid = self.compress(grid)
grid = self.merge(grid)
grid = self.compress(grid)
return grid
def step(self, direction):
"""
统一接口:根据方向变换矩阵,应用左移逻辑,再还原
direction: 'Left', 'Right', 'Up', 'Down'
"""
# 1. 变换坐标系
if direction == 'Up':
self.grid = self.transpose(self.grid)
elif direction == 'Down':
self.grid = self.transpose(self.grid)
self.grid = self.reverse(self.grid)
elif direction == 'Right':
self.grid = self.reverse(self.grid)
# 2. 应用核心逻辑 (Left)
new_grid = self.move_left(self.grid)
# 3. 检查是否有变化 (决定是否生成新数字)
changed = new_grid != self.grid
self.grid = new_grid
# 4. 还原坐标系
if direction == 'Up':
self.grid = self.transpose(self.grid)
elif direction == 'Down':
self.grid = self.reverse(self.grid)
self.grid = self.transpose(self.grid)
elif direction == 'Right':
self.grid = self.reverse(self.grid)
# 5. 如果盘面有变动,生成新数字
if changed:
self.add_new_tile()
return changed
第三部分:图形化呈现 (Pygame)
逻辑写好后,我们需要一个"皮囊"。我们将使用 Pygame 来渲染界面。
bash
pip install pygame
3.1 颜色配置与渲染循环
我们将颜色映射表硬编码在字典中,以便快速查找。
python
import pygame
import sys
# 颜色常量定义
COLORS = {
0: (205, 193, 180),
2: (238, 228, 218),
4: (237, 224, 200),
8: (242, 177, 121),
16: (245, 149, 99),
32: (246, 124, 95),
64: (246, 94, 59),
128: (237, 207, 114),
256: (237, 204, 97),
512: (237, 200, 80),
1024: (237, 197, 63),
2048: (237, 194, 46)
}
BG_COLOR = (187, 173, 160)
TEXT_COLOR = (119, 110, 101)
class GameUI:
def __init__(self):
pygame.init()
self.width, self.height = 400, 500
self.screen = pygame.display.set_mode((self.width, self.height))
pygame.display.set_caption("2048 - Matrix Engine")
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont("arial", 40, bold=True)
self.engine = LogicEngine()
def draw_grid(self):
self.screen.fill(BG_COLOR)
# 绘制分数
score_text = self.font.render(f"Score: {self.engine.score}", True, (255, 255, 255))
self.screen.blit(score_text, (20, 20))
# 绘制 4x4 方格
cell_size = 80
padding = 10
start_y = 100
for r in range(4):
for c in range(4):
value = self.engine.grid[r][c]
rect_x = padding + c * (cell_size + padding)
rect_y = start_y + r * (cell_size + padding)
# 绘制方块背景
color = COLORS.get(value, (60, 58, 50))
pygame.draw.rect(self.screen, color, (rect_x, rect_y, cell_size, cell_size), border_radius=5)
# 绘制数字
if value != 0:
text_color = TEXT_COLOR if value <= 4 else (255, 255, 255)
text_surf = self.font.render(str(value), True, text_color)
text_rect = text_surf.get_rect(center=(rect_x + cell_size/2, rect_y + cell_size/2))
self.screen.blit(text_surf, text_rect)
def run(self):
while True:
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_LEFT:
self.engine.step('Left')
elif event.key == pygame.K_RIGHT:
self.engine.step('Right')
elif event.key == pygame.K_UP:
self.engine.step('Up')
elif event.key == pygame.K_DOWN:
self.engine.step('Down')
self.draw_grid()
pygame.display.update()
self.clock.tick(30)
if __name__ == "__main__":
game = GameUI()
game.run()
第四部分:逻辑深度解析------为什么要这么做?
你可能会问:"为什么不直接写四个方向的逻辑?那样不是更直观吗?"
从软件工程的角度来看,重复是万恶之源(DRY Principle)。
如果我们分别为四个方向编写合并逻辑,我们不仅增加了 4 倍的代码量,更增加了 4 倍的 Debug 难度。如果在合并逻辑中发现了一个 Bug(例如分数计算错误),在传统写法中,你需要修改四个地方。
而在我们的矩阵变换写法中,所有的合并逻辑都收敛于 move_left 函数。Transpose 和 Reverse 只是改变数据的视角 ,而不改变数据的规则。
这种思想在数学上称为同构(Isomorphism)------虽然方向不同,但操作的代数结构是完全一致的。
第五部分:总结与扩展
我们用不到 150 行代码,就复刻了一个具有完整核心逻辑的 2048。
这个项目是一个绝佳的练手案例,它涵盖了:
- 数组操作:切片、索引。
- 线性代数:转置矩阵的应用。
- GUI 编程:事件循环与渲染。
- 算法思维:如何将复杂问题(4个方向)约简为简单问题(1个方向)。
下一步的挑战:
- 增加动画效果: 当前的方块是瞬间移动的。能否引入插值算法,让方块平滑滑动?
- AI 求解器: 能否编写一个 Expectimax 算法,让电脑自动玩到 2048?
游戏开发不仅仅是娱乐,更是对逻辑思维的极致训练。