<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          用 Python 寫個貪吃蛇,保姆級教程!

          共 21751字,需瀏覽 44分鐘

           ·

          2021-06-06 10:36

          本文基于 Windows 環(huán)境開發(fā),適合 Python 新手

          本文作者:HelloGitHub-Anthony

          HelloGitHub 推出的《講解開源項目》系列,本期介紹 Python 練手級項目——貪吃蛇!

          原本想推薦一個貪吃蛇的開源項目:python-console-snake,但由于該項目最近一次更新是 8 年前,而且在運行的時候出現(xiàn)了諸多問題。索性我就動手用 Python 重新寫了一個貪吃蛇游戲。

          項目地址:https://github.com/AnthonySun256/easy_games

          下面我們就一起用 Python 實現(xiàn)一個簡單有趣的命令行貪吃蛇小游戲,啟動命令:

          git clone https://github.com/AnthonySun256/easy_games
          cd easy_games
          python snake

          本文包含設(shè)計和講解,整體分為兩個部分:第一部分是關(guān)于 Python 命令行圖形化庫 curses 接著是 snake 相關(guān)代碼。

          一、初識 curses

          Python 已經(jīng)內(nèi)置了 curses 庫,但是對于 Windows 操作系統(tǒng)我們需要安裝一個補(bǔ)丁以進(jìn)行適配。

          Windows 下安裝補(bǔ)全包:

          pip install windows-curses

          curses 是一個應(yīng)用廣泛的圖形函數(shù)庫,可以在終端內(nèi)繪制簡單的用戶界面。

          在這里我們只進(jìn)行簡單的介紹,只學(xué)習(xí)貪吃蛇需要的功能

          如果您已經(jīng)接觸過 curses,請?zhí)^此部分內(nèi)容。

          1.1 簡單使用

          Python 內(nèi)置了 curses 庫,其使用方法非常簡單,以下腳本可以顯示出當(dāng)前按鍵對應(yīng)編號:

          # 導(dǎo)入必須的庫
          import curses
          import time

          # 初始化命令行界面,返回的 stdscr 為窗口對象,表示命令行界面
          stdscr = curses.initscr()
          # 使用 noecho 方法關(guān)閉命令行回顯
          curses.noecho()
          # 使用 nodelay(True) 方法讓 getch 為非阻塞等待(即使沒有輸入程序也能繼續(xù)執(zhí)行)
          stdscr.nodelay(True)
          while True:
              # 清除 stdscr 窗口的內(nèi)容(清除殘留的符號)
              stdscr.erase()
              # 獲取用戶輸入并放回對應(yīng)按鍵的編號
              # 非阻塞等待模式下沒有輸入則返回 -1
              key = stdscr.getch()
              # 在 stdscr 的第一行第三列顯示文字
              stdscr.addstr(13"Hello GitHub.")
              # 在 stdscr 的第二行第三列顯示文字
              stdscr.addstr(23"Key: %d" % key)
              # 刷新窗口,讓剛才的 addstr 生效
              stdscr.refresh()
              # 等待 0.1s 給用戶足夠反應(yīng)時間查看文字
              time.sleep(0.1)

          您也可以嘗試把 nodelay(True) 改為 nodelay(False) 后再次運行,這時候程序會阻塞在 stdscr.getch() 只有當(dāng)您按下按鍵后才會繼續(xù)執(zhí)行。

          1.2 整點花樣

          您也許會覺得上面的例子太菜了,隨便用幾個 print 都能達(dá)到相同的效果,現(xiàn)在我們來整點花樣以實現(xiàn)一些使用普通輸出無法達(dá)到的效果。

          1.2.1 新建一個子窗口

          說再多的話也不如一張圖來的實際:

          如果我們想要實現(xiàn)圖中 Game over! 窗口,可以使用 newwin 方法:

          import curses
          import time

          stdscr = curses.initscr()
          curses.noecho()
          stdscr.addstr(12"HelloGitHub")
          # 新建窗口,高為 5 寬為 25,在命令行窗口的 四行六列處
          new_win = curses.newwin(52546)
          # 使用阻塞等待模式
          new_win.nodelay(False)
          # 在新窗口的 2 行 3 列處添加文字
          new_win.addstr(23"www.HelloGitHub.com")
          # 給新窗口添加邊框,其中邊框符號可以這是,這里使用默認(rèn)字符
          new_win.border()
          # 刷新窗口
          stdscr.refresh()
          # 等待字符輸入(這里會一直等待輸入)
          new_win.getch()
          # 刪除新窗口對象
          del new_win
          # 清除所有內(nèi)容(比 erase 更徹底)
          stdscr.clear()
          # 重新添加文字
          stdscr.addstr(12"HelloGitHub")
          # 刷新窗口
          stdscr.refresh()
          # 等待兩秒鐘
          time.sleep(2)
          # 結(jié)束 curses 模式,恢復(fù)到正常命令行模式
          curses.endwin()

          除了 curses.newwin 新建一個獨立的窗口,我們還能在任意窗口上使用 subwin 或者 subpad 方法新建子窗口,例如 stdscr.subwin、 stdscr.subpadnew_win.subwin、new_win.subpad 等等,其使用方法與本節(jié)中創(chuàng)建的 new_win 或者 stdscr 沒有區(qū)別,只是新建窗口使用獨立的緩存區(qū),而子窗口和父窗口共享緩存區(qū)。

          如果某個窗口會在使用后刪除,最好使用 newwin 方法新建獨立窗口,以防止刪除子窗口造成父窗口的緩存內(nèi)容出現(xiàn)問題。

          1.2.2 上點顏色

          白與黑的搭配看久了也會顯得單調(diào),curses 提供了內(nèi)置顏色可以讓我們自定義前后背景。

          在使用彩色模式之前我們需要先使用使用 curses.start_corlor() 進(jìn)行初始化操作:

          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(11"HelloGitHub!", curses.color_pair(1))
          # 阻塞等待按鍵然后結(jié)束程序
          stdscr.getch()
          curses.endwin()

          需要注意的是,0號 位置顏色是默認(rèn)黑白配色,無法修改

          1.2.3 給點細(xì)節(jié)

          在此部分最后的最后,我們來說說如何給文字加一點文字效果:

          import curses
          import time
          stdscr = curses.initscr()
          stdscr.nodelay(False)
          curses.noecho()
          # 之后的文字都加上下劃線,直到調(diào)用 attroff為止
          stdscr.attron(curses.A_UNDERLINE)
          stdscr.addstr(11"www.HelloGitHub.com")
          stdscr.getch()

          二、貪吃蛇

          前面說了這么多,現(xiàn)在終于到了我們的主菜。在這部分,我將一步步教給大家如何從零開始做出一個簡單卻又不失細(xì)節(jié)的貪吃蛇。

          2.1 設(shè)計

          對于一個項目來講,相比于盡快動手寫下第一行代碼不如先花點時間進(jìn)行一些必要的設(shè)計,畢竟結(jié)構(gòu)決定功能,一個項目沒有一個良好的結(jié)構(gòu)是沒有前途的。

          snake 將貪吃蛇這個游戲分為了三大塊:

          1. 界面:負(fù)責(zé)顯示相關(guān)的所有工作
          2. 游戲流程控制:判斷游戲輸贏、游戲初始化等
          3. 蛇和食物:移動自身、判斷是否死亡、是否被吃等

          每一塊都被做成了單獨的對象,通過相互配合實現(xiàn)游戲。下面讓我們來分別看看應(yīng)該如何實現(xiàn)。

          2.2 蛇語者

          對于貪吃蛇游戲里面的蛇來講,它可以做的事情有三種:移動,死亡(吃到自己,撞墻)和吃東西

          圍繞著這三個功能,我們可以首先寫出一個簡陋的蛇,其類圖如圖所示:

          這個蛇可以檢查自己是不是死亡,是不是吃了東西,以及更新自己的位置信息。

          其中,bodylast_body 是列表,分別存儲當(dāng)前蛇身坐標(biāo)和上一步蛇身坐標(biāo),默認(rèn)列表第一個元素是蛇頭。direction 是當(dāng)前行進(jìn)方向,window_size 是蛇可以活動的區(qū)域大小。

          rest 方法用于重置蛇的狀態(tài),它與 __init__ 共同負(fù)責(zé)蛇的初始化工作:

          class Snake(object):
              def __init__(self) -> None:
                  # Position 是我自定義的類,只有 x, y 兩個屬性,存儲一個坐標(biāo)點
                  # 初始化蛇可以移動范圍的大小
                  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 = []
                  # 生成新的身體,默認(rèn)在左上角,頭朝下,長三個格子
                  for i in range(3):
                      self.body.append(Position(23 - i))
           # rest 重置相關(guān)屬性
              def reset(self) -> None:
                  self.direction = game_config.D_Down
                  self.body = []
                  self.last_body = []
                  for i in range(3):
                      self.body.append(Position(23 - i))

          Position 是我自定義的類,只有 x, y 兩個屬性,存儲一個坐標(biāo)點

          在最開始我們可能只是模糊的感覺應(yīng)該有這幾個屬性,但是對于其中的內(nèi)容和初始化方法又不完全清楚,這是正常的。我們需要做的就是繼續(xù)實現(xiàn)需要的功能,在實踐中添加和完善最初的構(gòu)想。

          之后,我們從繼續(xù)上到下實現(xiàn),對照類圖,我們接下來應(yīng)該實現(xiàn)一下 update_snake_pos 即 更新蛇的位置,這部分非常簡單:

          def update_snake_pos(self) -> None:
              # 這個函數(shù)在文章下方,獲得蛇在 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ì)節(jié),如果我們是第一次寫這個函數(shù),為了讓蛇頭能夠正確的按照玩家操作移動,我們需要知道蛇頭元素在 x, y 方向上各移動了多少。

          最簡單的方法是直接一串 if-elif,判斷方向再相加:

          if self.direction == LEFT:
              head.x -= 1
          elif self.direction == RIGHT:
              head.x += 1
              ....

          但是這樣的問題在于,如果我們的需求更改(比如我現(xiàn)在說蛇可以一次走兩個格子,或者吃了特殊道具 x, y 方向上走的距離不一樣等等)直接修改這樣的代碼會讓人很痛苦。

          所以在這里更好的解決辦法是使用一個 dis_increment_factor 存儲蛇再 x 和 y 上各移動多少,并且新建一個函數(shù) get_dis_inc_factor 進(jìn)行判斷:

          def get_dis_inc_factor(self) -> Position:
              # 初始化
              dis_increment_factor = Position(00)

              # 修改每個方向上的速度
              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

          當(dāng)然了,這么做或許有點多余,但是努力做到一個函數(shù)只做一件事情能幫助化簡我們的代碼,降低寫出又臭又長還難調(diào)試代碼的可能性。

          解決了移動問題,下一步就是考慮貪吃蛇如何吃到食物了,在這里我們用 check_eat_foodeat_food 兩個函數(shù)完成:

          def eat_food(self, food) -> None:
              self.body.append(self.last_body[-1])  # 長大一個元素

          def check_eat_food(self, foods: list) -> int:  # 返回吃到了哪個食物
              # 遍歷食物,看看當(dāng)前食物和蛇頭是不是重合,重合就是吃到
              for index, food in enumerate(foods):
                  if food == self.body[0]:
                      # 吃到食物則調(diào)用 eat_food 函數(shù),處理蛇身長大等操作
                      self.eat_food(food)
                      # 彈出吃掉的食物
                      foods.pop(index)
                      # 返回吃掉食物的序號,沒吃則返回 -1
                      return index
              return -1

          在這里,foods 是一個存儲著所有食物位置信息的列表,每次蛇體移動后都會調(diào)用 check_eat_food 函數(shù)檢查是不是吃到了某一個食物。

          可以發(fā)現(xiàn),檢查是不是「吃到」和「吃下去」這兩個動作我分為了兩個函數(shù),以做到每個函數(shù)「一心一意」方便后期修改。

          現(xiàn)在,我們的蛇已經(jīng)能跑能吃了。但是作為一只能照顧自己的貪吃蛇,我們還需要能夠判斷當(dāng)前自身狀態(tài),比如最基本的我需要知道我剛剛是不是咬到自己了,只需要看看蛇頭是不是移動到了身體里面:

          def check_eat_self(self) -> bool:
              return self.body[0in self.body[1:]  # 判斷蛇頭是不是和身體重合

          或者我想知道是不是跑得太快而撞了墻:

          def check_hit_wall(self) -> bool:
              # 是不是在上下邊框之間
              is_between_top_bottom = self.window_size.y - 1 > self.body[0].y > 0
              # 是不是在左右邊框之間
              is_between_left_right = self.window_size.x - 1 > self.body[0].x > 0
              # 返回 是 或者 不是 撞了墻
              return not (is_between_top_bottom and is_between_left_right)

          這些功能都是簡單得不能再簡單了,但是要相信自己,就是這么簡單的幾行代碼就能實現(xiàn)一個聽你指揮能做出復(fù)雜動作的

          完整代碼:https://github.com/AnthonySun256/easy_games

          2.3 命令行?畫板!

          上一節(jié)中我們實現(xiàn)了游戲里的第一位角色:。為了將它顯示出來我們現(xiàn)在需要將我們的命令行改造成一塊「畫板」。

          在動手之前我們同樣思考:我們需要畫哪些東西在我們的命令行上?直接上類圖:

          是不是覺得有些眼花繚亂以至于感覺無從下手?其實 Graphic 類方法雖多但是大多數(shù)方法只是執(zhí)行一個特定的功能而已,而且每次更新游戲只需要調(diào)用 draw_game 方法即可:

          def draw_game(self, snake: Snake, foods, lives, scores, highest_score) -> None:
              # 清理窗口字符
              self.window.erase()
              # 繪制幫助信息
              self.draw_help()
              # 更新當(dāng)前幀率
              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)

          遵循從上到下設(shè)計,從下到上實現(xiàn)的原則

          可以看出 draw_game 實際上已經(jīng)完成了 Graphic 的所有功能。

          再往下深入,我們可以發(fā)現(xiàn)類似 draw_foods、draw_snake_body 實現(xiàn)基本一樣,都是遍歷坐標(biāo)列表然后直接在相應(yīng)位置上添加字符即可:

          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)

          將其分開實現(xiàn)也是為了保持代碼干凈易懂以及方便后期修改。draw_helpdraw_fps、draw_lives_and_scores 也是分別打印了不同文字信息,沒有任何新的花樣。

          update_fps 實現(xiàn)了幀率的估算以及調(diào)節(jié)等待時間穩(wěn)定幀率:

          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)
              # 重置計數(shù)
              self.frame_count = 0
              return True

          def update_fps(self) -> None:
              # 如果重新估計了幀率
              if self.esp_fps():
                  # 計算誤差
                  err = self.true_fps - self.target_fps
                  # 調(diào)節(jié)等待時間,穩(wěn)定fps
                  self.delay_time += 0.00001 * err

          draw_message_window 則實現(xiàn)了繪制勝利、失敗的畫面:

          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)
              # 阻塞等待,實現(xiàn)任意鍵繼續(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()
           # 刷新內(nèi)容
              message_win.refresh()
              # 等待任意按鍵
              message_win.getch()
              # 恢復(fù)非阻塞模式
              message_win.nodelay(True)
              # 清空窗口
              message_win.clear()
              # 刪除窗口
              del message_win

          這樣,我們就實現(xiàn)了游戲動畫的顯示!

          2.4 控制!

          到目前為止,我們實現(xiàn)了游戲內(nèi)容繪制以及游戲角色實現(xiàn),本節(jié)我們來學(xué)習(xí) snake 的最后一個內(nèi)容:控制。

          老規(guī)矩,敲代碼之前我們應(yīng)該先想一想:如果要寫一個 control 類,他應(yīng)該都包含哪些方法呢?

          仔細(xì)思考也不難想到:應(yīng)該有一個循環(huán),只要沒輸或者沒贏就一直進(jìn)行游戲,每輪應(yīng)該更新畫面、蛇移動方向等等。這就是我們的 start

          def start(self) -> None:
              # 重置游戲
              self.reset()
           # 游戲運行標(biāo)志
              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 對于剩下的結(jié)構(gòu)也就能輕松的實現(xiàn),比如讀取按鍵控制就是最基本的比較數(shù)字是不是一樣大:

          def update_control(self) -> bool:
              key = self.graphic.game_area.getch()

              # 不允許 180度 轉(zhuǎn)彎
              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

          更新蛇的狀態(tài)時只需要判斷是不是死亡、勝利、吃到東西就可:

          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
                  # 如果填滿了游戲區(qū)域就勝利
                  if len(self.snake.body) >= (self.snake.window_size.x - 2) * (self.snake.window_size.y - 2):  # 蛇身已經(jīng)填滿游戲區(qū)域
                      self.win()
                  else:
                      # 再放置一個食物
                      self.span_food()
           # 如果死了,就看看是不是游戲結(jié)束
              if not self.snake.check_alive():
                  self.game_over()

          2.5 直接使用

          為了讓這個包能夠直接使用 python snake 就能直接開始游戲,我們來看一下 __main__.py

          import game

          g = game.Game()
          g.start()
          g.quit()

          當(dāng)我們嘗試直接運行一個包時,Python 從 __main__.py 中開始執(zhí)行,對于我們寫好的代碼,只需三行即可開始游戲!

          三、結(jié)尾

          到這里如何編寫一個貪吃蛇游戲就結(jié)束啦!實際上編寫一個小游戲不難,對于新手來講難點在于如何去組織程序的結(jié)構(gòu)。我所實現(xiàn)的只是其中的一種方法,每個人對于游戲結(jié)構(gòu)理解不同所寫出的代碼也會不同。但無論怎樣,我們都應(yīng)該遵循一個目標(biāo):盡量遵循代碼規(guī)范,養(yǎng)成良好的風(fēng)格。這樣不僅利于別人閱讀你的代碼,也利于自己排查 bug、增加新的功能。

          最后,感謝您的閱讀。這里是 HelloGitHub 分享 GitHub 上有趣、入門級的開源項目。您的每個點贊、留言、分享都是對我們最大的鼓勵,筆芯~

          - END -

          ??「點擊關(guān)注」發(fā)現(xiàn)更多驚喜??

          瀏覽 42
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  男女操逼视频在线播放 | 精品人妻无码一区二区三区四川人 | 在线观看AA | 最近中文字幕免费mv第一季歌词強上 | 尤物视频在线观看国产性感 |