孩子们的编程教学指南-全-

孩子们的编程教学指南(全)

原文:zh.annas-archive.org/md5/c6f249a0d80bfd1309d65403e539b29f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言:什么是编程,为什么它对你的孩子有益?

计算机编程或编码是每个孩子都应该学习的关键技能。我们使用计算机来解决问题、玩游戏、帮助我们更高效地工作、执行重复性任务、存储和回忆信息、创造新事物,并与朋友和世界连接。理解如何编码,让我们将所有这些力量掌握在自己手中。

每个人都可以学习编程;它就像解谜或者破解难题。你应用逻辑,尝试一种解决方案,再多做些实验,然后就能解决问题。现在就是开始学习编程的最佳时机!我们正处于一个前所未有的历史时期:以前从未有过像今天这样,数十亿人能够像现在这样每天通过计算机彼此连接。我们生活在一个充满新可能的世界里,从电动汽车、机器人护理员,到送货无人机,甚至是披萨外送。

如果你的孩子今天开始学习编程,他们可以帮助定义这个快速变化的世界。

为什么孩子们应该学习编程?

学习计算机编程有许多很好的理由,以下是我个人的两大理由:

  • 编程很有趣。

  • 编程是一项有价值的工作技能。

编程很有趣

技术正在成为日常生活的一部分。每家公司、慈善机构和事业都可以从技术中受益。现在有应用程序可以帮助你购物、捐赠、加入、玩耍、志愿、连接、分享——几乎任何你能想象的事情。

你的孩子们是否想为他们最喜欢的视频游戏创建自己的关卡?编码员就是这么做的!那他们是否想创建自己的手机应用程序呢?他们可以通过在计算机上编程将这个创意变为现实!他们见过的每一个程序、游戏、系统或应用,都使用了他们将在这本书中学到的相同编程构建模块。当孩子们进行编程时,他们积极参与到技术中——他们不仅仅是在,他们是在创造有趣的东西!

编程是一项有价值的工作技能

编程是 21 世纪的核心技能。今天的工作比以往任何时候都更加需要解决问题的能力,越来越多的职业将技术作为不可或缺的要求。

美国劳工统计局预测,在未来五年内,超过 800 万个科技岗位将被创造出来。2014-2015 年《职业展望手册》中,十个增长最快的职位中,有七个不要求硕士或博士学位的职位属于计算机科学或信息技术(IT)领域。

马克·扎克伯格在 2004 年还是一名大学生,他在宿舍里开发了 Facebook 的第一个版本。仅仅 10 年后,已有 13.9 亿人每月使用 Facebook(来源:* newsroom.fb.com/company-info/*)。历史上从未有过任何一个创意、产品或服务能够在不到十年的时间里吸引十亿人。Facebook 展示了技术的力量,它比以往任何时候都更快地让更多人连接在一起。

孩子们可以在哪里学习编程?

本书仅仅是开始。如今,有比以往更多的地方可以学习编程;像 Code.org、Codecademy(参见图 1)以及无数其他网站,教授从基础到高级的编程,涵盖了各种需求量大的编程语言。一旦你和你的孩子完成了本书的学习,他们可以通过 EdX、Udacity 和 Coursera 等网站免费参加课程,进一步扩展他们的学习。

Codecademy 教你如何一步步用多种编程语言进行编程。

图 1. Codecademy 教你如何一步步用多种编程语言进行编程。

“编程俱乐部”是和朋友一起快乐学习的好方法。获得相关领域的大学学位仍然是为职业生涯做准备的最佳方式之一,但即使目前无法上大学,你的孩子也可以开始建立编程作品集,展示他们作为程序员和问题解决者的技能。

如何使用本书

本书不仅仅是为孩子们准备的——它同样适合父母、教师、学生以及那些希望了解计算机编程基础的成年人,无论是为了娱乐还是为了进入高科技行业的新工作。不论年龄多大,你都可以愉快地学习编程基础。最好的方法是通过实验和共同努力来学习。

探索!

学习编程是令人兴奋的,只要你愿意尝试新事物。当你和孩子们跟随本书中的程序一起学习时,尝试改变代码中的数字和文本,看看程序会发生什么。即使把它弄坏了,修复时你也能学到新东西。在最坏的情况下,你只需要重新输入书中的例子或打开最后保存的正常版本。学习编程的重点是尝试新事物,学习新技能,并以新方式解决问题。确保你的孩子们也在玩耍——通过改变某些内容、保存程序、运行它、看看发生了什么,并修复任何错误来测试他们的代码。

学习编程的重点是尝试新事物,学习一项新技能,并以一种新的方式解决问题。通过改变一些内容、保存程序、运行它、查看发生了什么,并在需要时修复错误,来测试你的代码。

举个例子,我写了一些代码做了一幅彩色图画(图 2),然后回过头,改了一些数字,再次运行程序。这给我带来了另一幅完全不同但同样惊艳的画作。我再次回去,改了其他数字,得到了又一幅美丽独特的图画。你看看,只是通过玩一玩就能做到这些吗?

我通过在一个程序中的代码行尝试不同的数值所创建的三幅彩色螺旋图

图 2. 我通过在一个程序中的代码行尝试不同的数值所创建的三幅彩色螺旋图

一起做!

实验编程代码是学习程序如何运作的好方法,如果你和别人一起做会更加有效。无论你是在教孩子或学生,还是自己在学习,和别人一起玩代码不仅更有趣,而且也更有效

例如,在铃木音乐教育法中,家长和孩子一起上课,甚至提前学习,以便在课程之间帮助孩子。早期开始是铃木方法的另一个特点;孩子们可以在三岁或四岁时开始正式学习。

我在我的两个儿子分别两岁和四岁时开始向他们介绍编程,我鼓励他们通过改变每个程序中的一些小部分,比如颜色、形状和形状的大小,来玩得开心。

我在 13 岁时通过输入书本中的程序示例并修改它们来让它们做一些新的事情,学习了编程。现在,在我教授的计算机科学课程中,我经常给学生一个程序,并鼓励他们动手修改代码,创造出新的东西。

如果你正在用这本书自学,你可以通过找到一个朋友一起练习示例,或者通过成立一个课后或社区编程俱乐部来与他人合作(可以参考coderdojo.com/www.codecademy.com/afterschool/ 获取创意和建议)。编程是一项团队运动!

在线资源

本书的所有程序文件都可以在www.nostarch.com/teachkids/下载,此外还包括编程挑战的示例解答及其他信息。下载程序并进行实验,学习更多。如果卡住了,可以参考示例解答。赶紧去看看吧!

没有标题的图片

编程 = 解决问题

无论你的孩子是 2 岁在学习数数,还是 22 岁在寻找新的挑战,这本书及其介绍的概念都是通向一个有益、激励人心的兴趣爱好和更好职业机会的绝佳途径。能够编程的人——因此能够快速有效地解决问题——在今天的世界中是非常受重视的,他们可以做有趣且充实的工作。

并非所有世界上的问题都能仅靠技术解决,但技术能够在前所未有的规模和速度上促进沟通、协作、意识和行动。如果你能编程,你就能解决问题。解决问题的人有能力让世界变得更好,所以从今天开始编程吧。

第一章 Python 基础:认识你的开发环境

几乎任何东西都有可能内置计算机——手机、汽车、手表、视频游戏机、健身器材、医疗设备、工业设备、贺卡或机器人。计算机编程,或者说是编码,就是我们告诉计算机如何执行任务的方式,理解如何编写代码能让你充分利用计算机的强大功能。

计算机程序——也称为应用程序,或应用——告诉计算机做什么。一个网页应用程序可以告诉计算机如何跟踪你喜欢的音乐;一个游戏应用程序可以告诉计算机如何显示一个史诗般的战场和逼真的图像;一个简单的应用程序可以告诉计算机如何绘制像图 1-1 中的六边形那样的美丽螺旋图。

一幅色彩斑斓的螺旋图

图 1-1. 一幅色彩斑斓的螺旋图

有些应用由成千上万行代码组成,而有些应用则可能只有几行代码,比如图 1-2 中的程序NiceHexSpiral.py

NiceHexSpiral.py,一段短小的 Python 程序,用于绘制螺旋图

图 1-2. NiceHexSpiral.py,一段短小的 Python 程序,用于绘制图 1-1 中的螺旋

这个短小的程序绘制了图 1-1 中展示的色彩螺旋。我想要一幅漂亮的图像作为本书中的示例,所以我决定通过计算机程序来解决这个问题。首先,我草拟了一个想法,然后开始编写代码。

在本章中,我们将下载、安装并学习使用帮助我们编写代码的程序,以便构建任何你能想象的应用。

Python 入门

要开始编写代码,我们必须学习计算机的语言。计算机需要逐步的指令,并且只能理解某些特定的语言。就像一个俄罗斯人可能听不懂英语一样,计算机只懂为它们设计的语言。计算机代码是用像 Python、C++、Ruby 或 JavaScript 这样的编程语言编写的。这些语言允许我们与计算机“对话”并向它发出命令。想想看,当你教狗狗做一些把戏时——当你给它下“坐下”的命令时,它就坐下;当你说“说话”时,它就叫。这只狗理解这些简单的命令,但你说的其他话它听不懂。

没有标题的图片

同样,计算机也有自己的局限性,但它们可以用它们的语言做你告诉它做的任何事情。我们在本书中使用的语言是 Python,一种简单而强大的编程语言。Python 在高中和大学的计算机科学入门课程中教授,它被用于运行一些世界上最强大的应用程序,包括 Gmail、Google Maps 和 YouTube。

为了让你开始在计算机上使用 Python,我们将一起完成这三个步骤:

  1. 下载 Python。

  2. 在你的计算机上安装 Python。

  3. 使用一个简单的程序来测试 Python。

没有标题的图片

1. 下载 Python

Python 是免费的,可以从 Python 官网轻松下载,如图 1-3 所示。

Python 官网让下载 Python 变得简单。

图 1-3. Python 官网让下载 Python 变得简单。

在你的网页浏览器中,访问 www.python.org/。将鼠标悬停在顶部的 Downloads 菜单按钮上,然后点击以 Python 3 开头的按钮。

2. 安装 Python

找到你刚下载的文件(它可能在你的 Downloads 文件夹中),双击它来运行并安装 Python 和 IDLE 编辑器。IDLE 是我们用来编写和运行 Python 程序的程序。有关详细的安装说明,请参见附录 A。

3. 使用程序测试 Python

在你的开始菜单或 Applications 文件夹中,找到 IDLE 程序并运行它。你将看到一个基于文本的命令窗口,如图 1-4 所示。这就是 Python 终端。终端是一个窗口或屏幕,允许用户输入命令或代码行。

IDLE Python 终端—我们学习 Python 的指挥中心

图 1-4. IDLE Python 终端—我们学习 Python 的指挥中心

>>> 被称为 提示符,意味着计算机已经准备好接受你的第一个命令。计算机在请求你告诉它该做什么。请输入

print("Hello, world!")

然后按键盘上的 ENTER 或 RETURN 键。你应该会看到 Python 终端通过打印你在括号中输入的文本来响应:Hello, world!。就这样——你已经写好了你的第一个程序!

用 Python 编写程序

通常你会想写一些比一行更长的程序,因此 Python 配备了一个 编辑器 用于编写更长的程序。在 IDLE 中,进入 文件菜单,选择 FileNew WindowFileNew File。一个空白屏幕将弹出,顶部写着 Untitled

让我们编写一个稍微长一点的 Python 程序。在新的空白窗口中,输入以下三行代码:

# YourName.py
name = input("What is your name?\n")
print("Hi, ", name)

第一行叫做 注释。注释以井号 (#) 开头,是程序的备注或提醒,计算机会忽略它们。在这个例子中,注释只是一个提醒,提醒我们程序的名称。第二行要求用户输入他们的名字并将其存储为 name。第三行打印 "Hi, ",后面跟着用户的名字。注意,逗号(,)将引号中的文本 "Hi, " 与名字分开。

在 Python 中运行程序

进入程序上方菜单中的 Run 选项,选择 RunRun Module。这将 运行,或执行,你程序中的指令。它会首先要求你保存程序。我们将文件命名为 YourName.py。这告诉计算机将程序保存为名为 YourName.py 的文件,.py 部分表示这是一个 Python 程序。

没有说明的图片

当你保存文件并运行时,你会看到你的 Python Shell 窗口启动程序,显示问题 What is your name?。在下一行输入你的名字并按回车键。程序将打印 Hi,,后面跟上你输入的名字。由于这是你要求程序执行的唯一操作,程序会结束,你将再次看到 >>> 提示符,如 图 1-5 所示。

计算机知道我的名字!

图 1-5. 计算机知道我的名字!

对于年轻的学习者,比如我三岁的儿子,他很喜欢知道程序要求他们输入自己的名字。Max 知道自己名字中的字母,所以他在键盘上输入 m-a-x,当我告诉他程序说 Hi, max 时,他很喜欢。问问你的年轻学习者是否希望程序说些不同的话。Max 说了“Hello”,所以我在第三行修改了之前的程序,改成 Hello, 而不是 Hi,

然后我将第三行改为:

print("Hello, ", name, name, name, name, name)

Max 很喜欢程序回应他 Hello, max max max max max。尝试修改程序的第二行和第三行,让计算机问不同的问题并打印不同的答案。

你学到了什么

学习编程就像学习解决谜题、谜语或脑筋急转弯。你从一个问题开始,运用你所知道的,途中学到新的东西。当你完成时,你不仅锻炼了大脑,还解答了一个问题。希望你也玩得开心。

在这一章中,我们解决了第一个重大问题:我们在电脑上安装了 Python 编程语言,这样我们就可以开始编程了。这个过程就像下载文件、安装并运行一样简单。

在接下来的章节中,你将学习如何使用代码解决问题。你将从简单的视觉谜题开始,比如在计算机屏幕(或*板电脑或手机)上绘制图形,然后学会如何创建简单的游戏,如猜数字、石头剪子布和乒乓游戏。

通过这些初始程序,你将打下坚实的基础,之后你可以编写游戏、移动应用、网页应用等更多内容。

到目前为止,你应该...

  • 拥有一个功能完备的 Python 编程环境和文本编辑器。

  • 能够直接在 Python shell 中输入编程命令。

  • 能够在 IDLE 中编写、保存、运行和修改短小程序。

  • 准备好在第二章中尝试更高级、更有趣的程序。

编程挑战

在每一章的结尾,你可以通过尝试几个挑战来练习所学内容——并制作更酷的程序!(如果你卡住了,可以访问* www.nostarch.com/teachkids/*查看样例答案。)

#1: 疯狂填字游戏

简单的YourName.py应用程序包含了构建一个更有趣程序所需的所有必要组件,比如老式的疯狂填字游戏(如果你以前从未尝试过,可以访问* www.madlibs.com/*)。

让我们修改程序YourName.py并将其保存为MadLib.py。我们将不再询问用户的名字,而是询问形容词、名词和过去式动词,并将它们存储在三个不同的变量中,正如我们在原程序中处理name一样。然后,我们将打印出类似“adjective noun verb over the lazy brown dog.”的句子。这里是修改后的代码应该是什么样的。

MadLib.py

adjective = input("Please enter an adjective: ")
noun = input("Please enter a noun: ")
verb = input("Please enter a verb ending in -ed: ")
print("Your MadLib:")
print("The", adjective, noun, verb, "over the lazy brown dog.")

你可以输入任何形容词、名词和动词。保存并运行MadLib.py后,你应该看到这样的内容(我输入了smartteachersneezed):

>>>
Please enter an adjective: **smart**
Please enter a noun: **teacher**
Please enter a verb ending in -ed: **sneezed**
Your MadLib:
The smart teacher sneezed over the lazy brown dog.
>>>

#2: 更多的疯狂填字游戏!

让我们让疯狂填字游戏变得更加有趣。通过将其保存为MadLib2.py,开始一个新的MadLib.py版本。添加另一行输入,询问一种动物类型。然后,修改print语句,去掉单词dog,在引号句子的末尾添加新的animal变量(在print语句内的新变量前加一个逗号)。你也可以进一步修改这个句子。你可能最终得到类似于The funny chalkboard burped over the lazy brown gecko的句子——或者更搞笑的内容!

第二章. 海龟图形:用 Python 绘图

在这一章中,我们将编写简短、简单的程序来创建美丽而复杂的视觉效果。为此,我们将使用 海龟图形。在海龟图形中,你编写指令告诉一个虚拟的或想象中的海龟在屏幕上移动。海龟带着一支笔,你可以指示海龟在它走到的地方用笔画线。通过编写代码使海龟按照酷炫的模式移动,你可以让它画出惊人的图画。

使用海龟图形,你不仅可以用几行代码创建令人印象深刻的视觉效果,还可以跟随海龟观察每一行代码如何影响它的运动。这将帮助你理解代码的逻辑

我们的第一个海龟程序

让我们用海龟图形写下我们的第一个程序。在 IDLE 中的新窗口中输入以下代码,并将其保存为 SquareSpiral1.py。(你也可以在www.nostarch.com/teachkids/下载本书中的所有程序。)

SquareSpiral1.py

# SquareSpiral1.py - Draws a square spiral
import turtle
t = turtle.Pen()
for x in range(100):
    t.forward(x)
    t.left(90)

当我们运行这段代码时,我们会得到一个相当不错的图像(图 2-1)。

一个催眠的方形螺旋,由短程序 SquareSpiral1.py 创建

图 2-1. 一个催眠的方形螺旋,由短程序 SquareSpiral1.py 创建

它是如何工作的

让我们逐行分析这个程序,看看它是如何工作的。

SquareSpiral1.py 的第一行是注释。正如你在第一章中学到的,注释以井号(#)开始。注释让我们可以在程序中写下给自己或其他可能阅读程序的人的备注。计算机不会读取或尝试理解井号后的任何内容;注释只是用来写下关于程序正在做什么的说明。在这个例子中,我在注释中写了程序的名称,并简要描述了它的功能。

第二行 导入了绘制海龟图形的功能。导入已经编写好的代码是编程中最酷的事情之一。如果你编写了有趣且有用的东西,你可以与他人共享,甚至自己重复使用。有些酷炫的 Python 程序员开发了一个 ——一个可重用的代码集合——来帮助其他程序员在 Python 中使用海龟图形,尽管海龟图形最初来自 1960 年代的 Logo 编程语言。^([1]) 当你输入 import turtle 时,实际上是告诉程序你希望使用这些 Python 程序员编写的代码。图 2-1 中的小黑箭头代表了海龟,随着海龟在屏幕上移动,海龟用它的笔进行绘图。

没有说明文字的图片

我们程序的第三行,t = turtle.Pen(),告诉计算机我们将使用字母 t 来代表海龟的笔。这将使得我们可以通过输入 t.forward() 来使用海龟的笔进行绘图,而无需每次都写出 turtle.Pen().forward()。字母 t 是我们告诉海龟做什么的快捷方式。

第四行是最复杂的一行。在这里我们创建了一个 循环,它会将一组指令重复执行若干次(它 循环 这些代码行,反复执行)。这个特定的循环设置了一个从 0 到 99 的 range(范围),也就是一个包含 100 个数字的列表。(计算机几乎总是从 0 开始计数,而不是像我们通常那样从 1 开始。)然后,循环将字母 x 依次遍历这些数字。因此,x 从 0 开始,然后变为 1,接着是 2,依此类推,直到 99,总共执行 100 步。

这个 x 被称为一个 变量。^([2])(在 第一章 的程序 YourName.py 中,name 是一个变量。)变量存储的是一个可以改变或变化的值,我们在程序执行过程中会不断改变它。几乎在我们编写的每一个程序中都会使用变量,所以早点熟悉它们是很有帮助的。

接下来的两行是缩进的,或者说是从左边空出的位置。这意味着它们是 在循环中 的,并且与上面那行一起执行,因此它们会在 x 每次得到从 0 到 99 的新数字时被重复执行,总共执行 100 次。

发生了什么

让我们来看一下 Python 第一次读取这组指令时会发生什么。命令 t.forward(x) 告诉海龟笔在屏幕上向前移动 x 个点。因为 x 为 0,所以笔根本没有移动。最后一行 t.left(90) 告诉海龟左转 90 度,也就是四分之一圈。

由于for循环的存在,程序会继续运行,并返回到我们循环的起始位置。计算机会加 1,将x移动到范围内的下一个值,并且由于 1 仍在 0 到 99 的范围内,循环继续进行。现在x为 1,所以画笔向前移动 1 个像素。然后,由于t.left(90),画笔再次向左移动 90 度。这一过程会不断重复。当x达到 99 时,最后一次执行循环,画笔开始绘制外面长长的方形螺旋线条。

image with no caption

这是当x从 0 增长到 100 时,循环的逐步可视化过程:

for x in range(100):
    t.forward(x)
    t.left(90)
循环 0 到 4:绘制了前四条线(当 x = 4 时)。
循环 5 到 8:又绘制了四条线;我们的方形开始出现。
循环 9 到 12:我们的方形螺旋增长到 12 条线(三个方形)。

屏幕上的点,或者说是像素,可能太小了,以至于你很难看清楚它们。但是,当x接* 100 时,海龟绘制的线条会由越来越多的像素组成。换句话说,随着x的增大,t.forward(x)绘制的线条会越来越长。

屏幕上的海龟箭头绘制了一会儿,然后转向左边,继续绘制,转向左边,再次绘制,每次画的线都越来越长。

到最后,我们得到了一个催眠般的方形图案。向左转 90 度四次就能得到一个方形,就像围绕一栋建筑物转四圈,你就能绕一圈回到起点一样。

之所以在这个例子中会有螺旋,是因为每次转左时,我们都会走得更远。绘制的第一条线只有 1 步长(当x = 1 时),然后是 2 步(下一次进入循环时),接着是 3 步、4 步,以此类推,一直到 100 步,当时这条线有 99 个像素长。同样,屏幕上的像素可能太小,导致你无法轻易看到每个单独的点,但它们确实存在,而且你可以看到线条变得越来越长,包含的像素也越来越多。

通过将所有转角设为 90 度,我们得到了完美的方形。

Turtle on a Roll

让我们看看当我们改变程序中的一个数字时会发生什么。了解程序的新方法之一就是观察当你改变程序的某一部分时会发生什么。你不总是能得到一个漂亮的结果,但即使出现问题,你也可以从中学习。

只需将程序的最后一行改为t.left(91)并将其保存为SquareSpiral2.py

SquareSpiral2.py

import turtle
t = turtle.Pen()
for x in range(100):
    t.forward(x)
    t.left(91)

我提到过,90 度左转可以画出完美的正方形。只转过 90 度多一点——在这个例子中,每次转动 91 度——会使正方形稍微偏离一点。因为在下次转弯时已经偏离了一点,我们的新形状在程序继续执行时会越来越不像正方形。实际上,它形成了一个漂亮的螺旋形状,开始像楼梯一样向左旋转,就像你在图 2-2 中看到的那样。

方形螺旋程序只需一个小小的改变,就变成了螺旋楼梯。

图 2-2。只需一个小小的改变,方形螺旋程序就变成了螺旋楼梯。

这也是一个很好的视觉效果,帮助你理解仅仅偏差一个数字是如何极大地改变程序结果的。一个度数看起来似乎不算什么大事,除非你偏差了一度 100 次(加起来就是 100 度),或者 1,000 次,或者你正在使用程序来着陆一架飞机……

没有标题的图片

如果你还不理解度数是如何运作的,不用担心。现在只需玩一下数字,看看会发生什么。通过改变range后括号内的数字,可以将程序绘制的线条数量更改为 200、500 或 50。

还可以尝试将最后一行的角度改为 91、46、61 或 121 等等。记得每次都保存程序。然后运行它,看看你的更改如何影响程序绘制的内容。年长的读者可能对使用的角度看到一些熟悉的形状,甚至在程序运行之前就能预测出形状。年轻的读者可以尽情地玩玩这个,等到有一天上几何课时,这个练习可能会帮助他们。

海龟汇总

说到几何学,海龟图形不仅可以绘制直线,还能绘制更多有趣的形状。我们将在下一节中再次回到正方形,但现在让我们稍作偏离,探索更多 Python Turtle 库的内容。

让我们再修改一行代码:t.forward(x)。我们之前看到,这个命令,或者说是函数,将海龟的画笔向前移动x个像素并画出一段直线;然后海龟转身再来一次。如果我们修改这行代码,绘制一些更复杂的东西,比如一个圆呢?

幸运的是,绘制一定大小或半径的圆的命令和绘制直线的命令一样简单。将t.forward(x)改为t.circle(x),如下面的代码所示。

CircleSpiral1.py

import turtle
t = turtle.Pen()
for x in range(100):
    t.circle(x)
    t.left(91)

哇!仅仅将一个命令从 t.forward改为 t.circle就给我们带来了一个更复杂的形状,正如你在图 2-3 中看到的那样。t.circle(x)函数指示程序在当前的位置绘制一个半径为x的圆。请注意,这个图形与简单的方形螺旋形状有一些共同之处:这里有四组圆形螺旋,就像我们的方形螺旋有四个边一样。这是因为我们依然在使用 t.left(91)命令,每次只向左转略多于 90 度。如果你学过几何,你会知道一个点周围有 360 度,就像方形的四个 90 度角(4 × 90 = 360)。海龟通过每次转过略多于 90 度,绘制出这个螺旋形状。

再加一个变化,给我们带来了四个美丽的螺旋圆形。

图 2-3. 再加一个变化,给我们带来了四个美丽的螺旋圆形。

你会发现一个区别:圆形螺旋比方形螺旋要大——实际上大约是方形螺旋的两倍。这是因为t.circle(x)x作为圆的半径,即从圆心到圆边的距离,或者是圆的宽度的一半。

x的半径意味着圆的直径,即总宽度,将是x的两倍。换句话说,当x为 1 时,t.circle(x)绘制的圆的直径为 2 像素;当x为 2 时,直径为 4 像素;当x为 99 时,直径将达到 198 像素。那几乎是 200 像素,或是方形螺旋最大边长的两倍,因此圆形螺旋的大小大约是方形螺旋的两倍——也许它的酷度也是两倍!

增添一点色彩

这些螺旋形状很漂亮,但如果它们再多一些色彩,不是更酷吗?让我们回到方形螺旋的代码,并在 t = turtle.Pen()行后面添加一行代码,将笔的颜色设置为红色:

SquareSpiral3.py

import turtle
t = turtle.Pen()
t.pencolor("red")
for x in range(100):
    t.forward(x)
    t.left(91)

运行程序后,你将看到一个更有色彩的方形螺旋(图 2-4)。

方形螺旋变得更加丰富多彩。

图 2-4. 方形螺旋变得更加丰富多彩。

尝试将"red"替换为另一个常见的颜色,如"blue""green",然后重新运行程序。你可以使用 Turtle 库中的数百种不同颜色,包括一些奇怪的颜色,如"salmon""lemon chiffon"。(访问www.tcl.tk/man/tcl8.4/TkCmd/colors.htm查看完整列表。)将整个螺旋设置为不同的颜色是一个不错的步骤,但如果我们想让每一条都使用不同的颜色呢?那将需要对程序做一些更改。

四色螺旋

让我们思考一下算法—也就是一系列步骤—将单色螺旋变成四色螺旋。大多数步骤与我们之前的螺旋程序相同,但有一些新增的变化:

  1. 导入turtle模块并设置一只海龟。

  2. 告诉计算机我们想使用哪些颜色。

  3. 设置一个循环,在我们的螺旋图形中绘制 100 条线。

  4. 为螺旋的每一条边选择不同的笔颜色。

  5. 移动海龟向前绘制每一条边。

  6. 将海龟转向左边,为绘制下一条边做准备。

首先,我们需要一个颜色名称的列表,而不是单一的颜色,因此我们将设置一个名为colors的列表变量,并将四种颜色放入该列表,如下所示:

colors = ["red", "yellow", "blue", "green"]

这四种颜色的列表将为我们的方形的每一条边提供一种颜色。注意,我们将颜色列表放在了方括号[]内。确保每个颜色名称都在引号内,就像我们在第一章中打印的单词一样,因为这些颜色名称是字符串,或者说是文本值,我们稍后会将它们传递给pencolor函数。如上所述,我们使用一个叫做colors的变量来存储这四种颜色的列表,所以每当我们想从列表中获取颜色时,我们会用colors变量来代表笔的颜色。记住,变量存储的是会变化的值,正如它们的名字所示:它们是可变的!

没有标题的图像

接下来我们需要做的是在每次执行绘图循环时,每次都更改笔的颜色。为此,我们需要将 t.pencolor()函数移动到for循环下的指令组中。我们还需要告诉pencolor函数,我们希望使用列表中的某个颜色。

输入以下代码并运行它。

ColorSquareSpiral.py

import turtle
t = turtle.Pen()
colors = ["red", "yellow", "blue", "green"]
for x in range(100):
    t.pencolor(colors[x%4])
    t.forward(x)
    t.left(91)

四种颜色的列表是合理的,我们可以在运行示例中看到它们(图 2-5)。到目前为止,一切顺利。

我们的方形螺旋程序的一个更彩色的版本

图 2-5. 我们方形螺旋程序的更彩色版本

唯一的新部分是 pencolor 函数中的 (colors[x%4])。语句中的 x 是我们在程序中其他地方使用的同一个变量。所以 x 将从 0 增长到 99,就像我们之前看到的那样。括号中的 colors 变量名告诉 Python 从程序中我们之前添加的颜色名称列表 colors 中选择一个颜色。

[x%4]部分告诉 Python 我们将使用 colors 列表中的前四个颜色,编号从 0 到 3,并且每次 x 改变时都会在这四个颜色中循环。在这个例子中,我们的颜色列表只有四个颜色,所以我们会一直在这四个颜色之间循环:

colors = ["red", "yellow", "blue", "green"]
          0        1        2       3

[x%4]中的%符号称为取模运算符,也叫做运算符,它表示长除法中的余数(例如,5 ÷ 4 等于 1,余数为 1,因为 4 除以 5 得 1,剩下 1;6 ÷ 4 余数为 2,依此类推)。当你希望在列表中循环遍历一定数量的项时,模运算符非常有用,就像我们在操作四个颜色的列表时所做的那样。

在 100 步中,colors[x%4] 将循环遍历四种颜色(0、1、2、3,分别代表红色、黄色、蓝色和绿色),共计 25 次。如果你有时间(并且手边有放大镜),你可以在图 2-5 中数出 25 个红色、25 个黄色、25 个蓝色和 25 个绿色的段落。在第一次绘制循环中,Python 使用列表中的第一个颜色——红色;第二次使用黄色;依此类推。然后,在第五次循环时,Python 会回到红色,然后是黄色,依此类推,每四次循环后都会重新回到红色。

没有标题的图片

更改背景颜色

让我们再来点变化,看看是否能创造出比图 2-5 中更加美丽的效果。例如,正如我五岁的儿子 Alex 所指出的,黄色的边缘不容易看到。这是因为,就像在白色画纸上的黄色蜡笔一样,屏幕上的黄色像素在白色背景下不太明显。让我们通过将背景色改为黑色来解决这个问题。在程序中的 import 语句之后的任何位置输入以下代码行:

turtle.bgcolor("black")

增加这一行代码后,我们得到了一个更整洁的图形:所有的颜色在黑色背景下都更加鲜明。注意,我们并没有改变海龟的笔(在程序中由变量t表示)。相反,我们改变了海龟屏幕的一些设置,具体来说是背景颜色。turtle.bgcolor()命令允许我们将整个绘图屏幕的颜色更改为 Python 中任何命名的颜色。在 turtle.bgcolor("black")这一行中,我们选择了黑色作为背景色,因此明亮的红色、黄色、蓝色和绿色显示得非常清晰。

在此过程中,我们还可以将循环中的range()改成200,甚至更多,这样就能让螺旋中的方形更大。请参见图 2-6,这张图显示了背景为黑色的 200 条线的新版本。

我们的方形螺旋图形已经经历了从简单起步到如今的巨大变化。

图 2-6。我们的方形螺旋图形已经经历了从简单起步到如今的巨大变化。

总是乐于帮助让我程序更加精彩的 Alex,又提出了一个改动:如果我们现在用圆形代替线段呢?那不就是最酷的图形吗?嗯,是的,我得同意——它确实更酷了。下面是完整的代码。

ColorCircleSpiral.py

import turtle
t = turtle.Pen()
turtle.bgcolor("black")
colors = ["red", "yellow", "blue", "green"]
for x in range(100):
    t.pencolor(colors[x%4])
    t.circle(x)
    t.left(91)

你可以在图 2-7 中看到结果。

Alex 的酷炫彩色圆形螺旋——八行代码,简洁优雅

图 2-7。Alex 的酷炫彩色圆形螺旋——八行代码,简洁优雅

一种变量统治一切

到目前为止,我们已经用变量来改变螺旋图形的颜色、大小和转动角度。现在,我们再加一个变量,sides,来表示图形的边数。这个新变量会如何改变我们的螺旋图形呢?想知道答案,就试试这个程序,ColorSpiral.py

ColorSpiral.py

import turtle
t = turtle.Pen()
turtle.bgcolor("black")
# You can choose between 2 and 6 sides for some cool shapes!
sides = 6
colors = ["red", "yellow", "blue", "orange", "green", "purple"]
for x in range(360):
    t.pencolor(colors[x%sides])
    t.forward(x * 3/sides + x)
    t.left(360/sides + 1)
    t.width(x*sides/200)

你可以将sides的值从 6 改为 2(一个边不太有趣,除非你在程序的第六行中增加更多的颜色,否则无法使用更大的数字)。然后保存并运行程序,尽情多次尝试。图 2-8 展示了使用sides = 6sides = 5直到sides = 2的图像,其中sides = 2是图 2-8(e)中显示的奇怪扁*螺旋形。你可以改变颜色列表中的颜色顺序,也可以在绘图循环中的任何函数中使用更大或更小的数字。如果你破坏了程序,只需返回到原始的ColorSpiral.py并再玩一会儿。

ColorSpiral.py程序使用了一个新命令,t.width();这个命令改变了海龟画笔的宽度。在我们的程序中,画笔在绘制越来越大的形状时变得更宽(线条变厚)。我们将在第三章和第四章中再次讨论这个程序以及类似的程序,帮助你学习从零开始创建此类程序所需的技能。

通过将变量 sides 从 6(a)改变到 2(e)创建的五个彩色形状

图 2-8. 通过将变量sides从 6(a)改变到 2(e)创建的五个彩色形状

你所学到的内容

在本章中,我们使用 Python 的 Turtle 工具库绘制了令人印象深刻的彩色图形。我们通过使用import命令将该库引入到程序中,你学会了以这种方式重复使用代码是编程中最强大的特点之一。一旦我们编写了有用的代码,或者借用了别人好心分享的代码,我们不仅节省了时间,还能利用这些导入的代码做出有趣的新事情。

你也已经在我们的程序中接触过像xsides这样的变量。这些变量存储或记住一个数字或值,以便你可以在程序中多次使用它,并且在运行过程中修改其值。在下一章,你将进一步了解变量的强大功能,甚至 Python 还可以帮助你做数学作业!

到目前为止,你应该能够完成以下任务:

  • 使用 Turtle 库绘制简单的图形。

  • 使用变量来存储简单的数字值和字符串。

  • 在 IDLE 中更改、保存并运行程序。

编程挑战

尝试这些挑战来练习你在本章学到的内容。(如果你卡住了,可以访问www.nostarch.com/teachkids/获取示例答案。)

#1:改变边数

我们在《一个变量控制一切》的ColorSpiral.py程序中使用了变量sides,但是我们并没有大幅度变化它的值,除非编辑、保存并重新运行程序。试着将sides的值改为另一个数字,比如 5。保存并运行程序,看看这会如何影响你的绘图。现在试试 4、3、2,甚至是 1!现在,在程序第六行的颜色列表中添加两个或更多的颜色,用引号括起来,并用逗号隔开。增加sides的值,使用新的颜色数量——试试 8、10 或更多!

#2: 多少条边?

如果你希望在程序运行时让用户决定边数该怎么办?利用你在第一章学到的知识,你可以让用户输入边数,并将该输入存储在变量sides中。我们唯一额外的步骤是评估用户输入的数字。我们可以使用eval()函数来找出用户输入的数字,像这样:

sides = eval(input("Enter a number of sides between 2 and 6: "))

ColorSpiral.py 中的sides = 6行替换为上述行。你新的程序将询问用户想要显示多少条边。然后,程序将绘制用户要求的形状。试试看!

#3: 橡皮筋球

试着仅通过在绘图循环的末尾添加一个额外的转动,将 ColorSpiral.py 程序改为一个更复杂和抽象的形状。可以在for循环的底部添加一行像t.left(90)的代码,使角度更加尖锐(记得缩进,以保持语句在循环内)。如图 2-9 所示,结果看起来像一个几何玩具,或者可能是一个由彩色橡皮筋制成的球。

在 ColorSpiral.py 中每次转动增加 90 度,会将其转变为 RubberBandBall.py。

图 2-9. 在 ColorSpiral.py 中每次转动增加 90 度,会将其转变为 RubberBandBall.py

将这个新版本保存为RubberBandBall.py,或者访问www.nostarch.com/teachkids/,在第二章的源代码中找到该程序。


^([1]) Logo 编程语言于 1967 年创建,是一种教育用编程语言,五十年后,它仍然是学习编程基础的有用工具。酷吧?

^([2]) 年轻的读者可能会将x识别为未知数,就像他们在解方程x + 4 = 6 时找到未知数x一样。年长的读者可能会从代数课或其他数学课程中认识到x;这是早期程序员借用了变量的概念的地方。编程中有很多很好的数学知识:我们甚至会看到一些有趣的几何示例,随着课程的进行。

第三章. 数字与变量:Python 做数学运算

我们已经使用 Python 做了一些非常有趣的事情,例如只用几行代码就画出了五彩斑斓的图画,但我们的程序还是有些局限。我们只是运行它们,观看它们画图。如果我们想要和我们的 Python 程序互动呢?在本章中,我们将学习如何让 Python 询问用户的名字,甚至提供做用户数学作业的帮助!

变量:我们存放东西的地方

在第一章和第二章中,我们使用了一些变量(你可能还记得在第一章中的name或在第二章中的xsides)。现在让我们看看变量到底是什么以及它们是如何工作的。

变量是你希望计算机在程序运行时记住的内容。当 Python“记住”某个东西时,它会将该信息存储在计算机的内存中。Python 可以记住多种类型的,包括数字值(如742,甚至是98.6)和字符串(字母、符号、单词、句子或任何你能在键盘上输入的内容,甚至更多)。在 Python 中,就像大多数现代编程语言一样,我们使用赋值操作符(=)将一个值赋给变量。像x = 7这样的赋值操作告诉计算机记住数字 7,并在我们每次调用x时将它还给我们。我们也可以使用等号将一串键盘字符赋给变量;只需要记得在字符串周围加上引号(")就可以了,例如:

my_name = "Bryson"

在这里,我们将值"Bryson"赋给变量my_name。围绕"Bryson"的引号告诉我们它是一个字符串。

每当你将一个值赋给变量时,首先写出变量的名字,位于等号左侧,然后写出值,位于等号右侧。我们为变量取一个简单的名字,描述它的内容(比如当我存储我的名字时使用my_name),这样我们就能轻松记住它们并在程序中使用它们。在命名变量时,有几个规则需要牢记。

没有标题的图片

首先,我们总是以字母开始变量名。其次,变量名中的其余字符必须是字母、数字或下划线符号(_);这意味着变量名中不能有空格(例如,my name会导致语法错误,因为 Python 认为你列出了两个变量,中间用空格分隔)。第三,Python 中的变量名是区分大小写的;这意味着,如果我们在变量名中使用全小写字母(如abc),那么只有在完全按相同的大小写方式输入变量名时,才能使用该变量存储的值。例如,要使用abc中的值,我们必须写成abc;不能使用像ABC这样的大写字母。所以,My_Namemy_name不同,而MY_NAME是完全不同的变量名。在本书中,我们将在变量名中使用全小写字母,并用_符号分隔单词。

让我们尝试一个使用变量的程序。在新的 IDLE 窗口中输入以下代码,并将其保存为ThankYou.py

ThankYou.py

my_name = "Bryson"
my_age = 43
your_name = input("What is your name? ")
your_age = input("How old are you? ")
print("My name is", my_name, ", and I am", my_age, "years old.")
print("Your name is", your_name, ", and you are", your_age, ".")
print("Thank you for buying my book,", your_name, "!")

当我们运行这个程序时,我们告诉计算机记住my_name"Bryson",并且my_age43。然后我们要求用户(运行程序的人)输入他们的名字和年龄,并告诉计算机将这些作为your_nameyour_age变量来记住。我们使用 Python 的input()函数告诉 Python 我们希望用户通过键盘输入(或input)某些内容。Input是我们称之为在程序运行时输入的信息——在这种情况下,就是用户的名字和年龄。括号内引号中的部分("What is your name? ")被称为提示,因为它提示用户,或者询问他们一个需要输入的的问题。

在最后三行中,我们告诉计算机打印出我们在my_name和其他三个变量中存储的值。我们甚至使用了your_name两次,计算机能够正确记住所有内容,包括用户输入的部分。

这个程序记住了我的名字和年龄,询问用户他们的名字和年龄,并向他们打印一个友好的信息,如图 3-1 所示。

一个包含四个变量及其输出的程序

图 3-1. 一个包含四个变量及其输出的程序

Python 中的数字和数学运算

计算机非常擅长记住值。我们可以在同一个程序中使用相同的变量数百次或数千次,只要我们编程正确,计算机总是能给我们正确的值。计算机在执行计算(加法、减法等)方面也很擅长。你的计算机每秒钟能够执行超过十亿(1,000,000,000,或十亿)次计算!

这比我们用脑袋算数字要快得多;虽然在某些任务上我们仍然比计算机更强,但快速的数学运算是计算机每次都能赢的比赛。Python 让你能够利用这种数学计算能力,它提供了两种主要的数字类型,并且让你使用一整套符号来进行数学运算,从 +- 等等。

Python 数字

Python 中的两种主要数字类型分别是 整数(包括负数的整数,例如 7-90)和 浮动点数(有小数的数字,例如 1.02.50.9993.14159265)。还有两种额外的数字类型,在本书中我们不会多用到。第一种是 布尔值,它表示真假值(有点像学校里“对错”测试的答案),第二种是 复数,它表示包含虚数的数值(如果你学过一些高级代数,可能会对它感到兴奋,但我们现在保持实际——开个玩笑)。

整数或自然数对于计数很有用(在 第二章中,我们的变量 x 就是用来计数我们画出的螺旋线的数量)以及进行基础的数学运算(2 + 2 = 4)。我们通常用整数表示我们的年龄,所以当你说自己 5 岁、16 岁或 42 岁时,你使用的就是整数。当你数到 10 时,你也在使用整数。

浮动点数或小数在我们需要表示分数时非常有用,例如 3.5 英里、1.25 个比萨饼或 25.97 美元。当然,在 Python 中,我们不包含单位(英里、比萨饼、美元),只有带小数的数字。所以如果我们想存储一个变量来表示我们的比萨饼费用(cost_of_pizza),我们可能会这样赋值:cost_of_pizza = 25.97。我们只需要记住,我们在这里使用的单位是美元、欧元或其他货币。

没有标题的图片

Python 运算符

+(加)和 -(减)这样的数学符号被称为 运算符,因为它们对我们方程中的数字进行操作或计算。当我们大声说出“4 + 2”或在计算器中输入时,我们希望对数字 4 和 2 进行加法运算,得到它们的和 6。

Python 使用的运算符大多数与你在数学课上使用的相同,包括 +- 和括号 (),如 表 3-1 所示。然而,一些运算符与学校里学到的不同,比如乘法运算符(星号 *,而不是 ×)和除法运算符(正斜杠 /,而不是 ÷)。我们将在本节中更好地了解这些运算符。

表 3-1. Python 中的基本数学运算符

数学符号 Python 运算符 操作 示例 结果
+ + 加法 4 + 2 6
- 减法 4 - 2 2
× * 乘法 4 * 2 8
÷ / 除法 4 / 2 2.0
** 指数或幂 4 ** 2 16
( ) () 括号(分组) (4 + 2) * 3 18

在 Python Shell 中做数学题

现在是尝试 Python 数学的好时机;这次我们使用 Python shell。正如你可能从第一章记得的那样,Python shell 让你可以直接访问 Python 的强大功能,而无需编写整个程序。它有时被称为命令行,因为你可以逐行输入命令,并即时看到结果。你可以在 Python shell 的命令提示符(>>>符号,闪烁的光标)中直接输入一个数学问题(在编程中称为表达式),例如 4 + 2,然后按下回车键,你会看到这个表达式的结果,或者说是数学问题的答案。

无标题图片

尝试输入表 3-1 中列出的一些示例,看看 Python 会怎么说;图 3-2 展示了一些示例输出。也可以尝试你自己的数学问题。

输入示例数学题(表达式),然后 Python 给出答案!

图 3-2. 从表 3-1 中输入示例数学问题(表达式),然后 Python 给出答案!

语法错误:你说了什么?

在 Python shell 中输入时,我们有机会学习关于语法错误的知识。每当 Python 或任何编程语言无法理解你输入的命令时,它可能会返回类似于“语法错误”的消息。这意味着你要求计算机做某件事情的方式出了问题,或者是你的语法不正确。

语法是我们在构建语言中的句子或语句时遵循的规则集合。当我们编程时,如果语句中有错误,我们称之为语法错误;而当我们在英语句子中犯错时,我们可能称之为语法不规范。不同之处在于,与英语使用者不同,计算机完全不能理解语法错误。像大多数编程语言一样,Python 在遵循语法规则的情况下非常擅长进行计算,但如果我们搞错了语法,它就无法理解我们说的任何内容。查看图 3-3,你可以看到一些语法错误的例子,随后是 Python 能理解的表达式。

学习说 Python 的语言

图 3-3. 学习讲述 Python 的语言

当我们用普通的英语问 Python“4 + 2 是多少?”时,Python 会回答 "SyntaxError: invalid syntax",让我们知道它无法理解我们要求它做什么。当我们给 Python 正确的表达式 4 + 2 时,Python 每次都会正确回答:6。同样地,像等号这样的额外字符,在语句 3 + 3 = 末尾会让 Python 感到困惑,因为 Python 将等号视为赋值运算符,用于将值赋给变量。当我们输入 3 + 3 并按下回车键时,Python 就能理解,并且每次都会给出正确的答案:6

我们可以依赖计算机每次正确且迅速地回答我们给出的合理输入,这是编程中最强大的方面之一。只要我们用计算机理解的语言正确地编程,我们就可以依赖计算机进行快速、准确的计算。这正是你在学习用 Python 编程时要做的事情。

Python Shell 中的变量

正如我们讨论的那样,Python shell 使我们能够直接访问 Python 的编程功能,而无需编写完整的独立程序。我们甚至可以在输入 Python shell 时使用变量,如 xmy_age;我们只需为它们赋值,就像你在本章开头的示例中学到的那样。

如果你在命令提示符(>>>)下输入 x = 5,Python 会将值 5 存储在内存中作为变量 x,并会记住它,直到你告诉 Python 更改这个值(例如,通过输入 x = 9 来给 x 赋一个新的值 9)。请参见图 3-4 中的 Python shell 示例。

Python 会记住我们变量的值,只要我们希望。

图 3-4. Python 会记住我们变量的值,只要我们希望。

注意,在最后的赋值语句中,我们在等号的两边都使用了 xx = x - 7。在代数课上,这将是一个无效语句,因为 x 永远不可能等于 x - 7。但是在程序中,计算机会计算等式右边的部分,先计算出 x - 7 的值,然后再将这个值赋给左边的 x。等号右边的变量会用它们的值替代;这里,x 的值是 9,所以计算机会将 9 代入 x - 7,得到 9 - 7,即 2。最后,等号左边的变量 x 被赋予右边计算结果的值。x 的值只有在赋值过程结束时才会发生变化。

在进入编程示例之前,让我们先了解一下 Python 数学的一项额外功能。在表 3-1、图 3-2 和图 3-4 中,我们使用了除法运算符——斜杠 (/)——并且 Python 给出了一个小数值。对于 4 / 2,Python 给我们的是 2.0,而不是我们可能期望的 2。这是因为 Python 使用了它所称的 真除法,它旨在更容易理解,并且不太容易导致错误。

当我们请求 Python 计算 x / 2,其中 x 等于 5 时,我们可以在图 3-4 中看到 Python 真除法的积极效果。Python 告诉我们,5 除以 2 等于 2.5,这是我们预期的结果。这个除法就像是将五个披萨*均分配给两个团队:每个团队得到 2.5 个披萨(即 5 / 2 的结果)。在一些编程语言中,除法运算符只返回整数(在这个例子中是 2)。只要记住,Python 做的是“披萨除法”。

使用运算符编程:一个披萨计算器

说到披萨,现在让我们假设你拥有一家披萨店。

让我们编写一个小程序来计算一个简单披萨订单的总费用,包括销售税。假设我们正在订购一个或多个价格相同的披萨,并且我们在美国乔治亚州的亚特兰大下单。这里有一个不包含在菜单价格中的销售税,会在购买结算时加上。税率为 8%,意味着每付一美元购买披萨,我们还需要额外支付 8 美分的销售税。我们可以用文字来描述这个程序如下:

  1. 询问对方想要多少个披萨。

  2. 请求每个披萨的菜单价格。

  3. 计算披萨的总成本作为我们的小计。

  4. 计算应付的销售税,按小计的 8% 计算。

  5. 将销售税加到小计上,得到最终总额。

  6. 向用户显示包括税费在内的总金额。

我们已经看过如何请求用户输入。为了对我们输入的数字进行计算,我们还需要一个函数:eval()eval() 函数会 评估 或者说计算我们输入的内容的值。Python 中的键盘输入始终以文本字符串的形式接收,所以我们使用 eval() 将该输入转换为数字。如果我们在程序中输入 "20"eval("20") 会给出数字 20,我们就可以在数学公式中使用它来计算新的数字,例如 20 个披萨的费用。eval() 函数在处理 Python 中的数字时非常强大。

现在我们已经知道如何将用户输入转化为可以进行计算的数字,我们可以将程序计划中的编号步骤转换成实际的代码。

注意

对于每个编程示例,你可以先尝试自己编写程序,然后再查看书中的代码。首先编写注释(#),概述解决问题所需的步骤。然后在每个注释下填写编程步骤,当你需要提示时,查看书中的代码。

在新窗口中输入此内容并将其保存为AtlantaPizza.py

AtlantaPizza.py

# AtlantaPizza.py - a simple pizza cost calculator

# Ask the person how many pizzas they want, get the number with eval()
number_of_pizzas = eval(input("How many pizzas do you want? "))

# Ask for the menu cost of each pizza
cost_per_pizza = eval(input("How much does each pizza cost? "))

# Calculate the total cost of the pizzas as our subtotal
subtotal = number_of_pizzas * cost_per_pizza

# Calculate the sales tax owed, at 8% of the subtotal
tax_rate = 0.08 # Store 8% as the decimal value 0.08
sales_tax = subtotal * tax_rate

# Add the sales tax to the subtotal for the final total
total = subtotal + sales_tax

# Show the user the total amount due, including tax
print("The total cost is $",total)
print("This includes $", subtotal, "for the pizza and")
print("$", sales_tax, "in sales tax.")

这个程序将你所学的变量和运算符知识结合成一个强大的程序。通读它,确保你理解每一部分是如何工作的。你会如何修改程序,使其适用于不同的销售税率?

请注意,我们已经将程序的步骤作为注释包含在内,使用了#(井号)。记住,注释是供人类阅读的;IDLE 编辑器将注释标红,以提醒我们 Python 会忽略这些部分。在构建更长且更复杂的程序时,先用文字一步步列出程序的步骤,再将这些步骤作为注释加入程序,能非常有帮助。这就是我们的算法,程序中需要遵循的步骤集合。算法就像食谱:如果我们按正确的顺序执行所有步骤,程序就会顺利完成!

当我们用文字(如#注释)代码(如编程语句)来编写算法时,我们达成了两个目标。首先,我们通过确保不遗漏步骤来减少程序中的错误。其次,我们让程序变得更加易于我们自己以及他人以后阅读和理解。从一开始,你就应该养成在程序中写清晰注释的习惯,我们将在本书中经常这样做。如果你不想输入所有的注释,程序仍然会运行;它们只是帮助你理解程序在做什么。

没有说明的图片

当你编写完程序后,可以通过点击运行运行模块来运行并与其互动。图 3-5 展示了一些示例输出。

我们的 AtlantaPizza.py 比萨计算器程序的示例运行

图 3-5。我们的AtlantaPizza.py比萨计算器程序的示例运行

字符串:Python 中的真实字符

我们已经看到 Python 在处理数字方面非常出色,但是当我们想与人交流时怎么办呢?人们更擅长理解单词和句子,而不仅仅是数字。为了编写人们可以使用的程序,我们需要另一种类型的变量,叫做字符串。字符串是我们在编程语言中所说的文本,或者说是键盘字符;它们是字母、数字和符号的组合(或“字符串”)。你的名字就是一个字符串,你最喜欢的颜色也是—甚至这段话(或这本书)也是由字母、空格、数字和符号混合组成的长字符串。

字符串和数字之间的一个区别是我们不能用字符串做计算;它们通常是名字、单词或其他不能用于计算器的资料。使用字符串的常见方式是打印。例如,我们在本章开始时让用户输入他们的名字,以便稍后打印出来。

让我们用一个新的程序再做一次。我们会让用户输入他们的名字,将名字存储在一个名为name的变量中,然后在屏幕上打印他们的名字 100 次。就像在第一章和第二章中的酷炫螺旋画例子一样,我们使用了一个循环来重复打印用户的名字 100 次。将以下代码输入到新的 IDLE 窗口中,并将其保存为SayMyName.py

SayMyName.py

# SayMyName.py - prints a screen full of the user's name

# Ask the user for their name
name = input("What is your name? ")

# Print their name 100 times
for x in range(100):
    # Print their name followed by a space, not a new line
    print(name, end = " ")

这个程序最后一行中的print()语句有一个新内容:它包含了一个关键字参数。在这种情况下,关键字end,我们告诉程序将每个print()语句的结尾设置为空格(引号之间有一个空格:" "),而不是常规的行末字符。Python 中的print()语句通常以换行符结尾,就像按下键盘上的 ENTER 键一样,但通过这个关键字参数,我们告诉 Python 不希望每次打印名字时都换行。

为了更清楚地看到这一变化,将程序最后一行修改为以下内容,然后运行程序:

print(name, end = " rules! ")

如果你运行这个,你将看到"Your Name rules!"打印 100 次!关键字参数end = " rules! "让我们可以改变print()语句的工作方式。每个print()语句的结束现在是" rules! ",而不是默认的回车或换行符。

没有标题的图片

在编程语言中,参数并不是什么坏事;它只是我们告诉函数(如print())做某事的方式。我们通过将额外的值放入该函数的括号中来实现这一点。print()语句括号中的那些值就是参数,特殊的关键字参数意味着我们正在使用关键字end来改变print()打印每一行时的结尾方式。当我们将行的结尾从换行符更改为简单的空格符时,单词会被添加到当前行的末尾,而不会换行,直到当前行填满并换到下一行。请查看图 3-6 中的结果。

当我运行 SayMyName.py 时,Python 会在屏幕上打印满我的名字。

图 3-6. 当我运行SayMyName.py时,Python 会在屏幕上打印满我的名字。

使用字符串改进我们的彩色螺旋

字符串如此受欢迎,以至于即使是 Python 中的海龟图形也有函数来接受字符串作为输入并将其写入屏幕。海龟库中用于请求用户输入字符串或文本的函数是turtle.textinput();它会弹出一个窗口,要求用户输入文本,并允许我们将其作为字符串值存储。图 3-7 展示了我们使用turtle.textinput("Enter your name", "What is your name?")时,海龟为我们弹出的漂亮图形窗口。海龟的textinput()函数有两个参数。第一个参数"Enter your name"是弹出窗口的标题。第二个参数"What is your name?"是提示用户输入我们想要的信息。

海龟图形中的文本输入窗口

图 3-7. 海龟图形中的文本输入窗口

在海龟屏幕上写字符串的函数是write();它会以海龟的笔颜色和当前位置在屏幕上绘制文本。我们可以使用write()turtle.textinput()结合字符串的力量与丰富多彩的海龟图形。让我们试试!在下面的程序中,我们将像之前的螺旋图一样设置海龟图形,但不是在屏幕上绘制线条或圆圈,而是请求用户输入他们的名字,然后以彩色螺旋的形式将其绘制到屏幕上。将其输入到新窗口并保存为SpiralMyName.py

SpiralMyName.py

   # SpiralMyName.py - prints a colorful spiral of the user's name

   import turtle               # Set up turtle graphics
   t = turtle.Pen()
   turtle.bgcolor("black")
   colors = ["red", "yellow", "blue", "green"]

   # Ask the user's name using turtle's textinput pop-up window
➊ your_name = turtle.textinput("Enter your name", "What is your name?")

   # Draw a spiral of the name on the screen, written 100 times
   for x in range(100):
       t.pencolor(colors[x%4]) # Rotate through the four colors
➋     t.penup()               # Don't draw the regular spiral lines
➌     t.forward(x*4)          # Just move the turtle on the screen
➍     t.pendown()             # Write the user's name, bigger each time
➎     t.write(your_name, font = ("Arial", int( (x + 4) / 4), "bold") )
       t.left(92)              # Turn left, just as in our other spirals

SpiralMyName.py中的大部分代码与我们之前的彩色螺旋非常相似,但我们在➊处通过turtle.textinput弹出窗口询问用户的姓名,并将用户的回答存储在your_name中。我们还通过在➋处将海龟的画笔抬离屏幕来改变绘制循环,这样当我们在➌处移动海龟时,它不会留下痕迹或绘制正常的螺旋线。我们只想要在螺旋中显示用户的名字,所以在➌处海龟移动后,我们通过在➍处使用t.pendown()来告诉它重新开始绘制。然后,通过➎处的write命令,我们告诉海龟在每次循环时在屏幕上写下your_name。最终结果是一个漂亮的螺旋图案;我的儿子 Max 运行了图示中的一个,见图 3-8。

一条彩色文本螺旋

图 3-8. 一条彩色文本螺旋

列表:将一切集中在一起

除了字符串和数字值,变量还可以包含列表。列表是一组用逗号分隔的值,这些值被方括号[]包围。我们可以在列表中存储任何类型的值,包括数字和字符串;甚至可以拥有列表的列表。

在我们的螺旋程序中,我们将一个字符串列表—["red", "yellow", "blue", "green"]—存储在colors变量中。然后,当我们的程序需要使用颜色时,我们只需调用t.pencolor()函数,并告诉它使用colors列表来查找它应该使用的颜色名称。让我们将更多颜色名称添加到我们的颜色列表中,并学习 Turtle 包中的另一个输入函数:numinput()

在红色、黄色、蓝色和绿色的基础上,我们再添加四种命名颜色:橙色、紫色、白色和灰色。接下来,我们希望询问用户他们的形状应该有多少条边。就像 turtle.textinput()函数询问用户字符串一样,turtle.numinput()允许用户输入一个数字。

我们将使用这个numinput()函数来询问用户输入边的数量(在 1 到 8 之间),并为用户提供一个默认选择4,这意味着如果用户没有输入数字,程序将自动使用4作为边的数量。将以下代码输入到一个新的窗口中,并将其保存为ColorSpiralInput.py

ColorSpiralInput.py

  import turtle                      # Set up turtle graphics
  t = turtle.Pen()
  turtle.bgcolor("black")
  # Set up a list of any 8 valid Python
  color names colors = ["red", "yellow", "blue", "green", "orange", "purple", "white", "gray"]
  # Ask the user for the number of sides, between 1 and 8, with a default of 4
  sides = int(turtle.numinput("Number of sides",
                              "How many sides do you want (1-8)?", 4, 1, 8))
  # Draw a colorful spiral with the user-specified number of sides
  for x in range(360):
➊    t.pencolor(colors[x % sides])  # Only use the right number of colors
➋    t.forward(x * 3 / sides + x)   # Change the size to match number of sides
➌    t.left(360 / sides + 1)        # Turn 360 degrees / number of sides, plus 1
➍    t.width(x * sides / 200)       # Make the pen larger as it goes outward

该程序每次绘制新的一条边时都会使用用户输入的边的数量进行一些计算。让我们看看for循环内的四行带编号的代码。

在➊处,程序改变了海龟的画笔颜色,使颜色的数量与边的数量相匹配(三角形使用三种颜色,四边形使用四种颜色,依此类推)。在➋处,我们根据边的数量来改变每条线的长度(这样三角形在屏幕上就不会比八边形小得太多)。

在 ➌ 处,我们通过正确的角度转动海龟。为了得到这个角度,我们将 360 除以边数,这样可以得到外角,也就是我们需要转动的角度,才能绘制一个具有该边数的规则图形。例如,圆形的角度是 360 度,只有一个“边”;正方形由四个 90 度的角组成(总共也是 360 度);要绕六边形转动六个 60 度的角(同样总共是 360 度);依此类推。

最后,在 ➍ 处,我们随着距离屏幕中心的增大,增加笔的宽度或厚度。图 3-9 显示了输入八边形和三边形后的绘图结果。

来自 ColorSpiralInput.py 的图片,左侧是八边形,右侧是三边形

图 3-9. 来自 ColorSpiralInput.py 的图片,左侧是八边形,右侧是三边形

Python 完成你的作业

我们已经看到,Python 是一种强大且有趣的编程语言,可以处理各种数据:数字、字符串、列表,甚至是复杂的数学表达式。现在,你将利用 Python 的强大功能做一些非常实际的事情:你的数学作业!

我们将编写一个简短的程序,结合字符串和数字,使用 eval() 函数将数学问题转换为答案。在本章的早些时候,我提到过 eval() 函数可以将字符串 "20" 转换为数字 20。正如我承诺的,eval() 可以做更多的事情:它还可以将 "2 * 10" 转换为数字 20。当 eval() 函数处理一个键盘输入的字符串时,它会像 Python shell 一样对其进行求值。因此,当我们将数学问题作为输入时,运行 eval() 就能给出该问题的答案。

通过打印用户输入的原始问题,然后输出 eval(problem),我们可以将原始问题和答案显示在同一行。记得表 3-1 中的运算符:如果你需要 5 ÷ 2 的答案,你可以输入 5 / 2,而对于 4²,你可以输入 4 ** 2。以下是我们将程序 MathHomework.py 整合后的样子:

MathHomework.py

print("MathHomework.py")
# Ask the user to enter a math problem
problem = input("Enter a math problem, or 'q' to quit: ")
# Keep going until the user enters 'q' to quit
while (problem != "q"):
    # Show the problem, and the answer using eval()
    print("The answer to ", problem, "is:", eval(problem) )
    # Ask for another math problem
    problem = input("Enter another math problem, or 'q' to quit: ")
    # This while loop will keep going until you enter 'q' to quit

这个 while 语句会一直询问问题并打印答案,直到用户按下 Q 键退出程序。

尽管这个简单的程序还不能帮我们解决代数问题,但它可以处理比基础数学更多的内容。还记得我们讨论过 Python 的真除法吗?我们称之为“披萨除法”,因为它让我们可以将披萨*均分给任何数量的人。好吧,Python 仍然可以进行整数除法(即整除);我们只需要学习两个新的运算符。

你什么时候需要进行整数除法呢?假设你的老师给你和你三个朋友分发了 10 箱巧克力牛奶,大家想要公*地分配牛奶,让每个人获得相同数量的箱数。你们四个人(你加上三位朋友),所以 10 ÷ 4 等于 2.5。但不幸的是,你不能把一箱牛奶切成两半。如果有杯子,你可以把一箱牛奶分给两位朋友,但假设周围没有杯子。如果你想要公*,你就得每人拿两箱,剩下的两箱交还给老师。这听起来像长除法:剩下的两箱牛奶就是你用 10 除以 4 时的余数。在数学中,我们有时会像这样记录长除法的余数:10 ÷ 4 = 2 R2。换句话说,10 除以 4 的是 2,余数是 2。这意味着 4 能均匀地除进 10 两次,剩下 2。

在 Python 中,整数除法是通过双斜杠运算符//来进行的。所以10 // 4等于2,而7 // 4等于1(因为 4 只能除进 7 一次,余数是 3)。//运算符给我们的是商,那余数呢?要得到余数,我们使用取模运算符,在 Python 中用%符号表示。不要把%和百分号混淆——在 Python 中,百分比是用小数表示的(5%变成 0.05),而%运算符始终模数,也就是整数除法的余数。要得到长除法的余数,在 Python 中输入10 % 4(余数是2)或者7 % 4(余数是3)。图 3-10 展示了几种数学运算的结果,包括使用//%运算符进行的整数除法和余数运算。

Python 帮你做数学作业。

图 3-10. Python 帮你做数学作业。

随着我们继续阅读这本书,我们将在程序中使用%运算符,比如在我们的螺旋图形绘制中,用来将数字限制在某个范围内。

你学到了什么

在本章中,你学习了如何将不同类型的信息存储在变量中,包括数字、列表和字符串。你学会了在 Python 中命名变量的规则(字母、下划线、数字;区分大小写;不能有空格),以及如何用等号运算符(my_name = "Alex"my_age = 5)为变量赋值。

你还学习了整数(整数)和浮点数(小数值)。你了解了 Python 中的各种数学运算符,以及它们与数学课本中使用的符号的不同。你看到了如何使用字符串,包括如何让 Python 理解并计算某些字符串,就像当我们想使用用户输入的数字来进行计算时。

没有标题的图片

你看到了一些语法错误的例子,并学习了如何在编程时避免其中的一些。你了解了列表变量类型,能够用它存储各种类型的值,例如 colors = ["red", "yellow", "blue", "green"]。你甚至发现了 Python 如何帮助你进行简单的计算,包括长除法。

在第四章中,你将深入理解变量和数据类型,学习如何使用变量创建你自己的循环;在第五章中,你将学习如何让计算机做出决策;甚至在第六章及之后的章节中,你将学习如何编写程序让计算机玩游戏。变量是我们解决从视频游戏到卫星,再到医疗软件等最复杂问题的首要编程工具,它们帮助我们将复杂问题拆解为可以用代码解决的小块。继续练习本章的示例,并创建自己的示例,直到你对变量足够熟悉,以便进入下一个章节。

到这一点,你应该能够做到以下几点:

  • 创建自己的变量来存储数字、字符串和列表。

  • 讨论 Python 中数字类型的区别。

  • 使用 Python 中的基本数学运算符进行计算。

  • 解释字符串、数字和列表之间的区别。

  • 用英文写出简短的程序步骤,然后将这些步骤写成注释,帮助你构建代码。

  • 在各种情况下请求用户输入,并在你的程序中使用这些输入。

编程挑战

为了练习本章所学内容,尝试这些挑战。(如果遇到困难,请访问 www.nostarch.com/teachkids/ 查阅示例答案。)

#1: 圆形螺旋

回顾一下第二章中的 ColorCircleSpiral.py 程序 (ColorCircleSpiral.py),该程序在螺旋的每一侧绘制了圆圈,而不是线条。重新运行那个例子,看看你是否能够确定需要在哪些代码行中添加或删除内容,以便能够在 ColorSpiralInput.py 程序 (ColorSpiralInput.py) 中绘制具有任意边数(从一到八)的圆形螺旋。完成后,将新程序保存为 CircleSpiralInput.py

#2: 自定义名字螺旋

试想一下,如果让用户输入他们想要的螺旋边数、输入名字,然后画出一个螺旋,按照正确的螺旋边数和颜色写出他们的名字,是不是很酷?看看你能不能找出 SpiralMyName.py (SpiralMyName.py) 中哪些部分可以融入到 ColorSpiralInput.py (ColorSpiralInput.py) 中,从而创造出这个新颖且令人印象深刻的设计。当你搞定了(或者做出更酷的东西),把新程序保存为 ColorMeSpiralled.py

第四章. 循环很有趣(你可以再说一遍)

从我们编写第一个程序开始,就一直在使用循环来绘制重复的图形。现在是时候学习如何从头开始构建我们自己的循环了。每当我们需要在程序中做一些重复的事情时,循环可以让我们在不需要单独输入每个步骤的情况下重复这些步骤。图 4-1 展示了一个视觉示例——由四个圆圈组成的花纹。

四圈花纹图案

图 4-1. 四圈花纹图案

让我们思考一下如何编写程序来绘制四个重叠的圆圈,如图所示。正如你在第二章中看到的,Turtle 的circle()命令会根据我们在括号中指定的半径绘制一个圆形。这些圆形看起来像是位于屏幕的正北、正南、正东和正西,相距 90 度,我们知道如何将方向转 90 度。所以,我们可以编写四对语句来绘制一个圆圈,然后转 90 度,再绘制另一个圆圈,代码如下。将这段代码输入到一个新窗口,并将其保存为Rosette.py

Rosette.py

import turtle
t = turtle.Pen()
t.circle(100) # This makes our first circle (pointing north)
t.left(90)    # Then the turtle turns left 90 degrees
t.circle(100) # This makes our second circle (pointing west)
t.left(90)    # Then the turtle turns left 90 degrees
t.circle(100) # This makes our third circle (pointing south)
t.left(90)    # Then the turtle turns left 90 degrees
t.circle(100) # This makes our fourth circle (pointing east)

这段代码可以工作,但感觉是不是有点重复呢?我们编写了四次绘制圆形的代码,并且写了三次向左转的代码。通过我们之前的螺旋示例,我们知道应该能够将一段代码写一次,然后在for循环中重复使用。在本章中,我们将学习如何自己编写这些循环。现在就来试试吧!

构建你自己的for循环

为了构建我们自己的循环,首先需要确定重复的步骤。在前面的代码中,我们重复的指令是t.circle(100),用来绘制半径为 100 像素的龟形圆圈,以及t.left(90),用来在绘制下一个圆圈之前将龟形向左转 90 度。其次,我们需要弄清楚这些步骤需要重复多少次。我们想要绘制四个圆圈,所以从四开始。

既然我们已经知道了两个重复的指令和绘制圆形的次数,是时候构建我们的for循环了。

Python 中的for循环遍历一个项目列表,或者对于列表中的每个项目重复一次——例如数字 1 到 100,或 0 到 9。我们希望我们的循环运行四次——每个圆圈一次——因此我们需要设置一个包含四个数字的列表。

没有标题的图片

内建函数range()让我们能够轻松创建数字列表。构造n个数字范围的最简单命令是range(n);这个命令会让我们生成一个从 0 到n – 1(从零到小于n的数字)的数字列表。

例如,range(10)允许我们创建一个包含从 0 到 9 的 10 个数字的列表。让我们在 IDLE 命令提示符窗口中输入一些示例range()命令,看看它是如何工作的。为了查看我们打印出的列表,我们需要在range周围使用list()函数。在>>>提示符下,输入这一行代码:

>>> list(range(10))

IDLE 会给出输出[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]:一个包含 10 个数字的列表,从 0 开始。要获得更长或更短的数字列表,你可以在range()函数的括号中输入不同的数字:

>>> list(range(3))
[0, 1, 2]
>>> list(range(5))
[0, 1, 2, 3, 4]

如你所见,输入list(range(3))会给你一个包含三个数字的列表,从 0 开始,输入list(range(5))则会给你一个包含五个数字的列表,同样从 0 开始。

使用for循环绘制一个四圈花环

对于我们的四圈花环形状,我们需要重复画四次圆,range(4)可以帮助我们做到这一点。我们for循环的语法或词序将如下所示:

for x in range(4):

我们从关键字for开始,然后指定一个变量x,它将作为我们的计数器或迭代器变量。in关键字告诉for循环让x遍历范围列表中的每个值,而range(4)为循环提供从 0 到 3 的数字列表[0,1,2,3],让循环按顺序执行。记住,计算机通常从 0 开始计数,而不是像我们一样从 1 开始。

要告诉计算机哪些指令需要重复执行,我们使用缩进;我们通过在新文件窗口中按下 TAB 键来缩进每个需要在循环中重复执行的命令。输入这个新版本的程序并将其保存为Rosette4.py

Rosette4.py

import turtle
t = turtle.Pen()
for x in range(4):
    t.circle(100)
    t.left(90)

这是我们Rosette.py程序的一个更简短版本,得益于for循环,它仍然产生和没有循环版本一样的四个圆。这个程序总共循环执行第 3、4 和 5 行四次,在窗口的顶部、左侧、底部和右侧生成一个四圈花环。让我们逐步回顾这个循环,看看它是如何一次绘制一个圆的。

  1. 在第一次执行循环时,我们的计数器x的初始值是0,即[0, 1, 2, 3]范围列表中的第一个值。我们使用t.circle(100)在窗口的顶部画出第一个圆,然后使用t.left(90)将海龟向左转 90 度。

    没有标题的图片

  2. Python 回到循环的开始,将x设置为1,即[0, 1, 2, 3]中的第二个值。然后,它在窗口的左侧画出第二个圆,并将海龟向左转 90 度。

    没有标题的图片

  3. Python 再次回到循环中,将x增大到2。它在窗口底部画出第三个圆,并将海龟向左转。

    没有标题的图片

  4. 在第四次也是最后一次通过循环时,Python 将x增加到3,然后执行t.circle(100)t.left(90)来绘制第四个圆,并使海龟转动。玫瑰图案现在完成了。

    没有标题的图片

修改我们的 for 循环,制作六个圆的玫瑰图案

既然我们已经一起从头开始构建了自己的for循环,你能自己修改程序绘制一些新东西吗?如果我们想绘制一个有六个圆而不是四个圆的玫瑰图案,我们需要在程序中做什么改变呢?花点时间思考一下你如何解决这个问题。

没有标题的图片


你想到了什么点子吗?我们一起走一遍这个问题。首先,这次我们需要六个圆而不是四个,所以我们的for循环中的范围需要更改为range(6)。但是如果我们仅仅做这个更改,图形上不会看到任何不同,因为我们还是会继续绘制四个相隔 90 度的圆。如果我们想要六个圆围绕着玫瑰图案,我们需要把玫瑰图案分成六个左转,而不是四个。我们绘图中心周围有 360 度:四个 90 度的转弯让我们走了 4 × 90 = 360 度一圈。如果我们将 360 度除以 6 而不是 4,我们得到 360 ÷ 6 = 60 度每次转弯。所以在我们的t.left()命令中,我们需要每次通过循环时左转 60 度,即t.left(60)

修改你的玫瑰图程序,并将其保存为Rosette6.py

Rosette6.py

   import turtle
   t = turtle.Pen()
➊ for x in range(**6**):
➋     t.circle(100)
➌     t.left(**60**)

这次,➊中的 for 循环语句会让x遍历从05的六个值,所以我们将重复缩进部分的步骤➋和➌六次。在➋中,我们仍然绘制一个半径为100的圆。然而,在➌中,我们每次只转动 60 度,或者说是 360 度的六分之一,所以这次我们将绘制六个圆围绕着屏幕的中心,正如图 4-2 所示。

六个圆的玫瑰图案

图 4-2。六个圆的玫瑰图案

六个圆的玫瑰图案比四个圆的更漂亮,感谢我们的for循环,我们无需再写更多的代码行来绘制六个圆——我们只改动了两个数字!由于我们更改了这两个数字,你可能会想用变量来代替它们。让我们顺从一下这个诱惑,给用户提供绘制任意数量圆的能力。

通过用户输入改进我们的玫瑰图程序

在本节中,我们将使用我们在第三章中看到的turtle.numinput()函数(见 ColorSpiralInput.py),编写一个程序,要求用户输入一个数字,然后用这个数字的圆圈数绘制玫瑰图案。我们将用户输入的数字作为range()构造函数的大小。然后,我们所要做的就是将 360 度除以这个数字,就能得到每次循环时要左转的度数。键入并运行以下代码,保存为RosetteGoneWild.py

RosetteGoneWild.py

   import turtle
   t = turtle.Pen()
   # Ask the user for the number of circles in their rosette, default to 6
➊ number_of_circles = int(turtle.numinput("Number of circles",
                                           "How many circles in your rosette?", 6))
➋ for x in range(number_of_circles):
➌     t.circle(100)
➍     t.left(360/number_of_circles)

在➊处,我们通过将几个函数组合在一起,给一个变量赋值,名为number_of_circles。我们使用 Turtle 的numinput()函数询问用户要绘制多少个圆圈。第一个值Number of circles是弹出窗口的标题;第二个值How many circles in your rosette?是将在输入框中显示的文本;最后,6是默认值,以防用户没有输入任何内容。int()函数位于numinput()外部,将用户输入的数字转化为整数,以便我们可以在range()函数中使用。我们将用户输入的数字存储为number_of_circles,用于作为绘图循环中range()的大小。

➋处的for语句是我们的循环。它使用number_of_circles变量让x遍历一个包含如此多数字的列表。绘制圆圈的命令在➌处仍然相同,它将绘制半径为 100 像素的圆圈。在➍处,我们将 360 度的完整旋转除以圆圈的数量,这样就可以将圆圈均匀地分布在屏幕中心周围。例如,如果用户输入30作为圆圈的数量,则 360 ÷ 30 将得到每两个圆圈之间的 12 度角,如图 4-3 所示。

没有标题的图片

运行程序并尝试输入你自己的数字。你甚至可以创建一个包含 90 个圆圈的玫瑰图案,或者 200 个(但你可能得等一会儿,因为 Python 要画这么多圆圈!)。定制程序让它更符合你的需求:更改背景颜色或玫瑰图案的颜色,将圆圈做得更大或更小,或者同时做得更大更小!在创建程序并思考你想要它们做的有趣事情时,尽情地玩弄你的程序。图 4-4 展示了我的五岁儿子 Alex 通过在RosetteGoneWild.py中仅增加三行代码所创作的内容。请访问www.nostarch.com/teachkids/获取源代码。

用户定义的 30 个圆圈玫瑰图案

图 4-3. 用户定义的 30 个圆圈玫瑰图案

稍加想象和一小段代码就能将我们的玫瑰图程序变成五彩斑斓的有趣体验!

图 4-4。稍加想象和一小段代码就能将我们的玫瑰图程序变成五彩斑斓的有趣体验!

游戏循环与while循环

for循环很强大,但也有它的局限性。例如,假如我们希望在某个事件发生时停止循环,而不是遍历一个长长的数字列表,或者如果我们不确定要运行多少次循环呢?

例如,考虑一个游戏循环——当我们编写程序时,特别是游戏程序,用户可以选择继续玩还是退出。作为程序员,我们无法提前知道用户会选择玩多少次游戏或运行多少次程序,但我们需要给他们一个可以再次玩游戏而无需每次都重新加载和运行程序的能力。你能想象每次想再玩一次游戏时,都得重新启动 Xbox 或 PlayStation,或者每次玩游戏都必须准确玩 10 次,然后才切换到另一个游戏吗?那样可能会降低乐趣。

我们解决游戏循环问题的一种方法是使用另一种类型的循环——while循环。与for循环遍历预定义的值列表不同,while循环可以检查一个条件或情境,并决定是否继续循环或结束循环。while语句的语法如下:

while *condition*:
   *indented statement(s)*

这个条件通常是一个布尔值表达式,或是一个真/假测试。while循环的一个日常例子是吃饭和喝水。当你饿了,你就吃。当“我饿了吗?”这个问题的答案不再是“是”,这意味着“我饿了”的条件不再成立,你就停止吃饭。当你渴了,你就再喝一口水。当你不再感到渴时,你就停止喝水。饥饿和口渴是条件,当这些条件变为假时,你就退出吃饭和喝水的“循环”。while循环会在条件为真时继续重复循环中的语句(缩进的语句)。

没有标题的图片

while 循环中的真/假条件通常涉及比较值。我们可能会说:“x 的值大于 10 吗?只要大于 10,就运行这段代码。当 x 不再大于 10 时,停止运行代码。”换句话说,当条件 x > 10 评估为 True 时,我们运行这段代码 while。大于号(>)是一个比较运算符,与算术运算符如 +(加号)和 (减号)不同。比较运算符—如 >(大于)、<(小于)、==(等于)或 !=(不等于)— 允许你比较两个值,看看一个是否大于或小于另一个,或者它们是否相等或不相等。x 小于 7 吗?是或不是?True 还是 False?根据结果,TrueFalse,你可以让程序运行不同的代码。

while 循环与 for 循环有一些相似之处。首先,像 for 循环一样,它会根据需要重复一组语句。其次,使用 while 循环和 for 循环时,我们通过使用 TAB 键将语句缩进到右侧来告诉 Python 哪些语句需要重复。

让我们尝试一个带有 while 循环的程序,看看它如何运作。键入以下代码(或从 www.nostarch.com/teachkids/ 下载它),并运行:

SayOurNames.py

   # Ask the user for their name
➊ name = input("What is your name? ")
   # Keep printing names until we want to quit
➋ while name != "":
       # Print their name 100 times
➌     for x in range(100):
          # Print their name followed by a space, not a new line
➍        print(name, end = " ")
➎     print()  # After the for loop, skip down to the next line
       # Ask for another name, or quit
➏     name = input("Type another name, or just hit [ENTER] to quit: ")
➐ print("Thanks for playing!")

我们通过在 ➊ 处询问用户的名字并将其存储在变量 name 中来开始程序。我们需要一个名字来测试 while 循环的条件,所以我们必须在循环开始之前询问一次。然后,在 ➋ 处,我们启动了 while 循环,它会一直运行,直到用户输入的名字不是空字符串(由两个双引号表示,中间没有任何内容:"")。空字符串是当用户按下 ENTER 键以退出时 Python 所看到的输入。

在 ➌ 处,我们开始了 for 循环,它将打印名字 100 次,而在 ➍ 处,我们告诉 print() 语句每次打印名字后打印一个空格。我们会不断回到 ➌ 检查 x 是否已经达到 100,然后在 ➍ 打印,直到名字填满几行屏幕。当我们的 for 循环完成打印名字 100 次后,我们打印一个空白行,且没有空格 ➎,将输出移到下一行。然后,就该询问另一个名字了 ➏。

因为 ➏ 是最后一行缩进在 while 循环 ➋ 下,所以用户输入的新名字会被传回到 ➋,以便 while 循环可以检查它是否为空字符串。如果不是空的,我们的程序会启动 for 循环,打印新名字 100 次。如果名字是空字符串,这意味着用户按下了 ENTER 键结束程序,那么 while 循环会跳到 ➐,我们会感谢用户的参与。图 4-5 显示了当我的儿子们运行程序时的输出。

我的儿子们运行了*SayOurNames.py*并输入了我们全家每个人的姓名!

图 4-5. 我的儿子们运行了SayOurNames.py并输入了我们全家每个人的姓名!

家庭螺旋

现在我们可以请求姓名列表并将其打印到屏幕上,让我们将姓名打印循环与我们在第三章中的一个程序结合起来,在 SpiralMyName.py 中创建一个家庭或朋友姓名的彩色螺旋。

我们新的合并程序与SayOurNames.py中的名字重复器有一些不同,但最重要的区别是,我们不能仅仅一个一个地打印姓名;为了绘制我们的螺旋图形,我们需要一次性获取所有的姓名,以便在绘制螺旋时按顺序绘制每个姓名。

SayOurNames.py中,我们能够一次请求一个姓名,但对于我们的图形螺旋姓名程序,我们需要将所有姓名保存在一个列表中,就像我们对待颜色一样。然后,在遍历循环时,我们可以在螺旋的每个角落同时改变姓名和颜色。为此,我们将设置一个空列表:

family = [] # Set up an empty list for family names

每当我们在程序中创建颜色列表时,我们通常知道自己想使用的颜色名称,如红色、黄色、蓝色等等。然而,在我们的家庭名单中,我们必须等到用户输入姓名后才能知道具体内容。我们使用一个空列表——一对方括号[]——告诉 Python 我们将使用一个名为family的列表,但在程序运行之前我们并不知道这个列表中会有什么内容。

一旦我们有了一个空列表,我们可以像在SayOurNames.py中那样通过while循环请求姓名,并将这些姓名添加到列表中。append意味着将项添加到列表的末尾。在这个程序中,用户输入的第一个姓名将被添加到空列表中,第二个姓名将添加到第一个姓名之后,依此类推。当用户输入了他们想要的所有姓名后,他们会按下 ENTER 键告诉程序他们已经完成了姓名输入。然后,我们将使用for循环以彩色螺旋形状在屏幕上绘制这些姓名。

没有标题的图片

输入并运行以下代码,看看while循环和for循环如何一起完成一些美丽的工作:

SpiralFamily.py

   import turtle     # Set up turtle graphics
   t = turtle.Pen()
   turtle.bgcolor("black")
   colors = ["red", "yellow", "blue", "green", "orange",
          "purple", "white", "brown", "gray", "pink" ]
➊ family = []      # Set up an empty list for family names
   # Ask for the first name
➋ name = turtle.textinput("My family",
                          "Enter a name, or just hit [ENTER] to end:")
   # Keep asking for names
➌ while name != "":
      # Add their name to the family list
➍    family.append(name)
      # Ask for another name, or end
      name = turtle.textinput("My family",
                          "Enter a name, or just hit [ENTER] to end:")
  # Draw a spiral of the names on the screen
  for x in range(100):
➎    t.pencolor(colors[x%len(family)]) # Rotate through the colors
➏    t.penup()                         # Don't draw the regular spiral lines
➐    t.forward(x*4)                    # Just move the turtle on the screen
➑    t.pendown()                       # Draw the next family member's name
➒    t.write(family[x%len(family)], font = ("Arial", int((x+4)/4), "bold") )
➓    t.left(360/len(family) + 2)       # Turn left for our spiral

在➊,我们设置了一个空列表[],名为family,用于存储用户输入的姓名。在➋,我们通过turtle.textinput窗口请求第一个姓名,并在➌开始while循环收集所有的家庭成员姓名。将值添加到列表末尾的命令是append(),如在➍所示。该命令将用户输入的name添加到名为family的列表中。然后我们会请求另一个姓名,并继续重复while循环➌,直到用户按下 ENTER 键告诉我们他们已经完成输入。

我们的for循环与之前的螺旋相似,但在➎位置使用了一个新命令来设置笔的颜色。len()命令是长度的缩写,它告诉我们family列表中名字的数量。例如,如果你输入了四个家庭成员的名字,len(family)将返回4。我们使用取模运算符%和这个值来循环选择四种颜色,每个名字一个颜色。较大的家庭会循环更多的颜色(最多使用我们列表中的 10 种颜色),而较小的家庭则只需要更少的颜色。

在➏位置,我们使用penup()命令“抬起”海龟的笔,以便在➐位置前进时,海龟不会绘制任何东西;我们将在螺旋的角落绘制名字,而角落之间不会有线条。在➑位置,我们再次将海龟的笔放下,以便绘制我们的名字。

在➒位置,我们做了很多事情。首先,我们告诉海龟绘制哪个名字。请注意,family[x%len(family)]使用了取模运算符%,可以在用户输入的family列表中的名字之间进行循环。程序将从第一个输入的名字family[0]开始,然后依次是family[1]family[2],直到达到列表中的最后一个名字。这个语句中的font =部分告诉计算机我们想要使用 Arial 字体,并且采用粗体样式来绘制名字。它还设置了字体大小随着x的增长而增大;我们用(x+4)/4来计算字体大小,这意味着当循环结束时,x = 100,字体大小将是(100 + 4) / 4 = 26 磅——一个不错的大小。你可以通过修改这个公式来调整字体的大小。

最后,在➓位置,我们将海龟向左转360/len(family)度再加2度。对于一个有四个成员的家庭,我们会转 90 度再加 2 度,形成漂亮的正方形螺旋;六口之家的话,转 60 度再加 2 度,形成六边形螺旋,依此类推。额外的 2 度让螺旋稍微向左旋转,产生我们在其他螺旋中看到的漩涡效果。在图 4-6 中,我们运行了这个程序并输入了我们家庭的名字,包括我们的两只猫 Leo 和 Rocky,得到了一幅精彩的家庭螺旋图。

佩恩家庭螺旋,包括我们的两只猫 Leo 和 Rocky

图 4-6。佩恩家庭螺旋,包括我们的两只猫 Leo 和 Rocky

综合运用:螺旋走红

我们已经看到了循环的强大功能:它们将一段代码重复执行,以完成我们不想手动做的重复工作,比如打 100 次名字。让我们进一步深入了解循环,构建我们自己的嵌套循环,即一个循环内部再包含另一个循环(就像俄罗斯套娃——看一个娃娃,里面还有一个娃娃)。

没有说明文字的图片

为了探索嵌套循环,让我们绘制一个不仅仅是名字或线条的螺旋,而是 螺旋的螺旋!我们可以在螺旋的每个角落画一个小螺旋,而不是像在 图 4-6 中那样画一个名字。为此,我们需要一个大循环来绘制大螺旋,同时在里面嵌套一个小循环来绘制围绕大螺旋的小螺旋。

在编写程序之前,让我们先学习如何将一个循环嵌套到另一个循环中。首先,像往常一样启动一个循环。然后,在该循环内部,按一次 TAB 键并启动第二个循环:

# This is our first loop, called the outer loop
for x in range(10):
    # Things indented once will be done 10 times
    # Next is our inner loop, or the nested loop
    for y in range(10):
        # Things indented twice will be done 100 (10*10) times!

第一个循环称为 外部 循环,因为它包围了我们的嵌套循环。嵌套循环称为 内部 循环,因为它位于另一个循环内部。注意,在我们的嵌套循环中,任何缩进了两次的代码行(也就是说,它们在第二个循环内)将会重复 y 10 次,x 10 次,总共重复 100 次。

让我们开始编写程序 ViralSpiral.py。我们将一步步写—完成的程序可以在 ViralSpiral.py 中查看。

   import
   turtle t = turtle.Pen()
➊ t.penup()
   turtle.bgcolor("black")

我们程序的前几行看起来像我们编写的其他螺旋程序,不同之处在于我们不会为大螺旋绘制线条。我们计划用较小的螺旋代替这些线条,因此在 ➊ 我们使用了 t.penup() 来一开始就把乌龟的笔抬离屏幕。然后我们将背景色设置为黑色。

继续输入:我们还没完成!接下来,我们将使用 turtle.numinput() 提示用户输入他们想要的边数,默认值为 4,如果用户没有选择其他值,我们将限制允许的边数范围在 2 到 6 之间。

sides = int(turtle.numinput("Number of sides",
            "How many sides in your spiral of spirals (2-6)?", 4,2,6))
colors = ["red", "yellow", "blue", "green", "purple", "orange"]

turtle.numinput() 函数允许我们为输入对话框指定一个标题、一条提示问题,以及默认值、最小值和最大值,顺序如下:turtle.numinput(title, prompt, default, minimum, maximum)。在这里,我们指定了默认值 4,最小值 2 和最大值 6。(例如,如果用户尝试输入 17,他们将收到警告,告知最小允许值是 2,最大允许值是 6。)我们还设置了六种颜色的 colors 列表。

接下来我们将编写外部螺旋循环。外部循环将把乌龟定位到大螺旋的每个角落。

➋ for m in range(100):
      t.forward(m*4)
➌     position = t.position() # Remember this corner of the spiral
➍     heading = t.heading()   # Remember the direction we were heading

我们的外部循环将 m0 循环到 99,总共循环 100 次 ➋。在外部循环中,我们像其他螺旋程序一样前进,但当我们到达大螺旋的每个角落时,我们会停下来记住我们的 position ➌ 和 heading ➍。position 是乌龟在屏幕上的 (x, y) 坐标位置,heading 是乌龟正在移动的方向。

我们的海龟在大螺旋的每个位置上都会稍作偏离,去绘制小螺旋,因此在完成每个小螺旋后,它必须返回到该位置和朝向,以保持大螺旋的形状。如果我们在开始绘制小螺旋之前没有记住海龟的位置和方向,海龟就会在屏幕上乱窜,每个小螺旋都会从上一个小螺旋的位置出发,而不是按照大螺旋的形状。

告诉我们海龟位置和方向的两个命令是 t.position()t.heading()。海龟的位置通过 t.position() 访问,它包括海龟在屏幕上的 x(水*)和 y(垂直)坐标,就像坐标图上的点一样。海龟的朝向通过命令 t.heading() 获取,方向的角度范围是从 0.0 度到 360.0 度,0.0 度指向屏幕顶部。我们将在每次开始绘制小螺旋之前,将这些信息存储在变量 positionheading 中,以便每次都能在大螺旋中准确接着上次的地方绘制。

现在是内循环的时间了。我们在这里的缩进更加深入。这个内循环将在大螺旋的每个角落绘制一个小螺旋。

➎     for n in range(int(m/2)):
           t.pendown()
           t.pencolor(colors[n%sides])
           t.forward(2*n)
           t.right(360/sides - 2)
           t.penup()
➏     t.setx(position[0])     # Go back to the big spiral's x location
➐     t.sety(position[1])     # Go back to the big spiral's y location
➑     t.setheading(heading)   # Point in the big spiral's heading
➒     t.left(360/sides + 2)   # Aim at the next point on the big spiral

我们的内循环 ➎ 从 n = 0 开始,在 n = m/2 时停止,即 m 的一半,以保持内螺旋比外螺旋小。内螺旋看起来与我们之前的螺旋相似,唯一的区别是我们在绘制每一条线之前放下笔,绘制完每条线后抬起笔,这样我们的外螺旋保持干净。

在我们从 ➎ 绘制内螺旋后,我们通过将海龟的水*位置设置为我们在 ➌ 存储的位置,继续执行到 ➏。水*轴通常称为 x 轴,所以当我们设置水*位置时,我们使用 t.setx(),即设置海龟在屏幕上位置的 x 轴坐标。在 ➐,我们设置 y 轴位置,即在 ➌ 存储的垂直位置。在 ➑,我们将海龟转向我们在 ➍ 存储的朝向,然后继续进行大螺旋的下一个部分,直到 ➒。

当我们的外循环结束时,m099,我们将完成 100 个小螺旋,按大螺旋模式排列,形成一个漂亮的万花筒效果,如 图 4-7 所示。

一个正方形螺旋图,每个角落都有正方形螺旋(顶部),底部是一个五边形(五角形)螺旋图,由我们的 *ViralSpiral.py* 程序生成

图 4-7。一个正方形螺旋图,顶部是每个角落都有正方形螺旋的图形,底部是一个五边形(五角形)螺旋图,由我们的 ViralSpiral.py 程序生成

你会注意到,在等待程序运行时,嵌套循环的一个缺点:如图 Figure 4-7 and a five-sided (pentagonal) spiral of spirals (bottom) from our ViralSpiral.py program")所示的形状绘制比我们简单的螺旋要花费更长的时间。这是因为我们执行的步骤比简单螺旋多得多。事实上,当我们绘制六边形版本的ViralSpiral.py时,最终的图形由 2,352 条独立的线条组成!所有这些绘图命令,加上转动和设置画笔颜色,都需要大量的工作,即使是快速的计算机也需要时间。嵌套循环非常有用,但要记住,额外的步骤会拖慢程序的速度,因此只有当效果值得等待时,我们才使用嵌套循环。

这是ViralSpiral.py的完整代码。

没有标题的图片

ViralSpiral.py

import turtle
t = turtle.Pen()
t.penup()
turtle.bgcolor("black")
# Ask the user for the number of sides, default to 4, min 2, max 6
sides = int(turtle.numinput("Number of sides",
            "How many sides in your spiral of spirals? (2-6)", 4,2,6))
colors = ["red", "yellow", "blue", "green", "purple", "orange"]
# Our outer spiral loop
for m in range(100):
    t.forward(m*4)
    position = t.position() # Remember this corner of the spiral
    heading = t.heading()   # Remember the direction we were heading
    print(position, heading)
    # Our "inner" spiral loop
    # Draws a little spiral at each corner of the big spiral
    for n in range(int(m/2)):
        t.pendown()
        t.pencolor(colors[n%sides])
        t.forward(2*n)
        t.right(360/sides - 2)
        t.penup()
    t.setx(position[0])     # Go back to the big spiral's x location
    t.sety(position[1])     # Go back to the big spiral's y location
    t.setheading(heading)   # Point in the big spiral's heading
    t.left(360/sides + 2)   # Aim at the next point on the big spiral

你学到的内容

在本章中,你学会了通过识别程序中的重复步骤,并将这些步骤放入正确类型的循环中,来构建自己的循环。通过for循环,你可以让代码执行一定次数,例如使用for x in range(10)循环 10 次。通过while循环,你可以在满足条件或发生事件时运行代码,例如在用户没有输入任何内容时使用while name != ""

你学到,通过你创建的循环可以改变程序的流程。我们使用range()函数生成数值列表,帮助我们控制for循环的重复次数,并使用模运算符%来遍历列表中的值,从而更改颜色列表中的颜色,选择名字列表中的名字,等等。

我们使用了一个空列表[]append()函数将用户输入的信息添加到列表中,然后在程序中使用这个列表。你学到len()函数可以告诉你列表的长度——也就是列表中包含多少个值。

你学会了如何通过t.position()t.heading()函数记住海龟的当前位置和方向,并学会了如何通过t.setx()t.sety()t.setheading()将海龟带回该位置和方向。

最后,你看到了如何使用嵌套循环在一个循环内部重复执行另一组指令,首先在屏幕上打印一组名字,然后在万花筒模式中创建螺旋状的螺旋图案。在这个过程中,我们在屏幕上画了线条、圆圈以及一串串的单词或名字。

到此为止,你应该能够完成以下任务:

  • 创建你自己的for循环,将一组指令重复执行指定的次数。

  • 使用range()函数生成数值列表来控制你的for循环。

  • 使用append()函数创建空列表并向列表中添加元素。

  • 创建你自己的 while 循环,在条件为 True 时重复,或直到条件为 False

  • 解释每种类型的循环是如何工作的,以及如何在 Python 中编写它。

  • 举例说明每种类型的循环在什么情况下使用。

  • 设计并修改使用嵌套循环的程序。

编程挑战

尝试这些挑战,练习你在本章中学到的内容。(如果卡住了,可以访问 www.nostarch.com/teachkids/ 查找示例答案。)

#1: 螺旋玫瑰

想想你如何修改 ViralSpiral.py 程序,将小螺旋替换为像 Rosette6.py 中的玫瑰花样(修改我们的 for 循环来绘制带有六个圆的玫瑰花)和 RosetteGoneWild.py(RosetteGoneWild.py)。提示:首先用一个内循环替换内层循环,这个内循环将绘制一个玫瑰。然后,添加代码来改变每个玫瑰中圆的颜色和大小。为了增添趣味,当圆变大时,稍微调整画笔的宽度。当完成时,将新程序保存为 SpiralRosettes.py。图 4-8 显示了一个解决方案绘制的图形。

来自一个解决方案的螺旋玫瑰,解答编程挑战 #1

图 4-8。来自一个解决方案的螺旋玫瑰,解答编程挑战 #1

#2: 一螺旋中的多个家庭螺旋

绘制一个你家人名字的螺旋螺旋图形,岂不是很酷?看看 SpiralFamily.py(SpiralFamily.py),然后参考 ViralSpiral.py 的代码。在 SpiralFamily.py 中的 for 循环内创建一个内循环,用于绘制较小的螺旋。然后,修改外循环,在绘制每个小螺旋之前记住海龟的位置和朝向,并在继续到下一个大螺旋位置之前恢复它。当你完成后,将新程序保存为 ViralFamilySpiral.py

第五章 条件语句(如果?)

除了速度和准确性,计算机强大的另一个特点是它们能够快速评估信息并做出小的决策:温控器不断检查温度,并在温度高于或低于某个数值时迅速开启加热或制冷;新车上的传感器比我们反应更快,当前方有车突然停下时,它们会立即应用刹车;垃圾邮件过滤器会阻挡数十封电子邮件,以保持我们的收件箱清洁。

在这些情况下,计算机会检查一组条件:温度是否太冷?车前面是否有障碍物?电子邮件看起来像是垃圾邮件吗?

在第四章中,我们看到了一种使用条件做出决策的语句:while 语句。在那些例子中,条件告诉 while 循环应该执行多少次。如果我们想要根据是否执行一组语句来做决策会怎么样呢?想象一下,如果我们能编写一个程序,让用户决定他们是否希望在螺旋图案上使用圆形或其他形状。或者,如果我们想要圆形其他形状,像图 5-1 中的那样呢?

由  语句生成的玫瑰花形螺旋和更小的螺旋

图 5-1. 由 if 语句生成的玫瑰花形螺旋和更小的螺旋

没有标题的图像

使这一切成为可能的语句就是 if 语句。它询问是否某事为真,并根据答案决定是执行一组动作还是跳过它们。如果建筑物内的温度正常,暖气和空调系统不会运行,但如果温度过高或过低,系统会开启。如果外面下雨,你就带伞;否则你就不带。在本章中,我们将学习如何编程让计算机根据条件是否为真来做出决策。

if 语句

if 语句是一个重要的编程工具。它允许我们根据一个或一组条件来告诉计算机是否运行一组指令。通过 if 语句,我们可以告诉计算机做出选择。

if 语句的语法——即我们编写 if 语句的方式,使计算机能够理解——如下所示:

if *condition*:
   *indented statement(s)*

我们在if语句中测试的条件通常是一个布尔表达式,或者是一个真/假测试。布尔表达式的结果是TrueFalse。当你在if语句中使用布尔表达式时,你指定的是当表达式为真时要执行的动作或一组动作。如果表达式为真,程序将执行缩进的语句;如果为假,程序将跳过这些语句,并继续执行下一个未缩进的语句。

IfSpiral.py展示了一个if语句的代码示例:

IfSpiral.py

➊ answer = input("Do you want to see a spiral? y/n:")
➋ if answer == 'y':
➌     print("Working...")
       import turtle
       t = turtle.Pen()
       t.width(2)
➍     for x in range(100):
➎         t.forward(x*2)
➏         t.left(89)
➐ print("Okay, we're done!")

我们的IfSpiral.py程序的第一行 ➊要求用户输入yn,表示他们是否希望看到螺旋,并将用户的回答存储在answer中。在➋处,if语句检查answer是否等于'y'。请注意,测试“是否相等”的运算符使用的是两个等号==,以将其与赋值运算符(在➊处的单个等号)区分开来。==运算符检查answer'y'是否相等。如果相等,if语句中的条件为真。我们在测试变量时,如果是单个字符(例如用户输入的字符),会在字母或其他字符周围加上一对单引号(')。

如果我们在➋的条件为真,我们会在➌的屏幕上打印Working...,然后在屏幕上绘制一个螺旋。请注意,➌处的print语句以及绘制螺旋的语句一直到➏都被缩进了。这些缩进的语句只有在➋的条件为真时才会执行。否则,程序将跳过这些语句,直接跳到➐并打印Okay, we're done!

我们在➔4 之后的语句被缩进得更深(➎和➏)。这是因为它们属于for语句。就像我们在第四章通过缩进嵌套循环将一个循环添加到另一个循环中一样,我们也可以通过缩进整个循环将一个循环放入if语句中。

一旦螺旋完成,我们的程序将继续从➐开始,并告诉用户我们已经完成。这也是程序在用户在➊输入n或其他任何不是y的字符时跳转到的行。记住,如果➋的条件为False,整个if块从➌到➏都会被跳过。

在新的 IDLE 窗口中输入IfSpiral.py,或者从www.nostarch.com/teachkids/下载它,并运行几次,测试不同的答案。如果你在提示时输入字母y,你会看到像图 5-2 中的螺旋。

无标题图片如果你在 IfSpiral.py 中回答'y',你将看到像这样的螺旋。

图 5-2。如果你在IfSpiral.py中回答y,你将看到一个像这样的螺旋图案。

如果你输入一个小写字母y以外的字符,或者输入多个字符,程序将打印出Okay, we're done!并结束。

认识布尔值

布尔表达式,或称条件表达式,是重要的编程工具:计算机做决策的能力依赖于它评估布尔表达式为TrueFalse的能力。

我们必须使用计算机的语言告诉它我们想要测试的条件。Python 中条件表达式的语法是这样的:

*expression1 conditional_operator expression2*

每个表达式可以是一个变量、一个值或另一个表达式。在IfSpiral.py中,answer == 'y'是一个条件表达式,其中answer是第一个表达式,'y'是第二个表达式。条件运算符是==,用来检查answer是否等于'y'。除了==,Python 中还有许多其他的条件运算符。让我们来了解一些。

比较运算符

最常见的条件运算符是比较运算符,它让你测试两个值,看看它们如何互相比较。一个值是否大于或小于另一个值?它们是否相等?每次使用比较运算符进行比较时,都会产生一个条件,它的结果是TrueFalse。一个现实世界中的比较例子是你输入密码来进入大楼。布尔表达式将你输入的密码与正确的密码进行比较;如果输入匹配(等于)正确的密码,表达式的结果为True,门就会打开。

比较运算符展示在表 5-1 中。

表 5-1. Python 比较运算符

数学符号 Python 运算符 意义 示例 结果
< < 小于 1 < 2 True
> > 大于 1 > 2 False
<= 小于或等于 1 <= 2 True
>= 大于或等于 1 >= 2 False
= == 等于 1 == 2 False
!= 不等于 1 != 2 True

正如我们在第三章中看到的,Python 中的一些运算符与数学符号不同,这是为了让它们在标准键盘上更容易输入。小于大于使用我们熟悉的符号<>

对于小于或等于,Python 使用小于符号和等号符号一起,<=,中间没有空格。大于或等于也是如此,使用>=。记住不要在这两个符号之间加空格,否则会导致程序出错。

用于检查两个值是否相等的操作符是双等号 ==,因为单个等号已经用作赋值操作符。表达式 x = 5 将值 5 赋给变量 x,而 x == 5 用来测试 x 是否 等于 5。为了避免常见的错误,可以将双等号大声读作“等于”,这样就能避免错误地写出 if x = 5(错误的语句)而应写作正确的 if x == 5(“如果 x 等于 5”)。

没有标题的图片

用来测试两个值是否 不相等 的操作符是 !=,即感叹号后跟等号。你可以将其记为“不等于”,例如,看到 if x != 5 时,可以大声读作“如果 x 不等于 5”。

涉及条件操作符的测试结果是布尔值之一,TrueFalse。进入 Python shell,尝试输入图 5-3 中显示的一些表达式。Python 将返回 TrueFalse

在 Python shell 中测试条件表达式

图 5-3. 在 Python shell 中测试条件表达式

我们从进入 shell 开始,输入 x = 5 来创建一个名为 x 的变量,并赋值为 5。在第二行,我们通过单独输入 x 来检查其值,shell 会返回 5。我们的第一个条件表达式是 x > 2,也就是“x 大于 2”。Python 返回 True,因为 5 大于 2。接下来的表达式 x < 2(“x 小于 2”)在 x 等于 5 时为假,所以 Python 返回 False。剩余的条件表达式使用了 <=(小于或等于)、>=(大于或等于)、==(等于)和 !=(不等于)操作符。

每个条件表达式在 Python 中都会计算为 TrueFalse。这两者是唯一的布尔值,而且 True 中的 TFalse 中的 F 必须大写。TrueFalse 是 Python 内置的常量值。如果你将 True 写成 true(没有大写的 T)或将 False 写成 false,Python 将无法理解。

你还不够年龄!

让我们编写一个程序,使用布尔条件表达式来检查你是否足够年龄开车。在新窗口中输入以下内容,并将其保存为 OldEnough.py

OldEnough.py

➊ driving_age = eval(input("What is the legal driving age where you live? "))
➋ your_age = eval(input("How old are you? "))
➌ if your_age >= driving_age:
➍     print("You're old enough to drive!")
➎ if your_age < driving_age:
➏     print("Sorry, you can drive in", driving_age - your_age, "years.")

在 ➊,我们询问用户所在地区的合法驾龄,评估他们输入的数字,并将该值存储在变量 driving_age 中。在 ➋,我们询问用户当前的年龄,并将该数字存储在 your_age 中。

没有标题的图片

在➌处的if语句检查用户当前年龄是否大于或等于驾龄。如果➌评估为True,程序将运行➍处的代码,并打印出 "你足够大,可以开车!"。如果➌的条件评估为False,程序将跳过➍,并转到➎。在➎处,我们检查用户的年龄是否小于驾龄。如果是,程序将在➏处运行代码,并通过从your_age中减去driving_age来告诉用户距离他们可以开车还需多少年,并打印结果。图 5-4 展示了我和我儿子运行此程序的结果。

我足够大可以在美国开车,但我的五岁儿子不能。

图 5-4. 我足够大可以在美国开车,但我的五岁儿子不能。

唯一需要注意的是,在➎处的最后一个if语句显得有些冗余。如果用户在➌处已经足够大,我们就不需要再测试他们是否太年轻,因为我们已经知道他们不是。而如果用户在➌处不够大,我们也不需要在➎处测试他们是否太年轻,因为我们已经知道他们是。如果 Python 能有办法去掉这些不必要的代码就好了……嗯,恰好 Python确实有一种更短、更快的方法来处理像这种情况。

else语句

我们常常希望程序在某个条件为True时执行一件事情,在条件为False时执行另一件事情。实际上,这种情况非常常见,以至于我们有了一个快捷方式——else语句,它允许我们在不需要再次测试条件是否为False的情况下,直接测试条件是否为Trueelse语句只能在if语句后使用,不能单独使用,因此我们有时将这两者一起称为if-else。其语法如下:

if *condition*:
   *indented statement(s)*
else:
   *other indented statement(s)*

如果if语句中的条件为真,则执行if下的缩进语句,跳过else及其所有语句。如果if语句中的条件为假,则程序将直接跳到else下的其他缩进语句并执行它们。

我们可以使用else语句重写OldEnough.py,从而去除多余的条件判断(your_age < driving_age)。这不仅使代码更简洁易读,还能帮助避免在两个条件中出现编码错误。例如,如果我们在第一个if语句中测试your_age > driving_age,而在第二个if语句中测试your_age < driving_age,我们可能会不小心漏掉your_age == driving_age的情况。通过使用if-else语句对,我们只需测试if your_age >= driving_age来判断你是否足够大可以开车,并告知你,如果可以,否则进入else语句并打印你需要等多少年才能开车。

这是OldEnoughOrElse.py,它是OldEnough.py的修订版,使用了if-else语句代替了两个if语句:

OldEnoughOrElse.py

driving_age = eval(input("What is the legal driving age where you live? "))
your_age = eval(input("How old are you? "))
if your_age >= driving_age:
    print("You're old enough to drive!")
else:
    print("Sorry, you can drive in", driving_age - your_age, "years.")

两个程序的唯一区别是,我们将第二个if语句和条件替换为更短、更简单的else语句。

多边形或玫瑰花形

作为一个视觉示例,我们可以让用户输入他们是想绘制多边形(三角形、正方形、五边形等)还是玫瑰花形,指定边数或圆圈数。根据用户的选择(p表示多边形或r表示玫瑰花形),我们可以绘制准确的形状。

让我们输入并运行这个示例,PolygonOrRosette.py,它包含一个if-else语句对。

PolygonOrRosette.py

   import turtle
   t = turtle.Pen()
   # Ask the user for the number of sides or circles, default to 6
➊ number = int(turtle.numinput("Number of sides or circles",
               "How many sides or circles in your shape?", 6))
   # Ask the user whether they want a polygon or rosette
➋ shape = turtle.textinput("Which shape do you want?",
                           "Enter 'p' for polygon or 'r' for rosette:")
➌ for x in range(number):
➍     if shape == 'r':       # User selected rosette
➎         t.circle(100)
➏     else:                  # Default to polygon
➐         t.forward (150)
➑     t.left(360/number)

在➊处,我们要求用户输入边数(对于多边形)或圆圈数(对于玫瑰花形)。在➋处,我们给用户一个选择,p表示多边形,r表示玫瑰花形。运行程序几次,尝试每个选项,并输入不同的边数/圆圈数,看看➌处的for循环是如何工作的。

注意,➍到➑是缩进的,所以它们是for循环的一部分,位于➌,并且根据用户在➊输入的行数或圆圈数执行相应次数。➍处的if语句检查用户是否输入了r来绘制玫瑰花形,如果为真,➎将被执行并在当前位置绘制一个圆圈,作为玫瑰花形的一部分。如果用户输入了p或其他任何非r的内容,则会选择➏处的else语句,默认绘制一条线段,在➐处作为多边形的一条边。最后,在➑处我们按正确的角度(360 度除以边数或玫瑰花形的数量)左转并保持循环从➌到➑,直到形状完成。请参见图 5-5,查看示例。

我们的 PolygonOrRosette.py 程序,用户输入 7 边和 r 表示玫瑰花形

图 5-5。我们的PolygonOrRosette.py程序,用户输入7边和r表示玫瑰花形

偶数还是奇数?

if-else语句不仅可以测试用户输入。我们还可以用它来交替形状,就像在图 5-1 中那样,通过每次循环变量发生变化时,使用if语句测试它是偶数还是奇数。在每次偶数次通过循环时——当我们的变量等于024等——我们可以绘制一个玫瑰花形;在每次奇数次通过循环时,我们可以绘制一个多边形。

为了做到这一点,我们需要知道如何检查一个数字是奇数还是偶数。考虑一下我们如何判断一个数字是偶数;这意味着这个数字可以被 2 整除。有办法看一个数字是否可以被 2 *均整除吗?“*均整除”意味着没有余数。例如,4 是 偶数,因为 4 ÷ 2 = 2,没有余数。5 是 奇数,因为 5 ÷ 2 = 2,余数为 1。所以偶数除以 2 时余数为零,奇数除以 2 时余数为 1。还记得余数运算符吗?没错:就是我们熟悉的模运算符 %

在 Python 代码中,我们可以设置一个循环变量 m,并通过测试 m % 2 == 0 来检查 m 是否为偶数——也就是说,检查当我们将 m 除以 2 时,余数是否为零:

for m in range(number):
    if (m % 2 == 0): # Tests to see if m is even
        # Do even stuff
    else:            # Otherwise, m must be odd
        # Do odd stuff

让我们修改一个螺旋程序,在大螺旋的偶数角绘制花形,在奇数角绘制多边形。我们将使用一个大的 for 循环来绘制大螺旋,使用 if-else 语句来检查是绘制花形还是多边形,并使用两个小的内部循环来绘制花形或多边形。这将比我们到目前为止的大多数程序都要长,但注释将帮助解释程序的功能。输入并运行以下程序,RosettesAndPolygons.py,并确保检查循环和 if 语句的缩进是否正确。

RosettesAndPolygons.py

   # RosettesAndPolygons.py - a spiral of polygons AND rosettes!
   import turtle
   t = turtle.Pen()
   # Ask the user for the number of sides, default to 4
   sides = int(turtle.numinput("Number of sides",
               "How many sides in your spiral?", 4))
   # Our outer spiral loop for polygons and rosettes, from size 5 to 75
➊ for m in range(5,75):
       t.left(360/sides + 5)
➋     t.width(m//25+1)
➌     t.penup()       # Don't draw lines on spiral
       t.forward(m*4)  # Move to next corner
➍     t.pendown()     # Get ready to draw
       # Draw a little rosette at each EVEN corner of the spiral
➎      if (m % 2 == 0):
➏         for n in range(sides):
               t.circle(m/3)
               t.right(360/sides)
       # OR, draw a little polygon at each ODD corner of the spiral
➐     else:
➑         for n in range(sides):
               t.forward(m)
               t.right(360/sides)

让我们看看这个程序是如何工作的。在 ➊ 处,我们设置了一个从 5 到 75 的 for 循环;我们跳过了 0 到 4,因为很难看到大小为 4 像素或更小的形状。我们开始绘制螺旋;然后,在 ➋ 处,我们使用整数除法,使得每绘制 25 个形状后,画笔变宽(加粗)。图 5-6 显示了形状变大时,线条变得更粗。

在 ➌ 处,我们将海龟的画笔抬起并向前移动,这样就不会在花形和多边形之间画线了。到 ➍ 时,我们将画笔放下,并准备在大螺旋的角落处绘制一个形状。在 ➎ 处,我们测试循环变量 m,看看我们是否在偶数角绘制。如果 m 是偶数(m % 2 == 0),我们将在 ➏ 处使用 for 循环绘制花形。否则, ➐ 处的 else 告诉我们使用从 ➑ 开始的 for 循环绘制多边形。

没有标题的图像我们的 *RosettesAndPolygons.py* 程序的两次运行,用户输入为 4 边(上图)和 5 边(下图)

图 5-6. 我们的 RosettesAndPolygons.py 程序的两次运行,用户输入为 4 边(上图)和 5 边(下图)

注意,当我们使用偶数边数时,交替的形状会形成螺旋的不同部分,如图 5-6 所示。但当边数为奇数时,螺旋的每一条腿都与偶数(玫瑰花形)形状和奇数(多边形)形状交替。通过加上颜色和一些思考,你可以让这个程序绘制出像图 5-1 这样的设计。if-else语句为我们的编程工具包增添了另一维度。

Elif 语句

if语句还有一个有用的附加功能:elif子句。不是圣诞老人的助手哦!elif是一种将if-else语句串联在一起的方式,当你需要检查超过两个可能的结果时就能派上用场。关键字elif是“else if”的缩写。想想学校里的字母成绩:如果你在考试中得了 98 分,老师可能会根据评分标准给你 A 或 A+。但是如果你的分数较低,成绩不止 A 或 F(谢天谢地)。相反,老师可能会使用几个不同的成绩选项:A、B、C、D 或 F。

这是一个可以使用elif语句或一组elif语句的场景。我们以一个 10 分制的评分标准为例,90 分或以上为 A,80 到 89 分为 B,以此类推。如果你的分数是 95 分,我们可以打印字母成绩 A 并跳过其他选项。类似地,如果你得了 85 分,我们不需要再测试低于 B 的选项。if-elif-else结构帮助我们以直观的方式完成这一任务。尝试运行下面的程序,WhatsMyGrade.py,并输入不同的 0 到 100 之间的值。

WhatsMyGrade.py

➊ grade = eval(input("Enter your number grade (0-100): "))
➋ if grade >= 90:
       print("You got an A! :) ")
➌ elif grade >= 80:
       print("You got a B!")
➍ elif grade >= 70:
       print("You got a C.")
➎ elif grade >= 60:
       print("You got a D...")
➏ else:
       print("You got an F. :( ")

在➊,我们通过input()提示要求用户输入一个 0 到 100 之间的数值,将其通过eval()函数转换为数字,并将其存储在变量grade中。在➋,我们将用户的成绩与 90 进行比较,90 是 A 的字母成绩临界值。如果用户输入的分数为 90 或以上,Python 会打印You got an A! :),跳过其他elifelse语句,并继续执行程序的其余部分。如果分数不满 90,我们会进入➌,检查是否为 B 级成绩。同样,如果分数为 80 或以上,程序会打印正确的成绩,并跳过else语句。否则,➔ 语句检查 C 级成绩,➎检查 D 级成绩,最后,任何低于 60 的分数都会进入➏,并输出else语句中的You got an F. :(

我们可以使用if-elif-else语句来测试一个变量在多个值范围内的情况。然而,有时我们需要测试多个变量。例如,在决定穿什么衣服时,我们想知道温度(温暖或寒冷)和天气(晴天或下雨)。要结合条件语句,我们需要学习一些新技巧。

复杂条件:if、and、or、not

有时单个条件语句不足以解决问题。如果我们想知道天气是温暖而且晴朗,还是寒冷而且下雨,怎么办?

回想一下我们在本章中的第一个程序,当我们想要绘制螺旋时,如果输入y,就会得到答案。前两行代码请求输入并检查输入是否为y

answer = input("Do you want to see a spiral? y/n:")
if answer == 'y':

要看到螺旋,用户必须准确输入y;只有这个答案是被接受的。即使是类似的输入,比如大写Yyes,也不起作用,因为我们的if语句只检查y

解决Yy问题的一种简单方法是使用lower()函数,它将字符串转换为全小写字母。你可以在 IDLE 中尝试:

>>> 'Yes, Sir'.lower()
'yes, sir'

lower()函数将Yes, Sir中的大写YS转换为小写字母,其他部分的字符串保持不变。

我们可以在用户输入上使用lower(),这样无论他们输入的是Y还是yif语句中的条件都会是True

if answer.lower() == 'y':

现在,如果用户输入Yy,我们的程序会检查他们输入的答案的小写版本是否为y。但如果我们想检查完整的Yes这个单词,我们就需要使用复合if语句

复合if语句就像复合句:“我要去商店,而且我要买些杂货。”当我们想要做的不仅仅是测试一个条件是否为真时,复合if语句非常有用。我们可能想要测试这个条件另一个条件是否都为真。我们可能想测试这个条件另一个条件是否为真。我们也可能想知道这个条件是否为真。这些都是我们在日常生活中常做的事情。我们说:“如果天气冷而且下雨,我会穿上厚雨衣,” “如果刮风很冷,我会穿上夹克,”或者“如果下雨,我会穿上我最喜欢的鞋子。”

当我们构建复合if语句时,我们会使用表格 5-2 中展示的逻辑运算符

表格 5-2. 逻辑运算符

逻辑运算符 用法 结果
and if(condition1 and condition2): 仅当condition1condition2都为True时为真
or if(condition1 or condition2): 如果condition1condition2True,则为真
not if not(condition): 仅当conditionFalse时为真

我们可以使用or运算符来检查用户输入的是y yes,两者都可以。

answer = input("Do you want to see a spiral? y/n:").lower()
if answer == 'y' or answer == 'yes': # Checks for either 'y' or 'yes'

现在我们正在测试两个条件中的任何一个是否为True。如果其中任何一个是True,用户将看到螺旋图案。注意,我们在or关键字的两边写出完整的条件表达式:answer == 'y' or answer == 'yes'。新手程序员常犯的一个错误是试图通过省略第二个answer ==来缩短or条件。要记住正确使用or语句的方法,可以将每个条件分开考虑。如果or连接的任何条件评估为True,整个语句就为真,但每个条件必须完整才能使语句生效。

使用and的复合条件看起来类似,但and要求语句中的每个条件都为真,整个语句才会评估为True。举个例子,我们来编写一个程序,根据天气决定穿什么。可以在新窗口中输入WhatToWear.py,或者从www.nostarch.com/teachkids/下载并运行它:

WhatToWear.py

➊ rainy = input("How's the weather? Is it raining? (y/n)").lower()
➋ cold = input("Is it cold outside? (y/n)").lower()
➌ if (rainy == 'y' and cold == 'y'):      # Rainy and cold, yuck!
      print("You'd better wear a raincoat.")
➍ elif (rainy == 'y' and cold != 'y'):    # Rainy, but warm
      print("Carry an umbrella with you.")
➎ elif (rainy != 'y' and cold == 'y'):    # Dry, but cold
      print("Put on a jacket, it's cold out!")
➏ elif (rainy != 'y' and cold != 'y'):    # Warm and sunny, yay!
      print("Wear whatever you want, it's beautiful outside!")

在➊,我们询问用户外面是否在下雨,在➋,我们询问是否寒冷。我们还通过在input()函数的两行末尾加上lower()函数来确保rainycold中存储的答案是小写字母。有了这两个条件(是否下雨以及是否寒冷),我们可以帮助用户决定穿什么。在➌,复合if语句检查是否既下雨又寒冷;如果是,程序建议穿雨衣。在➍,程序检查是否既下雨又不寒冷。对于下雨但不寒冷的天气,程序推荐带伞。在➎,我们检查是否下雨(rainy 不等于 'y'),但仍然寒冷,需要穿夹克。最后,在➏,如果不下雨并且不寒冷,随便穿什么都行!

秘密信息

现在我们理解了如何使用条件语句,接下来我们将学习如何使用凯撒密码编码和解码秘密信息。密码是一个秘密的代码,或者是一种改变信息的方式,使其更加难以阅读。凯撒密码以尤利乌斯·凯撒命名,据说他喜欢通过在字母表中移动字母来发送私人信息:

SECRET MESSAGES ARE SO COOL! -> FRPERG ZRFFNTRF NER FB PBBY!

我们可以通过使用一个编码环来创建一个简单的凯撒密码,示例如图 5-7。要创建加密信息,首先决定密钥,即你想将每个字母移动多少个字母。在编码信息中以及在图 5-7 中,每个字母都被一个13的密钥值所移动,这意味着我们取一个想要编码的字母,然后在字母表中向后数 13 个字母,以得到我们的编码字母。A变成NB变成O,依此类推。

我们有时称这种转换为 旋转,因为当我们到达 M(它变为 Z)时,我们已经到达字母表的末尾。为了能够编码 N,我们会重新回到 AO 会变成 B,一直到 Z,它会变成 M。这里有一个凯撒密码查找表的示例,使用密钥值 13,每个字母都会按 13 个字母进行编码或解码:

凯撒密码

图 5-7. 凯撒密码

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
N O P Q R S T U V W X Y Z->A B C D E F G H I J K L M

注意到一个模式了吗?字母 A 被编码为 N,而 N 被编码为 A。我们称这为 对称密码对称码,因为它具有 对称性——它在两个方向上是相同的。我们可以使用相同的密钥 13 来编码和解码消息,因为英语字母表有 26 个字母,密钥值 13 意味着我们将每个字母正好偏移一半。你可以尝试用你自己的消息:HELLO -> URYYB -> HELLO

如果我们能写一个程序,逐个查看秘密消息中的每个字母,然后通过将其向右偏移 13 个字母来编码该字母,那么我们就能将编码后的消息发送给拥有相同程序的人(或能够破解密码模式的人)。为了编写一个操作字符串中单个字母的程序,我们需要掌握更多处理字符串的技能。

操控字符串

Python 提供了强大的字符串处理函数。有内建函数可以将字符组成的字符串转换为全大写字母,函数可以将单个字符转换为其数字等价物,还有函数可以告诉我们一个字符是否是字母、数字或其他符号。

让我们从一个将字符串转换为大写字母的函数开始。为了让我们的编码器/解码器程序更容易理解,我们将把消息转换为全大写字母,这样我们只需编码一组 26 个字母(AZ),而不是两组(AZaz)。将字符串转换为全大写字母的函数是 upper()。任何后跟点(.)和函数名 upper() 的字符串都将返回相同的字符串,字母变为大写,其他字符保持不变。在 Python shell 中,尝试输入你的名字或任何其他字符串并加上 .upper(),看看这个函数的效果:

>>> 'Bryson'.upper()
'BRYSON'
>>> 'Wow, this is cool!'.upper()
'WOW, THIS IS COOL!'

如我们之前所见,lower() 函数执行的是相反的操作:

>>> 'Bryson'.lower()
'bryson'

你可以使用 isupper() 函数检查一个字符是否是大写字母:

>>> 'B'.isupper()
True
>>> 'b'.isupper()
False
>>> '3'.isupper()
False

你也可以使用 islower() 函数检查一个字符是否是小写字母:

>>> 'P'.islower()
False
>>> 'p'.islower()
True

字符串是字符的集合,因此在 Python 中使用 for 循环遍历字符串会将字符串拆分为单个字符。这里,letter 将循环遍历字符串变量 message 中的每个字符:

for letter in message:

最后,我们可以使用常规的加法运算符 +(加号)将字符串连接起来,或将字母添加到字符串中:

>>> 'Bry' + 'son'
'Bryson'
>>> 'Payn' + 'e'
'Payne'

在这里,我们将第二个字符串附加到第一个字符串的末尾。将字符串连接在一起称为 附加。你可能还会看到字符串加法被称为 连接;只需记住,这是将两个或更多字符串相加的高级术语。

字符的值

我们构建编码器/解码器程序所需的最终工具是能够对单个字母进行数学运算,例如将字母 A 的值加 13 得到字母 N。Python 有一个或两个可以帮助实现这一功能的函数。

每个字母、数字和符号在计算机存储时都会转换为数字值。最流行的数字系统之一是 ASCII(美国信息交换标准代码)。 表 5-3 显示了某些键盘字符的 ASCII 值。

表 5-3:标准 ASCII 字符的数字值

符号 描述
32 空格
33 ! 感叹号
34 " 双引号
35 # 井号
36 `
--- --- ---
32 空格
33 ! 感叹号
34 " 双引号
35 # 井号
美元符号
37 % 百分号
38 & 和号
39 ' 单引号、撇号
40 ( 左括号
41 ) 右括号
42 * 星号
43 + 加号
44 , 逗号
45 - 连字符
46 . 句号、点、圆点
47 / 斜杠或除号
48 0
49 1
50 2
51 3
52 4
53 5
54 6
55 7
56 8
57 9
58 : 冒号
59 ; 分号
60 < 小于
61 = 等号
62 > 大于
63 ? 问号
64 @ @ 符号
65 A 大写字母 A
66 B 大写字母 B
67 C 大写字母 C
68 D 大写字母 D
69 E 大写字母 E
70 F 大写字母 F
71 G 大写字母 G
72 H 大写字母 H
73 I 大写字母 I
74 J 大写字母 J
75 K 大写字母 K
76 L 大写字母 L
77 M 大写字母 M
78 N 大写字母 N
79 O 大写字母 O
80 P 大写字母 P
81 Q 大写字母 Q
82 R 大写字母 R
83 S 大写字母 S
84 T 大写字母 T
85 U 大写字母 U
86 V 大写字母 V
87 W 大写字母 W
88 X 大写字母 X
89 Y 大写字母 Y
90 Z 大写字母 Z
91 [ 左括号
92 \ 反斜杠
93 ] 右括号
94 ^ 插入符号、抑扬符号
95 _ 下划线
96 ` 重音符号
97 a 小写字母 a

将字符转换为其 ASCII 数字值的 Python 函数是 ord()

>>> ord('A')
65
>>> ord('Z')
90

反向函数是 chr()

>>> chr(65)
'A'
>>> chr(90)
'Z'

这个函数将数字值转换为相应的字符。

我们的编码器/解码器程序

有了这些部分,我们可以组成一个程序,它接收一条消息并将其全部转换为大写。然后,它会遍历消息中的每个字符,如果字符是字母,则将其按 13 个字符偏移来编码或解码,最后将字母添加到输出消息中,并打印输出消息。

EncoderDecoder.py

   message = input("Enter a message to encode or decode: ") # Get a message
➊ message = message.upper()          # Make it all UPPERCASE :)
➋ output = ""                        # Create an empty string to hold output
➌ for letter in message:             # Loop through each letter of the message
➍     if letter.isupper():           # If the letter is in the alphabet (A-Z),
➎         value = ord(letter) + 13   # shift the letter value up by 13,
➏         letter = chr(value)        # turn the value back into a letter,
➐         if not letter.isupper():   # and check to see if we shifted too far
➑             value -= 26            # If we did, wrap it back around Z->A
➒             letter = chr(value)    # by subtracting 26 from the letter value
➓     output += letter               # Add the letter to our output string
  print("Output message: ", output)   # Output our coded/decoded message

第一行提示用户输入要编码或解码的消息。在➊处,upper()函数将消息转换为全大写,以便程序更容易读取字母,并简化编码的编写。在➋处,我们创建一个空字符串(双引号之间没有任何内容,""),命名为output,我们将在其中逐个字母存储编码后的消息。➌处的for循环利用了 Python 将字符串视为字符集合的特性;变量letter将遍历字符串message中的每个字符,一次一个。

在➍处,isupper()函数检查消息中的每个字符,看看它是否是大写字母(AZ)。如果是,在➎处我们通过ord()获取该字母的 ASCII 数值,并加上 13 来进行编码。在➏处,我们通过chr()将新的编码值转回字符,而在➐处,我们检查它是否仍然是AZ的字母。如果不是,我们在➑处通过从编码值中减去 26 将字母循环回字母表的前面(这就是Z变成M的方式),然后在➒处将新的值转为其字母对应的字符。

在➓处,我们使用+=运算符将字母添加到output字符串的末尾(将字符附加到字符串末尾)。+=运算符是结合数学运算(+)和赋值(=)的快捷运算符之一,output += letter意味着将letter添加到output中。这是for循环中的最后一行,所以这个过程会对输入消息中的每个字符重复,直到output被逐个字母构建为整个消息的编码版本。循环完成后,程序的最后一行会打印输出消息。

你可以使用这个程序发送编码消息来娱乐,但你应该知道,它并不像现代的消息编码方法那样安全——任何能解开星期日报纸谜题的人都能读懂你发送的编码消息——因此只应在和朋友玩乐时使用。

通过网络搜索加密密码学,了解如何让秘密消息更安全的科学原理。

没有标题的图片

你学到了什么

在本章中,你学会了如何编程让计算机根据代码中的条件做出决策。我们看到,if语句让程序只在条件为真时执行一系列语句(比如age >= 16)。我们使用布尔值(真/假)表达式来表示我们想检查的条件,并使用条件运算符如<><=等来构建表达式。

我们将ifelse语句组合在一起,以便在if语句不执行时执行else语句。我们进一步扩展了这一点,通过使用if-elif-else语句从多个选项中选择,如在我们的成绩程序中,根据输入的数字分数给出 A、B、C、D 或 F 的成绩。

我们学会了如何使用andor逻辑运算符同时测试多个条件来组合条件(如rainy == 'y' and cold == 'y')。我们使用not运算符来检查一个变量或表达式是否为False

在本章末尾的秘密信息程序中,你学到了所有字母和字符在计算机中存储时都会转换为数值,并且 ASCII 是一种将文本存储为数字值的方法。我们使用了chr()ord()函数来转换字符为其 ASCII 值并反向转换。我们使用upper()lower()将字母串转换为全大写或全小写,并使用isupper()islower()检查字符串是否为大写或小写。我们通过使用+运算符将字母逐个添加到字符串末尾,构建了一个字符串,并且学到了将字符串连接起来有时被称为附加连接

到此为止,你应该能够做到以下几点:

  • 使用if语句根据条件做出决策。

  • 使用条件语句和布尔表达式来控制程序流程。

  • 描述布尔表达式如何评估为TrueFalse

  • 使用比较运算符(<>==!=<=>=)编写条件表达式。

  • 使用if-else语句组合来选择两条不同的程序路径。

  • 使用模运算符%测试一个变量是否为奇数或偶数。

  • 编写if-elif-else语句,从多个选项中进行选择。

  • 使用andor一次测试多个条件。

  • 使用not运算符检查一个值或变量是否为False

  • 解释字母和其他字符如何在计算机中存储为数值。

  • 使用ord()chr()将字符转换为其 ASCII 等价物,并进行反向转换。

  • 使用各种字符串函数(如lower()upper()isupper())操作字符串。

  • 使用+运算符将字符串和字符连接在一起。

编程挑战

为了练习你在本章中学到的内容,尝试这些挑战。(如果遇到困难,可以访问* www.nostarch.com/teachkids/* 获取示例答案。)

#1: 彩色花环和螺旋

对于更具视觉挑战性的任务,回顾图 5-1 中的彩色螺旋和罗塞塔图案图像。你应该能够修改 RosettesAndPolygons.py,使其更加多彩,并且如果你愿意,可以用小型螺旋替换多边形,以匹配图 5-1 中的插图。

#2: 用户定义的密钥

对于更具文本挑战性的任务,可以通过允许用户输入自己的密钥值(1 到 25 之间的数值)来创建我们EncoderDecoder.py程序的高级版本,以确定消息需要位移多少字母。然后,在EncoderDecoder.py中标记为➎的那一行(我们的编码/解码程序),不再每次位移 13,而是根据用户提供的密钥值进行位移。

为了解码使用不同密钥发送的消息(假设我们使用5作为密钥值,那么A变为FB变为G,依此类推),接收消息的人需要知道密钥。他们通过再次使用反向密钥(26 减去密钥值,即 26 - 5 = 21)来恢复消息,这样F就会回绕到AG变为B,依此类推。

如果你希望使这个程序更易于使用,可以首先询问用户他们是否希望编码还是解码(输入ed),然后询问他们输入一个密钥值并将其存储为key(即字母位移的数量)。如果用户选择编码,就在➎处将密钥值加到每个字母上;如果选择解码,则将26 - key加到每个字母上。将这个程序发送给朋友,开始消息发送吧!

第六章 随机的乐趣与游戏:大胆尝试,挑战一下!

在第五章中,我们编写了程序让计算机根据条件做出决策。在这一章中,我们将编程让计算机在 1 到 10 之间选择一个数字,进行石头剪刀布游戏,甚至掷骰子或抽牌!

这些游戏的共同元素是随机性的概念。我们希望计算机在 1 到 10 之间随机选择一个数字,我们猜测那个数字是什么。我们希望计算机随机选择石头、剪刀或布,然后我们选择出招并看看谁赢了。这些例子——加上骰子游戏、纸牌游戏等等——都叫做机会游戏。当我们掷五个骰子来玩 Yahtzee 时,我们通常每次掷出的结果都会不同。正是这种随机性让这些游戏充满乐趣。

没有说明的图片

我们可以编程让计算机执行随机行为。Python 有一个叫做random的模块,允许我们模拟随机选择。我们可以使用random模块在屏幕上绘制随机形状,也可以编程制作机会游戏。让我们从一个猜数字游戏开始。

一个猜数字游戏

我们可以在经典的 Hi-Lo 猜数字游戏中使用随机数字。一个玩家选择一个 1 到 10 之间(或 1 到 100 之间)的数字,另一个玩家尝试猜测这个数字。如果猜测太高,猜测者就尝试一个更小的数字。如果猜测太低,他们会尝试一个更大的数字。当他们猜对时,他们就赢了!

我们已经知道如何使用if语句比较数字,也知道如何通过input()while循环来保持猜测。我们唯一需要学习的新技能是如何生成一个随机数字。我们可以通过random模块来实现这一点。

首先,我们必须使用命令import random导入random模块。你可以在 Python Shell 中输入import random并按下 ENTER 键来试试。这个模块有几个不同的函数来生成随机数字。我们将使用randint(),它是随机整数的缩写。randint()函数期望我们在它的括号内提供两个参数——也就是两个信息——:我们想要的最低值和最高值。指定括号中的最低值和最高值将告诉randint()从什么范围中随机选择。请在 IDLE 中输入以下内容:

>>> import random
>>> random.randint(1, 10)

Python 将返回一个介于 1 到 10 之间的随机数字,包括(这意味着随机数字可以是 1 和 10)。尝试运行random.randint(1, 10)命令几次,看看返回的不同数字。(提示:你可以使用 ALT-P,或在 Mac 上使用 CONTROL-P,重复最*输入的行,而无需重新输入整个命令。)

如果你运行那行代码足够多次(至少 10 次),你会注意到数字有时会重复,但从你所见,它们没有任何规律。我们称这些为伪随机数,因为它们并不是真正随机的(randint命令基于复杂的数学模式告诉计算机接下来应该“选择”哪个数字),但它们看起来是随机的。

让我们在一个名为GuessingGame.py的程序中使用random模块。请在新的 IDLE 窗口中键入以下内容,或者从www.nostarch.com/teachkids/下载该程序:

GuessingGame.py

➊ import random
➋ the_number = random.randint(1, 10)
➌ guess = int(input("Guess a number between 1 and 10: "))
➍ while guess != the_number:
➎     if guess > the_number:
          print(guess, "was too high. Try again.")
➏     if guess < the_number:
          print(guess, "was too low. Try again.")
➐     guess = int(input("Guess again: "))
➑ print(guess, "was the number! You win!")

在➊处,我们导入了random模块,它为我们提供了对random中所有已定义函数的访问,包括randint()。在➋处,我们写下模块名称random,后面跟一个点和我们要使用的函数名randint()。我们将110作为参数传递给randint(),让它生成一个 1 到 10 之间的伪随机数字,并将该数字存储在变量the_number中。这将是用户尝试猜测的秘密数字。

在➌处,我们要求用户在 1 到 10 之间猜一个数字,评估这个数字并将其存储在变量guess中。我们的游戏循环从➍处的while语句开始。我们使用!=(不等于)运算符检查猜测是否不等于秘密数字。如果用户第一次猜对了,guess != the_number的结果为False,那么while循环就不会执行。

只要用户的猜测不等于秘密数字,我们会通过两个if语句在➎和➏处检查猜测是太高(guess > the_number)还是太低(guess < the_number),然后打印一条消息要求用户再次猜测。在➐处,我们接受用户的另一个猜测并重新开始循环,直到用户猜对为止。

在➑处,用户已经猜出了数字,所以我们告诉他们这是正确的数字,然后程序结束。有关程序的一些示例运行,请参见图 6-1。

我们的 GuessingGame.py 程序,要求用户猜测三个随机数字是更大还是更小

图 6-1. 我们的GuessingGame.py程序,要求用户猜测三个随机数字是更大还是更小

在图 6-1 中程序的第一次运行中,用户猜测了 5,计算机回应说 5 太高了。用户猜了 2,结果是太低了。然后用户猜了 3,正好!每次在最低和最高可能数字之间猜测一半的数字,就像图 6-1 中的例子一样,这种策略叫做二分查找

如果玩家学会了使用这个策略,他们每次都可以在四次尝试内猜出 1 到 10 之间的数字!试试看吧!

为了使程序更有趣,你可以改变传递给randint()函数的参数,生成 1 到 100 之间的数字,或者更大的数字(记得也要修改input()的提示)。你还可以创建一个名为number_of_tries的变量,每次用户猜测时将其加 1,以跟踪用户的尝试次数。在程序结束时打印出尝试次数,让用户知道他们表现如何。为了增加额外的挑战,你可以添加一个外循环,在用户猜对数字后询问他们是否想重新开始游戏。自己试试这些,并访问www.nostarch.com/teachkids/查看示例解决方案。

五彩斑斓的随机螺旋线

random模块除了randint()之外还有其他有用的函数。让我们使用它们来帮助我们创建一个有趣的视觉效果:屏幕上充满了像图 6-2 那样的随机大小和颜色的螺旋线。

屏幕上随机位置的随机大小和颜色的螺旋线,来自 RandomSpirals.py

图 6-2. 屏幕上随机位置的随机大小和颜色的螺旋线,来自RandomSpirals.py

想想你如何编写一个程序,像创建图 6-2 那样的程序。你几乎已经掌握了绘制这些随机螺旋线所需的所有技巧。首先,你可以使用循环绘制不同颜色的螺旋线。你可以生成随机数,并使用其中一个来控制每个螺旋线的for循环运行的次数。这会改变它的大小:更多的迭代生成更大的螺旋,而较少的迭代生成较小的螺旋。让我们看看还需要什么,并一步一步构建程序。(最终版本是 RandomSpirals.py。)

选择一种颜色,任意颜色

我们需要的一个新工具是选择随机颜色的能力。我们可以使用random模块中的另一个方法random.choice()来轻松实现这一点。random.choice()函数将列表或其他集合作为参数(括号内的部分),并返回该集合中的随机选择元素。在我们的案例中,我们可以创建一个颜色列表,然后将该列表传递给random.choice()方法,为每个螺旋图形选择一个随机颜色。

你可以在 IDLE 的命令行窗口中尝试以下操作:

>>> # Getting a random color
>>> colors = ["red", "yellow", "blue", "green", "orange", "purple", "white", "gray"]
>>> random.choice(colors)
'orange'
>>> random.choice(colors)
'blue'
>>> random.choice(colors)
'white'
>>> random.choice(colors)
'purple'
>>>

在这段代码中,我们创建了我们以前的老朋友colors并将其设置为一个颜色名称的列表。然后我们使用random.choice()函数,将colors作为参数传递给它。该函数会从列表中随机选择一个颜色。第一次,我们得到了橙色,第二次是蓝色,第三次是白色,依此类推。这个函数可以为我们提供一个随机颜色,用来在绘制每个新螺旋图形之前设置我们的乌龟的笔颜色。

没有标题的图片

获取坐标

另一个剩下的问题是如何让螺旋图形在整个屏幕上展开,包括右上角和左下角。为了将螺旋图形随机放置在乌龟屏幕上,我们需要了解在我们的 Turtle 环境中使用的 x 和 y 坐标系统。

笛卡尔坐标

如果你曾经上过几何学课程,你一定见过在图表纸上绘制的(x, y)坐标,如同图 6-3 所示。这些是笛卡尔坐标,得名于法国数学家勒内·笛卡尔,他用一对数字标记网格上的点,这些数字我们称之为x-坐标y-坐标

在图 6-3 中的图表中,深色的横线叫做x 轴,它从左到右延伸。深色的竖线叫做y 轴,从下到上延伸。我们称这两条线相交的点(0, 0)为原点,因为网格上的所有其他点都是以该点为起点,按坐标测量而得的。可以将原点(0, 0)视为你的屏幕的中心。你想找到的每个其他点,都可以通过从原点出发,向左或向右、向下或向上移动来标记其 x 和 y 坐标。

一个包含四个点及其笛卡尔(x, y)坐标的图表

图 6-3. 一个包含四个点及其笛卡尔(x, y)坐标的图表

我们通过将一对坐标放在括号内并用逗号分隔来标记图上的点:(xy)。第一个数字 x 坐标告诉我们要向左或向右移动多远,而第二个数字 y 坐标告诉我们要向上或向下移动多远。正的 x 值告诉我们从原点向右移动;负的 x 值告诉我们向左移动。正的 y 值告诉我们从原点向上移动,负的 y 值告诉我们向下移动。

看看图 6-3 中标记的各个点。右上方的点标注了 x 和 y 坐标(4, 3)。为了找到这个点的位置,我们从原点(0, 0)开始,向右移动 4 个单位(因为 x 坐标 4 是正数),然后向上移动 3 个单位(因为 y 坐标 3 是正数)。

要到达右下角的点(3, –3),我们先回到原点,然后向右移动 3 个单位。由于这次 y 坐标是–3,所以我们向移动 3 个单位。向右移动 3 个单位并向下移动 3 个单位后,我们到达(3, –3)的位置。对于(–4, 2),我们从原点向移动 4 个单位,再向上移动 2 个单位,最终到达左上方的点。最后,对于(–3, –2),我们向左移动 3 个单位,再向下移动 2 个单位,最终到达左下方的点。

设置随机海龟位置

在海龟图形中,我们可以通过告诉计算机新位置的 x 和 y 坐标,使用turtle.setpos(x, y)命令将海龟从原点(0, 0)移动到任何其他位置。函数名setpos()设置位置的缩写。它将海龟的位置设置为我们给定的 x 和 y 坐标。例如,turtle.setpos(10,10)将把海龟从屏幕中心向右移动 10 个单位并向上移动 10 个单位。

在计算机中,我们通常使用的单位是我们老朋友像素。所以turtle.setpos(10,10)将把海龟从屏幕中心向右移动 10 个像素并向上移动 10 个像素。由于像素非常小——在大多数显示器上大约是 1/70 英寸(0.3 毫米)或更小——我们可能希望一次移动 100 个像素或更多。setpos()可以处理我们给定的任何坐标。

为了将海龟移动到屏幕上的一个随机位置,我们将生成一对随机数字 xy,然后使用 turtle.setpos(x,y) 将海龟移动到这些坐标。但在移动海龟之前,我们需要用 turtle.penup() 抬起海龟的笔。在设置新位置后,我们会调用 turtle.pendown() 将笔放下,允许海龟再次绘图。如果我们忘记抬起笔,海龟在通过 setpos() 移动到指定位置时会绘制一条线。正如你在图 6-2 中看到的那样,我们不希望螺旋之间有额外的线条。我们的代码看起来是这样的:

t.penup()
t.setpos(x,y)
t.pendown()

setpos() 函数结合几个随机数字作为 (x, y) 坐标,可以让我们把螺旋图形放置在不同的位置,但我们如何知道应该使用什么范围来生成随机数呢?这个问题引出了我们在寻找随机螺旋时必须解决的最后一个问题。

我们的画布有多大?

现在我们知道如何将螺旋图形随机放置在窗口或画布的不同位置,但仍然有一个问题:我们如何知道画布的大小呢?我们可以为一个位置生成随机的 x 和 y 坐标,并在该位置绘制一个螺旋,但我们如何确保所选的位置在可视窗口内,而不是位于窗口的右侧、左侧、顶部或底部之外呢?然后,我们如何确保从左到右、从上到下覆盖整个绘图窗口呢?

为了回答有关画布大小的问题,我们需要使用两个额外的函数,turtle.window_width()turtle.window_height()。首先,window_width() 会告诉我们海龟窗口的宽度,单位是像素。同样,window_height() 会告诉我们从海龟窗口底部到顶部的像素数。例如,我们的海龟窗口在图 6-2 中宽度为 960 像素,高度为 810 像素。

turtle.window_width()turtle.window_height() 将帮助我们确定随机的 x 和 y 坐标,但我们还面临一个障碍。记住,在海龟图形中,窗口的中心是原点,即 (0, 0)。如果我们只是生成介于 0 和 turtle.window_width() 之间的随机数,第一个问题是我们永远无法在窗口的左下角绘制任何东西:那里坐标在 x 轴和 y 轴上都是负数(向左和向下),但一个介于 0 和 window_width() 之间的随机数总是正数。第二个问题是,如果我们从中心开始,向右移动 window_width(),我们就会超出窗口的右边界。

没有标题的图像

我们不仅需要弄清楚窗口的宽度和高度,还需要了解坐标的范围。例如,如果我们的窗口宽度为 960 像素,并且原点(0, 0)位于窗口的中心,我们需要知道可以向右和向左移动多少像素而不超出可视窗口。因为原点(0, 0)位于窗口的正中间,所以我们只需要将宽度除以二。如果原点位于一个宽度为 960 像素的窗口的中心,那么原点右侧有 480 像素,左侧也有 480 像素。x 坐标的范围将是从–480(从原点向左 480 像素)到+480(从原点向右 480 像素),换句话说,就是从–960/2 到+960/2。

为了使我们的范围适用于任何大小的窗口,我们可以这样写,x 坐标的范围从-turtle.window_width()//2+turtle.window_width()//2。我们的原点也位于窗口从上到下的中间,所以原点上方和下方各有turtle.window_height()//2像素。我们在这些计算中使用整数除法,即//运算符,以确保在除以 2 时得到整数结果;因为窗口的宽度可能是奇数像素,我们希望保持所有像素测量值为整数。

现在我们知道如何计算画布的大小了,我们可以使用这些表达式来限制随机坐标的范围。这样,我们就能确保生成的任何随机坐标都能在我们的窗口中可见。Python 中的random模块有一个函数,可以让我们在指定的范围内生成一个随机数:randrange()。我们只需要告诉randrange()函数,以窗口宽度的一半负值作为范围的起始值,以窗口宽度的一半正值作为范围的结束值(我们需要在程序中导入turtlerandom,才能让这些代码行生效):

x = random.randrange(-turtle.window_width()//2,
                     turtle.window_width()//2)
y = random.randrange(-turtle.window_height()//2,
                     turtle.window_height()//2)

这些代码行将使用randrange()函数生成一对(x, y)坐标值,这些坐标始终位于视图窗口内,并覆盖视图窗口的整个区域,从左到右,从下到上。

将所有部分整合在一起

现在我们已经有了所有的部分——我们只需要将它们组合在一起,构建一个程序,用于在不同的颜色、大小和位置上绘制随机的螺旋线。以下是我们完成的RandomSpirals.py程序;仅用大约 20 行代码,它就能在图 6-2 中创建出类似万花筒的图像。

RandomSpirals.py

  import random
  import turtle
  t = turtle.Pen()
  turtle.bgcolor("black")
  colors = ["red", "yellow", "blue", "green", "orange", "purple",
            "white", "gray"]
  for n in range(50):
      # Generate spirals of random sizes/colors at random locations
➊     t.pencolor(random.choice(colors)) # Pick a random color
➋     size = random.randint(10,40) # Pick a random spiral size
      # Generate a random (x,y) location on the screen
➌     x = random.randrange(-turtle.window_width()//2,
                            turtle.window_width()//2)
➍     y = random.randrange(-turtle.window_height()//2,
                            turtle.window_height()//2)
➎     t.penup()
➏     t.setpos(x,y)
➐     t.pendown()
➑     for m in range(size):
          t.forward(m*2)
          t.left(91)

首先,我们导入randomturtle模块,并设置我们的海龟窗口和颜色列表。在我们的for循环中(n将从049,总共绘制 50 个螺旋),事情变得有趣起来。在➊处,我们将colors传递给random.choice(),让这个函数从列表中随机选择一种颜色。然后我们将随机选择的颜色传递给t.pencolor(),以将海龟的笔颜色设置为该随机颜色。在➋处,random.randint(10,40)从 10 到 40 之间随机选取一个数字。我们将这个数字存储在变量size中,并在➑处使用它来告诉 Python 绘制螺旋的线条数。➌和➍处的代码正是我们之前编写的,用来生成一对随机坐标值(xy),从而给我们提供一个随机位置,用于在视图窗口中显示螺旋。

在➎处,我们在将海龟移动到新的随机位置之前,先抬起海龟的笔。在➏处,我们通过设置海龟的位置为xy,即之前通过randrange()选择的随机坐标,来将海龟移动到新位置。现在海龟已到达目标位置,我们在➐处放下笔,这样我们就能看到我们即将绘制的螺旋。在➑处,我们有一个for循环来绘制螺旋的每一条线。在range(size)中,海龟会前进m*2的距离,绘制一段长度为m*2的线段(m0123,以此类推,所以线段的长度分别为 0、2、4、6 等)。然后海龟会向左旋转 91 度,为绘制下一段线段做好准备。

海龟从螺旋的中心开始,绘制一个长度为 0 的线段,然后向左旋转;这就是第一次进入循环。下一次进入时,m1,于是海龟绘制一个长度为 2 的线段,然后旋转。随着 Python 在循环中迭代,海龟将从螺旋的中心向外移动,绘制越来越长的线段。我们使用随机生成的size,它是一个介于 10 到 40 之间的整数,表示我们在螺旋中绘制的线条数。

在绘制完当前螺旋之后,我们回到外部for循环的顶部。我们选择一个新的随机颜色、大小和位置;抬起笔;将海龟移动到新位置;放下笔;然后进入内部for循环绘制一个新的随机大小的螺旋。绘制完这个螺旋后,我们返回外部循环并重复整个过程。我们这样做 50 次,最终得到 50 个颜色和形状各异的螺旋,随机分布在屏幕上。

石头剪刀布

现在我们已经掌握了编程的技能,可以编写一个游戏:石头剪刀布。两个玩家(或一个玩家和计算机)各自选择三个可能项之一(石头、剪刀或布);然后双方展示各自的选择;胜者根据三个规则判定:石头压剪刀,剪刀剪布,布包石头。

为了模拟这个游戏,我们将创建一个选择列表(就像我们在 RandomSpirals.py 中的 colors 列表),然后使用 random.choice() 从列表中随机选择三个项目中的一个作为计算机的选择。接着,我们将询问用户的选择,并使用一系列 if 语句来确定胜者。用户将与计算机对战!

让我们进入代码吧。在 IDLE 中新建一个窗口,输入 RockPaperScissors.py,或者从 www.nostarch.com/teachkids/ 下载它。

RockPaperScissors.py

➊ import random
➋ choices = ["rock", "paper", "scissors"]
   print("Rock crushes scissors. Scissors cut paper. Paper covers rock.")
➌ player = input("Do you want to be rock, paper, or scissors (or quit)? ")
➍ while player != "quit":                 # Keep playing until the user quits
      player = player.lower()              # Change user entry to lowercase
➎     computer = random.choice(choices)   # Pick one of the items in choices
      print("You chose " +player+ ", and the computer chose " +computer+ ".")
➏     if player == computer:
          print("It's a tie!")
➐     elif player == "rock":
          if computer == "scissors":
              print("You win!")
          else:
              print("Computer wins!")
➑     elif player == "paper":
          if computer == "rock":
              print("You win!")
          else:
              print("Computer wins!")
➒     elif player == "scissors":
          if computer == "paper":
              print("You win!")
          else:
              print("Computer wins!")
      else:
          print("I think there was some sort of error...")
      print()                              # Skip a line
➓     player = input("Do you want to be rock, paper, or scissors (or quit)? ")

在 ➊,我们导入 random 模块,以便访问帮助我们做出随机选择的函数。在 ➋,我们设置包含三项(石头、纸和剪刀)的列表,并将其命名为 choices。我们打印游戏的简单规则,确保用户了解它们。在 ➌,我们提示用户输入他们的选择:rockpaperscissorsquit,并将他们的选择存储在变量 player 中。在 ➍,我们通过检查用户是否在输入提示中选择了 quit 来开始游戏循环;如果选择了,游戏结束。

只要用户没有输入 quit,游戏就开始了。在将玩家输入转换为小写以便在我们的 if 语句中进行比较后,我们让计算机选择一个项目。在 ➎,我们让计算机从列表 choices 中随机选择一个项目,并将该项目存储在变量 computer 中。计算机的选择存储后,就可以开始测试谁赢了。在 ➏,我们检查玩家和计算机是否选择了相同的项目;如果是这样,我们告诉用户结果是*局。否则,我们在 ➐ 检查用户是否选择了 rock。在 ➐ 的 elif 语句中,我们嵌套一个 if 语句来检查计算机是否选择了 scissors。如果玩家选择了石头,而计算机选择了剪刀,那么石头砸剪刀,玩家获胜!如果不是石头对石头,并且计算机没有选择剪刀,那么计算机一定选择了纸,并且我们会打印出计算机获胜。

在剩下的两个 elif 语句中,➑ 和 ➒,我们做相同的测试,以检查玩家选择纸或剪刀时谁获胜。如果这些语句都不成立,我们会告诉用户他们输入了一个无法识别的选项:要么是选择的项目不存在,要么是他们拼写错误。最后,在 ➓,我们让用户输入下一个选择,然后重新开始游戏循环(即新的一轮)。请参见 图 6-4,了解程序的示例运行。

感谢计算机的随机选择,RockPaperScissors.py 是一款有趣的游戏!

图 6-4。感谢计算机的随机选择,RockPaperScissors.py 是一款有趣的游戏!

有时用户获胜,有时计算机获胜,有时则是*局。由于结果具有一定的随机性,游戏足够有趣,可以用来打发一些时间。现在我们对两人对战的游戏如何利用计算机的随机选择有了初步的了解,接下来让我们尝试创建一个扑克牌游戏。

选一张牌,任意一张牌

让扑克牌游戏有趣的一件事就是随机性。每一轮的结果都不完全相同(除非你洗牌技术差),所以你可以一遍又一遍地玩,而不会觉得无聊。

我们可以用我们学到的技能编写一个简单的扑克牌游戏。我们第一次尝试时不会显示图形牌面(我们需要学更多技巧才能实现),但我们可以通过使用一个数组,或字符串列表,像在螺旋程序中使用颜色名称一样,生成一个随机的卡牌名称(比如“二点红心”或“黑桃国王”)。我们可以编写一个像“战争”游戏的程序,在游戏中两名玩家各自从牌组中抽一张随机卡牌,拥有更大卡牌的一方获胜;我们只需要找到一种方法来比较卡牌大小。让我们一步一步看看这个过程是如何工作的。(最终的程序是 HighCard.py。)

堆叠牌组

首先,我们需要考虑如何在程序中构建一个虚拟的牌组。正如我之前提到的,我们暂时不会绘制牌面,但至少需要卡牌的名称来模拟牌组。幸运的是,卡牌名称只是字符串("二点红心", "黑桃国王"),而且我们知道如何构建字符串数组——从第一章开始,我们就已经用颜色名称做过类似的事情了!

数组是一个有序的或编号的相似事物的集合。在许多编程语言中,数组是一种特殊类型的集合。然而,在 Python 中,列表可以像数组一样使用。在本节中,我们将看到如何将列表当作数组来处理,逐个访问数组中的元素。

我们可以通过创建一个数组名称(cards),并将其设置为包含所有 52 张卡牌名称的列表,来构建一个包含所有卡牌名称的列表:

cards = ["two of diamonds",
         "three of diamonds",
         "four of diamonds",
         # This is going to take forever...

但哎呀——我们得输入 52 个长字符串的卡牌名称!在我们甚至开始编写游戏部分之前,代码就已经有 52 行了,而且我们会因为输入太多而感到疲惫,甚至没力气玩游戏。肯定有更好的方法。让我们像程序员一样思考!这些输入是重复的,我们想让计算机来做这些重复的工作。花色名称(方块, 红心, 梅花, 黑桃)每个会重复 13 次,因为每个花色有 13 张卡牌。面值(二点王牌)每个会重复 4 次,因为有 4 种花色。更糟的是,我们还要输入 52 次of这个词!

当我们之前遇到重复时,我们使用了循环来简化问题。如果我们想生成整个卡牌堆,循环会做得很好。但我们并不需要整个卡牌堆来玩一局战争游戏:我们只需要两张卡牌,计算机的卡牌和玩家的卡牌。如果循环不能帮助我们避免重复所有这些花色和面值,我们需要进一步拆解问题。

在战争游戏中,每个玩家出一张卡牌,较高的卡牌获胜。所以,正如我们讨论过的,我们只需要两张卡牌,而不是 52 张。让我们从一张卡牌开始。卡牌名称由一个面值(从二到 A)和一个花色名称(从梅花到黑桃)组成。这些看起来非常适合用作字符串列表:一个列表存储面值,一个列表存储花色。我们不再使用包含 52 个重复条目的列表来表示每一张卡牌,而是从 13 个可能的面值中随机选择一个面值,然后从 4 个可能的花色中随机选择一个花色。这个方法应该能帮助我们生成卡牌堆中的任何一张卡。

我们将之前的长数组cards替换成了两个更短的数组,suitsfaces

suits = ["clubs", "diamonds", "hearts", "spades"]
faces = ["two", "three", "four", "five", "six", "seven", "eight", "nine",
         "ten", "jack", "queen", "king", "ace"]

我们把 52 行代码减少到了大约 3 行!这真是聪明的编程。现在让我们来看如何使用这两个数组发牌。

发牌

image with no caption

我们已经知道如何使用random.choice()函数从列表中随机选择一个项目。所以要发牌,我们只需使用random.choice()从花色列表中随机选取一个面值,并从套牌列表中随机选取一个花色。一旦我们有了随机的面值和花色,完成卡片名称的唯一操作就是在它们之间加上of这个词(例如,two of diamonds)。

请注意,使用random.choice()这种方式,我们可能会连续发出相同的卡牌。我们没有强制程序检查卡牌是否已经发出,所以你可能会连续两次发到黑桃 A。例如,计算机并没有作弊;我们只是没有告诉它只能从一副牌中发牌。这就像程序从一个无限的牌堆中发牌一样,它可以永远发下去而不会用完。

import random
suits = ["clubs", "diamonds", "hearts", "spades"]
faces = ["two", "three", "four", "five", "six", "seven", "eight", "nine",
         "ten", "jack", "queen", "king", "ace"]
my_face = random.choice(faces)
my_suit = random.choice(suits)
print("I have the", my_face, "of", my_suit)

如果你尝试运行这段代码,每次都会得到一张新的随机卡牌。要发第二张卡牌,你可以使用类似的代码,但你会把随机选出的值存储在名为your_faceyour_suit的变量中。你需要修改print语句,以便打印这张新卡的名称。现在,我们离战争游戏的实现更*了一步,但我们还需要一种方法来比较计算机的卡和玩家的卡,看看谁赢了。

计数卡牌

我们将面牌值按升序排列的原因,从二到王牌,是希望卡牌的faces列表按从低到高的顺序排列,以便我们能相互比较卡牌,看看任意一对卡牌中哪一张的值更高。确定两张卡牌中哪一张更高很重要,因为在战争游戏中,较高的卡牌获胜。

在列表中查找项目

幸运的是,由于 Python 中列表和数组的工作方式,我们可以确定一个值在列表中出现的位置,并且可以利用这个信息来判断一张牌是否比另一张大。列表或数组中一个项的位置编号称为该项的索引。我们通常通过索引来引用数组中的每一项。

要查看 suits 数组及每个花色的索引的可视化表示,请参见 表 6-1。

表 6-1. suits 数组

"clubs" "diamonds" "hearts" "spades"
索引 0 1 2 3

当我们创建列表 suits 时,Python 会自动为列表中的每个值分配一个索引。计算机从零开始计数,因此 "clubs" 的索引是 0"diamonds" 的索引是 1,以此类推。查找列表中某个项的索引的函数是 .index(),它可以用于 Python 中的任何列表或数组。

要查找花色名称 "clubs" 在列表 suits 中的索引,我们调用函数 suits.index("clubs")。就像我们在询问 suits 数组哪个索引对应于值 "clubs"。让我们在 Python shell 中试试这个操作。输入以下行:

>>> suits = ["clubs", "diamonds", "hearts", "spades"]
>>> suits.index("clubs")
0
>>> suits.index("spades")
3
>>>

在我们创建了花色值的数组 suits 后,我们向 Python 询问值 "clubs" 的索引是什么,它会返回正确的索引 0。同样,"spades" 的索引是 3,而 "diamonds""hearts" 分别位于索引 12

哪张牌更大?

我们创建了一个从 twoacefaces 数组,因此值 two(即 faces 中的第一个项)会得到索引 0,直到 ace 位于索引 12(从 0 开始的第 13 个位置)。我们可以通过索引来判断哪张牌的值更大——换句话说,哪个面值的索引更大。我们最小的牌是 two,它的索引是最小的 0;最大的牌是 ace,它的索引是最大的 12

如果我们生成两个随机的面牌值(my_faceyour_face),我们可以比较 my_face 的索引与 your_face 的索引,看看哪张牌更大,如下所示。

import random
faces = ["two", "three", "four", "five", "six", "seven", "eight", "nine",
         "ten", "jack", "queen", "king", "ace"]
my_face = random.choice(faces)
your_face = random.choice(faces)
if faces.index(my_face) > faces.index(your_face):
    print("I win!")
elif faces.index(my_face) < faces.index(your_face):
    print("You win!")

我们使用random.choice()两次,从faces数组中抽取两个随机值,然后将这些值存储在my_faceyour_face中。我们使用faces.index(my_face)来查找my_facefaces中的索引,使用faces.index(your_face)来获取your_face的索引。如果my_face的索引较大,那么我的卡牌面值较高,程序会输出我赢了!。否则,如果my_face的索引低于your_face的索引,那么你的卡牌面值较高,程序会输出你赢了!。由于我们按顺序排列了列表,更高的卡牌总是对应较高的索引。有了这个方便的工具,我们几乎拥有了构建“高牌”游戏(如战争游戏)所需的一切。(我们还没有加入检测*局的功能,但我们会在完整程序整合部分添加这个功能。)

让游戏继续进行

我们需要的最终工具是一个循环,让用户可以继续玩游戏,直到他们愿意为止。我们将稍微不同地构建这个循环,以便能够在其他游戏中重用它。

首先,我们需要决定使用哪种类型的循环。记住,for循环通常意味着我们确切知道我们想要做某件事的次数。因为我们无法总是预测玩家会玩多少次游戏,所以for循环不适合我们的需求。while循环可以一直执行,直到某个条件为假——例如,当用户按下某个键以结束程序时。while循环将是我们游戏循环的选择。

while循环需要一个条件来检查,因此我们将创建一个变量,作为我们的标志,即结束程序的信号。我们将这个标志变量命名为keep_going,并将其初始值设置为True

keep_going = True

由于我们开始时将keep_going = True,程序将至少进入一次循环。

接下来,我们会问用户是否想继续玩游戏。为了让用户不必每次都输入Yyes,我们可以通过让他们按 ENTER 键来简化操作。

answer = input("Hit [Enter] to keep going, any other keys to exit: ")
if answer == "":
    keep_going = True
else:
    keep_going = False

在这里,我们将变量answer设置为一个输入函数。然后,我们使用if语句检查answer == "",以判断用户是否仅按了 ENTER 键,还是在按 ENTER 键之前按了其他键。(空字符串""告诉我们用户在按 ENTER 之前没有输入其他字符。)如果用户想退出,他们只需让answer等于空字符串以外的任何值""。换句话说,他们只需要在按 ENTER 键之前按下任意键,布尔表达式answer == ""将评估为False

我们的if语句检查answer == ""是否为True,如果是,它会将True存储在我们的标志变量keep_going中。但你注意到有些重复吗?如果answer == ""True,我们将值True赋给keep_going;如果answer == ""评估为False,我们需要将值False赋给keep_going

如果我们直接将keep_going设置为answer == ""的结果,那会更简单。我们可以用下面的更简洁的代码替换我们的代码:

answer = input("Hit [Enter] to keep going, any other keys to exit: ")
keep_going = (answer == "")

第一行没有变化。第二行将keep_going设置为布尔表达式answer == ""的结果。如果结果是True,则keep_going将为True,我们的循环将继续。如果结果是False,则keep_going将为False,我们的循环将结束。

让我们看一下整个循环:

keep_going = True
while keep_going:
    answer = input("Hit [Enter] to keep going, any key to exit: ")
    keep_going = (answer == "")

在这里,我们加入了while语句,因此只要keep_going的值为True,我们的循环就会继续。在最终的程序中,我们将“包裹”这个while循环,用来执行一次完整的游戏回合。我们通过将while语句放在选择卡片的代码前面,并在宣布谁赢之后放置提示信息来实现这一点。记得缩进循环中的代码!

将所有部分整合在一起

将所有这些组件组合在一起,我们可以构建一个类似于战争游戏的游戏,我们将其命名为HighCard.py。计算机会为自己和玩家各抽一张牌,检查哪张牌更大,并宣布胜者。输入HighCard.py的代码到一个新的 IDLE 窗口中,或访问www.nostarch.com/teachkids/下载并开始游戏。

HighCard.py

import random
suits = ["clubs", "diamonds", "hearts", "spades"]
faces = ["two", "three", "four", "five", "six", "seven", "eight", "nine",
         "ten", "jack", "queen", "king", "ace"]
keep_going = True
while keep_going: my_face = random.choice(faces)
    my_suit = random.choice(suits)
    your_face = random.choice(faces)
    your_suit = random.choice(suits)
    print("I have the", my_face, "of", my_suit)
    print("You have the", your_face, "of", your_suit)
    if faces.index(my_face) > faces.index(your_face):
        print("I win!")
    elif faces.index(my_face) < faces.index(your_face):
        print("You win!")
    else:
        print("It's a tie!")
    answer = input("Hit [Enter] to keep going, any key to exit: ")
    keep_going = (answer == "")

运行游戏,它将打印出计算机的卡片和你的卡片,接着宣布谁赢了,并提示你选择是继续玩还是退出。玩几轮后,你会注意到卡片的随机性足以让游戏结果变得有趣——有时计算机赢,有时你赢,但因为有了随机性,游戏充满了乐趣。

掷骰子:创建一个 Yahtzee 风格的游戏

在我们的纸牌游戏中,我们使用了数组来简化发牌所需的代码,并根据纸牌在卡片列表中的位置来测试哪张卡片更高。在这一部分中,我们将使用数组的概念来生成五个随机的骰子,并检查我们是否掷出了三同号、四同号或五同号,类似于简化版的骰子游戏 Yahtzee。

在 Yahtzee 中,你有五个骰子。每个骰子有六个面,每个面上显示从一到六的点数。在完整的游戏中,玩家掷出所有五个骰子,试图通过掷出三个相同的点数(我们称之为三同号)以及其他各种“组合”,类似于扑克牌游戏。在我们的简化版中,如果五个骰子都掷出相同的点数(比如全部是六点面朝上),那就叫做 Yahtzee,得分是可能的最高分。在我们简化的游戏中,我们只会模拟五个骰子的掷骰,并检查用户是否掷出了三同号、四同号或 Yahtzee,并告知他们结果。

image with no caption

设置游戏

现在我们已经理解了游戏的目标,让我们来讨论如何编写这个游戏。首先,我们需要设置一个游戏循环,让用户可以一直掷骰子,直到他们想退出。其次,我们需要设置一个五个模拟骰子的手牌数组,该数组可以存储五个随机值,从 1 到 6,表示每个骰子的点数。第三,我们将通过为数组中的五个位置赋予从 1 到 6 的随机值来模拟掷骰子。最后,我们需要将五个掷出的骰子彼此比较,看看是否有三个、四个或五个相同的值,并告诉用户结果。

最后这一部分可能是最具挑战性的。我们可以通过检查是否所有五个骰子的值都为 1,或者是否所有五个骰子的值都为 2,以此类推来检查是否有一个 Yahtzee,但这将意味着需要一长串复杂的 if 条件。由于我们并不关心是否是五个 1、五个 2 还是五个 6——我们只关心是否有五个相同的骰子——我们可以通过检查第一个骰子的值是否等于第二个骰子的值,第二个骰子的值是否等于第三个骰子的值,一直到第五个骰子,来简化这一过程。这样,无论五个相同的骰子是什么值,我们都知道所有五个骰子都是一样的,我们就有了 Yahtzee。

五个相同的值似乎很容易测试,但让我们试着找出如何测试四个相同的值。四个相同的手牌可能是像 [1, 1, 1, 1, 2] 这样的数组(这里我们掷出了四个 1 和一个 2)。然而,数组 [2, 1, 1, 1, 1] 也是一个四个相同的手牌,里面有四个 1,数组 [1, 1, 2, 1, 1][1, 2, 1, 1, 1][1, 1, 1, 2, 1] 也是如此。这就有五种可能的配置来测试是否有四个 1!这听起来需要一长串复杂的 if 条件……

幸运的是,作为一名熟练的程序员,你知道通常有更简单的方法来做这件事。前一段中的五个数组共有一个特点:在值的列表中有四个 1;问题在于第五个值,2,可能位于五个不同的数组位置中的任何一个。如果这四个 1 能并排在一起,其他值(2)则单独放置,我们可以更轻松地测试是否有四个相同的值。例如,如果我们能按从低到高或从高到低的顺序对数组进行排序,所有的 1 将被分到一起,从而将五种不同的情况减少为仅两种:[1, 1, 1, 1, 2] 或 [2, 1, 1, 1, 1]。

投掷骰子

在 Python 中,列表、集合和数组有一个内置的排序函数sort(),允许我们按从小到大的顺序或反向排序数组中的元素。例如,如果我们的骰子数组叫做dice,我们可以通过dice.sort()来排序这些值。默认情况下,sort()会按从小到大的顺序,或按升序排列dice中的元素。

在我们测试数组是否包含四个相同的骰子时,排序数组意味着我们只需要测试两种情况:四个低值相同和一个高值(如[1, 1, 1, 1, 2]),或者一个低值和四个高值相同(如[1, 3, 3, 3, 3])。在第一种情况下,我们知道如果骰子已经排序,并且第一和第四个骰子值相同,那么我们就得到了四个相同的骰子或更好的组合。在第二种情况下,同样是排序后的骰子,如果第二个和第五个骰子值相同,那么我们也得到了四个相同的骰子或更好的组合。

我们说四个相同的骰子或更好,因为在五个相同的骰子中,第一和第四个骰子也相同。这就引出了我们的第一个逻辑挑战:如果用户投出了五个相同的骰子,那么他们也投出了四个相同的骰子,我们只希望为他们计算更高的分数。我们将使用if-elif链来处理这个问题,这样如果用户投出了雅兹(Yahtzee),他们就不会同时得到四个相同和三个相同的骰子;只有最高的组合会获胜。将这个if-elif序列与我们关于排序骰子检查四个相同的知识结合起来,代码应该如下所示:

if dice[0] == dice[4]:
    print("Yahtzee!")
elif (dice[0] == dice[3]) or (dice[1] == dice[4]):
    print("Four of a kind!")

首先,如果我们已经对骰子数组进行了排序,我们可以发现一个快捷方式:如果第一个和最后一个骰子值相同(if dice[0] == dice[4]),那么我们就知道我们有一个“雅兹(Yahtzee)!”记住,我们的数组位置是从 0 到 4,分别代表第一到第五个骰子。如果我们没有五个相同的骰子,我们会检查两种四个相同的情况(前四个骰子相同,dice[0] == dice[3],或者后四个骰子相同,dice[1] == dice[4])。我们在这里使用布尔操作符or来判断四个相同的骰子是否存在,如果任意一种情况评估为True(前四个后四个)。

测试骰子

我们通过索引或位置来引用数组中的每个骰子:dice[0]表示骰子数组中的第一个元素,dice[4]表示第五个元素,因为我们从零开始计数。这是我们检查任何一个骰子值的方法,或者将它们彼此比较。就像在我们之前的suits[]数组中一样,在表 6-1 中,每个dice[]数组中的条目都是一个单独的值。当我们调用dice[0]来检查它是否等于dice[3]时,我们是在查看第一个dice元素的值,并将其与第四个dice元素的值进行比较。如果数组已排序,并且这两个值相同,那么我们就得到了四个相同的骰子。

为了测试三张相同的骰子,我们添加另一个elif语句,并将三张相同的骰子测试放在四张相同的骰子测试之后,这样只有在没有五张相同和四张相同的情况下,才会测试三张相同的骰子;我们希望报告最高的组合。如果我们使用排序后的骰子,三张相同的骰子有三种可能的情况:前面三张骰子相同,中间三张骰子相同,或者最后三张骰子相同。在代码中,应该是:

elif (dice[0] == dice[2]) or (dice[1] == dice[3]) or (dice[2] == dice[4]):
    print("Three of a kind")

现在我们可以在骰子游戏中测试各种获胜组合,让我们添加游戏循环和dice数组。

将一切整合在一起

这是完整的FiveDice.py程序。可以在新窗口中输入代码或从www.nostarch.com/teachkids/下载它。

FiveDice.py

  import random
  # Game loop
  keep_going = True
  while keep_going:
      # "Roll" five random dice
➊     dice = [0,0,0,0,0]           # Set up an array for five values dice[0]-dice[4]
➋     for i in range(5):           # "Roll" a random number from 1-6 for all 5 dice
➌         dice[i] = random.randint(1,6)
➍     print("You rolled:", dice)   # Print out the dice values
      # Sort them
➎     dice.sort()
      # Check for five of a kind, four of a kind, three of a kind
      # Yahtzee - all five dice are the same
      if dice[0] == dice[4]:
          print("Yahtzee!")
      # FourOfAKind - first four are the same, or last four are the same
      elif (dice[0] == dice[3]) or (dice[1] == dice[4]):
          print("Four of a kind!")
      # ThreeOfAKind - first three, middle three, or last three are the same
      elif (dice[0] == dice[2]) or (dice[1] == dice[3]) or (dice[2] == dice[4]):
          print("Three of a kind")
      keep_going = (input("Hit [Enter] to keep going, any key to exit: ") == "")

在我们导入random模块并通过while语句开始游戏循环之后,接下来的几行代码值得稍作解释。在 ➊ 处,我们设置了一个名为dice的数组,包含五个值,并将这些值初始化为零。方括号[]与我们在本章前面用于列出颜色、卡片面值和花色名称时所使用的方括号相同。在 ➋ 处,我们设置了一个for循环,循环五次,针对五个骰子,使用从 0 到 4 的范围;这些将是五个骰子的数组位置或索引号。

在 ➌ 处,我们将每个单独的骰子,从dice[0]dice[4],设为一个从 1 到 6 的随机整数,表示我们的五个骰子及其随机掷出的值。在 ➍ 处,我们通过打印dice数组的内容来展示玩家掷出的骰子;此print语句的结果显示在图 6-5 中。

我们骰子程序的示例运行。注意,我们掷出了几个三相同和一个四相同。

图 6-5. 我们骰子程序的示例运行。注意,我们掷出了几个三相同和一个四相同。

在 ➎ 处,我们对dice数组调用.sort()函数。通过将掷出的骰子值从小到大排序并将相同的值分组,这使得测试各种手牌变得简单——比如五个相同、四个相同,等等。所以,例如,如果我们掷出了[3, 6, 3, 5, 3]dice.sort()函数会将其转为[3, 3, 3, 5, 6]if语句检查第一个值是否等于第五个值;在这种情况下,由于第一个和第五个值(36)不相等,我们知道并非所有的骰子都落在相同的值上,所以它不是五个相同。第一个elif通过比较第一个和第四个值(35)以及第二个和第五个值(36)来检查四个相同;同样,这里没有匹配项,所以它不是四个相同。第二个elif检查三个相同;由于第一个和第三个值33相等,我们知道前三个值相等。我们通知玩家他们得到了三个相同,并提示他们根据是否继续玩游戏或退出游戏按下相应的键,如图 6-5 所示。

运行程序并按几次 ENTER 键查看你掷出的骰子结果。

你会注意到,你经常会掷出三张相同的点数,大约每五到六次掷骰子就能出现一次。四个相同点数则更为罕见,大约每 50 次掷骰子才会出现一次。在图 6-5 中,我们只在屏幕满是尝试的情况下掷出了四张相同的点数。雅兹更为罕见:你可能需要掷几百次才会得到雅兹,但由于随机数生成器的存在,你也许在前几次尝试时就能掷出雅兹。尽管它没有真正的游戏复杂,但我们简化版的雅兹依然足够有趣,因为它具有随机性。

我们已经看到,随机性如何通过为掷骰子、扑克牌游戏、剪刀石头布、猜谜游戏等增加运气元素,来让游戏变得有趣和好玩。我们也喜欢通过使用随机数生成器在屏幕上布置五光十色的螺旋,创造出万花筒般的图形。在接下来的章节中,我们将结合你学到的随机数、循环和一些几何知识,将随机螺旋程序转变为一个真正的虚拟万花筒,每次运行都会生成一组不同的反射图像!

没有标题的图片

计算雅兹的概率

如果你对雅兹背后的数学原理感兴趣,以及为什么五个相同点数这么罕见,下面是一个简要的解释。首先,有五颗骰子,每颗骰子有六个面,因此所有可能的组合数量是 6 × 6 × 6 × 6 × 6 = 6⁵ = 7,776。也就是说,掷出五颗正常六面骰子的方式一共有 7,776 种。要计算掷出五个相同点数(五个相同点数的概率),我们需要找出有多少种可能的雅兹:五个 1、五个 2,依此类推,直到五个 6。因此,我们可以掷出六种不同的五个相同点数的雅兹。将 6 种雅兹除以 7,776 种可能的结果,你就得到了掷出五个相同点数的概率:6/7,776,或者 1/1,296。

没错:你在一次掷骰子中掷出五个相同点数的概率只有 1/1,296。所以,如果你掷了很久才得到第一次五个相同的点数,不要灰心。*均来说,你大约每 1,300 次掷骰子能得到一次五个相同的点数。难怪“雅兹”会给 50 分!

万花筒

来自图 6-2 的随机螺旋色彩图形有点像万花筒。为了让它看起来更像真正的万花筒,我们来加入一个我们螺旋程序缺失的重要特性:反射。

在万花筒中,是镜子的定位使随机的颜色和形状变成一个美丽的图案。在这个结尾的示例中,我们将通过修改我们的 RandomSpiral.py 程序,将螺旋图案在屏幕上“反射”四次,从而模仿镜面效果。

没有标题的图片

为了理解如何实现这种镜面效果,我们需要更详细地讨论笛卡尔坐标系。让我们看一下四个点:(4, 2),(–4, 2),(–4, –2),(4, –2),如图 6-6 开始")所示。

比较 (4, 2) 和 (–4, 2),这两点位于顶部。如果竖直的 y 轴是镜子,那么这两点将是彼此的镜像;我们称 (4, 2) 为 (–4, 2) 关于 y 轴的反射。类似的情况发生在 (4, 2) 和 (4, –2) 之间,这两点位于右侧,但以水*的 x 轴作为假想的镜子:(4, –2) 是 (4, 2) 关于 x 轴的反射。

四个关于 x 和 y 轴对称的点,从 (4, 2) 开始

图 6-6. 四个关于 x 和 y 轴对称的点,从 (4, 2) 开始

如果你查看图 6-6 开始")中的每一对 (x, y) 坐标,你会注意到一些事情:所有四个 (x, y) 坐标都使用相同的数字,4 和 2,只是符号不同,+ 或 –,这取决于它们的位置。我们可以通过改变两个坐标的符号来创建任何四个关于 x 轴和 y 轴对称的点,方式如下:(x, y),(–x, y),(–x, –y),(x, –y)。如果你愿意,可以尝试在一张坐标纸上绘制这些点,选择任意一对 (x, y) 坐标。例如,尝试 (2, 3):(2, 3),(–2, 3),(–2, –3),(2, –3) 是四个关于 x 轴上下、y 轴两侧对称的点。

有了这些知识,我们可以按照以下方式构建万花筒程序的框架:

  1. 在屏幕右上角选择一个随机位置 (x, y) 并在该位置绘制一个螺旋图案。

  2. 在屏幕左上角的 (–x, y) 位置绘制相同的螺旋图案。

  3. 在屏幕左下角的 (–x, –y) 位置绘制相同的螺旋图案。

  4. 在屏幕右下角的 (x, –y) 位置绘制相同的螺旋图案。

如果我们一遍又一遍地重复这些步骤,就能获得一个美丽的万花筒效果,配合我们的随机螺旋图案。

让我们通过完整的 Kaleidoscope.py 代码一步步演示,并看到这一效果的实现。

Kaleidoscope.py

   import random
   import turtle
   t = turtle.Pen()
➊ t.speed(0)
   turtle.bgcolor("black")
   colors = ["red", "yellow", "blue", "green", "orange", "purple", "white", "gray"]
   for n in range(50):
       # Generate spirals of random sizes/colors at random locations on the screen
       t.pencolor(random.choice(colors)) # Pick a random color from colors[]
       size = random.randint(10,40) # Pick a random spiral size from 10 to 40
       # Generate a random (x,y) location on the screen
➋     x = random.randrange(0,turtle.window_width()//2)
➌     y = random.randrange(0,turtle.window_height()//2)
      # First spiral
      t.penup()
➍    t.setpos(x,y)
      t.pendown()
      for m in range(size):
          t.forward(m*2)
          t.left(91)
      # Second spiral
      t.penup()
➎    t.setpos(-x,y)
      t.pendown()
      for m in range(size):
          t.forward(m*2)
          t.left(91)
      # Third spiral
      t.penup()
➏    t.setpos(-x,-y)
      t.pendown()
      for m in range(size):
          t.forward(m*2)
          t.left(91)
      # Fourth spiral
      t.penup()
➐    t.setpos(x,-y)
      t.pendown()
      for m in range(size):
          t.forward(m*2)
          t.left(91)

我们的程序如常导入了 turtlerandom模块,但在➊处我们做了一些新的事情:我们通过t.speed(0)将海龟的速度设置为最快值。海龟图形中的speed()函数的参数范围从 0 到 10,其中1是慢动画设置,10是快动画设置,0表示没有动画(以最快速度绘制)。这是一个从 1 到 10 再到 0 的奇怪刻度,但只要记住,如果你想要最快的海龟,只需将速度设置为0。你会注意到,当你运行程序时,螺旋几乎是瞬间出现的。如果你想让海龟更快地移动,你可以对我们之前的绘图程序做相同的修改。

我们的for循环看起来和我们在RandomSpirals.py程序中使用的完全一样,直到到达➋和➌。在➋处,我们将随机数的横坐标范围缩小了一半,仅限于正的 x 坐标值(屏幕的右侧,从x = 0x = turtle.window_width()//2),在➌处,我们将纵坐标范围限制为屏幕的上半部分,从y = 0y = turtle.window_height()//2。记住,我们使用//运算符进行整数除法,以保持像素度量为整数。

这两行代码每次都会给我们一个随机的(x, y)坐标对,位于屏幕的右上角。在➋,我们将海龟画笔的位置设置到该点,然后立即通过for循环绘制第一个螺旋。接着,我们像在图 6-6")中做的那样,改变每个坐标值的符号,来创建该点的三个反射:左上角(–x, y)在➎,左下角(–x, –y)在➏,右下角(x, –y)在➐。请参见图 6-7,了解Kaleidoscope.py能够生成的图案示例。

你可以通过查看屏幕其他三个角落来找到每个螺旋的三个反射。这些不是严格的镜像:我们并不是从相同的角度开始每个螺旋,且在反射的螺旋中我们不会向右转,而在原始螺旋中我们也不会向左转。然而,如果你愿意,可以对程序进行调整。有关如何让这个万花筒程序更酷的想法,请参考本章的编程挑战部分。

Kaleidoscope.py 中的镜像/重复效果。

图 6-7. Kaleidoscope.py中的镜像/重复效果。

你学到了什么

在这一章之前,我们没有办法让计算机表现出随机行为。现在,我们可以让计算机掷骰子;从牌堆中抽取随机卡片;绘制随机颜色、形状、大小和位置的螺旋;甚至偶尔在石头剪子布游戏中赢我们一局。

使这些程序成为可能的工具是random模块。我们使用random.randint(1, 10)在猜数字游戏中生成 1 到 10 之间的随机数。在我们的随机螺旋程序中,我们加入了random.choice()函数,从列表中随机选择颜色。你还学习了如何使用turtle.window_width()turtle.window_height()来获取海龟屏幕的宽度和高度。

你还学习了如何使用笛卡尔坐标来找出屏幕上的(x, y)位置,并使用random.randrange()函数在我们定义的左、右 x 坐标值和上下 y 坐标值之间生成一个随机数。然后,我们使用turtle.setpos(x, y)将海龟移动到绘图屏幕的任意位置。

我们将使用random.choice()从列表中随机选择项目的能力与使用if-elif语句来测试和比较变量的能力结合,构建了一个“用户对电脑”的猜拳游戏版本。

你学习了数组的概念,并通过构建一个花色名称数组和一个面值数组,使得我们的纸牌游戏更容易编码。我们在每个数组上使用了random.choice()来模拟发牌。我们将面值从小到大排序,并使用.index()函数来查找数组中元素的位置。我们使用每张牌的面值索引,比较哪个牌的索引值更大,从而判断哪个玩家赢得了“战争”纸牌游戏的一局。我们构建了一个可重用的游戏循环,包含用户输入、一个标志变量keep_going和一个while语句;我们可以将这个循环应用到任何用户想玩的游戏或应用中,或者在一系列游戏中多次运行。

我们通过构建简化版的“雅兹牌游戏”来扩展了对数组的理解。我们创建了一个包含从 1 到 6 的五个值的数组,模拟五颗骰子,使用randint()来模拟掷骰子,并对骰子数组使用sort()函数,使得检查获胜组合变得更容易。我们看到,在一个已排序的数组中,如果第一个和最后一个值相同,说明数组中的所有元素都相同。在我们的游戏中,这意味着我们得到了五个相同的点数。我们使用复合if语句,并通过or运算符将两种四个相同的情况和三种三个相同的情况结合。我们使用if-elif语句来控制程序逻辑,确保五个相同的点数不会被误判为四个相同的点数,依此类推。

我们在万花筒程序中更多地使用了笛卡尔坐标,并通过改变(x, y)坐标值的符号来模拟反射效果。我们在屏幕上重复每个随机大小、颜色和位置的螺旋形状四次,创造了万花筒效果。你学习了如何使用t.speed(0)来增加海龟的绘图速度。

随机数和随机选择为游戏增加了偶然性,使游戏更加有趣。你玩过的几乎每个游戏都有一定的偶然性。现在,你可以在程序中构建随机性,编写人们喜欢玩的游戏。

到此为止,你应该能够完成以下操作:

  • random 模块导入到你的程序中。

  • 使用 random.randint() 在给定范围内生成一个随机整数。

  • 使用 random.choice() 从列表或数组中随机选择一个值。

  • 使用 random.choice() 从两个仅包含面值和花色的字符串数组中生成 52 张牌的值。

  • 使用 turtle.window_width()turtle.window_height() 确定绘图窗口的大小。

  • 使用 turtle.setpos(x,y) 将乌龟移动到绘图屏幕的任何位置。

  • 使用 random.randrange() 函数生成指定范围内的随机数。

  • 使用 .index() 函数查找列表或数组中元素的索引。

  • 使用布尔标志变量如 keep_going 构建一个 while 游戏循环。

  • 构建一个包含相似类型值的数组,通过索引为数组中的元素赋值(例如 dice[0] = 2),并像使用常规变量一样使用数组元素。

  • 使用 .sort() 函数对列表或数组进行排序。

  • 通过改变点的 (x, y) 坐标值的符号来反射点关于 x 轴和 y 轴的对称。

  • 使用 .speed() 函数来改变乌龟的绘图速度。

编程挑战

对于本章的挑战问题,我们将扩展 Kaleidoscope.pyHighCard.py 程序。(如果你卡住了,可以访问 www.nostarch.com/teachkids/ 获取示例答案。)

#1: 随机边数和线条粗细

通过添加两个随机变量来为 Kaleidoscope.py 增加更多随机性。添加一个名为 sides 的变量来表示边数,然后使用该变量每次在螺旋循环中改变转角(因此,螺旋的边数),通过 360/sides + 1 作为角度,而不是 91。接下来,创建一个名为 thick 的变量,用来存储一个 1 到 6 之间的随机数,表示乌龟笔的粗细。将 t.width(thick) 这一行添加到正确的位置,以改变每个螺旋线条的粗细。

#2: 逼真的镜像螺旋

如果你了解一些几何学,另外两个调整可以使这个万花筒更加逼真。首先,在绘制第一个螺旋之前,记录乌龟的方向(介于 0 到 360 度之间),通过调用 t.heading() 获取结果,并将其存储在一个名为 angle 的变量中。然后,在绘制每个镜像螺旋之前,通过使用 t.setheading() 将乌龟指向正确的镜像方向。提示:第二个角度将是 180 - angle,第三个螺旋的角度将是 angle - 180,第四个将是 360 - angle

然后,尝试在第一和第三个螺旋图形每绘制一条线后向左转,而在第二和第四个螺旋图形每次绘制一条线后向右转。如果你实现了这些改进,你的螺旋图形应该在大小、形状、颜色、粗细和方向上真正成为彼此的镜像。如果你愿意,你还可以通过将 x 和 y 坐标值的范围改为random.randrange(size,turtle.window_width()//2)random.randrange(size,turtle.window_height()//2)来减少形状之间的重叠。

#3: 战争

通过进行三项修改,将HighCard.py转变为完整的战争游戏。首先,记录分数:创建两个变量来追踪计算机赢得了多少局,用户赢得了多少局。其次,通过发牌 26 局(可以使用for循环代替while循环,或者通过记录已经玩过的局数)来模拟一副完整的牌,并根据哪一方的得分更高来宣布胜者。第三,处理*局情况:记住连续发生过多少次*局;然后,下次其中一方获胜时,将最*的*局次数加到该玩家的得分中,并将*局次数重置为零,进入下一轮。

第七章:函数——有个名字

到目前为止,我们已经使用了许多函数——从print()input(),再到turtle.forward()。但这些函数都是内置的或者从 Python 模块和库中导入的。在本章中,我们将编写自己的函数,做任何我们想做的事情,包括响应用户操作,比如鼠标点击和按键。

函数很有用,因为它们赋予我们组织可重用代码片段的能力,然后通过一个简短的名称或命令在程序中引用这些片段。以input()为例:它打印一个文本提示,询问用户输入,收集用户输入的内容,并将其作为字符串传递给我们的程序,我们可以将其存储在一个变量中。每次我们想从用户那里获取更多信息时,都可以重复使用input()函数。如果没有这个函数,每次我们想询问用户信息时,可能都需要自己做这些工作。

turtle.forward()函数是另一个很好的视觉示例:每当我们将乌龟向前移动以绘制螺旋的某一边时,Python 会在屏幕上按我们要求的精确长度,逐个像素地绘制,并朝着我们当前设置的方向前进。如果没有turtle.forward()函数,我们就需要搞清楚如何在屏幕上着色像素,跟踪位置和角度,并进行一些相当复杂的数学运算来绘制每次所需的距离。

没有说明的图片

如果没有这些函数,我们的程序将变得更长、更难以阅读,也更难编写。函数让我们能够利用许多其他程序员之前的编程工作。好消息是,我们也可以编写自己的函数,使我们的代码更简短、更易读且更具可重用性。

在第六章中,我们编写了绘制随机螺旋和万花筒图案的程序。我们可以使用函数来使这些程序中的代码更易读,并使代码的某些部分更具可重用性。

使用函数将内容组合起来

回顾一下 RandomSpirals.py。第一个for循环中的所有代码都是用来创建一个随机螺旋的。这个for循环利用这些代码绘制了 50 个随机颜色、大小和位置的螺旋。

假设我们想在另一个程序中使用那个随机螺旋代码,比如一个游戏或者屏幕保护程序。在RandomSpirals.py中,我们很难看出实际的螺旋绘制是从哪里开始或结束的,而我们几页前才写了那段代码。想象一下三个月后再回到这个程序!我们会很难弄清楚这个应用程序的功能是什么,以及如果我们想再次绘制随机螺旋,哪些代码行需要复制到新程序中。

为了使一段代码以后可以重用,或者让它现在更容易阅读,我们可以定义一个函数并给它一个易于理解的名字,就像input()turtle.forward()一样。定义一个函数也叫做声明函数,它意味着我们告诉计算机我们希望函数做什么。让我们创建一个在屏幕上绘制随机螺旋的函数;我们称它为random_spiral()。我们可以在任何程序中随时重用这个函数来绘制随机螺旋。

定义 Random_Spiral()

打开RandomSpirals.py(第六章),将其另存为一个名为RandomSpiralsFunction.py的新文件,并在设置海龟的笔、速度和颜色之后但在for循环之前开始定义这个函数。(你可以参考最终的程序 RandomSpiralsFunction.py,看看它应该是什么样子的。)我们将random_spiral()的定义放在海龟设置之后,因为这个函数需要使用海龟笔t和颜色列表。定义应该放在for循环之前,因为我们将在for循环中使用random_spiral(),你必须先定义函数才能使用它。现在我们已经找到了程序中的正确位置,让我们开始定义random_spiral()函数吧。

我们使用关键字def(即定义的缩写)来定义一个函数,后面跟着函数的名称、括号()和冒号(:)。这是我们将要构建的random_spiral()函数的第一行:

def random_spiral():

其余的函数定义将是一个或多个语句,从左侧缩进,就像我们在for循环中分组语句一样。为了绘制一个随机螺旋,我们需要设置一个随机颜色、随机大小和一个随机的(x, y)位置,然后将笔移动到那里并绘制螺旋。下面是完成我们random_spiral()函数的代码:

def random_spiral():
    t.pencolor(random.choice(colors))
    size = random.randint(10,40)
    x = random.randrange(-turtle.window_width()//2,
                         turtle.window_width()//2)
    y = random.randrange(-turtle.window_height()//2,
                         turtle.window_height()//2)
    t.penup()
    t.setpos(x,y)
    t.pendown()
    for m in range(size):
        t.forward(m*2)
        t.left(91)

注意,当函数被定义时,计算机并不会运行这段代码。如果我们在 IDLE 中输入函数定义,我们不会得到螺旋图形——还没有。要实际绘制螺旋,我们需要调用random_spiral()函数。

调用 Random_Spiral()

函数定义告诉计算机当有人实际调用函数时我们希望做什么。在定义了一个函数后,我们可以在程序中使用函数名后面跟上括号来调用它:

random_spiral()

你必须记住括号,因为它告诉计算机你想要运行这个函数。现在我们已经将random_spiral()定义为一个函数,当我们在程序中像这样调用random_spiral()时,计算机将在海龟屏幕上绘制一个随机的螺旋。

现在,为了绘制 50 个随机螺旋,我们可以简化for循环,而不是使用RandomSpirals.py中的所有代码:

for n in range(50):
    random_spiral()

由于我们使用了自己构建的函数,这个循环更容易阅读。我们让代码更易于理解,并且通过复制和粘贴函数定义,可以轻松地将随机螺旋代码移到另一个程序中。

这是完整的程序;将其输入到 IDLE 中并保存为 RandomSpiralsFunction.py,或者从 www.nostarch.com/teachkids/ 下载。

RandomSpiralsFunction.py

import random
import turtle
t = turtle.Pen()
t.speed(0)
turtle.bgcolor("black")
colors = ["red", "yellow", "blue", "green", "orange", "purple",
          "white", "gray"]
def random_spiral():
    t.pencolor(random.choice(colors))
    size = random.randint(10,40)
    x = random.randrange(-turtle.window_width()//2,
                         turtle.window_width()//2)
    y = random.randrange(-turtle.window_height()//2,
                         turtle.window_height()//2)
    t.penup()
    t.setpos(x,y)
    t.pendown()
    for m in range(size):
        t.forward(m*2)
        t.left(91)

for n in range(50):
    random_spiral()

除了更易读的程序,我们还得到了一个可重用的 random_spiral() 函数,我们可以将其复制、修改并轻松地在其他程序中使用。

如果你发现自己一次又一次地重复使用一段代码,将其转换成一个函数,就像我们用 def 定义的 random_spiral() 函数那样,你会发现将代码移植(即携带并在新应用中重用)到其他应用程序会更容易。

注意

你甚至可以创建自己的模块,里面充满了函数,并像我们在程序中导入 turtlerandom 一样导入你的模块(有关如何在 Python 中创建模块,请参见附录 C)。这样,你可以与朋友分享你的代码。

没有标题的图片

参数:为你的函数提供信息

在创建函数时,我们可以为该函数定义 参数。参数允许我们通过将值作为 参数 传递给函数的括号,将信息传递给函数。从我们第一次使用 print() 语句开始,我们就一直在给函数传递参数。当我们编写 print("Hello") 时,"Hello" 是一个代表我们希望在屏幕上打印的字符串值的参数。当我们调用海龟函数 t.left(90) 时,我们传递了值 90,表示我们希望海龟向左转 90 度。

random_spiral() 函数不需要参数。它所需的所有信息都在函数内部的代码中。但如果我们愿意,可以让我们构建的函数接受参数形式的信息。让我们定义一个函数 draw_smiley(),它将在屏幕上的随机位置画一个笑脸。这个函数将接受一对随机坐标,并在这些坐标处绘制笑脸。我们将在一个名为 RandomSmileys.py 的程序中定义并调用 draw_smiley()。完整程序见 Putting it all together —— 但让我们一步步构建它。

随机位置的笑脸

我们想编写一个程序,它不仅仅是画随机的螺旋,而是画笑脸。画笑脸比随机选一个颜色和大小然后画螺旋要多一些规划。让我们回到第六章,一张图表纸。因为我们以前的程序没有画过像笑脸这样复杂的东西,最好先在纸上画出来,然后再逐步转化为代码。图 7-1 展示了一个笑脸的图表纸网格,我们可以用它来规划我们的绘图。

我们的程序将像这样在屏幕上的随机(x, y)坐标处画笑脸。draw_smiley()的函数定义将接受两个参数,xy,表示笑脸要画在的位置。如图 7-1 所示,我们将把笑脸画得仿佛它坐落在(x, y)的位置,所以可以想象将这个笑脸模板移动到屏幕上的任何其他点(x, y),将它的原点(0, 0)放置在那个点上。让我们从给定的点开始,逐步弄清楚如何画出每个笑脸。

我们首先通过在图表纸上画一个笑脸来规划我们的程序。

图 7-1. 我们首先通过在图表纸上画一个笑脸来规划我们的程序。

画一个头部

每个笑脸的头部是一个黄色的圆圈,眼睛是两个小蓝色圆圈,嘴巴是一些黑色的线条。给定屏幕上的一个点,我们的draw_smiley()函数需要在相对于给定点的正确位置画出头部、眼睛和嘴巴。为了弄清楚放入函数定义中的代码,让我们先单独规划头部、眼睛和嘴巴,从头部开始。我们先画头部,这样它就不会遮挡接下来要画的眼睛和嘴巴。

我们将图中的每条网格线视为 10 像素,因此我们绘制的笑脸的高度将是 100 像素;在大多数计算机屏幕上,这大约等于一英寸。由于圆形的直径,即高度和宽度,都是 100 像素,这意味着它的半径(直径的一半)是 50 像素。我们需要半径,因为Turtle模块的circle()命令需要半径作为参数。绘制半径为 50(即直径为 100)的圆形的命令是t.circle(50)circle()函数会在海龟当前的(x, y)位置正上方绘制一个圆形。我们需要知道这一点,以便正确地放置眼睛和嘴巴,因此我将我的笑脸画在底边与原点(0, 0)对齐的位置。我们可以通过将每个部分的坐标加到起始(x, y)位置(0, 0)来确定其他部分的位置。

为了画出大黄头,我们将画笔颜色设置为黄色,填充颜色也设置为黄色,启用图形的油漆填充功能,画出圆形(由于启用了油漆填充,圆形会被填充为黄色),完成后关闭油漆填充功能。假设我们在程序中已经定义了一个名为t的海龟画笔,绘制笑脸的黄色圆头的代码看起来是这样的:

# Head
t.pencolor("yellow")
t.fillcolor("yellow")
t.begin_fill()
t.circle(50)
t.end_fill()

为了用黄色填充圆形,我们在t.circle(50)命令周围加上四行代码。首先,我们使用t.pencolor("yellow")设置画笔颜色为黄色。其次,我们用t.fillcolor("yellow")设置填充颜色。接着,在调用t.circle(50)命令绘制笑脸的圆形之前,我们告诉计算机我们希望填充所绘制的圆形。我们通过t.begin_fill()函数来实现。最后,绘制圆形后,我们调用t.end_fill()函数,告诉计算机我们已经完成了填充的形状。

绘制眼睛

首先,我们需要确定海龟的位置,以便在正确的位置绘制左眼,然后将填充颜色设置为蓝色,最后绘制正确大小的圆形。眼睛大约是 20 像素(两条网格线)高,我们知道直径为 20 意味着半径是直径的一半,即 10,因此我们将使用t.circle(10)命令绘制每只眼睛。难点是确定它们应该画在什么位置。

我们的(x, y)起点将是每个笑脸的局部原点,你可以在图 7-1 中找到左眼的位置。它看起来大约从原点上方 6 个网格线开始(即向上 60 个像素,沿 y 轴正方向),并且它位于 y 轴的左侧 1.5 个网格线处(或约 15 个像素,沿 x 轴负方向)。

为了告诉我们的程序如何到达正确的位置以绘制左眼,从大黄色圆圈的底部开始,给定的(x, y)作为一对参数传递给我们的函数,我们需要从x开始,向左移动 15 个像素,向上移动 60 个像素,或者移动到(x-15, y+60)。因此,调用t.setpos(x-15, y+60)应该会将乌龟放到我们需要开始绘制左眼的位置。以下是左眼的代码:

# Left eye
t.setpos(x-15, y+60)
t.fillcolor("blue")
t.begin_fill()
t.circle(10)
t.end_fill()

一个常见的错误是将setpos命令写成仅用(–15, 60)作为参数,但请记住,我们希望在屏幕上的不同(x, y)位置绘制多个笑脸;并不是所有的笑脸都会从(0, 0)开始。命令t.setpos(x-15, y+60)可以确保无论我们在绘制哪个黄色的笑脸,我们的左眼都会出现在那个笑脸的左上方。

绘制右眼的代码几乎与绘制左眼的代码相同。我们可以看到,右眼距离我们的位置(x, y)向右偏移了 15 个像素(1.5 个网格线),并且仍然是向上偏移了 60 个像素。命令t.setpos(x+15, y+60)应该能使右眼对称地放置。以下是右眼的代码:

# Right eye
t.setpos(x+15, y+60)
t.begin_fill()
t.circle(10)
t.end_fill()

填充颜色仍然是左眼的蓝色,所以我们只需将乌龟移动到正确的位置(x+15, y+60),打开填充,绘制眼睛,并完成填充。

没有标题的图片

绘制嘴巴

现在让我们规划笑脸上最重要的部分:微笑。为了简化微笑的绘制,我们将用三条粗黑线来绘制嘴巴。嘴巴的左边看起来大约从距离我们点(x, y)左侧 2.5 个网格线并上方 4 个网格线的地方开始,所以我们会把乌龟放置在(x-25, y+40)位置开始绘制微笑。我们将把笔的颜色设置为黑色,宽度设置为 10,这样微笑就会很粗且容易看见。从微笑的左上角,我们需要移动到(x-10, y+20),然后到(x+10, y+20),最后到微笑的右上角(x+25, y+40)。注意,这些点对是以 y 轴为对称轴的镜像关系;这使得我们的笑脸看起来非常均衡。

以下是嘴巴的代码:

   # Mouth
   t.setpos(x-25, y+40)
   t.pencolor("black")
   t.width(10)
   t.goto(x-10, y+20)
   t.goto(x+10, y+20)
   t.goto(x+25, y+40)
➊ t.width(1)

在我们将海龟定位在嘴巴的左上角后,我们将画笔颜色更改为黑色,并将宽度设置为 10。我们开始绘制,通过告诉海龟前往笑容的其他三个点。turtle模块的goto()函数和setpos()做的事情一样:它将海龟移动到给定的点。我在这里使用它是为了让你看到setpos()的替代方法。最后,在➊处,t.width(1)将画笔宽度恢复为 1,这样在绘制下一个笑脸时,我们的形状就不会太厚。

定义和调用Draw_Smiley()

剩下的就是定义draw_smiley()函数,包含所有绘制笑脸的代码,设置一个循环来生成屏幕上 50 个随机的(x, y)位置,并调用draw_smiley(x,y)函数,在这 50 个位置绘制笑脸。

draw_smiley()函数的定义需要接受两个参数,xy,用于指定笑脸绘制的位置,并且需要抬起海龟的画笔,将海龟移动到(x, y)位置,然后再将画笔放下准备绘制。之后,我们只需添加绘制大黄色脸、左右眼睛和嘴巴的代码片段。

def draw_smiley(x,y):
    t.penup()
    t.setpos(x,y)
    t.pendown()
    # All of your drawing code goes here...

最终的部分将是我们的for循环,用于生成 50 个随机位置的笑脸,并调用draw_smiley()函数绘制每个笑脸。它看起来是这样的:

for n in range(50):
    x = random.randrange(-turtle.window_width()//2,
                         turtle.window_width()//2)
    y = random.randrange(-turtle.window_height()//2,
                         turtle.window_height()//2)
    draw_smiley(x,y)

我们的随机 x 和 y 坐标值就像在第六章中看到的一样,从屏幕的左半部分到右半部分,从下半部分到上半部分生成随机点。通过draw_smiley(x,y),我们将这些随机坐标作为参数传递给draw_smiley()函数,它将在该随机位置绘制一个笑脸。

将所有内容组合起来

将程序组合起来,它大致看起来是这样的:

RandomSmileys.py

   import random
   import turtle
   t = turtle.Pen()
   t.speed(0)
   t.hideturtle()
   turtle.bgcolor("black")
➊ def draw_smiley(x,y):
       t.penup()
       t.setpos(x,y)
       t.pendown()
       # Head
       t.pencolor("yellow")
       t.fillcolor("yellow")
       t.begin_fill()
       t.circle(50)
       t.end_fill()
       # Left eye
       t.setpos(x-15, y+60)
       t.fillcolor("blue")
       t.begin_fill()
       t.circle(10)
       t.end_fill()
       # Right eye
       t.setpos(x+15, y+60)
       t.begin_fill()
       t.circle(10)
       t.end_fill()
       # Mouth
       t.setpos(x-25, y+40)
       t.pencolor("black")
       t.width(10)
       t.goto(x-10, y+20)
       t.goto(x+10, y+20)
       t.goto(x+25, y+40)
       t.width(1)
➋ for n in range(50):
       x = random.randrange(-turtle.window_width()//2,
                            turtle.window_width()//2)
       y = random.randrange(-turtle.window_height()//2,
                            turtle.window_height()//2)
       draw_smiley(x,y)

和往常一样,我们导入所需的模块并设置海龟,将它的速度设置为0(最快)。我们使用hideturtle()使海龟本身不出现在屏幕上,这样也可以加快绘制速度。

在➊部分,我们定义了draw_smiley()函数,使其任务是绘制笑脸的脸、左眼、右眼和微笑,使用我们之前写的所有代码。它只需要做这项工作的是一个 x 坐标和一个 y 坐标。

在我们for循环的➋部分,随机选择一个xy值,并传递给draw_smiley(),然后它会在该随机点相对位置绘制一个带有所有特征的笑脸。

RandomSmileys.py程序将在绘图屏幕上随机位置绘制 50 个笑脸,正如图 7-2 所示。

你可以自定义程序来绘制几乎任何形状,只要你设计一个函数,从任何(x, y)位置开始绘制该形状。像我们在这个示例中那样从图纸开始,可以更容易地找到重要的点。如果你觉得一些笑脸半挂在屏幕的左右两侧,或者几乎挂到屏幕顶部,你可以在 xy randrange() 语句中使用一些数学运算,确保笑脸完全留在屏幕上。可以访问 www.nostarch.com/teachkids/ 获取这个挑战的示例答案。

The RandomSmileys.py program produces a happy result.

图 7-2. RandomSmileys.py 程序产生了一个愉快的结果。

返回:你所给的才是最重要的

我们可以通过参数将信息传递给函数,但如果我们想从函数中接收信息呢?例如,假设我们构建了一个将英寸转换为厘米的函数,并且我们希望将转换后的数字存储起来用于进一步的计算,而不是直接将其打印到屏幕上?为了将信息从函数传递回我们的程序,我们使用 return 语句。

从函数返回值

有很多时候我们需要从函数中获取信息。例如,假设我们实际构建了一个将英寸转换为厘米的函数,并将其命名为 convert_in2cm()。我们可以设想这个函数可能需要接受的参数:一个英寸的测量值。但是这个函数完全可以返回信息给程序的其他部分——即转换后的厘米值。

为了将英寸长度转换为等效的厘米长度,我们将英寸数乘以 2.54——这是每英寸大约对应的厘米数。为了将该计算的结果传递回程序,我们将使用 return 语句。return 关键字后面的值将作为函数的 返回值 或结果返回给程序。让我们定义我们的函数:

def convert_in2cm(inches):
    return inches * 2.54

如果你将这两行代码输入到 Python shell 中,然后输入convert_in2cm(72)并按下 ENTER,Python 会返回 182.88。72 英寸(或 6 英尺——我的身高)大约等于 182.88 厘米。函数返回的值是 182.88,在命令行中,当我们调用函数时,返回值会在下一行打印出来。

我们还可以进行另一个有用的转换:英镑转公斤。要将英镑转换为公斤,我们将英镑的重量除以 2.2,这是 1 公斤大约等于多少英镑的*似值。我们来创建一个叫做 convert_lb2kg() 的函数,它将以英镑为参数,并返回转换后的公斤值:

image with no caption

def convert_lb2kg(pounds):
    return pounds / 2.2

return语句有点像是反向使用参数,只不过我们只能返回一个值,而不是像我们传入的参数那样返回一组值。(不过,这个值可以是一个列表,所以通过一些操作,你可以在一个返回变量中传回多个值。)

在程序中使用返回值

使用这两个转换函数,我们来构建一个有趣的应用程序:乒乓球的身高和体重计算器。这个程序将回答“我有多少个乒乓球高?”和“我的体重大约相当于多少个乒乓球?”

一个官方的乒乓球重 2.7 克(0.095 盎司),直径为 40 毫米(4 厘米,或 1.57 英寸)。为了计算与我们的身高和体重相等的乒乓球数量,我们需要将身高(单位:厘米)除以 4,并将体重(单位:克)除以 2.7。但是并不是每个人都知道自己的体重(以克为单位)或身高(以厘米为单位):在美国,我们通常以磅为单位测量体重,以英尺和英寸为单位测量身高。幸运的是,我们刚刚开发的两个转换函数将帮助我们将这些测量值转换为公制单位。然后我们可以使用这些数字进行乒乓球单位的转换。

我们的程序将定义两个转换函数convert_in2cm()convert_lb2kg()。然后,它将请求用户输入身高和体重,计算出用户的身高和体重对应的乒乓球数量,并将计算结果显示在屏幕上。请键入并运行以下代码:

PingPongCalculator.py

➊ def convert_in2cm(inches):
       return inches * 2.54

   def convert_lb2kg(pounds):
       return pounds / 2.2

➋ height_in = int(input("Enter your height in inches: "))
   weight_lb = int(input("Enter your weight in pounds: "))

➌ height_cm = convert_in2cm(height_in)
➍ weight_kg = convert_lb2kg(weight_lb)

➎ ping_pong_tall = round(height_cm / 4)
➏ ping_pong_heavy = round(weight_kg * 1000 / 2.7)

➐ feet = height_in // 12
➑ inch = height_in % 12

➒ print("At", feet, "feet", inch, "inches tall, and", weight_lb,
         "pounds,")
   print("you measure", ping_pong_tall, "Ping-Pong balls tall, and ")
   print("you weigh the same as", ping_pong_heavy, "Ping-Pong balls!")

在➊处,我们输入我们开发的两个转换公式。两个函数都接受一个输入参数(inchespounds),并且每个函数都返回一个值。在➋处,我们请求用户输入身高和体重,并将这些值存储在height_inweight_lb中。在➌处,我们调用convert_in2cm()函数,将height_in作为我们要转换的值,并将转换结果存储在变量height_cm中。在➍处,我们使用convert_lb2kg()函数进行另一项转换计算,将人的体重(以为单位)转换为等效的千克(kg)。

➎处的公式做了两件事:首先,它将用户的身高(单位为厘米)除以 4,得出以乒乓球为单位的身高;然后,它使用round()函数将这个结果四舍五入到最接*的整数,并将结果存储在变量ping_pong_tall中。到达➏时,我们做了类似的操作,将用户的体重(单位为千克)转换为克,通过乘以 1000,再除以 2.7——这就是一个标准乒乓球的质量(单位:克)。这个数字被四舍五入到最接*的整数,并存储在变量ping_pong_heavy中。

在➐和➑处,我们通过计算一个人的身高(以英尺和英寸为单位)进行稍微多一点的数学运算。正如我之前提到的,这通常是我们在美国表达身高的方式,它不仅是一个不错的收尾,而且也是一个让人检查自己是否输入了正确信息的方式。//运算符执行整数除法,因此 66 英寸,或者 5.5 英尺,将导致只将5存储在变量feet中,而%运算符(取余)则存储余数,6英寸。➒处的print语句会打印出用户的身高和体重,分别以标准单位和乒乓球的数量表示。

这里是乒乓球计算器程序的一些示例运行结果,包括我儿子 Max、Alex 和我自己的乒乓球数量(唯一的缺点是,现在我的孩子们想要 31,000 个乒乓球)。

>>> ================================ RESTART ================================
>>>
Enter your height in inches: 42
Enter your weight in pounds: 45
At 3 feet 6 inches tall, and 45 pounds,
you measure 27 Ping-Pong balls tall, and
you weigh the same as 7576 Ping-Pong balls!
>>> ================================ RESTART ================================
>>>
Enter your height in inches: 47
Enter your weight in pounds: 55
At 3 feet 11 inches tall, and 55 pounds,
you measure 30 Ping-Pong balls tall, and
you weigh the same as 9259 Ping-Pong balls!
>>> ================================ RESTART ================================
>>>
Enter your height in inches: 72
Enter your weight in pounds: 185
At 6 feet 0 inches tall, and 185 pounds,
you measure 46 Ping-Pong balls tall, and
you weigh the same as 31145 Ping-Pong balls!
>>>

我们创建的任何函数都可以返回一个值,就像我们定义的任何函数都可以接受参数作为输入一样。根据你希望函数执行的操作,使用这些功能之一或两者来编写你所需的函数代码。

一点交互

我们已经编写了一些好看的图形化应用程序,但距离构建下一个视频游戏或移动应用程序还有一两步的距离。我们需要学习的一个剩余技能是用户交互编程:让我们的程序对鼠标点击、按键等作出响应。

没有标题的图片

大多数应用程序是交互式的——它们允许用户触摸、点击、拖动、按下按钮,并感觉自己在控制程序。我们称这些应用程序为事件驱动应用程序,因为它们等待用户执行某个动作,或者事件。响应用户事件的代码,比如当用户点击图标时打开一个窗口,或者当他们点击按钮时启动游戏,称为事件处理程序,因为它处理或响应来自用户的事件。它也被称为事件监听器,因为就像计算机在耐心等待,倾听用户告诉它该做什么一样。我们将学习如何处理用户事件,使我们的程序更加引人入胜和互动。

事件处理:TurtleDraw

让应用程序处理用户事件有很多种方式。Python 的turtle模块包括一些处理用户事件的函数,包括鼠标点击和按键。我们将尝试的第一个函数是turtle.onscreenclick()。顾名思义,这个函数允许我们处理用户点击海龟屏幕时产生的事件。

这个函数与我们之前使用和构建的函数有所不同:我们传递给turtle.onscreenclick()的参数不是一个值——它是另一个函数的名字:

turtle.onscreenclick(t.setpos)

记得我们用过的setpos()函数吗?它可以将鼠标移动到屏幕上的某个(x, y)位置。现在,我们告诉计算机,当海龟屏幕接收到鼠标点击时,应该将海龟的位置设置为点击的屏幕位置。我们传递给另一个函数的函数有时被称为回调函数(因为它会被另一个函数“回调”)。注意,当我们将一个函数的名称作为参数传递给另一个函数时,内部函数不需要在名称后加括号。

通过将函数名t.setpos传递给turtle.onscreenclick(),我们告诉计算机我们希望屏幕点击事件执行什么操作:我们希望将海龟的位置设置为用户点击的位置。让我们在一个简单的程序中尝试一下:

TurtleDraw.py

import turtle
t = turtle.Pen()
t.speed(0)
turtle.onscreenclick(t.setpos)

输入这四行代码到 IDLE 中,运行程序,然后点击屏幕上不同的位置。你刚刚用四行代码创建了一个绘图程序!图 7-3 展示了我绘制的一个示例草图。

一个 TurtleDraw.py 草图(我之所以是作者而不是艺术家是有原因的)

图 7-3. 一个TurtleDraw.py草图(我之所以是作者而不是艺术家是有原因的)

之所以能这样工作,是因为我们告诉计算机,当用户点击屏幕时,海龟的当前位置应该被设置为鼠标点击的位置。海龟的画笔默认是落下的,所以当用户点击绘图窗口时,海龟会移动到点击位置,并从旧位置绘制一条线到用户点击的位置。

你可以通过修改屏幕的背景颜色、海龟的画笔颜色、画笔的宽度等来定制TurtleDraw.py。看看我四岁儿子制作的版本(在爸爸的帮助下):

TurtleDrawMax.py

import turtle
t = turtle.Pen()
t.speed(0)
turtle.onscreenclick(t.setpos)
turtle.bgcolor("blue")
t.pencolor("green")
t.width(99)

Max 喜欢这个绘图程序(非常喜欢),但他希望屏幕是蓝色的,画笔是绿色的,而且要非常粗,所以我们将bgcolor()pencolor()width()分别设置为bluegreen99。我们任意选择在告诉计算机处理鼠标点击事件后设置这些。

这没问题,因为程序会持续运行,即使它在监听鼠标点击,因此在用户第一次点击时,屏幕和画笔已经正确地着色和调整大小,如图 7-4 所示。

我通过多次点击使用 TurtleDrawMax.py 绘制的图像

图 7-4. 我通过多次点击使用TurtleDrawMax.py绘制的图像

使用setpos()函数作为 turtle.onscreenclick()的回调,我们已经构建了一个有趣的画图程序,当用户点击鼠标时,程序会通过画线与用户互动,画出用户点击的位置。尝试使用不同的颜色、线宽或任何你能想到的方式来自定义这个应用,使它更具个性。

监听键盘事件:ArrowDraw

在我们的乌龟绘图程序中,我们看到监听鼠标点击如何使用户感觉自己对程序有更多控制权。在本节中,我们将学习如何使用键盘交互给用户更多的选择。我们还将定义自己的函数,作为事件处理程序使用。

TurtleDraw.py程序中,我们将t.setpos作为回调函数,告诉计算机在发生onscreenclick()事件时该做什么;我们希望将乌龟的位置设置为鼠标点击屏幕的位置。setpos()函数已经在turtle模块中提供了,但如果我们想创建自己的函数来处理事件呢?假设我们想构建一个程序,允许用户通过按下箭头键来移动屏幕上的乌龟,而不是点击鼠标按钮。我们该怎么做呢?

首先,我们需要为每个箭头键的按下事件构建相应的函数来移动乌龟,然后告诉计算机监听这些键的按下事件。我们来写一个程序,监听上(↑)、左(←)和右(→)箭头键,让用户通过这些键来控制乌龟向前移动或左右转动。

我们定义几个函数——up()left()right()——来移动和转动乌龟:

def up():
    t.forward(50)
def left():
    t.left(90)
def right():
    t.right(90)

我们的第一个函数up()将乌龟向前移动 50 个像素。第二个函数left()将乌龟向左转 90 度。最后,right()将乌龟向右转 90 度。

为了在用户按下正确的箭头键时运行这些函数,我们需要告诉计算机哪个函数与哪个键对应,并让计算机开始监听按键事件。为了设置键盘事件的回调函数,我们使用 turtle.onkeypress()。这个函数通常需要两个参数:回调函数的名称(我们创建的事件处理函数)和要监听的特定键。为了将每个函数与相应的箭头键关联起来,我们可以这样写:

turtle.onkeypress(up,  "Up")
turtle.onkeypress(left,   "Left")
turtle.onkeypress(right,  "Right")

第一行将up()函数设置为“上”箭头键按下事件的处理函数;函数(up)在前,"Up"是上箭头键的名称,↑。左箭头和右箭头的键按事件也是如此。最后一步是告诉计算机开始监听按键事件,这一步我们通过这个命令来实现:

turtle.listen()

我们需要这一行代码有几个原因。首先,与鼠标点击不同,单纯按下一个键并不能确保我们的乌龟窗口接收到按键。当你点击桌面上的一个窗口时,该窗口会移到前面并获得焦点,意味着该窗口将接收来自用户的输入。点击乌龟窗口时,它会自动将该窗口设为焦点,接收后续的鼠标事件。但对于键盘,单纯按键并不会让某个窗口接收这些按键输入;turtle.listen() 命令确保了我们的乌龟窗口成为桌面的焦点,从而能够接收按键事件。其次,listen() 命令告诉计算机开始处理所有与 onkeypress() 函数关联的按键事件。

这是完整的 ArrowDraw.py 程序:

ArrowDraw.py

   import turtle
   t = turtle.Pen()
   t.speed(0)
➊ t.turtlesize(2,2,2)
   def up():
       t.forward(50)
   def left():
       t.left(90)
   def right():
       t.right(90)
   turtle.onkeypress(up, "Up")
   turtle.onkeypress(left, "Left")
   turtle.onkeypress(right, "Right")
   turtle.listen()

在➊位置,ArrowDraw.py 中唯一新增的一行代码,我们将乌龟箭头的大小增大了两倍,并使用 t.turtlesize(2,2,2) 给它加上了更粗的轮廓。三个参数分别是水*拉伸(2 表示宽度是原来的两倍),垂直拉伸(2 表示高度是原来的两倍)和轮廓厚度(2 像素)。图 7-5 显示了结果。

ArrowDraw.py 程序允许用户使用上、右和左箭头键进行绘图。更大的乌龟箭头使得更容易看出乌龟的移动方向。

图 7-5. ArrowDraw.py 程序允许用户使用上、右和左箭头键进行绘图。更大的乌龟箭头使得更容易看出乌龟的移动方向。

这个应用程序有点像旧款的 Etch-A-Sketch 玩具:你只需要使用这三个箭头键就可以绘制有趣的形状,并且可以重绘你的步骤。随意定制这个应用程序,可以修改颜色、笔宽以及任何你想添加的其他功能。你可以加入一个额外的功能,这个功能作为本章结尾的挑战,允许点击来移动乌龟到一个新的位置。发挥创意,设计新功能并进行尝试——这是学习新事物的最佳方式!

处理带参数的事件:Clickspiral

TurtleDraw.py 中,我们通过告诉 turtle.onscreenclick() 监听器在用户点击屏幕时调用 t.setpos 函数来让用户点击进行绘图。现在,让我们编写一个新的程序 ClickSpiral.py,它将在用户点击的地方绘制螺旋线,如 图 7-6 所示。

onscreenclick() 监听器将每次鼠标点击的 x 和 y 坐标作为参数传递给我们指定的回调函数。当我们想要用自己的函数处理鼠标点击事件时,只需编写一个接受这些值(鼠标点击的 x 和 y 坐标)作为一对参数的函数。

使用 ClickSpiral.py 应用程序绘制的笑脸

图 7-6. 使用 ClickSpiral.py 应用程序绘制的笑脸

RandomSpiralsFunction.py (RandomSpiralsFunction.py) 包含一个名为 random_spiral() 的函数,用于在屏幕上的随机位置绘制彩色螺旋。现在,我们不再希望在随机位置绘制螺旋,而是希望在用户点击鼠标的位置显示螺旋。为此,我们可以重写 random_spiral() 函数,接受来自 turtle.onscreenclick() 监听器的两个参数 xy。我们将函数重命名为 spiral(x,y)

def spiral(x,y):
    t.pencolor(random.choice(colors))
    size = random.randint(10,40)
    t.penup()
    t.setpos(x,y)
    t.pendown()
    for m in range(size):
        t.forward(m*2)
        t.left(91)

在这个新版本中,我们修改了函数的定义,以反映新的名称和我们将接收的两个参数,用于在屏幕上选择的位置绘制为 spiral(x,y)。我们仍然为每个螺旋选择一个随机的颜色和大小,但我们已经去除了生成随机 xy 的两行代码,因为我们将从 onscreenclick() 监听器中获取 xy 作为参数。与 random_spiral() 函数一样,我们将笔移动到正确的 (x, y) 位置,然后绘制螺旋。

剩下的唯一步骤是设置我们的海龟窗口和颜色列表,然后告诉 turtle.onscreenclick() 监听器在用户点击绘图窗口的鼠标按钮时调用螺旋函数。以下是完整的程序:

ClickSpiral.py

   import random
   import turtle
   t = turtle.Pen()
   t.speed(0) turtle.bgcolor("black")
   colors = ["red", "yellow", "blue", "green", "orange", "purple",
             "white", "gray"]
   def spiral(x,y):
       t.pencolor(random.choice(colors))
       size = random.randint(10,40)
       t.penup()
       t.setpos(x,y)
       t.pendown()
       for m in range(size):
           t.forward(m*2)
           t.left(91)
➊ turtle.onscreenclick(spiral)

TurtleDraw.py 中一样,我们省略了回调函数的括号和参数 ➊:turtle.onscreenclick(spiral) 告诉我们的程序每次用户在屏幕上点击鼠标时都应该调用 spiral(x,y) 函数,并且事件监听器会自动将两个参数——鼠标点击的 x 坐标和 y 坐标——传递给 spiral 回调函数。在 TurtleDraw.py 中,发生了类似的事情,使用的是 t.setpos 回调,但这一次,我们创建了自己的函数,在鼠标点击的位置绘制一个随机颜色和大小的螺旋。

更进一步:Clickandsmile

让我们通过做一个小小的改变来扩展这个互动应用。假设我们不再绘制螺旋线,而是希望在用户点击绘图屏幕时绘制一个微笑脸。代码将与我们在《综合应用》中使用的RandomSmileys.py程序非常相似,但不同的是,这个程序会处理鼠标点击事件,在用户选择的位置绘制一个微笑脸,用户点击多少次就绘制多少次。

实际上,因为我们的draw_smiley()函数已经接受两个参数(我们希望绘制微笑脸的 x 和 y 坐标),所以ClickAndSmile.py的代码与RandomSmileys.py完全相同,唯一的区别是最后一部分。只需将绘制 50 个随机微笑脸的for循环替换为调用turtle.onscreenclick(draw_smiley)。记得turtle.onscreenclick()函数如何让我们将一个函数的名字(如setpos)作为鼠标点击事件的处理程序吗?我们可以传递draw_smiley,这样当用户点击时,draw_smiley()函数就会在点击的位置执行它的工作。在turtle.onscreenclick()的括号中,我们不需要包含draw_smiley的括号或任何参数。

ClickAndSmile.py

import random
import turtle
t = turtle.Pen()
t.speed(0)
t.hideturtle()
turtle.bgcolor("black")
def draw_smiley(x,y):
    t.penup()
    t.setpos(x,y)
    t.pendown()
    # Face
    t.pencolor("yellow")
    t.fillcolor("yellow")
    t.begin_fill()
    t.circle(50)
    t.end_fill()
    # Left eye
    t.setpos(x-15, y+60)
    t.fillcolor("blue")
    t.begin_fill()
    t.circle(10)
    t.end_fill()
    # Right eye
    t.setpos(x+15, y+60)
    t.begin_fill()
    t.circle(10)
    t.end_fill()
    # Mouth
    t.setpos(x-25, y+40)
    t.pencolor("black")
    t.width(10)
    t.goto(x-10, y+20)
    t.goto(x+10, y+20)
    t.goto(x+25, y+40)
    t.width(1)
turtle.onscreenclick(draw_smiley)

现在,用户可以在屏幕上点击的任何地方绘制微笑脸,而不再是随机地绘制微笑脸;他们甚至可以用小的微笑脸绘制一个大的微笑脸,正如在图 7-7 中所示。

我们让微笑程序变得更加互动,可以在用户点击的地方绘制。

图 7-7. 我们让微笑程序变得更加互动,可以在用户点击的地方绘制。

无论你想构建什么样的应用程序,你可能都会依赖用户交互来驱动体验。想一想那些你花最多时间玩的游戏或其他应用程序:它们的共同点是你可以控制发生什么以及何时发生。不管是移动球拍来击打球;按下鼠标按钮或触摸并拖动来发射某物;还是点击、滑动和轻触来清除屏幕,你都在生成用户事件——而你喜欢的程序会通过做一些很酷的事情来处理这些事件。让我们再构建一个互动应用来练习一下,之后我们将构建更多我们每天玩的应用程序。

ClickKaleidoscope

让我们将创建函数的能力和处理互动点击的能力结合起来,创造一个互动的万花筒。用户可以在屏幕上的任何位置点击,然后从用户点击的位置开始绘制四个反射的随机形状和颜色的螺旋线。最终效果将类似于我们在 Kaleidoscope.py 程序中的展示,但用户将能够使用这个万花筒创造出他们自己的独特图案。

Draw_Kaleido() 函数

让我们谈谈构建定制化万花筒程序的挑战。我们知道我们想让用户点击屏幕以开始绘图过程,因此我们将使用前一节中的 turtle.onscreenclick() 函数。我们知道这个函数将给我们一个 (x, y) 的位置,我们可以在回调函数中使用这个位置。我们还可以回顾原始的万花筒程序,看到我们只需要在四个位置 (x, y), (–x, y), (–x, –y), 和 (x, –y) 绘制螺旋线,就能实现所需的反射效果。

我们的四个反射螺旋线应该具有相同的颜色和大小,以创造镜像效果。我们将调用函数 draw_kaleido() 并将其定义如下:

➊ def draw_kaleido(x,y):
➋     t.pencolor(random.choice(colors))
➌     size = random.randint(10,40)
       draw_spiral(x,y, size)
       draw_spiral(-x,y, size)
       draw_spiral(-x,-y, size)
       draw_spiral(x,-y, size)

在 ➊ 处,我们将函数命名为 draw_kaleido,并允许它从 turtle.onscreenclick() 事件处理器中获取两个参数 xy,这样四个反射的螺旋线就会从用户点击的 (x, y) 位置开始。接着,在 ➋ 处,我们随机选择一个颜色作为四个反射螺旋线的颜色,从我们常用的颜色列表 colors 中挑选。

在 ➌ 处,我们为四个反射的螺旋线选择一个随机大小,并将其存储在 size 中。最后,我们通过一个尚未实际编写的新函数 draw_spiral(),在它们的 (x, y), (–x, y), (–x, –y), 和 (x, –y) 位置绘制这四个螺旋线。

没有说明文字的图片

Draw_Spiral() 函数

我们的 draw_spiral() 函数需要从屏幕上一个自定义的 (x, y) 位置开始绘制螺旋。Python 的海龟笔一旦设置了颜色,就会记住这个颜色,因此我们不需要将颜色作为参数传递给 draw_spiral() 函数,但我们确实需要传递螺旋的 (x, y) 位置和 size。因此,我们将定义 draw_spiral() 函数,接受三个参数:

def draw_spiral(x,y, size):
    t.penup()
    t.setpos(x,y)
    t.pendown()
    for m in range(size):
        t.forward(m*2)
        t.left(92)

这个函数接受参数 xy,用于确定每个螺旋线的起始位置,以及 size 参数来指定螺旋线的大小。在函数内部,我们先抬起海龟的笔,使其可以在不留下痕迹的情况下移动,然后将笔移到给定的 (x, y) 位置,并重新放下笔准备绘制螺旋。我们的 for 循环将从 0size 遍历 m,绘制一个边长为 size 的方形螺旋。

在我们的程序中,除了导入randomturtle模块并设置屏幕和颜色列表外,我们只需要告诉计算机监听海龟屏幕上的点击,并在每次点击事件发生时调用draw_kaleido()函数。我们可以通过命令turtle.onscreenclick(draw_kaleido)来实现这一点。

整合所有内容

这是完整的ClickKaleidoscope.py程序。你可以在 IDLE 中输入它,或者从 www.nostarch.com/teachkids/ 下载并运行。

ClickKaleidoscope.py

import random
import turtle
t = turtle.Pen()
t.speed(0)
t.hideturtle()
turtle.bgcolor("black")
colors = ["red", "yellow", "blue", "green", "orange", "purple",
          "white", "gray"]
def draw_kaleido(x,y):
    t.pencolor(random.choice(colors))
    size = random.randint(10,40)
    draw_spiral(x,y, size)
    draw_spiral(-x,y, size)
    draw_spiral(-x,-y, size)
    draw_spiral(x,-y, size)
def draw_spiral(x,y, size):
    t.penup()
    t.setpos(x,y)
    t.pendown()
    for m in range(size):
        t.forward(m*2)
        t.left(92)
turtle.onscreenclick(draw_kaleido)

我们从正常的import语句开始,然后设置我们的海龟环境和颜色列表。接着,我们定义了draw_spiral()函数,然后是draw_kaleido()函数,最后我们告诉计算机监听海龟屏幕上的点击事件,并在发生点击事件时调用draw_kaleido()。现在,每当用户在绘图窗口中点击一个位置时,那里就会绘制一个螺旋图案,并且沿着 x 轴和 y 轴反射,总共生成四个相同的随机形状和大小的螺旋图案。

结果是我们螺旋万花筒程序的一个完全互动版本,允许用户通过点击屏幕的特定区域来控制反射图案,从而只在他们希望螺旋出现的地方生成图案。Figure 7-8 显示了程序运行时,螺旋构成的反射图案的样本。

使用我们的互动万花筒程序,你可以创建任何你想要的反射图案!

图 7-8. 使用我们的互动万花筒程序,你可以创建任何你想要的反射图案!

尝试你自己的图案(比如你的首字母!),并截图保存结果(在 Windows 中,按住 ALT 和 PRINT SCREEN 键将海龟窗口复制到剪贴板,然后粘贴到 Word 或你最喜欢的绘图程序中;在 Mac 上,按住 COMMAND、SHIFT 和 4 键,然后按空格键,再点击海龟绘图窗口,将图片保存到桌面,文件名为 Screenshot <日期和时间>.png)。在 Twitter 上用标签 #kidscodebook 将你最棒的截图发给我,@brysonpayne,我会尽力回复!

你学到了什么

在本章中,你学习了如何将可重用的代码块组织成函数,如何从程序的任何地方调用自己的函数,如何将信息作为参数传递给这些函数,以及如何通过返回值从函数中获取信息。我们通过告诉计算机监听鼠标点击和键盘按键,编写了第一个事件驱动的程序,并且你学会了如何编写自己的回调函数来响应用户事件。

我们已经开发了第一个完全互动的程序。运用你在本章中学到的技能,你已经准备好开始编写更先进的应用程序了。我们常用的应用程序通过响应点击、触摸、按键等方式,让用户体验到掌控程序的感觉。

在掌握本章概念后,你应该能够完成以下任务:

  • 使用函数使代码更加可重用。

  • 将代码组织和分组到函数中。

  • 使用def关键字在 Python 中定义函数。

  • 在你编写的程序中调用你自己的函数。

  • 定义并使用接受参数作为输入值的函数。

  • 编写函数,当调用时返回值。

  • 将数学公式转换为一个返回公式值的函数。

  • 解释事件驱动程序的一些特性。

  • 编写一个基本的事件驱动应用程序,使用事件处理程序。

  • 编写一个接受鼠标点击并在屏幕上绘图的应用程序。

  • 编写键盘事件的事件处理程序。

  • 编写接受参数的事件处理程序函数。

  • 使用屏幕上的 x 和 y 坐标绘制特定的图案,例如万花筒。

编程挑战

这里有三个挑战,旨在扩展你在本章中学到的内容。对于这些挑战的示例答案,请访问www.nostarch.com/teachkids/

#1: 镜像笑脸

ClickAndSmile.pyClickKaleidoscope.py程序合并,在屏幕的四个镜像角落绘制笑脸,当你点击时,就像万花筒程序用螺旋图案那样。如果你想挑战更高难度,绘制两个翻转过来的笑脸,使它们真正看起来像是沿 x 轴镜像的。

#2: 更多乒乓球计算

修改乒乓球计算器,使其要求用户输入乒乓球的数量。并告诉用户这些乒乓球如果堆叠在一起会有多高,以及这些乒乓球的重量是多少。

#3: 更好的绘图程序

修改ArrowDraw.py程序,允许用户以更小的增量旋转海龟——例如 45 度(甚至 30 度或 15 度)——以便更精细地控制海龟。然后,添加更多的键盘选项,比如允许用户按下大于符号(>)使绘图长度变长,按下小于符号(<)缩短绘图长度,按下 W 键使画笔变宽,按下 T 键使画笔变细。为了让它成为一个出色的绘图程序,可以通过在屏幕上绘制一条字符串,展示笔宽、段长度和海龟的方向来提供反馈,每次变化后都显示这些信息。

最后,为程序添加一个功能,允许点击重新定位海龟。(提示:创建一个接受两个参数(xy)的函数,抬起海龟的笔,移动到(xy)位置,然后放下笔。然后,将这个函数的名字传递给turtle.onscreenclick(),完成应用程序。)

第八章. 定时器与动画:迪士尼会怎么做?

我在青少年时期学习编程的一种方式是编写简短的游戏和动画,然后修改代码让它做些新事情。我很惊讶自己可以立即看到代码让图形出现在屏幕上,我相信你也会和我一样喜欢这种体验。

游戏和动画有几个共同点。首先,它们很有趣!其次,它们都涉及在屏幕上绘制图形,并随着时间的推移改变这些图形,从而产生运动的错觉。从本书开始,我们就能够绘制图形,但 Turtle 库对于大量的动画或移动对象来说太慢了。在这一章,我们将安装并使用一个新的模块,Pygame,它让我们能够绘制、动画化,甚至使用你到目前为止学到的技能创建街机风格的游戏。

使用 Pygame 获取所有图形界面

图形用户界面GUI,有时读作“gooey”)包括你在计算机屏幕上看到的所有按钮、图标、菜单和窗口;它是你与计算机交互的方式。当你拖放一个文件或点击一个图标打开程序时,你正在享受 GUI。在游戏中,当你按键、移动鼠标或点击时,你之所以能够期待某些事情发生(例如跑步、跳跃、旋转视角等),是因为程序员设置了 GUI。

和 Turtle 库一样,Pygame 非常可视化,非常适合用于游戏、动画等的图形用户界面(GUI)。它几乎可以移植到所有操作系统,从 Windows 到 Mac,再到 Linux 等,因此你在 Pygame 中创建的游戏和程序几乎可以在任何计算机上运行。图 8-1 展示了 Pygame 网站,你可以在这里下载 Pygame。

Pygame 是免费的,其网站上的教程和示例游戏也都是免费的。

图 8-1. Pygame 是免费的,其网站上的教程和示例游戏也都是免费的。

要开始,首先通过从 www.pygame.org/ 下载安装程序来安装 pygame 模块。对于 Windows,你可能需要下载 pygame-1.9.1 .win32-py3.1.msi,如果遇到问题,可以参考附录 B 获取帮助。对于 Mac 和 Linux,安装过程更为复杂;请参见附录 B 或访问 www.nostarch.com/teachkids/ 获取逐步指导。

你可以通过在 Python shell 中输入以下内容来检查 Pygame 是否安装没有错误:

>>> import pygame

如果你收到一个常规的 >>> 提示符,说明 Python 能够正确找到 pygame 模块并没有出错,Pygame 库已经准备好可以使用了。

使用 Pygame 绘制一个点

一旦你安装了 Pygame,你可以运行一个简短的示例程序,在屏幕上绘制一个点,就像图 8-2 中的那样。

ShowDot.py 程序的运行

图 8-2. ShowDot.py 程序的运行

在一个新的 IDLE 窗口中输入以下代码,或者从 www.nostarch.com/teachkids/ 下载它:

ShowDot.py

  import pygame

➊ pygame.init()
➋ screen = pygame.display.set_mode([800,600])

➌ keep_going = True
➍ GREEN = (0,255,0)    # RGB color triplet for GREEN
   radius = 50

➎ while keep_going:
➏     for event in pygame.event.get():
➐         if event.type == pygame.QUIT:
               keep_going = False
➑     pygame.draw.circle(screen, GREEN, (100,100), radius)
➒     pygame.display.update()

➓ pygame.quit()

让我们逐行分析这个程序。首先,我们导入 pygame 模块以访问其功能。在 ➊ 处,我们初始化 Pygame,或者说为其设置使用环境。每次想使用 Pygame 时,都需要调用 pygame.init(),并且它总是紧跟在 import pygame 命令之后,位于其他任何 Pygame 函数之前。

在 ➋ 处,pygame.display.set_mode([800,600]) 创建了一个宽 800 像素、高 600 像素的显示窗口。我们将其存储在名为 screen 的变量中。在 Pygame 中,窗口和图形被称为 表面,而显示表面 screen 是我们所有其他图形将被绘制的主窗口。

在 ➌ 处,你可能会认出我们的循环变量 keep_going:我们在第六章中的 HighCard.pyFiveDice.py 游戏循环中也使用了它,作为一个布尔标志来告诉程序继续游戏。在这个 Pygame 示例中,我们使用一个游戏循环来持续绘制图形窗口,直到用户关闭窗口。

在 ➍ 处,我们设置了两个变量,GREENradius,用于绘制我们的圆形。GREEN 变量被设置为 RGB 三元组值 (0,255,0),一种明亮的绿色。(RGB,即 红绿蓝,是指定颜色的多种方式之一。选择颜色时,需选择三个数字,每个数字的范围是 0 到 255。第一个数字决定颜色中的红色成分,第二个是绿色成分,第三个是蓝色成分。我们将绿色的值设为 255,红色和蓝色的值设为 0,因此我们的 RGB 颜色是全绿色,且没有红色或蓝色。)我们的 GREEN 变量是一个常量。我们有时会将常量—即我们不打算改变的变量—写成全大写字母。由于该颜色在整个程序中应保持不变,因此我们为 GREEN 使用了全大写字母。我们将 radius 变量设置为 50 像素,用于绘制直径为 100 像素的圆形。

在 ➎ 处的 while 循环是我们的游戏循环,它将不断运行 Pygame 窗口,直到用户选择退出。➏ 处的 for 循环是我们处理用户在程序中触发的所有交互事件的地方。在这个简单的示例中,我们唯一需要检查的事件是用户是否点击了红色的 X 来关闭窗口并退出程序 ➐。如果是这样,keep_going 被设置为 False,我们的游戏循环结束。

没有标题的图像

在➑处,我们在screen窗口中的位置(100,100)绘制一个半径为 50 的绿色圆圈:从窗口的左上角向右移动 100 像素,向下移动 100 像素(有关 Pygame 坐标系统如何不同于海龟图形的更多信息,请参见 What’s New in Pygame)。我们使用pygame.draw,这是 Pygame 的一个模块,用于绘制圆形、矩形和线段等形状。我们将四个参数传递给pygame.draw.circle()函数:我们要在其上绘制圆圈的表面(screen),圆圈的颜色(GREEN),圆心的坐标和半径。位于➒的update()函数告诉 Pygame 刷新屏幕并应用绘图的变化。

最后,当用户退出游戏循环时,位于➓的pygame.quit()命令会清除pygame模块(它撤销了➊中的所有设置),并关闭screen窗口,使得程序能够正常退出。

当你运行ShowDot.py时,你应该看到一个像图 8-2 中的图像。花些时间玩一下这个点程序——创建一个不同的 RGB 颜色三元组,改变点在屏幕上的位置,或者画第二个点。你将开始看到使用 Pygame 绘制图形的强大功能和简便性,同时也会玩得很开心。

这个第一个程序包含了我们将要基于其构建的基础,之后我们可以创建更复杂的图形、动画,最终制作游戏。

Pygame 中的新变化

在我们深入探讨 Pygame 的激动人心的世界之前,值得注意的是 Pygame 和我们之前熟悉的海龟图形之间一些重要的区别:

  • 我们有了一个新的坐标系统,如图 8-3 所示。在海龟图形中,原点位于屏幕的中心,y值随着向上移动而增大。Pygame 使用的是更常见的窗口坐标系统(在许多其他 GUI 编程语言中也会看到这种系统,包括 Java、C++等)。在 Pygame 中,窗口的左上角是原点(0, 0)。x 坐标值仍然随着向右移动而增大(但没有负的 x 坐标值,因为它们会在屏幕左侧消失);y 坐标值随着向下移动而增大(负的 y 坐标值会消失在窗口的顶部)。

    Pygame 使用窗口坐标系统。

    图 8-3. Pygame 使用窗口坐标系统。

  • 游戏循环在 Pygame 中总是被使用。在我们之前的程序中,只有在需要继续播放或者返回重新做某件事时才使用循环,但 Pygame 要求使用游戏循环来持续更新屏幕并处理事件(即使我们处理的唯一事件只是关闭窗口)。

  • 我们通过调用 pygame.event.get() 来获取用户执行的事件列表,从而处理 Pygame 中的事件。这些事件可能是鼠标点击、按键、甚至是窗口事件,例如用户关闭窗口。我们使用 for 循环来处理来自 pygame.event.get() 的所有事件。在我们的海龟程序中,我们使用回调函数来处理事件。在 Pygame 中,我们仍然可以创建函数并在事件处理代码中调用它们,但我们可以仅通过 if 语句来处理我们关心的事件。

这些差异使得 Pygame 成为解决问题的新方法,这正是我们一直在寻找的!工具越多,我们能解决的问题就越多。

游戏的组成部分

在这一节中,我们将修改 ShowDot.py 程序,将其显示笑脸图片,而不是绿色圆形,如 图 8-4 所示。

ShowPic.py 在屏幕上绘制了图片 CrazySmile.bmp。

图 8-4. ShowPic.py 在屏幕上绘制了图片 CrazySmile.bmp

在构建我们的 ShowPic.py 程序时,我们将了解 Pygame 中游戏或动画的三个主要部分。首先是设置阶段,我们在这里导入所需的模块,创建屏幕并初始化一些重要的变量。接着是游戏循环,它处理事件、绘制图形并更新显示。这个游戏循环是一个 while 循环,只要用户没有退出游戏,它就会一直运行。最后,我们需要一种方法来在用户退出游戏时结束程序。

设置

首先,下载笑脸图片,并将其保存在与 Python 程序相同的文件夹中。访问 www.nostarch.com/teachkids/ 下载源代码,并将图片 CrazySmile.bmp 保存到你保存 .py 文件的文件夹中。你保存 .py 文件的位置并不重要,只需确保将 BMP(即 位图,一种常见的图片文件格式)图片文件保存到相同的位置即可。

接下来,让我们处理设置:

   import pygame       # Setup
   pygame.init()
   screen = pygame.display.set_mode([800,600])
   keep_going = True
➊ pic = pygame.image.load("CrazySmile.bmp")

一如既往,我们导入 pygame 模块,然后使用 pygame.init() 函数进行初始化。接下来,我们设置 screen 为一个新的 Pygame 窗口,大小为 800×600 像素。我们创建布尔标志 keep_going 来控制游戏循环,并将其设为 True。最后,我们做了一些新尝试:在 ➊ 处,我们使用 pygame.image.load(),它从文件中加载一张图片。我们为图片文件创建一个变量,并加载 CrazySmile.bmp,在程序中我们将其称为 pic

创建游戏循环

此时,我们还没有绘制任何东西,但我们已经设置了 Pygame 并加载了一张图片。游戏循环是我们实际在屏幕上显示笑脸图片的地方。它也是我们处理用户事件的地方。我们首先处理一个重要的事件:用户选择退出游戏。

   while keep_going:    # Game loop
       for event in pygame.event.get():
➊         if event.type == pygame.QUIT:
               keep_going = False

我们的游戏循环将持续运行,直到keep_goingTrue。在循环内,我们立即检查用户的事件。在高级游戏中,用户可能同时触发许多事件,比如按下键盘上的下箭头、同时移动鼠标向左,并滚动鼠标滚轮。

在这个简单的程序中,我们监听的唯一事件是用户是否点击了关闭窗口按钮来退出程序。我们在➊处进行检查。如果用户尝试关闭窗口并触发了pygame.QUIT事件,我们希望告诉游戏循环退出。我们通过将keep_going设置为False来实现这一点。

我们仍然需要将图像绘制到屏幕上并更新绘图窗口,以确保所有内容都能显示在屏幕上,因此我们将在游戏循环中添加这两行代码:

screen.blit(pic, (100,100))
pygame.display.update()

blit()方法将pic(我们从磁盘加载的图像,笑脸)绘制到我们的显示表面screen上。当我们希望将一个表面(比如从磁盘加载的图像)上的像素复制到另一个表面(如绘图窗口)时,我们会使用blit()。在这里,我们需要使用blit(),因为pygame.image.load()函数的工作方式不同于我们之前用来绘制绿色圆点的pygame.draw.circle()函数。所有pygame.draw函数都接受一个表面作为参数,所以通过将screen传递给pygame.draw.circle(),我们能够让pygame.draw.circle()绘制到我们的显示窗口上。但pygame.image.load()不接受表面作为参数;相反,它会自动为你的图像创建一个新的、独立的表面。除非你使用blit(),否则图像不会出现在原始的绘图屏幕上。

无说明的图片

在这种情况下,我们告诉blit(),我们希望将pic绘制在位置(100,100),即从屏幕左上角向右偏移 100 像素、向下偏移 100 像素(在 Pygame 的坐标系统中,原点是屏幕的左上角;请参见图 8-3)。

我们游戏循环的最后一行是调用pygame.display.update()。该命令告诉 Pygame 显示绘图窗口,并展示这一轮循环中所做的所有更改,包括我们的笑脸。当update()运行时,窗口将更新,显示所有screen表面上的更改。

到目前为止,我们已经完成了设置代码,并且有一个带有事件处理器的游戏循环,监听用户点击关闭窗口按钮的事件。如果用户点击关闭窗口按钮,程序将更新显示并退出循环。接下来,我们将处理程序退出的部分。

退出程序

我们代码的最后一部分将在用户选择退出游戏循环后退出程序:

pygame.quit()      # Exit

如果你的程序中没有这一行,显示窗口在用户尝试关闭时会保持打开状态。调用pygame.quit()可以关闭显示窗口,并释放存储图像pic的内存。

汇总

将所有代码组合在一起,你将看到我们的CrazySmile.bmp图像文件——前提是你已将图像保存在与ShowPic.py程序文件相同的目录下。以下是完整的代码:

ShowPic.py

import pygame        # Setup
pygame.init()
screen = pygame.display.set_mode([800,600])
keep_going = True
pic = pygame.image.load("CrazySmile.bmp")
while keep_going:    # Game loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            keep_going = False
    screen.blit(pic, (100,100))
    pygame.display.update()

pygame.quit()        # Exit

当你点击关闭窗口按钮时,显示窗口应该会关闭。

这段代码包含了我们将用来构建更加互动的程序的所有基本组件。在本章的其余部分以及在第九章中,我们将向游戏循环中添加代码,以响应不同的事件(例如,当用户移动鼠标时,屏幕上的图像也随之移动)。现在,让我们看看如何创建一个绘制动画跳跃球的程序!

恰到好处的时机:移动和弹跳

我们已经具备了创造动画的技能,或者说是运动的幻觉,只需对我们的ShowPic.py程序做一个小改动。我们不再每次都将笑脸图像显示在固定的位置,而是每帧稍微改变一下位置。这里的是指游戏循环中的每次循环。这个术语来源于动画制作的方式:他们绘制数千张单独的图片,使每张图片与前一张有所不同。一张图片被视为一帧。然后,动画师将所有的图片拼接在一条胶片上,并通过放映机播放。当这些图片以非常快的速度依次显示时,就看起来像是画中的人物在运动。

没有标题的图像

使用计算机,我们可以通过在屏幕上绘制一张图片、清空屏幕、稍微移动图片,然后重新绘制它,来创造出相同的效果。这个效果看起来有点像图 8-5。

在第一次尝试动画时,我们的笑脸图像会从屏幕上滑出。

图 8-5。在第一次尝试动画时,我们的笑脸图像会从屏幕上滑出。

我们仍然称每一帧绘制为一个,动画的速度则是我们每秒绘制的帧数 (fps)。一款视频游戏可能会以每秒 60 到 120 帧的速度运行,就像高清电视一样。美国的老式标清电视运行在 30 fps,而许多电影放映机的帧率是 24 fps(新型的高清数字放映机可以运行到 60 fps 或更高)。

如果你曾制作或见过翻书动画(即在笔记本的角落上绘画,然后翻动它们以创建迷你卡通),你就知道,可以通过不同的帧率创建运动的错觉。我们的目标是约 60 帧每秒,足够快以创建*滑的动画。

移动表情符号

我们可以通过在while循环中绘制表情符号图像在不同位置上来实现简单的运动。换句话说,在我们的游戏循环中,我们只需要更新图片的(x, y)位置,并且每次循环时在新的位置绘制它。

我们将向ShowPic.py中添加两个变量:picxpicy,分别表示图像在屏幕上的 x 和 y 坐标。我们将在程序的设置部分末尾添加这两个变量,然后将程序的新版本保存为SmileyMove.py(最终版本见 SmileyMove.py)。

   import pygame       # Setup
   pygame.init()
➊ screen = pygame.display.set_mode([600,600])
   keep_going = True
   pic = pygame.image.load("CrazySmile.bmp")
➋ colorkey = pic.get_at((0,0))
➌ pic.set_colorkey(colorkey)
   picx = 0
   picy = 0

注意

行和行是一个可选的修复,解决了一个小问题。如果 CrazySmile.bmp 图像在您的屏幕上看起来有黑色方形角落,您可以包含这两行代码以确保这些角落看起来是透明的。

请注意,我们也将窗口屏幕的尺寸更改为 600×600 像素,使窗口在➊处变为正方形。游戏循环将以与ShowPic.py中相同的方式开始,但我们会添加代码,每次循环运行时将picxpicy变量的值增加 1 像素:

while keep_going:    # Game loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            keep_going = False

    picx += 1        # Move the picture
    picy += 1

+=运算符将某些值加到左侧的变量(picxpicy)上,所以通过+= 1,我们告诉计算机每次循环时将图片的 x 和 y 坐标(picx, picy)分别改变 1 像素。

最后,我们需要将图像复制到新位置并显示,更新显示内容,并告诉我们的程序如何退出:

    screen.blit(pic, (picx, picy))
    pygame.display.update()
pygame.quit()        # Exit

如果你运行这些代码,你会看到我们的图像飞速移动!事实上,你得快点看,因为它会快速离开屏幕。

请回顾图 8-5,以了解表情符号图像在它滑出视野之前的样子。

第一个版本可能会在表情符号图像离开绘图窗口时,屏幕上仍留下像素痕迹。我们可以通过在每一帧之间清除屏幕来让动画看起来更干净。我们看到的表情符号背后留下的拖尾线是表情符号图像的左上角像素;每次我们在每一帧中向下和向右移动来绘制图像的一个新版本并更新显示时,我们都会留下上一张图像的一些散落像素。

我们可以通过在绘图循环中添加一个screen.fill()命令来解决这个问题。screen.fill()命令接受一个颜色作为参数,所以我们需要告诉它我们想要使用哪种颜色来填充绘图屏幕。我们为BLACK添加一个变量(使用全大写的BLACK表示它是一个常量),并将其设置为黑色的 RGB 色彩三元组(0,0,0)。在我们绘制每一帧新图像之前,先用黑色像素填充屏幕,从而有效地清除它。

picy = 0之后立即在设置中添加这一行,以创建黑色背景填充颜色:

BLACK = (0,0,0)

并且在绘制我们的pic图像到屏幕上的screen.blit()语句前添加这一行:

screen.fill(BLACK)

我们的笑脸依旧飞速离开屏幕,但这次我们并没有在移动的图像后面留下像素的痕迹。通过用黑色像素填充屏幕,我们在每一帧中“擦除”旧的图像,然后在新位置绘制新图像。这就创造了更加流畅的动画效果。然而,在一台相对较快的电脑上,我们的笑脸飞出屏幕的速度太快了。为了解决这个问题,我们需要一个新的工具:一个计时器或时钟,可以保持我们每秒固定的帧数。

使用Clock类为笑脸动画添加动画效果

让我们的SmileyMove.py应用程序像游戏或电影中的动画一样表现出来的最后一块拼图是限制每秒绘制的帧数。目前,我们每次在游戏循环中只让笑脸图像向下移动 1 个像素,向右移动 1 个像素,但我们的电脑可以如此迅速地绘制这个简单场景,产生每秒数百帧,从而导致笑脸瞬间飞出屏幕。

没有说明的图片

流畅的动画每秒需要 30 到 60 帧,因此我们不需要每秒数百帧的快速闪过。

Pygame 有一个可以帮助我们控制动画速度的工具:Clock类。就像一个模板,可以用来创建某种类型的对象,这些对象具有一些功能和数值,帮助它们以特定方式运行。可以把类看作是饼干模具,而对象就是饼干:当我们想要做某种形状的饼干时,我们会制作一个可以随时用来做出同样形状饼干的模具。就像函数帮助我们将可重用的代码打包在一起一样,类允许我们将数据和函数打包成一个可重用的模板,我们可以用它来创建面向未来程序的对象。

我们可以通过以下这行代码在程序的设置中添加一个Clock类的对象:

timer = pygame.time.Clock()

这会创建一个名为timer的变量,链接到一个Clock对象。这个timer将允许我们在每次游戏循环时轻轻暂停,等待足够的时间,以确保我们不会绘制超过每秒设定帧数的图像。

在我们的游戏循环中添加以下行,将通过让我们名为timerClock每秒仅“tick” 60 次,保持帧率为 60 帧每秒:

timer.tick(60)

以下代码,SmileyMove.py,展示了整个应用程序的实现。它呈现了一个*滑、稳定的动画笑脸,慢慢滑出屏幕的右下角。

SmileyMove.py

import pygame                # Setup
pygame.init()
screen = pygame.display.set_mode([600,600])
keep_going = True
pic = pygame.image.load("CrazySmile.bmp")
colorkey = pic.get_at((0,0))
pic.set_colorkey(colorkey)
picx = 0
picy = 0
BLACK = (0,0,0)
timer = pygame.time.Clock()  # Timer for animation

while keep_going:            # Game loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            keep_going = False

    picx += 1                # Move the picture
    picy += 1

    screen.fill(BLACK)       # Clear screen
    screen.blit(pic, (picx,picy))
    pygame.display.update()
    timer.tick(60)           # Limit to 60 frames per second

pygame.quit()                # Exit

剩下的问题是,笑脸仍然会在几秒钟内完全滑出屏幕。这并不有趣。让我们修改程序,使笑脸保持在屏幕上,并在角落之间反弹。

让笑脸从墙上反弹

我们通过改变每次游戏循环中绘制图像的位置,加入了从一帧到下一帧的运动。我们已经看到如何通过添加Clock对象并告诉它每秒tick()多少次来调节动画的速度。在这一部分,我们将看到如何让笑脸保持在屏幕上。效果将类似于图 8-6,笑脸似乎在绘图窗口的两个角落之间来回反弹。

我们的目标是让笑脸在屏幕的角落之间“反弹”。

图 8-6。我们的目标是让笑脸在屏幕的角落之间“反弹”。

我们的图像之前跑出屏幕的原因是因为我们没有为动画设置边界或限制。我们在屏幕上绘制的一切都是虚拟的——意味着它们并不存在于现实世界中——因此物体并不会真正互相碰撞。如果我们希望屏幕上的虚拟物体进行交互,我们必须通过编程逻辑来创建这些交互。

撞墙

当我说我们希望笑脸“反弹”出屏幕的边缘时,我的意思是,当笑脸到达屏幕的边缘时,我们希望改变它的移动方向,使其看起来像是从屏幕的固体边缘反弹回来。为此,我们需要测试笑脸的(picx,picy)位置是否到达了屏幕边缘的虚拟边界。我们称这种逻辑为碰撞检测,因为我们正试图检测或注意到碰撞发生的时刻,比如笑脸图像“撞到”绘图窗口的边缘。

我们知道可以使用if语句来测试条件,所以我们可以通过检查picx是否大于某个值,来判断我们的图像是否与屏幕的右侧“接触”或碰撞

让我们来推测一下这个值可能是什么。我们知道我们的屏幕宽度是 600 像素,因为我们通过pygame.display.set_mode([600,600])创建了屏幕。我们可以使用 600 作为边界,但笑脸依然会滑出屏幕的边缘,因为坐标对(picx,picy)是我们笑脸图像左上角的像素位置。

为了找到我们的逻辑边界——也就是 picx 必须到达的虚拟线,以使我们的笑脸看起来像是撞上了 screen 窗口的右边缘——我们需要知道图片的宽度。因为我们知道 picx 是图像的左上角,并且它会向右移动,我们只需将图片的宽度加到 picx 上,当这个和等于 600 时,我们就知道图像的右边缘触碰到了窗口的右边缘。

查找图像宽度的一种方法是查看文件的属性。在 Windows 中,右键点击 CrazySmile.bmp 文件,选择“属性”菜单项,然后点击“详细信息”选项卡。在 Mac 上,点击选择 CrazySmile.bmp 文件,按下 -I 获取文件信息窗口,然后点击“更多信息”。你将看到图像的宽度和高度,如 图 8-7 所示。

为了确定我们的虚拟边界,以便让我们的笑脸能够反弹,我们需要知道图像文件的宽度。

图 8-7. 为了确定我们的虚拟边界,以便让我们的笑脸能够反弹,我们需要知道图像文件的宽度。

我们的 CrazySmile.bmp 文件的宽度是 100 像素(高度也是 100 像素)。所以如果我们的 screen 当前是 600 像素宽,而 pic 图像需要 100 像素来显示完整图像,那么我们的 picx 必须保持在 x 方向上小于 500 像素。图 8-8 展示了这些测量值。

计算与窗口右侧的反弹

图 8-8. 计算与窗口右侧的反弹

但是,如果我们更改了图像文件或希望处理不同宽度和高度的图像怎么办呢?幸运的是,Pygame 在 pygame.image 类中提供了一个方便的函数,我们的图片变量 pic 就是利用这个函数。函数 pic.get_width() 返回存储在 pygame.image 变量 pic 中图像的宽度(以像素为单位)。我们可以使用这个函数,而不是将程序硬编码为只能处理宽度为 100 像素的图像。类似地,pic.get_height() 给出存储在 pic 中图像的高度(以像素为单位)。

我们可以使用如下语句测试图像 pic 是否已经超出了屏幕的右边缘:

if picx + pic.get_width() > 600:

换句话说,如果图片的起始 x 坐标加上图片的宽度大于屏幕的宽度,我们就知道图像已经超出了屏幕的右边缘,我们可以改变图像的运动方向。

没有标题的图像

改变方向

“反弹”意味着在撞到屏幕边缘后朝相反的方向移动。图像的移动方向是由 picxpicy 的更新控制的。在我们以前的 SmileyMove.py 中,我们每次通过 while 循环时,都会用这两行代码给 picxpicy 各加 1 像素:

picx += 1
picy += 1

然而,这些代码每次都会让我们的图像向右和向下各移动 1 像素;没有出现“反弹”或者方向改变,因为我们从未改变过添加到picxpicy的数值。这两行代码意味着我们可以确保每一帧都会以每帧 1 像素的速度向右和向下移动,即使笑脸已经离开了屏幕。

没有标题的图像

另外,我们可以将常量值 1 改为一个变量,来表示速度,即每一帧图像应该移动的像素数。速度是指在一段时间内的移动量。例如,在短时间内移动得很快的车在以高速行驶,而在相同时间内几乎不动的蜗牛则在以低速爬行。我们可以在程序的设置部分定义一个名为 speed 的变量,表示每一帧所希望的像素移动量:

speed = 5

然后,在我们的游戏循环中,我们只需要每次通过循环时,将 picxpicy 按照新的速度值(而不是常量 1)进行改变:

picx += speed
picy += speed

SmileyMove.py 中,60 帧每秒时,每帧 1 像素的速度显得有些慢,所以我将速度提高到了 5 像素,让它移动得更快。但我们仍然没有让图像从屏幕的右边缘反弹回来;我们只是很快地让它移出了屏幕,因为当碰到屏幕边缘时,speed 变量并没有发生变化。

我们可以通过添加碰撞检测逻辑来解决这个问题,也就是测试图像是否碰到了屏幕的左或右边缘:

if picx <= 0 or picx + pic.get_width() >= 600:
    speed = -speed

首先,我们通过检查picx是否试图绘制在负的 x 坐标值(也就是屏幕左侧,x < 0),或者picx + pic.get_width()是否超过了屏幕的 600 像素宽度(即图片的起始 x 坐标加上它的宽度已经超出了屏幕的右边缘)来检查屏幕的左右边界。如果发生其中任一情况,我们就知道图像已超出边界,需要改变方向。

注意我们在进行边界测试时使用的小技巧。如果其中任何一个边界测试的结果为True,通过设置speed = -speed,我们就通过将speed乘以-1,或者将它改为自身的相反数,来改变移动的方向。可以这么理解:如果我们继续以speed等于5的速度循环,直到我们的picx加上图片的宽度碰到屏幕右边缘(600 像素)(picx + pic.get_width() >= 600),设置speed = -speed会将speed5变为-5(负五)。然后,每当我们的picxpicy在下一次循环中变化时,我们将会把-5加到我们的坐标上。这等于减去 5,意味着我们在屏幕上向左和上移动。如果这样工作的话,我们的笑脸就会从屏幕的右下角反弹开始,开始向移动,回到屏幕的左上角(0,0)的位置。

但这还不是全部!因为我们的if语句还在检查左边界(picx <= 0),当我们的笑脸看起来像是撞到了屏幕的左侧时,它会再次将speed的值改为-speed。如果speed-5,这会将其改为-(-5),即+5。所以,如果我们的负值speed变量导致我们每帧向左和向上移动 5 个像素,一旦我们在屏幕的左边缘遇到picx <= 0speed = -speed会把speed改回正值5,然后笑脸图像会重新开始向右和下移动,沿着正方向的 x 轴和 y 轴。

整合所有内容

尝试我们应用的 1.0 版本,SmileyBounce1.py,看看笑脸如何从窗口的左上角跳到右下角,再返回,始终不离开绘图区域。

SmileyBounce1.py

import pygame       # Setup
pygame.init()
screen = pygame.display.set_mode([600,600])
keep_going = True
pic = pygame.image.load("CrazySmile.bmp")
colorkey = pic.get_at((0,0))
pic.set_colorkey(colorkey)
picx = 0
picy = 0
BLACK = (0,0,0)
timer = pygame.time.Clock()
speed = 5

while keep_going:    # Game loop
    for event in pygame.event.get():
         if event.type == pygame.QUIT:
            keep_going = False
    picx += speed
    picy += speed

    if picx <= 0 or picx + pic.get_width() >= 600:
        speed = -speed

    screen.fill(BLACK)
    screen.blit(pic, (picx,picy))
    pygame.display.update()
    timer.tick(60)

pygame.quit()       # Exit

使用这个版本的程序,我们创建了一个看起来像是在两个角落之间*滑跳动的笑脸。我们之所以能够实现这个效果,恰恰是因为窗口是一个完美的正方形,大小为 600×600 像素,并且因为我们始终以相同的值(speed)来更改picxpicy——我们的笑脸只在* x = y*的对角线上移动。通过保持图像在这条简单的路径上,我们只需检查picx是否越过了屏幕左右边缘的边界值。

如果我们想要让笑脸从屏幕的四个边(上、下、左、右)反弹,并且窗口不是完美的正方形——比如说 800×600 的尺寸,我们需要添加一些逻辑来检查picy变量,看看它是否越过了上边界或下边界(屏幕的顶部或底部),并且我们还需要分别跟踪水*和垂直的速度。接下来我们会做这些。

让笑脸弹跳四个墙壁

SmileyBounce1.py中,我们保持水*(左右)和垂直(上下)运动的锁定,使得每当图像向右移动时,它也会向下移动,而每当图像向左移动时,它也会向上移动。这在我们的正方形窗口中效果很好,因为屏幕的宽度和高度是相同的。让我们在这个例子基础上,创建一个弹跳动画,使其能在绘图窗口的四个边缘上真实地反弹。我们将把窗口大小设置为 800×600 像素,使用screen = pygame.display.set_mode([800,600])来使动画更加有趣。

水*和垂直速度

首先,让我们将速度的水*和垂直分量分开。换句话说,我们为水*速度(图像向左或向右移动的速度)创建一个速度变量speedx,为垂直速度(图像向上或向下移动的速度)创建另一个速度变量speedy。我们可以通过在应用程序的设置部分将speed = 5修改为初始化speedxspeedy,如下所示:

speedx = 5
speedy = 5

然后,我们可以在游戏循环中修改图像位置的更新:

picx += speedx
picy += speedy

我们通过speedx(水*速度)来改变picx(水*或 x 位置),通过speedy(垂直速度)来改变picy(垂直或 y 位置)。

碰撞四个墙壁

最后需要解决的是屏幕四个边缘(上下左右)的边界碰撞检测。首先,让我们修改左右边界,以匹配新的屏幕大小(宽度为 800 像素),并使用新的水*速度speedx

if picx <= 0 or picx + pic.get_width() >= 800:
    speedx = -speedx

注意,左边界的情况仍然保持不变,即picx <= 0,因为当picx位于屏幕左侧时,0 仍然是左边界的值。不过,这次右边界的情况已经改为picx + pic.get_width() >= 800,因为我们的屏幕现在是 800 像素宽,而图像仍然从picx开始,然后向右绘制完整的宽度。所以,当picx + pic.get_width()等于 800 时,我们的笑脸就看起来像是碰到了绘图窗口的右侧。

我们稍微修改了左右边界触发的动作,将speed = -speed改为speedx = -speedx。现在我们有了两个速度分量,speedx将控制左右方向和速度(speedx的负值会让笑脸向左移动,正值则会让它向右移动)。所以,当笑脸碰到屏幕右边缘时,我们将speedx设为负值,让图像向左移动;当它碰到屏幕左边缘时,我们将speedx设为正值,让图像反弹回右边。

无标题的图像

让我们对picy做同样的处理:

if picy <= 0 or picy + pic.get_height() >= 600:
    speedy = -speedy

为了测试我们的笑脸图标是否碰到了屏幕的顶部边缘,我们使用picy <= 0,这与测试左边缘的picx <= 0类似。为了判断笑脸图标是否碰到了屏幕的底部边缘,我们需要知道绘图窗口的高度(600 像素)和图像的高度(pic.get_height()),然后判断图像的顶部picy加上图像的高度pic.get_height()是否超过了屏幕的高度 600 像素。

如果picy超出了上下边界,我们需要改变垂直速度的方向(speedy = -speedy)。这样,笑脸图标看起来就像是从窗口的底部反弹并向上移动,或者从顶部反弹并向下移动。

整合全部功能

当我们将整个程序整合到SmileyBounce2.py中时,我们得到了一个令人信服的弹跳球,它能够在运行应用程序时,反弹穿过屏幕的四个边缘。

SmileyBounce2.py

import pygame        # Setup
pygame.init()
screen = pygame.display.set_mode([800,600])
keep_going = True
pic = pygame.image.load("CrazySmile.bmp")
colorkey = pic.get_at((0,0))
pic.set_colorkey(colorkey)
picx = 0
picy = 0
BLACK = (0,0,0)
timer = pygame.time.Clock()
speedx = 5
speedy = 5

while keep_going:    # Game loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            keep_going = False
    picx += speedx
    picy += speedy

    if picx <= 0 or picx + pic.get_width() >= 800:
        speedx = -speedx
    if picy <= 0 or picy + pic.get_height() >= 600:
        speedy = -speedy

    screen.fill(BLACK)
    screen.blit(pic, (picx, picy))
    pygame.display.update()
    timer.tick(60)

pygame.quit()        # Exit

弹跳效果看起来很真实。如果笑脸图标以 45 度角朝着底部边缘靠*,并且向右下方移动,它会以 45 度角反弹,向右上方移动。你可以通过改变speedxspeedy的不同值(例如,35,或者74)来观察每次反弹时角度的变化。

纯粹为了好玩,你可以注释掉screen.fill(BLACK)这一行代码,在SmileyBounce2.py中查看笑脸图标在每次与屏幕边缘反弹时所走的路径。当你注释掉一行代码时,通过在行首加上井号,你可以将该行代码变成注释,像这样:

# screen.fill(BLACK)

这告诉程序忽略该行指令。现在,屏幕在每次绘制笑脸图标后不会被清除,你会看到一个由你的动画留下的轨迹,像在图 8-9 中一样形成的图案。由于每个新的笑脸图标覆盖在前一个图标上,最终的效果看起来像是酷炫的复古 3D 屏幕保护程序艺术作品。

我们的碰撞检测逻辑使得我们能够创建一个逼真的笑脸图标,在一个完整的绘图屏幕上反弹并与四个边缘碰撞。相比于我们原来的版本,笑脸图标不再滑向无尽的空白,而是反弹回去。我们在创建允许用户与屏幕上的元素进行交互的游戏时,就会使用类似的碰撞检测和边界检查技术,例如在《俄罗斯方块》游戏中,我们也使用了这种碰撞检测。

如果我们注释掉每帧后清除屏幕的那一行代码,笑脸图标在屏幕上留下一个有趣的弹跳轨迹。

图 8-9。如果我们注释掉每帧后清除屏幕的那一行代码,笑脸图标在屏幕上留下一个有趣的弹跳轨迹。

你学到的知识

在这一章中,你学会了如何通过在屏幕上不断地绘制图像来创造运动的错觉,我们称之为动画。我们看到 Pygame 模块可以让编程游戏或动画变得更快速,因为它有成百上千个函数,可以让游戏应用中的几乎每一件事都变得更加简单,从绘制图像到创建基于计时器的动画——甚至是检测碰撞。我们在计算机上安装了 Pygame,以便利用其功能创建我们自己的有趣应用程序。

你学到了我们可能在 Pygame 中构建的游戏或应用程序的结构,包括一个设置部分;一个处理事件、更新和绘制图形、更新显示的游戏循环;以及最后的退出部分。

我们通过在屏幕上指定位置绘制一个简单的绿色圆点来开始 Pygame 编程,但我们很快转向从磁盘上的图像文件绘制图片,图像文件保存在与程序相同的文件夹中,并将其显示到屏幕上。你学到 Pygame 与 Turtle 库使用不同的坐标系统,屏幕的左上角为原点(0, 0),y 坐标值随着向下移动而增大。

你学会了如何通过在屏幕上绘制物体、清除屏幕并在稍微不同的位置重新绘制物体来创建动画。我们发现pygame.time.Clock()对象可以通过限制每秒动画绘制次数来使我们的动画更加稳定,这个限制被称为每秒帧数,或fps

我们构建了自己的碰撞检测来检查物体是否“撞到”屏幕边缘,然后我们添加了逻辑,通过改变物体的速度或速度变量的方向(通过将它们乘以-1)使物体看起来像是弹回去的。

本章编写这些酷炫应用程序让我们掌握了以下技能:

  • 在我们的 Python 程序中安装并使用pygame模块。

  • 解释 Pygame 应用程序的结构,包括设置、游戏循环和退出部分。

  • 构建一个游戏循环,处理事件,更新和绘制图形,并更新显示。

  • 使用pygame.draw函数在屏幕上绘制形状。

  • 使用pygame.image.load()从磁盘加载图像。

  • 使用blit()函数将图像和物体绘制到屏幕上。

  • 通过不断地在不同的位置绘制物体来创建动画。

  • 使用pygame.time.Clock()计时器的tick()函数来限制每秒帧数,使动画变得*滑、整洁且可预测。

  • 通过构建if逻辑来检查边界情况,检测碰撞,比如图形碰到屏幕边缘时的情况。

  • 通过改变每帧中物体在 x 和 y 方向的移动量,控制物体在屏幕上水*和垂直的运动速度。

编程挑战

这里有三个挑战问题,可以扩展你在本章中学到的技能。样例答案请访问 www.nostarch.com/teachkids/

#1: 一个变色的点

让我们进一步探索 RGB 颜色三元组。本章中我们已经使用了一些 RGB 颜色;记住,绿色是(0,255,0),黑色是(0,0,0),等等。在 colorschemer.com/online/ 上,输入不同的红色、绿色和蓝色值(0 到 255 之间),你可以查看通过组合不同的红、绿、蓝光来创建的颜色。首先,选择一个你自己喜欢的颜色三元组来用于 ShowDot.py 程序。然后修改程序,使得点可以更大或更小,并且位于屏幕的不同位置。最后,尝试使用 random.randint(0,255) 为每个颜色分量创建一个随机的 RGB 颜色三元组(记得在程序顶部 import random),使得每次绘制时点的颜色都会变化。效果将是一个变色的点。将你的新程序命名为 DiscoDot.py

#2: 100 个随机点

作为第二个挑战,让我们用 100 个随机颜色、大小和位置的点替代单个点。为此,我们需要设置三个数组,分别能存储 100 个颜色、位置和大小的值:

# Colors, locations, sizes arrays for 100 random dots
colors = [0]*100
locations = [0]*100
sizes = [0]*100

然后,填充这三个数组,分别为 100 个随机点的颜色三元组、位置对和大小/半径值:

import random
# Store random values in colors, locations, sizes
for n in range(100):
    colors[n] = (random.randint(0,255),random.randint(0,255),
                 random.randint(0,255))
    locations[n] = (random.randint(0,800),
                    random.randint(0,600))
    sizes[n] = random.randint(10, 100)

最后,在我们的while循环中,不再绘制一个点,而是添加一个for循环,使用colorslocationssizes数组绘制 100 个随机点:

for n in range(100):
    pygame.draw.circle(screen, colors[n], locations[n],
                       sizes[n])

将你的新创建命名为RandomDots.py。最终的应用程序完成后应该类似于图 8-10。

我们点程序的高级版本,RandomDots.py,展示了 100 个随机颜色、位置和大小的点。

图 8-10. 我们的点程序的高级版本,RandomDots.py,展示了 100 个随机颜色、位置和大小的点。

#3: 下雨的点

最后,让我们将 RandomDots.py 进一步扩展,编程让这些点从屏幕的底部和右侧“下雨”,然后从顶部和左侧重新出现。你在本章中学到,通过随着时间变化改变物体的位置来创建动画。我们将每个点的位置保存在一个名为 locations 的数组中,所以如果我们改变每个点的 x 和 y 坐标,就能实现点的动画。将 RandomDots.py 中的 for 循环更改为,根据之前的值计算每个点的新的 x 和 y 坐标,如下所示:

for n in range(100):
    pygame.draw.circle(screen, colors[n], locations[n],
                       sizes[n])
    new_x = locations[n][0] + 1
    new_y = locations[n][1] + 1
    locations[n] = (new_x, new_y)

这个变化会在每次游戏循环中为每个点计算新的 x 和 y 坐标(new_xnew_y),但它允许这些点从屏幕的右侧和底部掉落。我们可以通过检查每个点的new_xnew_y是否超出了屏幕的右边或底边来修复这个问题,如果是的话,就将点移回到屏幕的顶部或左侧,然后再存储新的位置:

if new_x > 800:
    new_x -= 800
if new_y > 600:
    new_y -= 600
locations[n] = (new_x, new_y)

这些变化的综合效果是,随机点会稳定地“下落”并向右移动,消失在屏幕的右下角,然后从顶部或左侧重新出现。在图 8-11 中展示了这一序列的四帧图像;你可以跟踪这些点群在三张图像中向下和向右移动的过程。

将你的新应用保存为RainingDots.py

四帧图像展示了 100 个随机点随着时间向右和向下移动穿过屏幕

图 8-11. 四帧图像展示了 100 个随机点随着时间向右和向下移动穿过屏幕

第九章 用户交互:进入游戏

在第八章中,我们使用了 Pygame 库的一些功能在屏幕上绘制形状和图像。我们还能够通过在不同的位置随时间绘制形状来创建动画。不幸的是,我们无法像在游戏中那样动画对象进行交互;我们希望能够点击、拖动、移动、击打或弹出屏幕上的对象,从而影响或控制游戏的元素。

交互式程序给我们带来了在应用或游戏中控制的感觉,因为我们可以移动或与程序中的角色或其他对象互动。这正是你将在本章中学到的内容:我们将利用 Pygame 处理来自鼠标的用户交互,使我们的程序更加互动,更具吸引力。

添加交互:点击和拖动

让我们通过开发两个程序来添加用户交互,允许用户在屏幕上进行互动绘制。首先,我们将在 Pygame 基础上构建程序,处理鼠标按钮点击等事件,并让用户在屏幕上绘制点。然后,我们将添加逻辑,单独处理鼠标按钮的按下和释放,并让用户在按住按钮的情况下拖动鼠标进行绘制,类似于绘图程序。

点击绘制点

我们将使用与ShowPic.py相同的步骤来构建ClickDots.py程序(退出程序),包括设置、游戏循环和退出。请特别注意游戏循环中的事件处理部分,因为我们将在其中添加if语句来处理鼠标点击。

设置

以下是我们设置的前几行代码。创建一个新文件,并将其保存为ClickDots.py(最终程序见将所有内容整合)。

import pygame                           # Setup
pygame.init()
screen = pygame.display.set_mode([800,600])
pygame.display.set_caption("Click to draw")

我们的设置首先是像往常一样import pygamepygame.init(),然后我们创建一个screen对象作为我们的绘图窗口显示。这一次,我们给窗口添加了一个标题或字幕,使用了pygame.display.set_caption()。这可以让用户知道这个程序是什么。我们传递给set_caption()的参数是一个字符串,显示在窗口的标题栏上,如图 9-1 所示。

ClickDots.py 顶部的标题栏告诉用户,“点击以绘制。”

图 9-1. ClickDots.py顶部的标题栏告诉用户,“点击以绘制。”

我们的其余设置将创建我们的游戏循环变量keep_going,设置一个颜色常量(我们将在本程序中用红色绘制),并为我们的绘图点创建一个半径:

keep_going = True
RED = (255,0,0)                     # RGB color triplet for RED
radius = 15

现在,让我们继续我们的游戏循环。

游戏循环:处理鼠标点击

在我们的游戏循环中,我们需要告诉程序何时退出以及如何处理鼠标按钮按下事件:

   while keep_going:                      # Game loop
       for event in pygame.event.get():   # Handling events
➊         if event.type == pygame.QUIT:
               keep_going = False
➋         if event.type == pygame.MOUSEBUTTONDOWN:
➌             spot = event.pos
➍             pygame.draw.circle(screen, RED, spot, radius)

在➊处,我们通过将循环变量keep_going设置为False来处理pygame.QUIT事件。

第二个if语句,在➋处,处理了一种新的事件类型:pygame.MOUSEBUTTONDOWN事件,它告诉我们用户按下了鼠标按钮中的一个。每当用户按下鼠标按钮时,这个事件将出现在我们通过pygame.event.get()获取的事件列表中,我们可以使用if语句来检查事件并告诉程序在事件发生时该做什么。在➌处,我们创建了一个名为spot的变量,用来存储鼠标位置的 x 和 y 坐标。我们可以通过event.pos获取鼠标点击事件的位置;event是我们for循环中的当前事件。我们的if语句已经验证了这个特定的eventpygame.MOUSEBUTTONDOWN类型,而鼠标事件有一个pos属性(在本例中为event.pos),它存储了告诉我们鼠标事件发生位置的(x, y)坐标对。

一旦我们知道用户点击鼠标按钮时屏幕上的位置,在➍处我们告诉程序在screen表面上,以我们设置中的RED颜色,在spot位置绘制一个填充圆圈,半径为我们设置中指定的 15。

没有标题的图像

综合起来

剩下要做的就是更新显示并告诉我们的程序在退出时应该做什么。下面是ClickDots.py的完整程序。

ClickDots.py

import pygame                           # Setup
pygame.init()
screen = pygame.display.set_mode([800,600])
pygame.display.set_caption("Click to draw")
keep_going = True
RED = (255,0,0)                         # RGB color triplet for RED radius = 15

while keep_going:                       # Game loop
    for event in pygame.event.get():    # Handling events
        if event.type == pygame.QUIT:
            keep_going = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            spot = event.pos
            pygame.draw.circle(screen, RED, spot, radius)
    pygame.display.update()             # Update display

pygame.quit()                           # Exit

这个程序很简短,但它允许用户一笔一画地绘制图片,正如图 9-1 中所示。如果我们希望在按住鼠标按钮时拖动鼠标时连续绘制,我们只需要处理一种新的鼠标事件,即pygame.MOUSEBUTTONUP。让我们试试看。

拖动绘画

现在让我们创建一个更自然的绘画程序,DragDots.py,它允许用户点击并拖动以*滑地绘制,就像使用画笔一样。我们将得到一个*滑的互动绘画应用,如图 9-2 所示。

我们的 DragDots.py 程序是一个有趣的绘画方式!

图 9-2。我们的DragDots.py程序是一个有趣的绘画方式!

为了实现这个效果,我们需要修改程序的逻辑。在ClickDots.py中,我们通过在鼠标按下事件的位置绘制一个圆圈来处理MOUSEBUTTONDOWN事件。为了实现连续绘制,我们需要同时识别MOUSEBUTTONDOWNMOUSEBUTTONUP事件;换句话说,我们需要将鼠标点击事件分为按下释放,以便知道鼠标何时在拖动(按下按钮)以及仅在按钮弹起时被移动。

一种实现方法是使用另一个布尔标志变量。我们可以在用户按下鼠标按钮时将名为mousedown的布尔值设置为True,而在用户释放鼠标按钮时将其设置为False。在我们的游戏循环中,如果鼠标按钮被按下(换句话说,当mousedownTrue时),我们可以获取鼠标的位置并在屏幕上绘制圆圈。如果程序足够快,绘制应该像画笔应用程序那样流畅。

没有标题的图片

设置

让你的代码设置部分看起来像这样:

   import pygame                           # Setup
   pygame.init()
   screen = pygame.display.set_mode([800,600])
➊ pygame.display.set_caption("Click and drag to draw")
   keep_going = True
➋ YELLOW = (255,255,0)                    # RGB color triplet for YELLOW
   radius = 15
➌ mousedown = False

我们应用程序的设置部分看起来像ClickDots.py,除了不同的窗口标题 ➊,我们将使用的YELLOW颜色 ➋,以及最后一行 ➌。布尔变量mousedown将作为标志变量,告诉程序鼠标按钮已按下或被按住。

接下来,我们将在游戏循环中添加事件处理程序。这些事件处理程序将在用户按住鼠标时将mousedown设置为True,而在没有按住时将其设置为False

游戏循环:处理鼠标按下和释放

让你的游戏循环看起来像这样:

   while keep_going:                        # Game loop
       for event in pygame.event.get():     # Handling events
           if event.type == pygame.QUIT:
               keep_going = False
➊         if event.type == pygame.MOUSEBUTTONDOWN:
➋             mousedown = True
➌         if event.type == pygame.MOUSEBUTTONUP:
➍             mousedown = False
➎     if mousedown:                         # Draw/update graphics
➏         spot = pygame.mouse.get_pos()
➐         pygame.draw.circle(screen, YELLOW, spot, radius)
➑     pygame.display.update()               # Update display

游戏循环开始就像我们其他的 Pygame 应用程序一样,但在 ➊ 处,当我们检查用户是否按下了鼠标按钮时,程序不会立即绘制,而是将mousedown变量设置为True ➋。这将是程序开始绘制所需的信号。

下一个在 ➌ 处的if语句检查用户是否已释放鼠标按钮。如果是,那么 ➍ 处的代码将mousedown重置为False。这将让我们的游戏循环知道,当鼠标按钮弹起时停止绘制。

在 ➎ 处,我们的for循环结束(如缩进所示),然后我们的游戏循环继续,通过检查鼠标按钮是否当前被按下(也就是说,mousedown是否为True)。如果鼠标按钮被按下,说明鼠标正在被拖动,我们希望允许用户在screen上绘制。

在 ➏,我们直接获取鼠标的当前位置,使用 spot = pygame.mouse.get_pos(),而不是获取最后一次点击的位置,因为我们希望在用户拖动鼠标的任何位置绘制,而不仅仅是在他们第一次按下按钮的位置。在 ➐,我们在 screen 表面上绘制当前的圆圈,颜色由 YELLOW 指定,位于鼠标当前被拖动的 (x, y) 位置 spot,半径为 15,这是我们在代码设置部分指定的。最后,在 ➑,我们通过 pygame.display.update() 更新显示窗口来结束游戏循环。

综合起来

最后一步是像往常一样使用 pygame.quit() 结束程序。下面是完整的程序。

DragDots.py

import pygame                           # Setup
pygame.init()
screen = pygame.display.set_mode([800,600])
pygame.display.set_caption("Click and drag to draw")
keep_going = True
YELLOW = (255,255,0)                    # RGB color triplet for YELLOW
radius = 15
mousedown = False

while keep_going:                       # Game loop
    for event in pygame.event.get():    # Handling events
        if event.type == pygame.QUIT:
            keep_going = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            mousedown = True
        if event.type == pygame.MOUSEBUTTONUP:
            mousedown = False
    if mousedown:                       # Draw/update graphics
        spot = pygame.mouse.get_pos()
        pygame.draw.circle(screen, YELLOW, spot, radius)
    pygame.display.update()             # Update display

pygame.quit()                           # Exit

DragDots.py 应用程序非常快速且响应灵敏,以至于它几乎让我们感觉是在用连续的画笔而不是一系列点进行绘制;我们需要快速拖动鼠标才能看到点被单独绘制出来。Pygame 使我们能够比前几章中绘制的海龟图形构建更快速、更流畅的游戏和动画。

尽管 for 循环在每次通过保持应用程序打开的 while 循环时处理每一个事件,Pygame 的效率足够高,能够每秒执行几十次甚至几百次。这产生了即时运动和对我们每个动作和命令反应的错觉——这是我们构建动画和互动游戏时的重要考虑因素。Pygame 能够应对这一挑战,是满足我们图形密集型需求的正确工具包。

高级互动:笑脸爆炸

我的学生和儿子们喜欢构建的一个有趣动画是 SmileyBounce2.py 的放大版,名为 SmileyExplosion.py。它通过允许用户点击并拖动,创建出数百个大小随机、运动方向随机、速度随机的笑脸气球,将弹跳的笑脸提升到了一个有趣的新层次。这个效果就像是 图 9-3。我们将一步步构建这个程序;最终版本在 综合起来 中。

我们的下一个应用程序看起来像一场笑脸气球的爆炸,在屏幕上四处弹跳。

图 9-3. 我们的下一个应用程序看起来像一场笑脸气球的爆炸,在屏幕上四处弹跳。

正如你所见,我们将有几十到上百个笑脸气球在任何时候在屏幕上四处弹跳,因此我们需要快速且*滑地为每一帧上的数百个物体绘制图形。为此,我们将向工具包中添加一个新工具:精灵图形。

笑脸精灵

精灵这个术语可以追溯到视频游戏的早期。屏幕上移动的图形对象被称为精灵,因为它们像命名源于的幻想精灵一样漂浮在背景之上。这些轻巧、快速的精灵图形使得快速、流畅的动画成为可能,从而让视频游戏变得如此受欢迎。

Pygame 通过其pygame.sprite.Sprite类支持精灵图形。请记住,在第八章中,类就像一个模板,可以用来创建可重用的对象,每个对象都有自己完整的功能和属性。在 SmileyMove.py 中,我们使用了Clock类及其tick()方法,使我们的动画变得*滑和可预测。在笑脸爆炸应用程序中,我们将使用几个方便的 Pygame 类,并创建一个自己的类来跟踪每个笑脸在屏幕上的移动。

无标题的图片

更多关于类和对象的内容

在第八章中,你学到了类就像饼干模具,对象就像用特定模具做出的饼干。每当我们需要几个具有相似功能和特征的项目(如具有不同大小和位置的移动笑脸图像),尤其是当我们需要每个项目包含不同信息(如每个笑脸的大小、位置和速度)时,类可以提供模板,帮助我们创建任意数量的该类型对象。我们说对象是某一特定类的实例

Pygame 库有几十个可重用的类,每个类都有自己的方法(我们称之为类的函数)和属性数据,即存储在每个对象中的变量和值。在第八章中的Clock类,tick()方法是我们用来使动画以特定帧率发生的函数。在这个应用程序中,针对浮动的笑脸Sprite对象,我们关心的属性包括每个笑脸在屏幕上的位置、它的大小,以及它在 x 轴和 y 轴方向上的移动速度,因此我们将创建一个具有这些属性的Smiley类。我们可以随时创建自己的类来作为可重用的模板。

将问题或程序拆解成对象,然后构建创建这些对象的类,是面向对象编程的基础。面向对象编程是一种利用对象解决问题的方法。它是软件开发中最流行的编程方式之一,流行的原因之一是代码重用的概念。重用性意味着一旦我们为一个编程项目编写了一个有用的类,我们通常可以在另一个程序中重用该类,而无需从头开始。例如,一个游戏公司可以编写一个Card类来表示标准扑克牌中的卡片。然后,每次公司编写一个新游戏——比如二十一点、战争、扑克、鱼来来等——它都可以重用这个Card类,通过在未来的应用程序中使用相同的代码来节省时间和金钱。

Pygame 中的Sprite类是一个很好的例子。Pygame 团队编写了Sprite类,包含了我们在编程游戏对象时需要的许多功能,从跑步角色到宇宙飞船,再到漂浮的微笑表情。通过使用Sprite类,我们这些程序员不再需要编写所有基本代码来在屏幕上绘制一个对象、检测对象之间的碰撞等。Sprite类为我们处理了这些功能,我们可以专注于在这个基础上构建应用的独特特性。

另一个方便的 Pygame 类是Group类。Group是一个容器类,它让我们将多个Sprite对象作为一个组存储在一起。Group类帮助我们将所有精灵保存在一个地方(通过一个Group对象访问),当我们有数十个甚至数百个精灵在屏幕上漂浮时,这一点非常重要。Group类还提供了方便的方法,用于更新组内所有精灵(例如将Sprite对象移动到它们的新位置),添加新的Sprite对象,从Group中删除Sprite对象,等等。让我们看看如何利用这些类来构建我们的微笑爆炸应用。

使用类来构建我们的应用

我们将为我们的微笑气球创建Sprite对象,利用Sprite类的属性在屏幕上实现快速动画,即使成百上千的精灵在同一帧中被移动。我提到过,Pygame 也支持精灵组,所有精灵可以作为一个集合一起绘制和处理;这个精灵组的类型是pygame.sprite.Group()。让我们来看看应用的设置部分:

   import pygame
   import random

   BLACK = (0,0,0)
   pygame.init()
   screen = pygame.display.set_mode([800,600])
   pygame.display.set_caption("Smiley Explosion")
   mousedown = False
   keep_going = True
   clock = pygame.time.Clock()
   pic = pygame.image.load("CrazySmile.bmp")
   colorkey = pic.get_at((0,0))
   pic.set_colorkey(colorkey)
➊ sprite_list = pygame.sprite.Group()

设置看起来像SmileyBounce2.py,但我们在➊添加了一个名为sprite_list的变量,它将包含我们的微笑表情精灵组。将精灵存储在Group中将使得执行类似于每帧绘制所有微笑、每步动画移动所有微笑,甚至检查微笑精灵是否与物体或彼此碰撞的操作更加快捷和容易。

为了创建用于复杂动画和游戏的精灵对象,我们将创建我们自己的Sprite类,它扩展了 Pygame 的Sprite类,添加我们为自定义精灵所需要的变量和函数。我们将命名我们的精灵类为Smiley,并添加变量来表示每个笑脸的位置(pos)、其 x 和 y 速度(xvelyvel,记住速度是指速度的另一个词),以及其缩放,即每个笑脸的大小(scale):

class Smiley(pygame.sprite.Sprite):
    pos = (0,0)
    xvel = 1
    yvel = 1
    scale = 100

我们的Smiley类定义以class关键字开始,后跟我们为类命名的名称,以及我们要扩展的类型(pygame.sprite.Sprite)。

设置精灵

在开始我们的Smiley类并列出我们希望每个笑脸精灵对象记住的数据变量后,接下来的步骤称为初始化,有时也称为我们类的构造函数。这将是一个特殊的函数,每次在程序中创建一个新的Smiley类对象时都会被调用,或者说是构造。就像初始化变量会给它一个起始值一样,Smiley类中的初始化函数__init__()将为我们的精灵对象设置所有所需的起始值。__init__()函数名两边的两个下划线在 Python 中有特殊含义。在这个情况下,__init__()是用于初始化类的特殊函数名称。我们在这个函数中告诉 Python 每个Smiley对象应该如何初始化,每次我们创建一个Smiley时,这个特殊的__init__()函数都会在后台执行其任务,为每个Smiley对象设置变量等。

image with no caption

我们需要在__init__()函数中设置多个项目。首先,我们需要确定传递给__init__()函数的参数。对于我们的随机笑脸,我们可能会传递一个位置和起始的 x 和 y 速度。因为我们的Smiley是一个类,所有的笑脸精灵都会是Smiley类型的对象,所以类中所有函数的第一个参数都会是笑脸精灵对象本身。我们把这个参数命名为self,因为它将__init__()和其他函数与对象自身的数据连接起来。请看一下我们的__init__()函数的代码:

   def __init__(self, pos, xvel, yvel):
➊     pygame.sprite.Sprite.__init__(self)
➋     self.image = pic
      self.rect = self.image.get_rect()
➌    self.pos = pos
➍    self.rect.x = pos[0] - self.scale/2
      self.rect.y = pos[1] - self.scale/2
➎    self.xvel = xvel
      self.yvel = yvel

我们的__init__()函数有四个参数,分别是对象本身self、我们希望笑脸出现的位置pos,以及xvelyvel,即其水*和垂直的速度值。接下来,在➊处,我们调用主Sprite类的初始化函数,这样我们的对象就可以利用精灵图形的属性,而无需从头开始编写这些属性。在➋处,我们将精灵对象的图像(self.image)设置为从磁盘加载的pic图形(CrazySmile.bmp——你需要确保该文件仍然与这个新程序在同一个文件夹中),然后我们获取包含 100×100 图片的矩形的尺寸。

在➌处,语句self.pos = pos将传递给__init__()函数的位置存储在对象自己的pos变量中。然后,在➍处,我们将精灵绘制矩形的 x 和 y 坐标设置为存储在pos中的 x 和 y 坐标,并将它们按图像大小的一半(self.scale/2)进行偏移,以便使笑脸在用户点击的地方居中。最后,在➎处,我们将传递给__init__()函数的 x 和 y 速度值存储在对象的xvelyvel变量中(self.xvelself.yvel)。

这个__init__()构造函数将设置我们在屏幕上绘制每个笑脸所需的一切,但它并没有处理需要将精灵移动到屏幕上所需的动画。为此,我们将为我们的精灵添加另一个有用的函数,update()

更新精灵

精灵是为动画设计的,我们已经了解到,动画意味着每一帧(每次通过游戏循环时)更新图形的位置。Pygame 的精灵自带一个update()函数,我们可以重写或自定义这个函数,以编写我们希望从自定义精灵中获得的行为。

我们的update()函数相当简单;每帧更新我们弹跳的笑脸精灵的唯一方式就是根据其速度更改精灵的位置,并检查它是否与屏幕边缘发生碰撞:

def update(self):
    self.rect.x += self.xvel
    self.rect.y += self.yvel
    if self.rect.x <= 0 or self.rect.x > screen.get_width() - self.scale:
        self.xvel = -self.xvel
    if self.rect.y <= 0 or self.rect.y > screen.get_height() - self.scale:
        self.yvel = -self.yvel

update()函数有一个参数——精灵对象本身self——并且移动精灵的代码看起来很像我们在SmileyBounce2.py中的动画代码。唯一的真正区别是,我们通过self.rect.xself.rect.y引用精灵的(xy)位置,将 x 和 y 的速度分别称为self.xvelself.yvel。我们的屏幕边界碰撞检测也使用了screen.get_width()screen.get_height(),因此它们可以适用于任何大小的窗口。

更大和更小的笑脸

我们将为这个应用程序的第一个版本添加的最后一个功能是改变图像的比例,即大小。我们将在设置self.imagepic后,修改__init__()函数。首先,我们将对象的scale变量更改为 10 到 100 之间的随机数(用于生成一个大小在 10×10 到 100×100 像素之间的完整笑脸精灵)。我们将使用pygame.transform.scale()函数应用这一比例变换(也称为转换),代码如下:

self.scale = random.randrange(10,100)
self.image = pygame.transform.scale(self.image, (self.scale,self.scale))

Pygame 的transform.scale()函数接受一个图像(我们的笑脸图像self.image)和新的尺寸(我们新的随机self.scale值作为图像的宽度和高度),并返回缩放后的(放大或缩小)图像,我们将其存储为新的self.image

通过这个最后的修改,我们现在应该能够使用我们的Smiley精灵类在屏幕上绘制不同大小和速度的笑脸,绘制代码与我们的DragDots.py绘图应用类似,并做了一些修改。

没有标题的图片

综合总结

这是我们的完整SmileyExplosion.py应用:

SmileyExplosion.py

   import pygame
   import random

   BLACK = (0,0,0)
   pygame.init()
   screen = pygame.display.set_mode([800,600])
   pygame.display.set_caption("Smiley Explosion")
   mousedown = False
   keep_going = True
   clock = pygame.time.Clock()
   pic = pygame.image.load("CrazySmile.bmp")
   colorkey = pic.get_at((0,0))
   pic.set_colorkey(colorkey)
   sprite_list = pygame.sprite.Group()

   class Smiley(pygame.sprite.Sprite):
       pos = (0,0)
       xvel = 1
       yvel = 1
       scale = 100

       def __init__(self, pos, xvel, yvel):
           pygame.sprite.Sprite.__init__(self)
           self.image = pic
           self.scale = random.randrange(10,100)
           self.image = pygame.transform.scale(self.image, (self.scale,self.scale))
           self.rect = self.image.get_rect()
           self.pos = pos
           self.rect.x = pos[0] - self.scale/2
           self.rect.y = pos[1] - self.scale/2
           self.xvel = xvel
           self.yvel = yvel

      def update(self):
          self.rect.x += self.xvel
          self.rect.y += self.yvel
          if self.rect.x <= 0 or self.rect.x > screen.get_width() - self.scale:
              self.xvel = -self.xvel
          if self.rect.y <= 0 or self.rect.y > screen.get_height() - self.scale:
              self.yvel = -self.yvel
   while keep_going:
       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               keep_going = False
           if event.type == pygame.MOUSEBUTTONDOWN:
               mousedown = True
           if event.type == pygame.MOUSEBUTTONUP:
               mousedown = False
       screen.fill(BLACK)
➊     sprite_list.update()
➋     sprite_list.draw(screen)
       clock.tick(60)
       pygame.display.update()
       if mousedown:
           speedx = random.randint(-5, 5)
           speedy = random.randint(-5, 5)
➌         newSmiley = Smiley(pygame.mouse.get_pos(),speedx,speedy)
➍         sprite_list.add(newSmiley)

pygame.quit()

SmileyExplosion.py中的游戏循环代码与我们的绘图应用DragDots.py相似,但有一些显著的变化。在➊处,我们对存储在sprite_list中的笑脸精灵列表调用update()函数;这一行代码将调用更新函数,移动每个屏幕上的笑脸并检查边缘反弹。类似地,➋处的代码将在屏幕上按正确位置绘制每个笑脸。只需两行代码就可以对数百个精灵进行动画处理和绘制——这节省了大量时间,也是 Pygame 精灵图形强大功能的一部分。

在我们的mousedown绘图代码中,我们为每个新笑脸生成一个随机的speedxspeedy,用于控制笑脸在水*和垂直方向的速度,在➌处,我们通过调用Smiley类的构造函数来创建一个新的笑脸newSmiley。注意,我们不需要使用函数名__init__();相反,我们在构造或创建新的Smiley类或类型对象时,直接使用类名Smiley。我们将鼠标的位置和刚刚创建的随机速度传递给构造函数。最后,在➍处,我们将新创建的笑脸精灵newSmiley添加到我们的精灵组 sprite_list中。

我们刚刚创建了一个快速、流畅、互动的动画,可以展示数十个甚至数百个笑脸精灵图形,它们像气球一样在屏幕上漂浮,大小不一,随机速度四处移动。在这个应用程序的最后一次升级中,我们将看到一个更令人印象深刻、更强大的精灵图形功能,它能处理碰撞检测。

SmileyPop,第 1.0 版

对于我们的结尾示例,我们将在 SmileyExplosion.py 程序中添加一个重要的有趣功能:通过点击右键(或者在 Mac 上按住 CONTROL 键并点击)来“爆破”笑脸气球/泡泡。这个效果类似于气球爆破游戏、蚂蚁打击、打地鼠等。我们可以通过拖动左键创建笑脸气球,然后通过右键点击一个或多个笑脸精灵来将其“爆破”(即从屏幕上移除)。

检测碰撞并移除精灵

好消息是,Pygame 中的 Sprite 类内置了碰撞检测功能。我们可以使用 pygame.sprite.collide_rect() 函数检查两个精灵的矩形是否相撞;我们可以使用 collide_circle() 函数检查两个圆形精灵是否接触;如果我们只是检查一个精灵是否与一个单一的点(例如用户刚刚点击的鼠标像素)发生碰撞,我们可以使用精灵的 rect.collidepoint() 函数来检查精灵是否与该点重叠或碰撞。

如果我们已经确定用户点击了一个触及一个或多个精灵的点,我们可以通过调用 remove() 函数将这些精灵从我们的 sprite_list 组中移除。我们可以在 MOUSEBUTTONDOWN 事件处理程序代码中处理所有爆破笑脸气球的逻辑。为了将 SmileyExplosion.py 转变为 SmileyPop.py,我们只需替换这两行:

if event.type == pygame.MOUSEBUTTONDOWN:
    mousedown = True

使用以下七行代码:

         if event.type == pygame.MOUSEBUTTONDOWN:
➊           if pygame.mouse.get_pressed()[0]:    # Regular left mouse button, draw
                 mousedown = True
➋       elif pygame.mouse.get_pressed()[2]:  # Right mouse button, pop
➌           pos = pygame.mouse.get_pos()
➍           clicked_smileys = [s for s in sprite_list if s.rect.collidepoint(pos)]
➎           sprite_list.remove(clicked_smileys)

MOUSEBUTTONDOWN 事件的 if 语句保持不变,但现在我们关心的是哪个按钮被按下。在 ➊ 处,我们检查是否按下了鼠标按钮(第一个按钮,索引为 [0]);如果是,我们启用 mousedown 布尔标志,游戏循环将绘制新的笑脸。在 ➋ 处,我们检查是否按下了鼠标按钮,这开始了检查鼠标是否点击了我们 sprite_list 中一个或多个笑脸的逻辑。

首先,在 ➌ 处,我们获取鼠标的位置并将其存储在变量pos中。在 ➍ 处,我们使用编程快捷方式生成一个来自 sprite_list 的精灵列表,这些精灵与用户点击的位置 pos 相交或重叠。如果 sprite_list 中的某个精灵 s 的矩形与 pos 点相交,则将其组合为一个列表 [s] 并将该列表存储为 clicked_smileys。根据 if 条件从另一个列表、集合或数组中创建一个列表的能力是 Python 的一个强大特性,它使得我们的代码在这个应用中变得更加简洁。

最后,在 ➎ 处,我们调用了我们精灵列表 sprite_list 上的便捷 remove() 函数。这个 remove() 函数不同于 Python 的常规 remove() 函数,后者从列表或集合中移除一个单独的项。而 pygame.sprite.Group.remove() 函数可以从列表中移除任意数量的精灵。在这种情况下,它将移除所有与用户点击屏幕的点发生碰撞的精灵。一旦这些精灵从 sprite_list 中移除,当 sprite_list 在游戏循环中绘制到屏幕上时,被点击的精灵将不再出现在列表中,因此不会被绘制出来。就像它们消失了一样——或者我们像气球或泡泡一样把它们“弹出”!

没有标题的图像

综合起来

这是完整的 SmileyPop.py 代码。

SmileyPop.py

import pygame
import random

BLACK = (0,0,0)
pygame.init()
screen = pygame.display.set_mode([800,600])
pygame.display.set_caption("Pop a Smiley")
mousedown = False
keep_going = True
clock = pygame.time.Clock()
pic = pygame.image.load("CrazySmile.bmp")
colorkey = pic.get_at((0,0))
pic.set_colorkey(colorkey)
sprite_list = pygame.sprite.Group()

class Smiley(pygame.sprite.Sprite):
    pos = (0,0)
    xvel = 1
    yvel = 1
    scale = 100

    def __init__(self, pos, xvel, yvel):
        pygame.sprite.Sprite.__init__(self)
        self.image = pic
        self.scale = random.randrange(10,100)
        self.image = pygame.transform.scale(self.image, (self.scale,self.scale))
        self.rect = self.image.get_rect()
        self.pos = pos
        self.rect.x = pos[0] - self.scale/2
        self.rect.y = pos[1] - self.scale/2
        self.xvel = xvel
        self.yvel = yvel

    def update(self):
        self.rect.x += self.xvel
        self.rect.y += self.yvel
        if self.rect.x <= 0 or self.rect.x > screen.get_width() - self.scale:
            self.xvel = -self.xvel
        if self.rect.y <= 0 or self.rect.y > screen.get_height() - self.scale:
            self.yvel = -self.yvel
while keep_going:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            keep_going = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            if pygame.mouse.get_pressed()[0]:   # Regular left mouse button, draw
                mousedown = True
            elif pygame.mouse.get_pressed()[2]: # Right mouse button, pop
                pos = pygame.mouse.get_pos()
                clicked_smileys = [s for s in sprite_list if s.rect.collidepoint(pos)]
                sprite_list.remove(clicked_smileys)
        if event.type == pygame.MOUSEBUTTONUP:
            mousedown = False
    screen.fill(BLACK)
    sprite_list.update()
    sprite_list.draw(screen)
    clock.tick(60)
    pygame.display.update()
    if mousedown:
        speedx = random.randint(-5, 5)
        speedy = random.randint(-5, 5)
        newSmiley = Smiley(pygame.mouse.get_pos(),speedx,speedy)
        sprite_list.add(newSmiley)

pygame.quit()

记住,你需要将 CrazySmile.bmp 图像文件与代码存储在同一个文件夹或目录中才能使其正常工作。一旦它正常工作,这个程序玩起来非常有趣,几乎让人上瘾!在下一章中,我们将学习使游戏有趣的游戏设计元素,并从零开始制作一个完整的游戏!

你学到了什么

在本章中,我们将用户交互与动画结合起来,在屏幕上创造了一个笑脸的爆炸效果,并使用精灵图形使得成百上千个笑脸图像的动画制作变得轻松而快速。我们学习了如何构建自己的 Sprite 类,以便自定义精灵的特性和行为,包括数据变量、初始化函数和自定义更新函数。我们还学习了如何在 Pygame 中缩放图像,以便我们的笑脸可以有各种不同的形状和大小,并学习了使用 pygame.sprite.Group() 来存储所有精灵,以便快速更新和绘制到屏幕上的优势。

在我们的结束示例中,我们添加了基于精灵的碰撞检测,看看用户是否在一个或多个笑脸精灵上右键单击了鼠标。我们展示了如何分别检测鼠标左键和右键的事件。我们还学会了 Python 在根据 if 条件从列表中选择项目时的强大功能,并展示了如何使用 remove() 函数从 Group 中移除精灵。

在本章中,我们创建了有趣的应用程序,最后制作了一个 SmileyPop 应用程序,在第十章中我们将把它变得更加像游戏。Pygame 给我们提供了编写超酷游戏所需的最终技能!

本章中编写的酷应用程序赋予了我们以下技能:

  • 通过自定义 pygame.sprite.Sprite() 类使用精灵图形。

  • 使用 pygame.sprite.Group() 及其函数访问、修改、更新并绘制精灵列表。

  • 通过应用 pygame.trasform.scale() 函数来转换图像,增加或减少图像的像素大小。

  • 使用 rect.collidepoint()Sprite 类的类似函数来检测精灵碰撞。

  • 使用 remove() 函数从 Group 中移除精灵。

编程挑战

这里有三个挑战问题,可以扩展本章所开发的技能。对于这些挑战的示例答案,请访问 www.nostarch.com/teachkids/

#1: 随机颜色的点

从选择你自己的颜色三元组开始,用于 DragDots.py 程序。然后修改程序,创建一个由三个在 0 到 255 之间的随机数字组成的三元组作为颜色,来绘制随机颜色的点。将你新创作的程序命名为 RandomPaint.py

#2: 用颜色绘画

让用户使用以下任意选项,在两种或更多种一致的颜色中绘制:

  • 每当用户按下一个键时,更改当前的绘图颜色,可以是每次随机颜色,或者对于某些特定的按键(如 R 为红色,B 为蓝色,等等),将颜色更改为特定颜色。

  • 根据不同的鼠标按钮使用不同的颜色进行绘画(例如,左键为红色,中键为绿色,右键为蓝色)。

  • 在屏幕底部或侧面添加一些彩色矩形,并修改程序,使得如果用户点击某个矩形,绘图颜色就会改变为该矩形的颜色。

尝试一种方法,或者三种方法都尝试,并将你的新文件保存为 ColorPaint.py

#3: 扔笑脸

Pygame 有一个名为 pygame.mouse.get_rel() 的函数,它会返回 相对 移动的量,或者说自上次调用 get_rel() 以来,鼠标位置在 x 轴和 y 轴方向上变化的像素数。修改你的 SmileyExplosion.py 文件,使用相对鼠标移动的量作为每个笑脸的水*和垂直速度(而不是生成一对随机的 speedxspeedy 值)。这看起来像是用户正在扔笑脸,因为它们会朝着用户拖动鼠标的方向飞速移动!

为了增加另一个现实效果,通过将 xvelyvel 乘以小于 1.0 的数字(如 0.95),使得每次笑脸碰到屏幕边缘时,笑脸的速度稍微变慢。笑脸会随着时间的推移而变慢,就像每次碰到墙壁的摩擦力让它们的速度逐渐减慢一样。将你新制作的应用保存为 SmileyThrow.py

第十章。游戏编程:为娱乐而编码

在第九章中,我们结合了动画和用户交互,制作了一个有趣的应用程序。在本章中,我们将在这些概念的基础上,加入游戏设计元素,从零开始创建一款游戏。我们将结合绘制屏幕动画的能力和处理用户交互(如鼠标移动)的能力,创建一款经典的 Pong 类型游戏,我们将其命名为微笑 Pong

我们喜欢玩的游戏通常包含某些游戏设计元素。以下是我们微笑 Pong 设计的分解:

  • 游戏场地或棋盘。黑色屏幕代表乒乓球板的一半。

  • 目标和成就。玩家尝试得分并避免失去生命。

  • 游戏角色(游戏人物和物体)。玩家有一个球和一个挡板。

  • 规则。如果球击中挡板,玩家得分;但如果球击中屏幕底部,玩家会失去一条生命。

  • 机制。我们将通过鼠标控制挡板左右移动,防守屏幕底部;随着游戏进程,球的速度可能会增加。

  • 资源。玩家将有五条生命或五次机会来尽可能多地得分。

没有标题的图片

游戏通过这些元素来吸引玩家。一款有效的游戏将这些元素混合使用,使游戏容易上手,但又具有挑战性。

构建游戏框架:微笑 Pong,版本 1.0

Pong,如图 10-1 所示,是最早的街机电子游戏之一,起源于 1960 年代和 1970 年代。40 多年后,依然充满乐趣。

1972 年 Atari 的著名 Pong 游戏

维基媒体共享资源

图 10-1。1972 年 Atari 的著名 Pong 游戏

单人版 Pong 的游戏玩法很简单。挡板沿屏幕的一侧移动(我们将挡板放在底部),并反弹一个球,在我们的游戏中是一个笑脸。每次击中球时,玩家得分;每次未能击中球时,玩家失分(或失去一条生命)。

我们将使用来自第八章的弹跳笑脸程序作为游戏的基础。以SmileyBounce2.py(将一切整合)为基础,我们已经有了一个*滑动画的笑脸球在窗口的边缘反弹,并且我们已经处理了while循环,保持动画继续,直到用户退出。为了制作 Smiley Pong,我们将添加一个追踪鼠标的底部屏幕上的挡板,并增加更多的碰撞检测,以便检测笑脸球何时碰到挡板。最后的润色将是从零分和五条生命开始,当玩家击中球时给他们加分,而当球从屏幕底部反弹时扣掉一条生命。图 10-2 展示了我们最终的目标。完成后,我们的最终程序将像将一切整合中的示例那样。

我们将要构建的 Smiley Pong 游戏

图 10-2. 我们将要构建的 Smiley Pong 游戏

我们将在原来的SmileyBounce2.py应用中添加的第一个功能是挡板。

绘制棋盘和游戏组件

在我们完成的游戏中,挡板将沿着屏幕底部移动,跟随鼠标的移动,帮助用户避免球撞到屏幕底部。

为了让挡板开始工作,我们将把这些信息添加到应用的设置部分:

WHITE = (255,255,255)
paddlew = 200
paddleh = 25
paddlex = 300
paddley = 550

这些变量将帮助我们创建一个挡板,它只是一个宽度为 200,高度为 25 的白色矩形。我们希望它的左上角坐标从(300, 550)开始,这样挡板就会稍微位于屏幕底部上方,并且水*居中在 800 × 600 的屏幕上。

但是我们还不会画出这个矩形。这些变量足以在第一次时在屏幕上绘制一个矩形,但我们的挡板需要跟随用户的鼠标移动。我们希望挡板在屏幕上根据用户在x方向(左右)移动鼠标的位置进行绘制,同时保持 y 坐标固定在屏幕底部附*。为了实现这一点,我们需要获取鼠标的 x 坐标。我们可以通过pygame.mouse.get_pos()来获取鼠标的位置。在这种情况下,由于我们只关心get_pos()的 x 坐标,而x在鼠标位置中排在前面,我们可以这样获取鼠标的 x 坐标:

paddlex = pygame.mouse.get_pos()[0]

但是记住,Pygame我们提供的(x, y)位置开始绘制矩形,并且将矩形的其余部分绘制到该位置的右侧和下方。为了将挡板居中在鼠标位置,我们需要从鼠标的 x 位置中减去挡板宽度的一半,这样鼠标就会正好在挡板的中间:

paddlex -= paddlew/2

现在我们知道球拍的中心将始终在鼠标所在的位置,我们在游戏循环中需要做的就是在屏幕上绘制球拍矩形:

pygame.draw.rect(screen, WHITE, (paddlex, paddley, paddlew, paddleh))

如果你将这三行代码加到while循环中的pygame.display.update()之前,并在设置部分添加球拍颜色、paddlewpaddlehpaddlexpaddley,你会看到球拍跟随你的鼠标。但是球还不会从球拍上反弹,因为我们还没有添加检测球是否碰到球拍的逻辑。这是我们的下一步。

记录分数

记录分数是让游戏有趣的一部分。分数、生命、星星——无论你用什么方式来记录分数,看到分数增加都会带来一种成就感。在我们的笑脸乒乓游戏中,我们希望玩家每次球碰到球拍时获得一分,而当他们错过球并且球碰到屏幕底部时失去一条生命。接下来的任务是添加逻辑,使球能够从球拍反弹并获得积分,同时在球碰到屏幕底部时减少生命。图 10-3 展示了玩家获得一些积分后的游戏画面。注意,分数显示已经更新为 8。

当笑脸球从底部的球拍反弹时,我们会给玩家的分数加分。

图 10-3. 当笑脸球从底部的球拍反弹时,我们会给玩家的分数加分。

如前所述,我们将从代码的设置部分开始,设置零分和五条生命:

points = 0
lives = 5

接下来,我们需要弄清楚什么时候增加points,什么时候减少lives

减少生命

让我们从减少生命开始。我们知道,如果球碰到屏幕底部边缘,玩家就没有用球拍接住它,所以他们应该失去一条生命。

为了在球碰到屏幕底部时添加减少生命的逻辑,我们需要将判断球是否碰到屏幕顶部底部的if语句(if picy <= 0 or picy >= 500)拆分为两个部分,分别处理顶部和底部。如果球碰到屏幕顶部(picy <= 0),我们只希望它反弹回来,所以我们会通过-speedy改变球在y方向上的速度。

if picy <= 0:
    speedy = -speedy

如果球从底部反弹(picy >= 500),我们希望从lives中扣除一条生命,然后让球反弹回来:

if picy >= 500:
    lives -= 1
    speedy = -speedy

扣除生命已经完成,现在我们需要添加得分。在 SmileyPop,版本 1.0 中,我们看到 Pygame 包含一些函数,可以更轻松地检测碰撞。但是,由于我们是从头开始构建这个 Smiley Pong 游戏,让我们看看如何编写自己的代码来检测碰撞。这段代码可能在未来的应用程序中派上用场,而编写它是一个有价值的解决问题的练习。

用挡板击打球

要检测球是否反弹到挡板上,我们需要查看球可能如何与挡板接触。它可能会撞到挡板的左上角,撞到挡板的右上角,或者直接从挡板的顶部反弹。

没有说明文字的图片

在你编写碰撞检测逻辑时,最好在纸上画出来,并标记出你需要检查是否发生碰撞的角落和边缘。图 10-4 显示了挡板和球的两个角落碰撞的草图。

挡板与我们微笑球的两种碰撞情况

图 10-4. 挡板与我们微笑球的两种碰撞情况

因为我们希望球能现实地从挡板反弹,所以我们要检查球的底部中间是否正好接触到挡板的左边和右边的角落。我们希望确保玩家得分时,不仅仅是球从挡板的顶部反弹,还包括它从挡板的角落反弹时也能得分。为此,我们将检查球的垂直位置是否接*屏幕底部,即挡板所在的位置,如果是这样,我们还需要检查球的水*位置是否能够与挡板碰撞。

首先,让我们弄清楚什么范围的 x 坐标值可以让球与挡板碰撞。由于球的中间位置是从其 (picx, picy) 左上角开始,宽度的一半,我们将在应用的设置部分将球的宽度作为一个变量添加:

picw = 100

如图 10-4 所示,球可能会撞到挡板的左上角,当 picx 加上图片宽度的一半 (picw/2) 等于 paddlex,即挡板左角的 x 坐标时。在代码中,我们可以将这个条件作为 if 语句的一部分进行测试:picx + picw/2 >= paddlex

我们使用大于或等于的条件是因为球可以位于右边(在x方向上大于paddlex)并且仍然会与球拍碰撞;边界情况就是玩家因球拍碰撞到第一个像素而获得得分的情形。球拍的左角到右角之间的所有 x 坐标值都是有效的击球区域,因此这些值应该奖励用户得分并将球反弹回去。

要找出右上角的边界情况,我们可以从图中看到,我们要求球的中点,其 x 坐标是picx + picw/2,小于或等于球拍右上角的 x 坐标,后者是paddlex + paddlew(即球拍的起始 x 坐标加上球拍的宽度)。在代码中,这将是picx + picw/2 <= paddlex + paddlew

我们可以将这两个条件放在一个单一的if语句中,但这还不够。这些 x 坐标覆盖了从球拍的左角到右角,从屏幕的顶部到底部的整个屏幕。仅仅确定了 x 坐标,我们的球仍然可能出现在y方向上的任何位置,因此我们需要进一步缩小范围。仅仅知道我们的球在球拍的水*范围内是不够的;我们还必须知道球在可能与球拍碰撞的y坐标值的垂直范围内。

我们知道球拍的顶部位于y方向上的 550 像素处,接*屏幕底部,因为我们的设置中有paddley = 550这一行,且矩形从该 y 坐标开始,向下延伸 25 像素,这是我们在paddleh中存储的球拍高度。我们知道我们的图片高 100 像素,因此我们可以将其存储为变量pich(图片高度),并将其添加到我们的设置部分:pich = 100

为了让我们球的 y 坐标能与球拍接触,picy位置加上图片的高度pich,需要至少等于paddley或更大,才能让图片的底部(picy + pich)触碰到球拍的顶部(paddley)。我们判断球是否在y方向上碰撞球拍的if语句的一部分是if picy + pich >= paddley。但是,仅仅这个条件会允许球出现在大于paddley的任何地方,甚至是屏幕的底边。我们不希望用户在球击中屏幕底边后,移动球拍让球再次与球拍碰撞并得分,因此我们需要另一个if条件来设置我们将为之得分的最大 y 坐标值。

没有标题的图片

对于获得分数的最大 y 坐标值,自然的选择可能是挡板的底部,或者paddley + paddleh(挡板的 y 坐标加上它的高度)。但是,如果我们的球的底部越过了挡板的底部,玩家就不应该因为击中球而得分,所以我们希望picy + pich(球的底部)小于或等于paddley + paddleh——换句话说,picy + pich <= paddley + paddleh

还有一个条件需要检查。记住,球和挡板是虚拟的;也就是说,它们并不存在于现实世界中,没有实际的边缘,也不像现实中的游戏棋子那样互动。即使球从底部反弹回去,我们也可以让挡板穿过球。我们不想在玩家明显错过球的时候奖励分数,因此在加分之前,我们要检查确保球不仅在垂直和水*范围内,还要确保球是朝下的。如果球的速度在y方向上(speedy)大于零,那么我们就可以知道球正在朝屏幕下方移动。当speedy > 0时,球是在y正方向上向下移动。

我们现在有了创建两个if语句所需的条件,这两个语句将检查球是否撞到挡板:

if picy + pich >= paddley and picy + pich <= paddley + paddleh \
   and speedy > 0:
    if picx + picw/2 >= paddlex and picx + picw/2 <= paddlex + \
       paddlew:

首先,我们检查球是否在垂直范围内能够接触到挡板,并且是否朝下而不是朝上。然后,我们检查球是否在水*范围内能够接触到挡板。

在这两个if语句中,复合条件使得语句太长,无法完全显示在我们的屏幕上。反斜杠字符\可以让我们通过换行继续写一行长代码。你可以选择将一行长代码全部写在一行内,或者通过在第一行末尾加上反斜杠\,按下 ENTER 键,再在下一行继续代码来使代码适应屏幕。我们在本章的游戏中有一些长的逻辑行,所以你会在几个代码列表中看到反斜杠。只需记住,Python 会把由反斜杠分隔的多行代码视为一行代码。

添加得分

让我们构建逻辑来弹跳球并加分。为了完成我们的挡板逻辑,我们在两个if语句之后添加两行代码:

if picy + pich >= paddley and picy + pich <= paddley + paddleh \
   and speedy > 0:
if picx + picw/2 >= paddlex and picx + picw/2 <= paddlex + \
   paddlew:
    points += 1
    speedy = -speedy

加分很简单:points += 1。改变球的方向,让它看起来像是从挡板反弹回去也很简单;我们只需要在y方向上反转速度,让它回到屏幕上方:speedy = -speedy

你可以运行程序并查看球是否从挡板上反弹。每次挡板击中球时,你会得到一分,而每次球错过挡板时,你会失去一条生命,但目前我们还没有在屏幕上显示这些内容。接下来我们来做这件事。

显示分数

我们有了添加积分和扣除生命的逻辑,但在我们玩游戏时,屏幕上并没有显示积分。在这一部分,我们将向屏幕上绘制文本,以便在玩家游戏时为他们提供反馈,如图 10-5 所示。

Smiley Pong 1.0 版本,正在成为一款真正的游戏!

图 10-5. Smiley Pong 1.0 版本,正在成为一款真正的游戏!

第一步是将我们想要显示的文本字符串组合起来。在典型的电子游戏中,我们会看到我们的积分和剩余的生命或回合数——例如,生命:4,积分:32。我们已经有了表示生命数(lives)和总积分(points)的变量。我们只需要使用str()函数将这些数字转换为文本形式(比如5变为"5"),并在每次通过游戏循环时添加文本以指示这些数字的含义:

没有标题的图片

draw_string = "Lives: " + str(lives) + " Points: " + str(points)

我们的字符串变量将命名为draw_string,它包含了我们希望在屏幕上显示的文本,以便在玩家游戏时展示给他们。为了在屏幕上绘制该文本,我们需要一个与文本绘制模块pygame.font相关联的对象或变量。字体字形的另一种名称,指的是字符绘制的样式,比如 Arial 或 Times New Roman。在你的应用的设置部分,添加以下代码:

font = pygame.font.SysFont("Times", 24)

这将创建一个名为font的变量,允许我们在 Pygame 显示屏上绘制 24 点的 Times 字体。你可以使文本变得更大或更小,但目前 24 点的字体大小就足够了。接下来,我们将绘制文本;这应该被加入到游戏循环中,在draw_string声明之后。为了在窗口上绘制文本,我们首先通过render()命令在我们创建的font对象上绘制字符串,绘制到一个独立的表面上:

text = font.render(draw_string, True, WHITE)

这将创建一个名为text的变量,用来存储一个表面,该表面包含了构成我们字符串中所有字母、数字和符号的白色像素。下一步将获取该表面的尺寸(宽度和高度)。较长的字符串会渲染或绘制得更宽,而较短的字符串会占用更少的像素来绘制。较大的字体和较小的字体也是如此。文本字符串将在一个矩形表面上渲染,因此我们将为存储我们绘制的字符串的矩形命名变量为text_rect

text_rect = text.get_rect()

对我们text表面上的get_rect()命令将返回绘制字符串的尺寸。接下来,我们将通过.centerx属性将文本矩形text_rect水*居中,并将文本矩形的位置设置为离屏幕顶部10像素的位置,这样就能轻松看到。以下是设置位置的两条命令:

text_rect.centerx = screen.get_rect().centerx
text_rect.y = 10

现在是时候将text_rect图像绘制到屏幕上了。我们将像处理图片pic一样使用blit()函数来完成:

screen.blit(text, text_rect)

经过这些更改,我们的 Smiley Pong 游戏变得像经典版本的游戏一样,但球是我们的笑脸。运行应用程序,你会看到类似于图 10-5 的内容。我们正在向街机级游戏迈进!

将所有内容整合在一起

我们使用了许多编程技巧来制作这个游戏。变量、循环、条件语句、数学、图形、事件处理——几乎用尽了我们的全部工具包。游戏对程序员和玩家来说都是一场冒险。制作一个游戏既具有挑战性又充满成就感;我们可以构建自己想要的游戏玩法,然后与他人分享。我儿子们非常喜欢 Smiley Pong 游戏的 1.0 版本,并给了我很多想法,让我可以将它扩展到 2.0 版本。

这是完整的 1.0 版本,SmileyPong1.py

SmileyPong1.py

import pygame       # Setup
pygame.init()
screen = pygame.display.set_mode([800,600])
pygame.display.set_caption("Smiley Pong")
keepGoing = True
pic = pygame.image.load("CrazySmile.bmp")
colorkey = pic.get_at((0,0))
pic.set_colorkey(colorkey)
picx = 0
picy = 0
BLACK = (0,0,0)
WHITE = (255,255,255)
timer = pygame.time.Clock()
speedx = 5
speedy = 5
paddlew = 200
paddleh = 25
paddlex = 300
paddley = 550
picw = 100
pich = 100
points = 0
lives = 5
font = pygame.font.SysFont("Times", 24)

while keepGoing:    # Game loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            keepGoing = False
    picx += speedx
    picy += speedy

    if picx <= 0 or picx + pic.get_width() >= 800:
        speedx = -speedx
    if picy <= 0:
        speedy = -speedy
    if picy >= 500:
        lives -= 1
        speedy = -speedy

    screen.fill(BLACK)
    screen.blit(pic, (picx, picy))

    # Draw paddle
    paddlex = pygame.mouse.get_pos()[0]
    paddlex -= paddlew/2
    pygame.draw.rect(screen, WHITE, (paddlex, paddley, paddlew, paddleh))

    # Check for paddle bounce
    if picy + pich >= paddley and picy + pich <= paddley + paddleh \
       and speedy > 0:
        if picx + picw / 2 >= paddlex and picx + picw / 2 <= paddlex + \
           paddlew:
            points += 1
            speedy = -speedy

    # Draw text on screen
    draw_string = "Lives: " + str(lives) + " Points: " + str(points)

    text = font.render(draw_string, True, WHITE)
    text_rect = text.get_rect()
    text_rect.centerx = screen.get_rect().centerx
    text_rect.y = 10
    screen.blit(text, text_rect)
    pygame.display.update()
    timer.tick(60)

pygame.quit()      # Exit

我们的游戏玩法几乎完成:球从挡板上反弹,得分,玩家如果错过球并且球撞到屏幕底部,就会失去一条命。所有基本组件都已经到位,可以让这个游戏成为一款街机风格的游戏。现在,想想你希望看到哪些改进,理清逻辑,并尝试向 1.0 版本添加代码,让你的游戏更加有趣。在接下来的部分,我们将添加三个新特性,创造一个完全互动、像视频游戏一样的体验,并与他人分享。

添加难度和结束游戏:Smiley Pong,版本 2.0

我们的 Smiley Pong 游戏 1.0 版本是可玩的。玩家可以得分、失去生命,并在屏幕上看到自己的进度。现在我们还没有的一个功能是游戏结束的提示。另一个是随着游戏进行,提升挑战性。我们将为 Smiley Pong 1.0 版本添加以下功能,以便在 2.0 版本中创造一个更完整的游戏:当玩家失去最后一条命时,显示游戏结束,提供重新开始或启动新游戏而无需关闭程序的选项,并随着游戏进行增加难度。我们将一一添加这三个功能,最终制作出一个有趣、充满挑战的街机风格游戏!最终版本展示在将所有内容整合在一起中。

游戏结束

1.0 版本从未停止游戏,因为我们没有添加处理游戏结束的逻辑。我们知道需要测试的条件:当玩家没有剩余生命时,游戏结束。现在我们需要弄清楚,当玩家失去最后一条命时该怎么做。

我们要做的第一件事是停止游戏。我们不想关闭程序,但我们确实想停止球的运动。第二件事是更改屏幕上的文字,告诉玩家游戏结束并显示他们的得分。我们可以通过在draw_string声明生命和得分后添加一个if语句来完成这两个任务。

if lives < 1:
    speedx = speedy = 0
    draw_string = "Game Over. Your score was: " + str(points)
    draw_string += ". Press F1 to play again. "

通过将 speedxspeedy(分别是球的水*和垂直速度)设置为零,我们让球停止了移动。用户仍然可以在屏幕上移动挡板,但我们通过视觉效果结束了游戏,以让用户知道游戏已经结束。屏幕上的文字使这一点更加明确,并告诉玩家他们在这一局中的表现如何。

现在,我们告诉用户按下 F1 键重新开始游戏,但按下这个键目前没有任何反应。我们需要添加逻辑来处理按键事件并重新开始游戏。

重新开始

我们希望在玩家用完所有生命后,让他们开始新的一局游戏。我们已经在屏幕上添加了文本,告诉玩家按 F1 键重新开始游戏,那么接下来我们要添加代码来检测按键事件并重新开始游戏。首先,我们将检查是否有按键被按下,并且该按键是否是 F1 键:

if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_F1: # F1 = New Game

在我们的游戏循环中的事件处理器 for 循环里,我们添加一个 if 语句来检查是否发生了 KEYDOWN 事件。如果有,我们检查事件中按下的键(event.key),看它是否等于 F1 键(pygame.K_F1)。跟随这个第二个 if 语句的代码将是我们的 重新开始新游戏 代码。

注意

你可以在 www.pygame.org/docs/ref/key.html 获取完整的 Pygame 键码列表,如 K_F1

“重新开始”意味着我们要从头开始。对于 Smiley Pong,我们从 0 分、5 条命开始,球从屏幕的左上角(0, 0)以每帧 5 像素的速度向我们移动。如果我们重置这些变量,就能得到新的游戏效果:

points = 0
lives = 5
picx = 0
picy = 0
speedx = 5
speedy = 5

将这些代码行添加到 F1 键 KEYDOWN 事件的 if 语句中,你就可以随时重新开始游戏。如果你希望仅在游戏已经结束时允许重新开始,可以添加一个额外的条件 lives == 0,但在我们的 2.0 版本中,我们将保持当前的 if 语句,这样用户就可以随时重新开始游戏。

更快更快

我们的游戏缺少一个最终的游戏设计元素:随着游戏进行,游戏的挑战性并没有增加,因此玩家可以玩得几乎永远,且注意力越来越分散。让我们随着游戏的进展增加难度,以吸引玩家并使游戏更具街机风格。

没有标题的图片

我们希望随着游戏的进行,稍微增加球的速度,但不能增加太多,否则玩家可能会感到沮丧。我们希望每次反弹后,游戏稍微加速。最自然的做法是在检查反弹的代码中进行修改。增加速度意味着让 speedxspeedy 的值更大,这样球在每一帧中就能向每个方向移动得更远。尝试将我们的碰撞检测 if 语句(我们在这里让球从屏幕的每个边缘反弹)更改为以下内容:

if picx <= 0 or picx >= 700:
    speedx = -speedx * 1.1
if picy <= 0:
    speedy = -speedy + 1

在第一个情况下,当球在水*方向上从屏幕的左右两边反弹时,我们通过将水*速度speedx乘以1.1来增加它的速度(我们仍然通过负号来改变方向)。每次左右反弹后,速度增加 10%。

当球从屏幕顶部反弹时(if picy <= 0),我们知道球的速度会变为正值,因为它会从顶部反弹并沿着正y方向向下移动,因此我们可以在改变方向时加上负号后,将speedy加 1。如果球以每帧 5 像素的速度朝顶部移动,那么它离开时的速度将是每帧 6 像素,接着是 7 像素,依此类推。

如果你做了这些修改,你会看到球的速度越来越快。但一旦球开始变得更快,它就不会再变慢了。很快,球的速度会快到玩家可能在一秒钟内就失去所有五条命。

我们通过在玩家失去一条命时重置速度,使我们的游戏更加可玩(并且公*)。如果球的速度太快,用户无法用球拍打到它,那么很可能是时候将速度重置为一个较慢的值,让玩家赶上。

我们的代码用于从屏幕底部反弹时会减少玩家的一条生命,因此在减去一条生命后,我们应该改变速度:

if picy >= 500:
    lives -= 1
    speedy = -5
    speedx = 5

这样可以使游戏更加合理,因为球不再失控并保持这种状态;当玩家失去一条命后,球的速度会减慢到足以让玩家再打几次球,然后才会加速。

然而,有一个问题是,球的速度可能会快到在屏幕底部“卡住”;玩几局游戏后,玩家可能会遇到这样一种情况:他们在一次从屏幕底部反弹后就失去了所有剩余的生命。这是因为如果球移动得非常快,它可能会远远超出屏幕底部的边缘,并且当我们重置速度时,下一帧可能无法将球完全带回屏幕内。

为了解决这个问题,让我们在那行if语句的末尾添加一行代码:

picy = 499

我们在失去一条命后将球完全移回屏幕上,方法是将picy设置为一个值,比如499,使球完全位于屏幕底部边界之上。这样可以确保无论球以多快的速度撞击底部边缘,都会安全地回到屏幕上。

在这些修改之后,版本 2.0 看起来像 图 10-6。

版本 2.0 的我们的笑脸乒乓游戏增加了更快的游戏玩法、游戏结束和重新开始功能。

图 10-6。版本 2.0 的我们的笑脸乒乓游戏增加了更快的游戏玩法、游戏结束和重新开始功能。

版本 2.0 就像一个真正的街机游戏,配有游戏结束/再玩一次的屏幕。

整合所有内容

这是我们的完成版本 2.0,SmileyPong2.py。它的代码不到 80 行,是一款完整的街机风格游戏,你可以向朋友和家人展示。你也可以在此基础上继续开发,提升你的编码技能。

SmileyPong2.py

import pygame       # Setup
pygame.init()
screen = pygame.display.set_mode([800,600])
pygame.display.set_caption("Smiley Pong")
keepGoing = True
pic = pygame.image.load("CrazySmile.bmp")
colorkey = pic.get_at((0,0))
pic.set_colorkey(colorkey)
picx = 0
picy = 0
BLACK = (0,0,0)
WHITE = (255,255,255)
timer = pygame.time.Clock()
speedx = 5
speedy = 5
paddlew = 200
paddleh = 25
paddlex = 300
paddley = 550
picw = 100
pich = 100
points = 0
lives = 5
font = pygame.font.SysFont("Times", 24)

while keepGoing:      # Game loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            keepGoing = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_F1:     # F1 = New Game
                points = 0
                lives = 5
                picx = 0
                picy = 0
                speedx = 5
                speedy = 5
    picx += speedx
    picy += speedy

    if picx <= 0 or picx >= 700:
        speedx = -speedx * 1.1
    if picy <= 0:
        speedy = -speedy + 1
    if picy >= 500:
        lives -= 1
        speedy = -5
        speedx = 5
        picy = 499

    screen.fill(BLACK)
    screen.blit(pic, (picx, picy))

    # Draw paddle
    paddlex = pygame.mouse.get_pos()[0]
    paddlex -= paddlew/2
    pygame.draw.rect(screen, WHITE, (paddlex, paddley, paddlew, paddleh))

    # Check for paddle bounce
    if picy + pich >= paddley and picy + pich <= paddley + paddleh \
       and speedy > 0:
        if picx + picw/2 >= paddlex and picx + picw/2 <= paddlex + \
           paddlew:
            speedy = -speedy
            points += 1

    # Draw text on screen
    draw_string = "Lives: " + str(lives) + " Points: " + str(points)
    # Check whether the game is over
    if lives < 1:
        speedx = speedy = 0
        draw_string = "Game Over. Your score was: " + str(points)
        draw_string += ". Press F1 to play again. "

    text = font.render(draw_string, True, WHITE)
    text_rect = text.get_rect()
    text_rect.centerx = screen.get_rect().centerx
    text_rect.y = 10
    screen.blit(text, text_rect)
    pygame.display.update()
    timer.tick(60)

pygame.quit()       # Exit

你可以继续在这个示例中的游戏元素上进行扩展(参见编程挑战),或者你可以使用这些构建模块开发新的东西。大多数游戏,甚至其他应用程序,都具有像你在本章中添加的功能,我们通常会遵循一个类似于构建 Smiley Pong 时的过程。首先,规划出游戏的框架,然后构建一个可运行的原型,即版本 1.0。一旦它正常工作,再添加功能,直到达到你想要的最终版本。当你构建更复杂的应用程序时,你会发现迭代版本控制——一步一步添加功能,创造新版本——非常有用。

添加更多功能:SmileyPop V2.0

我们将再一次按照迭代版本过程,通过添加我和我的儿子 Max 在第九章中希望在 SmileyPop 应用程序中看到的功能来进行开发。首先,他希望每次鼠标点击气泡(或气球)时能够播放声音效果。其次,我们都希望有某种反馈和显示(比如已经创建了多少个气泡,已经爆破了多少个),我还希望有一个进度显示,比如我们已经爆破了多少个气泡的百分比。SmileyPop 应用程序已经很有趣了,但这些元素可以让它变得更好。

回顾一下 SmileyPop.py;我们将从这个版本的应用程序开始,并通过添加代码构建第二个版本(v2.0,版本 2.0 的简称)。最终版本,SmileyPop2.py,如图 10-7 所示。

我们将首先添加 Max 的请求:爆破声音。

使用 Pygame 添加声音

www.pygame.org/docs/,你可以找到模块、类和函数,它们能让你的游戏更加有趣,编程也更简单。我们需要用到的声音效果模块是pygame.mixer。要使用这个混音模块为游戏添加声音,首先需要一个声音文件。对于我们的爆破声音效果,请从www.nostarch.com/teachkids/下载pop.wav文件,它位于第十章的源代码和文件下。

我们将在SmileyPop.py的设置部分添加这两行代码,紧接在sprite_list = pygame.sprite.Group()之后:

pygame.mixer.init()    # Add sounds
pop = pygame.mixer.Sound("pop.wav")

我们首先初始化混音器(就像我们用pygame.init()初始化 Pygame 一样)。然后我们将pop.wav声音效果加载到Sound对象中,以便在程序中播放。

第二行将pop.wav加载为pygame.mixer.Sound对象,并将其存储在变量pop中,我们稍后将在需要播放弹出声音时使用它。与图像文件一样,你需要将pop.wav保存到与SmileyPop.py程序相同的目录或文件夹中,代码才能找到并使用该文件。

接下来我们将添加逻辑,检查是否点击了表情符号,如果表情符号被弹出,则播放我们的pop声音。我们将在游戏循环的事件处理部分执行此操作,放在处理右键单击事件的相同elif语句中(elif pygame.mouse.get_pressed()[2])。在sprite_list.remove(clicked_smileys)移除被点击的表情符号后,我们可以检查是否真的有表情符号碰撞发生,然后播放声音。

无标题的图片

用户可能会在没有表情符号的屏幕区域右键单击,或者他们可能会在点击时错过一个表情符号。我们将检查是否有表情符号被实际点击,方法是查看if len(clicked_smileys) > 0len()函数告诉我们列表或集合的长度,如果长度大于零,就说明有被点击的表情符号。记住,clicked_smileys是一个包含与用户点击点碰撞或重叠的表情符号精灵的列表。

如果clicked_smileys列表中有表情符号精灵,那么用户正确地右键单击了至少一个表情符号,因此我们播放弹出声音:

if len(clicked_smileys) > 0:
    pop.play()

请注意,这两行代码的缩进与我们处理右键单击的elif语句中的其他代码对齐。

这四行新增的代码就是当用户成功右键单击表情符号时播放弹出声音所需要的所有代码。为了实现这些更改并听到效果,请确保已将pop.wav声音文件下载到与修改后的SmileyPop.py文件相同的文件夹中,调整扬声器音量至适当水*,然后开始弹出吧!

跟踪和显示玩家进度

我们想要添加的下一个功能是某种方式来帮助用户感觉他们在取得进展。添加的声音效果提供了一种有趣的反馈(用户只有在实际点击了表情符号精灵时才会听到弹出声音),但我们也想跟踪用户创建和弹出的气泡数量,以及他们弹出了多少百分比的表情符号。

为了构建跟踪用户创建的表情符号数量和他们弹出的表情符号数量的逻辑,我们将首先在应用程序的设置部分添加一个font变量和两个计数器变量,count_smileyscount_popped

font = pygame.font.SysFont("Arial", 24)
WHITE = (255,255,255)
count_smileys = 0
count_popped = 0

我们将font变量设置为 Arial 字体,大小为 24 号。我们希望以白色文字在屏幕上绘制文本,因此我们添加了一个颜色变量WHITE,并将其设置为白色的 RGB 三元组(255, 255, 255)。我们的count_smileyscount_popped变量将分别存储已创建和已弹出的笑脸数量,初始值都为零,当应用程序首次加载时。

已创建和已弹出的笑脸

首先,让我们在笑脸被添加到 sprite_list时进行计数。为此,我们几乎要到达SmileyPop.py代码的底部,在那里if mousedown语句检查鼠标按钮是否按下并被拖动,且笑脸被添加到sprite_list中。只需将最后一行添加到该if语句中:

if mousedown:
    speedx = random.randint(-5, 5)
    speedy = random.randint(-5, 5)
    newSmiley = Smiley(pygame.mouse.get_pos(), speedx, speedy)
    sprite_list.add(newSmiley)
    count_smileys += 1

每次一个新的笑脸被添加到sprite_list时,将 1 加到count_smileys,将帮助我们跟踪绘制的笑脸总数。

我们将向播放点击音效的if语句中添加类似的逻辑,每当一个或多个笑脸被点击时,触发音效,但是我们不仅仅将 1 添加到count_popped—我们将添加实际被点击的笑脸数量。请记住,用户可能会点击屏幕上重叠的多个笑脸精灵。在处理右键点击事件时,我们将所有这些重叠的笑脸作为clicked_smileys列表来收集。为了找出应该添加多少分数到count_popped,我们再次使用len()函数来获取用户通过右键点击弹出的笑脸数量。将这行代码添加到你为弹出音效写的if语句中:

if len(clicked_smileys) > 0:
    pop.play()
    count_popped += len(clicked_smileys)

通过将len(clicked_smileys)添加到count_popped,我们可以确保在任何时刻都有正确的已弹出笑脸数量。现在,我们只需要向游戏循环中添加代码,显示已创建的笑脸数量、已弹出的笑脸数量以及已弹出笑脸的百分比,以衡量用户的进度。

没有标题的图片

就像在我们的笑脸乒乓显示中一样,我们将创建一串文本在屏幕上绘制,并通过str()函数将数字作为字符串显示。将这些行添加到你的游戏循环中,紧接在pygame.display.update()之前:

draw_string = "Bubbles created: " + str(count_smileys)
draw_string += " - Bubbles popped: " + str(count_popped)

这些行将创建我们的draw_string,并显示已创建和已弹出的笑脸气泡数量。

已弹出笑脸的百分比

在两个draw_string语句后面添加这三行代码:

if (count_smileys > 0):
    draw_string += " - Percent: "
    draw_string += str(round(count_popped/count_smileys*100, 1))
    draw_string += "%"

为了得到已弹出笑脸占所有已创建笑脸的百分比,我们将count_popped除以count_smileyscount_popped/count_smileys),然后乘以 100 得到百分比值(count_popped/count_smileys*100)。但是如果我们尝试显示这个数字,会遇到两个问题。首先,当程序启动并且这两个值都为零时,我们的百分比计算将产生“除以零”的错误。为了解决这个问题,我们只会在count_smileys大于零时显示已弹出的百分比。

其次,如果用户创建了三个笑脸并弹出了其中一个——这是三分之一的比例,即 1/3——那么百分比将是 33.33333333……。我们不希望每次百分比计算有循环小数时,显示变得很长,因此我们可以使用round()函数将百分比四舍五入到一位小数。

最后一步是将字符串用白色像素绘制,将这些像素居中显示在屏幕的上方,然后调用screen.blit()将这些像素复制到游戏窗口的绘图屏幕中:

text = font.render(draw_string, True, WHITE)
text_rect = text.get_rect()
text_rect.centerx = screen.get_rect().centerx
text_rect.y = 10
screen.blit (text, text_rect)

你可以在图 10-7 中看到这些变化的效果。

较小的笑脸表情更难捕捉并弹出,尤其是当它们快速移动时,因此很难弹出超过 90%的笑脸。这正是我们想要的效果。我们利用这个反馈和挑战/成就组件,使应用程序更像是我们可能会玩的游戏。

通过添加声音和进度/反馈显示,SmileyPop 应用程序现在更像是一个游戏。

图 10-7。通过添加声音和进度/反馈显示,SmileyPop 应用程序现在更像是一个游戏。

弹出声音和进度显示反馈让 SmileyPop 感觉像一个移动应用程序。当你通过右键点击弹出笑脸时,你可能能想象到在移动设备上用手指轻点笑脸来弹出它们。(想了解如何构建移动应用程序,可以访问 MIT 的 App Inventor:appinventor.mit.edu/。)

将一切整合起来

这是 SmileyPop 2.0 版本的完整代码。记得将.py源代码文件、CrazySmile.bmp图像文件和pop.wav声音文件都保存在同一个文件夹中。

这款应用程序几乎有 90 行代码,可能手动输入会有点长。你可以访问www.nostarch.com/teachkids/下载代码以及声音和图片文件。

SmileyPop2.py

import pygame
import random

BLACK = (0,0,0)
WHITE = (255,255,255)
pygame.init()
screen = pygame.display.set_mode([800,600])
pygame.display.set_caption("Pop a Smiley")
mousedown = False
keep_going = True
clock = pygame.time.Clock()
pic = pygame.image.load("CrazySmile.bmp")
colorkey = pic.get_at((0,0))
pic.set_colorkey(colorkey)
sprite_list = pygame.sprite.Group()
pygame.mixer.init()    # Add sounds
pop = pygame.mixer.Sound("pop.wav")
font = pygame.font.SysFont("Arial", 24)
count_smileys = 0
count_popped = 0

class Smiley(pygame.sprite.Sprite):
    pos = (0,0)
    xvel = 1
    yvel = 1
    scale = 100

    def __init__(self, pos, xvel, yvel):
        pygame.sprite.Sprite.__init__(self)
        self.image = pic
        self.scale = random.randrange(10,100)
        self.image = pygame.transform.scale(self.image,
                                            (self.scale,self.scale))
        self.rect = self.image.get_rect()
        self.pos = pos
        self.rect.x = pos[0] - self.scale/2
        self.rect.y = pos[1] - self.scale/2
        self.xvel = xvel
        self.yvel = yvel
    def update(self):
        self.rect.x += self.xvel
        self.rect.y += self.yvel
        if self.rect.x <= 0 or self.rect.x > screen.get_width() - self.scale:
            self.xvel = -self.xvel
        if self.rect.y <= 0 or self.rect.y > screen.get_height() - self.scale:
            self.yvel = -self.yvel

while keep_going:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            keep_going = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            if pygame.mouse.get_pressed()[0]: # Left mouse button, draw
                mousedown = True
            elif pygame.mouse.get_pressed()[2]: # Right mouse button, pop
                pos = pygame.mouse.get_pos()
                clicked_smileys = [s for s in sprite_list if
                                   s.rect.collidepoint(pos)]
                sprite_list.remove(clicked_smileys)
                if len(clicked_smileys) > 0:
                    pop.play()
                    count_popped += len(clicked_smileys)
        if event.type == pygame.MOUSEBUTTONUP:
            mousedown = False
    screen.fill(BLACK)
    sprite_list.update()
    sprite_list.draw(screen)
    clock.tick(60)
    draw_string = "Bubbles created: " + str(count_smileys)
    draw_string += " - Bubbles popped: " + str(count_popped)
    if (count_smileys > 0):
        draw_string += " - Percent: "
        draw_string += str(round(count_popped/count_smileys*100, 1))
        draw_string += "%"

    text = font.render(draw_string, True, WHITE)
    text_rect = text.get_rect()
    text_rect.centerx = screen.get_rect().centerx
    text_rect.y = 10
    screen.blit (text, text_rect)

    pygame.display.update()
    if mousedown:
        speedx = random.randint(-5, 5)
        speedy = random.randint(-5, 5)
        newSmiley = Smiley(pygame.mouse.get_pos(), speedx, speedy)
        sprite_list.add(newSmiley)
        count_smileys += 1

pygame.quit()

写得越多的程序,你的编程技能就会越好。你可以从编写你感兴趣的游戏开始,写一个解决你关心问题的应用程序,或者为其他人开发应用程序。继续编程,解决更多问题,编程能力会越来越强,你很快就能帮助创造出惠及全球用户的产品。

无论你是在编写移动游戏和应用程序;编写控制汽车、机器人或无人机的程序;还是构建下一个社交媒体网络应用程序,编程都是一项能够改变你生活的技能。

你拥有了这些技能,你也有能力。继续练习,继续编程,走出去去改变世界——改变你自己的人生,改变你关心的人的生活,甚至改变整个世界。

你学到了什么

在本章中,你了解了游戏设计的元素,从目标和成就到规则和机制。我们从零开始构建了一个单人《Smiley Pong》游戏,并将我们的 SmileyPop 应用转变为一个我们可以在智能手机或*板电脑上玩的游戏。我们结合了动画、用户互动和游戏设计,构建了《Smiley Pong》游戏的两个版本和 SmileyPop 的第二个版本,并随着进展不断增加更多特性。

在《Smiley Pong》中,我们绘制了棋盘和游戏棋子,添加了用户互动以移动挡板,加入了碰撞检测和计分功能。我们在屏幕上显示文本,提供用户关于其成就和游戏状态的信息。你学习了如何在 Pygame 中检测按键事件,添加了“游戏结束”和“重新开始”逻辑,并通过让球在游戏进程中加速完成了 2.0 版本。现在,你已经具备了构建更复杂游戏的框架和部分内容。

在 SmileyPop 中,我们从一个已经很有趣的应用开始,使用pygame.mixer模块添加了通过爆炸声反馈的用户反馈,然后添加了逻辑和显示来跟踪用户的进度,随着更多气泡的产生和爆炸。

你用编程技能创建的应用也将从一个简单的版本开始,一个概念验证版本,你可以运行并作为新版本的基础。你可以从任何程序开始,一次性添加一个新特性,并在此过程中保存每个新版本——这个过程叫做迭代版本管理。这个过程帮助你调试每个新特性,直到它正常工作,同时也帮助你在新代码出现问题时保留最后一个正常版本的文件。

有时候,一个新特性会非常合适,并成为下一个版本的基础。有时候你的新代码无法工作,或者特性没有你预期的那么酷。不管怎样,你通过尝试新事物并解决新问题来提升你的编程技能。

编程愉快!

掌握本章的概念后,你应该能够做到以下几点:

  • 识别你所使用的游戏和应用中的常见游戏设计元素。

  • 将游戏设计元素融入你编写的应用中。

  • 通过绘制棋盘和游戏棋子并添加用户互动,构建一个游戏框架。

  • 在应用或游戏中编写碰撞检测逻辑并保持得分。

  • 使用pygame.font模块在屏幕上显示文本信息。

  • 编写游戏逻辑以判断游戏是否结束。

  • 在 Pygame 中检测和处理按键事件。

  • 开发代码以启动新游戏或在游戏结束后重新开始。

  • 使用数学和逻辑使游戏逐步变得更加困难。

  • 使用pygame.mixer模块为应用添加音效。

  • 显示百分比和四舍五入的数字,以便让用户了解自己在游戏中的进展。

  • 理解迭代版本管理的过程:逐个添加特性并将其保存为新版本(1.0、2.0 等)。

编程挑战

要查看这些挑战的示例答案,并下载本章的音频文件,请访问 www.nostarch.com/teachkids/

#1: 音效

我们可以为 Smiley Pong 2.0 版本添加的一个功能是音效。在经典的 Pong 游戏机和街机游戏中,球在玩家得分时会发出“哔”声,错失时会发出“嗡嗡”声或“啪”的声音。作为你最终挑战之一,运用在 SmileyPop 2.0 版本中学到的技能,通过为得分和错失的反弹添加音效,将 Smiley Pong v2.0 升级为 v3.0。将这个新文件保存为 SmileyPong3.py

#2: 命中与错失

为了让 SmileyPop 应用程序更具游戏性,添加逻辑来跟踪总点击次数中的命中和错失次数。如果用户在右键点击时命中了任何笑脸精灵,就将 hits 加 1(每次点击 1 次命中——我们不希望重复记录 count_popped)。如果用户右键点击但没有命中任何笑脸精灵,记录为一次 miss。你可以编写逻辑,在错失一定次数后结束游戏,或者给用户一个总点击次数,让他们尽可能获得最高的命中率。你甚至可以添加一个计时器,告诉玩家在比如 30 秒内尽可能多地创建和弹出笑脸气泡。将这个新版本保存为 SmileyPopHitCounter.py

#3: 清除气泡

你可能想要添加一个“清除”功能(或者作弊按钮),通过按下功能键弹出所有气泡,类似于我们在 Smiley Pong 中的“重新开始”功能。你还可以通过在气泡每次碰到边缘时,将它们的速度乘以一个小于 1 的数值(比如 0.95),使得弹跳的笑脸逐渐减速。可能性无穷无尽。

附录 A. Windows、Mac 和 Linux 上的 Python 设置

本附录将指导你完成在 Windows、Mac 或 Linux 上安装 Python 的每个步骤。根据你的操作系统版本,看到的内容可能与你屏幕上的有所不同,但这些步骤应该能帮助你顺利完成安装。

如果你在学校或工作场所的计算机上安装 Python,可能需要 IT 部门的帮助或许可来进行安装。如果你在学校安装 Python 时遇到问题,请寻求 IT 帮助并告诉他们你在学习编程。

Windows 上的 Python

对于 Windows,我们将使用 Python 3.2.5 版本,这样我们的 Pygame 安装(见附录 B)将使 第八章到 第十章 中的程序更容易。

下载安装程序

  1. 访问 python.org/,将鼠标悬停在 Downloads 链接上。你将看到一个下拉选项列表,如图 A-1 所示。

    将鼠标悬停在 Downloads 上以显示选项列表。

    图 A-1. 将鼠标悬停在 Downloads 上以显示选项列表。

  2. 在下拉列表中,点击 Windows 链接。这将带你进入 Python Windows 版本下载页面,如图 A-2 所示。

    Python Windows 版本下载页面

    图 A-2. Python Windows 版本下载页面

  3. 向下滚动,直到看到以 Python 3.2.5 开头的链接。在该链接下方,你将看到几个项目,如图 A-3 所示。

    在 Python Releases For Windows 下找到 Python 3.2.5 安装程序。

    图 A-3. 在 Python Releases For Windows 下找到 Python 3.2.5 安装程序。

  4. 在 Python 3.2.5 下,点击 Windows x86 MSI 安装程序。这将下载安装程序文件。

运行安装程序

  1. 等待下载完成后,打开 Downloads 文件夹。你应该能看到 python-3.2.5 Windows 安装程序文件,如图 A-4 所示。

    双击 *Downloads* 文件夹中的安装程序。

    图 A-4. 双击 Downloads 文件夹中的安装程序。

  2. 双击 python-3.2.5 Windows 安装程序文件以开始安装。

  3. 可能会弹出一个安全警告对话框,如图 A-5 所示。如果你看到安全警告窗口,点击运行;Windows 只是通知你该软件正在尝试在你的计算机上安装某些内容。

    点击运行以允许安装。

    图 A-5。点击运行以允许安装。

  4. 安装程序可能会询问你是否希望为所有用户安装 Python 或仅为自己安装,如图 A-6 所示。通常选择为所有用户安装是最好的,但如果在学校或办公室无法允许此操作,或者你无法正常安装,尝试选择仅为我安装。然后点击下一步 >

    为所有用户安装。

    图 A-6。为所有用户安装。

  5. 接下来,你将看到一个选择目标文件夹的窗口,像图 A-7 所示。这是你可以选择安装 Python 的文件夹位置。程序会尝试将其安装到你的C:*驱动器下的一个名为Python32的文件夹中,这应该适用于你的笔记本电脑或家庭 PC。点击下一步 >继续安装。(如果你在学校或公司安装,遇到问题时,IT 部门可能会要求你安装到其他文件夹,比如用户桌面*。)

    选择一个文件夹安装 Python。

    图 A-7。选择一个文件夹安装 Python。

  6. 现在你将看到一个像图 A-8 所示的窗口,要求你自定义 Python。这里无需更改任何内容。只需点击下一步 >

    不要更改任何设置,只需点击下一步 >。

    图 A-8。不要更改任何设置,只需点击下一步 >

  7. 你现在已经完成了安装程序,应该会看到一个像图 A-9 所示的窗口。点击完成

    点击完成退出安装程序。

    图 A-9。点击完成退出安装程序。

    你已经安装了 Python!接下来,你可以尝试运行它,以确保它正常工作。

尝试 Python

  1. 转到开始程序Python 3.2IDLE (Python GUI),如图 A-10 所示。(在 Windows 8 及以后的版本中,你可以点击 Windows/开始按钮,进入搜索工具,然后输入IDLE。)

    从开始菜单打开 IDLE。

    图 A-10。从开始菜单打开 IDLE。

  2. Python Shell 编辑器界面应该会出现。这个 Python shell 程序是你可以输入代码并立即查看结果的地方。如果你感到好奇,可以开始尝试一些代码。输入 print("Hello, Python!") 并按回车键。Python shell 应该会返回 Hello, Python!,如图 A-11 所示。尝试一个加法语句,例如 2 + 3。按下回车键,Python 会返回答案!

    在 Python shell 中尝试一些命令

    图 A-11. 在 Python shell 中尝试一些命令

  3. 最后,你可能想要调整 IDLE 中文本的大小,使其更容易阅读。转到 选项配置 IDLE...。在 字体/标签 下,如图 A-12 所示,将 大小 选项更改为 18 或任何对你来说最容易阅读的大小。你还可以勾选粗体选项,使文本更加粗体。自定义字体,使其适合你的眼睛。

    在 IDLE 中配置首选项

    图 A-12. 在 IDLE 中配置首选项

  4. 选择了字体和大小选项以便让 IDLE 输入更易读后,点击 应用,然后点击 确定 返回到 IDLE Python shell 屏幕。现在当你输入时,应该会看到按照你选择的字体和大小显示的文本。

现在你已经准备好开始学习 第一章 到 第七章。要使用 第八章 到 第十章 中的程序,请转到附录 B 并按照步骤安装 Pygame。祝编码愉快!

Python for Mac

大多数 Apple 电脑预装了早期版本的 Python,但我们要安装版本 3.4.2,以便使用 Python 3 的新特性来运行书中的示例代码。

下载安装程序

  1. 访问 python.org/,并将鼠标悬停在 下载 链接上,查看下拉列表中的选项。你会看到 Mac OS X 在此列表中,如图 A-13 所示。

    将鼠标悬停在下载链接上,你应该会看到下拉列表中有一个 Mac OS X 链接。

    图 A-13. 将鼠标悬停在下载链接上,你应该会看到下拉列表中有一个 Mac OS X 链接。

  2. 点击下拉列表中的 Mac OS X 链接。这将带你到 Mac OS X 的 Python 发行版页面。

  3. 在 Python Releases For Mac OS X 页面,找到以Python 3.4.2开头的链接并点击它。这将下载安装程序。

运行安装程序

  1. 等待下载完成。然后打开你的下载文件夹,找到python-3.4.2 Mac 安装程序文件,如图 A-14 所示。双击该文件开始安装。

    双击你的下载文件夹中的安装程序。

    图 A-14. 双击你的下载文件夹中的安装程序。

  2. 双击安装程序文件将会打开一个安装 Python 的窗口。你会看到一个欢迎界面,如图 A-15 所示。点击继续

    点击欢迎屏幕上的继续按钮。

    图 A-15. 在欢迎屏幕上点击继续按钮。

  3. 阅读并在软件许可协议弹出对话框中点击同意,如图 A-16 所示。

    阅读并点击同意软件许可协议对话框。

    图 A-16. 阅读并点击同意软件许可协议对话框。

  4. 你将进入一个选择目标安装位置的屏幕,如图 A-17 所示,在这里你可以选择将 Python 安装到哪个磁盘。程序通常会安装到你的 Mac HD 硬盘上,这对你的 MacBook 或家用 Mac 应该没有问题。点击继续以继续安装。(如果你在学校或公司安装并遇到问题,IT 人员可能会告诉你安装到其他文件夹;如有需要,向他们寻求帮助。)

    点击继续以继续安装。

    图 A-17. 点击继续以继续安装。

  5. 在下一个屏幕上点击安装,如图 A-18 所示。

    点击安装按钮。

    图 A-18. 点击安装。

  6. 你应该会看到一个确认安装完成的屏幕,如图 A-19 所示。点击关闭退出安装程序。

    要退出安装程序,请点击关闭按钮。

    图 A-19. 要退出安装程序,请点击关闭按钮。

    你已经成功安装了 Python!接下来,你可以尝试运行它看看是否正常工作。

尝试运行 Python

  1. 打开启动台并点击IDLE,或者去Finder应用程序,双击Python 3.4文件夹,再双击IDLE打开 Python shell,如图 A-20 所示。

    从启动台(左)或应用程序文件夹(右)打开 IDLE

    图 A-20. 从启动台(左)或应用程序文件夹(右)打开 IDLE。

  2. Python shell 编辑器屏幕应该会出现。你已经准备好在 shell 中尝试编写代码了。输入print("Hello, Python!")并按回车;Python shell 应该会回应Hello, Python!,如图 A-21 所示。试试加法语句,比如2 + 3。按回车,Python 将给出答案。

    在 Python shell 中尝试一些命令

    图 A-21. 在 Python shell 中尝试一些命令

  3. 你可能想调整 IDLE 窗口中文本的大小,使其在电脑上更容易阅读。进入IDLE首选项...。在字体/标签下,将大小选项更改为20,如图 A-22 所示,或者调整为更大或更小,直到你觉得易于阅读。如果需要,可以勾选加粗框,使文本更粗。根据需要自定义字体,选择适合自己眼睛的字体。

    在 IDLE 中配置首选项

    图 A-22. 在 IDLE 中配置首选项

    现在你已准备好攻克第一章到第七章的内容。要使用第八章到第十章中的程序,请前往附录 B 并按照步骤安装 Pygame。祝编程愉快!

Linux 上的 Python

大多数 Linux 发行版,包括 Ubuntu,甚至是 Raspberry Pi 自带的 Linux 操作系统,都已经预装了一个较早版本的 Python。然而,本书中的大多数应用程序需要 Python 3。要在 Linux 上安装 Python 3,请按照以下步骤操作:

  1. 在 Dash 菜单中,进入系统工具并运行 Ubuntu 软件中心,或者根据你的 Linux 版本运行类似的应用程序。图 A-23 展示了在 Lubuntu 中运行的软件中心。

    在运行 Lubuntu Linux 的计算机上安装 Python 3

    图 A-23. 在运行 Lubuntu Linux 的计算机上安装 Python 3

  2. 搜索 python3 并找到 Idle 3。点击 添加到应用程序篮子

  3. 打开应用程序篮子标签,点击 安装软件包,如 图 A-24 所示。

    安装包含 Python 3 的 Idle 3 软件包

    图 A-24. 安装包含 Python 3 的 Idle 3 软件包。

  4. 安装完成后,打开文件窗口,选择 应用程序,然后选择 编程,你应该能看到 IDLE(使用 Python-3.4),如 图 A-25 所示。

    IDLE,Python Shell 程序

    图 A-25. IDLE,Python Shell 程序

  5. 通过运行 IDLE 并输入 2 + 3 然后按 ENTER 键来测试 IDLE。输入 print("Hello, world.") 然后按 ENTER 键。IDLE 应该会如 图 A-26 所示进行响应。

    通过运行 IDLE 测试 Python。你已经准备好编写代码了!

    图 A-26. 通过运行 IDLE 测试 Python。你已经准备好编写代码了!

    你已经准备好尝试 第一章 到 第七章 的所有程序。要使用 第八章 到 第十章 中的程序,参见附录 B 了解如何为 Linux 安装 Pygame。祝编程愉快!

附录 B. Pygame 在 Windows、Mac 和 Linux 上的设置

安装 Python 后(参见附录 A),你需要安装 Pygame 以运行第八章到第十章中的动画和游戏。本附录将帮助你完成安装。如果你在学校或工作单位的电脑上安装 Pygame,可能需要 IT 部门的帮助或许可才能完成安装。如果遇到问题,向 IT 部门寻求帮助。

Windows 上的 Pygame

对于 Windows,我们将使用适用于 Python 3.2 的 Pygame 1.9.2 版本(有关 Python 3.2.5 设置的帮助,请参见附录 A)。

  1. 访问 pygame.org/,然后点击左侧的 Downloads 链接,如图 B-1 所示。

    点击“下载”链接。

    图 B-1. 点击“下载”链接。

  2. 在 Windows 部分,找到 pygame-1.9.2a0.win32-py3.2.msi 的链接,点击下载安装程序,如图 B-2 所示。

    下载适用于 Windows 的安装程序。

    图 B-2. 下载适用于 Windows 的安装程序。

  3. 下载完成后,打开 Downloads 文件夹,找到 pygame-1.9.2a0.win32-py3.2 Windows 安装程序文件,如图 B-3 所示。双击该文件开始安装。如果弹出“安全警告”窗口,请点击 运行。Windows 只是提醒你软件正在尝试安装到你的电脑上。

    双击“下载”文件夹中的安装程序。

    图 B-3. 双击 Downloads 文件夹中的安装程序。

  4. 安装程序可能会询问你是否希望为所有用户安装 Pygame,还是仅为你自己安装。通常最好选择 为所有用户安装,但如果在学校或办公室不允许这么做,或者无法使其正常工作,可以尝试选择 仅为我安装。然后点击 下一步 >,如图 B-4 所示。

    为所有用户安装。

    图 B-4. 为所有用户安装。

  5. 程序应该能够从附录 A 中找到你的 Python 3.2.5 安装。选择从注册表中选择 Python 3.2。点击下一步 >继续安装,如图 B-5 所示。(如果你在学校或公司安装时遇到问题,IT 支持人员可能需要选择其他位置的 Python 安装选项。)

    从注册表中选择 Python 3.2。

    图 B-5. 从注册表中选择 Python 3.2。

  6. 完成安装程序后,点击完成退出,如图 B-6 所示。

    点击“完成”退出。

    图 B-6. 点击“完成”退出。

  7. 前往开始程序Python 3.2IDLE(Python GUI),如图 B-7 所示。(在 Windows 8 及更高版本上,你可以按 Windows/开始按钮,打开搜索工具,输入IDLE。)

    从开始菜单打开 IDLE。

    图 B-7. 从开始菜单打开 IDLE。

  8. 在 Python shell 编辑器中,输入 import pygame 并按下 ENTER 键。Python shell 应该会以 >>> 做出响应,如图 B-8 所示。如果是这样,那么说明 Pygame 安装正确,已准备好使用。

    在 Python shell 中导入 Pygame。

    图 B-8. 在 Python shell 中导入 Pygame。

    现在你已经准备好运行从第八章到第十章的程序了。祝编程愉快!

Pygame for Mac

在 Mac 上安装 Pygame 比在 PC 上更复杂。你有三种选择:

  1. 如果你有 Windows PC 的使用权限,可能会发现安装 Windows 版本的 Python 和 Pygame 更容易,这样就能运行从第八章到第十章的程序。如果选择这个选项,请按照 Windows 上的 Python 安装指南中的步骤操作。然后,按照 Pygame for Windows 中的步骤操作。

  2. 你可以安装较旧版本的 Python,例如 Python 2.7.9,并搭配 Pygame 1.9.2 用于 OS X,以便运行第八章到第十章中的 Pygame 程序。安装 Python 2.7.9 和 Pygame 1.9.2 比让 Pygame 与 Python 3.4.2 配合工作要容易。但 Python 2 和 3 之间有一些区别,因此对于第一章到第七章的内容,我建议使用 Python 3.4.2,以确保示例能够正常运行。然后,对于第八章到第十章,你可以使用 Python 2.7 和 Pygame 1.9.2 来运行 Pygame 示例。如果选择这个选项,请遵循下一节中的 Python 2.7 和 Pygame 1.9.2 中的步骤。

  3. 要在你的 Mac 上安装适用于 Python 3.4 的 Pygame,请查看在线说明,地址为 www.nostarch.com/teachkids/。如果你在学校或工作中进行此操作,你几乎肯定需要 IT 支持。把在线说明交给你的 IT 专业人员,让他们作为指南来使用。

Python 2.7 和 Pygame 1.9.2

较新的 Mac 设备预装了一个由 Apple 提供的 Python 2.7 版本,作为 OS X 的一部分。但 Apple 提供的 Python 版本可能无法与 Pygame 安装程序兼容。我建议在尝试安装 Pygame 之前,先从 python.org/ 安装最新版的 Python 2.7。

  1. 要在你的 Mac 上安装 Python 2.7,请返回附录 A,然后再次按照 Mac 上的 Python 部分的步骤进行。但这一次,不是从 Mac 下载页面 python.org/ 下载 3.4.2 安装程序,而是下载并运行 2.7 安装程序(截至本文写作时为 2.7.9),如图 B-9 所示。

    安装 Python 2.7

    图 B-9. 安装 Python 2.7。

  2. 安装 Python 2.7 的过程应该类似于 3.4 的安装程序。继续按照附录 A 中 Mac 上的 Python 部分的步骤进行,直到完成安装。

  3. 检查你的 应用程序 文件夹。现在应该除了你的 Python 3.4 文件夹外,还有一个 Python 2.7 文件夹,如图 B-10 所示。

    你应该同时安装 Python 2.7 和 Python 3.4。

    图 B-10. 你应该同时安装 Python 2.7 和 Python 3.4。

  4. 访问 pygame.org/,进入下载页面,下载适用于 Python 2.7 的 Pygame 1.9.2 安装程序:pygame-1.9.2pre-py2.7-macosx10.7.mpkg.zip

  5. 通过按住 CONTROL 键,点击文件,并从弹出菜单中选择 打开方式安装程序,来运行 Pygame 安装程序。安装步骤将与安装 Python 时类似:点击 继续 几次,接受许可协议,并选择安装驱动器。安装完成后,点击 关闭

  6. 为了测试 Pygame 的安装,进入你的 应用程序 文件夹,选择 Python 2.7,并打开 IDLE。在 Python 2.7 的 IDLE 中,输入 import pygame。IDLE 应该会显示 >>>,如 图 B-11 所示。

    在 Python shell 中导入 Pygame。

    图 B-11. 在 Python shell 中导入 Pygame。

  7. 你可能会看到像 图 B-12 中那样的弹出通知,提示你需要安装 X11,这是 Pygame 使用的窗口系统。点击 继续 进入 XQuartz 网站,* xquartz.macosforge.org/*。下载 XQuartz-2.7.7.dmg,打开文件并运行安装程序。

    点击继续以安装 X11。

    图 B-12. 点击继续以安装 X11。

  8. 若要运行 第八章至 第十章中的 Pygame 程序,请使用 Python 2.7 IDLE,而不是 Python 3.4 IDLE。

注意

在配备 Retina 显示屏的新款 Mac 上,使用 Python 2.7 运行 Pygame 的效果与其他电脑有所不同,因为 Retina 显示屏使用了更高的屏幕分辨率。你的程序应该能正常运行,但它们会出现在屏幕的较小区域。

Linux 上的 Pygame

类似于在 Mac 上安装 Pygame,你在 Linux 上有两种安装 Pygame 的选择:

  1. 你可以为 Python 2 安装 Pygame,这是你使用的 Linux 版本可能已经预安装的 Python 版本。对于第一章到第七章,你需要使用 Python 3,因此请按照附录 A 中的说明操作,并使用该版本的 IDLE 来完成前七章中的应用程序。然后,对于第八章到第十章,你可以使用 Python 2 的 Pygame 来运行这些章节中的 Pygame 示例。如果你选择此选项,请在下一节中按照 Python 2 的 Pygame 中的步骤操作。

  2. 要在 Linux 上为 Python 3.4 安装 Pygame,请参考在线说明 www.nostarch.com/teachkids/。如果你是在学校或工作中进行此操作,可能需要 IT 支持。将这些在线说明提供给你的 IT 专业人员,让他们作为参考指南。

Python 2 的 Pygame

大多数 Linux 操作系统已预装 Python,通常是 Python 2。第八章到第十章中的基于游戏和图形的应用可以在这个旧版本的 Python 上正常运行。以下步骤将帮助你在 Linux 系统上安装并运行 Pygame。

  1. 在 Dash 菜单中,进入系统工具,运行 Synaptic 包管理器或适用于你 Linux 版本的类似应用程序。图 B-13 显示了 Lubuntu 上运行的包管理器。

    在 Linux 上安装 Python 2 的 Pygame

    图 B-13. 在 Linux 上安装 Python 2 的 Pygame

  2. 搜索 python-pygame。在搜索结果中勾选 python-pygame 旁边的框,并点击 应用 完成安装。

  3. 运行 系统工具终端(或 XTerm 或适用于你 Linux 版本的类似应用程序)。你可以在终端窗口中输入 python2 启动 Python 2。然后,在 >>> 提示符下输入 import pygame 来测试 Pygame 的安装,如图 B-14 所示。如果 Pygame 成功导入,Python 应该会以 >>> 响应,表示安装成功。

    你可以从 Linux 命令行终端测试 Python 2 的 Pygame 安装情况。

    图 B-14. 你可以从 Linux 命令行终端测试 Python 2 的 Pygame 安装情况。

  4. 你可以使用软件中心(如 Linux 上的 Python 所示)或在图 B-13 中显示的 Synaptic 包管理器来搜索并安装适用于 Python 2 的 IDLE。运行来自第八章到第十章的 Pygame 应用时,使用此版本的 IDLE。

附录 C. 构建你自己的模块

在本书中,你已经将像 turtlerandompygame这样的模块导入到你的程序中,以便添加绘图、生成随机数和动画图形的功能,而不需要从头编写它们。但你知道吗,你也可以编写自己的模块并将其导入到程序中?Python 使得构建模块变得简单,你可以保存有用的代码,并在多个程序中使用它。

要创建可重用的模块,我们像编写其他程序文件一样,在 IDLE 的文件编辑窗口中编写模块,并将其保存为一个新的.py文件,文件名就是模块的名称(例如,colorspiral.py可能是一个用于绘制彩色螺旋的模块的文件名)。我们在模块中定义函数和变量。然后,为了在另一个程序中重用它们,我们通过键入import和模块名称将模块导入到程序中(例如,import colorspiral将使程序能够使用colorspiral.py中的代码来绘制彩色螺旋)。

为了练习编写我们自己的模块,让我们创建一个实际的colorspiral模块,看看它如何帮助我们避免重复编写代码。

构建 colorspiral 模块

让我们创建一个colorspiral模块,帮助我们在程序中快速轻松地绘制螺旋,只需调用import colorspiral。将以下代码输入到新的 IDLE 窗口,并将其保存为colorspiral.py

colorspiral.py

➊ """A module for drawing colorful spirals of up to 6 sides"""
   import turtle
➋ def cspiral(sides=6, size=360, x=0, y=0):
➌     """Draws a colorful spiral on a black background.

       Arguments:
       sides -- the number of sides in the spiral (default 6)
       size -- the length of the last side (default 360)
       x, y -- the location of the spiral, from the center of the screen
       """
       t=turtle.Pen()
       t.speed(0)
       t.penup()
       t.setpos(x,y)
       t.pendown()
       turtle.bgcolor("black")
       colors=["red", "yellow", "blue", "orange", "green", "purple"]
       for n in range(size):
           t.pencolor(colors[n%sides])
           t.forward(n * 3/sides + n)
           t.left(360/sides + 1)
           t.width(n*sides/100)

这个模块导入了 turtle模块,并定义了一个名为cspiral()的函数,用于绘制不同形状、大小和位置的彩色螺旋。让我们看看这个模块和我们编写的其他程序之间的区别。首先,在➊处,我们有一个特殊的注释,称为文档字符串。文档字符串是一种向我们打算重用或与他人共享的文件添加文档的方式;在 Python 中,模块应该包含文档字符串,以帮助未来的用户理解该模块的功能。文档字符串总是模块或函数中的第一个语句,每个文档字符串都以三重双引号""",三个双引号连在一起且中间没有空格)开头和结束。文档字符串之后,我们导入 turtle模块——是的,我们可以将模块导入到我们的模块中!

在➋位置,我们定义了一个名为cspiral()的函数,该函数最多接受四个参数——sidessizexy——用于指定螺旋的边数、螺旋的大小,以及螺旋从海龟屏幕中心开始的(xy)位置。cspiral()函数的文档字符串从➌开始;这个多行文档字符串提供了关于函数的更具体信息。文档字符串的第一行以三重双引号开始,描述了函数的总体功能。接下来,我们留空一行,随后列出了该函数接受的参数。通过这些文档,未来的用户可以轻松阅读函数接受哪些参数以及每个参数的含义。函数的其余部分是绘制彩色螺旋的代码,类似于第二章、第四章和第七章中的代码。

使用 Colorspiral 模块

一旦我们完成并保存了colorspiral.py,我们就可以将它作为模块导入到另一个程序中。创建一个新的文件,在 IDLE 中保存为MultiSpiral.py,并将其与colorspiral.py保存在同一文件夹中。

MultiSpiral.py

import colorspiral
colorspiral.cspiral(5,50)
colorspiral.cspiral(4,50,100,100)

这个三行程序导入了我们创建的colorspiral模块,并使用该模块的cspiral()函数在屏幕上绘制了两个螺旋,如图 C-1 所示。

通过三行程序创建的两个彩色螺旋,感谢 colorspiral.py 模块

图 C-1. 通过三行程序创建的两个彩色螺旋,感谢colorspiral.py模块

使用colorspiral模块,程序员想要创建彩色螺旋时,只需导入该模块并调用colorspiral.cspiral()即可!

重新使用 Colorspiral 模块

让我们重用colorspiral模块来绘制 30 个随机的彩色螺旋。为此,我们将导入我们之前使用过的另一个模块random。在 IDLE 中输入以下八行代码并将文件保存为SuperSpiral.py

SuperSpiral.py

import colorspiral
import random
for n in range(30):
    sides = random.randint(3,6)
    size = random.randint(25,75)
    x = random.randint(-300,300)
    y = random.randint(-300,300)
    colorspiral.cspiral(sides, size, x, y)

该程序以两个import语句开始:一个用于我们创建的colorspiral模块,另一个用于我们在整个书中使用的random模块。for循环将执行 30 次。该循环生成四个随机值,分别用于确定边数(3 到 6 之间)、螺旋的大小(25 到 75 之间),以及用于在屏幕上绘制螺旋的 x 和 y 坐标,坐标范围在(–300,–300)到(300,300)之间。(请记住,海龟的原点(0,0)位于绘图屏幕的中心。)最后,每次循环都会调用我们模块中的colorspiral.cspiral()函数,绘制一个带有随机生成属性的彩色螺旋。

尽管这个程序只有八行代码,但它可以生成令人惊艳的图形,参考 图 C-2。

colorspiral 模块使得 SuperSpiral.py 仅用八行代码就能生成一个美丽的多螺旋拼贴。

图 C-2. colorspiral模块使得 SuperSpiral.py 仅用八行代码就能生成一个美丽的多螺旋拼贴。

创建可重用模块的能力意味着你可以花更多的时间解决新问题,而不是重复编写已有的解决方案。每当你构建一个有用的函数或一组函数,想要反复使用时,你可以创建一个模块来自己使用或与其他程序员分享。

其他资源

Python 的官方文档中包含了关于模块和 Python 语言的更多信息,访问地址是 docs.python.org/3/。Python 教程中有一个关于模块的章节,地址是 docs.python.org/3/tutorial/modules.html。在你学习新的 Python 编程技能时,可以利用这些资源来丰富你的编程工具集。

附录 D. 术语表

在学习编程时,你会遇到许多日常用语,这些词你已经理解。然而,也有一些是全新的,或者在计算机程序员的世界中有着特殊的意义。本术语表定义了你在本书中将遇到的几种新词汇,以及一些在编码世界中赋予新意义的常见词汇。

算法

执行任务的一组步骤,如食谱。

动画

当类似的图像迅速一个接一个地显示时,产生的运动错觉,如卡通动画中的效果。

应用程序

应用程序的简称,一种执行某些有用(或有趣!)功能的计算机程序。

追加

将某物添加到末尾;例如,将字母添加到字符串的末尾,或将元素添加到列表或数组的末尾。

参数

传递给函数的值;在语句 range(10) 中,10 就是一个参数。

数组

一种有序的值或对象列表,通常是相同类型的,通过其索引或在列表中的位置进行访问。

赋值

设置变量的值,如 x = 5,将值 5 赋给变量 x

一组编程语句。

布尔值

一个可以为真或为假的值或表达式。

定义某种类型的对象中包含的函数和值的模板。

代码

程序员用计算机能理解的语言编写的语句或指令。

碰撞检测

检查两个虚拟对象是否接触或碰撞,如 Pong 中的球和挡板。

连接

将两个文本字符串合并为一个字符串。

条件表达式

一条语句,允许计算机测试一个值并根据测试结果执行不同的操作。

常量

程序中的一个命名值,保持不变,例如 math.pi(3.1415...)。

声明

一条或一组语句,告诉计算机一个变量或函数名称的含义。

元素

列表或数组中的单个项目。

事件

一种计算机可以检测到的活动,如鼠标点击、值变化、按键、计时器滴答声等。响应事件的语句或函数称为事件处理器事件监听器

表达式

任何有效的值集、变量、运算符和函数,能够产生一个值或结果。

文件

计算机在某种存储设备(如硬盘、DVD 或 USB 驱动器)上存储的数据或信息集合。

for 循环

一条编程语句,允许在给定值范围内重复执行一段代码。

动画、视频或计算机图形中的一个移动序列中的单个图像。

每秒帧数 (fps)

动画、视频游戏或电影中,屏幕上绘制图像的速率或速度。

函数

一组命名的、可重用的编程语句,用于执行特定任务。

导入

从另一个程序或模块将可重用的代码或数据引入程序。

索引

元素在列表或数组中的位置。

初始化

给一个变量或对象赋予其第一个或初始值。

输入

输入到计算机中的任何数据或信息;输入可以来自键盘、鼠标、麦克风、数码相机或任何其他输入设备。

迭代版本控制

反复对程序进行小幅修改或改进,并保存为新版本,例如Game1Game2,依此类推。

关键字

一个特殊的保留字,在某种特定的编程语言中具有特定含义。

列表

一个容器,用于存储有序的值或对象。

循环

一组指令,直到满足某个条件才会停止重复执行。

模块

一组相关的文件或文件集合,其中包含可以在其他程序中重用的变量、函数和类。

嵌套循环

嵌套循环。

对象

包含关于类单个实例信息的变量,例如Sprite类中的单个精灵。

运算符

代表某种操作或比较并返回结果的符号或符号集合,例如+-*//<>==等。

参数

函数的输入变量,在函数定义中指定。

像素

图像元素的缩写,构成计算机屏幕上图像的小彩点。

程序

一组计算机能理解的指令。

伪随机

在一个序列中看起来是随机的或不可预测的值,足够随机以模拟掷骰子或抛硬币。

随机数

一种不可预测的数字序列,在某个范围内均匀分布。

范围

一组有序的值,介于已知的起始值和结束值之间;在 Python 中,range函数返回一个值的序列,例如从010

RGB 颜色

红绿蓝颜色的缩写,是一种通过混合红色、绿色和蓝色光来再现每种颜色的方式。

Shell

一种基于文本的命令行程序,它读取用户输入的命令并执行它们;IDLE 是 Python 的 Shell。

排序

将列表或数组中的元素按某种顺序排列,例如按字母顺序。

字符串

一系列字符,可以包括字母、数字、符号、标点符号和空格。

语法

一种编程语言的拼写和语法规则。

透明度

在图形学中,看到图像部分透明的能力。

变量

在计算机程序中,一个可以变化的命名值。

while 循环

一条编程语句,允许一段代码在某个条件为真时被重复执行。

附录 E:更新

访问 www.nostarch.com/teachkids/ 获取更新、勘误、项目的程序文件、编程挑战和其他信息。

更多适合好奇孩子的智能书籍!

没有说明文字的图片

孩子们的 Python

编程的趣味入门

作者 JASON R. BRIGGS

2012 年 12 月,344 页,$34.95

ISBN 978-1-59327-407-8

全彩

没有说明文字的图片

孩子们的 JavaScript

编程的趣味入门

作者 NICK MORGAN

2014 年 12 月,336 页,$34.95

ISBN 978-1-59327-408-5

全彩

没有说明文字的图片

LAUREN IPSUM

关于计算机科学和其他不太可能的事情的故事

作者 CARLOS BUENO

2014 年 12 月,192 页,$16.95

ISBN 978-1-59327-574-7

全彩

没有说明文字的图片

乐高® MINDSTORMS® EV3 探索书

机器人构建与编程的初学者指南

作者 LAURENS VALK

2014 年 6 月,396 页,$34.95

ISBN 978-1-59327-532-7

全彩

没有说明文字的图片

用 Python 做数学

用编程探索代数、统计学、微积分等!

作者 AMIT SAHA

2015 年春季,304 页,$29.95

ISBN 978-1-59327-640-9

没有说明文字的图片

使用 Scratch 学习编程

通过游戏、艺术、科学和数学的视觉编程入门

作者 MAJED MARJI

2014 年 2 月,288 页,$34.95

ISBN 978-1-59327-543-3

全彩

800.420.7240 或 415.863.9900 | sales@nostarch.com | www.nostarch.com

posted @ 2025-11-28 09:41  绝不原创的飞龙  阅读(10)  评论(0)    收藏  举报