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

Python寫個小游戲:看圖猜成語(下)

編輯:Python

文章目錄

  • 前言
  • 看圖猜成語
    • 1. 玩法簡介
    • 2. 游戲流程
    • 3. 代碼實現
      • 1). 創建成語庫及初始化
        • OS內置模塊
      • 2). 隨機抽取成語
        • 隨機選詞
        • 繪制圖片
        • 准備漢字庫
        • 按鈕上的漢字
      • 3). 重新定義選字按鈕
        • 顯示玩家選擇的漢字
        • 自定義按鈕類
      • 4). 判斷玩家的選擇
      • 5). 清空選擇
      • 6). 電腦提示
    • 4. 完整代碼
  • 總結與思考


前言

大家好,上篇我們把游戲的樣子已經搭起來了,今天我們就繼續未完成的內容,用代碼實現游戲的功能,廢話不多說,讓我們開始吧。

上篇 —— 游戲界面的搭建
下篇 —— 後台程序的實現


看圖猜成語

1. 玩法簡介

上篇中已經介紹過玩法,這裡簡單帶過,相信大家一看就懂。

游戲截圖:

2. 游戲流程

上篇中我們已經畫了一個流程圖:

NoYesYesNo 游戲開始 畫面展示 玩家選擇漢字組成成語 判斷玩家輸入是否正確 清空玩家的選擇 恭喜,是否進入下一關 重新選詞 游戲結束 玩家選擇提示

這裡我們再回顧下,可以幫助我們了解到需要創建哪些自定義函數。在上篇中可以說我們已經完成了“畫面展示”的部分,唯一需要對其進行補充的,是如何隨機顯示成語圖片,已經玩家回答正確後,切換至下一張。

此外,我們還需要完成以下功能的程序或自定義函數:

  1. 創建成語庫,並隨機抽取單個成語
  2. 判斷玩家選擇是否正確
  3. 清空玩家選擇
  4. 提示一個漢字

讓我們逐個來講解。

3. 代碼實現

1). 創建成語庫及初始化

問哥從網上下載了444個看圖猜成語的圖片,上傳在這裡,或者也可以私信問哥領取。

這些圖片有三個特點:

  1. 大小一致,都是120x120
  2. 都是PNG圖片
  3. 都是以各自所代表的成語命名

前兩個特點方便我們在程序裡展示,不用再寫代碼去調整圖片大小的位置。第三個特點更是方便我們創建成語庫,我們只需要把所有成語圖片放在文件夾images下,再提取該文件夾下所有文件的名稱(格式均為xxxx.png),然後選取前4個字符就是我們的成語庫了。調用的時候,我們只要先確定好成語,再在成語後面加上.png就是對應的圖片了。

OS內置模塊

提取文件夾下所有文件的名稱,需要用到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)

這樣我們就創建好了成語庫,也完成了初始化(洗牌)。

2). 隨機抽取成語

考慮到抽取成語的動作是在每個關卡都要操作的,所以還是自定義一個函數來實現比較方便,這樣可以省去重復書寫的代碼。這裡問哥為這個函數取名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) # 把圖片畫在指定位置

運行看看效果:

3). 重新定義選字按鈕

現在擺在我們面前的有兩個問題:

  1. 怎樣實現點擊按鈕,就能得到相對應的漢字;
  2. 怎樣把玩家通過點擊按鈕得到的漢字顯示在圖片下面那四個正方形的格子裡。

我們先來說第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

實現效果如下:

4). 判斷玩家的選擇

在我們剛才新建立的按鈕子類的click方法裡,我們需要先判斷玩家選擇的漢字有沒有達到4個。如果達到4個,我們就需要對其進行判斷。所以我們在click方法裡,加上以下代碼:

 if len(txt)==4:
is_winner()

如果txt的長度為4,即說明有4個漢字被選中的話,調用is_winner()方法,來判斷玩家是否獲勝,以及後續的操作。於是我們開始編寫is_winner()方法,或者稱之為自定義函數。

這個自定義函數要實現的功能其實有不少,我們畫一個局部流程圖來加以講解。

YesYesYesYesNoNo猜中NoNo未猜中 開始判斷 玩家是否猜中? 彈出對話框,詢問是否繼續 判斷是否已猜完詞庫 詢問玩家是否重新開始 重新導入詞庫,關卡清零 從頭開始游戲 游戲結束 清空玩家的選擇 調用create_random_word 回到游戲

由此可見,當玩家選擇正確的時候,要做的事情還真不少:

  1. 詢問是否繼續
  2. 檢查詞庫是否猜完
  3. 詢問是否重頭再玩一次

我先把代碼貼出來:

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個漢字清空。

5). 清空選擇

清空選擇的自定義函數就比較簡單了,但也要完成三件事:

  1. 清空玩家選擇的漢字變量txt
  2. 清空Canvas畫布上的文本框
  3. 把所有按鈕的狀態全部變成NORMAL(因為有些按鈕在按下後被設置成了DISABLE),這樣玩家才能重新選擇。

代碼實現如下:

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)

6). 電腦提示

最後的一步,是電腦提示按鈕。其實問哥覺得這個按鈕有點多余,基本上不會遇到猜不出的情況。但是考慮到可以把游戲進行變種,比如不采用選漢字的方式猜成語,而是讓玩家手工輸入(需要增加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)

4. 完整代碼

到這裡,我們這個小游戲就編寫完成了。大家不妨分享給你的小伙伴一起玩玩看。

附上完整代碼:

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的一些基本操作,希望大家如果感到有問哥沒有講透徹的地方,也可以先通過在網上搜索同類文章加以學習掌握,或者直接私信問哥解答。

感謝大家讀到這裡,我們下次再見!


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