海龟绘图案例分析之移动火柴变等式游戏(3)
说在前面
移动一根火柴棒使等式成立,是一种简单有趣的智力游戏,只需具备简单的算术知识,就能参与游戏,老少皆宜,深受大家的喜爱。
上两节课我们已经学习了创建题库和绘制七段管数字算式的方法,今天我们就把这两者组合起来,并学习第三部分——点击鼠标移动火柴棒。
本小游戏主要分为三个部分:创建题库,绘制游戏界面和响应屏幕事件。
各部分对应的自定义函数和主函数部分代码框架如下所示:
创建题库部分我已经在第一节课中做了详细介绍,直接把源代码复制过来就行了,绘制算式模块相关函数我们在第二节课也已经分析过了。接下来我将依次介绍绘制游戏界面和响应屏幕事件。
游戏界面主要包括标题和3个功能键按钮,点击“随机出题”按钮后,还会在按钮下方显示题目。
首先定义一个函数来输出文字信息,代码如下:
'''
函数功能:显示提示信息
函数名:draw_info(x, y, text, c, size, mypen)
参数表:x, y -- 显示信息位置;
text -- 显示信息内容;
c -- 画笔颜色;
size -- 字体大小;
mypen -- 当前画笔对象。
返回值:没有返回值。
'''
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个功能键按钮,我们先在主函数中定义好功能键方框的大小,位置和文字等信息,然后调用自定义函数draw_keys()绘制功能键按钮。相关代码如下:
#主函数部分
draw_info(x0+300, y0+100, "移动火柴算术游戏:左键点击选择的火柴棒或目的地,右键撤销操作", '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 = ("随机出题", "输入题目", "显示答案")
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)
#自定义函数
'''
函数功能:根据输入的坐标和大小,绘制方框和文字
函数名:draw_keys(x, y, w, h, text, mypen)
参数表:x, y -- 方框左上角坐标;
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)函数来响应单击鼠标事件,其中fun是该事件绑定的函数名,调用fun函数时,系统自动传入两个参数表示在画布上点击的坐标;btn表示鼠标按钮编号,默认值为1(鼠标左键),也可以设置为2(鼠标中间滚轮)或3(鼠标右键)。
我们在主函数中添加如下代码,即可使用屏幕事件:
tt.onscreenclick(play_game, 1) #左键单击(选择来源地或目的地)
tt.onscreenclick(cancel_game, 3) #右键单击(撤销刚才的移动操作)
本游戏的重头戏是响应单击鼠标左键事件。它根据玩家点击的不同位置做出不同反应。有效点击位置有两处:功能键方框和构成算式的火柴棒。
已知3个功能键方框的坐标已经存储在列表key_pos中,所有火柴棒的坐标已经存储在列表pos_map中,我们只需判断鼠标在画布上点击坐标的所处范围,就可以判断玩家点击的是哪个功能键或火柴棒。
(1) 点击“随机出题”按钮
if key_pos[0][0] < x < key_pos[0][0]+key_w and key_pos[0][1]-key_h < y < key_pos[0][1]: #点击“随机出题”按钮
from_num = to_num = -1 #火柴棒的来源地和目的地下标
stick_flag = [False for i in range(25)] #判断某根火柴棒是否已绘制,共25根
for p in stick_pens:
p.clear()
que_num = random.randint(0, len(ques)-1) #随机生成题目编号
draw_expression(x0, y0, 'green', ques[que_num]) #绘制算术表达式
代码说明:程序用到了4个全局变量que_nu, from_num, to_num, stick_flag,每次出题时都先对它们进行初始化处理,同时清除所有的火柴棒画面,从题库中随机生成题目编号,并绘制该题目算式。
(2) 点击“输入题目”按钮
elif key_pos[1][0] < x < key_pos[1][0]+key_w and key_pos[1][1]-key_h < y < key_pos[1][1]: #点击“输入题目”按钮
from_num = to_num = -1 #火柴棒的来源地和目的地下标
stick_flag = [False for i in range(25)] #判断某根火柴棒是否已绘制,共25根
for p in stick_pens:
p.clear()
try:
que = read_question() #从文本框手动输入题目,并检查是否有解
if que: #要预防玩家取消输入的情形
draw_expression(x0, y0, 'green', que) #绘制算术表达式
que_num = ques.index(que) #在题库中查找该算术式的编号,若无解则抛出异常
except ValueError:
draw_expression(x0+200, y0-250, 'red', '无解') #若为无效算式,显示无解
代码说明:此段代码的操作和“随机出题”基本相同,只多了“从文本框手动输入题目,并检查是否有解”的环节。程序使用内置函数index()来查找算式在题库中的编号,若找不到则抛出异常,说明该算式无效。为避免因抛出异常而使程序中断,需要使用try/except 语句来捕捉异常。
自定义read_question()用来处理从列表框输入题目的操作,代码如下:
'''
函数功能:从列表框输入原始算式
函数名:read_question()
参数表:无
返回值:返回输入的原始算式。
'''
def read_question():
s = tt.textinput("输入原始算式", "请输入原始算式")
que = []
if s:
for c in s:
if c != ' ':
que.append(c)
return ''.join(que)
(3) 点击“显示答案”按钮
elif key_pos[2][0] < x < key_pos[2][0]+key_w and key_pos[2][1]-key_h < y < key_pos[2][1]: #点击“显示答案”按钮
for i in range(len(anss[que_num])): #显示所有可能的答案,每行显示一个答案
draw_expression(x0, y0-200*(i+1), 'red', anss[que_num][i])
代码说明:此段代码很简单,只需根据题目编号,显示所有可能的答案即可。
(4) 点击火柴棒区域
else: #点击火柴棒区域
stick_num = get_stick_num(x, y) #根据点击位置坐标,获取火柴棒的序号
if stick_num >= 0: #点中了某根火柴棒
if stick_flag[stick_num] and from_num == -1 and to_num == -1: #首次单击,只能移动已绘制的火柴棒,且只能移动一次
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]: #二次单击,只能把火柴棒移动到未绘制处
stick_pens[from_num].clear()
to_num = stick_num
stick_flag[from_num] = False
stick_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])
代码说明:首先调用get_stick_num(x, y)函数,根据点击位置坐标,获取火柴棒的序号,若未选择任何火柴棒则返回-1。参考代码如下:
'''
函数功能:根据鼠标左键点击的位置,获取该位置火柴棒的序号
函数名:get_stick_num(x, y)
参数表:x, y -- 表示鼠标在画布上点击的坐标。
返回值:返回该位置火柴棒在全局变量pos_map列表中的下标,若未选择任何火柴棒则返回-1。
'''
def get_stick_num(x, y): #获取火柴棒的序号
for i, p in enumerate(pos_map):
if p[0]+x0 <= x <= p[2]+x0 and p[3]+y0 <= y <= p[1]+y0:
return i
return -1
if stick_flag[stick_num] and from_num == -1 and to_num == -1用来判断当前被点中的火柴棒是否已经被绘制?是否还没有选中来源地和目的地火柴棒?当结果均为真时,表示是首次单击来源地火柴棒。此时设置全局变量from_num = stick_num,并绘制被点中的火柴棒为红色。
elif from_num != -1 and to_num == -1 and not stick_flag[stick_num]用来判断是否属于第二次单击?即来源地火柴棒已被选中,但目的地火柴棒尚未选中的情形。因为第二次单击时,只能把火柴棒移动到未绘制处,故还要判断当前被选中的位置是否还没有绘制火柴棒。
如果是第二次单击,则清除来源地火柴棒,设置全局变量to_num = stick_num,stick_flag[from_num] = False,stick_flag [to_num] = True,绘制目的地火柴棒为绿色,并输出文字提示答案是否正确。
其中自定义函数show_answer()用来判断答案是否正确并显示结果。参考代码如下:
'''
函数功能:通过对比参考答案中火柴棒的分布情况,判断玩家移动火柴棒后组成算式是否为正确答案。
其中全局变量stick_flag中存储了玩家移动火柴棒后组成算式中火柴棒的绘制情况;
局部变量ans_stick_flag中存储了正确答案组成算式中火柴棒的绘制情况。
函数名:show_answer(ans)
参数表:ans -- 当前题目对应的参考答案。
返回值:若玩家答案与某个参考答案相同,则返回"正确",否则返回"错误"。
'''
def show_answer(ans): #判断答案是否正确并显示结果
for exp in ans: #可能有多个答案
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] = True
for i in range(21, 25):
if i == 22 and exp[1] == '-': #减法没有竖线
continue
ans_stick_flag[i] = True
if ans_stick_flag == stick_flag: #答案正确
return "正确"
return "错误"
'''
函数功能:当已经选择或者移动火柴棒后,在任意位置点击右键均可撤销相关操作
函数名:cancel_game(x, y)
参数表:x, y -- 表示鼠标在画布上点击的坐标。
返回值:没有返回值。
'''
def cancel_game(x, y):
global from_num, to_num #火柴棒的来源地和目的地下标
global stick_flag #记录火柴棒是否已经绘制
if from_num == -1: #没有点击火柴棒的来源地
return
if to_num != -1: #已经移动火柴棒,撤销移动操作
stick_flag[to_num] = False
stick_flag[from_num] = True
stick_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: #尚未移动火柴棒,撤销选择操作,并还原已选择火柴棒的颜色为绿色
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
代码说明:当from_num的值为-1时,表示还没有选择来源地火柴棒,则无需做任何撤销操作;如果to_num != -1,表示已经完成了第二次单击(移动火柴棒)操作,此时撤回移动操作,回到第一次单击(选中火柴棒)状态;若to_num == -1,表示仅完成第一次单击(选中火柴棒)操作,此时撤销操作会取消对来源地火柴棒的选择,并还原其颜色为绿色。
至此,所有功能模块和核心代码都已经介绍完毕,你只需把这些代码组合在一起,就能愉快地享受移动火柴棒游戏了。
1.本程序仅实现了移动火柴棒游戏的基本功能,还有更多有趣的功能等待你来扩展和开发,例如增加计分功能,每答对1题记1分;又比如增加定时功能,限定时间内未解题则自动跳到下一题等。
2.在第二节课的课后练习中我请大家用另一种思路绘制七段管算术表达式,细心的朋友可能会发现视频中的算式就是用这种方法绘制的,看上去更美观些。
失之毫厘,谬以千里,绘图方式的不一样,会使得后面响应鼠标事件的代码也大不相同。你能否画出更具个性化的七段管数字,并完成整个游戏功能呢?
需要本文PPT、源代码和课后练习答案的,可以加入“Python算法之旅”知识星球参与讨论和下载文件,“Python算法之旅”知识星球汇集了数量众多的同好,更多有趣的话题在这里讨论,更多有用的资料在这里分享。
我们专注Python算法,感兴趣就一起来!
相关优秀文章: