在这篇文章中,你将学会如何用40行Python代码,来实现一个简单的《贪食蛇》游戏。
我们在上期的文章中介绍了Python3的curses包的用法,并实现了一个控制台时钟。如果你对curses的用法还不熟,请先阅读这一期的文章。酸痛鱼:Python3实践之:控制台时钟zhuanlan.zhihu.com
本文中用到的所有知识点,你可以在如下两篇文章中学习到。一个是python官方的教材,一个是这个教材的中文翻译,请任选一个阅读。
官网文章:https://docs.python.org/3/howto/curses.htmldocs.python.org
中文翻译:CSDN-专业IT技术社区-登录blog.csdn.net
0、问题描述
本期文章将继续深入介绍curses,并实现两个版本的控制台《贪食蛇》游戏。
第一个版本我们将直接运用我们上期学到的知识,实现一个文字(字符)版本的《贪食蛇》游戏。在文字版本中,我们将深入介绍《贪食蛇》游戏的实现细节,这些细节在第二个版本中,将不再重复介绍。
第二个版本,我们引入了色彩系统,我们将使用色块来代替字符,使得游戏更加美观和完善。
1、文字版本《贪食蛇》
这个版本的实现中,游戏窗口的范围是整个控制台。这样做是为了减少没必要的边界和位移处理,以便将重心放在游戏的核心逻辑的实现上。
由于游戏功能比较简单,代码量比较少(除去空行和注释,其实只有不到40行的代码),所以我将不会拆分讲解游戏的实现,所有的实现细节将以代码+注释的形式来呈现。如果你对代码有不能理解之处,请给我留言。
在贴代码之前,你可能需要了解curses的以下几个知识点:
# 关闭光标闪烁效果
curses.curs_set(0)
# 开启光标闪烁效果(默认为开启)
curses.curs_set(1)
# 键盘无响应超时
# 即调用 getch、getkey等方法是,最长的等待时间
# 在指定的时间内,如200ms,如果用户输入,则返回ERR或者抛出异常
stdscr.timeout(200)
# 等待用户的输入,返回一个整数,curses为每个键的字声明了一个常量
# 如果用户敲击回车键,key的值为10
# 左、右、下、上 四个箭头键的常量分别为:
# curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_DOWN, curses.KEY_UP
key = stdscr.getch()
游戏效果如下图。我建议你在理解的基础上自己实现一下,并运行体验一下。如果你不太理解游戏的玩法,可能拷贝本文的代码,运行并体验游戏,以便加深对代码的理解。文版本游戏效果
代码来啦:
import curses
from random import randint
def game(stdscr):
curses.curs_set(0) # 关闭光标闪烁效果
h = curses.LINES
w = curses.COLS
stdscr.timeout(200) # 键盘无响应超时
# 蛇身的初始长度为4, snake[0]为蛇头
snake = [(h//2, x) for x in range(w//2 - 2, w//2 + 2)]
# 移动方向:下,上,左,右
dirs = [(1, 0), (-1, 0), (0, -1), (0, 1)]
cur_dir = 2 # 初始移动方向为“左”
food = None # 食物位置
# 展示初始的蛇身
for node in snake:
stdscr.addch(node[0], node[1], "+")
# 游戏循环
while True:
key = stdscr.getch()
if key == 10: # 监听到回车键,退出游戏
break
# 监听到 左、右、下、上 键,改变蛇的移动方向
if key in (curses.KEY_LEFT, curses.KEY_RIGHT, \
curses.KEY_DOWN, curses.KEY_UP):
cur_dir = key - curses.KEY_DOWN
# 如果食物被吃,重新生成食物,并展示
if not food:
food = (randint(0, h - 1), randint(0, w - 1))
while food in snake: # 食物不可生成蛇向上
food = (randint(0, h - 1), randint(0, w - 1))
stdscr.addch(food[0], food[1], "*", curses.A_BLINK)
# 新蛇头:new_head = snake[0] + dirs[cur_dir]
new_head = (snake[0][0] + dirs[cur_dir][0],
snake[0][1] + dirs[cur_dir][1])
# 蛇头超出屏蔽或者撞到自己,则判失败
if new_head[0] < 0 or new_head[0] >= h \
or new_head[1] < 0 or new_head[1] >= w \
or new_head in snake:
break
# 把新蛇头放入蛇链中, 并显示
snake.insert(0, new_head)
stdscr.addch(new_head[0], new_head[1], "+")
if new_head == food:
# 吃到食物,则蛇尾不移动,相当于吃到食物放到食尾
food = None
else:
# 未吃到食物,则将蛇尾弹出,并从屏幕中擦除
tail = snake.pop()
stdscr.addch(tail[0], tail[1], " ")
# 启动游戏
curses.wrapper(game)
2、色块版本《贪食蛇》
这个版本将以40x40的格子阵列作为游戏区。由于字符的高度是宽度的2倍,为了呈现正方形的色块,我们需要用两个字符来表示一个格子。加上游戏的边框,控制台至少要有84列和至少43行。如果你的控制台窗口太小,程序启动后会得到一个窗口太小的提示,游戏将无法进行。
每个字符都有前景颜色(即文字的颜色)和背景颜色,如果我们将前景颜色和背景颜色设置成一样的,那么我们将无法看到字符,而是看到一个纯色块,我们就利用这个原理来呈现一个色块的。字符前景和背景的设置如下代码所示:
# 初始化颜色1,前景为黑色,背景为白色
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
# 初始化颜色2,前景为白色,背景为黑色
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK)
# 在屏幕中展示字符串,字黑底白(使用颜色1)
stdscr.addstr(0, 0, "Hello World!", curses.color_pair(1))
# 在屏幕中展示字符串,字白底黑(使用颜色2)
stdscr.addstr(0, 0, "Hola Mondo!", curses.color_pair(2))
游戏效果如图:色块版游戏效果
代码(没有注释)
import curses
from random import randint
class Snake:
def __init__(self):
self.score = 0
self.width = 80
self.height = 40
self.dirs = [(1, 0), (-1, 0) , (0, -2), (0, 2)]
self.dir = 3
self.snake = [(20, 44), (20, 42), (20, 40), (20, 38)]
self.food = None
def __call__(self, stdscr):
curses.curs_set(0)
self.stdscr = stdscr
self.win_h = curses.LINES
self.win_w = curses.COLS
self.start_x = (self.win_w - self.width) // 2
self.start_y = (self.win_h - self.height) // 2
self.score_y = self.start_y - 1
if self.win_w < 84 or self.win_h < 43:
self.stdscr.addstr(1, 0, "Screen size is less than 42x43. Please resize it")
self.stdscr.addstr(2, 0, "Press any key to exit")
self.stdscr.getch()
return
self.startUp()
self.runForever()
self.closeDown()
def draw(self, pos, color):
self.stdscr.addstr(self.start_y + pos[0], self.start_x + pos[1], self.node, color)
def startUp(self):
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_BLACK)
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_WHITE)
curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_CYAN)
curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_GREEN)
curses.init_pair(5, curses.COLOR_RED, curses.COLOR_WHITE)
self.sn_color = curses.color_pair(1)
self.bg_color = curses.color_pair(2)
self.fr_color = curses.color_pair(3)
self.fd_color = curses.color_pair(4)
self.failed_color = curses.color_pair(5)
self.node = " "
self.stdscr.addstr(self.score_y, self.start_x, "Score:0")
for y in range(self.height + 2):
for x in range(0, self.width + 4, 2):
if x in (0, self.width + 2) or y in (0, self.height + 1):
color = self.fr_color
else:
color = self.bg_color
self.draw((y, x), color)
for n in self.snake:
self.draw(n, self.sn_color)
self.stdscr.timeout(200)
def runForever(self):
while True:
key = self.stdscr.getch()
if key == 10:
break
if key in (curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_DOWN, curses.KEY_UP):
self.dir = key - curses.KEY_DOWN
ok = self.gameLoop()
if not ok:
self.onFailed()
break
def closeDown(self):
self.stdscr.nodelay(False)
def onFailed(self):
self.stdscr.addstr(self.win_h//2 - 1, self.win_w//2 - 2, " Failed! ", self.failed_color)
self.stdscr.addstr(self.win_h//2, self.win_w//2 - 2, "Press Any Key To Exit", self.failed_color)
self.stdscr.timeout(5000)
self.stdscr.getch()
def gameLoop(self):
if not self.food:
self.food = randint(1, 40), 2 * randint(1, 40)
while self.food in self.snake:
self.food = randint(1, 40), 2 * randint(1, 40)
self.draw(self.food, self.fd_color)
new_head = self.snake[0][0] + self.dirs[self.dir][0], self.snake[0][1] + self.dirs[self.dir][1]
if new_head in self.snake or new_head[0] in (0, 41) or new_head[1] in (0, 82):
return False
if new_head == self.food:
self.food = None
else:
tail = self.snake.pop()
self.draw(tail, self.bg_color)
self.snake.insert(0, new_head)
self.draw(new_head, self.sn_color)
self.stdscr.addstr(self.score_y, self.start_x, "Score:{}".format(len(self.snake)))
return True
if __name__ == "__main__":
snake = Snake()
curses.wrapper(snake)