写在开始
萌新的第一份博客,希望能通过这样的形式记录下自己的成长吧。废话不多说,下面进入正题。
项目描述
贪吃蛇游戏:
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()
.(๑><๑)۶
敲完真累啊…不说了我要去给我保温杯里换枸杞了~~