基于强化学习的贪吃蛇游戏(二)——贪吃蛇游戏实现

贪吃蛇游戏实现

在本项目中,我们使用PyGame库来实现贪吃蛇游戏的可视化界面和基本交互逻辑。PyGame 是一个基于 Python 的跨平台游戏开发库,专为构建 2D 游戏而设计。它提供了一系列简单易用的工具与模块,用于处理游戏开发中的各种核心功能,如 图形绘制、声音播放、用户输入交互和事件管理等。PyGame 基于 SDL(Simple DirectMedia Layer)库开发,能够高效地管理低层次的多媒体操作,同时保持了 Python 语言的简洁和可读性。在贪吃蛇项目中,PyGame 的灵活性和高效性使其成为一个理想的选择,尤其是在快速构建游戏原型时。

在实现贪吃蛇游戏时,我们将其分为多个模块,包括画面初始化、蛇的逻辑、食物生成、奖励与状态更新等部分

游戏初始化

在游戏初始化阶段,我们使用PyGame创建游戏窗口并设置基本参数,如窗口大小、背景颜色以及帧率控制器。
我们设置窗口大小为 像素,并设置每个单元格的大小为 像素。这样,我们就建立了一个 的游戏场景供蛇活动。接着我们初始化屏幕填充颜色为黑色,并调用pygame.display.update()实现界面刷新。

1
2
3
4
5
6
7
8
9
import pygame

def main():
pygame.init() # 初始化 PyGame
screen_size = (SCREEN_X, SCREEN_Y) # 窗口大小
screen = pygame.display.set_mode(screen_size) # 创建窗口
clock = pygame.time.Clock() # 帧率控制器
screen.fill((0, 0, 0)) # 设置背景颜色为黑色
pygame.display.set_caption("人工智能游戏") # 设置窗口标题

对于一个完整的游戏来说,参数的设置是必不可少的。这些参数往往是一些常数,决定了游戏的运行方式。在这里我们需要设置画面的宽和高,并对每一个单元格的尺寸进行基本的设置。同时,在游戏中,我们也需要对“上”、“下”、“左”、“右”四个方向进行基本的设置。考虑到这些设置应该全局统一,我们单独创建一个config.py文件用于存储这些设置。

1
2
3
4
5
6
7
8
# 定义画板大小  
SCREEN_X = 600
SCREEN_Y = 600
UNIT = 50

# 定义方向
DIRECTION = ['up', 'down', 'left', 'right']
DIRECTIONS = {'up': (0, -1), 'down': (0, 1), 'left': (-1, 0), 'right': (1, 0)}

蛇的实现

我们通过创建一个Snake类来实现蛇的逻辑,包括蛇的初始化、移动、增长、方向控制以及死亡检测等功能。为了让整个项目结构更清晰,我们单独创建snake.py来存储Snake类。

蛇的初始化

在Snake类的初始化阶段,我们通过一个列表存储蛇的身体,每个身体块为一个pygame.Rect对象,初始化方向为右。考虑到蛇身的初始化与蛇吃到食物时蛇身长度的增长均可以视为蛇身长度的增加,我们设计了add_body方法来模块化这些代码。
add_body方法中,通过对self.body列表的操作完成在蛇头处加入身体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pygame
from config import *

class Snake:
def __init__(self):
self.direction = 'right' # 初始方向
self.body = [] # 蛇的身体
for x in range(5): # 初始化蛇的长度为 5
self.add_body(self.direction)

def add_body(self, direction):
# 在蛇头添加新节点(默认行为)
left, top = (0, 150)
if self.body:
left, top = (self.body[0].left, self.body[0].top)
new_node = pygame.Rect(left, top, UNIT, UNIT)
if direction == 'left':
new_node.left -= UNIT
elif direction == 'right':
new_node.left += UNIT
elif direction == 'up':
new_node.top -= UNIT
elif direction == 'down':
new_node.top += UNIT
self.body.insert(0, new_node) # 插入到蛇头位置

蛇的移动

蛇的移动是贪吃蛇游戏中最基础也最核心的功能。在我们的实现中,蛇的移动采用了一种简单而高效的方法:通过对蛇身体列表的操作来实现移动效果。每当蛇移动时,我们会在移动方向上创建一个新的身体块作为新的蛇头,同时删除蛇尾的最后一个身体块,这样就能实现蛇的前进效果。在此我们仍需考虑的是,蛇并不能原地“回头”,所以我们对蛇的行进方向进行判断,阻止与蛇行进方向相反的转向。

1
2
3
4
5
6
7
8
def move(self, direction):
if (direction == 1 and self.direction != DIRECTION[0]) or \
(direction == 0 and self.direction != DIRECTION[1]) or \
(direction == 2 and self.direction != DIRECTION[3]) or \
(direction == 3 and self.direction != DIRECTION[2]):
self.direction = DIRECTION[direction]
self.add_body(self.direction) # 在方向上添加一个新块
self.body.pop() # 删除尾部块

死亡检测

在贪吃蛇游戏中,死亡检测是保证游戏规则和游戏结束判定的重要机制。我们需要实时检测两种可能导致游戏结束的情况:边界碰撞和自身碰撞。
边界碰撞检测。游戏场景有明确的边界,规定了蛇可以活动的范围。我们需要检查蛇头的坐标是否超出了屏幕的范围。一旦超出区域,则表示蛇已撞到边界,游戏结束。
自身碰撞检测。随着蛇身变长,蛇可能会与自己的身体发生碰撞。我们需要检查蛇头的位置是否与蛇身的任何一个部分重叠。这种检测通过比较蛇头与除蛇头外的所有身体块的位置来实现。一旦发生重叠,则表示蛇已经咬到了自己,游戏结束。

1
2
3
4
5
6
7
8
def is_dead(self):
# 判断蛇头是否撞墙
if self.body[0].x not in range(SCREEN_X) or self.body[0].y not in range(SCREEN_Y):
return True
# 判断蛇头是否撞到身体
if self.body[0] in self.body[1:]:
return True
return False

至此,我们已经完成对Snake类的定义,它具备移动和判断死亡的功能。

食物的生成

我们通过Food类来实现食物的创建和位置管理,确保食物在游戏场景中的合理分布。食物系统的核心是创建一个与蛇身体块大小相同的矩形对象。在游戏开始之前,食物对玩家并不可见,这其实从代码实现上很简单,只要让食物出现在场景的边界之外即可。故食物的初始位置设置在屏幕外(x=-50),表示食物尚未生成。
当需要在游戏区域内生成食物时,我们调用Food类中的set_food方法。set_food方法会在距离边界50像素以内的区域随机选择一个位置。在生成过程中,系统会检查新生成的食物位置是否与蛇的身体发生重叠,如果发生重叠,则会递归调用set_food方法,直到找到一个合适的位置。
同样的,为了让整个项目结构清晰,我们创建food.py来存储Food类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pygame
import random
from config import *

class Food:
def __init__(self):
self.rect = pygame.Rect(-50, 0, UNIT, UNIT) # 初始位置屏幕外

def set_food(self, snake):
all_positions = [pos for pos in range(50, SCREEN_X - 50, UNIT)]
self.rect.left = random.choice(all_positions)
self.rect.top = random.choice(all_positions)
# 确保食物不会生成在蛇身体上
for body in snake.body:
if self.rect.topleft == body.topleft:
return self.set_food(snake) # 递归调用,直到食物生成在蛇身体外

游戏逻辑

现在我们已经做好了贪吃蛇游戏所有的准备工作,下一步就是根据游戏规则实现游戏逻辑。
在编写具体游戏过程之前,我们首先要对我们定义的Snake类和Food实例化,并设置初始食物的位置。此外,我们在游戏开始之前要将得分设置为0。

1
2
3
4
5
6
7
8
9
# 初始化蛇和食物
snake = Snake()
food = Food()
food.set_food(snake) # 随机生成初始食物
direction = 1
KEYBOARD_CONTROL = True # 是否使用键盘控制

# 初始化得分
score = 0

在做好这些设置之后,我们就可以开始编写整个游戏了。对于一个2D图像游戏,其实是通过每秒高速刷新的画面让玩家观察游戏状态并交互。在程序中,我们通过编写一个循环完成画面的更新与交互判断。

游戏主循环

游戏主循环是贪吃蛇游戏逻辑的核心部分,负责处理游戏的各种事件、状态更新和画面渲染。其主要流程如图1所示:

游戏主循环主要模块流程图

图1 游戏主循环主要模块流程图

在事件处理部分,我们需要对游戏运行中的各种发生的事件进行相应的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while True:
for event in pygame.event.get(): # 获取事件
if event.type == pygame.QUIT: # 退出游戏
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN and KEYBOARD_CONTROL:
# 控制蛇的方向
if event.key == pygame.K_UP:
direction = 0
elif event.key == pygame.K_DOWN:
direction = 1
elif event.key == pygame.K_LEFT:
direction = 2
elif event.key == pygame.K_RIGHT:
direction = 3

pygame的事件系统是一个核心机制,通过pygame.event.get()获取事件队列中的所有事件,每个事件都是Event对象,包含type属性表示事件类型。pygame.QUIT是点击窗口关闭按钮时触发的事件类型。通过循环遍历事件队列并检查事件类型,可以响应用户的输入操作。每帧都需要处理事件队列,否则程序会失去响应。当我们使用键盘控制蛇时,我们也需要检测键盘上是否有对应的按键被按下。pygame.KEYDOWN是按下按键的事件,当键盘上有按键被按下时,我们检测是否是方向键,并记录下其对应的移动方向。
在接收到移动方向之后(或许没有),我们根据方向让蛇进行移动。

1
2
# 移动蛇
snake.move(direction)

贪吃蛇游戏中,每一次蛇的位置发生移动,系统都需要判断是否触发碰撞。当蛇头与食物的位置发生碰撞,我们需要增加蛇的长度,并让食物刷新在新的位置。而如果蛇头与边界或蛇身发生碰撞,游戏就以失败告终了。在这里,我们只需要增加对吃到食物情况的判断,因为我们已经在Snake类中完成了对死亡情况的判断。

1
2
3
4
5
6
7
8
9
10
# 判断是否吃到食物
if snake.body[0].colliderect(food.rect): # 如果蛇头与食物碰撞
snake.body.append(snake.body[-1].copy()) # 增加蛇的长度
food.set_food(snake) # 生成新的食物
score += 1

# 判断是否死亡
elif snake.is_dead():
pygame.quit()
sys.exit()

更新画面

游戏画面的更新是让游戏生动起来的关键环节。在贪吃蛇游戏中,我们需要绘制背景、蛇、食物和得分显示。pygame提供了丰富的绘图函数来帮助我们完成这些工作。
首先,我们绘制一个棋盘式的背景,使用深浅两种绿色交替出现:

1
2
3
4
5
6
7
8
# 绘制游戏画面
screen.fill((0, 0, 0)) # 清空画面
for i in range(SCREEN_X // UNIT):
for j in range(SCREEN_Y // UNIT):
if (i+j) % 2 == 0:
pygame.draw.rect(screen, (0, 150, 0), (i * UNIT, j * UNIT, UNIT, UNIT))
else:
pygame.draw.rect(screen, (0, 180, 0), (i * UNIT, j * UNIT, UNIT, UNIT))

接下来是游戏中最重要的角色——蛇的绘制。我们为蛇头使用图片来增加游戏的美观度,并根据移动方向旋转蛇头图片。图片应放在同级目录下的image文件夹中,以与游戏主目录进行区分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for i, rect in enumerate(snake.body):
if i == 0:
head_file = './image/head.png' # 加载蛇头图片
head_image = pygame.image.load(head_file)
head_image = pygame.transform.scale(head_image, (UNIT, UNIT)) # 缩放图片
# 根据移动方向旋转蛇头
if snake.direction == 'down':
head_image = pygame.transform.rotate(head_image, 180)
elif snake.direction == 'left':
head_image = pygame.transform.rotate(head_image, 90)
elif snake.direction == 'right':
head_image = pygame.transform.rotate(head_image, -90)
screen.blit(head_image, rect) # 绘制蛇头
else: # 蛇身
# 使用两种橙色交替绘制蛇身
color = (255, 201, 14) if i & 1 == 0 else (255, 143, 14)
pygame.draw.rect(screen, color, rect)

食物使用红色圆形表示,这样可以与方块形状的蛇形成视觉区分:

1
pygame.draw.circle(screen, (255, 0, 0), (food.rect.center), 25)  # 绘制圆形食物

最后,我们使用pygame的文字渲染功能,在屏幕右上角显示当前得分:

1
2
3
4
# 显示得分
font = pygame.font.Font(None, 36)
text = font.render(f"Score: {score}", True, (255, 255, 255))
screen.blit(text, (10, 10))

最后,我们使用pygame的文字渲染功能,在屏幕左上角显示当前得分:

1
2
3
# 更新画面
pygame.display.update()
clock.tick(10) # 控制帧率

通过这些绘制操作,我们实现了游戏画面的完整呈现。pygame.display.update()确保所有绘制内容都显示到屏幕上,而clock.tick()控制游戏运行速度,防止蛇移动过快影响游戏体验。至此,我们就完整的实现了贪吃蛇的游戏内容,玩家可以通过键盘上的方向键进行游戏。游戏画面如图2所示:

游戏运行画面

图2 游戏运行画面


基于强化学习的贪吃蛇游戏(二)——贪吃蛇游戏实现
http://dufolk.github.io/2024/12/19/snake-1/
作者
Dufolk
发布于
2024年12月19日
许可协议