大家好,上篇我們把游戲的樣子已經搭起來了,今天我們就繼續未完成的內容,用代碼實現游戲的功能,廢話不多說,讓我們開始吧。
上篇 —— 游戲界面的搭建
下篇 —— 後台程序的實現
上篇中已經介紹過玩法,這裡簡單帶過,相信大家一看就懂。
游戲截圖:
上篇中我們已經畫了一個流程圖:
這裡我們再回顧下,可以幫助我們了解到需要創建哪些自定義函數。在上篇中可以說我們已經完成了“畫面展示”的部分,唯一需要對其進行補充的,是如何隨機顯示成語圖片,已經玩家回答正確後,切換至下一張。
此外,我們還需要完成以下功能的程序或自定義函數:
讓我們逐個來講解。
問哥從網上下載了444個看圖猜成語的圖片,上傳在這裡,或者也可以私信問哥領取。
這些圖片有三個特點:
前兩個特點方便我們在程序裡展示,不用再寫代碼去調整圖片大小的位置。第三個特點更是方便我們創建成語庫,我們只需要把所有成語圖片放在文件夾images下,再提取該文件夾下所有文件的名稱(格式均為xxxx.png),然後選取前4個字符就是我們的成語庫了。調用的時候,我們只要先確定好成語,再在成語後面加上.png就是對應的圖片了。
提取文件夾下所有文件的名稱,需要用到Python的另一個內置模塊os。顧名思義,這個模塊就是和操作系統(operation system)相關的。用起來也很簡單:
import os
filenames = os.listdir(r'images\words')
只需要使用os模塊裡的listdir方法,就可以把文件夾下所有的文件名都提取出來,返回一個列表。(因為問哥把背景圖片也放在了images文件夾下,所以為了省事,又在下面創建了一個words文件夾專門用來存放成語圖片。)
得到這個列表後,我們就可以使用列表切片操作,提取每個字符串的前四個字符,組成成語列表了。
word_list = [i[:4] for i in filenames]
當然,我們希望每次開始玩游戲的時候,成語顯示都是隨機的,所以我們需要使用前面學到過的random.shuffle方法把這些列表隨機打散,就像洗牌一樣,然後每次從牌堆頂或底抽取一張,成為我們讓玩家猜的成語。
import random
random.shuffle(word_list)
這樣我們就創建好了成語庫,也完成了初始化(洗牌)。
考慮到抽取成語的動作是在每個關卡都要操作的,所以還是自定義一個函數來實現比較方便,這樣可以省去重復書寫的代碼。這裡問哥為這個函數取名create_random_word()
我們先自定義一個全局變量word,用來保存每個關卡電腦抽取的正確成語。隨著游戲進行,我們需要在自定義函數裡不斷地改變這個變量的值(每次抽取的成語都不一樣),所以最好是在函數內部使用global關鍵字把它聲明為全局變量,免去傳參的煩惱。
此外,因為在初始化的過程中,我們已經使用shuffle方法把成語庫的順序打亂,所以在抽取成語的時候我們就不需要再使用隨機方法,而是直接從成語庫(牌堆)的頂或底取一張就可以。問哥使用的是列表的pop()方法,也就是從列表的末端(牌堆的底部)抽取一張,同時成語列表的元素減一。代碼實現如下:
word=''
def create_random_word():
global word
word = word_list.pop()
現在我們就可以把隨機選擇的成語所對應的圖片繪制到Canvas畫布上了。
上篇中,我們是靜態地定義了圖片,回顧一下:
img = tk.PhotoImage(file=f"images\words\一帆風順.png")
cv_word = cv.create_image(150,120,image = img)
現在我們有了自定義函數,就要把這兩句代碼移動到自定義函數中去。但是需要注意的是,繪制在Canvas畫布上的img圖片變量需要帶到主窗口的循環中(mainloop),如果移動到自定義函數中變成局部變量的話,一旦程序運行離開自定義函數,這個變量就消失了,圖片也就為沒有了。所以,我們需要把img也聲明成全局變量。
可以直接在global關鍵字裡一起聲明,並用逗號分隔開。
word=''
def create_random_word():
global word, img
word = word_list.pop()
img = tk.PhotoImage(file=f"images\words\{
word}.png")
cv.create_image(150,120,image = img)
現在我們可以把這個函數在最後調用,(注意,要放在cv.pack()的前面,不然成語圖片畫不上去)
create_random_word()
cv.pack() # 在主窗口裝載畫布
root.mainloop() # 主窗口循環展示
檢查看看效果:
圖片是有了,而且還有一個全局變量word,用來代表圖片所對應的正確成語。但是我們還需要為玩家准備一個可供選擇的漢字庫。這樣玩家將可以從中選取正確的漢字組成成語。
我們定義一個局部列表變量lib來代表這個字庫。同時我們必須要確保字庫裡有正確成語word,還要有另外4個隨機的成語,當做干擾答案。所以我們可以使用random.sample方法從剩下的成語庫裡隨機算出4個成語來,然後用字符串拼接的方法(“join”和“+”)和正確成語word組成一個20個漢字的字符串。接著再把這20個漢字的字符串轉成列表裝進lib。最後再使用random.shuffle方法把這個列表打亂,以保證我們最後顯示的字庫是亂序的。代碼如下:
def create_random_word():
lib = list(''.join(random.sample(word_list,4))+word)
random.shuffle(lib)
但是這裡有個小問題,我們是從“牌堆”(word_list)中隨機選取另外4個成語,隨著游戲的進行,“牌堆”必定會越來越少。當只剩下少於4個成語的時候,這種取樣方式必定會報錯,所以我們必須想辦法保證即使是最後一個成語,也能找到另外4個干擾成語組成lib。
如果把成語詞庫比作牌堆,我們必然會有另一個牌堆列表——棄牌堆,所以我們可以考慮利用棄牌堆。定義一個新的空列表word_copy,用來收集每次用完後的成語word。只要word不為空(游戲剛開始時),就把這個詞加入“棄牌堆”列表word_copy。然後在隨機選取的時候,我們可以從“棄牌堆”和“牌堆”這兩個列表裡選,問題就可以解決了。修改後代碼如下:
word=''
word_copy = []
def create_random_word():
global word, img, word_copy
if word: word_copy.append(word)
word = word_list.pop()
lib = list(''.join(random.sample(word_copy+word_list,4))+word)
這樣我們就准備好了20個(5個成語)亂序的漢字,現在只要把它們“寫”到按鈕上就好了。
還記得上篇我們已經准備好了20個光禿禿的按鈕嗎?
for i in range(4):
for j in range(5):
btn = tk.Button(root, font =('方正楷體簡體',11),width=2,relief='flat',bg='lightyellow')
btn_window = cv.create_window(300+40*i, 75+35*j, window=btn)
現在我們可以把lib裡的漢字寫在按鈕上了,但是當時為了方便布置按鈕,我們把所有的按鈕都取名叫btn,現在我們想在每個按鈕上寫上不同漢字的時候,就必須知道每個按鈕的名字了。所以我們定義一個按鈕的列表,把所有按鈕都放在列表裡,這樣通過列表索引就可以引用不同的按鈕了。於是,這部分代碼修改如下:
btn = []
for i in range(4):
for j in range(5):
btn.append(tk.Button(root, font =('方正楷體簡體',11),width=2,relief='flat',bg='lightyellow'))
btn_window = cv.create_window(300+40*i, 75+35*j, window=btn[i*5+j])
現在我麼可以在create_random_word函數裡在這20個按鈕上寫上漢字了,只要依次修改它們的text屬性。所以最後create_random_word函數的代碼如下:
word=''
word_copy=[]
def create_random_word():
global word, img, word_copy
if word: word_copy.append(word) # “棄牌堆”
word = word_list.pop() # 抽取新的成語
lib = list(''.join(random.sample(word_copy+word_list,4))+word)
random.shuffle(lib) # 准備干擾字庫
for i in range(len(btn)):
btn[i]['text'] = lib[i] # 在按鈕上寫漢字
img = tk.PhotoImage(file=f"images\words\{
word}.png")
cv.create_image(150,120,image = img) # 把圖片畫在指定位置
運行看看效果:
現在擺在我們面前的有兩個問題:
我們先來說第2個問題,因為這個比較好實現。
在上篇裡,那四個正方形的格子其實就是普通的矩形,我們沒法在裡面寫字。
for i in range(4):
cv.create_rectangle(50*i+50,210,50*i+86,246,fill='ivory')
但是我們可以在它們上面寫字。假設玩家選擇的漢字組成的字符串變量是txt,那我們只要在相應的位置用畫布的create_text方法創建4個文本就可以了。
txt = '接二連三'
for i in range(4):
cv.create_text(50*i+68,228,fill='black', text=txt[i], font =('方正楷體簡體',18,'bold'))
但是因為我們還要隨時改變這四個文本,所以最好還是把它們也放在數組裡。代碼修改如下:
txt = '接二連三'
text=[]
for i in range(4):
text.append(cv.create_text(50*i+68,228,fill='black',text=txt[i], font =('方正楷體簡體',18,'bold')))
看看效果:
接下來我們只要通過按鈕去改變變量txt的值就好了。當然在游戲開始的時候,txt的值應該為空(“”)。
現在看看我們剛剛說的第一個問題。tkinter的按鈕組件都可以添加一個command參數,用來指定按下該按鈕後需要執行的函數。但是問題在於,這個command指定的函數不能傳參,而我們的按鈕卻需要在被按下的時候執行不同的動作(把按鈕上的字添加進txt裡)。
這個時候我們可以使用面向對象編程的方法,把每個按鈕想象成一個有生命的物體。每個按鈕有他自己的方法,就是把自己的名字添加進txt。除此之外,這些按鈕還需要和tkinter的Button類有一樣的屬性和方法。於是我們可以創建一個子類,繼承tkinter的Button類。因為只需要添加一個自己獨特的方法,所以不需要定義初始化,默認讓類成員使用tkinter的Button類初始化。代碼如下:
class MyButton(tk.Button):
def click(self):
global txt
if len(txt)<4: # 判斷玩家是不是已經選了4個漢字
txt+=self['text'] # 把按鈕自己的漢字添加進txt
for i in range(len(txt)):
cv.itemconfig(text[i],text=txt[i]) # 改變文本的內容
self.config(state=tk.DISABLED)
self就代表了每個調用click方法的按鈕實例。在這個方法裡,我們需要判斷txt是不是已經有4個字符了(因為只有4個漢字),如果沒有的話,我們把就把按鈕上的漢字(self[‘text’])添加進txt裡。同時,根據變化的txt,使用canvas的itemconfig方法來改變文本組件text的值。這樣就可以達到根據玩家的選擇不同,文本的內容實時變化的效果。然後,當玩家選擇了某個漢字(按下了某個按鈕),我們想要那個按鈕變成灰色不可選的狀態,於是可以直接使用self.config方法將按鈕的狀態變成DISABLED(也可以直接用字典的方式調用,self[‘state’]=tk.DISABLED)。
最後,我們只要把之前創建的按鈕改成這個新的子類,再把之前的循環放在一起,代碼修改如下:
txt = ''
text=[]
btn = []
for i in range(4):
cv.create_rectangle(50*i+50,210,50*i+86,246,fill='ivory')
text.append(cv.create_text(50*i+68,228,fill='black', font =('方正楷體簡體',18,'bold')))
for j in range(5):
btn.append(MyButton(root, font =('方正楷體簡體',11),width=2,relief='flat',bg='lightyellow'))
btn_window = cv.create_window(300+40*i, 75+35*j, window=btn[i*5+j])
for i in btn:
i['command']=i.click
實現效果如下:
在我們剛才新建立的按鈕子類的click方法裡,我們需要先判斷玩家選擇的漢字有沒有達到4個。如果達到4個,我們就需要對其進行判斷。所以我們在click方法裡,加上以下代碼:
if len(txt)==4:
is_winner()
如果txt的長度為4,即說明有4個漢字被選中的話,調用is_winner()方法,來判斷玩家是否獲勝,以及後續的操作。於是我們開始編寫is_winner()方法,或者稱之為自定義函數。
這個自定義函數要實現的功能其實有不少,我們畫一個局部流程圖來加以講解。
由此可見,當玩家選擇正確的時候,要做的事情還真不少:
我先把代碼貼出來:
import tkinter.messagebox as tm
def is_winner():
global word, txt, level, word_list
if txt==word:
for i in btn:
i.config(state=tk.DISABLED)
result=tm.askquestion ("恭喜","恭喜你,答對了!繼續下一題嗎?")
if result == 'yes':
if len(word_list)==0:
newgame = tm.askquestion ("恭喜",f'恭喜你!你已經通過了全部{
level}關,繼續從頭開始游戲嗎?')
if newgame == 'yes':
word_list = [i[:4] for i in filenames]
random.shuffle(word_list)
level = 0
else:
root.destroy()
level += 1
cv.itemconfig(level_indicator,text=f'第 {
level} 關')
clean_word()
create_random_word()
else:
tm.showinfo(title='再見', message=f'您一共通過了{
level}關')
root.destroy()
else:
clean_word()
首先,為了實現windows消息框的效果,我們需要導入tkinter的另一個子模塊messagebox。使用下面的語句將模塊導入,並簡寫為tm
import tkinter.messagebox as tm
然後就可以使用tkinter自帶的幾種消息框了。在這個小游戲中我們只要用到兩種就夠了,一種是在詢問玩家是否繼續的時候,需要得到玩家的選擇,另一種是不需要玩家進行選擇,只是通知玩家一些信息。前者我們使用的是tm的askquestion方法,該方法根據用戶的選擇不同,返回一個字符串,要麼是“yes”,要麼是“no”。
後者僅僅是一個通知消息框,待玩家鼠標點擊“確認”後,程序自動執行後面的命令,也就是“銷毀”主窗口,退出游戲。
root.destroy()
需要注意的是,如果玩家真的把所有成語都猜過了,又選擇重新開始的時候,需要在這裡把word_list重新初始化一遍(從文件名導入成語、隨機打亂)。同時還要把關卡計數清零,從頭開始計數。
而不管是猜錯了,還是猜對後再開始一局,都需要調用另一個自定義函數clean(),用來把文本框裡玩家選擇的4個漢字清空。
清空選擇的自定義函數就比較簡單了,但也要完成三件事:
代碼實現如下:
def clean_word():
global txt
txt = ''
for i in range(4):
cv.itemconfig(text[i],text='')
for i in btn:
i.config(state=tk.NORMAL)
同時我們在游戲的右下角還有另外兩個按鈕,“清空”和“提示”。現在我們可以直接把自定義函數clean_word()綁定到“清空”按鈕上了。
btn_clean=ttk.Button(root, text='清空', width=5, command=clean_word)
最後的一步,是電腦提示按鈕。其實問哥覺得這個按鈕有點多余,基本上不會遇到猜不出的情況。但是考慮到可以把游戲進行變種,比如不采用選漢字的方式猜成語,而是讓玩家手工輸入(需要增加Entry輸入框或使用Label組件),難度就會大大增加了,那樣的話電腦提示可能還是有點作用的。所以這裡我們先把功能做出來,要不要用再說。
其實說來也很簡單,就是隨機選一個漢字,然後讓Canvas的文本框顯示那個漢字就好。於是我們可以定義一個局部變量hint_word,然後再使用隨機函數生成一個0到3的數字,代表我們要選取的漢字在成語中的位置。然後再將hint_word賦值給文本框就好了。
同時不要忘記,玩家有可能猜了一兩個字然後使用提示的情況,這樣一些按鈕狀態可能是DISABLED,所以我們需要在這裡把所有按鈕的狀態恢復成NORMAL。
代碼如下:
def hint():
global word
hint_word = ['']*4
i = random.randint(0,3)
hint_word[i] = word[i]
for i in range(4):
cv.itemconfig(text[i],text=hint_word[i])
for i in btn:
i.config(state=tk.NORMAL)
最後,別忘記把這個自定義函數綁定在“提示”按鈕上。
btn_submit=ttk.Button(root, text='提示', width=5, command=hint)
到這裡,我們這個小游戲就編寫完成了。大家不妨分享給你的小伙伴一起玩玩看。
附上完整代碼:
import tkinter as tk
from tkinter import ttk
import tkinter.messagebox as tm
import os
import random
class MyButton(tk.Button):
def click(self):
global txt
if len(txt)<4:
txt+=self['text']
for i in range(len(txt)):
cv.itemconfig(text[i],text=txt[i])
self.config(state=tk.DISABLED)
if len(txt)==4:
is_winner()
def create_random_word():
global word, img, word_copy
if word: word_copy.append(word)
word = word_list.pop()
lib = list(''.join(random.sample(word_copy+word_list,4))+word)
random.shuffle(lib)
for i in range(len(btn)):
btn[i]['text'] = lib[i]
img = tk.PhotoImage(file=f"images\words\{
word}.png")
cv.create_image(150,120,image = img)
def is_winner():
global word, txt, level, word_list
if txt==word:
for i in btn:
i.config(state=tk.DISABLED)
result=tm.askquestion ("恭喜","恭喜你,答對了!繼續下一題嗎?")
if result == 'yes':
if len(word_list)==0:
newgame = tm.askquestion ("恭喜",f'恭喜你!你已經通過了全部{
level}關,繼續從頭開始游戲嗎?')
if newgame == 'yes':
word_list = [i[:4] for i in filenames]
random.shuffle(word_list)
level = 0
else:
root.destroy()
level += 1
cv.itemconfig(level_indicator,text=f'第 {
level} 關')
clean_word()
create_random_word()
else:
tm.showinfo(title='再見', message=f'您一共通過了{
level}關')
root.destroy()
else:
clean_word()
def clean_word():
global txt
txt = ''
for i in range(4):
cv.itemconfig(text[i],text='')
for i in btn:
i.config(state=tk.NORMAL)
def hint():
global word
hint_word = ['']*4
i = random.randint(0,3)
hint_word[i] = word[i]
for i in range(4):
cv.itemconfig(text[i],text=hint_word[i])
for i in btn:
i.config(state=tk.NORMAL)
# 游戲從這裡開始
filenames = os.listdir(r'images\words')
word_list = [i[:4] for i in filenames]
random.shuffle(word_list)
# 初始化完成
word=''
word_copy=[]
txt = ''
text=[]
btn = []
level=1
# 開始創建GUI窗口
root = tk.Tk()
root.geometry("500x300")
root.resizable(0,0)
root.title('看圖猜成語')
cv=tk.Canvas(root,bg='white',width=500,height=300)
bg = tk.PhotoImage(file=r"images\bg.png")
cv_bg = cv.create_image(250,150,image = bg)
title = tk.PhotoImage(file=r"images\title.png")
cv_tt = cv.create_image(250,30,image = title)
cv.create_rectangle(90,60,210,180,fill='moccasin',outline = '')
for i in range(4):
cv.create_rectangle(50*i+50,210,50*i+86,246,fill='ivory')
text.append(cv.create_text(50*i+68,228,fill='black', font =('方正楷體簡體',18,'bold')))
for j in range(5):
btn.append(MyButton(root, font =('方正楷體簡體',11),width=2,relief='flat',bg='lightyellow'))
btn_window = cv.create_window(300+40*i, 75+35*j, window=btn[i*5+j])
for i in btn:
i['command']=i.click
btn_clean=ttk.Button(root, text='清空', width=5, command=clean_word)
btn_submit=ttk.Button(root, text='提示', width=5, command=hint)
cv.create_window(320, 265, window=btn_clean)
cv.create_window(400, 265, window=btn_submit)
level_indicator = cv.create_text(150,270,text=f'第 {
level} 關', fill='black', font=('微軟正楷',9,'bold'))
create_random_word()
cv.pack()
root.mainloop()
經過一個星期的熬夜,問哥終於把這個小游戲完整地講解出來了。不知道講得夠不夠細致,大家還有沒有不明白的呢?歡迎私信我或給我留言。同時,通過這個小游戲,我們也學習到了Python自帶的圖形化組件tkinter的一些基本操作,希望大家如果感到有問哥沒有講透徹的地方,也可以先通過在網上搜索同類文章加以學習掌握,或者直接私信問哥解答。
感謝大家讀到這裡,我們下次再見!