趣味Python游戏编程:第3章 递归函数的威力:扫雷

趣味Python游戏编程:第3章 递归函数的威力:扫雷

在第2章中,我们制作了一个拼图游戏,玩家通过鼠标操作图片块移动。本章设计一款扫雷游戏,玩法是在一个方块阵列中随机埋设一定数量的地雷,然后由玩家逐个打开方块,并以排除所有地雷为最终游戏目标。如果玩家打开的方块中有地雷,则游戏失败。我们将继续使用鼠标作为游戏的交互手段,但是更进一步,让鼠标的左右键对应不同的操作:当单击鼠标右键时为方块插上旗子;当单击左键时将方块打开。此外,在扫雷游戏的编写中,将通过函数的递归调用来实现打开方块的操作,从而见识递归函数的巨大威力。

本章主要涉及如下知识点:

自动生成方块阵列

操作鼠标的左、右键

布尔变量的作用

递归函数的使用

判定游戏胜利和失败

3.1 创建方块阵列

3.1.1 准备图片资源

扫雷游戏中,场景中的各个方块会表现出不同的样子:当没有对其操作时,方块会显示没有打开的图像;当右键单击方块时,它会显示插上旗子的图像;当左键单击方块时,则会显示方块打开的图像。因此需要准备一些图片,分别来表示扫雷游戏中方块的不同形态。

我们为游戏准备了如图3.1所示的图片资源。其中包括:方块没有打开时的图片minesweep_block.png,方块插上旗子时的图片minesweep_flag.png,打开地雷时的图片minesweep_bomb.png,用于显示方块周围地雷数量的图片minesweep_number0.png至minesweep_number8.png。

在这里插入图片描述

3.1.2 创建游戏场景

下面来创建游戏场景,首先确定场景的大小。可以定义三个常量ROWS、COLS和SIZE,分别来表示方块阵列的行数、列数和方块尺寸。于是场景的宽度WIDTH就是方块尺寸与列数的乘积,而场景高度HEIGHT则是方块尺寸与行数的乘积。代码如下所示:

ROWS = 15                # 方块行数
COLS = 15                # 方块列数
SIZE = 25                # 方块尺寸
WIDTH = SIZE * COLS      # 屏幕宽度
HEIGHT = SIZE * ROWS     # 屏幕高度

由于扫雷游戏的场景主要是为了放置方块阵列,其背景本身不会显露出来,因此无须为场景指定一个特殊的背景颜色,程序会默认将窗口的背景设置为黑色。

3.1.3 生成方块阵列

接下来创建所有的方块角色,并通过列表统一进行管理。这样的操作是不是感觉很熟悉呢?没错,在拼图游戏中也是这样做的。

之前ROWS和COLS常量的值都设为15,表明场景是由15×15个方块所形成的阵列,于是可以通过双重循环语句来自动创建方块角色。代码如下所示:
在这里插入图片描述

上述代码首先创建了方块列表blocks,然后使用双重循环逐行逐列地创建方块角色,接着设置每个方块的坐标,并将其加入列表中。由于所有方块在初始时都还没有打开,因此所有的方块角色都是通过minesweep_block.png图片文件来创建的。在双重循环语句中,变量i表示方块阵列的行号,j表示列号,于是各方块的left属性值可由j与SIZE相乘得到,而top属性值可由i与SIZE相乘得到。还记得拼图游戏中的类似操作吗?我们为各个图片块设置坐标时也是这样做的。

为方块设置好坐标后便可以将它们显示出来,为此在draw()函数中编写如下代码:

def draw():
    for block in blocks:
        block.draw()

上述代码使用一个遍历循环,逐一地获取列表中的各个方块角色,并调用其draw()方法进行显示。效果如图3.2所示。

在这里插入图片描述

3.1.4 埋设地雷

现在已经创建了方块阵列,接下来要做的就是在方块之下埋设地雷。首先思考一下该如何表示方块是否埋设了地雷呢?仔细想想,对于某个方块来说,它要么埋了地雷,要么没有埋,只有这两种可能。实际上,是否埋设了地雷,这可以看作是方块角色的一种状态,其取值只有是或否两种可能。因此,可以借助布尔变量表示。

在拼图游戏中,曾使用布尔变量表示游戏是否结束的状态。类似地,这里可以为方块设置一个布尔变量,用来表示它的下面是否有地雷,若有则将布尔变量的值设为True,若没有则设为False。不过由于各个方块的状态不一定相同,可能有的埋有地雷,而有的没有埋,所以要分别为每个方块都设置一个布尔变量。于是在创建方块角色之后,马上给它添加一个名叫isbomb的属性,用来表示方块之下是否埋有地雷,并将其初值赋为False。代码如下所示:

block.isbomb = False 

如此一来,就能很方便地为方块埋设地雷。若是希望在某个方块下面埋地雷,只需要将该方块的isbomb属性设为True即可。例如要在场景中埋下20颗地雷,只需要使用循环语句从列表中获取20个方块,并分别将它们的isbomb属性设置为True。代码如下所示:
在这里插入图片描述

上面的代码中首先定义了一个常量BOMBS,用来表示地雷的数量,并将其设置为20。然后执行了一个计数循环语句,依次为列表的前20个方块埋设了地雷。

聪明的你或许会产生疑问:仅仅是将地雷埋在列表的前20个方块中,那地雷的位置不就是固定不变了吗?确实是这样的。因为我们为各个方块设置坐标时是按顺序依次进行的,列表的前20个方块就对应着窗口中第一行的15个方块,以及第二行的前5个方块。所以游戏每次运行时,地雷总是埋在这20个方块中。

如果是这样,那游戏还有何乐趣可言呢?根据扫雷游戏的规则,地雷应该是随机出现的才对,玩家事先并不知道地雷的位置,而且每次游戏时地雷的位置都会发生改变。不要着急,下面马上处理这个问题。其实解决这个问题非常容易,还记得在拼图游戏中是如何让图片块随机显示的吗?在那里我们仅仅用了一行代码便解决了问题,即调用随机函数来随机地打乱图片块在列表中的次序。在扫雷游戏中也可以采取类似的操作。在埋设地雷的for语句之前加入这样一句:

random.shuffle(blocks)

该语句调用Python提供的random库的shuffle()方法,随机地打乱了列表blocks中各个方块的次序。这意味着列表的前20个方块不再是固定顺序的,它们的位置可能分散在窗口的各个角落。于是程序再去调用埋设地雷的语句,为这20个方块来埋设地雷,便会将地雷埋藏在窗口的不同位置。而且游戏每次运行时调用shuffle()方法的结果都不一样,因此每次游戏的地雷位置也不会相同。

3.2 给方块插上旗子

下面来实现方块的操作。在扫雷游戏中,可以对方块执行两种操作:将它打开或给它插上旗子。由于方块的数量很多,可以考虑使用鼠标作为交互手段,因为鼠标指针可以快速准确地定位到目标方块,同时鼠标的左、右按键可以分别执行打开方块和插旗子的操作。

较之于打开方块的操作,为方块插旗子的操作相对而言简单一些,先挑软的柿子捏,实现插旗子的操作。

3.2.1 使用鼠标右键来操作

根据之前的设想,使用鼠标右键来执行插旗子的操作。如何让游戏响应鼠标的右键单击事件呢?我们已经知道,Pgzero提供了on_mouse_down()函数来处理鼠标的单击事件。事实上,该函数不仅提供了获取坐标的参数pos,还提供了一个参数button,用来保存鼠标的按键信息。此外,Pgzero还提供了一个内置对象mouse,并在其中定义了一些常量值,用来表示不同的鼠标按键,例如mouse.LEFT表示鼠标左键,mouse.RIGHT表示鼠标右键。于是可以使用表达式“button == mouse.RIGHT”来判断玩家是否单击了鼠标右键。

另外,在执行插旗子之前,还需要知道鼠标右键单击的是哪一个方块。对于方块而言,就是要判断鼠标单击处的坐标是否位于自身范围之内,这可以通过方块角色的collidepoint()方法来实现。collidepoint()方法是Pgzero为Actor类定义的方法,该方法接受一对坐标值作为参数,用来判断该坐标是否位于角色范围之内。因此可以将鼠标单击处的坐标值传给该方法,进而判断某个方块是否被鼠标单击。

接下来定义一个遍历循环,依次对各个方块进行检查,并对符合条件的方块执行插旗子操作。具体来说,程序首先判断某个方块是否被鼠标单击,进而判断鼠标单击的是否为右键,若这两个条件都满足则给该方块插上旗子。为on_mouse_down()函数编写如下代码:
在这里插入图片描述

运行一下程序,试试能否执行插旗子操作?结果可能令你失望了,当你试着用鼠标右键单击某个方块时,旗子并没有出现,而是收到了编辑器的错误提示:名字set_flag没有定义。

那么set_flag(block)这一句的作用究竟是什么呢?看起来像是一个函数调用,可是我们并没有定义该函数啊。别着急,先来猜测一下它的作用。该函数的参数是block,表示是对某个方块进行操作,而从名字来看,似乎意味着插旗子的操作。没错,该函数就是用来对方块执行插旗子操作的!还记得模块化的编程方法吗?可以将程序中的特定操作定义为函数,当需要执行操作的时候直接调用函数即可。这样做使得代码结构清晰,可读性好,也可以重复利用代码。

这里正是使用了模块化的编程方法,为插旗子操作定义了一个新的函数set_flag(),并将方块角色作为参数传给该函数,以实现对某个指定方块的插旗子操作。下面看看如何实现该函数的功能。

3.2.2 定义函数执行插旗操作

在设计set_flag()函数的具体操作之前,为了保证程序能正常运行,不妨先这样编写代码:
在这里插入图片描述

上述代码给出了set_flag()函数的定义,但是在函数体内只是执行一个pass语句。

提示:

其实这是编程的一个小技巧,就是在不确定函数内部细节的情况下,先保证函数形式的完整,使得程序能够正确执行。而pass关键字的作用就是占据位置,虽然什么都没有做,但是能够保证代码整体完整。

现在重新运行一下程序,虽然右键单击方块还是没有出现旗子,但是程序也不再报错了。

接下来是时候考虑set_flag()函数的具体动作了。先不要着急编写代码,首先来设计这个函数的操作流程,然后再去编程实现。这实际上也是程序设计的一般步骤,即先设计算法,再编写程序。

说明:

所谓算法,就是计算机完成某个操作的具体步骤和方法。设计算法通常可采用两种方式:一种称为流程图,另一种则叫作伪代码。前者是按照规定的符号对程序的执行流程进行刻画,优点是结构清晰、格式规范;后者则是通过非正式的,类似于自然语言的形式来描述程序的执行动作,好处是形式灵活,可以和程序语言的语法相结合。

下面试着用伪代码来描述set_flag()函数的具体操作。根据扫雷游戏规则,玩家用鼠标右键单击某个方块时,如果该方块上面没有插旗子,则可以给它插上旗子;若方块上已经插了旗子,则取消它的旗子。我们结合Python的语法规则来书写伪代码,那么插旗子操作的伪代码可以这样描述:

在这里插入图片描述

在上面的伪代码中,if语句以及缩进的格式都属于Python语法,而判断条件及具体操作则使用了自然语言来描述。可以看到伪代码的特点,它的形式介于程序语言与自然语言之间,也可以看作是两者的混合。通过伪代码的描述,我们清楚了插旗子操作的具体动作,那么接下来就要将伪代码“翻译”成计算机能够理解的程序代码。

由于此前已经准备好了所有的图片资源,所以改换方块的图像很简单,只需要将方块角色的image属性设置为插上旗子的图片即可。然而如何来判断方块是否插了旗子呢?事实上,是否插旗子可看作是方块的某种状态,就如同方块是否埋地雷一样,同样可借助布尔变量进行标识。在创建方块角色的时候,可以为每个方块定义一个isflag属性,用来表示方块是否插了旗子,初值设为False,表示初始时方块没有插旗子。当玩家执行插旗子操作后,再将方块的isflag属性设为True,表示方块已经插了旗子。

在创建方块的语句后面加入这样一行代码:
在这里插入图片描述

然后在set_flag()函数中加入如下代码:

def set_flag(block):
    if not block.isflag:
        block.image = "minesweep_flag"
        block.isflag = True
    else:
        block.image = "minesweep_block"
        block.isflag = False

现在运行一下游戏,试着用鼠标右键单击某个方块,看看能否给它插上旗子呢?游戏的运行画面如图3.3所示。

在这里插入图片描述

3.3 打开方块

处理完鼠标右键的操作,接下来处理鼠标左键的操作。根据游戏规则,当玩家在方块上单击鼠标左键时,将执行打开方块的操作,而打开方块时还需要对可能引爆地雷的情形进行判断和处理。为此我们需要完善鼠标事件的处理流程。

3.3.1 完善鼠标事件处理

检测鼠标左键的单击事件很简单,类似于检测鼠标右键,可以通过表达式“button ==mouse.LEFT”来判断鼠标的左键是否被单击。只不过加入左键单击操作后,方块又增加了一个新的状态,即是否被打开。与标识方块是否埋地雷以及是否插旗子类似,还要再为方块定义一个布尔变量,用来标识它是否被打开。具体来说,在创建方块角色的时候为其设置一个isopen属性,初值设为False,表示初始时方块没有打开。当执行方块打开操作时将该属性设为True,表示方块打开了。于是可以在创建方块的语句后面加入这样一行代码:

 block.isopen = False  

接着修改on_mouse_down()函数,加入处理鼠标左键的单击事件操作。代码如下所示(粗体部分表示新添加的代码):
在这里插入图片描述

上述代码有两处需要注意:一处是鼠标单击操作的条件,除了判断方格是否被单击,还要判断方格是否已经打开了,因为无论是左键还是右键的操作,都只能针对没有打开的方块来执行;另一处是打开方块的条件,除了判断是否单击了鼠标左键,还要判断方块是否插上了旗子,也就是说鼠标左键只能打开没插旗子的方块。

可以看到,打开方块的操作代码又是由另一个if语句构成的,而判断条件则是方块埋设地雷与否。若打开的方块之下有地雷,则执行blow_up()函数,否则执行open_block()函数。按照模块化的编程方法,我们预先定义了blow_up()函数和open_block()函数,前者执行地雷爆炸的动作,后者逐一地将方块打开。先将这两个函数简单地定义如下:
在这里插入图片描述

可以看到,目前这两个函数只是建立了一个形式,并没有具体内容。下面便考虑如何编写具体的操作代码,首先来实现open_block()函数。

3.3.2 获取周围的方格

根据上面的介绍,我们已经知道open_block()函数是用来打开方块的,但是怎样表示方块被打开了呢?其实从图2.1可以看到,我们事先准备了多张图片来表示打开的方块,即minesweep_number0.png至minesweep_number8.png文件名所对应的图片。你会发现,除了minesweep_number0.png图片之外,其他8张图片分别显示了1至8的数字,这表示什么意思呢?

事实上,按照扫雷游戏的规则,如果打开的方块没有埋地雷,则要将它周围各方块的地雷总数显示出来。而资源图片中的数字正是用于表示不同的地雷数量。需要明确一点,所谓方块的周围,不仅是指上下左右相邻的四个方块,还包含斜向相邻的四个方块,因此总共是8个方块,如图3.4所示。当方块被打开时,若它的周围没有地雷,则显示minesweep_number0.png图片,否则根据其周围的地雷数量来显示相应的数字图片。

在这里插入图片描述

看来问题的关键在于,首先要弄清楚某方格周围相邻的是哪些其他方格,这样才便于执行进一步的判断。为此,不妨再专门定义一个get_neighbours()函数,用来获取指定方块周围的其他方块,并以列表的形式将它们返回。因为地雷仅埋藏在未曾打开的方块中,所以实际上只需获取没有打开的方块即可。代码如下所示:

def get_neighbours(bk):
    nblocks = []
    for block in blocks:
        if block.isopen:
            continue
        if block.x == bk.x - SIZE and block.y == bk.y \
          or block.x == bk.x + SIZE and block.y == bk.y \
          or block.x == bk.x and block.y == bk.y - SIZE \
          or block.x == bk.x and block.y == bk.y + SIZE \
          or block.x == bk.x - SIZE and block.y == bk.y - SIZE \
          or block.x == bk.x + SIZE and block.y == bk.y - SIZE \
          or block.x == bk.x - SIZE and block.y == bk.y + SIZE \
          or block.x == bk.x + SIZE and block.y == bk.y + SIZE :
            nblocks.append(block)
    return nblocks

可以看到,get_neighbours()函数传入一个参数bk,表示某个指定的方块。然后在函数体内执行for循环,来遍历列表blocks中的各个方块。对于某个方块block,若它已经打开了,程序便执行continue语句跳过本次循环;否则,程序将分别从8个不同的方向来检查block与指定方块bk的位置关系。由于相邻方块的坐标仅仅相差SIZE值所代表的距离,所以程序用bk的横坐标或纵坐标对SIZE进行简单的加减运算,就可以确定它在某个方向上相邻的其他方块。

上述代码中if语句的条件很长,因此使用反斜杠“\”将其分为几行来显示,其中的8个关系表达式通过逻辑运算符or相连,表示只要列表blocks中的某个方块符合8个条件之一,则可判定它和指定的方块bk相邻。对于满足条件的方块,程序将其加入另一个列表nblocks,并在函数的末尾调用return语句将nblocks作为返回值提交。

3.3.3 统计地雷数量

现在回过头来看看,怎样才能知道方块周围有多少地雷呢?由于已经有了get_neighbours()函数,这个问题便迎刃而解了。对于某个方块来说,可以首先调用get_neighbours()函数来获取它周围的所有其他方块,然后逐一检查这些方块是否埋了地雷,同时统计一下地雷的总数。与之前类似,再单独定义一个函数get_bomb_number(),用来获取指定方格周围的地雷数量。代码如下所示:

def get_bomb_number(bk):
    num = 0
    for block in get_neighbours(bk):
        if block.isbomb:
            num += 1
    return num

上面的get_bomb_number()函数传入一个参数bk,表示某个指定的方块。然后以bk作为参数去调用get_neighbours()函数,来获取bk相邻方块的列表,同时执行for语句遍历该列表。对于与bk相邻的每个方块,检查它的isbomb属性是否为True,若是则将num变量的值加1。num变量的作用正是统计地雷的总数,它的值将在函数的末尾通过return语句返回。

万事俱备,是时候实现open_block()函数了。

3.3.4 递归调用打开方块函数

经过之前的讨论我们已经知道,open_block()函数的作用是将没有埋设地雷的方块打开,同时显示方块周围的地雷数量。之所以要显示地雷数量,是为了给玩家提供游戏的线索,以便于判断哪些方块之下可能有地雷。倘若打开的方块周围没有地雷,则说明与它相邻的其他8个方块都可以安全打开。为了简化游戏操作,避免让玩家自己逐一地去打开没有地雷的方块,游戏规则设计为让程序自动打开没有地雷的方块。具体来说,若是open_block()函数打开的方块周围没有地雷,还要进一步将其相邻的方块都打开,并反复地执行上述操作。这究竟是一种怎样的操作?岂不是意味着要在open_block()函数的内部反复地调用自身吗?没错,这就叫作函数的递归调用。

说明:

在程序设计中,递归是一种常见而有效的编程手段,它利用计算机的强大运算能力自动地完成对复杂问题的求解。通常来说,将需要反复执行的操作定义为递归函数,然后在递归函数的内部反复调用它自己。然而要注意,使用递归函数要满足两个基本条件:首先,下一步的递归调用要以上一步的执行为基础;其次,递归要具备终止条件。

下面按照递归函数的设计思路来实现open_block()函数。对于功能比较复杂的函数,可以首先写出它的伪代码。open_block()函数的伪代码如下所示:
在这里插入图片描述

由于之前已经定义好了获取地雷的函数get_bomb_number(),以及获取相邻方块的函数get_neighbours(),因此可以很容易地将上述伪代码转换为Python程序代码。为open_block()函数编写如下代码:

def open_block(bk):
    bk.isopen = True
    bombnum = get_bomb_number(bk)
    bk.image = "minesweep_number" + str(bombnum)
    if bombnum != 0:
        return
    # 若方块周围没有地雷,则递归地打开周围的方块
    for block in get_neighbours(bk):
        if not block.isopen :
            open_block(block)

上述代码首先将bk的isopen属性设为True,然后调用get_bomb_number()函数获取bk周围的地雷数,并保存在变量bombnum中。同时将bombnum的值与字符串minesweep_number连接起来,形成地雷数字对应的图片文件名,并将其赋给bk的image属性来显示地雷数。接着对bombnum的值进行判断:若其不为0,说明bk周围埋有地雷,程序调用return语句立即返回;否则调用get_neighbours()函数获取bk相邻的其他方块,并对没有打开的方块递归地调用open_block()函数自身。

可以看到open_block()函数满足递归的两个条件:首先,在执行下一步的递归调用之前(即打开其他方块之前),执行了对当前方块的操作,例如标记方块的打开状态,以及获取方块相邻的其他方块;其次,递归具备了终止条件,即判定方块周围有地雷则返回。

运行一下游戏,试着用鼠标左键单击窗口中的方块,你会发现有时只点了一个方块,结果却自动打开了一大片,效果如图3.5所示。是不是很神奇呢?这就是递归函数的巨大威力!

在这里插入图片描述

3.4 判定游戏胜负

到目前为止,游戏的主要规则已经实现了,但还存在一个明显的问题,就是当用鼠标左键单击某些方块时,它们不会做出任何反应。想想这是怎么回事呢?相信你已经知道,这些方块肯定是埋设了地雷,所以单击时无法将其打开。根据之前设计的鼠标事件处理流程,若玩家用鼠标左键单击的是埋有地雷的方块,则要执行blow_up()函数来执行地雷爆炸的动作,而目前我们还没有实现该函数的具体操作。

根据游戏规则,当玩家单击了埋设地雷的方块,游戏便会结束。事实上,扫雷游戏的结束具体分为两种情况:一种情况是玩家单击了地雷方块而结束,这意味着游戏失败;另一种情况是玩家将所有没有地雷的方块都打开了,这可看作是游戏胜利。因此,我们需要对游戏失败和胜利的不同情况分别进行处理。

3.4.1 游戏失败的处理

下面来处理游戏失败的情况,即玩家用鼠标左键单击地雷方块的情况。根据游戏规则,如果玩家单击了地雷方块,游戏会播放爆炸的音效,并将所有的地雷都显示出来。同时游戏停止运行,游戏窗口中央会显示游戏失败的文字提示。

首先定义一个全局的布尔变量failed,用来表示游戏失败的状态,然后将游戏结束时的操作统一放到blow_up()函数中执行。为blow_up()函数编写如下代码:

def blow_up():
    global failed
    failed = True
    sounds.bomb.play()
    for i in range(BOMBS):
        blocks[i].image = "minesweep_bomb"

该函数首先将全局变量failed的值设为True,表示游戏当前的状态为失败状态。然后播放地雷爆炸的声音文件bomb.wav(注意事先要将该文件放置在sounds文件夹下)。接下来执行for循环语句,根据BOMBS常量保存的地雷总数(这里是20),将列表中前20个埋了地雷的方块图像设置为minesweep_bomb文件所表示的地雷爆炸的图像。

最后,在draw()函数中加入一点代码,用来显示游戏失败时的提示文字,如下所示:

if failed:
        screen.draw.text("Failed", center=(WIDTH // 2, HEIGHT // 2),
                         fontsize=100, color="red")

再次运行游戏,试着去单击一个地雷方块,看看游戏会不会出现如图3.6所示的画面。

在这里插入图片描述

3.4.2 游戏胜利的处理

最后处理游戏胜利的情况。根据游戏规则,若玩家将所有没有地雷的方块都打开,则表明游戏胜利。此时游戏会播放一段获胜的音效,同时窗口中央会显示游戏胜利的文字提示。

与游戏失败的处理类似,定义一个全局的布尔变量finished,用来表示游戏胜利的状态。然后在update()函数中对游戏胜利进行判定和处理。为update()函数编写如下代码:

def update():
    global finished
    if finished or failed:
        return
    # 检查是否所有没埋地雷的方块都被打开
    for block in blocks:
        if not block.isbomb and not block.isopen:
            return
    finished = True
    sounds.win.play()

上述代码首先判断目前游戏是否结束,若finished或failed两者之一的值为True,表示游戏已经结束,于是立即返回;否则继续执行后面的for语句,即遍历列表blocks中的所有方块:若某个方块的isbomb属性为False,同时它的isopen属性也为False,则调用return语句返回。而只有当for语句执行完毕,即所有没有地雷的方块都打开了,才会执行最后的两行代码:将全局变量finished设置为True,表示游戏当前的状态为胜利状态,同时播放游戏胜利的音效文件win.wav。

最后修改draw()函数,加入如下代码来显示游戏胜利的文字提示:

if finished:
        screen.draw.text("Finished", center=(WIDTH // 2, HEIGHT // 2),
                         fontsize=100, color="red")

再次运行游戏并试着玩一玩,看看能不能见到如图3.7所示的游戏胜利画面。

在这里插入图片描述

至此,扫雷游戏终于大功告成啦!我们又完成了一款游戏的设计和编写,好好地庆祝一下吧!

3.5 回顾与总结

在本章中,我们学习了如何编写扫雷游戏。首先创建了方块阵列,并统一将它们添加到列表中。然后对鼠标的右键和左键单击事件分别进行了处理:若玩家单击了鼠标右键,则执行插旗子的操作;若单击左键则执行打开方块的操作。我们着重讨论了打开方块的操作,并介绍了如何使用递归函数实现自动打开方块。最后对游戏结束的状态进行了判定,并针对游戏失败和胜利的两种情况分别进行了处理。

本章涉及的Pgzero库的新特性总结如表3.1所示。

在这里插入图片描述

下面给出扫雷游戏的完整源程序代码。

# -*- coding:gb18030 -*-
# 扫雷游戏源代码minesweep.py

import random
BOMBS = 20               # 炸弹数量
ROWS = 15                # 方块行数
COLS = 15                # 方块列数
SIZE = 25                # 方块尺寸
WIDTH = SIZE * COLS      # 屏幕宽度
HEIGHT = SIZE * ROWS     # 屏幕高度
failed = False           # 游戏失败标记
finished = False         # 游戏完成标记
blocks = []              # 方块列表

# 将所有方块添加到场景中
for i in range(ROWS):
    for j in range(COLS):
        block = Actor("minesweep_block")
        block.left = j * SIZE       # 设置方块的水平位置
        block.top = i * SIZE        # 设置方块的垂直位置
        block.isbomb = False        # 标记方块是否埋设地雷
        block.isopen = False        # 标记方块是否被打开
        block.isflag = False        # 标记方块是否插上棋子
        blocks.append(block)

# 随机打乱方块列表的次序
random.shuffle(blocks)

# 埋设地雷
for i in range(BOMBS):
    blocks[i].isbomb = True


# 更新游戏逻辑
def update():
    global finished
    if finished or failed:
        return
    # 检查是否所有没埋地雷的方块都被打开
    for block in blocks:
        if not block.isbomb and not block.isopen:
            return
    finished = True
    sounds.win.play()


# 绘制游戏图像
def draw():
    for block in blocks:
        block.draw()
    if finished:
        screen.draw.text("Finished", center=(WIDTH // 2, HEIGHT // 2),
                         fontsize=100, color="red")
    elif failed:
        screen.draw.text("Failed", center=(WIDTH // 2, HEIGHT // 2),
                         fontsize=100, color="red")


# 处理鼠标点击事件
def on_mouse_down(pos, button):
    if failed or finished:
        return
    for block in blocks:
        # 若方块被鼠标点击,且该方块未曾打开
        if block.collidepoint(pos) and not block.isopen:
            # 若鼠标右键点击方块
            if button == mouse.RIGHT:
                set_flag(block)
            # 若鼠标左键点击方块,且方块没有插上棋子
            elif button == mouse.LEFT and not block.isflag:
                if block.isbomb:
                    blow_up()
                else:
                    open_block(block)


# 为方块插上棋子
def set_flag(block):
    if not block.isflag:
        block.image = "minesweep_flag"
        block.isflag = True
    else:
        block.image = "minesweep_block"
        block.isflag = False


# 地雷爆炸后显示所有地雷
def blow_up():
    global failed
    failed = True
    sounds.bomb.play()
    for i in range(BOMBS):
        blocks[i].image = "minesweep_bomb"


# 打开方块
def open_block(bk):
    bk.isopen = True
    bombnum = get_bomb_number(bk)
    bk.image = "minesweep_number" + str(bombnum)
    if bombnum != 0:
        return
    # 若方块周围没有地雷,则递归地打开周围的方块
    for block in get_neighbours(bk):
        if not block.isopen :
            open_block(block)


# 获取某方块周围的地雷数量
def get_bomb_number(bk):
    num = 0
    for block in get_neighbours(bk):
        if block.isbomb:
            num += 1
    return num


# 获取某方块周围的所有方块
def get_neighbours(bk):
    nblocks = []
    for block in blocks:
        if block.isopen:
            continue
        if block.x == bk.x - SIZE and block.y == bk.y \
          or block.x == bk.x + SIZE and block.y == bk.y \
          or block.x == bk.x and block.y == bk.y - SIZE \
          or block.x == bk.x and block.y == bk.y + SIZE \
          or block.x == bk.x - SIZE and block.y == bk.y - SIZE \
          or block.x == bk.x + SIZE and block.y == bk.y - SIZE \
          or block.x == bk.x - SIZE and block.y == bk.y + SIZE \
          or block.x == bk.x + SIZE and block.y == bk.y + SIZE :
            nblocks.append(block)
    return nblocks

在这里插入图片描述

Python扫雷游戏源码通常包含以下几个步骤: 1. **创建游戏界面**:使用Python库如`tkinter`或`pygame`来构建一个简单的图形用户界面(GUI),显示网格、数字表示地雷的数量以及标记已探索的方块。 2. **随机生成地雷**:初始化游戏板时,需要随机在指定区域放置一定数量的地雷。可以将每个单元格视为一个二维数组,并将其设置为0表示安全,设置为非0值表示地雷。 3. **标志函数**:定义函数来处理点击事件,检查当前点击位置是否有地雷。如果没有,更新周围方块的状态,如果是地雷则结束游戏,否则递归探测邻近区域。 4. **计数器和边界处理**:维护一个计数器跟踪剩余未发现的地雷数,并确保玩家不会越界操作。 5. **游戏循环**:通过while循环控制游戏流程,接收用户的输入,不断更新游戏状态直到游戏胜利或失败。 6. **游戏结束条件**:当所有非地雷的方块都被标记为安全,或者玩家触发了地雷,游戏就结束了。 下面是一个非常简化的Python扫雷游戏源码片段示例: ```python import random # 初始化一个8x8的游戏板和地雷分布 board = [[0] * 9 for _ in range(9)] mines = random.sample(range(9 * 9), 10) for mine in mines: x, y = divmod(mine, 9) board[x][y] = -1 def reveal_square(x, y): if board[x][y] == -1: print(' boom! game over') else: print(f' {board[x][y]} ', end='') if board[x][y] > 0: for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)]: # 邻域 nx, ny = x + dx, y + dy if 0 <= nx < 9 and 0 <= ny < 9: reveal_square(nx, ny) # 示例游戏开始 reveal_square(0, 0) ``` 这只是一个基础版本,实际完整的游戏会更复杂,包括错误处理、用户交互和更多的游戏规则。如果你想了解完整的代码实现,可以在网上找一些开源的Python扫雷教程或项目参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值