美文网首页LyndonPyProj
编程实现一个有GUI的24点小程序

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

作者: 放翁lcf | 来源:发表于2019-12-06 00:53 被阅读0次

    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说明这种组合成立。后一种需要的判断更少些,于是选择实现这一思路。

    代码的大致流程如下:

    • 1),对给定的4个数进行排列,得到A(4,4)=4!=24种排列,对这24种情况执行:
    • 2),前2个数实现第一步计算,合并成1个数,生成一个3个数的新序列;
    • 3),对这3个数做排列,同样前2个做四则运算,3个数合并成2个;
    • 4),最后两个数的排列为[a,b]和[b,a],分别做加减乘除运算,变成一个数;
    • 5),如果最后生成的数是24,则记录这种计算方式;否则继续对下一个排列重复上面2~4。
    降数法计算过程

    得到一个序列的全排列的递归方法在之前的一个 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**

    相关文章

      网友评论

        本文标题:编程实现一个有GUI的24点小程序

        本文链接:https://www.haomeiwen.com/subject/bdhsgctx.html