首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >第七章进阶:Pygame实战小项目-AI贪吃蛇、Python五子棋、植物大战僵尸

第七章进阶:Pygame实战小项目-AI贪吃蛇、Python五子棋、植物大战僵尸

作者头像
啊阿狸不会拉杆
发布2026-01-21 10:10:21
发布2026-01-21 10:10:21
1980
举报

一、项目概述

    本次将带大家深入探索三个经典的Pygame实战项目:AI贪吃蛇、Python五子棋以及植物大战僵尸。通过这三个项目,我们将从基础到进阶,全面掌握Pygame在游戏开发中的应用,同时领略其在人工智能与游戏设计结合方面的魅力。资源绑定含有全部项目文件,请感兴趣的读者自行下载。

二、AI贪吃蛇

项目结构与思路
  • 环境搭建与初始化 :导入必要的库,如pygame、random、sys等,设置游戏窗口的尺寸、颜色等基础参数,初始化pygame环境。
  • 蛇与食物的表示 :用一维数组表示蛇的身体,通过索引操作实现蛇的移动、生长等操作;食物的位置随机生成,并确保不会与蛇的身体重合。
  • 游戏逻辑实现 :包括蛇的移动方向控制、碰撞检测(与墙壁、自身碰撞)、食物的生成与吃掉的逻辑、分数计算等。
  • AI算法集成 :运用如AI算法等路径搜索算法,让蛇能够自动规划路径去追逐食物,实现智能化的贪吃蛇。
关键知识点
  • Pygame基础 :窗口创建、事件循环、绘图函数(如绘制矩形、线条等)的使用。
  • 数组操作 :对蛇身体的一维数组进行插入、删除、移动等操作,实现蛇的动态变化。
  • 随机数生成 :用于食物的随机位置生成,确保游戏的随机性和趣味性。
  • 算法应用 :理解并应用路径搜索算法,使蛇具备智能行为,这是本项目的一大亮点,涉及到对算法逻辑的代码实现以及在游戏场景中的适配。
源代码:
代码语言:javascript
复制
​
# 导入必要的库
import pygame  # 用于游戏开发
import sys  # 系统相关操作
from random import randint  # 随机数生成

# 游戏场地尺寸(以格子数计算)
HEIGHT = 25  # 场地高度(y轴方向格子数)
WIDTH = 25  # 场地宽度(x轴方向格子数)

# 计算实际屏幕像素尺寸(每个格子25x25像素)
SCREEN_X = HEIGHT * 25  # 屏幕宽度
SCREEN_Y = WIDTH * 25  # 屏幕高度

FIELD_SIZE = HEIGHT * WIDTH  # 场地总格子数

# 蛇头在snake数组中的索引位置
HEAD = 0  # 蛇头总是数组第一个元素

# 定义场地元素状态常量(需保持足够间隔)
FOOD = 0  # 食物标识
UNDEFINED = (HEIGHT + 1) * (WIDTH + 1)  # 未定义状态(路径长度初始值)
SNAKE = 2 * UNDEFINED  # 蛇身标识

# 移动方向常量(用一维数组索引偏移量表示二维移动)
LEFT = -1  # 向左移动(索引-1)
RIGHT = 1  # 向右移动(索引+1)
UP = -WIDTH  # 向上移动(跨过WIDTH个元素)
DOWN = WIDTH  # 向下移动

# 错误码常量
ERR = -1111  # 错误移动方向

# 初始化游戏数据(用一维数组模拟二维场地)
board = [0] * FIELD_SIZE  # 主场地状态数组
snake = [0] * (FIELD_SIZE + 1)  # 蛇身位置数组(最大长度+1)
snake[HEAD] = 1 * WIDTH + 1  # 初始蛇头位置(第1行第1列)
snake_size = 1  # 初始蛇长度

# 临时变量用于模拟移动时的计算
tmpboard = [0] * FIELD_SIZE  # 临时场地状态
tmpsnake = [0] * (FIELD_SIZE + 1)  # 临时蛇身数组
tmpsnake[HEAD] = 1 * WIDTH + 1  # 临时蛇头初始位置
tmpsnake_size = 1  # 临时蛇长度

food = 3 * WIDTH + 3  # 初始食物位置(第3行第3列)
best_move = ERR  # 当前最佳移动方向
mov = [LEFT, RIGHT, UP, DOWN]  # 移动方向数组
key = pygame.K_RIGHT  # 初始移动方向(右侧)
score = 1  # 游戏分数(等于蛇长)


def show_text(screen, pos, text, color, font_bold=False, font_size=60, font_italic=False):
    """
    在屏幕上显示文字

    参数:
    screen: pygame.Surface - 要绘制文字的屏幕表面
    pos: tuple (x, y) - 文字左上角坐标
    text: str - 要显示的文字内容
    color: tuple (R, G, B) - 文字颜色
    font_bold: bool - 是否加粗(默认False)
    font_size: int - 字体大小(默认60)
    font_italic: bool - 是否斜体(默认False)
    """
    try:
        # 尝试加载中文字体(需要确保SimHei.ttf文件存在)
        font = pygame.font.Font("SimHei.ttf", font_size)
    except FileNotFoundError:
        font = pygame.font.Font(None, font_size)  # 使用默认字体

    # 设置字体样式
    font.set_bold(font_bold)
    font.set_italic(font_italic)

    # 渲染文字表面
    text_fmt = font.render(text, True, color)
    # 将文字绘制到屏幕
    screen.blit(text_fmt, pos)


def is_cell_free(idx, psize, psnake):
    """
    检查指定位置是否未被蛇身占据

    参数:
    idx: int - 要检查的位置索引
    psize: int - 当前蛇的长度
    psnake: list - 蛇身位置数组

    返回值:
    bool - True表示该位置空闲,False被蛇身占据
    """
    return idx not in psnake[:psize]


def is_move_possible(idx, move):
    """
    检查指定位置能否向给定方向移动

    参数:
    idx: int - 当前位置索引
    move: int - 移动方向(LEFT/RIGHT/UP/DOWN)

    返回值:
    bool - 是否可以移动
    """
    if move == LEFT:
        return idx % WIDTH > 1  # 不在最左列
    elif move == RIGHT:
        return idx % WIDTH < (WIDTH - 2)  # 不在最右列
    elif move == UP:
        return idx > (2 * WIDTH - 1)  # 不在最顶行
    elif move == DOWN:
        return idx < (FIELD_SIZE - 2 * WIDTH)  # 不在最底行


def board_reset(psnake, psize, pboard):
    """
    重置场地状态数组

    参数:
    psnake: list - 蛇身位置数组
    psize: int - 蛇当前长度
    pboard: list - 要重置的场地数组
    """
    for i in range(FIELD_SIZE):
        if i == food:
            pboard[i] = FOOD
        elif is_cell_free(i, psize, psnake):
            pboard[i] = UNDEFINED
        else:
            pboard[i] = SNAKE


def board_refresh(pfood, psnake, pboard):
    """
    使用BFS算法计算各位置到食物的最短路径

    参数:
    pfood: int - 食物位置索引
    psnake: list - 蛇身数组
    pboard: list - 场地状态数组

    返回值:
    bool - 是否找到从食物到蛇头的路径
    """
    queue = [pfood]  # BFS队列(从食物开始)
    inqueue = [0] * FIELD_SIZE
    found = False

    while queue:
        idx = queue.pop(0)
        if inqueue[idx]:
            continue
        inqueue[idx] = 1

        for move in mov:
            new_idx = idx + move
            # 检查移动是否合法
            if is_move_possible(idx, move):
                # 如果新位置是蛇头,标记找到路径
                if new_idx == psnake[HEAD]:
                    found = True
                # 更新路径长度
                if pboard[new_idx] < SNAKE:  # 排除蛇身位置
                    if pboard[new_idx] > pboard[idx] + 1:
                        pboard[new_idx] = pboard[idx] + 1
                    if not inqueue[new_idx]:
                        queue.append(new_idx)
    return found


def choose_shortest_safe_move(psnake, pboard):
    """
    选择离食物最近的合法移动方向

    参数:
    psnake: list - 蛇身数组
    pboard: list - 场地状态数组

    返回值:
    int - 最佳移动方向(LEFT/RIGHT/UP/DOWN)
    """
    best_move = ERR
    min_dist = SNAKE  # 初始设为最大值

    for move in mov:
        new_idx = psnake[HEAD] + move
        # 检查移动是否合法且路径更短
        if is_move_possible(psnake[HEAD], move) and pboard[new_idx] < min_dist:
            min_dist = pboard[new_idx]
            best_move = move
    return best_move


def choose_longest_safe_move(psnake, pboard):
    """
    选择离食物最远的合法移动方向(用于追尾策略)

    参数同上
    """
    best_move = ERR
    max_dist = -1

    for move in mov:
        new_idx = psnake[HEAD] + move
        if is_move_possible(psnake[HEAD], move) and pboard[new_idx] < UNDEFINED:
            if pboard[new_idx] > max_dist:
                max_dist = pboard[new_idx]
                best_move = move
    return best_move


def is_tail_inside():
    """
    检查虚拟移动后蛇尾是否可达(防止被困)

    使用临时数组进行计算,不影响主游戏状态
    """
    global tmpboard, tmpsnake, food, tmpsnake_size
    # 将临时蛇尾设为食物
    tmpboard[tmpsnake[tmpsnake_size - 1]] = FOOD
    tmpboard[food] = SNAKE  # 原食物位置设为蛇身

    # 计算路径
    result = board_refresh(tmpsnake[tmpsnake_size - 1], tmpsnake, tmpboard)

    # 还原临时修改
    tmpboard[tmpsnake[tmpsnake_size - 1]] = SNAKE
    # 检查是否紧邻蛇尾(长于3时不允许)
    for move in mov:
        if (tmpsnake[HEAD] + move == tmpsnake[tmpsnake_size - 1] and
                tmpsnake_size > 3 and
                is_move_possible(tmpsnake[HEAD], move)):
            result = False
    return result


def follow_tail():
    """
    让蛇头朝着蛇尾移动(防被困策略)
    """
    global tmpboard, tmpsnake, tmpsnake_size
    tmpsnake_size = snake_size
    tmpsnake = snake.copy()

    # 在临时场地模拟追尾
    board_reset(tmpsnake, tmpsnake_size, tmpboard)
    tmpboard[tmpsnake[-1]] = FOOD  # 蛇尾设为食物
    tmpboard[food] = SNAKE  # 食物位置设为蛇身
    board_refresh(tmpsnake[-1], tmpsnake, tmpboard)
    return choose_longest_safe_move(tmpsnake, tmpboard)


def any_possible_move():
    """
    当所有策略失败时,随机选择一个合法移动方向
    """
    board_reset(snake, snake_size, board)
    board_refresh(food, snake, board)

    best_move = ERR
    min_dist = SNAKE
    for move in mov:
        new_idx = snake[HEAD] + move
        if is_move_possible(snake[HEAD], move) and board[new_idx] < min_dist:
            min_dist = board[new_idx]
            best_move = move
    return best_move


def shift_array(arr, size):
    """
    将数组元素向右移位(模拟蛇移动)

    参数:
    arr: list - 要移位的数组
    size: int - 有效数据长度
    """
    for i in range(size, 0, -1):
        arr[i] = arr[i - 1]


def new_food():
    """
    随机生成新的食物位置
    """
    global food
    while True:
        # 生成随机行列(避开边界)
        row = randint(1, HEIGHT - 2)
        col = randint(1, WIDTH - 2)
        new_food = row * WIDTH + col
        # 检查位置是否空闲
        if is_cell_free(new_food, snake_size, snake):
            food = new_food
            break


def make_move(pbest_move):
    """
    执行实际的移动操作

    参数:
    pbest_move: int - 要执行的移动方向
    """
    global snake, board, snake_size, score
    # 移动蛇身
    shift_array(snake, snake_size)
    snake[HEAD] += pbest_move

    # 吃到食物的处理
    if snake[HEAD] == food:
        board[snake[HEAD]] = SNAKE
        snake_size += 1
        score += 1
        if snake_size < FIELD_SIZE:
            new_food()
    else:
        board[snake[HEAD]] = SNAKE
        board[snake[snake_size]] = UNDEFINED  # 清除旧蛇尾


def virtual_shortest_move():
    """
    虚拟移动(用于路径预测)
    """
    global tmpsnake, tmpboard, tmpsnake_size
    tmpsnake = snake.copy()
    tmpsnake_size = snake_size
    tmpboard = board.copy()

    food_eated = False
    while not food_eated:
        board_refresh(food, tmpsnake, tmpboard)
        move = choose_shortest_safe_move(tmpsnake, tmpboard)
        shift_array(tmpsnake, tmpsnake_size)
        tmpsnake[HEAD] += move

        if tmpsnake[HEAD] == food:
            tmpsnake_size += 1
            board_reset(tmpsnake, tmpsnake_size, tmpboard)
            tmpboard[food] = SNAKE
            food_eated = True
        else:
            tmpboard[tmpsnake[HEAD]] = SNAKE
            tmpboard[tmpsnake[tmpsnake_size]] = UNDEFINED


def find_safe_way():
    """
    综合策略寻找安全路径
    """
    if board_refresh(food, snake, board):
        # 优先使用最短路径
        virtual_shortest_move()
        if is_tail_inside():
            return choose_shortest_safe_move(snake, board)
    # 次选追尾策略
    return follow_tail()


def main():
    """
    主游戏循环
    """
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_X, SCREEN_Y))
    pygame.display.set_caption('AI-Snake By aalibuhuilagan')
    clock = pygame.time.Clock()
    isdead = False

    # 加载背景图(需要背景.png存在)
    try:
        bg = pygame.image.load("背景.png").convert()
        bg = pygame.transform.scale(bg, (SCREEN_X, SCREEN_Y))
    except:
        bg = None

    while True:
        # 事件处理
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE and isdead:
                return main()  # 重新开始

        # 画面绘制
        screen.fill((255, 255, 255)) if not bg else screen.blit(bg, (0, 0))

        # 绘制蛇身(根据长度调整线条连接方式)
        if snake_size == 1:
            # 单节蛇特殊处理
            pygame.draw.circle(screen, (136, 0, 21),
                               ((snake[0] // WIDTH) * 25 + 12, (snake[0] % WIDTH) * 25 + 12), 10)
        else:
            # 多节蛇用线条连接
            points = [((pos // WIDTH) * 25 + 12, (pos % WIDTH) * 25 + 12) for pos in snake[:snake_size]]
            pygame.draw.lines(screen, (136, 0, 21), False, points, 20)

        # 绘制食物
        pygame.draw.rect(screen, (20, 220, 39),
                         ((food // WIDTH) * 25, (food % WIDTH) * 25, 20, 20))

        # 游戏逻辑更新
        if not isdead:
            board_reset(snake, snake_size, board)
            if board_refresh(food, snake, board):
                best_move = find_safe_way()
            else:
                best_move = follow_tail()

            best_move = best_move if best_move != ERR else any_possible_move()

            if best_move != ERR:
                make_move(best_move)
            else:
                isdead = True  # 游戏结束

        # 显示游戏状态
        if isdead:
            show_text(screen, (100, 200), 'Game Over!', (227, 29, 18), font_size=100)
            show_text(screen, (150, 260), 'Try again!', (0, 0, 22), font_size=30)
        show_text(screen, (50, 500), f'Score: {score}', (255, 255, 255))

        pygame.display.update()
        clock.tick(20)  # 控制游戏帧率


if __name__ == '__main__':
    main()

​

效果展示:

参数详解

参数

含义

取值范围

作用

HEIGHT

场地高度(y轴方向格子数)

25

定义游戏场地的高度

WIDTH

场地宽度(x轴方向格子数)

25

定义游戏场地的宽度

SCREEN_X

屏幕宽度

HEIGHT * 25

计算实际屏幕像素宽度

SCREEN_Y

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、项目概述
  • 二、AI贪吃蛇
    • 项目结构与思路
    • 关键知识点
    • 源代码:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档