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