首页 > 编程开发 > Python    日期:2026-06-18 / 浏览

项目概述

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 的核心:

  1. 压缩:先过滤掉所有 0,把非零数字左移紧靠。
  2. 合并:从左到右扫描,相邻相等则合并(每个方块每次移动只能参与一次合并)。
  3. 补零:将合并后的结果右侧填充 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()

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章

1 2 3 4 5