前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编程实现一个有GUI的24点游戏

编程实现一个有GUI的24点游戏

作者头像
蛰虫始航
发布2019-12-12 16:39:09
1K0
发布2019-12-12 16:39:09
举报
文章被收录于专栏:蛰虫始航蛰虫始航

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点计算方法的代码如下:

代码语言:javascript
复制
#枚举列表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库,创建主窗体->添加控件->处理交互->进入主事件循环

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

整体的界面如下图:

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

代码语言:javascript
复制
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下的效果:

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

代码语言:javascript
复制
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)))

示例效果如下:

导出24点GUI脚本为exe程序

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

用pyinstaller打包成exe

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

公众号后台回复 24点 可下载文中代码,代码持续更新。

cal24withGUI[3]

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 蛰虫始航 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 给定序列算出24点
  • 给24点小程序加上GUI
  • 导出24点GUI脚本为exe程序
  • References
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档