零、序章
这是一个使用Python写的贪吃蛇游戏。
可以练习一些面向对象的知识。
实现的功能有:
- 🐍的移动、生长、死亡
- 食物的刷新、碰撞检测
- 计分板的刷新、最高分的本地读写
可以改进的地方有:
- 暂停功能
- 生长部分代码也许可以继续优化
- 即使🐍只有3块,理论上绝不可能撞到自己,但方向键按太快仍然会死,估计是碰撞检测的原因
一、游戏主程序
import time
from turtle import Screen
from snake import Snake
from food import Food
from scoreboard import ScoreBoard
def quit_game():
"""
这是一个负责退出游戏的函数
:return: 无
"""
global game_is_on
game_is_on = False
# 实例化屏幕对象
screen = Screen()
# 设置宽、高
screen.setup(width=600, height=600)
# 设置背景色
screen.bgcolor('Black')
# 设置标题
screen.title('A Snake Game')
# 关闭屏幕实时刷新
screen.tracer(0)
# 实例化计分板对象
scoreboard = ScoreBoard(screen)
# 实例化🐍对象
snake = Snake()
# 计算边界(emmm这个计算边界的函数我不是很确定放哪儿。。。。)
limits = snake.cal_limits(screen)
# 实例化食物对象
dot = Food()
dot.refresh_pos(limits)
# 监听用户键盘行为,上下左右分别控制🐍的上下左右移动
screen.listen()
screen.onkey(key='Up', fun=snake.up)
screen.onkey(key='Down', fun=snake.down)
screen.onkey(key='Left', fun=snake.left)
screen.onkey(key='Right', fun=snake.right)
screen.onkey(key='q', fun=quit_game)
# 使用game_is_on参数控制循环
game_is_on = True
while game_is_on:
# 每次循环刷新一次屏幕,这是为了保证所有物体(Turtle)移动完了才刷新,防止🐍分裂
screen.update()
# 循环间隔设置成1秒
time.sleep(0.1)
# 🐍必须保持移动
snake.move()
# 碰撞检测,使用turtle自带的distance方法
if snake.segments[0].distance(dot) <= 10:
# 撞到了就刷新分数
scoreboard.refresh_score()
# 🐍生长
snake.grow()
# 食物刷新位置
dot.refresh_pos(limits)
# 检测🐍是否死亡
if snake.is_dead(limits):
# 刷新🐍
snake.refresh()
# 刷新分数板
scoreboard.score = -1
scoreboard.refresh_score()
# 把最高分写入本地存储
scoreboard.write_highest_score('highest_score.txt')
print('Gameover!')
二、🐍类
import time
from turtle import Turtle, Screen
# 蛇的初始位置
STARTING_POSITION = [(0, 0), (-20, 0), (-40, 0)]
# 记录蛇尾后一块位置,这是为了方便写蛇生长的函数
LAST_POS = (-60, 0)
# 每次移动距离
MOVE_DISTANCE = 20
class Snake:
def __init__(self):
# 🐍的身体,用列表存储Turtle对象
self.segments = []
# 把尾巴后一格位置单独作为一个属性,为了方便写生长函数
self.last_pos = LAST_POS
# 初始先生长三格
self.create_snake()
def create_snake(self):
# 按照初始位置常量实例化三个Turtle对象组成🐍
for position in STARTING_POSITION:
new_segment = Turtle(shape='square')
new_segment.penup()
new_segment.color('white')
new_segment.goto(position)
# 加入身体列表中
self.segments.append(new_segment)
def move(self):
"""
这是一个控制蛇移动的函数,核心思想是每一块要移动到它前面一块的位置,这样只需要头动,后面的就跟着动
:return:
"""
# 每次移动要刷新🐍尾后一格的坐标
self.last_pos = self.segments[-1].pos()
# 每一块移动到它前面一块的位置
for seg_num in range(len(self.segments) - 1, 0, -1):
self.segments[seg_num].goto(self.segments[seg_num - 1].pos())
# 头往前移动一个MOVE_DISTANCE的长度
self.segments[0].forward(MOVE_DISTANCE)
def grow(self):
"""
这是一个控制🐍生长的函数
:return: 无
"""
new_seg = Turtle(shape='square')
new_seg.penup()
new_seg.color('white')
# 这里让新的Turtle对象去🐍尾后一格跟着
new_seg.goto(self.last_pos)
self.segments.append(new_seg)
def refresh(self):
"""
这是一个刷新🐍的函数
:return: 无
"""
# 根据初始位置参数获取🐍初始长度
n = len(STARTING_POSITION)
# 其他部分都不要了
for seg in self.segments[n:]:
seg.ht()
self.segments = self.segments[0:n]
# 回到初始位置
for i in range(n):
self.segments[i].goto(STARTING_POSITION[i])
# 🐍头朝前
self.segments[0].setheading(0)
def check_heading(self, target_heading):
"""
这是一个检测🐍头朝向的函数
:param target_heading: 目标方向
:return: 是否和目标方向不一致
"""
return self.segments[0].heading() != target_heading
def up(self):
# 如果🐍头不是朝下,那么允许往上移动,否则会原地掉头,违反贪吃蛇游戏规则,后面函数同理
if self.check_heading(270):
self.segments[0].setheading(90)
def down(self):
if self.check_heading(90):
self.segments[0].setheading(270)
def left(self):
if self.check_heading(0):
self.segments[0].setheading(180)
def right(self):
if self.check_heading(180):
self.segments[0].setheading(0)
def is_hit_body(self):
# 检测是否和自己的身体相撞,坐标由于浮点数运算原因,经常跑出些奇怪的小数,所以要四舍五入
x = round(self.segments[0].pos()[0])
y = round(self.segments[0].pos()[1])
# 坐标重合即判定为相撞
if (x, y) in [(round(seg.pos()[0]), round(seg.pos()[1])) for seg in self.segments[1:]]:
return True
def cal_limits(self, screen):
"""
这是一个根据屏幕计算边界的函数,是为了让整个函数更灵活,不用把一些位置写死
:param screen: 屏幕
:return: 上下左右四个边界
"""
up_limit = screen.window_height() / 2
down_limit = -screen.window_height() / 2
left_limit = -screen.window_width() / 2
right_limit = screen.window_width() / 2
return up_limit, down_limit, left_limit, right_limit
def is_hit_wall(self, limits):
"""
这是一个检测是否撞到边界的函数
:param limits: 边界
:return: 是否出界
"""
x = round(self.segments[0].pos()[0])
y = round(self.segments[0].pos()[1])
# 超出边界即返回True
if x < limits[2] or x > limits[3] or y < limits[1] or y > limits[0]:
return True
def is_dead(self, limits):
"""
根据之前的检测撞身体和出界两个函数检测是否死亡
:param limits: 边界
:return: 是否死亡
"""
return self.is_hit_wall(limits) or self.is_hit_body()
三、食物类
from turtle import Turtle
import random
# 食物类,继承自Turtle类
class Food(Turtle):
def __init__(self):
super().__init__()
# 形状改为圆形
self.shape('circle')
# 提笔
self.penup()
# 颜色改为绿色
self.color('green')
# 加速
self.speed(10000)
# 改小点
self.shapesize(stretch_wid=0.5, stretch_len=0.5)
def refresh_pos(self, screen_limits):
"""
这是一个刷新食物位置的函数
:param screen_limits: 屏幕上下左右边界
:return: 无
"""
l_limit = screen_limits[2] + 50
r_limit = screen_limits[3] - 50
d_limit = screen_limits[1] + 50
u_limit = screen_limits[0] - 50
# 随机生成新位置
pos = random.randint(l_limit, r_limit) // 20 * 20, random.randint(d_limit, u_limit) // 20 * 20
# 去新位置
self.goto(pos)
四、计分板类
import os
from turtle import Turtle
# 对齐方式和字体
ALIGNMENT = 'center'
FONT = 'TimesNewRoman', 20, 'normal'
# 计分板类,继承自Turtle类
class ScoreBoard(Turtle):
def __init__(self, screen):
super().__init__()
# 分数属性,默认-1分(因为后面马上就要+1)
self.score = -1
# 隐藏自身
self.ht()
# 加速,以免显示得太慢
self.speed(1000)
# 提笔,以免出现移动轨迹
self.penup()
# 抵达屏幕中上方
self.goto(0, screen.window_height() / 2 - 50)
# 从本地文件中获取最高分
self.highest_score = self.get_highest_score()
# 刷新分数
self.refresh_score()
def refresh_score(self):
"""
负责刷新屏幕内显示分数的函数
:return: 无
"""
# 首先要清空之前的分数
self.clear()
# 分数+1
self.score += 1
# 落笔
self.pendown()
# 笔颜色改为白色
self.pencolor('white')
# 每次刷新分数时一并刷新最高分,这是为了保证屏幕上的最高分同步刷新
self.refresh_highest_score()
# 写到屏幕上
self.write(arg=f'Score: {self.score} Highest Score: {self.highest_score}', align=ALIGNMENT, font=FONT)
def get_highest_score(self):
"""
这是从本地获取最高分的函数
:return: 获取到的最高分,如果首次运行就是0
"""
# 使用os模块检测本地文件
if not os.path.exists('highest_score.txt'):
# 没有就创建文件并写入0
with open('highest_score.txt', mode='w', encoding='utf-8') as f:
f.write('0')
return 0
else:
# 有就读取分数
with open('highest_score.txt', mode='r', encoding='utf-8') as f:
return eval(f.read())
def refresh_highest_score(self):
"""
这是个刷新计分板最高分属性的函数
:return: 无
"""
# 如果发现当前分数超过最高分了,就把当前分数赋给最高分
if self.score > self.highest_score:
self.highest_score = self.score
def write_highest_score(self, path):
"""
这是一个把最高分写入本地的函数
:return: 无
"""
with open(path, mode='w', encoding='utf-8') as f:
f.write(str(self.highest_score))