前言

该程序未使用pygame库, 而是采用keyboard库的on_press()函数捕获键盘操作, 使用windows控制台缓冲区作为图形界面。
第一次写文章,如有错误请谅解

运行示例

在这里插入图片描述


程序分析

捕获键盘操作

Python从控制台读取可以使用input()函数, 但很明显的是, 贪吃蛇游戏需要在游戏运行的同时不断读取键盘操作, 使用input()输入需要每次都使用回车键, 可行性较差
本程序中使用keyboard库中的on_press()函数捕获键盘操作, 并将捕获的键盘操作传递给一个key_envent()函数进一步处理, 进而控制游戏

输出游戏画面

贪吃蛇游戏需要不断的更新和显示游戏画面, 游戏地图可以使用二维矩阵的形式储存, 显示画面时, 将矩阵中的信息转换成字符串并输出即可


代码分析一

安装运行环境

# 测试python版本为3.9.8
pip install keyboard
pip install win32

游戏地图的实现

创建地图需要的基本信息包括宽度width, 以及高度height
地图的每个格子代表一种元素, 0代表空格, 1代表食物, 2代表炸弹, 3代表蛇的头部, 4代表蛇的身体
对地图的操作包括读取(x, y)位置的元素, 改变(x, y)位置的元素, 以及将列表输出为可以显示的形式

  • 地图的初始化
class Map():
    def __init__(self, width=10, height=10):
        if (width < 10):
            width = 10
        if (height < 10):
            height = 10
        self.size = (width, height)
        self.__map = [[0 for i in range(width)] for i in range(height)]
        # 0为空白块,1为食物,2为炸弹,3为蛇头,4为蛇尾
  • 读取某位置的元素
    def read(self, x, y):
        if (x >= 0 and x < self.size[0] and y >= 0 and y < self.size[1]):
            return self.__map[y][x]
        return -1	# 如果该位置超过地图边界, 返回-1
  • 改变某位置的元素
    def write(self, x, y, val=0):
        self.__map[y][x] = val
  • 将地图以可显示形式输出

输出部分为按行输出(便于后续图形界面的排版), 将每行结果储存在列表中并返回

    def list(self):
        ls = []
        ls.append('# ' * (self.size[0] + 2))	# 地图上边界
        for line in self.__map:
            li = '# '
            for k in line:
                if (k == 0):	# 0表示空白块
                    li += '  '
                elif (k == 1):	# 1表示食物
                    li += "\033[0;32m$\033[0m "
                elif (k == 2):	# 2表示炸弹
                    li += "\033[0;31m@\033[0m "
                elif (k == 3):	# 3表示蛇头
                    li += "\033[0;33m■\033[0m "
                elif (k == 4):# 4代表蛇的身体
                    li += "\033[0;36m■\033[0m "
            li += '#'
            ls.append(li)
        ls.append('# ' * (self.size[0] + 2))	# 地图下边界
        return ls

简单测试打印一下地图

ma = Map(20, 20)
ls = ma.list()
for line in ls:
    print(line)

在这里插入图片描述

炸弹的实现

对于单个炸弹, 其包含的信息有在地图中的位置x y剩余存在时间life
炸弹生成时位置应该保持随机, 并且只能在空白块处生成

class Bomb():
    def __init__(self, map : Map):
        self.x = randint(0, map.size[0] - 1)
        self.y = randint(0, map.size[1] - 1)
        while (map.read(self.x, self.y) != 0):	# 读取地图中该位置是否为空格
            self.x = randint(0, map.size[0] - 1)
            self.y = randint(0, map.size[1] - 1)
        self.life = randint(3, 6)	# 随机的存活时间

游戏地图中显然炸弹有多个, 因此创建一个Bombs类用于处理地图中的全部炸弹
每一帧游戏需要对全部炸弹进行更新, 更新操作包含生成新的炸弹, 重新计算炸弹存在时间, 将炸弹显示在地图上

class Bombs():
    def __init__(self):
        self.list = []	# 储存每一个炸弹的信息

    def update(self, map : Map):
        tmp = self.list.copy()	# 将炸弹的信息拷贝到一个临时列表中
        self.list.clear()
        if (randint(0, 49) == 0):	# 按概率每秒生成一个新的炸弹
            tmp.append(Bomb(map))
        for bomb in tmp:	# 遍历每一个炸弹
            map.write(bomb.x, bomb.y, 0)	# 先将炸弹位置的地图重置
            bomb.life -= 1 / 50	# 计算存在时间
            if (bomb.life > 0):	# 如果存在时间大于零将其加入到炸弹列表中
                self.list.append(bomb)
        del tmp
        for bomb in self.list:	# 将炸弹显示在地图上
            map.write(bomb.x, bomb.y, 2)

食物的实现

食物的实现思路与炸弹的实现基本相同, 但是食物可以被蛇吃掉, 所以食物需要增添一个eat()方法

  • 单个食物的实现 不能说和炸弹很相似, 只能说是一模一样
class Food():
    def __init__(self, map : Map):
        self.x = randint(0, map.size[0] - 1)
        self.y = randint(0, map.size[1] - 1)
        while (map.read(self.x, self.y) != 0):
            self.x = randint(0, map.size[0] - 1)
            self.y = randint(0, map.size[1] - 1)
        self.life = randint(3, 6)
  • 全部食物信息的实现, 相较于炸弹类, 仅多一个eat()方法
class Foods():
    def __init__(self):
        self.list = []

    def update(self, map : Map):
        tmp = self.list.copy()
        self.list.clear()
        if (randint(0, 49) == 0):
            tmp.append(Food(map))
        for food in tmp:
            map.write(food.x, food.y, 0)
            food.life -= 1 / 50
            if (food.life > 0):
                self.list.append(food)
        del tmp
        for food in self.list:
            map.write(food.x, food.y, 1)

    def eat(self, x, y):	# 将坐标处被吃掉的食物的存在时间变为0, 下一次更新时食物会被删除
        for index, food in enumerate(self.list):
            if (food.x == x and food.y == y):
                self.list[index].life = 0

蛇的实现

既然是贪吃蛇, 最重要的自然是蛇
蛇有两个部分组成, 分别是蛇头head和蛇的身体body, 蛇头需要储存的信息为位置[x, y,]方向, 蛇的身体由多节组成, 每一节身体都需要储存其位置[x, y]

  • 蛇的初始化
class Snake():
    def __init__(self, map : Map):
        # [x, y], 创建蛇时需要随机蛇头的位置和方向
        self.__head = [randint(3, map.size[0] - 5), randint(3, map.size[1] - 5)]	# 随机时需要防止太靠近边界导致开局碰墙
        self.__direction = randint(1, 4)
        # [[x, y], [x, y], ....]
        self.__body = []	# 开始游戏时蛇的身体长度为0

蛇的主要操作为移动move(), 在移动时会触发各种场景

  1. 蛇头移向空白处, 即移动后蛇头位置处的地图为空白块, 蛇整体移动一格
  2. 蛇头移向食物处, 即移动后蛇头位置处的地图为食物, 蛇长度增长一格并整体前进一格, 同时触发食物的eat()操作, 吃掉该位置处的食物
  3. 蛇头移向炸弹处, 即移动后蛇头位置处的地图为炸弹, 游戏结束
  4. 蛇头移向墙, 即移动后蛇头位置的位置超过地图边界, 游戏结束
  5. 蛇头移向蛇身体, 即移动后蛇头位置处的地图为蛇身体, 游戏结束

蛇向前移动时, 并不需要改变每一部分身体的位置, 只需在身体的最前方添加一节身体, 位置与原蛇头位置相同, 如果蛇没有变长, 删除最后一节蛇尾即可, 如果蛇变长, 不用删除最后一节蛇尾

  • 蛇身体的移动
    def move(self, map : Map, direction=0):
        self.__body.insert(0, [self.__head[0], self.__head[1]])
        map.write(self.__body[0][0], self.__body[0][1], 4)    # 第一节身体位置移动到原蛇头位置
        map.write(self.__body[-1][0], self.__body[-1][1], 0)    # 删除最后一节蛇尾位置
  • 蛇头根据给定方向移动
        if (direction != 0):	# 为0时表示无方向输入, 按照原来的轨迹移动
            self.__direction = direction
        if (self.__direction == 1):     # 向上
            self.__head[1] -= 1
        elif (self.__direction == 2):   # 向下
            self.__head[1] += 1
        elif (self.__direction == 3):   # 向左
            self.__head[0] -= 1
        elif (self.__direction == 4):   # 向右
            self.__head[0] += 1
  • 读取蛇头移动后位置处地图的情况
        result = map.read(self.__head[0], self.__head[1])   # 移动结果
  • 根据移动情况判断游戏下一步操作
        longer = False	# 是否变长
        move = True	# 是否能够移动
        tip = "just move"	# 提示信息
        if (result == -1):  # 碰墙
            move = False
            tip = "hit the wall"
        elif (result == 1): # 碰到食物
            longer = True
            tip = "eat food"
        elif (result == 2): # 碰到炸弹
            move = False
            tip = "hit the bomb"
        elif (result == 4): # 碰到蛇尾
            move = False
            tip = "eat your body"
        else:
            pass
  • 根据移动情况判断蛇尾是否变化, 以及返回移动信息(提示词, (移动后蛇头的坐标x, y))
        if (move):	# 是否能够移动
            if (not longer):	# 是否变长
                self.__body.pop()
            else:
                map.write(self.__body[-1][0], self.__body[-1][1], 4)
            map.write(self.__head[0], self.__head[1], 3)
        return (tip, (self.__head[0], self.__head[1]))

初步测试

此时游戏所需的地图, 食物, 炸弹等已经全部实现, 可通过简单代码进行初步测试

game_map = Map(20, 20)	# 初始化地图
foods = Foods()	# 初始化食物
bombs = Bombs()	# 初始化炸弹
snake = Snake(game_map)	# 初始化蛇
tick = 0	# 游戏刻, 用于控制蛇的移动速度
while True:
    move = ("just move", (0, 0))	# 用来记录蛇move之后的信息
    if (tick == 0):	# 0刻时蛇移动一次
        move = snake.move(game_map, randint(1, 4))
    if (move[0] == "eat food"):	# 吃到食物执行eat()操作
        foods.eat(move[1][0], move[1][1])
    elif (move[0] != "just move"):	# 触发游戏结束条件
        break
    foods.update(game_map)	# 更新食物
    bombs.update(game_map)	# 更新炸弹
    ls = game_map.list()	# 地图可视化
    for line in ls:
        print(line)
    tick = (tick + 1) % 5	# 游戏刻加一
    time.sleep(0.02)	# 控制游戏帧率
    os.system("cls")	# 清屏
  • 运行效果
    运行效果

基本上已经正常了, 再加上键盘操作即可控制蛇的移动
但有一个明显的问题, print()+clear操作闪瞎玩家的眼睛会导致屏幕严重闪烁, 产生该问题的原因是清除控制台再重新输出不是瞬间完成, 为解决该问题需要使用双缓冲DoubleBuffer, 当前缓冲区显示, 下一个缓冲区更新完成后直接替换该缓冲区的内容, 即可解决屏幕更新不及时造成的闪烁问题
双缓冲的实现参考Python控制台双缓冲Double Buffer
本文章直接调用Buffers()类, 不再进行额外介绍

键盘控制的实现

到现在为止, 虽然蛇已经可以移动, 吃食物, 游戏判断等等, 但是蛇的移动是不受玩家控制的
控制蛇的移动需要不断读取键盘操作, 并将键盘操作处理后传递给Snake.move()
keyboard中的keyboard.on_press(call)可以绑定一个函数call(x), 每次有按键按下时将会执行call(x), 参数x为键盘事件, 读取x.name即可获得按下按键的名称

  • key_event()函数
def key_envent(key):
    global direction	# 全局变量direction, Snake.move()的方向参数
    global gaming	# 全局变量gaming, 记录游戏是否正在运行, 以及结束游戏
    global pause	# 全局变量pause, 用于游戏的暂停操作
    if (key.name == "up"):	# 按上方向键
        direction = 1
    elif (key.name == "down"):	# 按下方向键
        direction = 2
    elif (key.name == "left"):	# 按左方向键
        direction = 3
    elif (key.name == "right"):	# 按右方向键
        direction = 4
    elif (key.name == "space"):	# 按空格键, 暂停/继续
        pause = not pause
    elif (key.name == "esc" and gaming):	# 按ESC键退出游戏
        gaming = False
  • keyboard.on_press() 绑定

keyboard.on_press() 绑定key_event()函数后, 每一次按下键盘按键都会执行key_event()函数, 直到程序的主进程退出

keyboard.on_press(key_envent)

主程序

上文中已经实现了游戏的基本流程和键盘操作, 实现游戏的主程序之后即可正常游玩
部分内容本文未作详细解释, 请参考源码使用

game函数

将所有的游戏内流程, 如创建各种对象, 各种对象的更新封装在game()函数中, 方便多次重复游戏
该部分代码为测试代码的扩充
加入了多缓冲区, 游戏暂停, 固定时间刷新画面内容等

def game():
    global direction	# 方向
    global gaming		# 游戏是否在进行
    global pause		# 是否暂停
    buffers = Buffers()	# 创建一个双缓冲区用于显示游戏画面
    game_map = Map(20, 20)	# 指定大小创建游戏地图
    bombs = Bombs()
    foods = Foods()
    snake = Snake(game_map)
    tick = 0
    direction = 0
    score = 0	# 记录游戏得分
    tip = ""	# 记录游戏退出时的提示次
    gaming = True
    pause = False
    start_time = time.time()
    while gaming:	# 如果游戏结束退出循环
        if (pause):	# 游戏暂停, 休眠一秒后再判断pause的状态, 降低计算消耗
            start_time += 1	# 休眠时时间不流动
            time.sleep(1)
            continue
        loop_time = time.perf_counter()    # 记录循环开始时间

        move = ("just move", (0, 0))
        if (tick == 0):
            move = snake.move(game_map, direction)
        if (move[0] == "eat food"):
            foods.eat(move[1][0], move[1][1])
            score += 1
        elif (move[0] != "just move"):
            tip = move[0]
            gaming = False
            break
        foods.update(game_map)
        bombs.update(game_map)

        buffers.switch()	# 切换画面缓冲区
        map_ls = show_info(game_map.list(), score, int(time.time() - start_time)) # 在游戏地图后添加游戏时间, 游戏得分, 排版游戏画面
        for line in map_ls:	# 将游戏画面输出到下一个缓冲区
            buffers.print(line+'\n')
        buffers.print("ESC键退出游戏  空格键暂停\\继续")
        buffers.flash()	# 刷新游戏画面

        tick = (tick + 1) % 5
        time.sleep(0.02 - (loop_time - time.perf_counter()))	# 按照固定时间(0.02s)运行游戏程序, 即指定游戏帧数
    end(tip, score, map_ls)	# 执行结束函数显示提示信息

图形界面显示分数, 得分

  • 向该函数输入转换后的地图列表, 游戏时间, 分数信息, 返回一个新的地图列表, 列表中包含游戏的时间 T 和游戏分数 S
def show_info(map_ls, score, game_time):
	pass
	return map_ls

结束函数

游戏结束后打印地图并显示提示语

def end(tip, score, map_ls):
    os.system("cls")
    for line in map_ls:
        print(line)
    if (tip == "hit the wall"):
        print("\033[0;31m您撞墙后不治身亡!\033[0m")
    elif (tip == "hit the bomb"):
        print("\033[0;31m炸弹真美味, 可惜会爆炸\033[0m")
    elif (tip == "eat your body"):
        print("\033[0;31m您真狠, 饿了连自己都不放过\033[0m")
    elif (tip == ""):
        print("\033[0;31m请问你为什么要退出游戏呢?\033[0m")
    print("\033[0;33m游戏结束\033[0m")
    print("\033[0;34m您的得分为: \033[0;32m{}\033[0m".format(score))
    print("\033[0;33m输入任意内容退出游戏  \033[0;32m输入\033[0;34m空格\033[0;32m重新开始游戏\033[0m")

游戏主函数

在主函数中绑定键盘操作, 判断是否继续下一次游戏等

def main():
    keyboard.on_press(key_envent)	# 绑定键盘操作
    while True:	# 实现游戏的多次
        game()	# 执行游戏函数
        if (input("\n") != " "):	# 根据输入内容判断是否进行下一次游戏
            break
main()	# 运行主函数

源码下载

希望本文对您有所帮助, 感谢您花时间浏览本文

Logo

华为云1024程序员节送福利,参与活动赢单人4000元礼包,更有热门技术干货免费学习

更多推荐