记忆拼图游戏设计及完整代码

python 专栏收录该内容
46 篇文章 2 订阅

原文地址:http://inventwithpython.com/pygame/chapter3.html
中文注释的代码下载

记忆拼图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qdypR4E0-1626091784745)(C:/Users/admin/Pictures/typora/image001.jpg)]

如何玩记忆拼图

在 Memory Puzzle 游戏中,几个图标被白框覆盖。每个图标有两个。玩家可以单击两个框来查看它们后面的图标。如果图标匹配,则这些框保持未覆盖。当棋盘上的所有盒子都被揭开时,玩家获胜。为了给玩家一个提示,在游戏开始时,这些盒子会很快被揭开。

嵌套的循环

一个概念,你会在记忆拼图看到(和大部分在这本书的游戏)是使用的对的另一循环内的循环。这些被称为嵌套for循环。嵌套for循环对于遍历两个列表的每种可能组合都很方便。

在 Memory Puzzle 代码中,我们需要多次迭代棋盘上每个可能的 X 和 Y 坐标。我们将使用嵌套的for循环来确保我们得到每个组合。请注意,内部for循环(for循环内的另一个for 循环)将在进入外部for循环的下一次迭代之前完成其所有迭代。如果我们颠倒for循环的顺序,将打印相同的值,但它们将以不同的顺序打印。

记忆拼图的源代码

该源代码可以从http://invpy.com/memorypuzzle.py下载。

继续,首先在 IDLE 的文件编辑器中输入整个程序,将其保存为memorypuzzle.py,然后运行它。如果您收到任何错误消息,请查看错误消息中提到的行号并检查您的代码是否有任何拼写错误。您还可以将您的代码复制并粘贴到http://invpy.com/diff/memorypuzzle 上的 Web 表单中,以查看您的代码与本书中的代码之间是否存在差异。

只需输入一次,您就可能会了解到有关该程序如何工作的一些想法。完成输入后,您就可以自己玩游戏了。

版权与模块导入

  1. # Memory Puzzle
  2. # By Al Sweigart al@inventwithpython.com
  3. # http://inventwithpython.com/pygame
  4. # Released under a "Simplified BSD" license
  5.
  6. import random, pygame, sys
  7. from pygame.locals import *

程序的顶部是关于游戏是什么、是谁制作的以及用户可以在哪里找到更多信息的评论。还有一个说明,源代码可以在“简化的 BSD”许可下自由复制。简化版 BSD 许可证比 Creative Common 许可证(本书在其下发行)更适用于软件,但它们的基本含义相同:人们可以自由复制和共享此游戏。有关许可证的更多信息,请访问http://invpy.com/licenses。

该程序利用了其他模块中的许多函数,因此它在第 6 行导入这些模块。 第 7 行是from(模块名称)import格式的导入语句,这意味着您不必在它的前面键入模块名称。pygame.locals模块中没有函数,但其中有几个我们想要使用的常量变量,例如MOUSEMOTIONKEYUPQUIT。使用这种风格的导入 语句,我们只需要输入MOUSEMOTION而不是pygame.locals.MOUSEMOTION。

幻数是坏的

9. FPS = 30 #每秒帧数,程序的一般速度
10. WINDOWWIDTH = 640 # 窗口宽度的大小(以像素为单位)
11. WINDOWHEIGHT = 480 # 窗口高度的大小(以像素为单位)
12. REVEALSPEED = 8 # 速度盒的滑动显示和覆盖
13. BOXSIZE = 40 # 盒子高度和宽度的大小(以像素为单位)
14. GAPSIZE = 10 # 框之间的间隙大小(以像素为单位)

本书中的游戏程序使用了很多常量变量。您可能没有意识到它们为何如此方便。例如,我们可以直接在代码中输入整数40,而不是在我们的代码中使用BOXSIZE变量。但是使用常量变量有两个原因。

首先,如果我们以后想更改每个框的大小,我们必须遍历整个程序,每次输入40时都必须查找和替换。使用BOXSIZE常量,我们只需更改第 13 行,程序的其余部分已经是最新的。这要好得多,特别是因为我们可能会将整数值40用于除白框大小之外的其他内容,而意外更改40会导致我们的程序出现错误。

其次,它使代码更具可读性。转到下一部分并查看第 18 行。这将设置XMARGIN常量的计算,即整个棋盘的一侧有多少像素。这是一个看起来很复杂的表达,但你可以仔细找出它的意思。第 18 行如下所示:

XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2)
但是如果第 18 行没有使用常量变量,它看起来像这样:
XMARGIN = int((640 – (10 * (40 + 10))) / 2)

现在已经不可能记住程序员的意图到底是什么了。源代码中这些无法解释的数字通常称为幻数。每当您发现自己输入了幻数时,您应该考虑将它们替换为常量变量。对于 Python 解释器,前两行完全相同。但是对于正在阅读源代码并试图理解其工作原理的人类程序员来说,第 18 行的第二个版本根本没有多大意义!常量确实有助于源代码的可读性。

使用assert 语句进行健全性检查

 15. BOARDWIDTH = 10 # 图标的列数
 16. BOARDHEIGHT = 7 # 图标的行数
 17. assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, 'Board 需要有偶数个盒子来匹配配对。
 18. XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2)
 19. YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2)

第17 行的assert语句确保我们选择的棋盘宽度和高度将产生偶数个框(因为在这个游戏中我们会有成对的图标)。assert语句由三部分组成:assert关键字,一个表达式,如果表达式为False,则会导致程序崩溃。第三部分(表达式后的逗号之后)是一个字符串,如果程序因为断言而崩溃时会出现该字符串。

带有表达式的assert语句基本上是说,“程序员断言这个表达式必须是 True,否则程序会崩溃。” 这是向您的程序添加健全性检查以确保正确执行,断言是可以保证该代码按预期工作的好方法。

判断一个数是偶数还是奇数

如果电路棋盘宽度和高度的乘积除以 2,余数为 0(% 模数运算符评估余数是多少),则该数字为偶数。偶数除以 2 的余数总是为零。奇数除以 2 的余数总是 1。如果你需要你的代码来判断一个数字是偶数还是奇数,这是一个记住的好技巧:

>>> isEven = someNumber % 2 == 0

>>> isOdd = someNumber % 2 != 0

在上述情况下,如果someNumber 中的整数是偶数,则isEven将为True。如果是奇数,则isOdd将为True。

早崩溃,经常崩溃!

让你的程序崩溃是一件坏事。当您的程序在代码中出现错误并且无法继续时,就会发生这种情况。但是在某些情况下,尽早使程序崩溃可以避免以后出现更严重的错误。

如果我们在第 15 和 16行 BOARDWIDTH 和BOARDHEIGHT选择的值导致棋盘具有奇数个框(例如,如果宽度为 3,高度为 5),那么总会剩下一个没有配对的图标。这将导致程序后期出现错误,并且可能需要进行大量调试工作才能确定错误的真正来源在程序的最开始。事实上,只是为了好玩,尝试注释掉断言使其不运行,然后将BOARDWIDTH和BOARDHEIGHT 常量都设置为奇数。当你运行程序时,它会立即显示在memorypuzzle.py 中的第 149 行发生错误,这是在getRandomizedBoard()函数中!

Traceback (most recent call last):
  File "C:\book2svn\src\memorypuzzle.py", line 292, in <module>
    main()
  File "C:\book2svn\src\memorypuzzle.py", line 58, in main
    mainBoard = getRandomizedBoard()
  File "C:\book2svn\src\memorypuzzle.py", line 149, in getRandomizedBoard
    columns.append(icons[0])
IndexError: list index out of range

我们可以花很多时间查看getRandomizedBoard(), 试图找出它有什么问题,然后才意识到getRandomizedBoard()非常好:错误的真正来源是在第 15 行和第 16 行,我们设置了BOARDWIDTH 和BOARDHEIGHT常量.

断言确保这永远不会发生。如果我们的代码将要崩溃,我们希望它在检测到某些严重错误时立即崩溃,否则错误可能要到程序的后期才会变得明显。早早崩溃!

每当您的程序中存在必须始终为True 的某些条件时,您都希望添加assert语句。经常崩溃!您不必太过分并在任何地方放置断言语句,经常因断言而崩溃对于检测错误的真正来源大有帮助。

使源代码看起来漂亮

 21. #            R    G    B
 22. GRAY     = (100, 100, 100)
 23. NAVYBLUE = ( 60,  60, 100)
 24. WHITE    = (255, 255, 255)
 25. RED      = (255,   0,   0)
 26. GREEN    = (  0, 255,   0)
 27. BLUE     = (  0,   0, 255)
 28. YELLOW   = (255, 255,   0)
 29. ORANGE   = (255, 128,   0)
 30. PURPLE   = (255,   0, 255)
 31. CYAN     = (  0, 255, 255)
 32.
 33. BGCOLOR = NAVYBLUE
 34. LIGHTBGCOLOR = GRAY
 35. BOXCOLOR = WHITE
 36. HIGHLIGHTCOLOR = BLUE

请记住,Pygame 中的颜色由0到255 之间的三个整数组成的元组表示。这三个整数表示颜色中红色、绿色和蓝色的数量,这就是为什么这些元组被称为 RGB 值的原因。请注意,第 22 到 31 行元组的间距使得 R、G 和 B 整数排成一行。在 Python 中,缩进(即行首的空格)需要精确,但行其余部分的间距没有那么严格。通过将元组中的整数隔开,我们可以清楚地看到 RGB 值如何相互比较。(有关间距和缩进的更多信息见http://invpy.com/whitespace。)

以这种方式使您的代码更具可读性是一件好事,但不要花费太多时间来做这件事。代码不一定要漂亮才能工作。在某个时候,您将花费更多的时间来输入空格,而不是only具有可读元组值而节省的时间。

使用常量变量代替字符串

 38. DONUT = 'donut'
 39. SQUARE = 'square'
 40. DIAMOND = 'diamond'
 41. LINES = 'lines'
 42. OVAL = 'oval'

该程序还为某些字符串设置了常量变量。这些常量将用于棋盘的数据结构中,跟踪棋盘上的哪些空间具有哪些图标。使用常量变量代替字符串值是个好主意。看下面的代码,它来自第 187 行:

if shape == DONUT:

形状变量将设置为字符串'donut'、'square'、'diamond'、'lines'或'oval' 之一,然后与DONUT 常量进行比较。例如,如果我们在编写第 187 行时打错了字,如下所示:

if shape == DNUOT:

然后 Python 会崩溃,给出一条错误消息,指出没有名为DUNOT 的变量。这很好。由于程序在第 187 行崩溃,当我们检查该行时,很容易看出错误是由拼写错误引起的。但是,如果我们使用字符串而不是常量变量并输入相同的拼写错误,第 187 行将如下所示:

if shape == 'dunot':

这是完全可以接受的 Python 代码,所以当你运行它时它不会一开始就崩溃。但是,这将导致我们程序稍后出现奇怪的错误。因为代码不会在导致问题的地方立即崩溃,所以要找到它可能会困难得多。

确保我们有足够的图标

 44. ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN)
 45. ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL)
 46. assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * BOARDHEIGHT, "对于定义的形状/颜色数量来说,棋盘子太大了。"

为了让我们的游戏程序能够创建各种可能的颜色和形状组合的图标,我们需要创建一个包含所有这些值的元组。第 46 行还有另一个断言,以确保有足够的颜色/形状组合来满足我们所拥有的棋盘的大小。如果没有,那么程序将在第 46 行崩溃,我们将知道我们要么必须添加更多颜色和形状,要么使棋盘的宽度和高度更小。使用 7 种颜色和 5 种形状,我们可以制作 35 个(即 7 x 5)不同的图标。并且因为每个图标都有一对,这意味着我们可以拥有最多 70 个(即 35 x 2 或 7 x 5 x 2)空间的棋盘。

元组 vs. 列表,不可变 vs. 可变

您可能已经注意到ALLCOLORSALLSHAPES变量是元组而不是列表。我们什么时候要使用元组,什么时候要使用列表?无论如何,它们之间有什么区别?

元组和列表在各方面都相同,除了两点:元组使用圆括号而不是方括号,元组中的项不能修改(但列表中的项可以修改)。我们经常称列表是可变的(意味着它们可以被改变)和元组是不可变的(意味着它们不能被改变)。

有关尝试更改列表和元组中的值的示例,请查看以下代码:

>>> listVal = [1, 1, 2, 3, 5, 8]
>>> tupleVal = (1, 1, 2, 3, 5, 8)
>>> listVal[4] = 'hello!'
>>> listVal
[1, 1, 2, 3, 'hello!', 8]
>>> tupleVal[4] = 'hello!'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> tupleVal
(1, 1, 2, 3, 5, 8)
>>> tupleVal[4]
5

请注意,当我们尝试更改元组中索引2处的项目时,Python 会给我们一条错误消息,指出元组对象不支持“项目分配”。

元组的不变性有一个愚蠢的好处和一个重要的好处。愚蠢的好处是使用元组的代码比使用列表的代码稍快。(Python 能够进行一些优化,因为知道元组中的值永远不会改变。)但是让代码运行快几纳秒并不重要。

使用元组的重要好处类似于使用常量变量的好处:它表明元组中的值永远不会改变,所以任何人稍后阅读代码都可以说,“我可以预期这个元组将永远是相同的。否则程序员会使用一个列表。” 这也让未来的程序员在阅读你的代码时说:“如果我看到一个列表值,我知道它可以在这个程序的某个时候修改。否则,编写此代码的程序员将使用元组。”

您仍然可以为变量分配一个新的元组值:

\>>> tupleVal = (1, 2, 3)

\>>> tupleVal = (1, 2, 3, 4)

这段代码有效的原因是第二行的代码没有改变(1, 2, 3)元组。它分配一个全新的元组(1,2,3,4)的tupleVal,并覆盖旧的元组值。但是,您不能使用方括号修改元组中的项目。

字符串也是一种不可变的数据类型。您可以使用方括号读取字符串中的单个字符,但不能更改字符串中的单个字符:

\>>> strVal = 'hello'
\>>> strVal[1]
'e'
\>>> strVal[1] = 'X'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

一个项目元组需要一个尾随逗号

此外,关于元组的一个小细节:如果您需要编写有关其中具有一个值的元组的代码,那么它需要在其中包含一个尾随逗号,例如:

oneValueTuple = (42, )

如果你忘记了这个逗号(而且很容易忘记),那么 Python 将无法区分这与一组只是改变操作顺序的括号之间的区别。例如,看下面两行代码:

variableA = (5 * 6)
variableB = (5 * 6, )

存储在variableA 中的 值只是整数30。但是,variableB的赋值语句的表达式是单项元组值(30, )。空元组值不需要逗号,它本身可以是一组括号:()

在列表和元组之间转换

您可以在列表和元组值之间进行转换,就像在字符串和整数值之间进行转换一样。只需将元组值传递给list()函数,它就会返回该元组值的列表形式。或者,将列表值传递给tuple()函数,它将返回该列表值的元组形式。尝试在交互式 shell 中键入以下内容:

>>> spam = (1, 2, 3, 4)
>>> spam = list(spam)
>>> spam
[1, 2, 3, 4]
>>> spam = tuple(spam)
>>> spam
(1, 2, 3, 4)
>>>

全局声明,为什么全局变量是邪恶

48. def main():
 49.     global FPSCLOCK, DISPLAYSURF
 50.     pygame.init()
 51.     FPSCLOCK = pygame.time.Clock()
 52.     DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
 53.
 54.     mousex = 0 # used to store x coordinate of mouse event
 55.     mousey = 0 # used to store y coordinate of mouse event
 56.     pygame.display.set_caption('Memory Game')

这是main() 函数的开始,这是游戏代码的主要部分所在。本章稍后将解释调用main()函数的函数。

第 49 行是一个全局语句。全局语句是global关键字后跟以逗号分隔的变量名的列表。将这些变量名称标记为全局变量。在main() 函数内部,这些名称不适用于可能恰好与全局变量同名的局部变量。它们全局变量。在main()函数中分配给它们的任何值都将保留在main()函数之外。我们将FPSCLOCK和DISPLAYSURF 变量标记为全局变量,因为它们在程序调用其他几个函数中使用。(更多信息在http://invpy.com/scope。)

有四个简单的规则可以确定变量是局部变量还是全局变量:

  1. 如果在函数的开头有一个变量的global语句,那么这个变量就是全局的。

  2. 如果函数中的变量名与全局变量同名,并且函数从不为变量赋值,则该变量就是全局变量。

  3. 如果函数中变量的名称与全局变量的名称相同,并且函数确实为变量赋值,则该变量是局部变量。

  4. 如果函数中没有与变量同名的全局变量,那么该变量显然是局部变量。

您通常应避免在函数内使用全局变量

函数应该像程序中的小程序,具有特定的输入(参数)和输出(返回值)。但是读取和写入全局变量的函数具有额外的输入和输出。由于在调用函数之前,全局变量可能已在许多地方被修改,因此追踪涉及全局变量中设置的错误值的错误可能会很棘手。

将函数作为不使用全局变量的单独小程序可以更容易地发现代码中的错误,因为函数的参数是明确已知的。它还使更改函数中的代码更容易,因为如果新函数使用相同的参数并给出相同的返回值,它将像旧函数一样自动与程序的其余部分一起工作。

基本上,使用全局变量可能会使编写程序更容易,但它们通常会使调试更难

在本书的游戏中,全局变量主要用于那些永远不会改变的全局常量,但需要先调用pygame.init()函数的变量。由于这发生在main()的函数,它们在设定的main()函数,并且仅仅是让其他功能读取。全局变量作为常量使用并且不会改变,因此它们不太可能导致令人困惑的错误。

如果您不明白这一点,请不要担心。只需编写代码,以便将值传递给函数,而不是让函数读取全局变量作为一般规则。

数据结构和二维列表

 58.     mainBoard = getRandomizedBoard()
 59.     revealedBoxes = generateRevealedBoxesData(False)

getRandomizedBoard()函数返回表示棋盘的状态的数据结构。generateRevealedBoxesData()函数返回代表盒子的数据结构。这些函数的返回值是二维 (2D) 列表或列表列表。值列表的列表将是 3D 列表。二维或多维 列表的另一个词是多维列表。

如果我们有一个名为spam的变量的列表值,我们可以使用方括号访问该列表中的值,例如spam[2]以检索列表中的第三个值。如果spam[2] 中的值本身就是一个列表,那么我们可以使用另一组方括号来检索该列表中的值。例如,这看起来像spam[2][4],它将检索列表spam的第三个值中的第五个值。使用列表中存放列表的这种表示法可以轻松地将 2D 棋盘映射到 2D 列表值。由于棋盘 变量将在其中存储图标,如果我们想获取棋盘上在 (4, 5) 位置的图标,那么我们可以使用表达式mainBoard[4][5]。由于图标本身存储具有形状和颜色的二项元组,因此完整的数据结构是二项元组列表的列表。

这是一个小例子。看起来像这样:
在这里插入图片描述

对应的数据结构为:

mainBoard = [[(DONUT, BLUE), (LINES, BLUE), (SQUARE, ORANGE)], 
             [(SQUARE, GREEN), (DONUT, BLUE), (DIAMOND, YELLOW)], 
             [(SQUARE, GREEN), (OVAL, YELLOW), (SQUARE, ORANGE)], 
             [(DIAMOND, YELLOW), (LINES, BLUE), (OVAL, YELLOW)]]

(如果您的书是黑白的,您可以在http://invpy.com/memoryboard 上看到上图的彩色版本。)您会注意到mainBoard[x][y]将对应于位于棋盘上的 (x, y) 坐标。

同时,“revealed boxes”数据结构也是一个二维列表,除了不是像棋盘子数据结构那样的二元元组之外,它具有布尔值:如果在x、y坐标处的盒子被暴露,则为真,如果是,则为假被掩盖了。将False传递给generateRevealedBoxesData()函数会将revealed boxes所有值设置为False。(此功能稍后详细说明。)

这两个数据结构用于跟踪游戏棋盘的状态。

“开始游戏”动画

61.     firstSelection = None # stores the (x, y) of the first box clicked.
62.
63.     DISPLAYSURF.fill(BGCOLOR)
64.     startGameAnimation(mainBoard)

第 61 行设置了一个名为firstSelection的变量 ,其值为None。(None 是表示缺少值的值。它是数据类型NoneType的唯一值。更多信息请访问http://invpy.com/None)当玩家点击棋盘上的图标时,程序需要跟踪这是单击的配对图标的第一个图标还是第二个图标。如果firstSelectionNone,则单击是在第一个图标上,我们将XY坐标存储在firstSelection变量中作为两个整数的元组(一个用于 X 值,另一个用于 Y 值)。在第二次单击时,值将是元组而不是None,这是程序跟踪它是第二次单击图标的方式。第 63 行用背景颜色填充整个表面。也将绘制过去在表面上的任何东西,这为我们提供了一个干净的棋盘来开始绘制图形。

如果您玩过 Memory Puzzle 游戏,您会注意到在游戏开始时,所有的盒子都会被快速覆盖和随机揭开,让玩家可以先睹为快,看看哪些图标位于哪些盒子下方。这一切都发生在startGameAnimation() 函数中,本章稍后将对此进行解释。

重要的是让玩家先睹为快(但不要让玩家轻易记住图标的位置),否则他们将不知道任何图标在哪里。盲目点击图标并不像有一点提示那样有趣。

游戏循环

 66.     while True: # main game loop
 67.         mouseClicked = False
 68.
 69.         DISPLAYSURF.fill(BGCOLOR) # drawing the window
 70.         drawBoard(mainBoard, revealedBoxes)

游戏循环是一个无限循环,从第 66 行开始,只要游戏正在进行,就会不断迭代。请记住,游戏循环处理事件、更新游戏状态并将游戏状态绘制到屏幕上。

Memory Puzzle 程序的游戏状态存储在以下变量中:

  • mainBoard
  • revealedBoxes
  • firstSelection
  • mouseClicked
  • mousex
  • mousey

在 Memory Puzzle 程序中游戏循环的每次迭代中,mouseClicked变量存储一个布尔值,如果玩家在游戏循环的此迭代期间单击了鼠标,则该值为True。(这是跟踪游戏状态的一部分。)

在第 69 行,表面涂上背景颜色以擦除之前在其上绘制的任何内容。然后程序调用 drawBoard()以根据棋盘和我们传递给它的“revealedBoxes”数据绘制棋盘子的当前状态。(这些代码行是绘制和更新屏幕的一部分。)

请记住,我们的绘图函数仅在内存中的显示 Surface 对象上绘图。在我们调用pygame.display.update() 之前,这个 Surface 对象实际上不会出现在屏幕上,这在第 121 行的游戏循环结束时完成。

事件处理循环

 72.     for event in pygame.event.get(): # 事件处理循环
 73.       if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):
 74.         pygame.quit()
 75.         sys.exit()
 76.       elif event.type == MOUSEMOTION:
 77.         mousex, mousey = event.pos
 78.       elif event.type == MOUSEBUTTONUP:
 79.         mousex, mousey = event.pos
 80.         mouseClicked = True

第72 行的for循环为从游戏循环的最后一次迭代以来发生的每个事件执行代码。这个循环被称为事件处理循环 (它与游戏循环不同,虽然事件处理循环在游戏循环内部),它遍历调用 pygame.event.get()返回的pygame.Event对象列表.

如果事件对象是QUIT事件或Esc 键的KEYUP事件,则程序应终止。否则,在发生MOUSEMOTION 事件(即鼠标光标已移动)或MOUSEBUTTONUP 事件(即先前按下鼠标按钮而现在松开按钮)时,应在mousex和mousey变量中存储鼠标光标的位置。如果这是一个MOUSEBUTTONUP事件,则mouseClicked也应设置为True

一旦我们处理了所有事件,存储在 mousex、mousey和mouseClicked 中的值将告诉我们玩家给我们的任何输入。现在我们应该更新游戏状态并将结果绘制到屏幕上。

检查鼠标光标在哪个框上

82.         boxx, boxy = getBoxAtPixel(mousex, mousey)
83.         if boxx != None and boxy != None:
84.             # The mouse is currently over a box.
85.             if not revealedBoxes[boxx][boxy]:
86.                drawHighlightBox(boxx, boxy)

getBoxAtPixel()函数将返回两个整数的元组。整数表示鼠标坐标所在框的 XY 棋盘坐标。 稍后解释getBoxAtPixel()如何做到这一点。我们现在只需要知道,如果mousex和mousey坐标在一个盒子上,则函数返回一个 XY 棋盘坐标元组并存储在boxx和boxy 中。如果鼠标光标不在任何框上(例如,如果它位于棋盘的一侧或在框之间的间隙中),则该函数返回元组(None, None)并且都将返回存储在boxx和boxy中。

我们只对boxx和boxy 中没有None的情况感兴趣,因此接下来的几行代码位于第83 行检查这种情况的if语句之后的块中。如果在这个块内执行,我们知道用户将鼠标光标放在一个框上(并且可能还点击了鼠标,这取决于存储在mouseClicked 中的值)。

第85 行的if语句通过读取RevealBoxes[boxx][boxy]中存储的值来检查框是否被覆盖。如果它是False,那么我们知道盒子被覆盖了。每当鼠标悬停在一个被遮盖的盒子上时,我们想在盒子周围画一个蓝色的高光来通知玩家他们可以点击它。对于已经未覆盖的盒子,不会进行此突出显示。高亮绘制由我们的drawHighlightBox() 函数处理,稍后解释。

87.             if not revealedBoxes[boxx][boxy] and mouseClicked:
88.                 revealBoxesAnimation(mainBoard, [(boxx, boxy)])
89.                 revealedBoxes[boxx][boxy] = True # set the box as "revealed"

在第 87 行,我们检查鼠标光标是否位于被覆盖的框上,而且还检查鼠标是否被点击过。在被点击过的情况下,通过调用我们的revealBoxesAnimation() 函数来播放那个盒子的“展示”动画(与所有其他函数main() 调用一样,本章稍后会解释)。您应该注意,调用此函数只会绘制被揭开的盒子的动画。直到第 89 行我们设置了revealBoxes[boxx][boxy] = True ,跟踪游戏状态的数据结构才被更新。

如果您注释掉第 89 行,然后运行程序,您会注意到在单击一个框后播放了显示动画,但该框立即再次被覆盖。这是因为RevealBoxes[boxx][boxy]仍然设置为False,所以在游戏循环的下一次迭代中,画棋盘时会盖住这个盒子。没有第 89 行会在我们的程序中导致相当奇怪的错误。

处理第一个点击的框

 90.         if firstSelection == None: # 当前框是第一个被点击的框
 91.           firstSelection = (boxx, boxy)
 92.         else: # 当前框是第二个被点击的框
 93.           # 检查两个图标是否匹配。
 94.           icon1shape, icon1color = getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1])
 95.           icon2shape, icon2color = getShapeAndColor(mainBoard, boxx, boxy)

在执行进入游戏循环之前,firstSelection变量被设置为None。这意味着没有点击任何框,因此如果第 90 行的条件为True,这意味着这是点击的两个可能匹配的框中的第一个。我们播放盒子的显示动画,然后保持盒子未被覆盖。我们还将firstSelection变量设置为被单击的框的框坐标元组。

如果这是玩家点击的第二个框,我们想播放该框的显示动画,然后检查框下的两个图标是否匹配。getShapeAndColor() 函数将检索图标的形状和颜色值。(这些值将是ALLCOLORSALLSHAPES元组中的值之一。)

处理一对不匹配的图标

 97.                     if icon1shape != icon2shape or icon1color != icon2color:
 98.                         # Icons don't match. Re-cover up both selections.
 99.                         pygame.time.wait(1000) # 1000 milliseconds = 1 sec
100.                         coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (boxx, boxy)])
101.                         revealedBoxes[firstSelection[0]][firstSelection [1]] = False
102.                         revealedBoxes[boxx][boxy] = False

第97 行的if语句检查两个图标的形状或颜色是否不匹配。如果是这种情况,那么我们希望通过调用pygame.time.wait(1000)将游戏暂停 1000 毫秒(即1 秒),以便玩家有机会看到两个图标没有“匹配。然后为两个盒子播放“覆盖”动画。我们还希望更新游戏状态以将这些盒子标记为未显示(即被遮盖)。

处理如果玩家赢了

103.                     elif hasWon(revealedBoxes): # check if all pairs found
104.                         gameWonAnimation(mainBoard)
105.                         pygame.time.wait(2000)
106.
107.                         # Reset the board
108.                         mainBoard = getRandomizedBoard()
109.                         revealedBoxes = generateRevealedBoxesData(False)
110.
111.                         # Show the fully unrevealed board for a second.
112.                         drawBoard(mainBoard, revealedBoxes)
113.                         pygame.display.update()
114.                         pygame.time.wait(1000)
115.
116.                         # Replay the start game animation.
117.                         startGameAnimation(mainBoard)
118.                     firstSelection = None # reset firstSelection variable

否则,如果第 97 行的条件为False,则两个图标必须匹配。此时程序实际上不需要对盒子做任何其他事情:它可以让两个盒子都处于显示状态。但是,程序应该检查这是否是棋盘上最后一对要匹配的图标。这是在我们的hasWon()函数中完成的,如果棋盘处于获胜状态(即所有的盒子都被显示出来),它会返回True。

如果是这样的话,我们想通过调用玩“游戏赢”动画gameWonAnimation() ,然后稍微停下来让他们的胜利玩家陶醉,然后重置在数据结构主棋盘和revealedBoxes 开始一个新游戏。

第 117 行再次播放“开始游戏”动画。之后,程序执行将像往常一样循环游戏循环,玩家可以继续玩,直到他们退出程序。

无论两个框是否匹配,在第二个框被点击后,第 118 行会将firstSelection 变量设置回None以便玩家点击的下一个框将被解释为一对可能匹配的第一个点击框图标。

将游戏状态绘制到屏幕上

120.     # 重绘屏幕并等待时钟滴答。
121.     pygame.display.update()
122.     FPSCLOCK.tick(FPS)

此时,游戏状态已经根据玩家的输入进行了更新,最新的游戏状态已经被绘制到DISPLAYSURF这个Surface对象中。我们已经到达了游戏循环的末尾,因此我们调用pygame.display.update()DISPLAYSURF Surface 对象绘制到计算机屏幕上。

第 9 行将 FPS 常量设置为整数值30,这意味着我们希望游戏(最多)以每秒 30 帧的速度运行。如果我们希望程序运行得更快,我们可以增加这个数字。如果我们希望程序运行得更慢,我们可以减少这个数字。它甚至可以设置为像0.5这样的浮点值,这将以每秒半帧的速度运行程序,即每两秒一帧。

为了在每秒30帧的运行中,每帧必须在1/30秒绘制完成。这意味着pygame.display.update() 和游戏循环中的所有代码必须在 33.3 毫秒内执行。任何现代计算机都可以轻松完成此操作,只需剩余大量时间。为了防止程序运行得太快,我们 在FPSCLOCK中调用了pygame.Clock对象的tick()方法,让它在剩下的 33.3 毫秒内暂停程序。

由于这是在游戏循环的最后完成的,它确保游戏循环的每次迭代需要(至少)33.3 毫秒。如果由于某种原因pygame.display.update() 调用和游戏循环中的代码花费的时间超过 33.3 毫秒,则tick()方法根本不会等待并立即返回。

我一直在说其他功能将在本章后面解释。现在我们已经了解了main() 函数,并且您对一般程序的工作方式有了一个了解,让我们深入了解从main()调用的所有其他函数的细节。

创建“显示框”数据结构

125. def generateRevealedBoxesData(val):
126.     revealedBoxes = []
127.     for i in range(BOARDWIDTH):
128.         revealedBoxes.append([val] * BOARDHEIGHT)
129.     return revealedBoxes

generateRevealedBoxesData()函数需要创建一个列表,该列表的元素是元素值为布尔值的列表。布尔值作为val 参数传递给函数。我们开始将数据结构revealBoxes变量设置为一个空列表。

为了使数据结构具有revealBoxes[x][y]结构,我们需要确保内部列表代表棋盘的垂直列而不是水平行。否则,数据结构将有一个RevealedBoxes[y][x] 结构。

该循环将创建列,然后把它们添加到revealedBoxes。列是使用列表复制创建的,因此列列表具有与BOARDHEIGHT 指示的一样多的 val值。

创建棋盘数据结构:步骤 1 – 获取所有可能的图标

132. def getRandomizedBoard():
133.     # Get a list of every possible shape in every possible color.
134.     icons = []
135.     for color in ALLCOLORS:
136.         for shape in ALLSHAPES:
137.             icons.append( (shape, color) )

board 数据结构是一个元组列表的列表,其中每个元组有两个值:一个是图标的形状,一个是图标的颜色。创建这个数据结构有点复杂。我们需要确保有与棋盘上方框数量完全相同的图标,并且确保每种类型只有两个且只有两个图标。

执行此操作的第一步是创建一个列表,其中包含各种可能的形状和颜色组合。回想一下,我们在ALLCOLORS和ALLSHAPES中有每个颜色和形状的列表,因此第 135 和 136 行的嵌套for循环将遍历每种可能颜色的每种可能形状。这些都添加到第 137 行的图标变量列表中。

第 2 步 – 改组和截断所有图标的列表

139.     random.shuffle(icons) # randomize the order of the icons list
140.     numIconsUsed = int(BOARDWIDTH * BOARDHEIGHT / 2) # calculate how many icons are needed
141.     icons = icons[:numIconsUsed] * 2 # make two of each
142.     random.shuffle(icons)

但请记住,可能有比棋盘上的空格更多的可能组合。我们需要通过BOARDWIDTH乘以BOARDHEIGHT计算的电路棋盘上的空格数。然后我们将该数字除以 2,因为我们将有成对的图标。在一个有 70 个空格的棋盘上,我们只需要 35 个不同的图标,因为每个图标将有两个。这个数字将存储在numIconsUsed 中。

第 141 行使用列表切片来获取列表中第一个numIconsUsed数量的图标。(如果您忘记了列表切片的工作原理,请查看http://invpy.com/slicing。)该列表已在第 139 行重新排列,因此每个游戏的图标并不总是相同。然后使用* 运算符复制此列表,以便每个图标都有两个。这个新的加倍列表将覆盖图标变量中的旧列表。由于这个新列表的前半部分与后半部分相同,我们再次调用shuffle()方法来随机混合图标的顺序。

第 3 步 – 将图标放置在棋盘上

144.     # Create the board data structure, with randomly placed icons.
145.     board = []
146.     for x in range(BOARDWIDTH):
147.         column = []
148.         for y in range(BOARDHEIGHT):
149.             column.append(icons[0])
150.             del icons[0] # remove the icons as we assign them
151.         board.append(column)
152.     return board

现在我们需要为棋盘创建一个元素为列表数据结构的列表。我们可以使用嵌套的for循环来做到这一点,就像generateRevealedBoxesData()函数所做的那样。对于棋盘上的每一列,我们将创建一个随机选择的图标列表。就像在第 149 行向列中添加图标一样,在第 150 行将它们从图标列表中逐个删除。这样,随着图标列表越来越短,icons[0] 将有不同的图标要添加到列。

为了更好地说明这一点,请在交互式 shell 中键入以下代码。请注意del语句如何更改myList列表。

>>> myList = ['cat', 'dog', 'mouse', 'lizard']
>>> del myList[0]
>>> myList
['dog', 'mouse', 'lizard']
>>> del myList[0]
>>> myList
['mouse', 'lizard']
>>> del myList[0]
>>> myList
['lizard']
>>> del myList[0]
>>> myList
[]

因为我们要删除列表前面的项目,所以其他项目向前移动,以便列表中的下一个项目成为新的“第一个”项目。这与第 150 行的工作方式相同。

将列表拆分为列表的列表

155. def splitIntoGroupsOf(groupSize, theList):
156.     # splits a list into a list of lists, where the inner lists have at
157.     # most groupSize number of items.
158.     result = []
159.     for i in range(0, len(theList), groupSize):
160.         result.append(theList[i:i + groupSize])
161.     return result

splitIntoGroupsOf()函数(这将由被称为startGameAnimation()函数调用)将列表分割并放入新的列表中作为列表项,其中内部的列表具有GROUPSIZE的数量元素的列表。(如果剩余的项目少于groupSize,最后一个列表会少。)

在159行调用包含三个参数的range()函数 。(如果您不熟悉此形式,请查看http://invpy.com/range。)让我们使用一个示例说明,如果列表的长度为20 且groupSize参数为8,则range(0, len(theList), groupSize)的计算结果为range(0, 20, 8)。对于 for循环的三个迭代,这将为i 变量提供值0、8和16。

在第 160 行使用theList[i:i + groupSize] 进行列表切片创建了添加到结果列表中的列表。在i 为0、8和16(groupSize为8)的每次迭代中,此列表切片表达式将是theList[0:8],然后 是第二次迭代时的theList[8:16],然后是theList[16: 24]在第三次迭代中。

请注意,即使在我们的示例中theList的最大索引是19,即使24 大于19,theList[16:24]也不会引发IndexError错误。它只会创建一个列表切片,其中包含列表中的其余项目。列表切片不会破坏或更改存储在theList 中的原始列表。它只是复制其中的一部分以作为新的列表值。第160 行这个新的列表值附加到结果变量列表中。因此,当我们在此函数的末尾返回结果时,我们返回一个元素值是列表的列表。

不同的坐标系

164. def leftTopCoordsOfBox(boxx, boxy):
165.     # Convert board coordinates to pixel coordinates
166.     left = boxx * (BOXSIZE + GAPSIZE) + XMARGIN
167.     top = boxy * (BOXSIZE + GAPSIZE) + YMARGIN
168.     return (left, top)

您应该熟悉笛卡尔坐标系。(如果您想复习这个主题,请阅读http://invpy.com/coordinates。)在我们的大多数游戏中,我们将使用多个笛卡尔坐标系。Memory Puzzle 游戏中使用的一种坐标系统是像素或屏幕坐标。但是我们还将为框使用另一个坐标系。这是因为它使用(3,2)来指代从左侧箱第4个 和从顶部第3个(记住,号码从0开始,而不是1)会更容易,而不是使用的像素坐标框的左上角,(220, 165)。但是,我们需要一种在这两个坐标系之间进行转换的方法。

这是游戏的图片和两个不同的坐标系。请记住,窗口宽 640 像素,高 480 像素,因此 (639, 479) 是右下角(因为左上角的像素是 (0, 0),而不是 (1, 1))。
在这里插入图片描述

该·leftTopCoordsOfBox()·函数将采取箱坐标并像素坐标。因为一个框在屏幕上占据多个像素,所以我们将始终返回框左上角的单个像素。该值将作为两个整数元组返回。当我们需要绘制这些框像素坐标时往往会调用leftTopCoordsOfBox()

从像素坐标转换为框坐标

171. def getBoxAtPixel(x, y):
172.     for boxx in range(BOARDWIDTH):
173.         for boxy in range(BOARDHEIGHT):
174.             left, top = leftTopCoordsOfBox(boxx, boxy)
175.             boxRect = pygame.Rect(left, top, BOXSIZE, BOXSIZE)
176.             if boxRect.collidepoint(x, y):
177.                 return (boxx, boxy)
178.     return (None, None)

我们还需要一个函数来将像素坐标(鼠标点击和鼠标移动事件使用的坐标)转换为框坐标(以便我们可以找出鼠标事件发生在哪个框上)。Rect 对象有一个collidepoint()方法,您也可以传递 X 和 Y 坐标, 如果坐标在 Rect 对象的区域内(即碰撞),它将返回True。

为了找到鼠标坐标在哪个盒子上,我们将遍历每个盒子的坐标,并 在具有这些坐标的 Rect 对象上调用collidepoint()方法。当collidepoint()返回True 时,我们知道找到了被点击或移过的框,并将返回框坐标。如果它们都没有返回True,那么getBoxAtPixel()函数将返回值(None, None)。这个元组被返回而不是简单地返回None 因为getBoxAtPixel()的调用者期望返回一个包含两个值的元组。

绘制图标和语法糖(Syntactic sugar)

181. def drawIcon(shape, color, boxx, boxy):
182.     quarter = int(BOXSIZE * 0.25) # syntactic sugar
183.     half =    int(BOXSIZE * 0.5)  # syntactic sugar
184.
185.     left, top = leftTopCoordsOfBox(boxx, boxy) # get pixel coords from board coords

drawIcon()函数将在参数boxx和boxy指定的坐标处绘制图标(具有指定形状和颜色)。每个可能的形状都有一组不同的 Pygame 绘图函数调用,因此我们必须有大量的ifelif语句来区分它们。(这些陈述在第 187 到 198 行。)

可以通过调用·leftTopCoordsOfBox()· 函数获取框的左上边缘的 X 和 Y 坐标。盒子的宽度和高度都在BOXSIZE常量中设置。但是,许多形状绘制函数调用也使用框的中点和四分之一上的点。我们可以计算它并将其存储在变量quarterhalf 中。我们可以很容易地使用代码int(BOXSIZE * 0.25),而不是变量quarter,这种方式的代码变得更容易阅读,因为它是比较明显的 。

这些变量是语法糖的一个例子。语法糖(Syntactic sugar)是当我们添加可以用另一种方式编写的代码(可能使用较少的实际代码和变量),使源代码更易于阅读。常量变量是一种语法糖。预先计算一个值并将其存储在变量中是另一种类型的语法糖。
例如,在getRandomizedBoard()函数中,我们可以轻松地将第 140 行和第 141 行的代码

141.     icons = icons[:numIconsUsed] * 2 # make two of each
142.     random.shuffle(icons)

变成一行代码。
random.shuffle(icons = icons[:numIconsUsed] * 2)
但作为两行单独的行更容易阅读。
我们不需要额外quarterhalf变量,但拥有它们会使代码更易于阅读。易于阅读的代码便于日后调试和升级。

144.     # Draw the shapes
145.     if shape == DONUT:
146.         pygame.draw.circle(DISPLAYSURF, color, (left + half, top + half), half - 5)
147.         pygame.draw.circle(DISPLAYSURF, BGCOLOR, (left + half, top + half), quarter - 5)
148.     elif shape == SQUARE:
149.         pygame.draw.rect(DISPLAYSURF, color, (left + quarter, top + quarter, BOXSIZE - half, BOXSIZE - half))
150.     elif shape == DIAMOND:
151.         pygame.draw.polygon(DISPLAYSURF, color, ((left + half, top), (left + BOXSIZE - 1, top + half), (left + half, top + BOXSIZE - 1), (left, top + half)))
152.     elif shape == LINES:
153.         for i in range(0, BOXSIZE, 4):
154.             pygame.draw.line(DISPLAYSURF, color, (left, top + i), (left + i, top))
155.             pygame.draw.line(DISPLAYSURF, color, (left + i, top + BOXSIZE - 1), (left + BOXSIZE - 1, top + i))
156.     elif shape == OVAL:
157.         pygame.draw.ellipse(DISPLAYSURF, color, (left, top + quarter, BOXSIZE, half))

donut, square, diamond, lines, and oval函数中的每一个都需要不同的绘图基元函数调用来创建。

获得棋盘空间图标形状和颜色的语法糖

202. def getShapeAndColor(board, boxx, boxy):
203.     # shape value for x, y spot is stored in board[x][y][0]
204.     # color value for x, y spot is stored in board[x][y][1]
205.     return board[boxx][boxy][0], board[boxx][boxy][1]

getShapeAndColor()函数只有一行。您可能想知道为什么我们需要一个函数,而不是在需要时只输入一行代码。这样做的原因与我们使用常量变量的原因相同:它提高了代码的可读性。

很容易弄清楚像shape, color = getShapeAndColor()这样的代码是做什么的。但是,如果您查看类似shape, color = board[boxx][boxy][0], board[boxx][boxy][1]之类的代码,就会有点难以弄清楚。

绘制盒盖

208. def drawBoxCovers(board, boxes, coverage):
209.     # Draws boxes being covered/revealed. "boxes" is a list
210.     # of two-item lists, which have the x & y spot of the box.
211.     for box in boxes:
212.         left, top = leftTopCoordsOfBox(box[0], box[1])
213.         pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top, BOXSIZE, BOXSIZE))
214.         shape, color = getShapeAndColor(board, box[0], box[1])
215.         drawIcon(shape, color, box[0], box[1])
216.         if coverage > 0: # only draw the cover if there is an coverage
217.             pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, coverage, BOXSIZE))
218.     pygame.display.update()
219.     FPSCLOCK.tick(FPS)

drawBoxCovers()函数具有三个参数:棋盘数据结构,每个盒子绘制时(X,Y)元组列表,然后绘制盒子的数目。

由于我们希望对box参数中的每个框使用相同的绘制代码,我们将在第 211 行使用for循环,因此我们对框列表中的每个框执行相同的代码。在这个for循环中,代码应该做三件事:绘制背景颜色(覆盖之前绘制的任何东西),绘制图标,然后在需要的图标上绘制任意多的白框。leftTopCoordsOfBox()函数将返回盒子左上角的像素坐标。第 216 行的if语句确保如果coverage 中的数字 恰好小于0,我们将不会调用pygame.draw.rect()功能。

coverage参数为0 时,根本没有覆盖。当coverage设置为20 时,会有一个 20 像素宽的白色框覆盖图标。我们希望coverage设置的最大尺寸为BOXSIZE 值时,其中整个图标被完全覆盖。

drawBoxCovers()将从与游戏循环不同的单独循环中调用。因此,它需要自己调用pygame.display.update()FPSCLOCK.tick(FPS)来显示动画。(这确实意味着在这个循环中,没有运行任何代码来处理正在生成的任何事件。这很好,因为封面和展示动画只需要一秒钟左右的时间。)

处理显示和覆盖动画

222. def revealBoxesAnimation(board, boxesToReveal):
223.     # Do the "box reveal" animation.
224.     for coverage in range(BOXSIZE, (-REVEALSPEED) - 1, - REVEALSPEED):
225.         drawBoxCovers(board, boxesToReveal, coverage)
226.
227.
228. def coverBoxesAnimation(board, boxesToCover):
229.     # Do the "box cover" animation.
230.     for coverage in range(0, BOXSIZE + REVEALSPEED, REVEALSPEED):
231.         drawBoxCovers(board, boxesToCover, coverage)

请记住,动画只是在短时间内显示不同的图像,它们一起使屏幕上的事物看起来好像在移动。revealBoxesAnimation()coverBoxesAnimation()只需要绘制不同量的白框来覆盖图标。我们可以编写一个名为drawBoxCovers()的函数来完成此操作,然后让我们的动画函数 为每一帧动画调用drawBoxCovers()。正如我们在上一节中看到的,drawBoxCovers()调用pygame.display.update()FPSCLOCK.tick(FPS)本身。

为此,我们将设置一个for 循环来减少(在revealBoxesAnimation()中)或增加(在coverBoxesAnimation()中)converage参数的数值。converage变量的数值将依据REVEALSPEED常量来增加或减少。在第 12 行,我们将此常量设置为8,这意味着在每次调用drawBoxCovers() 时,每次迭代时,白框将减少/增加 8 个像素。如果我们增加这个数字,那么每次调用都会绘制更多的像素,这意味着白色框的大小会更快地减小/增加。如果我们将其设置为1,那么每次迭代时,白色框只会减少或增加 1 个像素,从而使整个显示或覆盖动画花费更长的时间。

把它想象成爬楼梯。如果你每走一步,你都爬了一个楼梯,那么爬整个楼梯需要正常的时间。但是,如果您在每一步爬两个楼梯(并且这些步骤的时间和以前一样长),您可以以两倍的速度爬整个楼梯。如果你一次可以爬 8 层楼梯,那么你可以以 8 倍的速度爬整个楼梯。

绘制整个棋盘

234. def drawBoard(board, revealed):
235.     # Draws all of the boxes in their covered or revealed state.
236.     for boxx in range(BOARDWIDTH):
237.         for boxy in range(BOARDHEIGHT):
238.             left, top = leftTopCoordsOfBox(boxx, boxy)
239.             if not revealed[boxx][boxy]:
240.                 # Draw a covered box.
241.                 pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, BOXSIZE, BOXSIZE))
242.             else:
243.                 # Draw the (revealed) icon.
244.                 shape, color = getShapeAndColor(board, boxx, boxy)
245.                 drawIcon(shape, color, boxx, boxy)

drawBoard()函数在棋盘上的盒子调用drawIcon()。第236 行和第 237 行的嵌套for循环将遍历框的每个可能的 X 和 Y 坐标,并将在该位置绘制图标或绘制一个白色正方形(以表示被覆盖的框)。

高亮绘制

248. def drawHighlightBox(boxx, boxy):
249.     left, top = leftTopCoordsOfBox(boxx, boxy)
250.     pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5, top - 5, BOXSIZE + 10, BOXSIZE + 10), 4)

为了帮助玩家认识到他们可以点击一个被覆盖的盒子来显示它,我们将在一个盒子周围出现一个蓝色轮廓以突出显示它。这个轮廓是通过调用pygame.draw.rect() 来绘制的,以创建一个宽度为 4 像素的矩形。

“开始游戏”动画

253. def startGameAnimation(board):
254.     # Randomly reveal the boxes 8 at a time.
255.     coveredBoxes = generateRevealedBoxesData(False)
256.     boxes = []
257.     for x in range(BOARDWIDTH):
258.         for y in range(BOARDHEIGHT):
259.             boxes.append( (x, y) )
260.     random.shuffle(boxes)
261.     boxGroups = splitIntoGroupsOf(8, boxes)

游戏开始时播放的动画为玩家提供了所有图标所在位置的快速提示。为了制作这个动画,我们必须一个接一个地显示和掩盖成组的盒子。为此,首先我们将创建一个包含棋盘上所有可能盒子的列表。第257 和 258 行的嵌套for循环会将 (X, Y) 元组添加到boxes列表变量中。

我们将显示并掩盖此列表中的前 8 个框,然后是接下来的 8 个,然后是接下来的 8 个,依此类推。但是,由于最初框中 (X, Y) 元组的顺序每次都相同,因此将显示相同顺序的框。(尝试将第 260 行注释掉,然后运行几次以查看此效果。),我们需要打乱顺序,随机显示。

为了在每次游戏开始时改变盒子,我们将调用random.shuffle()函数来随机打乱盒子列表中元组的顺序。然后,当我们显示并覆盖此列表中的前 8 个盒子(以及之后的每组 8 个盒子)时,它将是随机的 8 个盒子。

为了获得 8 个盒子的列表,我们调用我们的splitIntoGroupsOf()函数,传递8boxes 列表。函数返回的列表中的列表将存储在名为boxGroups的变量中。

显示和覆盖框组

263.     drawBoard(board, coveredBoxes)
264.     for boxGroup in boxGroups:
265.         revealBoxesAnimation(board, boxGroup)
266.         coverBoxesAnimation(board, boxGroup)

首先,我们画棋盘。由于CoveredBoxes中的每个值都设置为False,因此对drawBoard()的调用最终将仅绘制被覆盖的白框。revealBoxesAnimation()coverBoxesAnimation()函数将在这些白盒的空间上绘制。

循环将处理boxGroups列表中的每个列表。我们将这些传递给revealBoxesAnimation(),它将执行拉开白框的动画以显示下面的图标。然后调用coverBoxesAnimation()将使白框展开动画以覆盖图标。然后for循环进入下一次迭代,为下一组 8 个框设置动画。

“游戏获胜”动画

269. def gameWonAnimation(board):
270.     # flash the background color when the player has won
271.     coveredBoxes = generateRevealedBoxesData(True)
272.     color1 = LIGHTBGCOLOR
273.     color2 = BGCOLOR
274.
275.     for i in range(13):
276.         color1, color2 = color2, color1 # swap colors
277.         DISPLAYSURF.fill(color1)
278.         drawBoard(board, coveredBoxes)
279.         pygame.display.update()
280.         pygame.time.wait(300)

当玩家通过匹配棋盘上的每一对来揭开所有的盒子时,我们想通过闪烁背景颜色来祝贺他们。在用于循环将绘制以color1变量为颜色背景,然后绘制棋盘上。然而,每次迭代进行循环,在276行值color1color2彼此交替这种方式,程序将绘制两种不同的背景颜色之间交替。

请记住,此函数需要调用pygame.display.update()才能真正使DISPLAYSURF表面出现在屏幕上。

判断玩家是否赢了

283. def hasWon(revealedBoxes):
284.     # Returns True if all the boxes have been revealed, otherwise False
285.     for i in revealedBoxes:
286.         if False in i:
287.             return False # return False if any boxes are covered.
288.     return True

当所有图标对都匹配时,玩家就赢得了比赛。由于图标已经匹配时“revealed”数据结构的值已经设置为True,我们可以通过每一个空间的简单循环revealedBoxes寻找一个假值。如果即使是一个False 值在revealBoxes 中,那么我们就知道棋盘上仍然有不匹配的图标。

请注意,因为revealBoxes 是一个列表的列表,第285 行的for循环会将内部列表设置为i的值。但是我们可以使用 in 运算符在整个内部列表中搜索False值。这样我们就不需要编写额外的代码行,也不需要像这样有两个嵌套的for循环:

for x in revealedBoxes:
    for y in revealedBoxes[x]:
        if False == revealedBoxes[x][y]:
            return False

为什么要使用main() 函数?

291. if __name__ == '__main__':
292.     main()

拥有main() 函数似乎毫无意义,因为您可以将该代码放在程序底部的全局范围内,并且代码将运行完全相同。但是,将它们放在main() 函数中有两个很好的理由。

首先,这让您拥有局部变量,否则main()函数中的局部变量必须成为全局变量。限制全局变量的数量是保持代码简单和易于调试的好方法。(请参阅本章中的“为什么全局变量是邪恶的”部分。)

其次,这还允许您导入程序,以便您可以调用和测试单个函数。如果memorypuzzle.py文件在 C:\Python32 文件夹中,那么您可以从交互式 shell 中导入它。键入以下内容以测试splitIntoGroupsOf()getBoxAtPixel()函数以确保它们返回正确的返回值:

>>> import memorypuzzle
>>> memorypuzzle.splitIntoGroupsOf(3, [0,1,2,3,4,5,6,7,8,9])
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
>>> memorypuzzle.getBoxAtPixel(0, 0)
(None, None)
>>> memorypuzzle.getBoxAtPixel(150, 150)
(1, 1)

导入模块时,将运行其中的所有代码。如果我们没有main()函数,并且在全局范围内有它的代码,那么我们一导入游戏就会自动启动,这真的不会让我们在其中调用单个函数。

这就是为什么代码位于我们命名为main()的单独函数中的原因。然后我们检查内置的 Python 变量__name__来看看我们是否应该调用main()函数。 如果程序本身正在运行,则该变量由 Python 解释器自动设置为字符串’__main__’,如果 正在导入,则为’memorypuzzle’。这就是为什么 当我们 在交互式 shell 中执行import memorypuzzle语句时main()函数没有运行的原因。

这是一种方便的技术,可以从交互式 shell 导入您正在处理的程序,并通过一次测试一次调用来确保各个函数返回正确的值。

为什么要考虑可读性?

本章中的很多建议并不是关于如何编写计算机可以运行的程序,而是关于如何编写程序员可以阅读的程序。您可能不明白为什么这很重要。毕竟,只要代码有效,谁在乎人类程序员阅读是难还是容易?

然而,要意识到软件的重要一点是它很少被孤立。当您创建自己的游戏时,您很少会“完成”程序。您总是会获得想要添加的游戏功能的新想法,或者发现程序的新错误。因此,重要的是您的程序可读,以便您可以查看代码并理解它。理解代码是更改代码以添加更多代码或修复错误的第一步。

例如,这是一个完全不可读的 Memory Puzzle 程序的混淆版本。如果您输入(或从http://invpy.com/memorypuzzle_obfuscated.py下载)并运行它,您会发现它的运行方式与本章开头的代码完全相同。但是如果这段代码有错误,就不可能阅读代码并理解发生了什么,更不用说修复错误了。

计算机不介意像这样不可读的代码。对它来说都是一样的。

import random, pygame, sys
from pygame.locals import *

def hhh():
    global a, b
    pygame.init()
    a = pygame.time.Clock()
    b = pygame.display.set_mode((640, 480))
    j = 0
    k = 0
    pygame.display.set_caption('Memory Game')

    i = c()
    hh = d(False)
    h = None

    b.fill((60, 60, 100))
    g(i)

    while True:
        e = False
        b.fill((60, 60, 100))
        f(i, hh)
        for eee in pygame.event.get():
            if eee.type == QUIT or (eee.type == KEYUP and eee.key == K_ESCAPE):
                pygame.quit()
                sys.exit()
            elif eee.type == MOUSEMOTION:
                j, k = eee.pos
            elif eee.type == MOUSEBUTTONUP:
                j, k = eee.pos
                e = True
        bb, ee = m(j, k)
        if bb != None and ee != None:
            if not hh[bb][ee]:
                n(bb, ee)
            if not hh[bb][ee] and e:
                o(i, [(bb, ee)])
                hh[bb][ee] = True
                if h == None:
                    h = (bb, ee)
                else:
                    q, fff = s(i, h[0], h[1])
                    r, ggg = s(i, bb, ee)
                    if q != r or fff != ggg:
                        pygame.time.wait(1000)
                        p(i, [(h[0], h[1]), (bb, ee)])
                        hh[h[0]][h[1]] = False
                        hh[bb][ee] = False
                    elif ii(hh):
                        jj(i)
                        pygame.time.wait(2000)
                        i = c()
                        hh = d(False)
                        f(i, hh)
                        pygame.display.update()
                        pygame.time.wait(1000)
                        g(i)

                    h = None
        pygame.display.update()
        a.tick(30)

def d(ccc):
    hh = []
    for i in range(10):
        hh.append([ccc] * 7)
    return hh

def c():
    rr = []
    for tt in ((255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 128, 0), (255, 0, 255), (0, 255, 255)):
        for ss in ('a', 'b', 'c', 'd', 'e'):
            rr.append( (ss, tt) )
    random.shuffle(rr)
    rr = rr[:35] * 2
    random.shuffle(rr)
    bbb = []
    for x in range(10):
        v = []
        for y in range(7):
            v.append(rr[0])
            del rr[0]
        bbb.append(v)
    return bbb

def t(vv, uu):
    ww = []
    for i in range(0, len(uu), vv):
        ww.append(uu[i:i + vv])
    return ww

def aa(bb, ee):
    return (bb * 50 + 70, ee * 50 + 65)
def m(x, y):
    for bb in range(10):
        for ee in range(7):
            oo, ddd = aa(bb, ee)
            aaa = pygame.Rect(oo, ddd, 40, 40)
            if aaa.collidepoint(x, y):
                return (bb, ee)
    return (None, None)

def w(ss, tt, bb, ee):
    oo, ddd = aa(bb, ee)
    if ss == 'a':
        pygame.draw.circle(b, tt, (oo + 20, ddd + 20), 15)
        pygame.draw.circle(b, (60, 60, 100), (oo + 20, ddd + 20), 5)
    elif ss == 'b':
        pygame.draw.rect(b, tt, (oo + 10, ddd + 10, 20, 20))
    elif ss == 'c':
        pygame.draw.polygon(b, tt, ((oo + 20, ddd), (oo + 40 - 1, ddd + 20), (oo + 20, ddd + 40 - 1), (oo, ddd + 20)))
    elif ss == 'd':
        for i in range(0, 40, 4):
            pygame.draw.line(b, tt, (oo, ddd + i), (oo + i, ddd))
            pygame.draw.line(b, tt, (oo + i, ddd + 39), (oo + 39, ddd + i))
    elif ss == 'e':
        pygame.draw.ellipse(b, tt, (oo, ddd + 10, 40, 20))

def s(bbb, bb, ee):
    return bbb[bb][ee][0], bbb[bb][ee][1]

def dd(bbb, boxes, gg):
    for box in boxes:
        oo, ddd = aa(box[0], box[1])
        pygame.draw.rect(b, (60, 60, 100), (oo, ddd, 40, 40))
        ss, tt = s(bbb, box[0], box[1])
        w(ss, tt, box[0], box[1])

        if gg > 0:
            pygame.draw.rect(b, (255, 255, 255), (oo, ddd, gg, 40))
    pygame.display.update()
    a.tick(30)

def o(bbb, cc):
    for gg in range(40, (-8) - 1, -8):
        dd(bbb, cc, gg)

def p(bbb, ff):
    for gg in range(0, 48, 8):
        dd(bbb, ff, gg)

def f(bbb, pp):
    for bb in range(10):
        for ee in range(7):
            oo, ddd = aa(bb, ee)
            if not pp[bb][ee]:
                pygame.draw.rect(b, (255, 255, 255), (oo, ddd, 40, 40))
            else:
                ss, tt = s(bbb, bb, ee)
                w(ss, tt, bb, ee)
def n(bb, ee):
    oo, ddd = aa(bb, ee)
    pygame.draw.rect(b, (0, 0, 255), (oo - 5, ddd - 5, 50, 50), 4)
def g(bbb):
    mm = d(False)
    boxes = []
    for x in range(10):
        for y in range(7):
            boxes.append( (x, y) )
    random.shuffle(boxes)
    kk = t(8, boxes)
    f(bbb, mm)
    for nn in kk:
        o(bbb, nn)
        p(bbb, nn)
def jj(bbb):
    mm = d(True)
    tt1 = (100, 100, 100)
    tt2 = (60, 60, 100)
    for i in range(13):
        tt1, tt2 = tt2, tt1
        b.fill(tt1)
        f(bbb, mm)
        pygame.display.update()
        pygame.time.wait(300)

def ii(hh):
    for i in hh:
        if False in i:
            return False
    return True

if __name__ == '__main__':
    hhh()

永远不要写这样的代码。如果你在关灯的浴室里对着镜子这样编程,Ada Lovelace 的幽灵会从镜子里出来,把你扔进提花织机的下巴里。

总结和黑客建议

本章涵盖了 Memory Puzzle 程序如何工作的完整说明。再次阅读本章和源代码以更好地理解它。本书中的许多其他游戏程序都使用了相同的编程概念(例如嵌套for循环、语法糖和同一程序中的不同坐标系),因此为了使本书简短,将不再对其进行解释。

尝试理解代码如何工作的一个想法是通过注释掉随机行来故意破坏它。对某些行执行此操作可能会导致语法错误,从而完全阻止脚本运行。但是注释掉其他行会导致奇怪的错误和其他很酷的效果。尝试这样做,然后找出为什么程序会出现错误。

这也是能够将您自己的秘密作弊或黑客添加到程序中的第一步。通过打破程序的正常功能,您可以学习如何更改它以实现一些巧妙的效果(例如秘密地提示您如何解决难题)。随意尝试。如果您想再次玩常规游戏,您始终可以将未更改源代码的副本保存在不同的文件中。

事实上,如果你想练习修复错误,这个游戏的源代码有几个版本都有小错误。您可以从http://invpy.com/buggy/memorypuzzle下载这些有问题的版本。尝试运行程序以找出错误是什么,以及程序为什么会这样。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值