LyndonPyProj

编程实现一个有GUI的24点小程序

2019-12-06  本文已影响0人  放翁lcf

24点是指从去除大小王后的52张扑克牌中任取 4 张,通过「加、减、乘、除」四则运算得到 24。是一个历史悠久的趣味小游戏。

《数据化管理》书中在测试数据敏感度章节提到一个细节“每天上下班的路上,盯着公交车外看到的汽车尾部牌照玩24点”,去练运算能力。根据排列组合知识可以算出:在1~ 10的数字中任选4个,有C(13,4)=715种情况(因为数字可以重复,如[5,5,5,5],故不是直接从10个数中取4个的组合),从1~ 13中任选4个是C(16,4)=1820种情况,经过大佬们的枚举和推导,只考虑加减乘除,715种情况中,有566种有解,也就是79.16%的概率,而从1~13中选的1820种情况中是1362种情况下能算出24点,概率为74.83% [1]

给定序列算出24点

最近自己也在练24点的计算,需要随机生成4个数的组合,并且在需要有答案,看这题有哪些做法能算出24点,于是就打算用Python来实现生成4个随机数以及求给定序列的24点计算方法。可以选择在4个数之间的3个空格中枚举各种符号的情况,并且考虑括号,还有一种思路是“降数法”:4个数经过一步运算“降维”成3个数,再变成2个数,最后得到1个数,如果得到24说明这种组合成立。后一种需要的判断更少些,于是选择实现这一思路。

代码的大致流程如下:

降数法计算过程

得到一个序列的全排列的递归方法在之前的一个 Ann全排列的文章 有具体讲解,这里不赘述。

求24点计算方法的代码如下:

#枚举列表lst的全排列
def perm(lst): #input:list,[1,2,3,4]
    n=len(lst) 
    if n<=1: #终止条件1
        return lst
    elif n==2:
        return [[lst[0],lst[1]],[lst[1],lst[0]]] #终止条件2
    kk=[]
    for i in range(n):
        nlst=lst[0:i]+lst[i+1:] #除lst[i]外的元素
        c=perm(nlst) #对子序列进行递归
        ss=[]
        for j in c:
            sw=[lst[i]]
            sw.extend(j)
            ss.append(sw)
        kk.extend(ss) #注意是extend不是append
    return kk
def cal24(a): #24点计算
    lst=[[i,''] for i in a]
    d1=perm(lst)  #len==24
    ev=['+','-','*','/']
    res=[]
    for d in d1: #len(d)==4
        for e1 in ev: #24*4
            if e1=='/' and d[1][0]==0: #被除数为0
                continue
            r='({0}{1}{2})'.format(d[0][0],e1,d[1][0])
            k1=[[eval(r),r],d[2],d[3]]  #k1=[eval(),d[2],d[3]]  k1.extend(d[2:])
            d2=perm(k1) #len(k1)==3  len(d2)==A(3,2)=6
            for d3 in d2: #len(d3)==3
                for e2 in ev:
                    if e2=='/' and d3[1][0]==0: #被除数为0
                        continue
                    r1='{0}{1}{2}'.format(d3[0][0],e2,d3[1][0])
                    y0=d3[0][0] if d3[0][1]=='' else d3[0][1]
                    y1=d3[1][0] if d3[1][1]=='' else d3[1][1]
                    r2='({0}{1}{2})'.format(y0,e2,y1)
                    k2=[[eval(r1),r2],d3[2]] # k2.extend(d3[2:]) 
                    d4=[[k2[0],k2[1]],[k2[1],k2[0]]]
                    for d5 in d4:
                        for e3 in ev:
                            if e3=='/' and d5[1][0]==0:
                                continue
                            k3=eval('{0}{1}{2}'.format(d5[0][0],e3,d5[1][0]))
                            if abs(k3-24)<1e-6:
                                y0=d5[0][0] if d5[0][1]=='' else d5[0][1]
                                y1=d5[1][0] if d5[1][1]=='' else d5[1][1]
                                rss='({0}{1}{2})'.format(y0,e3,y1)
                                k4=eval(rss)
                                if abs(k4-24)<1e-6:
                                    res.append(rss)
    return list(set(res)) #初步去重

我们拿几个实例来进行测试,输入结果如下:

几个实例的结果

这种实现还是有些粗暴,没有很好地进行各种情况的去重,例如2×7+6+4和2×7+4+6是一种情况,对交换律和括号的去重实现可以参考 如何不重复地枚举 24 点算式?(上) - 王赟 Maigo[2]

给24点小程序加上GUI

基于上面写的代码我们可以求任意4个数算24的所有情况,加上随机数生成平时就不缺24点的练习了,为了更好用,我们再加上GUI。为了兼容性,这里选择用内置的tkinter去实现GUI。

整体流程如下:

导入tk库,创建主窗体->添加控件->处理交互->进入主事件循环

交互的逻辑还是“降数法”的思路。

整体的界面如下图:

image

代码比较长,主要分为了生成各种按钮并设置坐标放在合适的位置,编写按钮按下的回调函数两个部分。部分代码如下:

root=tk.Tk()
root.geometry('280x320+400+100') #大小和位置  widthxheight+x+y
root.title('cal 24')
ctv=tk.StringVar(root,'')
btnUs=tk.IntVar(root,0)
cur=[]
result=[]
if result==[]:
    for _ in range(4):
        cur.append(random.randint(0,10))
cur.append('') #对应各个按钮当前值
scur=cur.copy() #重来 用
stk=[['',''],'',['',''],'']  #操作符点击
itv=tk.StringVar(root,'---')
infov=tk.Label(root,textvariable=itv) #显示信息用 
infov.place(x=170,y=5,width=120,height=20)

stk[3]=tk.Button(root,text='').cget("background")  #默认按钮背景色 linux: #d9d9d9 win:SystemButtonFace
#回调函数
def btnClick(btn,bt=''): #btn:按下的按钮   bt:所按下按钮的标识,主要是数值键用
    global cur,stk,scur,result
    ith=itv.get()
    btnus=btnUs.get()
    uop=[i for i in range(15)] #[0,14]
    opw=['+','-','*','/']
    if btn=='--':return
    if btn in uop: #按的是数值类型的键
        btnn=cur[bt-1]
        itv.set('{0}'.format(btnn))
        if stk[0][0]=='': #第一次按到数值键
            stk[0]=[btnn,bt]  #or stk[0][0]=btnn;stk[0][1]=bt
        elif stk[1]=='':#没有按过符号键
            if stk[0][0] !='':#如两次点到数值键
                stk[0]=[btnn,bt]
        elif stk[1]!='': #关键 完成了 a+b的输入
            stk[2]=[btnn,bt]
            btnus+=1 #在这个if条件下会合并两个按钮为一个,用掉一个按钮
            vss='{0}{1}{2}'.format(stk[0][0],stk[1],stk[2][0]) #a+b

            cur[4]='({0})'.format(vss)
            #暂时不好区分是cur[4],stk[1],stk[2][0] 还是 stk[0][0],stk[1],cur[4]
            v=eval(vss)
            itv.set(vss)
            ccv=float("%.3f" %v)
            if abs(v-ccv)<1e-6: setVBtnval(v,bt)
            else: setVBtnval(ccv,bt)
            setVBtnCol('#808080',stk[0][1]) #“失效”一个按钮
            setVBtnval('--',stk[0][1])
            stk[0]=[v,bt]
            stk[1]='' #置空后两步操作,第一步更新为v的值,以方便实现a*b+c (a+b)*c
            stk[2]=['','']
            if abs(v-24)<1e-6:
                if btnus==3: #用掉三个,结果正确,到达endgame
                    messagebox.showinfo(str(scur[:4]),'恭喜你计算正确!')
    elif btn in opw: #操作符,更新stk[1]
        if stk[0][0]=='':
            itv.set('操作符前没有数值')
            return #无效  操作符前没有数值
        elif stk[1] in opw: #覆盖上一步点的操作符
            stk[1]=btn
        elif stk[1]=='': #当前循环还没有输入过运算符
            stk[1]=btn
    elif btn=='C': #清空操作重来
        itv.set('--')
        cur=scur.copy()
        updateVBtn(cur) #更新数值按钮上的值
        resetVBtnColor(stk[3]) #重设按钮的背景色
        stk=resetStk(stk) #重设stk的值
        btnus=0 #按钮使用数重设为0
    elif btn=='Next': #下一题
        ch=[]
        for i in range(150):
            ch=[]
            for _ in range(4):
                ch.append(random.randint(0,10))
            result=cal24(ch)
            if result!=[]:
                if len(result)>9: #只取前10个答案
                    result=result[:9]
                break
        if ch==[]:
            for i in range(4):
                cur[i]=random.randint(0,10)
        else:
            for i in range(4):
                cur[i]=ch[i]
        cur[4]=''
        updateVBtn(cur)
        resetVBtnColor(stk[3])
        stk=resetStk(stk)
        scur=cur.copy()
        itv.set('--')
        btnus=0
    btnUs.set(btnus)

def showAnswer(): #用消息框展示当前题目的答案
    global result,cur
    rss='\n'.join([str(i) for i in result])
    messagebox.showinfo(str(cur),rss)

btn1=tk.Button(root,text=str(cur[0]),command=lambda x=cur[0]:btnClick(x,1))
btn1.place(x=0,y=10,width=90,height=90)
btn2=tk.Button(root,text=str(cur[1]),command=lambda x=cur[1]:btnClick(x,2))
btn2.place(x=90,y=10,width=90,height=90)
btn3=tk.Button(root,text=str(cur[2]),command=lambda x=cur[2]:btnClick(x,3))
btn3.place(x=0,y=100,width=90,height=90)
btn4=tk.Button(root,text=str(cur[3]),command=lambda x=cur[3]:btnClick(x,4))
btn4.place(x=90,y=100,width=90,height=90)

btn5=tk.Button(root,text='+',command=lambda :btnClick('+'))
btn5.place(x=0,y=200,width=40,height=20)
#……
btnClear=tk.Button(root,text='重来',command=lambda :btnClick('C'))
btnClear.place(x=0,y=250,width=60,height=20)
# ……
root.mainloop()

运行效果如下:

运行示例图

(另一个剪得更好的视频导gif超7兆,压缩效果不好,这个运行效果不够典型)

换个环境,Ubuntu下的效果:

Ubuntu下的运行效果

结合GUI会更容易理解上面的“降数法”和相应的代码。代码改一下可以变成命令行下的交互版本:


def cmdcal24():
    import random
    print('欢迎使用命令行版24点训练器!\n## 说明')
    q=''
    cur,res=[],[]
    while q!='q':
        if res==[]:
            res,cur=getOne()
            q=input('当前题目:{0}\n输入您的答案:'.format(str(cur)))
        elif q=='a':
            print(res)
            res,cur=getOne()
            q=input('当前题目:{0}\n输入您的答案:'.format(str(cur)))
        else:
            try:
                c=re.compile(r'\d+').findall(q)
                if len(c)!=4:
                    q=input('式子有问题,请检查后重新输入\n')
                else:
                    cr=[str(i) for i in cur]
                    if cmptlst(c,cr):
                        c=eval(q)
                        if abs(c-24)<1e-6:
                            print('计算正确!')
                            res,cur=getOne()
                            q=input('当前题目:{0}\n输入您的答案:'.format(str(cur)))
            except Exception as e:
                print(e)
                q=input('输入您的答案:'.format(str(cur)))

示例效果如下:

在cmd下运行脚本的效果

导出24点GUI脚本为exe程序

最后GUI版的脚本可以导出为exe文件,其他人也可以方便的使用,通过pyindatller可以快速打包py脚本为exe文件。

image

用pyinstaller打包成exe

image

Python打包为exe普遍文件会比较大(C#在这方面还是更有优势),我这边导出的结果是8.3MB,可以接受,用内置库的好处。写小型程序用tkinter是够用的。

文中代码可复制cal24withGUI[3]的github链接,代码持续更新。

References

[1] 为什么算数纸牌游戏是计算 24 点而不是别的数?- 曾加的回答: https://www.zhihu.com/question/22381727/answer/28821827
[2] 如何不重复地枚举 24 点算式?(上) - 王赟 Maigo: https://zhuanlan.zhihu.com/p/33998387

[3]cal24withGUI: https://github.com/QLWeilcf/cal24withGUI**

上一篇下一篇

猜你喜欢

热点阅读