Python制作小游戏:用线性代数思想构建 2048 游戏引擎

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 的移动逻辑看起来很复杂,但其实可以分解为两个原子操作:

  1. 压缩 (Compress):将非零元素向一侧堆叠,挤掉中间的零。
  2. 合并 (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 函数。TransposeReverse 只是改变数据的视角 ,而不改变数据的规则

这种思想在数学上称为同构(Isomorphism)------虽然方向不同,但操作的代数结构是完全一致的。

第五部分:总结与扩展

我们用不到 150 行代码,就复刻了一个具有完整核心逻辑的 2048。

这个项目是一个绝佳的练手案例,它涵盖了:

  1. 数组操作:切片、索引。
  2. 线性代数:转置矩阵的应用。
  3. GUI 编程:事件循环与渲染。
  4. 算法思维:如何将复杂问题(4个方向)约简为简单问题(1个方向)。

下一步的挑战:

  • 增加动画效果: 当前的方块是瞬间移动的。能否引入插值算法,让方块平滑滑动?
  • AI 求解器: 能否编写一个 Expectimax 算法,让电脑自动玩到 2048?

游戏开发不仅仅是娱乐,更是对逻辑思维的极致训练。

相关推荐
孟无岐2 小时前
【Laya】Scene3D 介绍
typescript·游戏引擎·游戏程序·laya
小二·2 小时前
Python Web 开发进阶实战:时空数据引擎 —— 在 Flask + Vue 中构建实时地理围栏与轨迹分析系统
前端·python·flask
孟无岐2 小时前
【Laya】Sprite3D 介绍
typescript·游戏引擎·游戏程序·laya
Uncertainty!!2 小时前
pycharm本地Failed to open X display(exiting)
ide·python·pycharm
小二·2 小时前
Python Web 开发进阶实战:可验证网络 —— 在 Flask + Vue 中实现去中心化身份(DID)与零知识证明(ZKP)认证
前端·网络·python
勇往直前plus2 小时前
解决:pycharm运行程序时出现Run ‘python tests for XXX.py‘的问题
ide·python·pycharm
Kratzdisteln2 小时前
【1902】自适应学习系统 - 完整技术方案
java·python·学习
天若有情6732 小时前
【Python】从0到1实现轻量级接口测试工具:基于Python+FastAPI+Pytest
python·测试工具·fastapi
Howrun7772 小时前
虚幻引擎_创建组件
游戏引擎·虚幻