程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

Python寫個小游戲:蛇棋(上)

編輯:Python

文章目錄

  • 前言
  • 蛇棋
    • 1. 玩法簡介
    • 2. 游戲流程
    • 3. 搭建游戲界面
      • 1). 繪制棋盤
      • 2). 放置信息框
      • 3). 擲骰子的動畫
    • 4. 子窗口
      • 1). 游戲開始函數
      • 2). 棋子初始化函數
      • 3). 更新文本框函數
    • 5. 面向對象——定義棋子類
      • 1). 把棋子看做“有生命的對象”
        • 棋子類的屬性
        • 棋子類的方法
    • 6. 知識點回顧


前言

大家好,好久沒更新游戲主題的文章了。復工以來,因忙於工作,構思游戲的步伐慢了下來,請大家諒解。

今天給大家帶來的游戲叫《蛇棋》,不一定很多人玩過,但規則比較簡單,問哥寧可稱其為簡化版的強手棋。選擇這個游戲的一方面是因為同類教程比較少(問哥總想著推陳出新 ),另一個方面可以通過這個游戲學習到基本的角色動作的實現以及面向對象編程。

因為內容比較多,還是和之前一樣,分為上下篇:

上篇 —— 游戲界面搭建與基本邏輯
下篇 —— 移動棋子的動畫實現與算法


蛇棋

1. 玩法簡介

一般是2到4個玩家,在由一系列格子組成的地圖上移動,玩家通過擲骰子來決定前進的點數,如果遇到梯子的尾部,則前進至梯子頂部的位置,如果遇到蛇頭,稱為“蛇吻”,則滑落至蛇尾的位置。最先到達終點的角色獲勝,但是骰子的點數必須等於最終到達終點的步數,如果大於步數,則棋子倒退移動。

本程序做了一定的簡化,僅提供兩名玩家(人機大戰),而且地圖由10 x 10的正方形格子組成,方便計算。

游戲截圖:

2. 游戲流程

出於慣例,先畫出游戲流程圖。棋類、回合類游戲的流程比較簡單,無非是玩家交替執行相同的程序,然後每次移動後判斷是否達成勝利條件,最後再由玩家決定是否開始新的對戰。

蛇棋的簡易流程圖如下:

玩家回合梯子電腦回合蛇頭 游戲開始 玩家選擇棋子 顯示棋子 判斷當前回合 玩家擲骰子 移動棋子 判斷是否遇到梯子、蛇頭 移動棋子至梯子頂部 是否到達終點 是否開始新游戲 游戲結束 電腦擲骰子 移動棋子至蛇尾

3. 搭建游戲界面

問哥從網上下載了蛇棋的棋盤圖片,色彩比較鮮艷、分辨率還可以。但因為不是自己繪制的棋盤,所以無法精確到每個格子的大小,但這個數值卻對我們在後面對移動的棋子進行定位至關重要。問哥經過反復測量與實驗,得到圖片中棋盤每一格的寬度為58像素,高度為63像素。記住這兩個數字,後面會用到。

1). 繪制棋盤

游戲背景、標題,相信大家已經很熟悉了。問哥還是使用Canvas進行游戲界面的繪制,而窗口的尺寸就是背景棋盤圖片的大小。

直接上代碼:

from tkinter import *
root = Tk()
root.geometry('1000x750+300+100')
root.title('蛇棋')
root.resizable(0,0)
cv = Canvas(root, width=1000,height=750,bg='lightyellow')
bg = PhotoImage(file=r"image\snake\bg.png")
cv_bg = cv.create_image(500,375,image = bg)
cv.pack()
root.mainloop()

2). 放置信息框

圖片原本自帶的規則說明的部分在這裡有點多余,而且我們也需要在右邊放置一個滾動顯示當前游戲信息的窗口,於是在其位置插入一個帶滾動條的文本框 ScrolledText組件。注意,該組件需要從tkinter的子模塊scrolledtext中,所以需要先導入該模塊。

在Cavans畫布上插入此文本框,以擋住規則說明的部分(橫縱坐標為反復測試獲得)。

import tkinter.scrolledtext as ts
info=ts.ScrolledText(root,width=25, height=17)
cv.create_window(890,390,window=info)

3). 擲骰子的動畫

在文本框的下面,我們放置一個投擲骰子的區域,並添加一個按鈕,這樣當玩家按下按鈕,骰子將會轉動,然後隨機得到一個點數,即為玩家或電腦棋子要前進的步數。

關於擲骰子的動畫,我在之前的文章介紹過,大家可以參考 Python動畫制作:用tkinter模擬擲骰子

可以直接把代碼復制過來,不過問哥在之前那篇文章裡使用了一個全局變量i,用來決定骰子轉動的起始圖片位置。這裡,我們可以使用另一種寫法,將按鈕的回調函數寫成lambda匿名函數,然後向函數裡傳參i。最後調整一下橫縱坐標,放置在合適的位置。

import random
def roll(i):
btn['state']=DISABLED
if i<13:
cv.itemconfig(image1,image=dice_rotate[i])
root.after(50,lambda :roll(i+1))
else:
res = random.randint(1,6)
cv.itemconfig(image1,image=dice[res-1])
dice_rotate = [PhotoImage(file=r'image\snake\roll.gif', format=f'gif -index {
i}') for i in range(13)]
dice = [PhotoImage(file=f'image\snake\{
i+1}.png') for i in range(6)]
image1 = cv.create_image(880,580,image=dice[0])
btn = Button(root, text='擲骰子',command=lambda :roll(0))
cv.create_window(880,680,window=btn)

其中lambda匿名函數 lambda :roll(0) 以及 lambda :roll(i+1) 相當於下面這種寫法,只不過省去了fun這個名字(所以才叫匿名函數啊):

def fun():
roll(i+1)

為了得到隨機的擲骰子結果,別忘記導入random模塊,生成1到6之間的隨機整數。

當然,我們不希望玩家不停地按按鈕,於是在按下按鈕後,有必要將其設置為DISABLED,禁用狀態,在後面輪到玩家擲骰子的時候再將其重新啟用。實現效果如下:

4. 子窗口

根據我們之前畫出的流程圖,當游戲開始的時候(包括游戲結束玩家選擇重新開始游戲的時候),我們需要讓玩家選擇代表自己的棋子,於是需要彈出一個窗口,並提供棋子讓玩家選擇。

其實這也可以在同一個Canvas畫布上實現,在畫布上創建一個Label組件,然後當玩家點擊後,刪除Label。但是使用子窗口會更加靈活一點,玩家可以移動它的位置,也可以在子窗口上自由布局,不用擔心影響主窗口。

創建子窗口的組件是Toplevel(),可以將其放在游戲開始的自定義函數裡,當我們開始游戲的時候就調用它。為此,我們需要創建以下幾個自定義函數:

1). 游戲開始函數

為了方便反復調用,我們自定義一個游戲開始函數,這樣,當游戲結束的時候,如果玩家選擇繼續游戲,可以再次調用此函數。

本游戲需要在游戲開始函數裡進行初始化的環境變量並不多。把擲骰子的按鈕禁掉(防止玩家不選棋子直接去擲骰子)、把滾動文本框的輸入功能禁掉(該文本框用於顯示游戲信息,並不允許玩家進行輸入),然後就要通過Toplevel()創建一個子窗口。

子窗口的尺寸、標題等參數和主窗口一樣,這裡不再贅述。該子窗口要實現的功能也非常簡單,只有三個組件,一個標簽,用來提示玩家,在這裡選擇棋子,兩個按鈕(因為人機大戰,只有兩個棋子),按鈕上分別顯示兩個棋子的圖片,如果玩家按下了相應棋子的案件,則通過回調函數將棋子初始化。

def start_game():
global tl
btn['state']=DISABLED
info['state']=DISABLED
tl = Toplevel(root)
tl.geometry('220x150+690+400')
tl.title('游戲開始')
tl.resizable(0,0)
tl.wm_transient(root)
Label(tl,text='請選擇棋子').place(x=80,y=10)
Button(tl,image=player_img[0],command=lambda :choose_piece(0)).place(x=50,y=50)
Button(tl,image=player_img[1],command=lambda :choose_piece(1)).place(x=120,y=50)
player_img = [PhotoImage(file=f'image\snake\p{
i+1}.png') for i in range(2)]
start_game()

在按鈕上展示圖片只需要把按鈕的image參數設置為圖片即可。因為只有電腦和玩家兩個棋子,所以只需要准備兩張圖片即可。問哥這裡使用了國際象棋的棋子圖片作為例子,大家可以選擇自己喜歡的圖片。

2). 棋子初始化函數

當玩家按下棋子的按鈕後,自動調用另一個自定義函數 choose_piece() 用來將玩家的棋子初始化(繪制在棋牌左下角開始位置),並創建一個players的列表,用來表示棋子的順序。關於棋子初始化的部分,我們用到了面向對象的功能,後面會介紹,這裡可以先將其注釋掉。

def choose_piece(img):
# global players
# P1 = Piece("Player", cv, (30,650),player_img[img])
# P2 = Piece("電腦", cv, (50,655),player_img[1-img])
# players=[P1,P2]
info_update("游戲開始")
info_update("請玩家先擲骰子")
btn['state']=NORMAL
tl.destroy()

因為我們的規則默認玩家先行動,於是緊接著我們就可以在信息框裡顯示“請玩家先擲骰子”這樣的信息。(可以增加一個隨機變量用來決定誰先移動,大家可以思考一下如何實現)

最後再把擲骰子的按鈕狀態恢復,然後使用子窗口的destroy()方法將其銷毀。

3). 更新文本框函數

由於文本框的內容會被頻繁更新,所以我們需要創建一個更新文本框的函數 info_update(),方便調用,實現的效果是只需要把需要顯示的文本傳遞進去,就會自動顯示在文本框的最後一行,同時將滾動條移動到最後一行。

代碼如下:

def info_update(content):
info['state']=NORMAL
info.insert(END, content+'\n')
info.see('end')
info['state']=DISABLED

文本框的 insert() 方法使用的關鍵字“END”表示在文本框現有內容的末尾開始插入,然後轉義符號\n表示在末尾插入一個換行符,以方便下次調用該函數時繼續在末尾插入文字。

文本框的 see(‘end’) 方法會自動將滾動條向下滾動,把最後一行顯示出來。如果不調用這個函數,雖然文本同樣會正常更新進去,但如果內容太多的話,我們需要手動將滾動條拉到最下面才能看到。

函數的最後,還要記得把文本框的狀態設置為禁用,因為我們不希望玩家在文本框內手工修改信息。

最後,測試效果,沒有問題:

5. 面向對象——定義棋子類

1). 把棋子看做“有生命的對象”

面向對象編程其實就是這麼一個概念,把無生命的東西、概念實體化,變成有“生命”的物體,就像“成精”了。這些物體有他們自己的屬性(名字、年齡等),還有自己的方法(會跑、跳、攻擊、死亡等等)。游戲編程裡有個概念叫Sprite,表示在屏幕上出現的各種物體:主角、敵人、道具等等。——很多中文翻譯都太文雅了,稱其“精靈”,其實翻譯成“妖精”也無不可。特指的就是這些原本無生命,卻被實體化出現在游戲屏幕上“有生命的對象”們。

這些對象自創建以後,他們的“屬性”便一直跟隨,可以隨時查看調用。比如這個例子裡,我們把棋子自定義成對象後,就可以把其在棋盤上的位置定義成棋子的屬性,這樣,如果我們想知道棋子現在在哪裡,只要調用它的屬性一看便知。這便是自定義棋子類的一個好處。

而對象的方法是針對這個對象裡的所有實例。比如這個例子裡,我們有玩家和電腦兩個棋子,都屬於棋子類,那麼當我們賦予這個類“移動”的方法時,兩個棋子就都“會”移動了。只不過,每個棋子移動的方向、距離、方式等都是根據棋子當前在棋盤的位置決定的。換句話說,當棋子執行自己的方法時,需要調用自己的屬性,那我們就沒有必要指定參數了,只要告訴棋子去移動到哪裡就好。這便是另一個好處。

棋子類的屬性

自定義類的方法使用 “class 類名” 就可以。對於類名的要求,和變量的規則一樣,但是為了便於和普通變量區分,一般約定首字母大寫。

為了給我們的棋子“妖精”們賦上屬性,我們需要定義一個初始化函數,然後在函數裡定義這些變量。下面我們來整理一下,看看棋子們需要哪些屬性:

  1. 名字:用來標記這個棋子是“電腦”還是“玩家”,純文本,僅在輸出提示信息的時候被用到。
  2. Canvas:因為我們需要借助Canvas的move方法來移動棋子,而move方法屬於Canvas類,所以我們可以把Canvas對象也傳進來,使它成為棋子的屬性,方便調用。
  3. 棋子在Canvas上的唯一標識符:用來表示Canvas繪制該棋子圖片時的id。這樣後面使用move方法的時候,才能指定移動哪個棋子。
  4. 位置:最關鍵的信息,表示棋子目前處於哪個格子。因為我們有100個格子,於是可以定義一個整數,范圍在0到100即可。

結合這些信息,初始化棋子類的代碼如下:

class Piece:
def __init__(self,name,cv,co,bg):
self.name = name
self.cv = cv
self.id = cv.create_image(co,image = bg)
self.pos = 0

注意到我們剛剛在主程序初始化棋子的函數裡,根據用戶的選擇,來定義棋子的圖片和繪制該圖片的坐標。實際上,這就是類的實例化過程,只需要調用類名,然後在參數裡傳入類的屬性即可。於是我們根據剛才定義的棋子類的屬性,按順序傳入以下幾個參數:

  1. 名字:用於給棋子類的name屬性賦值
  2. Canvas對象:表示當前操作的Canvas,也是棋子類的屬性
  3. 坐標:用於Canvas繪制在什麼位置棋子
  4. 棋子圖片:用於Canvas繪制棋子
def choose_piece(img):
global players
P1 = Piece("Player", cv, (30,650),player_img[img])
P2 = Piece("電腦", cv, (50,655),player_img[1-img])
players=[P1,P2]

這裡的坐標參數是我試了很多次,結合棋盤上的寬度和後面要移動的距離,還要使兩個棋子相互錯開,於是得到一個比較適合的坐標對。大家也可以改成其他數字作為棋子在棋盤上的初始位置。

棋子類的方法

顯而易見,棋子第一個方法就是移動。我們想要展現棋子跳躍前進的一個效果,如下圖所示,就勢必需要定義一個棋子跳動的方法。

當棋子移動到兩邊,需要向上移動時,或者遇到梯子和蛇頭,需要向上、向下移動時,就要定義另一種移動的方法:

關於這兩種移動方法的定義,我們放在下篇再詳細介紹。

6. 知識點回顧

  1. Lambda 匿名函數
  2. 滾動文本框 ScrolledText 組件
  3. 子窗口 Toplevel()
  4. 面向對象與自定義類


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved