Python写个小游戏:看图猜成语(下)



前言

大家好,上篇我们把游戏的样子已经搭起来了,今天我们就继续未完成的内容,用代码实现游戏的功能,废话不多说,让我们开始吧。

上篇 —— 游戏界面的搭建
下篇 —— 后台程序的实现


看图猜成语

1. 玩法简介

上篇中已经介绍过玩法,这里简单带过,相信大家一看就懂。

游戏截图:
在这里插入图片描述

2. 游戏流程

上篇中我们已经画了一个流程图:

No
Yes
Yes
No
游戏开始
画面展示
玩家选择汉字组成成语
判断玩家输入是否正确
清空玩家的选择
恭喜,是否进入下一关
重新选词
游戏结束
玩家选择提示

这里我们再回顾下,可以帮助我们了解到需要创建哪些自定义函数。在上篇中可以说我们已经完成了“画面展示”的部分,唯一需要对其进行补充的,是如何随机显示成语图片,已经玩家回答正确后,切换至下一张。

此外,我们还需要完成以下功能的程序或自定义函数:

  1. 创建成语库,并随机抽取单个成语
  2. 判断玩家选择是否正确
  3. 清空玩家选择
  4. 提示一个汉字

让我们逐个来讲解。

3. 代码实现

1). 创建成语库及初始化

问哥从网上下载了444个看图猜成语的图片,上传在这里,或者也可以私信问哥领取。

这些图片有三个特点:

  1. 大小一致,都是120x120
  2. 都是PNG图片
  3. 都是以各自所代表的成语命名

在这里插入图片描述

前两个特点方便我们在程序里展示,不用再写代码去调整图片大小的位置。第三个特点更是方便我们创建成语库,我们只需要把所有成语图片放在文件夹images下,再提取该文件夹下所有文件的名称(格式均为xxxx.png),然后选取前4个字符就是我们的成语库了。调用的时候,我们只要先确定好成语,再在成语后面加上.png就是对应的图片了。

OS内置模块

提取文件夹下所有文件的名称,需要用到Python的另一个内置模块os。顾名思义,这个模块就是和操作系统(operation system)相关的。用起来也很简单:

import os
filenames = os.listdir(r'images\words')

只需要使用os模块里的listdir方法,就可以把文件夹下所有的文件名都提取出来,返回一个列表。(因为问哥把背景图片也放在了images文件夹下,所以为了省事,又在下面创建了一个words文件夹专门用来存放成语图片。)

得到这个列表后,我们就可以使用列表切片操作,提取每个字符串的前四个字符,组成成语列表了。

word_list = [i[:4] for i in filenames]

当然,我们希望每次开始玩游戏的时候,成语显示都是随机的,所以我们需要使用前面学到过的random.shuffle方法把这些列表随机打散,就像洗牌一样,然后每次从牌堆顶或底抽取一张,成为我们让玩家猜的成语。

import random
random.shuffle(word_list)

这样我们就创建好了成语库,也完成了初始化(洗牌)。

2). 随机抽取成语

考虑到抽取成语的动作是在每个关卡都要操作的,所以还是自定义一个函数来实现比较方便,这样可以省去重复书写的代码。这里问哥为这个函数取名create_random_word()

随机选词

我们先自定义一个全局变量word,用来保存每个关卡电脑抽取的正确成语。随着游戏进行,我们需要在自定义函数里不断地改变这个变量的值(每次抽取的成语都不一样),所以最好是在函数内部使用global关键字把它声明为全局变量,免去传参的烦恼。

此外,因为在初始化的过程中,我们已经使用shuffle方法把成语库的顺序打乱,所以在抽取成语的时候我们就不需要再使用随机方法,而是直接从成语库(牌堆)的顶或底取一张就可以。问哥使用的是列表的pop()方法,也就是从列表的末端(牌堆的底部)抽取一张,同时成语列表的元素减一。代码实现如下:

word=''
def create_random_word():
    global word
    word = word_list.pop()

现在我们就可以把随机选择的成语所对应的图片绘制到Canvas画布上了。

上篇中,我们是静态地定义了图片,回顾一下:

img = tk.PhotoImage(file=f"images\words\一帆风顺.png")
cv_word = cv.create_image(150,120,image = img)
绘制图片

现在我们有了自定义函数,就要把这两句代码移动到自定义函数中去。但是需要注意的是,绘制在Canvas画布上的img图片变量需要带到主窗口的循环中(mainloop),如果移动到自定义函数中变成局部变量的话,一旦程序运行离开自定义函数,这个变量就消失了,图片也就为没有了。所以,我们需要把img也声明成全局变量。

可以直接在global关键字里一起声明,并用逗号分隔开。

word=''
def create_random_word():
    global word, img
    word = word_list.pop()
    img = tk.PhotoImage(file=f"images\words\{word}.png")
    cv.create_image(150,120,image = img)

现在我们可以把这个函数在最后调用,(注意,要放在cv.pack()的前面,不然成语图片画不上去)

create_random_word()
cv.pack() # 在主窗口装载画布
root.mainloop() # 主窗口循环展示

检查看看效果:
在这里插入图片描述

准备汉字库

图片是有了,而且还有一个全局变量word,用来代表图片所对应的正确成语。但是我们还需要为玩家准备一个可供选择的汉字库。这样玩家将可以从中选取正确的汉字组成成语。

我们定义一个局部列表变量lib来代表这个字库。同时我们必须要确保字库里有正确成语word,还要有另外4个随机的成语,当做干扰答案。所以我们可以使用random.sample方法从剩下的成语库里随机算出4个成语来,然后用字符串拼接的方法(“join”和“+”)和正确成语word组成一个20个汉字的字符串。接着再把这20个汉字的字符串转成列表装进lib。最后再使用random.shuffle方法把这个列表打乱,以保证我们最后显示的字库是乱序的。代码如下:

def create_random_word():
    lib = list(''.join(random.sample(word_list,4))+word)
    random.shuffle(lib)

但是这里有个小问题,我们是从“牌堆”(word_list)中随机选取另外4个成语,随着游戏的进行,“牌堆”必定会越来越少。当只剩下少于4个成语的时候,这种取样方式必定会报错,所以我们必须想办法保证即使是最后一个成语,也能找到另外4个干扰成语组成lib。

如果把成语词库比作牌堆,我们必然会有另一个牌堆列表——弃牌堆,所以我们可以考虑利用弃牌堆。定义一个新的空列表word_copy,用来收集每次用完后的成语word。只要word不为空(游戏刚开始时),就把这个词加入“弃牌堆”列表word_copy。然后在随机选取的时候,我们可以从“弃牌堆”和“牌堆”这两个列表里选,问题就可以解决了。修改后代码如下:

word=''
word_copy = []
def create_random_word():
    global word, img, word_copy
    if word: word_copy.append(word)
    word = word_list.pop()
    lib = list(''.join(random.sample(word_copy+word_list,4))+word)

这样我们就准备好了20个(5个成语)乱序的汉字,现在只要把它们“写”到按钮上就好了。

按钮上的汉字

还记得上篇我们已经准备好了20个光秃秃的按钮吗?

for i in range(4):
    for j in range(5):
        btn = tk.Button(root, font =('方正楷体简体',11),width=2,relief='flat',bg='lightyellow')
        btn_window = cv.create_window(300+40*i, 75+35*j, window=btn)

现在我们可以把lib里的汉字写在按钮上了,但是当时为了方便布置按钮,我们把所有的按钮都取名叫btn,现在我们想在每个按钮上写上不同汉字的时候,就必须知道每个按钮的名字了。所以我们定义一个按钮的列表,把所有按钮都放在列表里,这样通过列表索引就可以引用不同的按钮了。于是,这部分代码修改如下:

btn = []
for i in range(4):
    for j in range(5):
        btn.append(tk.Button(root, font =('方正楷体简体',11),width=2,relief='flat',bg='lightyellow'))
        btn_window = cv.create_window(300+40*i, 75+35*j, window=btn[i*5+j])

现在我么可以在create_random_word函数里在这20个按钮上写上汉字了,只要依次修改它们的text属性。所以最后create_random_word函数的代码如下:

word=''
word_copy=[]
def create_random_word():
    global word, img, word_copy
    if word: word_copy.append(word) # “弃牌堆”
    word = word_list.pop() # 抽取新的成语
    lib = list(''.join(random.sample(word_copy+word_list,4))+word)
    random.shuffle(lib) # 准备干扰字库
    for i in range(len(btn)):
        btn[i]['text'] = lib[i] # 在按钮上写汉字
    img = tk.PhotoImage(file=f"images\words\{word}.png")
    cv.create_image(150,120,image = img) # 把图片画在指定位置

运行看看效果:
在这里插入图片描述

3). 重新定义选字按钮

现在摆在我们面前的有两个问题:

  1. 怎样实现点击按钮,就能得到相对应的汉字;
  2. 怎样把玩家通过点击按钮得到的汉字显示在图片下面那四个正方形的格子里。

我们先来说第2个问题,因为这个比较好实现。

显示玩家选择的汉字

在上篇里,那四个正方形的格子其实就是普通的矩形,我们没法在里面写字。

for i in range(4):
    cv.create_rectangle(50*i+50,210,50*i+86,246,fill='ivory')

但是我们可以在它们上面写字。假设玩家选择的汉字组成的字符串变量是txt,那我们只要在相应的位置用画布的create_text方法创建4个文本就可以了。

txt = '接二连三'
for i in range(4):
    cv.create_text(50*i+68,228,fill='black', text=txt[i], font =('方正楷体简体',18,'bold'))

但是因为我们还要随时改变这四个文本,所以最好还是把它们也放在数组里。代码修改如下:

txt = '接二连三'
text=[]
for i in range(4):
    text.append(cv.create_text(50*i+68,228,fill='black',text=txt[i], font =('方正楷体简体',18,'bold')))

看看效果:
在这里插入图片描述
接下来我们只要通过按钮去改变变量txt的值就好了。当然在游戏开始的时候,txt的值应该为空(“”)。

自定义按钮类

现在看看我们刚刚说的第一个问题。tkinter的按钮组件都可以添加一个command参数,用来指定按下该按钮后需要执行的函数。但是问题在于,这个command指定的函数不能传参,而我们的按钮却需要在被按下的时候执行不同的动作(把按钮上的字添加进txt里)。

这个时候我们可以使用面向对象编程的方法,把每个按钮想象成一个有生命的物体。每个按钮有他自己的方法,就是把自己的名字添加进txt。除此之外,这些按钮还需要和tkinter的Button类有一样的属性和方法。于是我们可以创建一个子类,继承tkinter的Button类。因为只需要添加一个自己独特的方法,所以不需要定义初始化,默认让类成员使用tkinter的Button类初始化。代码如下:

class MyButton(tk.Button):
    def click(self):
        global txt
        if len(txt)<4: # 判断玩家是不是已经选了4个汉字
            txt+=self['text'] # 把按钮自己的汉字添加进txt
            for i in range(len(txt)):
                cv.itemconfig(text[i],text=txt[i]) # 改变文本的内容
            self.config(state=tk.DISABLED)

self就代表了每个调用click方法的按钮实例。在这个方法里,我们需要判断txt是不是已经有4个字符了(因为只有4个汉字),如果没有的话,我们把就把按钮上的汉字(self[‘text’])添加进txt里。同时,根据变化的txt,使用canvas的itemconfig方法来改变文本组件text的值。这样就可以达到根据玩家的选择不同,文本的内容实时变化的效果。然后,当玩家选择了某个汉字(按下了某个按钮),我们想要那个按钮变成灰色不可选的状态,于是可以直接使用self.config方法将按钮的状态变成DISABLED(也可以直接用字典的方式调用,self[‘state’]=tk.DISABLED)。

最后,我们只要把之前创建的按钮改成这个新的子类,再把之前的循环放在一起,代码修改如下:

txt = ''
text=[]
btn = []
for i in range(4):
    cv.create_rectangle(50*i+50,210,50*i+86,246,fill='ivory')
    text.append(cv.create_text(50*i+68,228,fill='black', font =('方正楷体简体',18,'bold')))
    for j in range(5):
        btn.append(MyButton(root, font =('方正楷体简体',11),width=2,relief='flat',bg='lightyellow'))
        btn_window = cv.create_window(300+40*i, 75+35*j, window=btn[i*5+j])
for i in btn:
    i['command']=i.click

实现效果如下:
在这里插入图片描述

4). 判断玩家的选择

在我们刚才新建立的按钮子类的click方法里,我们需要先判断玩家选择的汉字有没有达到4个。如果达到4个,我们就需要对其进行判断。所以我们在click方法里,加上以下代码:

        if len(txt)==4:
            is_winner()

如果txt的长度为4,即说明有4个汉字被选中的话,调用is_winner()方法,来判断玩家是否获胜,以及后续的操作。于是我们开始编写is_winner()方法,或者称之为自定义函数。

这个自定义函数要实现的功能其实有不少,我们画一个局部流程图来加以讲解。

Yes
Yes
Yes
Yes
No
No
猜中
No
No
未猜中
开始判断
玩家是否猜中?
弹出对话框,询问是否继续
判断是否已猜完词库
询问玩家是否重新开始
重新导入词库,关卡清零
从头开始游戏
游戏结束
清空玩家的选择
调用create_random_word
回到游戏

由此可见,当玩家选择正确的时候,要做的事情还真不少:

  1. 询问是否继续
  2. 检查词库是否猜完
  3. 询问是否重头再玩一次

我先把代码贴出来:

import tkinter.messagebox as tm
def is_winner():
    global word, txt, level, word_list
    if txt==word:
        for i in btn:
            i.config(state=tk.DISABLED)
        result=tm.askquestion ("恭喜","恭喜你,答对了!继续下一题吗?")
        if result == 'yes':
            if len(word_list)==0:
                newgame = tm.askquestion ("恭喜",f'恭喜你!你已经通过了全部{level}关,继续从头开始游戏吗?')
                if newgame == 'yes':
                    word_list = [i[:4] for i in filenames]
                    random.shuffle(word_list)
                    level = 0
                else:
                    root.destroy()
            level += 1
            cv.itemconfig(level_indicator,text=f'第 {level} 关')
            clean_word()
            create_random_word()
        else:
            tm.showinfo(title='再见', message=f'您一共通过了{level}关')
            root.destroy()
    else:
        clean_word()

首先,为了实现windows消息框的效果,我们需要导入tkinter的另一个子模块messagebox。使用下面的语句将模块导入,并简写为tm

import tkinter.messagebox as tm

然后就可以使用tkinter自带的几种消息框了。在这个小游戏中我们只要用到两种就够了,一种是在询问玩家是否继续的时候,需要得到玩家的选择,另一种是不需要玩家进行选择,只是通知玩家一些信息。前者我们使用的是tm的askquestion方法,该方法根据用户的选择不同,返回一个字符串,要么是“yes”,要么是“no”。
在这里插入图片描述
后者仅仅是一个通知消息框,待玩家鼠标点击“确认”后,程序自动执行后面的命令,也就是“销毁”主窗口,退出游戏。

root.destroy()

在这里插入图片描述
需要注意的是,如果玩家真的把所有成语都猜过了,又选择重新开始的时候,需要在这里把word_list重新初始化一遍(从文件名导入成语、随机打乱)。同时还要把关卡计数清零,从头开始计数。

而不管是猜错了,还是猜对后再开始一局,都需要调用另一个自定义函数clean(),用来把文本框里玩家选择的4个汉字清空。

5). 清空选择

清空选择的自定义函数就比较简单了,但也要完成三件事:

  1. 清空玩家选择的汉字变量txt
  2. 清空Canvas画布上的文本框
  3. 把所有按钮的状态全部变成NORMAL(因为有些按钮在按下后被设置成了DISABLE),这样玩家才能重新选择。

代码实现如下:

def clean_word():
    global txt
    txt = ''
    for i in range(4):
        cv.itemconfig(text[i],text='')
    for i in btn:
        i.config(state=tk.NORMAL)

同时我们在游戏的右下角还有另外两个按钮,“清空”和“提示”。现在我们可以直接把自定义函数clean_word()绑定到“清空”按钮上了。

btn_clean=ttk.Button(root, text='清空', width=5, command=clean_word)

6). 电脑提示

最后的一步,是电脑提示按钮。其实问哥觉得这个按钮有点多余,基本上不会遇到猜不出的情况。但是考虑到可以把游戏进行变种,比如不采用选汉字的方式猜成语,而是让玩家手工输入(需要增加Entry输入框或使用Label组件),难度就会大大增加了,那样的话电脑提示可能还是有点作用的。所以这里我们先把功能做出来,要不要用再说。

其实说来也很简单,就是随机选一个汉字,然后让Canvas的文本框显示那个汉字就好。于是我们可以定义一个局部变量hint_word,然后再使用随机函数生成一个0到3的数字,代表我们要选取的汉字在成语中的位置。然后再将hint_word赋值给文本框就好了。

同时不要忘记,玩家有可能猜了一两个字然后使用提示的情况,这样一些按钮状态可能是DISABLED,所以我们需要在这里把所有按钮的状态恢复成NORMAL。

代码如下:

def hint():
    global word
    hint_word = ['']*4
    i = random.randint(0,3)
    hint_word[i] = word[i]
    for i in range(4):
        cv.itemconfig(text[i],text=hint_word[i])
    for i in btn:
        i.config(state=tk.NORMAL)

最后,别忘记把这个自定义函数绑定在“提示”按钮上。

btn_submit=ttk.Button(root, text='提示', width=5, command=hint)

4. 完整代码

到这里,我们这个小游戏就编写完成了。大家不妨分享给你的小伙伴一起玩玩看。

附上完整代码:

import tkinter as tk
from tkinter import ttk
import tkinter.messagebox as tm
import os
import random

class MyButton(tk.Button):
    def click(self):
        global txt
        if len(txt)<4:
            txt+=self['text']
            for i in range(len(txt)):
                cv.itemconfig(text[i],text=txt[i])
            self.config(state=tk.DISABLED)
        if len(txt)==4:
            is_winner()

def create_random_word():
    global word, img, word_copy
    if word: word_copy.append(word)
    word = word_list.pop()
    lib = list(''.join(random.sample(word_copy+word_list,4))+word)
    random.shuffle(lib)
    for i in range(len(btn)):
        btn[i]['text'] = lib[i]
    img = tk.PhotoImage(file=f"images\words\{word}.png")
    cv.create_image(150,120,image = img)

def is_winner():
    global word, txt, level, word_list
    if txt==word:
        for i in btn:
            i.config(state=tk.DISABLED)
        result=tm.askquestion ("恭喜","恭喜你,答对了!继续下一题吗?")
        if result == 'yes':
            if len(word_list)==0:
                newgame = tm.askquestion ("恭喜",f'恭喜你!你已经通过了全部{level}关,继续从头开始游戏吗?')
                if newgame == 'yes':
                    word_list = [i[:4] for i in filenames]
                    random.shuffle(word_list)
                    level = 0
                else:
                    root.destroy()
            level += 1
            cv.itemconfig(level_indicator,text=f'第 {level} 关')
            clean_word()
            create_random_word()
        else:
            tm.showinfo(title='再见', message=f'您一共通过了{level}关')
            root.destroy()
    else:
        clean_word()

def clean_word():
    global txt
    txt = ''
    for i in range(4):
        cv.itemconfig(text[i],text='')
    for i in btn:
        i.config(state=tk.NORMAL)

def hint():
    global word
    hint_word = ['']*4
    i = random.randint(0,3)
    hint_word[i] = word[i]
    for i in range(4):
        cv.itemconfig(text[i],text=hint_word[i])
    for i in btn:
        i.config(state=tk.NORMAL)


# 游戏从这里开始
filenames = os.listdir(r'images\words')
word_list = [i[:4] for i in filenames]
random.shuffle(word_list)
# 初始化完成
word=''
word_copy=[]
txt = ''
text=[]
btn = []
level=1
# 开始创建GUI窗口
root = tk.Tk()
root.geometry("500x300")
root.resizable(0,0) 
root.title('看图猜成语') 

cv=tk.Canvas(root,bg='white',width=500,height=300)
bg = tk.PhotoImage(file=r"images\bg.png")
cv_bg = cv.create_image(250,150,image = bg)
title = tk.PhotoImage(file=r"images\title.png")
cv_tt = cv.create_image(250,30,image = title)
cv.create_rectangle(90,60,210,180,fill='moccasin',outline = '')

for i in range(4):
    cv.create_rectangle(50*i+50,210,50*i+86,246,fill='ivory')
    text.append(cv.create_text(50*i+68,228,fill='black', font =('方正楷体简体',18,'bold')))
    for j in range(5):
        btn.append(MyButton(root, font =('方正楷体简体',11),width=2,relief='flat',bg='lightyellow'))
        btn_window = cv.create_window(300+40*i, 75+35*j, window=btn[i*5+j])
for i in btn:
    i['command']=i.click

btn_clean=ttk.Button(root, text='清空', width=5, command=clean_word)
btn_submit=ttk.Button(root, text='提示', width=5, command=hint)
cv.create_window(320, 265, window=btn_clean)
cv.create_window(400, 265, window=btn_submit)
level_indicator = cv.create_text(150,270,text=f'第 {level} 关', fill='black', font=('微软正楷',9,'bold'))

create_random_word()
cv.pack()
root.mainloop() 

总结与思考

经过一个星期的熬夜,问哥终于把这个小游戏完整地讲解出来了。不知道讲得够不够细致,大家还有没有不明白的呢?欢迎私信我或给我留言。同时,通过这个小游戏,我们也学习到了Python自带的图形化组件tkinter的一些基本操作,希望大家如果感到有问哥没有讲透彻的地方,也可以先通过在网上搜索同类文章加以学习掌握,或者直接私信问哥解答。

感谢大家读到这里,我们下次再见!

  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我问哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值