海龜繪圖案例分析之移動(dòng)火柴變等式游戲(2)
說在前面
移動(dòng)一根火柴棒使等式成立,是一種簡單有趣的智力游戲,只需具備簡單的算術(shù)知識(shí),就能參與游戲,老少皆宜,深受大家的喜愛。
上節(jié)課我和大家分享了創(chuàng)建題庫的方法,并花了很大力氣來介紹如何將結(jié)構(gòu)相同的多段代碼精簡成循環(huán)語句,里面用到的一些技巧非常有趣,希望能對大家有所啟發(fā)。
今天我們學(xué)習(xí)第二部分——繪制七段管算術(shù)表達(dá)式。
所謂七段管數(shù)字,就是用7個(gè)發(fā)光二極管來組成的阿拉伯?dāng)?shù)字。我們?yōu)檫@7個(gè)二極管按照從上到下、從左到右的順序依次編號(hào)為0-6。部分?jǐn)?shù)字所需二極管的編號(hào)如下圖所示:

根據(jù)七段管數(shù)字的形狀,我們可以創(chuàng)建字典digits來存儲(chǔ)各數(shù)字所需二極管的編號(hào),有digits ={0:(0,1,2,4,5,6),1:(2,5),2:(0,2,3,4,6),3:(0,2,3,5,6),4:(1,2,3,5),5:(0,1,3,5,6),6:(0,1,3,4,5,6),7:(0,2,5),8:(0,1,2,3,4,5,6),9:(0,1,2,3,5,6)}。
2.繪制單個(gè)二極管
使用turtle模塊繪制二極管的方法很多,最簡單的方法就是繪制一條線段。在本例中可以設(shè)置畫筆粗細(xì)等于二極管的寬度,繪制與二極管等長的線段。因?yàn)椴煌幪?hào)的二極管方向不一樣,需要考慮畫筆的朝向,故操作比想象的要復(fù)雜。
另一種思路是把每個(gè)二極管都看作是矩形,可以根據(jù)二極管的長度d和寬度w,按照編號(hào)順序,設(shè)置各二極管的左上角和右下角坐標(biāo)(假設(shè)數(shù)字的左上角為坐標(biāo)原點(diǎn)),并存儲(chǔ)到列表x_y中,有x_y =[(w*5/4,0,w*5/4+d,-w),(0,-w/2,w,-w/2-d),(d+w*3/2+1,-w/2,d+w*5/2+1,-w/2-d),(w*5/4,-w/4-d,w*5/4+d,-w/4-d-w),(0,-w-d,w,-w-d*2),(d+w*3/2+1,-w-d,d+w*5/2+1,-w-d*2),(w*5/4,-w/2-d*2,w*5/4+d,-w*3/2-d*2)]。
由此可以根據(jù)二極管的坐標(biāo)信息,繪制矩形并填充顏色(此外,確定各二極管的坐標(biāo)范圍,還為后面鼠標(biāo)定位提供了方便)。自定義函數(shù)如下:
'''函數(shù)功能:根據(jù)左上角和右下角坐標(biāo)繪制矩形并填充顏色函數(shù)名:draw_rectangle(x1, y1, x2, y2, c)參數(shù)表:x1, y1 -- 矩形的左上角坐標(biāo);x2, y2 -- 矩形的右下角坐標(biāo);c -- 畫筆顏色。返回值:沒有返回值。'''def draw_rectangle(x1, y1, x2, y2, c): #繪制某根二極管tt.color(c)tt.begin_fill()tt.penup()tt.goto(x1, y1)tt.pendown()tt.goto(x2, y1)tt.goto(x2, y2)tt.goto(x1, y2)tt.goto(x1, y1)tt.end_fill()
?? 有了字典digits和列表x_y,我們就可以調(diào)用draw_rectangle()函數(shù)在指定位置繪制單個(gè)七段管數(shù)字了。方法很簡單,就是遍歷digits[num],依次繪制組成數(shù)字num的每根二極管即可。參考代碼如下:
'''函數(shù)功能:根據(jù)給定的左上角坐標(biāo)、長度、寬度和畫筆顏色,利用字典digits繪制七段管數(shù)字num函數(shù)名:draw_num(x, y, d, w,c, num)參數(shù)表:x,y -- 七段管數(shù)字的左上角坐標(biāo);d,w -- 二極管的長度和寬度;c -- 畫筆顏色;num -- 要繪制的七段管數(shù)字。返回值:沒有返回值。'''def draw_num(x, y, d, w, c,num): #繪制七段管數(shù)字for i in digits[num]: #digits[num]包含了組成數(shù)字num的所有二極管編號(hào)draw_rectangle(x_y[i][0]+x,x_y[i][1]+y, x_y[i][2]+x, x_y[i][3]+y, c)
for i in range(10):draw_num(-500+i*(d+d),150, d, w, 'green', i)draw_num(-500+i*(d+d),300, d, w, 'red', i)
效果圖如下所示:

4.繪制七段管算術(shù)表達(dá)式
'''函數(shù)功能:根據(jù)給定的左上角坐標(biāo)、邊長和畫筆顏色,繪制運(yùn)算符op函數(shù)名:draw_operator(x, y, d,w, c, op)參數(shù)表:x,y -- 七段管數(shù)字的左上角坐標(biāo);d,w -- 二極管的長度和寬度;c-- 畫筆顏色;op -- 要繪制的運(yùn)算符。返回值:沒有返回值。'''def draw_operator(x, y, d, w,c, op): #繪制運(yùn)算符if op == '-':draw_rectangle(x+w, y-w/4-d, x+w+d,y-w/4-d-w, c)elif op == '+':draw_rectangle(x+w, y-w/4-d, x+w+d,y-w/4-d-w, c)draw_rectangle(x+w/2+d/2, y-w*3/4-d/2,x+w/2+d/2+w, y-w*3/4-d/2-d, c)elif op == '=':draw_rectangle(x+w, y+w*5/4-d, x+w+d,y+w/4-d, c)draw_rectangle(x+w, y-w*7/4-d, x+w+d,y-w*11/4-d, c)
exp = '3+6-5=4'for i, ch in enumerate(exp):if ch in '+-=':draw_operator(-500+i*(d+d), 0, d, w, 'red', ch)else:ch = int(ch)draw_num(-500+i*(d+d), 0, d, w, 'red', ch)
效果圖如下所示:

5.程序升級(jí)?
上述程序能夠繪制七段管算術(shù)表達(dá)式,但還沒有實(shí)現(xiàn)游戲中“移動(dòng)火柴棒”功能,因?yàn)樗挥幸恢М嫻P。要想獨(dú)立繪制或清除每一根火柴棒(二極管),我們需要為每根火柴棒(二極管)都創(chuàng)建一支畫筆。
人生沒有白走的路,每一步都算數(shù)。每一個(gè)項(xiàng)目的編程經(jīng)驗(yàn)都可以遷移到新項(xiàng)目中。有了前面的鋪墊,我們可以順利地完成“火柴棒算式”的繪制工作。
因?yàn)椤盎鸩癜羲闶健钡母袷绞枪潭ǖ?,形如a+b=c或a-b=c。所以我們可以事先設(shè)置好組成算式的各火柴棒的左上角和右下角坐標(biāo),以便今后繪圖。
前面我們介紹了存儲(chǔ)單個(gè)七段管數(shù)字信息的列表x_y,我們可以在它的基礎(chǔ)上依次存儲(chǔ)數(shù)字a,b,c的全部二極管坐標(biāo)信息,再把構(gòu)成運(yùn)算符的二極管坐標(biāo)信息也存儲(chǔ)起來。每個(gè)數(shù)字和運(yùn)算符之間距離為2*d,在數(shù)字a的基礎(chǔ)上分別向右平移4*d和8*d,即可得到數(shù)字b和c的二極管坐標(biāo)信息。
由此我們可以得到組成“火柴棒算式”的總共25根火柴棒(二極管)的坐標(biāo)信息(即矩形的左上角和右下角坐標(biāo))。參考代碼如下:
'''函數(shù)功能:設(shè)置組成算式的各火柴棒左上角和右下角坐標(biāo),以便今后繪圖函數(shù)名:get_pos(x, y, d, w)參數(shù)表:d,w – 二極管的長度和寬度。返回值:返回存儲(chǔ)了各火柴棒左上角和右下角坐標(biāo)的列表。'''def get_pos(d, w): #設(shè)置組成算式的各火柴棒左上角和右下角坐標(biāo)#根據(jù)七段管的長度和寬度,設(shè)置各二極管的左上角和右下角坐標(biāo)(以數(shù)字的左上角為坐標(biāo)原點(diǎn))x_y =[(w*5/4,0,w*5/4+d,-w),(0,-w/2,w,-w/2-d),(d+w*3/2+1,-w/2,d+w*5/2+1,-w/2-d),(w*5/4,-w/4-d,w*5/4+d,-w/4-d-w),(0,-w-d,w,-w-d*2),(d+w*3/2+1,-w-d,d+w*5/2+1,-w-d*2),(w*5/4,-w/2-d*2,w*5/4+d,-w*3/2-d*2)]for i in (2, 4): #添加數(shù)字b和c各條邊的左上角和右下角坐標(biāo),向右平移2*dfor j in range(7):x_y.append((x_y[j][0]+i*2*d, x_y[j][1], x_y[j][2]+i*2*d, x_y[j][3]))#組成運(yùn)算符的各火柴棒左上角和右下角坐標(biāo)(減號(hào)只有一條橫線,加號(hào)再加一條豎線)x_y.append((w+2*d,-w/4-d, w+d+2*d, -w/4-d-w))x_y.append((w/2+d/2+2*d, -w*3/4-d/2, w/2+d/2+w+2*d, -w*3/4-d/2-d))#組成等號(hào)的各火柴棒左上角和右下角坐標(biāo)(等號(hào)有2條橫線)x_y.append((w+6*d,w*5/4-d, w+d+6*d, w/4-d))x_y.append((w+6*d,-w*7/4-d, w+d+6*d, -w*11/4-d))return x_y
繪制單根火柴棒的函數(shù)與前面繪制單個(gè)二極管的函數(shù)幾乎一模一樣,唯一的區(qū)別就是:每根火柴棒都有單獨(dú)的畫筆,所以需要提供一個(gè)畫筆對象。代碼如下:
'''函數(shù)功能:根據(jù)左上角和右下角坐標(biāo)繪制矩形并填充顏色函數(shù)名:draw_rectangle(x1, y1, x2, y2, c, mypen)參數(shù)表:x1, y1 -- 矩形的左上角坐標(biāo);x2, y2 -- 矩形的右下角坐標(biāo);c -- 畫筆顏色;mypen -- 該條邊對應(yīng)的畫筆對象。返回值:沒有返回值。'''def draw_rectangle(x1, y1, x2, y2, c, mypen): #繪制七段管的某條邊mypen.color(c)mypen.begin_fill()mypen.penup()mypen.goto(x1, y1)mypen.pendown()mypen.goto(x2, y1)mypen.goto(x2, y2)mypen.goto(x1, y2)mypen.goto(x1, y1)mypen.end_fill()
8.繪制操作數(shù)?
繪制操作數(shù)就相當(dāng)于前面所講的繪制單個(gè)七段管數(shù)字,區(qū)別在于a,b,c三個(gè)操作數(shù)的位置已經(jīng)定下來了,我們可以根據(jù)其在算式字符串中的下標(biāo)來找到構(gòu)成它的火柴棒。
此外,已經(jīng)存在的火柴棒不能被重復(fù)繪制,所以我們需要定義一個(gè)全局變量stick_flag,它是一個(gè)列表,存儲(chǔ)了各火柴棒是否已經(jīng)被繪制的信息——該列表記錄的信息是移動(dòng)火柴棒的憑據(jù),避免重復(fù)繪制火柴棒或移動(dòng)根本不存在的火柴棒。參考代碼如下:
'''函數(shù)功能:根據(jù)某個(gè)操作數(shù)的起點(diǎn)坐標(biāo)和它在算式字符串中的下標(biāo)等信息,利用字典digits繪制該操作數(shù)函數(shù)名:draw_num(x, y, i, num, d, w, c)參數(shù)表:x, y -- 該操作數(shù)的左上角坐標(biāo);i -- 該操作數(shù)在算式字符串中的下標(biāo);num -- 該操作數(shù)的值;d -- 七段管的邊長,即火柴棒的長度;w,c -- 畫筆粗細(xì)和顏色。返回值:沒有返回值,全局變量stick_flag記錄用到了哪些火柴棒。'''def draw_num(x, y, i, num, d, w, c): #繪制某個(gè)操作數(shù)global stick_flag #記錄火柴棒是否已經(jīng)被繪制for j in digits[num]:#digits[num]包含了組成數(shù)字num的所有火柴棒編號(hào)stick_flag[7*i+j]= True #編號(hào)為7*i+j的火柴棒已經(jīng)被繪制draw_rectangle(pos_map[7*i+j][0]+x,pos_map[7*i+j][1]+y,pos_map[7*i+j][2]+x,pos_map[7*i+j][3]+y,c,stick_pens[7*i+j])
我們可以先調(diào)用函數(shù)get_pos()獲取組成“火柴棒算式”的總共25根火柴棒的坐標(biāo)信息,并依次存儲(chǔ)到列表pos_map中。然后根據(jù)各操作數(shù)和運(yùn)算符在列表pos_map中的下標(biāo),依次繪制3個(gè)操作數(shù)、運(yùn)算符和等號(hào)。代碼如下:
'''函數(shù)功能:根據(jù)算式的起點(diǎn)坐標(biāo)和畫筆顏色,繪制算式exp函數(shù)名:draw_expression(x, y, c, exp)參數(shù)表:x, y -- 該算式的左上角坐標(biāo);c -- 畫筆顏色;exp -- 表示算式的字符串,例如'a+b=c'。返回值:沒有返回值,全局變量stick_flag記錄用到了哪些火柴棒。'''def draw_expression(x, y, c, exp): #繪制算式global stick_flag #記錄火柴棒是否已經(jīng)被繪制for i in (0, 1, 2): #先依次繪制3個(gè)操作數(shù)draw_num(x, y, i,int(exp[i*2]), d, w, c)for i in range(21,25): #再依次繪制運(yùn)算符和等號(hào)if i == 22 andexp[1] != '+':continuestick_flag[i] =True #編號(hào)為i的火柴棒已經(jīng)被繪制draw_rectangle(pos_map[i][0]+x,pos_map[i][1]+y,pos_map[i][2]+x,pos_map[i][3]+y,c,stick_pens[i])
有了這些自定義函數(shù),我們就可以在主函數(shù)中設(shè)置全局變量的初始值,并調(diào)用函數(shù)繪制算式了。主函數(shù)部分代碼如下:
import turtle as ttimport randomtt.TurtleScreen._RUNNING =True # 啟動(dòng)繪圖,在IDE中運(yùn)行加這句可避免報(bào)錯(cuò)tt.speed(0)tt.delay(0)#組成七段管數(shù)字所需管子的編號(hào)(按照從上到下、從左到右順序)digits ={0:(0,1,2,4,5,6),1:(2,5),2:(0,2,3,4,6),3:(0,2,3,5,6),4:(1,2,3,5),5:(0,1,3,5,6),6:(0,1,3,4,5,6),7:(0,2,5),8:(0,1,2,3,4,5,6),9:(0,1,2,3,5,6)}x0, y0 = -300, 250 #算式左上角坐標(biāo)d, w = 50, 10 #七段管的長度和寬度pos_map = get_pos(d, w) #存儲(chǔ)組成算術(shù)式的各火柴棒左上角和右下角坐標(biāo)stick_flag = [False for i inrange(25)] #判斷某根火柴棒是否已繪制,共25根火柴棒stick_pens = [tt.Pen() for iin range(26)] #為每一跟火柴棒設(shè)置一支畫筆,共25根,再加一支寫無解的畫筆for p in stick_pens:p.ht()#隱藏筆頭p.pensize(1)exp = '3+2=5'draw_expression(x0, y0,'red', exp) #繪制算式tt.done()
程序輸出效果圖如下所示:

除了將每個(gè)二極管都看作是矩形,我在文中還提到了設(shè)置畫筆粗細(xì)等于二極管的寬度,繪制與二極管等長的線段來表示二極管的方法。請根據(jù)此思路編寫繪制七段管算術(shù)表達(dá)式的程序。參考效果圖如下:

需要本文PPT、源代碼和課后練習(xí)答案的,可以加入“Python算法之旅”知識(shí)星球參與討論和下載文件,“Python算法之旅”知識(shí)星球匯集了數(shù)量眾多的同好,更多有趣的話題在這里討論,更多有用的資料在這里分享。
我們專注Python算法,感興趣就一起來!
相關(guān)優(yōu)秀文章:
