Python贪吃蛇游戏

写在开始

萌新的第一份博客,希望能通过这样的形式记录下自己的成长吧。废话不多说,下面进入正题。

项目描述

贪吃蛇游戏:
1.一款终端控制游戏,通过四个方向键wsad控制蛇的移动,蛇头和食物重叠后,发生吞食,蛇的长度变长,在其他位置生成新的食物。
2.蛇头和边框,或身体的任意部分碰触,游戏结束
3.随着分数的提高,蛇的移动速度逐渐加快

框架

在着手实现代码之前先把框架理清楚
在这里插入图片描述

代码实现

构造地图

要写一个贪吃蛇出来,其实实现的功能说少也不算少,总之先从简单的来,一步一步实现吧,先把小蛇的舞台给它搭好。

# 假设游戏区域是24x24的方块矩阵,则每个方块(像素)拥有一个坐标
# 先建一个坐标类,用这个类创建的对象就是一个像素了
class Point:
    def __init__(self, x=0, y=0):
        self.__x = x  # 行
        self.__y = y  # 列
    
    # property属性
    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, value):
        self.__x = value

    @property
    def y(self):
        return self.__y

    @y.setter
    def y(self, value):
        self.__y = value

这里我用了property属性,主要是为了练习一下。x,y直接设为公有属性这里应该也无伤大雅。

有了像素接下来就是用位置信息一点点把围墙造出来了,这里建的就是一个普通的正方形围墙。

# 创建围墙
class Wall:
    def __init__(self):
    	# 创建一个空列表用来存放像素对象
        self.__position = []
        size = 24 	# 设置size为24   即24*24的分辨率
        # 利用for循环将围墙的像素对象存进列表
        for i in range(0, size):
            # 第一列和最后一列
            self.__position.append(Point(0, i)) 	# Point即之前创建的坐标类(像素点)
            self.__position.append(Point(size-1, i))
            # 第一行和最后一行
            if i != 0 and i != size-1: 	# 防止四个角的冲突(其实不加影响也不大)
                self.__position.append(Point(i, 0))
                self.__position.append(Point(i, size-1))
	
	# 设置一个返回围墙位置信息的方法
    def get_position(self):
        return self.__position

有了围墙的信息,接下来就是让它显示在屏幕上了。再创建一个显示模块。

class Display:
    @staticmethod
    def display(wall_list): 		# 传入的参数就是墙的像素点的位置信息(列表)
        size = 24					# 分辨率24*24
        s = ""
        for x in range(0, size):
            for y in range(0, size):
                for i in wall_list:
                    if x == i.x and y == i.y:   # 判断当前坐标的点是否在列表里(是否被占用)
                        temp = True
                        break
                    else:
                        temp = False
                if temp:                
                    s += " o"			# 存在则这个点的内容就是“ o”
                else:
                    s += "  "           # 否则就是“  ”
            s += "\n"		
        print(s)

试试效果

wall = Wall()
Display.display(wall.get_position())

在这里插入图片描述
需要补充的一点就是,最后display模块每次打印之前需要先清一次屏,这里我没有写上以免混淆。最后的完整代码会加上

创建小蛇

跟造围墙同样的思路,总之先不考虑小蛇的功能先把它的身体造出来

class Snake:
    def __init__(self):
        self.__position = []							# 存储位置信息
        self.__position.append(Point(1, 2))				# 两个初始位置
        self.__position.append(Point(1, 1))
        self.speed = 1.6								# 移动速度
        self.__toward = "d"								# 移动方向  开始时默认向右走
    @property
    def toward(self):								# 跟之前一样,把移动方向设定为property属性
        return self.__toward
    
    @toward.setter
    def toward(self, t):						# "t"为传入的字符("w","s","a","d")
        s = self.__toward + t					# 这里这样设置是为了防止出现能直接掉头的尴尬场面
        if not (s == "ws"or s == "sw"or s == "ad"or s == "da"):
            self.__toward = t

    # 移动
    def step(self):
        # 只有头需要按照方向移动,其它“骨节”只需获取前一个骨节的坐标就可以了
        # 获取顺序为从尾到头
        for i in range(len(self.__position)-1, 0, -1):
            self.__position[i] = self.__position[i-1]	
        go = {							# wsad分别对应上下左右
            "w": Point(0, -1),
            "s": Point(0, +1),
            "a": Point(-1, 0),
            "d": Point(+1, 0)
        }
        # 蛇头的移动
        # 因为不能直接对x, y 两个属性进行修改(这里牵扯到内存的问题,如果直接修改
        # 两个属性值的话self.__position[1]的x, y属性值同样会被修改,因为此时两个对象
        # 指向的是同一块内存地址)所以这里实际应该是重新生成一个新的对象来替换
        self.__position[0] = Point(self.__position[0].x+go[self.__toward].x, self.__position[0].y+go[self.__toward].y)

    def get_position(self):
        return self.__position				# 返回位置信息,之后同样传给display用于打印

这样小蛇就有了,不过只有移动的功能,之后再回来完善

创建食物

from random import Random


class Food:
    def __init__(self):
        self.__position = None

    # 生成食物
    def chose_new_position(self, used_position):
        size = 24
        random = Random()
        while True:
            x = random.randint(1, size - 2)
            y = random.randint(1, size - 2)
            temp = True
            for i in used_position:
                if x == i.x and y == i.y: 		# 判断当前坐标的点是否在列表里
                    temp = True    
                    break
            if not temp: 						# 当前位置未被占用则退出while循环
                break
        self.__position = Point(x, y)

    def get_position(self):
        return [self.__position]			# 返回位置信息

这里再一次判断了当前位置是否被占用,可以把这个函数封装一下来减少代码量。

class Tools:
    @staticmethod
    def in_list(point_tuple, uesd_point_list):
        for i in uesd_point_list:
            if i.x == point_tuple[0] and i.y == point_tuple[1]:
                return True
        return False

完善蛇的功能

添加上吞食跟判定死亡的功能

class Snake:
    def __init__(self):
        self.__position = []
        self.__position.append(Point(1, 2))
        self.__position.append(Point(1, 1))
        self.speed = 1.6
        self.__toward = "d"

    @property
    def toward(self):
        return self.__toward

    @toward.setter
    def toward(self, t):
        s = self.__toward + t
        if not (s == "ws"or s == "sw"or s == "ad"or s == "da"):
            self.__toward = t

    def step(self, food):
        for i in range(len(self.__position)-1, 0, -1):
           self.__position[i] = self.__position[i-1]	
        go = {							# wsad分别对应上下左右
            "w": Point(0, -1),
            "s": Point(0, +1),
            "a": Point(-1, 0),
            "d": Point(+1, 0)
        }
        self.__position[0] = Point(self.__position[0].x+go[self.__toward].x, self.__position[0].y+go[self.__toward].y)
        food_position = food.get_position()[0]
        # 食物被吃
        if self.__position[0].x == food_position.x and self.__position[0].y == food_position.y:
            # 蛇身长度+1
            last_point = self.__position[-1]
            self.__position.append(Point(last_point.x, last_point.y))
            # 生成新食物
            bug.chose_new_position(self.get_position())
            # 速度变快
            self.speed += 0.1

    # 判断死亡
    def check_dead(self):
        head = self.__position[0]
        size = BaseInfo.get_size()
        # 触墙
        if head.x < 1 or head.x > size-2 or head.y < 1 or head.y > size-2:
            return True
        # 触身   这里就用到了刚刚封装的函数
        if Tools.in_list((head.x, head.y), self.__position[1:]):
            return True
        return False

    def get_position(self):
        return self.__position

控制模块

各个对象建立完成之后,接下来就是对小蛇的控制了。
当然,用键盘上的"wsad"控制方向那肯定不能用input一次又一次的输入"wsad"来控制,这里我调用了一个"msvcrt"模块来检测输入实现控制,同时为了不出现赋值阻塞还需要开一个子线程。

import msvcrt
import threading

class ListenInput(threading.Thread):
    def __init__(self, s):
        threading.Thread.__init__(self)
        self.__snake = s
        self.keeprunning = True

    def run(self):
        # run中的代码在子线程中执行
        while self.keeprunning:
            c = str(msvcrt.getch())[2]
            if c == "q":							# 检测到输入q则结束进程
                self.keeprunning = False
            if c in "wsad":
                self.__snake.toward = c

主函数main()

终于到了最后的主函数了…

def main():
    wall = Wall()
    snake = Snake()
    food = Food()
    food.chose_new_position(snake.get_position())
    listen = ListenInput(snake)
    listen.start()      # 启动监听输入线程
    while listen.keeprunning:
        snake.step(food)        # 没病走两/2步
        Display.display(wall.get_position()
                        + food.get_position()
                        + snake.get_position())
        if snake.check_dead():
            print("Game Over")
            listen.keeprunning = False
        time.sleep(1/snake.speed)


if __name__ == "__main__":
    main()

结果试玩的时候出现了这种原地爆炸的离奇死亡…
在这里插入图片描述
当时我想回头吃掉这个食物,连着按了"s,d",结果就提示我Game Over了,分析一下原因,其实原因很简单,我设定的屏幕刷新时间(蛇移动一格的时间)是"1/speed"秒,我按下那两个键的时间低于刷新时间,所以这里蛇响应的移动是最后我所按下的"d",蛇头没有响应"s",就相当于直接碰到了自己的蛇身。
为了避免这种状况,需要设置一个响应按键所需的时间。在这里导入datetime模块,加入一段用来检测时间的代码。

    def run(self):
        last_time = datetime.datetime.now()
        # run中的代码在子线程中执行
        while self.keeprunning:
            c = str(msvcrt.getch())[2]
            if c == "q":
                self.keeprunning = False
            if c in "wsad":
                # 设置接受按键灵敏度(按键速度超过响应速度时会出现掉头的状况)
                cooling_time = 1 / self.__snake.speed
                if (datetime.datetime.now() - last_time).total_seconds() < cooling_time:
                    continue
                self.__snake.toward = c
                last_time = datetime.datetime.now()

这样如果按键速度过快就不会响应之后的按键了。

完整代码

没有拆分成多个模块,想拆的话也不过是剪切一下的事了。

from random import Random
import time
import datetime
import msvcrt
import threading
import os


class Point:
    def __init__(self, x, y):
        self.__x = x  # 行
        self.__y = y  # 列

    # property属性
    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, value):
        self.__x = value

    @property
    def y(self):
        return self.__y

    @y.setter
    def y(self, value):
        self.__y = value


class Tools:
    @staticmethod
    def in_list(point_tuple, uesd_point_list):
        for i in uesd_point_list:
            if i.x == point_tuple[0] and i.y == point_tuple[1]:
                return True
        return False


class Wall:
    def __init__(self):
        # 创建一个空列表用来存放像素对象
        self.__position = []
        size = 24  # 设置size为24   即24*24的分辨率
        # 利用for循环将围墙的像素对象存进列表
        for i in range(0, size):
            # 第一列和最后一列
            self.__position.append(Point(0, i))  # Point即之前创建的坐标类(像素点)
            self.__position.append(Point(size - 1, i))
            if i != 0 and i != size-1:  # 防止四个角的冲突
                # 第一行和最后一行
                self.__position.append(Point(i, 0))
                self.__position.append(Point(i, size - 1))

    # 设置一个返回围墙信息的方法
    def get_position(self):
        return self.__position


class Display:
    @staticmethod
    def display(used_position): 	# 传入的参数就是像素点的位置信息(列表)
        size = 24					# 分辨率24*24
        s = ""
        for y in range(0, size):
            for x in range(0, size):
                if Tools.in_list((x, y), used_position):
                    s += " o"           # 存在则这个点的内容就是“ o”
                else:
                    s += "  "           # 否则就是“  ”
            s += "\n"
        os.system("cls")                # 清屏
        print(s)


class Snake:
    def __init__(self):
        self.__position = []  # 存储位置信息
        self.__position.append(Point(2, 1))  # 两个初始位置
        self.__position.append(Point(1, 1))
        self.speed = 1.6  # 移动速度
        self.__toward = "d"  # 移动方向  开始时默认向右走

    @property
    def toward(self):  # 跟之前一样,把移动方向设定为property属性
        return self.__toward

    @toward.setter
    def toward(self, t):  # "t"为传入的字符("w","s","a","d")
        s = self.__toward + t  # 这里这样设置是为了防止出现能直接掉头的尴尬场面
        if not (s == "ws" or s == "sw" or s == "ad" or s == "da"):
            self.__toward = t

    # 移动
    def step(self, food):
        # 只有头需要按照方向移动,其它“骨节”只需获取前一个骨节的坐标就可以了
        for i in range(len(self.__position) - 1, 0, -1):
            self.__position[i] = self.__position[i - 1]
        go = {  # wsad分别对应上下左右
            "w": Point(0, -1),
            "s": Point(0, +1),
            "a": Point(-1, 0),
            "d": Point(+1, 0)
        }
        # 不能直接对x, y进行赋值,这里需要重新返回一个对象。
        self.__position[0] = Point(self.__position[0].x+go[self.__toward].x, self.__position[0].y+go[self.__toward].y)
        food_position = food.get_position()[0]
        if self.__position[0].x == food_position.x and self.__position[0].y == food_position.y:
            # 食物被吃,蛇身长度+1
            last_point = self.__position[-1]
            self.__position.append(Point(last_point.x, last_point.y))
            # 生成新食物
            food.chose_new_position(self.get_position())
            # 速度变快
            self.speed += 0.1

    # 判断死亡
    def check_dead(self):
        head = self.__position[0]
        size = 24
        # 触墙
        if head.x < 1 or head.x > size - 2 or head.y < 1 or head.y > size - 2:
            return True
        # 触身
        if Tools.in_list((head.x, head.y), self.__position[1:]):
            return True
        return False

    def get_position(self):
        return self.__position  # 返回位置信息,之后同样传给display用于打印


class Food:
    def __init__(self):
        self.__position = None

    def chose_new_position(self, used_position):
        size = 24
        random = Random()
        while True:
            x = random.randint(1, size - 2)
            y = random.randint(1, size - 2)
            if not Tools.in_list((x, y), used_position):
                break
        self.__position = [Point(x, y)]

    def get_position(self):
        return self.__position


class ListenInput(threading.Thread):
    def __init__(self, s):
        threading.Thread.__init__(self)
        self.__snake = s
        self.keeprunning = True

    def run(self):
        last_time = datetime.datetime.now()
        # run中的代码在子线程中执行
        while self.keeprunning:
            c = str(msvcrt.getch())[2]
            if c == "q":
                self.keeprunning = False
            if c in "wsad":
                # 设置接受按键灵敏度(按键速度超过响应速度时会出现掉头的状况)
                cooling_time = 1 / self.__snake.speed
                if (datetime.datetime.now() - last_time).total_seconds() < cooling_time:
                    continue
                self.__snake.toward = c
                last_time = datetime.datetime.now()


def main():
    wall = Wall()
    snake = Snake()
    food = Food()
    food.chose_new_position(snake.get_position())
    listen = ListenInput(snake)
    listen.start()      # 启动监听输入线程
    while listen.keeprunning:
        snake.step(food)        # 没病走两/2步
        Display.display(wall.get_position()
                        + food.get_position()
                        + snake.get_position())
        if snake.check_dead():
            print("Game Over")
            listen.keeprunning = False
        time.sleep(1/snake.speed)


if __name__ == "__main__":
    main()

.(๑>؂<๑)۶

敲完真累啊…不说了我要去给我保温杯里换枸杞了~~

  • 17
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值