海龜繪圖案例分析之移動(dòng)火柴變等式游戲(3)
說在前面
移動(dòng)一根火柴棒使等式成立,是一種簡單有趣的智力游戲,只需具備簡單的算術(shù)知識(shí),就能參與游戲,老少皆宜,深受大家的喜愛。
上兩節(jié)課我們已經(jīng)學(xué)習(xí)了創(chuàng)建題庫和繪制七段管數(shù)字算式的方法,今天我們就把這兩者組合起來,并學(xué)習(xí)第三部分——點(diǎn)擊鼠標(biāo)移動(dòng)火柴棒。
本小游戲主要分為三個(gè)部分:創(chuàng)建題庫,繪制游戲界面和響應(yīng)屏幕事件。
各部分對(duì)應(yīng)的自定義函數(shù)和主函數(shù)部分代碼框架如下所示:

創(chuàng)建題庫部分我已經(jīng)在第一節(jié)課中做了詳細(xì)介紹,直接把源代碼復(fù)制過來就行了,繪制算式模塊相關(guān)函數(shù)我們?cè)诘诙?jié)課也已經(jīng)分析過了。接下來我將依次介紹繪制游戲界面和響應(yīng)屏幕事件。
游戲界面主要包括標(biāo)題和3個(gè)功能鍵按鈕,點(diǎn)擊“隨機(jī)出題”按鈕后,還會(huì)在按鈕下方顯示題目。

首先定義一個(gè)函數(shù)來輸出文字信息,代碼如下:
'''函數(shù)功能:顯示提示信息函數(shù)名:draw_info(x, y, text, c, size, mypen)參數(shù)表:x, y -- 顯示信息位置;text -- 顯示信息內(nèi)容;c -- 畫筆顏色;size -- 字體大??;mypen -- 當(dāng)前畫筆對(duì)象。返回值:沒有返回值。'''def draw_info(x, y, text, c, size, mypen):mypen.color(c)mypen.clear()mypen.penup()mypen.goto(x, y)mypen.pendown()mypen.write(text, align="center", font=("Arial", size, "normal"))
接下來繪制3個(gè)功能鍵按鈕,我們先在主函數(shù)中定義好功能鍵方框的大小,位置和文字等信息,然后調(diào)用自定義函數(shù)draw_keys()繪制功能鍵按鈕。相關(guān)代碼如下:
#主函數(shù)部分draw_info(x0+300, y0+100, "移動(dòng)火柴算術(shù)游戲:左鍵點(diǎn)擊選擇的火柴棒或目的地,右鍵撤銷操作", 'black', 20, tt)key_w, key_h = 110, 35 #功能鍵方框大小key_pos = [(x0+100+i*(key_w+30), y0+80) for i in range(3)]key_text = ("隨機(jī)出題", "輸入題目", "顯示答案")for i in range(len(key_pos)): #繪制功能鍵按鈕draw_keys(key_pos[i][0], key_pos[i][1], key_w, key_h, key_text[i], tt)#自定義函數(shù)'''函數(shù)功能:根據(jù)輸入的坐標(biāo)和大小,繪制方框和文字函數(shù)名:draw_keys(x, y, w, h, text, mypen)參數(shù)表:x, y -- 方框左上角坐標(biāo);w, h -- 方框的寬和高;text -- 方框中文字;mypen -- 繪制方框和文字所需要的畫筆返回值:沒有返回值。'''def draw_keys(x, y, w, h, text, mypen):mypen.penup()mypen.goto(x, y)mypen.down()mypen.seth(0)for i in range(2):mypen.fd(w)mypen.right(90)mypen.fd(h)mypen.right(90)mypen.penup()mypen.goto(x+w/2, y-h*5/6)mypen.down()mypen.write(text, align="center", font=("黑體", 18, "normal"))
?
turtle使用onclick(fun, btn=1, add=None)函數(shù)來響應(yīng)單擊鼠標(biāo)事件,其中fun是該事件綁定的函數(shù)名,調(diào)用fun函數(shù)時(shí),系統(tǒng)自動(dòng)傳入兩個(gè)參數(shù)表示在畫布上點(diǎn)擊的坐標(biāo);btn表示鼠標(biāo)按鈕編號(hào),默認(rèn)值為1(鼠標(biāo)左鍵),也可以設(shè)置為2(鼠標(biāo)中間滾輪)或3(鼠標(biāo)右鍵)。
我們?cè)谥骱瘮?shù)中添加如下代碼,即可使用屏幕事件:
tt.onscreenclick(play_game, 1)????? #左鍵單擊(選擇來源地或目的地)
tt.onscreenclick(cancel_game, 3)??? #右鍵單擊(撤銷剛才的移動(dòng)操作)
?
本游戲的重頭戲是響應(yīng)單擊鼠標(biāo)左鍵事件。它根據(jù)玩家點(diǎn)擊的不同位置做出不同反應(yīng)。有效點(diǎn)擊位置有兩處:功能鍵方框和構(gòu)成算式的火柴棒。
已知3個(gè)功能鍵方框的坐標(biāo)已經(jīng)存儲(chǔ)在列表key_pos中,所有火柴棒的坐標(biāo)已經(jīng)存儲(chǔ)在列表pos_map中,我們只需判斷鼠標(biāo)在畫布上點(diǎn)擊坐標(biāo)的所處范圍,就可以判斷玩家點(diǎn)擊的是哪個(gè)功能鍵或火柴棒。
(1)???點(diǎn)擊“隨機(jī)出題”按鈕
if key_pos[0][0] < x < key_pos[0][0]+key_w and key_pos[0][1]-key_h < y < key_pos[0][1]: #點(diǎn)擊“隨機(jī)出題”按鈕from_num = to_num = -1 #火柴棒的來源地和目的地下標(biāo)stick_flag = [False for i in range(25)] #判斷某根火柴棒是否已繪制,共25根for p in stick_pens:p.clear()que_num = random.randint(0, len(ques)-1) #隨機(jī)生成題目編號(hào)draw_expression(x0, y0, 'green', ques[que_num]) #繪制算術(shù)表達(dá)式
代碼說明:程序用到了4個(gè)全局變量que_nu, from_num, to_num, stick_flag,每次出題時(shí)都先對(duì)它們進(jìn)行初始化處理,同時(shí)清除所有的火柴棒畫面,從題庫中隨機(jī)生成題目編號(hào),并繪制該題目算式。
(2)???點(diǎn)擊“輸入題目”按鈕
elif key_pos[1][0] < x < key_pos[1][0]+key_w and key_pos[1][1]-key_h < y < key_pos[1][1]: #點(diǎn)擊“輸入題目”按鈕from_num = to_num = -1 #火柴棒的來源地和目的地下標(biāo)stick_flag = [False for i in range(25)] #判斷某根火柴棒是否已繪制,共25根for p in stick_pens:p.clear()try:que = read_question() #從文本框手動(dòng)輸入題目,并檢查是否有解if que: #要預(yù)防玩家取消輸入的情形draw_expression(x0, y0, 'green', que) #繪制算術(shù)表達(dá)式que_num = ques.index(que) #在題庫中查找該算術(shù)式的編號(hào),若無解則拋出異常except ValueError:draw_expression(x0+200, y0-250, 'red', '無解') #若為無效算式,顯示無解
代碼說明:此段代碼的操作和“隨機(jī)出題”基本相同,只多了“從文本框手動(dòng)輸入題目,并檢查是否有解”的環(huán)節(jié)。程序使用內(nèi)置函數(shù)index()來查找算式在題庫中的編號(hào),若找不到則拋出異常,說明該算式無效。為避免因拋出異常而使程序中斷,需要使用try/except 語句來捕捉異常。
自定義read_question()用來處理從列表框輸入題目的操作,代碼如下:
'''函數(shù)功能:從列表框輸入原始算式函數(shù)名:read_question()參數(shù)表:無返回值:返回輸入的原始算式。'''def read_question():s = tt.textinput("輸入原始算式", "請(qǐng)輸入原始算式")que = []if s:for c in s:if c != ' ':que.append(c)????????return?''.join(que)
(3)???點(diǎn)擊“顯示答案”按鈕
elif key_pos[2][0] < x < key_pos[2][0]+key_w and key_pos[2][1]-key_h < y < key_pos[2][1]: #點(diǎn)擊“顯示答案”按鈕for i in range(len(anss[que_num])): #顯示所有可能的答案,每行顯示一個(gè)答案draw_expression(x0, y0-200*(i+1), 'red', anss[que_num][i])
代碼說明:此段代碼很簡單,只需根據(jù)題目編號(hào),顯示所有可能的答案即可。
(4)???點(diǎn)擊火柴棒區(qū)域
else: #點(diǎn)擊火柴棒區(qū)域stick_num = get_stick_num(x, y) #根據(jù)點(diǎn)擊位置坐標(biāo),獲取火柴棒的序號(hào)if stick_num >= 0: #點(diǎn)中了某根火柴棒if stick_flag[stick_num] and from_num == -1 and to_num == -1: #首次單擊,只能移動(dòng)已繪制的火柴棒,且只能移動(dòng)一次from_num = stick_num#繪制來源地火柴棒為紅色draw_rectangle(pos_map[stick_num][0]+x0,pos_map[stick_num][1]+y0,pos_map[stick_num][2]+x0,pos_map[stick_num][3]+y0,'red',stick_pens[stick_num])elif from_num != -1 and to_num == -1 and not stick_flag[stick_num]: #二次單擊,只能把火柴棒移動(dòng)到未繪制處stick_pens[from_num].clear()to_num = stick_numstick_flag[from_num] = Falsestick_flag[to_num] = True#繪制目的地火柴棒為綠色draw_rectangle(pos_map[stick_num][0]+x0,pos_map[stick_num][1]+y0,pos_map[stick_num][2]+x0,pos_map[stick_num][3]+y0,'green',stick_pens[stick_num])#輸出答案是否正確draw_info(x0+600, y0-100, show_answer(anss[que_num]), 'red', 60, stick_pens[25])
代碼說明:首先調(diào)用get_stick_num(x, y)函數(shù),根據(jù)點(diǎn)擊位置坐標(biāo),獲取火柴棒的序號(hào),若未選擇任何火柴棒則返回-1。參考代碼如下:
'''函數(shù)功能:根據(jù)鼠標(biāo)左鍵點(diǎn)擊的位置,獲取該位置火柴棒的序號(hào)函數(shù)名:get_stick_num(x, y)參數(shù)表:x, y -- 表示鼠標(biāo)在畫布上點(diǎn)擊的坐標(biāo)。返回值:返回該位置火柴棒在全局變量pos_map列表中的下標(biāo),若未選擇任何火柴棒則返回-1。'''def get_stick_num(x, y): #獲取火柴棒的序號(hào)for i, p in enumerate(pos_map):if p[0]+x0 <= x <= p[2]+x0 and p[3]+y0 <= y <= p[1]+y0:return ireturn -1
if stick_flag[stick_num] and from_num == -1 and to_num == -1用來判斷當(dāng)前被點(diǎn)中的火柴棒是否已經(jīng)被繪制?是否還沒有選中來源地和目的地火柴棒?當(dāng)結(jié)果均為真時(shí),表示是首次單擊來源地火柴棒。此時(shí)設(shè)置全局變量from_num = stick_num,并繪制被點(diǎn)中的火柴棒為紅色。
elif from_num != -1 and to_num == -1 and not stick_flag[stick_num]用來判斷是否屬于第二次單擊?即來源地火柴棒已被選中,但目的地火柴棒尚未選中的情形。因?yàn)榈诙螁螕魰r(shí),只能把火柴棒移動(dòng)到未繪制處,故還要判斷當(dāng)前被選中的位置是否還沒有繪制火柴棒。
如果是第二次單擊,則清除來源地火柴棒,設(shè)置全局變量to_num = stick_num,stick_flag[from_num] = False,stick_flag [to_num] = True,繪制目的地火柴棒為綠色,并輸出文字提示答案是否正確。
其中自定義函數(shù)show_answer()用來判斷答案是否正確并顯示結(jié)果。參考代碼如下:
'''函數(shù)功能:通過對(duì)比參考答案中火柴棒的分布情況,判斷玩家移動(dòng)火柴棒后組成算式是否為正確答案。其中全局變量stick_flag中存儲(chǔ)了玩家移動(dòng)火柴棒后組成算式中火柴棒的繪制情況;局部變量ans_stick_flag中存儲(chǔ)了正確答案組成算式中火柴棒的繪制情況。函數(shù)名:show_answer(ans)參數(shù)表:ans -- 當(dāng)前題目對(duì)應(yīng)的參考答案。返回值:若玩家答案與某個(gè)參考答案相同,則返回"正確",否則返回"錯(cuò)誤"。'''def show_answer(ans): #判斷答案是否正確并顯示結(jié)果for exp in ans: #可能有多個(gè)答案ans_stick_flag = [False for i in range(25)] #判斷某根火柴棒是否已繪制,共25根for i in (0, 1, 2):for j in digits[int(exp[i*2])]:ans_stick_flag[7*i+j] = Truefor i in range(21, 25):if i == 22 and exp[1] == '-': #減法沒有豎線continueans_stick_flag[i] = Trueif ans_stick_flag == stick_flag: #答案正確return "正確"????return?"錯(cuò)誤"
'''函數(shù)功能:當(dāng)已經(jīng)選擇或者移動(dòng)火柴棒后,在任意位置點(diǎn)擊右鍵均可撤銷相關(guān)操作函數(shù)名:cancel_game(x, y)參數(shù)表:x, y -- 表示鼠標(biāo)在畫布上點(diǎn)擊的坐標(biāo)。返回值:沒有返回值。'''def cancel_game(x, y):global from_num, to_num #火柴棒的來源地和目的地下標(biāo)global stick_flag #記錄火柴棒是否已經(jīng)繪制if from_num == -1: #沒有點(diǎn)擊火柴棒的來源地returnif to_num != -1: #已經(jīng)移動(dòng)火柴棒,撤銷移動(dòng)操作stick_flag[to_num] = Falsestick_flag[from_num] = Truestick_pens[to_num].clear()stick_pens[25].clear() #清除提示文字to_num = -1#還原已選擇火柴棒,并顯示其為紅色draw_rectangle(pos_map[from_num][0]+x0,pos_map[from_num][1]+y0,pos_map[from_num][2]+x0,pos_map[from_num][3]+y0,'red',stick_pens[from_num])else: #尚未移動(dòng)火柴棒,撤銷選擇操作,并還原已選擇火柴棒的顏色為綠色draw_rectangle(pos_map[from_num][0]+x0,pos_map[from_num][1]+y0,pos_map[from_num][2]+x0,pos_map[from_num][3]+y0,'green',stick_pens[from_num])from_num = -1
代碼說明:當(dāng)from_num的值為-1時(shí),表示還沒有選擇來源地火柴棒,則無需做任何撤銷操作;如果to_num != -1,表示已經(jīng)完成了第二次單擊(移動(dòng)火柴棒)操作,此時(shí)撤回移動(dòng)操作,回到第一次單擊(選中火柴棒)狀態(tài);若to_num == -1,表示僅完成第一次單擊(選中火柴棒)操作,此時(shí)撤銷操作會(huì)取消對(duì)來源地火柴棒的選擇,并還原其顏色為綠色。
至此,所有功能模塊和核心代碼都已經(jīng)介紹完畢,你只需把這些代碼組合在一起,就能愉快地享受移動(dòng)火柴棒游戲了。
1.本程序僅實(shí)現(xiàn)了移動(dòng)火柴棒游戲的基本功能,還有更多有趣的功能等待你來擴(kuò)展和開發(fā),例如增加計(jì)分功能,每答對(duì)1題記1分;又比如增加定時(shí)功能,限定時(shí)間內(nèi)未解題則自動(dòng)跳到下一題等。
2.在第二節(jié)課的課后練習(xí)中我請(qǐng)大家用另一種思路繪制七段管算術(shù)表達(dá)式,細(xì)心的朋友可能會(huì)發(fā)現(xiàn)視頻中的算式就是用這種方法繪制的,看上去更美觀些。

失之毫厘,謬以千里,繪圖方式的不一樣,會(huì)使得后面響應(yīng)鼠標(biāo)事件的代碼也大不相同。你能否畫出更具個(gè)性化的七段管數(shù)字,并完成整個(gè)游戲功能呢?
?
需要本文PPT、源代碼和課后練習(xí)答案的,可以加入“Python算法之旅”知識(shí)星球參與討論和下載文件,“Python算法之旅”知識(shí)星球匯集了數(shù)量眾多的同好,更多有趣的話題在這里討論,更多有用的資料在這里分享。
我們專注Python算法,感興趣就一起來!
相關(guān)優(yōu)秀文章:
