目錄
- 一、前言
- 二、初識 curses
- 2.1 簡單使用
- 2.2 整點花樣
- 2.2.1 新建一個子窗口
- 2.2.2 上點顏色
- 2.2.3 給點細節
- 三、貪吃蛇
- 3.1 設計
- 3.2 蛇語者
- 3.3 命令行?畫板!
- 3.4 控制!
- 3.5 直接使用
- 四、結尾
一、前言
本期介紹 Python 練手級項目——貪吃蛇!
原本想推薦一個貪吃蛇的開源項目:python-console-snake
,但由于該項目最近一次更新是 8 年前,而且在運行的時候出現了諸多問題。索性我就動手用 Python 重新寫了一個貪吃蛇游戲。
下面我們就一起用 Python 實現一個簡單有趣的命令行貪吃蛇小游戲,啟動命令:
git clone https://github.com/AnthonySun256/easy_games
cd easy_games
python snake

本文包含設計和講解,整體分為兩個部分:第一部分是關于 Python 命令行圖形化庫 curses 接著是 snake 相關代碼。
二、初識 curses
Python 已經內置了 curses 庫,但是對于 Windows 操作系統我們需要安裝一個補丁以進行適配。
Windows 下安裝補全包:
pip install windows-curses
curses 是一個應用廣泛的圖形函數庫,可以在終端內繪制簡單的用戶界面。
在這里我們只進行簡單的介紹,只學習貪吃蛇需要的功能
如果您已經接觸過 curses,請跳過此部分內容。
2.1 簡單使用
Python 內置了 curses 庫,其使用方法非常簡單,以下腳本可以顯示出當前按鍵對應編號:
# 導入必須的庫
import curses
import time
# 初始化命令行界面,返回的 stdscr 為窗口對象,表示命令行界面
stdscr = curses.initscr()
# 使用 noecho 方法關閉命令行回顯
curses.noecho()
# 使用 nodelay(True) 方法讓 getch 為非阻塞等待(即使沒有輸入程序也能繼續執行)
stdscr.nodelay(True)
while True:
# 清除 stdscr 窗口的內容(清除殘留的符號)
stdscr.erase()
# 獲取用戶輸入并放回對應按鍵的編號
# 非阻塞等待模式下沒有輸入則返回 -1
key = stdscr.getch()
# 在 stdscr 的第一行第三列顯示文字
stdscr.addstr(1, 3, "Hello GitHub.")
# 在 stdscr 的第二行第三列顯示文字
stdscr.addstr(2, 3, "Key: %d" % key)
# 刷新窗口,讓剛才的 addstr 生效
stdscr.refresh()
# 等待 0.1s 給用戶足夠反應時間查看文字
time.sleep(0.1)
也可以嘗試把 nodelay(True) 改為 nodelay(False) 后再次運行,這時候程序會阻塞在 stdscr.getch() 只有當您按下按鍵后才會繼續執行。
2.2 整點花樣
您也許會覺得上面的例子太菜了,隨便用幾個 print 都能達到相同的效果,現在我們來整點花樣以實現一些使用普通輸出無法達到的效果。
2.2.1 新建一個子窗口
說再多的話也不如一張圖來的實際

如果我們想要實現圖中 Game over! 窗口,可以使用 newwin 方法:
import curses
import time
stdscr = curses.initscr()
curses.noecho()
stdscr.addstr(1, 2, "HelloGitHub")
# 新建窗口,高為 5 寬為 25,在命令行窗口的 四行六列處
new_win = curses.newwin(5, 25, 4, 6)
# 使用阻塞等待模式
new_win.nodelay(False)
# 在新窗口的 2 行 3 列處添加文字
new_win.addstr(2, 3, "www.HelloGitHub.com")
# 給新窗口添加邊框,其中邊框符號可以這是,這里使用默認字符
new_win.border()
# 刷新窗口
stdscr.refresh()
# 等待字符輸入(這里會一直等待輸入)
new_win.getch()
# 刪除新窗口對象
del new_win
# 清除所有內容(比 erase 更徹底)
stdscr.clear()
# 重新添加文字
stdscr.addstr(1, 2, "HelloGitHub")
# 刷新窗口
stdscr.refresh()
# 等待兩秒鐘
time.sleep(2)
# 結束 curses 模式,恢復到正常命令行模式
curses.endwin()
除了curses.newwin新建一個獨立的窗口,我們還能在任意窗口上使用 subwin或者 subpad方法新建子窗口,例如 stdscr.subwin、 stdscr.subpad、new_win.subwin、new_win.subpad 等等,其使用方法與本節中創建的 new_win 或者 stdscr沒有區別,只是新建窗口使用獨立的緩存區,而子窗口和父窗口共享緩存區。
如果某個窗口會在使用后刪除,最好使用 newwin 方法新建獨立窗口,以防止刪除子窗口造成父窗口的緩存內容出現問題。
2.2.2 上點顏色
白與黑的搭配看久了也會顯得單調,curses
提供了內置顏色可以讓我們自定義前后背景。
在使用彩色模式之前我們需要先使用使用 curses.start_corlor()
進行初始化操作:
import curses
import time
stdscr = curses.initscr()
stdscr.nodelay(False)
curses.noecho()
# 初始化彩色模式
curses.start_color()
# 在1號位置添加前景色是綠色,背景色是黑色的彩色對兒
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
# 在一行一列處顯示文字,使用 1號 色彩搭配
stdscr.addstr(1, 1, "HelloGitHub!", curses.color_pair(1))
# 阻塞等待按鍵然后結束程序
stdscr.getch()
curses.endwin()
需要注意的是,0號 位置顏色是默認黑白配色,無法修改
2.2.3 給點細節
在此部分最后的最后,我們來說說如何給文字加一點文字效果:
import curses
import time
stdscr = curses.initscr()
stdscr.nodelay(False)
curses.noecho()
# 之后的文字都加上下劃線,直到調用 attroff為止
stdscr.attron(curses.A_UNDERLINE)
stdscr.addstr(1, 1, "www.HelloGitHub.com")
stdscr.getch()
三、貪吃蛇
前面說了這么多,現在終于到了我們的主菜。在這部分,我將一步步教給大家如何從零開始做出一個簡單卻又不失細節的貪吃蛇。
3.1 設計
對于一個項目來講,相比于盡快動手寫下第一行代碼不如先花點時間進行一些必要的設計,畢竟結構決定功能,一個項目沒有一個良好的結構是沒有前途的。
snake 將貪吃蛇這個游戲分為了三大塊:
- 界面:負責顯示相關的所有工作游戲
- 流程控制:判斷游戲輸贏、游戲初始化等
- 蛇和食物:移動自身、判斷是否死亡、是否被吃等
每一塊都被做成了單獨的對象,通過相互配合實現游戲。下面讓我們來分別看看應該如何實現。
3.2 蛇語者
對于貪吃蛇游戲里面的蛇來講,它可以做的事情有三種:移動,死亡(吃到自己,撞墻)和吃東西
圍繞著這三個功能,我們可以首先寫出一個簡陋的蛇,其類圖如圖所示:

這個蛇可以檢查自己是不是死亡,是不是吃了東西,以及更新自己的位置信息。
其中,在這里插入代碼片body
和last_body
是列表,分別存儲當前蛇身坐標和上一步蛇身坐標,默認列表第一個元素是蛇頭。direction
是當前行進方向,window_size
是蛇可以活動的區域大小。
rest
方法用于重置蛇的狀態,它與 __init__
共同負責蛇的初始化工作:
class Snake(object):
def __init__(self) -> None:
# Position 是我自定義的類,只有 x, y 兩個屬性,存儲一個坐標點
# 初始化蛇可以移動范圍的大小
self.window_size = Position(game_config.game_sizes["width"], game_config.game_sizes["height"])
# 初始化移動方向
self.direction = game_config.D_Down
# 重置身體列表
self.body = []
self.last_body = []
# 生成新的身體,默認在左上角,頭朝下,長三個格子
for i in range(3):
self.body.append(Position(2, 3 - i))
# rest 重置相關屬性
def reset(self) -> None:
self.direction = game_config.D_Down
self.body = []
self.last_body = []
for i in range(3):
self.body.append(Position(2, 3 - i))
Position 是我自定義的類,只有 x, y 兩個屬性,存儲一個坐標點
在最開始我們可能只是模糊的感覺應該有這幾個屬性,但是對于其中的內容和初始化方法又不完全清楚,這是正常的。我們需要做的就是繼續實現需要的功能,在實踐中添加和完善最初的構想
之后,我們從繼續上到下實現,對照類圖,我們接下來應該實現一下 update_snake_pos
即 更新蛇的位置,這部分非常簡單:
def update_snake_pos(self) -> None:
# 這個函數在文章下方,獲得蛇在 x, y 方向上分別增加多少
dis_increment_factor = self.get_dis_inc_factor()
# 需要注意,這里要用深拷貝(import copy)
self.last_body = copy.deepcopy(self.body)
# 先移動蛇頭,然后蛇身依次向前
for index, item in enumerate(self.body):
if index 1:
item.x += dis_increment_factor.x
item.y += dis_increment_factor.y
else: # 剩下的部分要跟著前一部分走
item.x = self.last_body[index - 1].x
item.y = self.last_body[index - 1].y
其實 last_body 可以只記錄最后一次修改的身體
在這里有一個細節,如果我們是第一次寫這個函數,為了讓蛇頭能夠正確地按照玩家操作移動,我們需要知道蛇頭元素在 x, y 方向上各移動了多少。
最簡單的方法是直接一串 if-elif,判斷方向再相加:
if self.direction == LEFT:
head.x -= 1
elif self.direction == RIGHT:
head.x += 1
但是這樣的問題在于,如果我們的需求更改(比如我現在說蛇可以一次走兩個格子,或者吃了特殊道具 x, y 方向上走的距離不一樣等等)直接修改這樣的代碼會讓人很痛苦。
所以在這里更好的解決辦法是使用一個 dis_increment_factor
存儲蛇再 x 和 y 上各移動多少,并且新建一個函數get_dis_inc_factor
進行判斷:
def get_dis_inc_factor(self) -> Position:
# 初始化
dis_increment_factor = Position(0, 0)
# 修改每個方向上的速度
if self.direction == game_config.D_Up:
dis_increment_factor.y = -1
elif self.direction == game_config.D_Down:
dis_increment_factor.y = 1
elif self.direction == game_config.D_Left:
dis_increment_factor.x = -1
elif self.direction == game_config.D_Right:
dis_increment_factor.x = 1
return dis_increment_factor
當然了,這么做或許有點多余,但是努力做到一個函數只做一件事情能幫助化簡我們的代碼,降低寫出又臭又長還難調試代碼的可能性。
解決了移動問題,下一步就是考慮貪吃蛇如何吃到食物了,在這里我們用 和 eat_food
兩個check_eat_food
函數完成:
def eat_food(self, food) -> None:
self.body.append(self.last_body[-1]) # 長大一個元素
def check_eat_food(self, foods: list) -> int: # 返回吃到了哪個食物
# 遍歷食物,看看當前食物和蛇頭是不是重合,重合就是吃到
for index, food in enumerate(foods):
if food == self.body[0]:
# 吃到食物則調用 eat_food 函數,處理蛇身長大等操作
self.eat_food(food)
# 彈出吃掉的食物
foods.pop(index)
# 返回吃掉食物的序號,沒吃則返回 -1
return index
return -1
在這里,foods
是一個存儲著所有食物位置信息的列表,每次蛇體移動后都會調用 check_eat_food
函數檢查是不是吃到了某一個食物。
可以發現,檢查是不是「吃到」和「吃下去」這兩個動作我分為了兩個函數,以做到每個函數「一心一意」方便后期修改。
現在,我們的蛇已經能跑能吃了。但是作為一只能照顧自己的貪吃蛇,我們還需要能夠判斷當前自身狀態,比如最基本的我需要知道我剛剛是不是咬到自己了,只需要看看蛇頭是不是移動到了身體里面:
ef check_eat_self(self) -> bool:
return self.body[0] in self.body[1:] # 判斷蛇頭是不是和身體重合
或者我想知道是不是跑得太快而撞了墻
3.3 命令行?畫板!
上一節中我們實現了游戲里的第一位角色:蛇。為了將它顯示出來我們現在需要將我們的命令行改造成一塊「畫板」。
在動手之前我們同樣思考:我們需要畫哪些東西在我們的命令行上?直接上類圖:

是不是覺得有些眼花繚亂以至于感覺無從下手?其實Graphic
類方法雖多但是大多數方法只是執行一個特定的功能而已,而且每次更新游戲只需要調用 draw_game
方法即可:
def draw_game(self, snake: Snake, foods, lives, scores, highest_score) -> None:
# 清理窗口字符
self.window.erase()
# 繪制幫助信息
self.draw_help()
# 更新當前幀率
self.update_fps()
# 繪制幀率信息
self.draw_fps()
# 繪制生命、得分信息
self.draw_lives_and_scores(lives, scores, highest_score)
# 繪制邊框
self.draw_border()
# 繪制食物
self.draw_foods(foods)
# 繪制蛇身體
self.draw_snake_body(snake)
# 更新界面
self.window.refresh()
# 更新界面
self.game_area.refresh()
# 延遲一段時間,以控制幀率
time.sleep(self.delay_time)
遵循從上到下設計,從下到上實現的原則
可以看出draw_game
實際上已經完成了 Graphic
的所有功能。
再往下深入,我們可以發現類似draw_foods、draw_snake_body
實現基本一樣,都是遍歷坐標列表然后直接在相應位置上添加字符即可:
def draw_snake_body(self, snake: Snake) -> None:
for item in snake.body:
self.game_area.addch(item.y, item.x,
game_config.game_themes["tiles"]["snake_body"],
self.C_snake)
def draw_foods(self, foods) -> None:
for item in foods:
self.game_area.addch(item.y, item.x,
game_config.game_themes["tiles"]["food"],
self.C_food)
將其分開實現也是為了保持代碼干凈易懂以及方便后期修改。draw_help、draw_fps
、draw_lives_and_scores
也是分別打印了不同文字信息,沒有任何新的花樣。
update_fps
實現了幀率的估算以及調節等待時間穩定幀率:
def esp_fps(self) -> bool: # 返回是否更新了fps
# 每 fps_update_interval 幀計算一次
if self.frame_count self.fps_update_interval:
self.frame_count += 1
return False
# 計算時間花費
time_span = time.time() - self.last_time
# 重置開始時間
self.last_time = time.time()
# 估算幀率
self.true_fps = 1.0 / (time_span / self.frame_count)
# 重置計數
self.frame_count = 0
return True
def update_fps(self) -> None:
# 如果重新估計了幀率
if self.esp_fps():
# 計算誤差
err = self.true_fps - self.target_fps
# 調節等待時間,穩定fps
self.delay_time += 0.00001 * err
draw_message_window
則實現了繪制勝利、失敗的畫面:
def draw_message_window(self, texts: list) -> None: # 接收一個 str 列表
text1 = "Press any key to continue."
nrows = 6 + len(texts) # 留出行與行之間的空隙
ncols = max(*[len(len_tex) for len_tex in texts], len(text1)) + 20
# 居中顯示窗口
x = (self.window.getmaxyx()[1] - ncols) / 2
y = (self.window.getmaxyx()[0] - nrows) / 2
pos = Position(int(x), int(y))
# 新建獨立窗口
message_win = curses.newwin(nrows, ncols, pos.y, pos.x)
# 阻塞等待,實現任意鍵繼續效果
message_win.nodelay(False)
# 繪制文字提示
# 底部文字居中
pos.y = nrows - 2
pos.x = self.get_middle(ncols, len(text1))
message_win.addstr(pos.y, pos.x, text1, self.C_default)
# 繪制其他信息
pos.y = 2
for text in texts:
pos.x = self.get_middle(ncols, len(text))
message_win.addstr(pos.y, pos.x, text, self.C_default)
pos.y += 1
# 繪制邊框
message_win.border()
# 刷新內容
message_win.refresh()
# 等待任意按鍵
message_win.getch()
# 恢復非阻塞模式
message_win.nodelay(True)
# 清空窗口
message_win.clear()
# 刪除窗口
del message_win
這樣,我們就實現了游戲動畫的顯示!
3.4 控制!
到目前為止,我們實現了游戲內容繪制以及游戲角色實現,本節我們來學習 snake 的最后一個內容:控制。
老規矩,敲代碼之前我們應該先想一想:如果要寫一個 control 類,它應該都包含哪些方法呢?

仔細思考也不難想到:應該有一個循環,只要沒輸或者沒贏就一直進行游戲,每輪應該更新畫面、蛇移動方向等等。這就是我們的 start:
def start(self) -> None:
# 重置游戲
self.reset()
# 游戲運行標志
while self.game_flag:
# 繪制游戲
self.graphic.draw_game(self.snake, self.foods, self.lives, self.scores, self.highest_score)
# 讀取按鍵控制
if not self.update_control():
continue
# 控制游戲速度
if time.time() - self.start_time 1/game_config.snake_config["speed"]:
continue
self.start_time = time.time()
# 更新蛇
self.update_snake()
只要我們寫出了 start 對于剩下的結構也就能輕松地實現,比如讀取按鍵控制就是最基本的比較數字是不是一樣大:
def update_control(self) -> bool:
key = self.graphic.game_area.getch()
# 不允許 180度 轉彎
if key == curses.KEY_UP and self.snake.direction != game_config.D_Down:
self.snake.direction = game_config.D_Up
elif key == curses.KEY_DOWN and self.snake.direction != game_config.D_Up:
self.snake.direction = game_config.D_Down
elif key == curses.KEY_LEFT and self.snake.direction != game_config.D_Right:
self.snake.direction = game_config.D_Left
elif key == curses.KEY_RIGHT and self.snake.direction != game_config.D_Left:
self.snake.direction = game_config.D_Right
# 判斷是不是退出
elif key == game_config.keys['Q']:
self.game_flag = False
return False
# 判斷是不是重開
elif key == game_config.keys['R']:
self.reset()
return False
更新蛇的狀態時只需要判斷是不是死亡、勝利、吃到東西就可:
def update_snake(self) -> None:
self.snake.update_snake_pos()
index = self.snake.check_eat_food(self.foods)
if index != -1: # 如果吃到食物
# 得分 +1
self.scores += 1
# 如果填滿了游戲區域就勝利
if len(self.snake.body) >= (self.snake.window_size.x - 2) * (self.snake.window_size.y - 2): # 蛇身已經填滿游戲區域
self.win()
else:
# 再放置一個食物
self.span_food()
# 如果死了,就看看是不是游戲結束
if not self.snake.check_alive():
self.game_over()
3.5 直接使用
為了讓這個包能夠直接使用 python snake
就能直接開始游戲,我們來看一下__main__.py
:
import game
g = game.Game()
g.start()
g.quit()
當我們嘗試直接運行一個包時,Python 從 __main__.py
中開始執行,對于我們寫好的代碼,只需三行即可開始游戲!

四、結尾
到這里如何編寫一個貪吃蛇游戲就結束啦!實際上編寫一個小游戲不難,對于新手來講難點在于如何去組織程序的結構。我所實現的只是其中的一種方法,每個人對于游戲結構理解不同所寫出的代碼也會不同。但無論怎樣,我們都應該遵循一個目標:盡量遵循代碼規范,養成良好的風格。這樣不僅利于別人閱讀你的代碼,也利于自己排查 bug、增加新的功能。
到此這篇關于用Python簡單實現個貪吃蛇小游戲(保姆級教程)的文章就介紹到這了,更多相關Python貪吃蛇小游戲內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- 使用python+pygame開發消消樂游戲附完整源碼
- 憶童年!用Python實現憤怒的小鳥游戲
- 用Python手把手教你實現2048小游戲
- python用tkinter開發的掃雷游戲
- Python實現簡單2048小游戲
- Python趣味挑戰之用pygame實現飛機塔防游戲
- 只需要100行Python代碼就可以實現的貪吃蛇小游戲
- python編寫五子棋游戲
- 你喜歡籃球嗎?Python實現籃球游戲