Minecraft-编程学习指南-全-

Minecraft 编程学习指南(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

image

欢迎来到《用 Minecraft 学编程》!在本书中,你将学习如何使用一种叫做 Python 的编程语言来控制 Minecraft 世界中的一切。你将学习编程是如何工作的,然后运用所学的知识用代码建造建筑、编写小游戏,并将无聊的 Minecraft 物品转变成令人兴奋的新玩具。到本书结束时,你应该掌握将所有狂野创意变为现实所需的技能。

编程是富有创意和想象力的,就像 Minecraft 一样。通过本书中学习到的技能,你将能够创造各种各样的东西(例如游戏、应用程序和有用的工具),不仅限于使用 Minecraft 的程序。这是你成为一名出色程序员和 Minecraft 大师的第一步!

为什么要学编程?

学习编程的一个主要原因是它教你如何解决问题。你将学会如何将大问题拆解成更小的部分,从而更容易处理。你尝试解决的许多问题将要求你以创造性的方式思考并测试不同的想法。

编程的另一个好处是,它教会你逻辑思维,帮助你更好地理解和规划程序的结构和流程。即使在没有使用计算机代码的情况下,解决问题、创造力和逻辑思维也是非常宝贵的技能。

编程职业也非常有回报。你每天都能为问题提出创造性的解决方案。即使你不选择成为一名程序员,把编程当作爱好也是非常有趣和充实的。事实上,我就是从编程爱好开始的,最终转变成了全职工作。

最重要的是,编程可以非常有趣!没有什么比看到自己创建的程序做出酷炫的事情更令人满足的了!

为什么选择 Python?

那么,为什么要选择用 Python 编程呢?Python 是任何初学者程序员的理想第一语言。它易于阅读和编写,而且足够强大,能够创建真正的计算机程序。Python 是世界上最流行的编程语言之一!

为什么选择 Minecraft?

Minecraft 非常受欢迎,因为它既有趣又富有创意。在你的 Minecraft 世界中,你拥有完全的自由去创造。你可以让自己的想象力自由驰骋。通过将 Minecraft 与自己的 Python 程序结合,你可以对 Minecraft 拥有更多控制,并解锁更多的创意。你将能够做一些单靠 Minecraft 无法做到的事情(例如,在短短几秒钟内建造一座庞大的建筑)。

有时,开始编程是困难的,因为你需要学习一堆没有任何激动人心功能的代码。但通过将 Python 与 Minecraft 结合,你将能够即时看到自己编写的精彩程序在 Minecraft 世界中的效果。

本书内容简介

每一章都专注于一个单独的 Python 概念。当你逐章学习时,你将不断扩展 Python 编程知识。每一章都包括了 Python 如何工作的解释、展示 Python 实际应用的示例,以及 Minecraft 任务。在这些任务中,你将编写与 Minecraft 互动的程序。我会提供一些框架代码,接下来的工作就交给你来填充空缺并完成程序。通过这些任务,你将培养成为程序员所必需的解决问题的能力。

让我们回顾一下每一章将探索的内容。

第一章:为冒险做好准备 帮助你设置好 Python 和 Minecraft,让你准备好向前进,开始编程冒险!

第二章:通过变量进行瞬间传送 教你如何通过操控变量来瞬间传送玩家。你将了解变量及其如何在程序中存储数据。你甚至会在此基础上发挥你高超的传送技巧,进行一场奇妙的传送之旅,探索你的世界。

第三章:通过数学快速构建和远距离旅行 向你展示如何利用数学赋予自己超能力,并以超快的速度构建你的创作。你想在不到一秒钟的时间内建造一个 Minecraft 房子吗?数学运算符能帮助你做到这一点。你想跳得超级高吗?数学运算符也能助你一臂之力!

• 在 第四章:与字符串对话 中,你将学习所有关于字符串的知识,制作一个互动聊天。在编程中,字符串是指文本。你将学习如何编写 Minecraft Python 程序,向你和你的玩家传达消息。

第五章:使用布尔值弄清楚什么是真的,什么是假的 教你如何使用布尔值和逻辑,让你的程序能够回答问题。换句话说,你将能够让程序告诉你某事是否为真或假。你的 Minecraft Python 程序能够回答各种问题:我在水下吗?我在树上吗?我靠近我的房子吗?

第六章:通过 if 语句制作迷你游戏 将布尔逻辑带到更高的层次。通过 if 语句,你将学会如何创建根据给定数据做出决策的程序。你是否曾经想过,当你在 Minecraft 中把某个方块放到特定位置时,打开一个秘密通道?你可以通过 if 语句做到这一点!

第七章:使用 while 循环举行舞会和花卉游行 向你展示了一种非常酷的方式,使用循环让程序重复运行。你将能够自动化代码,创造出令人惊叹的效果。例如,想象一下,一条花朵的轨迹跟随着玩家,或者一个神奇的舞池闪烁着不同的颜色。这些是我最喜欢用来展示给大家的程序。

• 在第八章:函数赋予你超能力中,你将学习如何通过使用函数快速构建整个森林和城市。你还将学习如何通过重用程序的部分来简化你的编程工作。

• 在第九章:用列表和字典打击物体中,你将学习如何使用列表制作迷你游戏。列表是一个强大的编程概念,因为它允许你将重要的信息存储在一个地方。你将用列表让你的程序记住你用剑击中的所有方块,再加几行代码,你就能将其转化为一个迷你游戏。非常酷吧!

第十章:用 for 循环实现 Minecraft 魔法教你如何使用for循环构建结构,比如金字塔。你甚至可以用for循环绘制像素艺术或复制你的 Minecraft 建筑。你可以建造一座壮丽的雕像,然后复制它,创造一整支雕像军队!

• 在第十一章:使用文件和模块保存和加载建筑中,你将使用程序创建和编辑文件,保存你所建造的物体,并将它们加载到不同的游戏世界中。换句话说,你将把你的建筑转换成文件,可以随时转移到任何地方。想保存你建造的那座令人难以置信的豪宅吗?没问题!有了文件,你可以随时保存并加载它。

第十二章:用面向对象编程变得更强大介绍了一些高级话题:类、对象和继承。完成这一章后,你将成为 Python 大师。在任务中,你将从一些构建建筑的代码开始,然后使用类、对象和继承,通过只增加几行代码,轻松构建出复制品和变体,比如村庄和酒店。

方块 ID 备忘单是一个方便的 Minecraft 方块 ID 参考,你可以在程序中使用。

在线资源

本书的所有代码和资源都可以在其配套网站上找到,* www.nostarch.com/pythonwithminecraft/*。如果你卡住了,想查看解决方案,或者想修改代码来创建自己的精彩程序,可以下载 Minecraft 任务的代码!你也可以下载安装文件—我将在第一章中引导你完成安装过程。

冒险开始!

我希望你和我一样兴奋,迫不及待地开始。写这本书和制作所有帮助你学习编程的 Minecraft 任务,我真是非常享受。让我们开始吧!

第一章:1

为你的冒险做好准备

image

在你开始为 Minecraft 世界制作酷炫的 Python 程序之前,你需要在计算机上安装 Minecraft、Python 和其他一些组件。在本章中,我将向你展示如何安装和运行所有必需的软件。

你可以在 Windows PC 或 Mac 上使用 Minecraft,或者在树莓派计算机上使用 Minecraft: Pi Edition。如果你使用的是 Windows PC,继续阅读。如果你使用的是 Mac,翻到第 11 页的“设置 Mac”部分。如果你使用的是树莓派,翻到第 18 页的“设置树莓派”部分。

注意

有关其他平台的信息以及本说明的更新,请访问 www.nostarch.com/pythonwithminecraft/

设置你的 Windows PC

你需要安装五个组件才能通过 Python 控制 Minecraft:

• Minecraft

• Python 3

• Java

• Minecraft Python API

• Spigot Minecraft 服务器

在本节中,我将引导你完成如何在计算机上安装这些组件的步骤。我们从 Minecraft 开始。

安装 Minecraft

如果你已经拥有 Minecraft 并且 PC 上安装了最新版本,跳过至第 3 页的“安装 Python”部分。如果你不确定是否安装了最新版本的 Minecraft,按照本节的步骤安装最新版本。

如果你还没有购买游戏,你可以在 Minecraft 的官方网站上购买一份 minecraft.net/。你可能需要找一个成年人帮你!记得记下购买 Minecraft 时使用的用户名和密码——你稍后登录时会用到它们。

在购买 Minecraft 之后,按照以下步骤将 Minecraft 安装到你的 PC 上:

  1. 访问 minecraft.net/download

  2. 在“Minecraft for Windows”部分,找到Minecraft.msi链接并点击它进行下载。如果系统提示保存或打开文件,选择保存文件

  3. 等待文件下载并打开它。如果弹出对话框询问是否要运行该文件,点击运行。不用担心,我们知道这个文件是安全的!

  4. 当 Minecraft 安装向导打开时,点击下一步。然后再次点击下一步。最后点击安装

  5. 系统可能会询问你是否要安装 Minecraft,当然你会选择安装!点击。稍等片刻,Minecraft 将开始安装。我在安装游戏时喝了杯水,还吃了块饼干。

  6. 安装完成后,点击完成

Minecraft 现在应该已经安装好了。

你知道什么主意最棒吗?当然是玩 Minecraft 了。花点时间把它设置好:

  1. 要打开 Minecraft,点击开始菜单(或按下键盘上的 Windows 键),在程序列表中找到 Minecraft,并点击图标。

  2. Minecraft 将启动并可能安装更新。

  3. 接下来将打开登录窗口。输入你在购买 Minecraft 时使用的用户名和密码,然后点击登录

  4. 点击开始游戏。Minecraft 将在打开前下载一些更新。

  5. 最后,点击单人游戏创建新世界。为你的世界命名,随意选择名称,然后点击创建新世界。世界将开始生成,你可以尽情玩耍。

玩得开心!如果你以前从未玩过 Minecraft,试着在游戏中玩一段时间,直到 Minecraft 世界变黑。小心怪物!请注意,当你将 Minecraft 与 Python 一起使用时,你将进入一个多人游戏世界,这与当前世界不同。我们将在“运行 Spigot 并创建游戏”部分的第 7 页中详细讲解。

回到工作上!现在是时候安装 Python 了。要解锁 Minecraft 中的光标,只需按键盘上的 ESC 键。在继续安装之前,先关闭 Minecraft。

安装 PYTHON

Python 是你将在本书中学习的编程语言。现在让我们安装它。

  1. 访问*www.python.org/downloads/

  2. 点击标有下载 Python 3.5.0的按钮。(这是本文写作时 Python 3 的最新版本,但你可能会看到更新的版本。安装最新版本。)

  3. Python 将开始下载。如果询问你是否选择保存或打开文件,选择保存文件

  4. 当安装程序下载完成后,点击它。如果弹出对话框询问是否运行该文件,点击运行

  5. 当安装程序打开时,勾选底部的Add Python 3.5 to Path复选框,如图 1-1 所示。然后点击立即安装

    image

    图 1-1:确保勾选 Add Python 3.5 to Path。

  6. 安装过程中可能会弹出一个对话框,询问是否允许程序在计算机上安装软件。点击,然后等待 Python 安装完成。我站起来关闭窗口时,安装已经完成。

  7. 点击完成。Python 现在已安装。

安装 JAVA

现在 Minecraft 和 Python 都已安装,你需要进行一些设置,使它们能够相互通信。你将使用一个叫做 Spigot 的程序来实现这一点,但为了让 Spigot 正常工作,你首先需要确保计算机上安装了 Java。我们现在来做这个检查。

首先,检查 Java 是否已经安装:

  1. 点击开始菜单(或按下键盘上的 Windows 键),在搜索框中输入cmd。打开名为 cmd 的程序。

  2. 你将看到一个黑色背景的窗口和提示符(我的显示为C:\Users\Craig>)。在提示符下,输入java -version并按回车键。

  3. 如果你看到类似于图 1-2 中的信息,说明 Java 已经安装。请跳转到第 6 页的“安装 Minecraft Python API 和 Spigot”部分。

  4. 如果你收到提示消息说 Java 无法识别,请按照以下说明进行安装。

image

图 1-2:输入java -version命令后,我可以看到 Java 已安装。

安装 Java 的步骤如下:

  1. 访问www.java.com/en/download/

  2. 点击免费下载 Java按钮。然后点击同意并开始免费下载按钮。

  3. 安装程序下载完成后,点击它。如果弹出对话框询问是否允许该程序对你的计算机进行更改,请选择

  4. 当安装程序打开时,点击安装

  5. 这一点非常重要!如果出现提示询问是否安装其他程序,如 Ask 搜索应用、Yahoo!搜索栏或其他程序,请取消勾选该选项,这样就不会安装这些额外的程序。这些都是你不需要的程序。

  6. 你可能会被问到是否想将 Yahoo!设置为主页。你可能不需要。选择不更新浏览器设置并点击下一步

  7. 等待 Java 安装完成。在它安装期间,我给朋友发了个简短的消息。安装完成后点击关闭

现在让我们检查 Java 是否已正确安装:

  1. 点击开始菜单,在搜索框中输入cmd,打开 cmd 程序。

  2. 在 cmd 窗口中,输入java -version并按回车键。

  3. 如果你看到类似于图 1-2 中的信息,说明 Java 已正确安装。如果出现“‘Java’不是内部或外部命令,也不是可操作的程序或批处理文件”的错误,说明 Java 没有正确安装。要解决此问题,请尝试重新安装 Java 并再次运行。如果重新安装后仍然出现此错误,请访问www.java.com/en/download/help/path.xml获取更多信息。

就这样!Java 已经安装完成,可以运行 Minecraft 服务器了!接下来我们就开始这个部分。

安装 Minecraft Python API 和 Spigot

接下来,你需要在电脑上安装 Minecraft Python API 和 Minecraft 服务器。

API代表应用程序编程接口。它让程序能够与其他人创建的应用程序进行通信。在本例中,Minecraft Python API 允许你用 Python 编写的程序与 Minecraft 进行通信。例如,你可以编写一个 Python 程序,利用 API 指示 Minecraft 在游戏中创建一个方块,或者改变玩家的位置。

标准的 Minecraft 单人游戏不支持 API。相反,你的程序将与 Minecraft服务器进行交互,服务器支持使用 API。Minecraft 服务器通常用于在线模式,让多人能够在同一个游戏世界中一起玩耍。你也可以在自己的电脑上运行一个服务器,独自游戏。无论是多人游戏还是单人游戏的 Minecraft 服务器,都允许你在 Minecraft 中使用 API。在本书中,你将使用一个名为 Spigot 的单人 Minecraft 服务器。

现在你已经了解了 API 和服务器的作用,接下来我们将把它们安装到你的计算机上。我已经创建了一个方便的下载文件,帮助你快速完成设置。只需按照以下步骤操作:

  1. 访问www.nostarch.com/pythonwithminecraft/并下载适用于 Windows 的Minecraft Tools.zip文件。

  2. 下载文件后,右键点击文件并选择全部解压。系统会询问你想将解压后的文件放在哪里。点击浏览按钮,进入你的我的文档文件夹。点击新建文件夹按钮,并命名新文件夹为Minecraft Python。选择该文件夹并点击确定。点击解压以解压文件。

  3. 进入我的文档文件夹中的Minecraft Python文件夹,你应该能看到已解压的文件。

  4. 打开Minecraft Tools文件夹。文件夹内容如图 1-3 所示。

  5. 双击名为Install_API的文件。这将打开一个新窗口并安装 Minecraft Python API。如果出现警告提示,请点击仍然运行

  6. 安装完成后,按任意键完成操作。

注意

如果出现“pip 未被识别”的错误提示,说明你没有正确安装 Python。请返回到 “安装 Python” 的第 3 页,重新安装 Python。确保勾选了“将 Python 3.5 添加到 Path”的复选框。

Minecraft Python API 和 Minecraft 服务器现在已安装完毕。最后一步是运行服务器。我们将在下一部分进行操作。

image

图 1-3: Minecraft Tools 文件夹

运行 Spigot 并创建游戏

当 Spigot 第一次运行时,它将为你创建一个 Minecraft 世界。要启动 Spigot,请按照以下步骤操作:

  1. 进入你的Minecraft Python文件夹并打开Minecraft Tools文件夹。

  2. Minecraft Tools文件夹中,双击Start_Server文件。如果弹出提示询问是否允许访问,请点击允许

  3. Spigot 将启动你的 Minecraft 服务器。你会看到一个窗口弹出,里面有很多文字,这时 Spigot 正在为你生成游戏世界。当 Spigot 完成后,你的屏幕将显示如图 1-4 所示的内容。请保持此窗口打开。

    image

    图 1-4:Spigot 服务器已准备好

  4. 打开 Minecraft 并点击多人游戏

  5. 点击添加服务器按钮。

  6. 在服务器名称框中,将你的服务器命名为 Minecraft Python World,在服务器地址框中输入 localhost,如 图 1-5 所示。然后点击 完成

    image

    图 1-5:设置服务器

  7. 双击 Minecraft Python World,Spigot 创建的世界将会打开。

让我们快速查看一下你在 Spigot 服务器上的新 Minecraft 世界。这个世界设置为创意模式,你可以自由飞行。双击空格键开始飞行。按住空格键会让你飞得更高,按住 SHIFT 键会让你向地面下降。如果你想停止飞行,只需再次双击空格键。

从新世界开始

使用服务器创建一个全新的 Minecraft 世界与在单人模式中创建新世界稍有不同。按照以下步骤创建新世界:

  1. 转到 Minecraft Python 文件夹。右键点击 Minecraft Tools 文件夹并点击 复制

  2. 右键点击 Minecraft Python 文件夹中的任何位置并点击 粘贴。这将创建一个名为 Minecraft Tools - CopyMinecraft Tools 文件夹副本。

  3. 右键点击 Minecraft Tools - Copy 文件夹并点击 重命名。我将新文件夹命名为 New World,但你可以将其命名为你想要的任何名字。

  4. 打开 New World 文件夹(或你所命名的文件夹),然后打开 server 文件夹。

  5. server 文件夹中,选择 worldworld_netherworld_the_end 文件夹,如 图 1-6 所示。按 DELETE 键删除这些文件夹。

    image

    图 1-6:我已标出需要删除的文件夹。

  6. 仍然在 server 文件夹中,点击 start 文件。(请注意,必须点击位于 server 文件夹内的 start 文件,而不是原始的 Start_Server 文件!)这将重新启动服务器并生成一个新世界。

  7. 现在当你打开 Minecraft 并进入 Minecraft Python World 时,你将看到一个新生成的世界。

你可以重复这个过程,创建任意数量的新世界。如果你想打开旧世界,仍然可以通过点击 Minecraft Tools 文件夹中的 Start_Server 文件来运行它。

要删除一个世界并用新世界替换它,只需删除你想替换的世界文件夹中的 worldworld_netherworld_the_end 文件夹。

离线游戏

如果你无法连接到互联网,尝试从 Minecraft 游戏连接到 Minecraft 服务器时会出现错误。你可以通过更改服务器的属性来解决这个问题。首先,确保你已经关闭了服务器窗口。然后打开 Minecraft Python 文件夹,接着是 Minecraft Tools 文件夹,再打开 server 文件夹。在文本编辑器(如记事本)中打开 server.properties 文件,将 online-mode 设置(见图 1-7)从 true 改为 false。保存更改后,返回 Minecraft Tools 文件夹,双击 Start_Server 文件重新启动服务器。现在你就可以离线玩了。

image

图 1-7:将高亮的设置从 true 改为 false

切换到生存模式

我已将你的 Minecraft 服务器的默认游戏模式设置为创造模式。这将使你在编写和运行 Python 程序时更加方便,因为你不必担心玩家的健康、饥饿或被攻击问题。

但你可能只是想在生存模式下测试一些程序,纯粹是为了好玩。将服务器从创造模式切换到生存模式很简单,而且可以随时切换回来。

要将服务器从创造模式切换到生存模式,按照以下步骤操作:

  1. 打开 Minecraft Tools 文件夹。然后在该文件夹中打开 server 文件夹。

  2. 找到 server.properties 文件,并用文本编辑器(如记事本)打开它。

  3. 在文件中找到 gamemode=1 这一行,并将其改为 gamemode=0,如图 1-8 所示。

    image

    图 1-8:我通过将 gamemode 设置为 0 切换到了生存模式

  4. 保存文件并关闭它。

  5. 通过点击 Start_Server 文件(位于 Minecraft Tools 文件夹中)启动服务器。当你加入 Minecraft Python World 游戏时,它现在将处于生存模式。

你可以随时切换回创造模式。只需重复这些步骤,但在第 3 步中,将 server.properties 文件中的 gamemode=0 改为 gamemode=1

现在你已经在电脑上设置好了!接下来,让我们认识一下 IDLE,这是你编写代码的地方。翻到“了解 IDLE”第 20 页。

设置你的 Mac

你需要安装五个组件,以便用 Python 控制 Minecraft:

• Minecraft

• Python 3

• Java 开发工具包(JDK)

• Minecraft Python API

• Spigot Minecraft 服务器

在这一部分,我将指导你在电脑上安装这些组件。我们从安装 Minecraft 开始。

安装 Minecraft

如果你已经拥有 Minecraft 并且安装了最新版本的 Minecraft,请跳到 “安装 Python”第 13 页。如果你不确定是否已经安装了最新版本的 Minecraft,请按照本节中的步骤安装最新版本。

如果你还没有购买这款游戏,你可以从官方的 Minecraft 网站购买一份,minecraft.net/。你可能需要找个成年人来帮你哦!记得在购买 Minecraft 时使用的用户名和密码——你稍后登录时会需要它。

在购买 Minecraft 之后,按照以下步骤在 Mac 上安装 Minecraft:

  1. 访问* minecraft.net/download*。

  2. 在 Minecraft for Mac OS X 部分,找到Minecraft.dmg链接并点击它进行下载。(如果看不到 Minecraft for Mac OS X 部分,请点击显示所有平台。)

  3. 等待文件下载(我稍微看了下窗外),然后打开它。当窗口弹出时,按照图 1-9 所示,将 Minecraft 图标拖动到应用程序文件夹中。

    image

    图 1-9:将 Minecraft 图标拖入 应用程序 文件夹进行安装。

现在 Minecraft 应该已经安装好了。

你知道什么是个好主意吗?当然是玩 Minecraft 了。花几分钟让它启动并运行吧:

  1. 要打开 Minecraft,点击 Dock 上的 Finder 图标以打开文件浏览器。

  2. 在侧边栏中,点击应用程序

  3. 应用程序文件夹中找到 Minecraft,如图 1-10 所示。双击它并选择打开

  4. 你可能会被询问是否要打开 Minecraft,因为它是从互联网下载的。点击打开

  5. Minecraft 将启动并可能安装更新。

  6. 接下来将弹出登录窗口。输入你在购买 Minecraft 时使用的用户名和密码,然后点击登录

    image

    图 1-10:在 应用程序 文件夹中找到 Minecraft。

  7. 点击开始游戏。Minecraft 将下载一些更新,然后打开。

  8. 最后,点击单人游戏创建新世界。给你的世界命名,然后点击创建新世界。世界将会生成,你可以尽情地玩耍。

玩得开心!如果你从未玩过 Minecraft,试着玩一会儿,直到 Minecraft 世界里天黑了。小心怪物哦!注意,当你使用 Python 时,Minecraft 将会是一个多人游戏世界,与这个世界不同。我们将在“运行 Spigot 并创建游戏”的第 16 页中讲到这一点。

回到正事!现在是安装 Python 的时候了。要从 Minecraft 中解放你的光标,只需按下键盘上的 ESC 键。在继续安装其他内容之前,请先关闭 Minecraft。

安装 Python

Python 是你将在本书中学习的编程语言。现在让我们开始安装它吧。

  1. 访问* www.python.org/downloads/mac-osx/

  2. 点击链接上写着Latest Python 3 Release - Python 3.5.0的部分。(这是本文写作时 Python 3 的最新版本,但你可能会看到更新的版本。请安装最新版本。)Python 将开始下载。

  3. 当安装程序下载完成后,点击它。

  4. 当安装程序打开时,点击继续三次。系统会要求你同意软件许可协议的条款,点击同意

  5. 点击安装,然后等待 Python 安装完成。我在等待时查看了天气预报。

  6. 点击关闭。Python 现在已安装完成。

安装 Java

现在 Minecraft 和 Python 都已经安装完毕,你需要配置它们,以便它们能够互相通信。你将使用一个名为 Spigot 的程序来实现这一点,但为了让 Spigot 正常工作,你首先需要在计算机上安装最新的 Java 开发工具包(JDK)。现在就来做吧:

  1. 访问www.oracle.com/technetwork/java/javase/downloads/index.html并点击Java 下载按钮。

  2. 选择接受许可协议,然后点击Mac OSX x64

  3. 当安装程序下载完成后,点击它。

  4. 当安装程序打开时,双击安装图标。

  5. 当系统提示你输入密码时,输入你的密码。

  6. 等待 Java 安装完成。当安装完成时,点击关闭

现在让我们测试一下 JDK 是否正确安装:

  1. 点击系统偏好设置

  2. 你应该能在“系统偏好设置”下看到一个 Java 图标,如图 1-11 所示。

    image

    图 1-11:Java 已安装。

就这样!Java 已经设置好,可以运行 Minecraft 服务器了!接下来我们继续进行。

安装 Minecraft Python API 和 Spigot

接下来,你需要在计算机上安装 Minecraft Python API 和 Minecraft 服务器。

API代表应用程序编程接口。它允许程序与其他人创建的应用程序进行通信。在本例中,Minecraft Python API 允许你编写的 Python 程序与 Minecraft 进行交互。例如,你可以编写一个 Python 程序,使用 API 让 Minecraft 在游戏中生成一个方块,或者改变玩家的位置。

标准的 Minecraft 单人游戏不支持 API。相反,你的程序将与 Minecraft 服务器进行交互,这样就能使用 API。Minecraft 服务器通常是在线使用,让多人可以在同一个游戏世界中一起玩。你也可以在自己的计算机上运行服务器,独自玩游戏。无论是多人游戏还是单人游戏 Minecraft 服务器,都允许你与 Minecraft 一起使用 API。在本书中,你将使用一个名为 Spigot 的单人 Minecraft 服务器来在你的计算机上运行。

现在你已经知道 API 和服务器的作用了,接下来让我们把它们安装到你的计算机上。我已经为你创建了一个方便的下载方式,让你可以快速完成这些设置。只需按照这些步骤进行即可:

  1. 前往 www.nostarch.com/pythonwithminecraft/ 并下载 MinecraftTools Mac.zip 文件。

  2. 文件下载完成后,打开 Downloads 文件夹并点击 Show in Finder

  3. 在 Finder 中,CONTROL-click 文件并选择 Copy MinecraftTools Mac.zip

  4. 前往你的 Documents 文件夹。在文件夹中,CONTROL-click 并选择 New Folder。将新文件夹命名为 MinecraftPython。确保文件夹名称中不包含空格。

  5. 打开 MinecraftPython 文件夹。在文件夹中,CONTROL-click 并选择 Paste ItemMinecraftTools Mac.zip 文件将被复制到这里。

  6. CONTROL-click 并选择 Open WithArchive Utility。当 Archive Utility 打开压缩文件后,你会看到一个名为 MinecraftTools 的新文件夹。

  7. 打开 MinecraftTools 文件夹。它的内容如 图 1-12 所示。

  8. CONTROL-click 名为 Install_API.command 的文件并选择 Open。这将打开一个新窗口。输入你的密码以安装 Minecraft Python API。

    注意

    如果你收到一个错误提示,说 Install_API.command 因为来自未识别的开发者而无法打开,请点击 System Preferences,然后点击 Security and Privacy。你会看到一条信息说 “Install_API.command 无法打开,因为它来自未识别的开发者。”点击 Open Anyway。然后窗口应该会弹出。

    image

    图 1-12: MinecraftTools 文件夹的内容

  9. 安装完成后,关闭窗口。

Minecraft Python API 和 Minecraft 服务器现在已经安装。最后一步是运行服务器。我们接下来会进行此操作。

运行 SPIGOT 和创建游戏

当 Spigot 第一次运行时,它将为你创建一个 Minecraft 世界。要启动 Spigot,请按照以下步骤操作:

  1. 前往你的 MinecraftPython 文件夹并打开 MinecraftTools 文件夹。

  2. MinecraftTools 文件夹中,CONTROL-click Start_Server 文件并选择 Open。如果出现错误信息,请前往 System Preferences,然后到 Security and Privacy,点击 Open Anyway

  3. Spigot 将启动你的 Minecraft 服务器。你会看到一个窗口弹出,里面有大量文本,当 Spigot 为你生成游戏世界时。完成后,确保保持这个窗口开启。

  4. 打开 Minecraft 并点击 Multiplayer

  5. 点击 Add Server 按钮。

  6. 在服务器名称框中,命名你的服务器为 Minecraft Python World,在服务器地址框中,输入 localhost,如 图 1-13 所示。然后点击 Done

  7. 双击 Minecraft Python World,Spigot 创建的世界将打开。

让我们快速查看一下你在 Spigot 服务器上的新 Minecraft 世界。这个世界已设置为创意模式,你可以自由飞行。双击空格键开始飞行,按住空格键可以让你飞得更高,按住 SHIFT 键则会将你拉向地面。如果你想停止飞行,只需再次双击空格键。

图片

图 1-13:添加服务器,以便将来可以轻松访问它。

从新世界开始

使用服务器创建全新的 Minecraft 世界与在单人模式下创建新世界有些不同。按照以下步骤创建一个新世界:

  1. 转到 MinecraftPython 文件夹。按住 CONTROL 键并点击 MinecraftTools 文件夹,然后选择 复制

  2. 在文件夹中任意位置按住 CONTROL 键并点击,然后选择 粘贴。这将创建一个名为 MinecraftTools copyMinecraftTools 文件夹副本。

  3. 按住 CONTROL 键并点击 MinecraftTools copy 文件夹,然后选择 重命名。我将新文件夹命名为 New World,但你可以根据自己的需要命名。

  4. 打开 New World 文件夹(或你为它命名的任何名称),然后打开 server 文件夹。

  5. server 文件夹中,选择 worldworld_netherworld_the_end 文件夹。按 SHIFT-DELETE 删除它们。

  6. 返回到 New World 文件夹并点击 Start_Server 文件。这将重新启动服务器并生成一个新世界。

  7. 现在,当你打开 Minecraft 并进入 Minecraft Python 世界时,你将看到一个新生成的世界。

你可以根据需要重复这个过程来创建新世界。如果你想打开旧世界,仍然可以通过点击 MinecraftTools 文件夹中的 Start_Server 文件,而不是 New World 文件夹中的 Start_Server 文件来运行它。

要删除一个世界并用新世界替换它,只需删除你想替换的世界文件夹中的 worldworld_netherworld_the_end 文件夹。

离线游戏

如果你没有网络连接,当你尝试从 Minecraft 游戏连接到 Minecraft 服务器时会出现错误。你可以通过修改服务器的属性来修复此问题。首先,确保你已经关闭了服务器窗口。然后打开 MinecraftPython 文件夹,再打开 MinecraftTools 文件夹,然后打开 server 文件夹。用文本编辑器(例如 TextEdit)打开 server.properties 文件,并将 online-mode 设置从 true 更改为 false(见 图 1-7,第 10 页)。保存更改后,返回到 MinecraftTools 文件夹并点击 Start_Server 重新启动服务器。现在,你就可以离线游戏了。

切换到生存模式

我已将你的 Minecraft 服务器的默认游戏模式设置为创意模式。这将使你在编写和运行 Python 程序时更轻松,因为你不必担心玩家失去生命、饿死或被攻击。

但你可能会想在生存模式下测试一些程序,仅仅是为了好玩。从创意模式切换到生存模式再切换回来是很容易的。

要将服务器从创意模式切换到生存模式,按照以下步骤操作:

  1. 打开 MinecraftTools 文件夹。在该文件夹内打开 server 文件夹。

  2. 找到 server.properties 文件,并使用文本编辑器(如 TextEdit)打开它。

  3. 在文件中,找到包含 gamemode=1 的那一行,并将其更改为 gamemode=0(参见 图 1-8 在 第 11 页)。

  4. 保存文件并关闭它。

  5. 通过点击 MinecraftTools 文件夹中的 Start_Server 文件来启动服务器。当你加入 Minecraft Python World 游戏时,游戏将进入生存模式。

你可以随时切换回创造模式。只需重复这些步骤,但在第 3 步中,将 server.properties 文件中的 gamemode=0 改为 gamemode=1

现在你的 Mac 设置好了!接下来让我们了解 IDLE,这是你编写代码的地方。请翻到 “了解 IDLE” 在 第 20 页。

设置你的树莓派

登录到你的树莓派,并使用 startx 命令启动桌面。(如果你使用的是最新版本的树莓派操作系统,你不需要输入此命令。)

根据你的树莓派版本,可能会安装两到三个不同版本的 Python。对于本书,你将使用最新版本的 Python,即 Python 3。

默认情况下,树莓派计算机已安装了一个简化版的 Minecraft,名为 Minecraft: Pi Edition。你所需的一切,用于开始用 Python 编程控制 Minecraft 世界的工具,已经安装完毕。如果你是第一次使用树莓派,可以在官方网站上找到开始使用的说明,* www.raspberrypi.org/ *。

如果你使用的是较旧的 SD 卡镜像(创建于 2014 年 8 月之前),你可能会发现没有安装 Minecraft。如果未安装,安装起来很简单。首先,你需要将树莓派连接到互联网。你可以在 www.raspberrypi.org/ 上找到连接树莓派到互联网的指南。

一旦连接到互联网,按照以下步骤操作:

  1. 在桌面上,双击 LXTerminal

  2. 一旦 LXTerminal 打开,输入以下命令:

    $ sudo apt-get update
    
  3. 更新完成后,输入以下命令:

    $ sudo apt-get install minecraft-pi
    
  4. 等待安装完成。Minecraft 已经安装好。

与桌面版本相比,Minecraft 在树莓派上的功能有所限制。游戏世界要小得多,许多方块和其他功能(如生存模式)缺失,但你仍然可以编写并运行本书中的所有精彩程序。

在继续之前,让我们创建一个文件夹来存储你的 Python 程序。在任务栏上,点击文件浏览器图标。打开 Documents 文件夹,然后右键单击文件浏览器的空白处,选择 创建新建...文件夹。命名文件夹为 Minecraft Python 并点击 确定

注意

如果你使用的是原版树莓派,你会发现本书中的一些程序由于树莓派的限制运行缓慢。树莓派 2 在速度上会有较少的问题。

要打开 Minecraft,请点击桌面左上角的开始菜单。(如果你使用的是较旧版本的 Raspberry Pi 操作系统,开始菜单会在左下角。)进入 游戏 并点击 Minecraft。Minecraft 将打开。第一次打开 Minecraft 时,你需要点击 创建世界

一般来说,不要调整窗口大小,因为这样可能会遇到一些问题。

有时,当你打开其他窗口或对话框(例如确认是否要在 Python 中保存文件)时,它们会隐藏在 Minecraft 窗口后面。只需最小化 Minecraft,便可以使用其他窗口。如果遇到任何问题,可以在安装 Minecraft 后尝试重启 Raspberry Pi。

了解 IDLE

现在你已经安装并设置好了一切,让我们来看看 IDLE——你将用来编写和运行 Python 程序的软件。Python 安装包中包含了 IDLE,所以你不需要单独安装它。现在让我们打开 IDLE!

Windows 打开开始菜单,在搜索框中输入 IDLE

Mac 打开 应用程序 文件夹并点击 IDLE 图标。

Raspberry Pi 在桌面上,双击标有 Python 3 的 IDLE 图标。

一个 IDLE 窗口会打开,如图 1-14 所示。这个窗口叫做 Python shell。当我学会用 Python 编程时,Python shell 太棒了,简直让我惊讶!

image

图 1-14:用于编写 Python 程序的 IDLE 窗口

了解 Python Shell

Python shell 允许你逐行编写和运行程序。你可以编写一行代码,立即运行它并查看结果,然后再编写另一行。这非常棒,因为你可以轻松地进行实验并测试代码。

在窗口中,你应该看到行首有三个小箭头(>>>)。这叫做 命令提示符。命令提示符是 Python shell 告诉你,它准备好接受命令了。让我们从一个非常基础的命令开始:让 Python 加法运算两个数字。

点击 Python shell 中的命令提示符旁边,输入 2 + 2。注意,你不需要输入命令提示符本身(>>>)。你应该看到如下内容:

>>> 2 + 2

输入该命令后,按回车。Python shell 会输出结果。在这个例子中,结果是 4:

>>> 2 + 2
4

你还可以在 shell 中输入文本。将以下代码输入 Python shell 并按回车:

>>> "W" + "o" * 5
Wooooo

如你所见,这段代码输出了单词 Wooooo。命令末尾的数字决定了单词中有多少个 o。通过更改这个数字,你可以改变单词的长度。试着把它改成 20(或者你想要的任何数字):

>>> "W" + "o" * 20
Woooooooooooooooooooo

哇哦哦哦哦哦哦!Python shell 真的很有趣。

注意,IDLE 会为代码上色。这叫做语法高亮,它使代码的不同部分更容易查看。本书中的所有代码颜色与 IDLE 中的一致,因此当你编写程序时,颜色将匹配。

接下来让我们看看 IDLE 的文本编辑器。

向 IDLE 的文本编辑器问好

当谈到编写较长的程序时,你不能使用命令行。IDLE 的文本编辑器是解决方案!与命令行不同,它不会在你输入每一行代码后立即运行。而是,当你指示它时,它会运行整个程序。

在 IDLE 中,点击菜单栏中的文件,然后选择新建文件。一个新窗口会打开,看起来就像图 1-15 中的窗口。这就是文本编辑器。

image

图 1-15:IDLE 的文本编辑器

“嘿!”我听到你说。“文本编辑器看起来和 IDLE 的 Python 命令行一样。”嗯,确实是的,但有一个非常大的区别。新窗口的每一行开头没有命令提示符(>>>)。

让我们看看这意味着什么。在文本编辑器的第一行,输入以下代码并按回车:

print(2 + 2)

你预期会发生什么吗?按回车并不会在这里运行代码——它只是创建了一个新行。因为文本编辑器在你按回车时不会运行代码,所以你可以在运行它们之前编写任意多的行。让我们再添加几行。这就是完成后你文件的样子:

print(2 + 2)
print("W" + "o" * 20)
print("PYTHON!")
print("<3s")
print("Minecraft")

在你从 IDLE 的文本编辑器运行 Python 代码之前,你需要保存它。要保存程序,点击文件,然后选择另存为。在你的Minecraft Python文件夹中创建一个名为Setting Up的文件夹。将此程序保存为pythonLovesMinecraft.pySetting Up文件夹中。

现在让我们运行它。进入菜单中的运行,然后点击运行模块。命令行窗口将会打开,你的程序将在其中运行。输出结果显示在图 1-16 中。

image

图 1-16:Python 程序的输出结果

与命令行不同,从文本编辑器运行的命令不会自动输出它们的结果。这就是为什么你需要使用print()来输出代码的结果。现在不必太担心细节——你将在本书的后面学到这些。

每次你从 IDLE 的文本编辑器运行程序时,命令行都会打开来运行程序。即使你在一个单独的窗口中编写程序,IDLE 始终使用命令行来运行你的程序。

何时使用 Python 命令行,何时使用文本编辑器

现在你已经看到了 IDLE 的 Python 命令行和 IDLE 的文本编辑器之间的区别,你可能会想知道什么时候使用其中之一更好。一般来说,我在只想测试几行代码且不打算重复使用它们时使用 Python 命令行。当你跟随本书学习时,我建议你使用 Python 命令行运行短小的示例。

我使用文本编辑器来编写那些代码行数较多或需要重复使用的程序。本书中的所有任务都使用文本编辑器,以便你能保存进度,但你也可以随时在命令行中玩耍,快速尝试一些东西。

本书中使用的提示

在本书中,每当你看到一段写在 IDLE Python shell 中的代码,它将以命令提示符(>>>)开始,如下所示:

>>> print("Wooooo Minecraft")

我建议你在阅读时将代码复制到 IDLE 中,这样你可以熟悉代码的写法。命令行的任何输出都会显示在下一行:

>>> print("Wooooo Minecraft")
Wooooo Minecraft

在文本编辑器中编写的代码不会以命令提示符开始,像这样:

print("Adventures")

代码的输出不会自动在你的计算机上显示。为了向你展示运行代码时的输出效果,我会通过解释或在新框中显示输出。例如,运行上述代码应输出:

Adventures

为了让你更容易理解书中代码的解释,我添加了标记来指出我正在讲解的部分。每当你在代码中看到标记时,文本中会有相应的解释,反之亦然。这些标记是这样的:

➊ ➋ ➌ ➍ ➎ ➏

测试你的 Minecraft Python 设置

让我们确认你已正确安装所有软件。为此,我们将快速编写一个非常基础的 Python 程序,它将与 Minecraft 互动。

首先,最重要的是:如果你使用的是 PC 或 Mac,你需要打开三款软件。请按照以下步骤操作:

  1. 通过进入你的Minecraft Tools文件夹并点击Start_Server来打开 Spigot。

  2. 打开 Minecraft,并通过选择多人游戏菜单中的Minecraft Python World来连接到 Spigot 服务器。

  3. 按下键盘上的 ESC 键,释放 Minecraft 窗口中的光标,然后在 IDLE 中打开 Python shell。

每次编写与 Minecraft 交互的程序时,你都需要打开这三款软件。

如果你使用的是 Raspberry Pi,请打开 IDLE 和 Minecraft。

现在,将以下内容输入到你的 shell 中。确保输入的小写字母和大写字母完全匹配!

>>> from mcpi.minecraft import Minecraft

按下 ENTER 键,让光标移动到下一行。然后输入以下代码:

>>> mc = Minecraft.create()

如果此时你看到类似于图 1-17 的错误信息,那么说明出现了问题。

image

图 1-17:表示我尚未启动 Spigot 的错误信息

按照以下顺序检查:你是否已打开 Minecraft?Spigot 是否正在运行?你是否在多人游戏世界中?你是否使用的是正确版本的 Python(3,而不是 2)?如果在输入第一行后出现错误,说明你没有正确安装 API。请重新按照步骤安装 API。如果错误出现在第二行之后,可能是你的 Java 或 Spigot 没有正确安装。尝试逐一重新安装这些软件。

如果你收到一个错误,提示ImportError: No module named 'mcpi',可能是你正在使用旧版本的 Python。确保你安装了最新版本!

如果没有出现错误信息,请在 IDLE 中将这行代码添加到程序中:

mc.player.setTilePos(0, 120, 0)

当你这样做时,玩家将飞得很高!这段代码将玩家传送到一个新的位置。你将在第二章中学到更多内容。翻到下一页开始吧!

第二章:2

使用变量进行瞬间传送

image

你准备好用 Python 的力量控制你的《Minecraft》世界了吗?在这一章中,你将简要了解 Python 的基础知识。然后你将把新学到的技能付诸实践,创建属于你自己的《Minecraft》世界瞬间传送之旅!

本章中描述的概念不仅限于《Minecraft》Python,因此你可以在任何你创建的 Python 程序中使用它们。

什么是程序?

程序是一组指令,让你的计算机执行特定的任务。想象一下手机上的计时器应用。计时器程序有指令,告诉它当你按下开始和停止按钮时应该做什么。它还有指令,会在屏幕上显示计时结果。某个人或某个女孩编写了这个计时器程序。

每天全球有数百万个程序在被使用。手机的消息应用是一个程序,交通灯是由程序控制的,甚至像《Minecraft》这样的计算机游戏也是程序。

在本书中,你将学习编程的基本原理,并学会如何编写程序,让你的创意在《Minecraft》中得以实现。

使用变量存储数据

让我们从学习如何使用变量存储数据开始。变量让你存储数据,以便稍后在程序中使用。数据是任何你可能想记录的信息,例如数字、名字、任何类型的文本、物品清单等。例如,下面是一个名为pickaxes的变量,它存储了数字值12

>>> pickaxes = 12

变量可以存储数字、单词,甚至完整的句子,例如“快滚开,苦力怕!”你还可以修改变量,这让你在《Minecraft》中做一些非常酷的事情。事实上,很快你就会利用变量来发挥瞬间传送的强大力量!

要在 Python 中创建一个变量,你需要使用变量名、等号(=)和一个值。假设你即将开始一次穿越《Minecraft》多个生物群落的伟大冒险;你会想带上很多食物。你可以将食物表示为一个变量。例如,在以下的 Python 终端中,bread是变量名,145是它的值:

>>> bread = 145

变量的名称总是位于等号的左侧,而你想要存储的值则总是在右侧,如图 2-1 所示。这行 Python 代码声明了变量bread赋值145给它。

image

图 2-1:变量声明的部分。如果你有 145 块面包,那一定是非常饿了。

在你声明一个变量并为其赋值之后,你可以在 Python 终端输入变量名,查看它所存储的内容:

>>> bread
145

你几乎可以为变量起任何名字,但最好使用一个能描述变量用途的名字,这样你就能理解程序中的内容。虽然这不是强制规则,但你应该用小写字母开始变量名,而不是大写字母。这是 Python 程序员遵循的风格,遵循这个风格对你也有好处,这样别人如果需要查看你的代码时会更容易理解。

注意

虽然变量的值是存储的,但它并没有被保存。变量的值保存在计算机的临时内存中,这意味着当计算机关闭或程序停止运行时,变量的值将不再被保存。试试关闭 IDLE 再重新打开它。当你尝试获取bread的值时,会发生什么?*

编程语言的结构

语法是一组规则,用来描述编程语言的语法和标点,类似于人类语言中的语法和标点。一旦你理解了 Python 的语法,你就能更好地编写计算机能够理解的程序;然而,如果你不使用正确的语法,计算机将无法理解你让它做什么。

想把你代码中的每一条指令当做一个句子来看。在英语中,句子以句号(在英国叫作“full stop”)结束。而 Python 使用新的一行来表示一条指令的结束和下一条指令的开始。每行上的指令称为语句

比如,假设你想跟踪你拥有多少个镐、铁矿块和圆石块。在 Python 解释器中,你会这样写:

>>> pickaxes = 12
>>> iron = 30
>>> cobblestone = 25

图 2-2 展示了在 Python 解释器中这是什么样子的。

image

图 2-2:在 Python 解释器中输入代码

注意,每条语句都在自己的行上。由于有了新的一行,Python 会理解你想跟踪三项不同的内容。但是如果你没有把每条语句放在新的一行,Python 会感到困惑,并且给你语法错误:

>>> pickaxes = 12 iron = 30 cobblestone = 25
SyntaxError: invalid syntax

语法错误是 Python 告诉你它无法理解的方式。Python 无法执行这些指令,因为它不知道哪一条语句结束,哪一条语句开始。

如果你在行首加上空格,Python 也不会知道该怎么做:

>>>   iron = 30
SyntaxError: unexpected indent

如果你仔细观察,你会发现代码的每一行开头都有空格。当你遇到意外的缩进语法错误时,比如这里的错误,你就知道你的代码行开头有不该有的空格。

Python 对代码的书写非常挑剔。如果你在输入本书中的示例时遇到语法错误,请仔细检查你的代码。很可能,你会发现一个小错误。

变量的语法规则

你需要了解一些变量命名的语法规则,这样 Python 才能理解它们:

• 变量名中不要包含符号,除了下划线(_),否则你会遇到语法错误。

• 变量名不能以数字开头,例如 9bread。在变量名的其他位置使用数字是可以的,例如 bread9

• 你不需要在等号两边添加空格:即使没有空格,你的程序也会正常运行。但空格会让代码更易读,所以最好加上它们。

变量非常方便。接下来,你将学习如何更改变量的值,然后你就能准备好传送你的玩家了!

更改变量的值

你可以像声明变量一样随时更改变量的值。例如,假设你遇到五只 Minecraft 猫,并且想将这个值保存为变量。你首先声明一个变量 cats,并将值 5 赋给它,在 Python 交互式命令行中会是这样的:

>>> cats = 5
>>> cats
5

后来你遇到了五只猫,并决定想更新这个值。如果你将 cats 的值更改为 10,会发生什么呢?

>>> cats = 10
>>> cats
10

当你向 Python 请求 cats 的新值时,它不再是 5!现在,当你在程序中使用 cats 变量时,它将使用新值 10

你可以在变量中存储许多类型的数据。数据类型 告诉计算机如何处理特定的数据。我将首先讨论你最常使用的一种类型:整数。稍后在本章中,我还将介绍浮点数数据类型。

整数

整数 是正数或负数的整数。像 10、32、–6、194689 和 –5 这样的值是整数,但 3.14 和 6.025 不是。

你可能每天都在不经意间使用整数,即使是在 Minecraft 中!例如,你可能在去采矿时看到山坡上的 12 只牛,身上带着 2 个新鲜的苹果和 5 颗钻石。这些数字都是整数。

假设你在 Minecraft 世界里有五只猪,你想编写一个程序以某种方式使用这些猪的数量。在 Python 中,你可以声明一个整数变量来表示猪的数量:

>>> pigs = 5

你还可以在变量中存储负值。例如,要表示温度是零下五度,你可以这样设置变量:

>>> temperature = -5

要在 Minecraft 中使用 Python 变量和整数,请完成第一个任务。

任务 #1: 传送玩家

在这个任务中,你将通过使用整数将玩家传送到新位置,来探索变量是如何工作的。

如图 2-3 所示,你的玩家在 Minecraft 世界中有一个位置,由三个坐标表示:xyz。字母 y 代表高度,而 xz 代表平面上的水平位置。

图片

图 2-3: 3D 坐标

如果你使用的是 Raspberry Pi 版游戏,玩家的位置由游戏窗口左上角的三个数字表示,你可以在 图 2-4 中看到。如果你使用的是桌面版游戏,可以通过按 F3 来查看玩家的坐标,并在左侧第二块文本的第一行找到标有 XYZ 的位置,如 图 2-5 所示。

在游戏中移动玩家并观察位置数字的变化;坐标应该会实时更新,当玩家行走时,坐标会即时变化。很酷吧?不过,走很远的路需要很长时间。为什么要花那么多时间走路,当你可以通过 Python 立即改变位置呢?让我们来看看如何实现这一点。

image

图 2-4:Minecraft: Pi Edition 中显示的玩家位置

image

图 2-5:桌面版 Minecraft 中显示的玩家位置

打开你的计算机或 Raspberry Pi,并按照以下步骤操作:

  1. 打开 IDLE,点击 文件新建文件(或某些计算机上是 新建窗口)。你可以在 图 2-6 中看到空的文本编辑器窗口。如果你使用的是 Raspberry Pi 或计算机上安装了多个版本的 Python,请确保使用 Python 3,而不是 Python 2.7。

    image

    图 2-6:IDLE 中的新文本编辑器窗口

  2. 当新窗口出现时,点击 文件另存为

  3. 在 第一章 中创建的 Minecraft Python 文件夹内,创建一个名为 variables 的新文件夹。

  4. 打开 variables 文件夹,命名你的文件为 teleport.py,然后点击 保存

现在你在 IDLE 的文本编辑器中工作,将以下两行代码添加到程序的顶部:

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

这些行代码将你的程序与 Minecraft 连接;你将在每个与 Minecraft 交互的程序中使用它们。接下来,创建三个整数变量,分别命名为 xyz

x = 10
y = 110
z = 12

这些变量表示你想要传送玩家到的位置。现在,将这些变量的值设为 10、110 和 12,如下所示。

然后输入以下代码行,这将传送玩家:

mc.player.setTilePos(x, y, z)

程序中的 setTilePos() 部分是一个 函数,它是一个预先编写且可重用的代码块。setTilePos(x, y, z) 函数告诉 Minecraft 使用你刚刚设置的三个变量来改变玩家的位置。括号中的值叫做 参数。你将刚才创建的变量作为参数传递给函数,以便函数在执行时使用 xyz 的值。

警告

如果你使用的是 Raspberry Pi,请不要为 x z 变量使用大于 127 或小于 -127 的值。Minecraft Pi 版的世界很小,超出此范围的数字会导致游戏崩溃。

清单 2-1 包含了完整的代码,用于传送玩家,你也可以在 图 2-7 中看到:

teleport.py

➊ # Connect to Minecraft
   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   # Set x, y, and z variables to represent coordinates
   x = 10
   y = 110
   z = 12

   # Change the player's position
   mc.player.setTilePos(x, y, z)

代码清单 2-1:完成的传送代码

为了让这个程序更容易理解,我在代码中加入了一些 注释 ➊。注释是代码中的有用语句,用来描述代码的功能,但 Python 会忽略它们。换句话说,当你运行程序时,Python 会跳过注释的行而不执行任何操作。单行注释以井号(#)开始。我的注释描述了 teleport.py 中每个部分的功能。养成在代码中写注释的习惯,这样当你回到程序时,可以帮助你记住每个部分的作用。

图 2-7 显示了在 IDLE 文本编辑器中编写的完整程序。

image

图 2-7:在 IDLE 文本编辑器中的完整程序

现在让我们运行程序!按照以下步骤操作:

  1. 通过点击桌面图标打开 Minecraft。

  2. 如果你使用的是 Raspberry Pi,点击 开始游戏创建新世界。如果你使用的是桌面版 Minecraft,请根据 第 7 页 Windows 系统和 第 16 页 Mac 系统中的“运行 Spigot 和创建游戏”中的说明打开游戏世界。

  3. 世界生成后,按 ESC 键(如果你使用的是 Raspberry Pi,请按 TAB)来释放鼠标。你现在可以将鼠标移到 Minecraft 窗口外,或者双击 Minecraft 窗口重新选择游戏。图 2-8 显示了我电脑上的 IDLE 和 Minecraft 窗口。

    image

    图 2-8:这是我喜欢的 Minecraft 和 IDLE 文本编辑器窗口的排列方式。

  4. 点击包含 teleport.py 程序的 IDLE 文本编辑器窗口。

  5. 点击 运行运行模块 或按 F5。如果你还没有保存程序,IDLE 会在运行前询问是否保存。点击 确定 来保存程序。如果点击“取消”,程序将不会运行。

注意

当你在 Raspberry Pi 上从 IDLE 运行程序时,可能会弹出一个要求你保存程序的对话框,并且它可能会隐藏在 Minecraft 窗口后面。如果你认为 IDLE 已经冻结,可能是对话框被隐藏了。只需最小化 Minecraft 窗口,然后在 IDLE 对话框中点击“确定”。点击“确定”后,最大化 Minecraft 窗口。

做得好!现在你的程序应该能够运行了,几秒钟后,玩家应该会被传送到坐标 (10, 110, 12),如 图 2-9 所示。你的世界可能和我的不一样,所以你在自己电脑上运行时会看到一些不同的地方。

附加目标:跳跃

你觉得你已经掌握了传送技能吗?试着将 xyz 替换为其他整数,看看玩家会出现在什么位置!也可以尝试使用负值!

image

图 2-9:我已经从我的房子传送到了位置 (10, 110, 12),那里位于一个沼泽上方。小心下面!

浮动

不是所有数字都是整数。小数点用于表示无法用整数描述的值。例如,你可能只有一个苹果的一半(0.5)。使用小数点的数字被称为浮点数,或浮动数。这是 Python 使用的另一种数据类型。当你需要更高精度时,浮点数就代替了整数。浮点数也可以表示整数(如3.0),但整数不能表示带小数部分的数字。

你可能已经注意到,玩家的位置坐标(如图 2-4 和图 2-5 所示)包含小数,这意味着它们是浮点数!

在 Python 中,声明浮点变量的方式和声明整数变量的方式相同。例如,要将变量x设置为1.34,你可以这样写:

>>> x = 1.34

要创建一个负浮点数,可以在数值前加上负号(-):

>>> x = -1.34

在下一个任务中,你将通过使用浮点数将玩家传送到精确位置,获得对传送能力的更大控制。

任务 #2: 精确到你想要的位置

你已经学习了如何使用整数设置玩家的位置,但如果使用浮点数,你可以更精确地设置玩家的位置。在这个任务中,我们将修改任务 #1 中的程序,通过使用浮点值来传送玩家:

  1. 在 IDLE 中,打开teleport.py程序(见第 34 页),通过点击文件打开并从你的variables文件夹中选择该文件。

  2. 将程序另存为teleportPrecise.py,保存在你的variables文件夹中。

  3. teleportPrecise.py文件中,将xyz变量更改为使用浮点数,而不是整数。也就是说,将xyz的值从 10、110 和 12 更改为 10.0、110.0 和 12.0。

  4. 将代码的最后一行改为mc.player.setPos(x, y, z),去掉Tile这个词。

  5. 保存程序。

  6. 打开一个 Minecraft 世界并运行代码。

最终结果应该如下所示:

teleportPrecise.py

# Connect to Minecraft
from mcpi.minecraft import Minecraft
mc = Minecraft.create()

# Set x, y, and z variables to represent coordinates
x = 10.0
y = 110.0
z = 12.0

# Change the player's position
mc.player.setPos(x, y, z)

注意这里使用的mc.player.setPos(x, y, z)与清单 2-1 中使用的mc.player.setTilePos(x, y, z)的区别。setTilePos()函数使用整数告诉游戏你想要传送到的方块的位置。而setPos()函数稍有不同——它使用浮点数告诉游戏方块的位置,以及你想要传送到该方块上具体的位置。通过我的程序,我传送到了我的塔顶,如图 2-10 所示。

image

图 2-10: 我已将自己传送到塔顶,使用浮点数来做到非常精确。

附加目标:精确传送

使用混合的正负浮点数更改xyz变量的值,并运行程序。然后,稍微调整这些新值的十进制部分。会发生什么?

通过时间模块减慢传送速度

Python 运行代码的速度尽可能快。但是你可以通过让程序在继续执行之前等待一定的秒数来放慢动作。

要在你的程序中使用时间,你需要 time 模块,它包含了一组与时间相关的预编写函数。要使用 time 模块,在你的程序顶部添加以下代码:

import time

使用 time 模块和 sleep() 函数时,顺序非常重要,sleep() 函数是 time 模块的一部分。sleep() 函数会使程序等待指定的秒数后再继续。如果你不先导入 time 模块,Python 将找不到 sleep() 函数,它会变得如此困惑,以至于会停止程序运行。这就是为什么最好在代码的顶部导入你使用的任何模块的原因。你所有的 import 语句都会被集中在程序的顶部。例如,我通常首先包括连接到 Minecraft 的代码,然后在第三行添加 import time 语句。

下面是如何使用 sleep() 函数的示例:

time.sleep(5)

这一行代码将使程序暂停五秒钟。你可以使用任何数字,包括整数和浮动数值,如以下示例所示:

time.sleep(0.2)

当程序到达这一行代码时,它将等待 0.2 秒。现在你可以控制时间的流动了,准备好迎接下一个任务!

任务 #3:传送门之旅

Minecraft 中传送的美妙之处在于你可以把玩家传送到任何地方。利用到目前为止学到的所有技能,你将让玩家在整个 Minecraft 世界中进行自动化旅游!

在这个任务中,你将通过修改任务 #1 中的代码(第 31 页)来练习更改变量的值,将玩家传送到地图上的多个位置。玩家将传送到一个位置,等待几秒钟,然后传送到另一个位置。

  1. 在 IDLE 中,通过点击 文件打开,并从你的 variables 文件夹中选择文件,打开 teleport.py 程序(第 34 页)。

  2. 将程序保存为 tour.py 文件到你的 variables 文件夹中。

  3. 在连接程序与 Minecraft 的代码之后,添加 import time

  4. 在程序的末尾添加 time.sleep(10)

  5. 复制包含 xyz 变量以及 setTilePos() 函数的行,并将它们粘贴到程序的末尾,这样这些行将出现两次。

  6. 将两个 xyz 变量的值更改为你想要的任何数字。你可以通过移动到游戏中的某个位置并像本章前面所做的那样记录玩家的坐标,来找到游戏中任何位置的坐标。

  7. 保存程序。

  8. 打开一个 Minecraft 世界并运行代码。

最终的结果应该像这样,填入新的坐标:

tour.py

# Connect to Minecraft
from mcpi.minecraft import Minecraft
mc = Minecraft.create()
import time

# Set x, y, and z variables to represent coordinates
x = # Fill in
y = # Fill in
z = # Fill in

# Change the player's position
mc.player.setTilePos(x, y, z)

# Wait 10 seconds
time.sleep(10)

# Set x, y, and z variables to represent coordinates
x = # Fill in
y = # Fill in
z = # Fill in

# Change the player's position
mc.player.setTilePos(x, y, z)

玩家应该传送到第一个位置,等待 10 秒钟,然后传送到第二个位置,如图 2-11 所示。

额外目标:更多的传送

复制tour.py代码,重复移动玩家多次。将函数time.sleep(10)中的10替换为不同的值。你甚至可以为每个sleep()函数使用不同的数字,让你的玩家在每个位置等待不同的时间。

然后编辑代码,使得在两次传送之间,只有 xyz 中的一个变量发生变化。每次都修改所有变量是不必要的!尝试使用浮点数而不是整数。

image

图 2-11:我已在程序中设置坐标,使玩家先传送到我的房子,然后再传送到沙漠。

调试

每个人都会犯错;即使是最优秀的程序员也不可能第一次就写对代码。写出一个能够正常运行的程序是一个好程序员需要的技能,而在程序无法正常运行时进行修复是另一个至关重要的技能。这个过程叫做调试,在一个出现问题的程序中,每个问题都叫做bug。在本节中,你将学习修复所有未来程序的技巧和方法。

错误可以完全阻止程序运行,或者使程序以意想不到的方式运行。当程序无法运行时,Python 会显示错误信息,比如图 2-12 中的错误信息。

image

图 2-12:Python 给我提示错误,因为我没有遵循 Python 的语法。

在图 2-12 中,你可以看到我在 Python shell 中输入了一些代码,并且它返回了一个错误信息。错误信息中显示了很多内容,但根据最后一行(NameError: name 'x' is not defined),我可以判断是我的x变量出了问题。具体来说,x变量没有被定义。为了解决这个问题,我需要添加一行代码来定义x变量,像这样:

>>> x = 10

这一行代码将修复错误信息,但这并不意味着所有的错误都会被修复。

允许程序运行但导致它行为异常的错误不会显示错误信息,但当程序产生意外结果时,你就会知道出了问题。例如,如果你忘记在传送程序中写一行代码,比如setTilePos(),程序会正常运行,但玩家的位置不会改变。这可不是一个有用的传送程序!

警告

拼写错误是导致程序出错的常见原因之一。拼写错误会让计算机无法理解,从而阻止程序的运行。小心,确保你的拼写和大小写正确!

任务 #4:修复有问题的传送

在这个任务中,你将调试两个程序。第一个程序,清单 2-2,与teleport.py(第 34 页)类似,但这个版本有五个错误。在 IDLE 文本编辑器中打开一个新文件,将清单 2-2 复制到其中,并保存为teleportBug1.py

teleportBug1.py

from mcpi.minceraft inport Minecraft
# mc = Minecraft.create()

x = 10
  y = 11
z = 12

清单 2-2:损坏版的传送程序

要调试这个程序,完成以下步骤:

  1. 运行teleportBug1.py

  2. 当你遇到错误消息时,阅读最后一行,获取关于问题的提示。

  3. 修正错误并再次运行代码。

  4. 继续修复程序中的错误,直到程序将玩家传送到新的位置。

提示

不要忘记仔细检查你是否实际调用了 setTilePos() 函数!

让我们尝试调试另一个程序。清单 2-3 中的teleport.py可以运行,但由于某些原因,玩家没有传送到指定的位置。将清单 2-3 复制到 IDLE 文件中,并保存为teleportBug2.py

teleportBug2.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

x = 10
y = 110
z = -12

mc.player.setPos(x, z, y)

清单 2-3:带有错误的传送程序

teleportBug1.py不同,运行该程序时你不会收到任何错误消息。要修复该程序,你需要阅读代码直到找到错误。程序应该将玩家传送到位置(10, 110, -12)。运行程序并检查玩家被传送到的坐标。这可能帮助你调试程序并找出问题所在。

当你修复了这两个程序中的所有错误后,给每个程序添加注释,解释问题所在。记录调试过程中遇到的问题,可以帮助你记住以后要注意类似的错误。

你学到了什么

恭喜你!你已经写出了第一个 Python 程序,通过变量和函数控制 Minecraft 玩家。你探索了两种数据类型(整数和浮点数),控制了时间,并调试了损坏的程序。你还学会了 Minecraft Python API 中两个非常有用的函数:setPos()setTilePos()

在第三章中,你将掌握 Minecraft 中的快速建造技巧,使用数学运算和设置方块的函数!

第三章:3

通过数学快速构建和远距离旅行

image

在第二章中,你学习了如何创建变量并改变其值。在这一章,你将学习如何在 Python 中使用数学运算来生成任何你想要的块,并在你的 Minecraft 世界中快速构建复杂的结构。你甚至可以赋予自己超能力,让玩家实现超级跳跃!

表达式和语句

当你和某人交谈时,你希望他们理解你所说的内容。你会使用短语,例如“三个钻石”或“树后面”,将信息传递给对方。然而,单独这些短语并没有意义,除非它们组合成完整的句子,比如“我在树后面找到了三个钻石。”

Python 编程有类似于短语和句子的概念,称为表达式和语句。

你可以将值、变量和运算符结合起来,创建叫做表达式的小代码块,像2 + 2。表达式可以组合成语句,你在第二章中已经学习过了。语句是执行某个操作的单行或短代码块,例如zombies = 2 + 2。在这个例子中,2 + 2是一个表达式,且它是语句zombies = 2 + 2的一部分。

对于较长的程序,使用文本编辑器而不是 Python shell 时,一定要写完整的语句。例如,Python shell 和在文本编辑器中编写的程序会以完全不同的方式处理表达式2 + 2。当你在 IDLE 中使用 Python shell 时,Python 会输出4作为2 + 2的结果,如下所示:

>>> 2 + 2
4

然而,当你使用文本编辑器时,Python 不会对表达式做任何处理,因为它不是完整语句的一部分。为了将这个表达式转变为完整的语句,你可以将其值赋给一个变量,像这样:

zombies = 2 + 2

然后打印这个变量,查看它的值:

print(zombies)

当你运行这段代码时,它会打印出4

再次提醒,当你在文本编辑器中编写程序时,使用完整的语句非常重要,而不仅仅是表达式。

运算符

在数学中,运算符用于改变和组合数字。例如,加法运算符让你将两个(或更多)数字相加,减法运算符则用于从一个数字中减去另一个数字。

Python 使用所有你已经知道的基本数学运算符——加法、减法、乘法和除法——以及更高级的运算符,比如指数运算。我们从加法开始。

加法

在 Python 中,加法就像你平常写的那样,使用加号(+)。例如,如果你有两朵花,然后再摘了两朵,你可以用加法表达式来描述这一过程:

>>> flowers = 2 + 2

Python 会计算等号右边表达式的结果,并将结果赋值给左边的变量。在这个例子中,右边表达式的结果是4。在这段代码的其余使用过程中,变量flowers的值将保持为4

你可以在 Minecraft 中使用加法在眨眼之间建造东西。准备好迎接下一个任务了吗?让我们开始吧!

任务 #5: 堆叠方块

你可以使用setBlock()函数在 Minecraft 中创建并放置一个方块。与setPos()setTilePos()类似,setBlock()也需要 x、y、z 坐标作为参数,但它还需要第四个值:方块类型。这个值标识了你希望在游戏中放置的方块类型。

无论是草地、岩浆、西瓜,还是其他任何方块,每种类型的方块都有一个特定的整数值。例如,草地是2,空气是0,水是8,西瓜是103。要查看完整的方块及其整数值列表,请参见“方块 ID 备忘单”,该内容在第 283 页。

要使用setBlock(),请传递 x、y、z 坐标值和表示方块类型的整数值,值之间用逗号分隔。例如,我们可以将一个西瓜方块(类型 103)放置在坐标(6, 5, 28)处:

from mcpi.minecraft import Minecraft
mc = Minecraft.create()
mc.setBlock(6, 5, 28, 103)

在你在所有 Minecraft Python 程序中都会看到的前两行之后,只需调用setBlock()并传递你想要使用的所有值。你还可以使用变量来代替数字,以达到相同的效果,如列表 3-1 所示。

blockStack.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()
x = 6
y = 5
z = 28
blockType = 103
mc.setBlock(x, y, z, blockType)

列表 3-1: 创建一个西瓜方块的程序

首先,创建表示方块坐标(xyz)和类型(blockType)的变量。然后,将所有变量传递给setBlock()函数,Minecraft Python API 将发挥它的魔力。现在,你可以在程序中的任何地方再次使用这些变量,如果以后决定更改它们的值,只需在一个地方进行修改。

当你将这段代码与数学运算符结合使用时,你可以做一些相当酷的事情。让我们创建一堆方块。

Minecraft Python文件夹内创建一个名为math的新文件夹。打开 IDLE 并使用 IDLE 的文本编辑器创建一个空白程序。将此文件保存为blockStack.pymath文件夹中。从列表 3-1 中复制代码到你的编辑器,并将列表 3-2 中的两行代码添加进去,将另一个西瓜方块堆叠在你刚才设置的西瓜方块上。

blockStack.py

➊ y = y + 1
➋ mc.setBlock(x, y, z, blockType)

列表 3-2: 将第二个西瓜方块堆叠在第一个西瓜方块上的附加代码

你正在将y的值加 1➊,并且你正在使用setBlock()函数创建另一个新块➋。通过将y的值增加 1,第二个块的位置会比第一个块在 y 轴上更高,因此第二个块就堆叠在第一个块的上面。

从这里开始,你的任务是将堆叠的方块增加两个。尝试修改你的blockStack.py程序,使其堆叠四个方块,而不是两个!当你运行程序时,应该会看到四个西瓜方块堆叠在一起,如图 3-1 所示。

image

图 3-1:我已经堆了一个西瓜方块堆。

提示

为了在第一个方块上方添加第二个方块,我们将y变量增加了 1,然后再次使用setBlock()函数。你觉得如果在程序的最后重新使用这两条语句会发生什么?如果你用三次呢?这会是创建四个方块堆叠的解决方案吗?

附加目标:创建彩虹

你可以编写许多不同版本的blockStack.py程序。通过修改方块类型,你可以创建彩虹或熔岩塔!试着更改方块类型,看看你能创造出什么。

任务 #6:超级跳跃

在第二章中,你学会了如何改变玩家的位置。让我们把这个技能再提升一步,利用加法的力量将玩家送上高空。首先,像清单 3-3 中所示,通过调用getTilePos()来获取玩家的位置。

superJump.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

position = mc.player.getTilePos()
x = position.x
y = position.y
z = position.z

清单 3-3:查找玩家位置的代码

position变量与xyz之间的点(.)叫做点表示法。点表示法被某些变量和函数使用,比如你在 Minecraft Python API 中使用的所有函数(例如,mc.setTilePos())。你将在第十一章和第十二章中学习更多关于点表示法的内容。

一旦你得到了玩家的位置,你可以将xyz变量设置为玩家当前的坐标,分别通过position.xposition.yposition.z表示。然后,你可以根据当前坐标将玩家传送到任何你想要的位置,如清单 3-4 所示。

superJump.py

x = x + 5
mc.player.setTilePos(x, y, z)

清单 3-4:将玩家的 x 位置向上移动 5 个方块的代码

在这里,我将玩家沿 x 轴传送了 5 个方块,但这并不特别:你可以随时在 Minecraft 中水平移动玩家。让我们给玩家一个超级跳跃吧!

你的任务是让玩家跳跃到当前坐标上方 10 个方块的地方。你应该能够使用清单 3-3 和 3-4 中的代码来完成这个任务,虽然会有一些小的不同。将清单 3-3 和 3-4 中的代码复制到 IDLE 中,保存为superJump.py,并按照我更改x变量的方式修改y变量。当你运行程序时,玩家应该会像图 3-2 中那样跳跃到空中。

image

图 3-2:超级跳跃的实际效果!

减法

Python 处理减法与加法的方式类似。假设你正在探索一个洞穴,一只蜘蛛攻击了你,你失去了一些生命值:

health = 20
health = health - 2

语句中 health 的值现在是 18。就像加法操作一样,Python 会计算等号右边操作的结果,并将该结果赋值给变量。

让我们在《Minecraft》中玩点减法吧!

任务 #7:改变你下方的方块

你是否曾想在《Minecraft》中为某人设置一个陷阱?想象一下,当玩家最不期望的时候,脚下的地面突然变成了岩浆。你可以使用 Python 实现这个愿望。通过减法运算,你可以在玩家当前位置下方放置方块。实际上,只需要几行代码就能将你想要的任何方块直接放在玩家脚下!

在这个任务中,你将使用 getTilePos()setBlock() 将玩家下方的方块改为岩浆。但这是一个危险的任务,所以在测试时要小心:如果你没有足够快地将玩家移动到新位置,他们可能会掉进岩浆里!

在清单 3-5 中的程序会在玩家当前位置创建一个方块。将这段代码复制到 IDLE 中的新文件,并保存为 blockBelow.py。然后,利用你对减法运算符的理解,修改代码,使它在玩家的脚下直接放置一个岩浆方块,如图 3-3 所示。

blockBelow.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()
pos = mc.player.getTilePos()
x = pos.x
y = pos.y
z = pos.z
blockType = 10
mc.setBlock(x, y, z, blockType)

清单 3-5:这段代码在玩家当前位置放置一个方块。

注意,我将存储玩家位置的变量命名为 pos。我选择这个名称是因为我经常使用这个变量,它很容易理解它的意思,而且比 position 更短、更快输入。

y 坐标决定了方块的高低。你的任务是弄明白如何改变 y 变量,将方块放在玩家下方。

image

图 3-3:当我下方的方块改变后,我掉进了岩浆。

附加目标:你周围的方块

你已经学会了如何在玩家下方放置方块。你能算出如何在玩家上方放置方块吗?一旦你弄明白了,试着一次在玩家周围放置多个方块。然后,你就可以开始在玩家周围建造建筑物了!

尝试将这个程序与任务 #6 中的程序结合使用(见第 51 页)。你能算出如何让玩家跳到空中,然后立刻在他们脚下放一个方块让他们不掉下来吗?感觉很邪恶?你可以编写一个程序,让玩家从高空掉进岩浆池中。

在参数中使用数学运算符

当你使用一个函数时,比如 setBlock()setTilePos(),你会给函数传递一些参数,这些参数指定了函数运行时需要使用的值。

到目前为止,你已经接触了加法和减法运算符。你可以在函数的括号内使用这些运算符来设置参数的值。让我们回顾一下任务 #5 中的堆叠块(第 49 页)。我们可以在setBlock()函数的括号内使用加法运算符,如清单 3-6 所示,它会在括号内将两个值相加,而无需额外的语句。

blockStack1.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   x = 6
   y = 5
   z = 28
   blockType = 103
   mc.setBlock(x, y, z, blockType)
➊ mc.setBlock(x, y + 1, z, blockType)

清单 3-6:带有运算符的块堆叠程序

清单 3-6 几乎与堆叠块程序相同。然而,它在setBlock()函数的括号内使用加法运算符,而不是在单独的语句中使用。最后一行在函数 ➊ 中使用y + 1作为参数。尽管该参数的值为65 + 1),但y变量的值仍然是5。这个参数允许你在不实际改变y值的情况下对y变量进行加法操作,如果你希望在代码的其他地方再次使用y,这就非常有用。

你也可以将两个变量相加,并将它们作为一个单一的参数。清单 3-7 与清单 3-6 相同,但额外添加了一个名为up的变量,它决定了新块在 y 轴上的放置位置。

blockStack2.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   x = 6
   y = 5
   z = 28
   blockType = 103
   up = 1
   mc.setBlock(x, y, z, blockType)
➊ mc.setBlock(x, y + up, z, blockType)

清单 3-7:使用加法运算符的堆叠程序的另一个版本

在最后一行,yup变量被加在一起 ➊。和清单 3-6 一样,这使得setBlock()函数的第二个参数变成了6。变量非常有用,因为如果你想将新块放置在 y 轴上比之前高两个方块,你只需要修改代码,将up设置为 2。你可以在图 3-4 中看到程序的所有三个版本(清单 3-1 和 3-2,3-6,以及 3-7)的效果。

image

图 3-4:虽然程序的三个版本不同,但它们的效果是一样的。

任务 #8:快速建造

通常,你在《我的世界》中度过的第一天会用来建造一个避难所。凭借目前学到的知识,你可以建造一个简单的房子,并以一种有风格的方式度过你的第一晚!这个任务中的程序将帮助你快速生成建筑物的墙壁、天花板和地板。你不需要花费大量时间手动放置每一个方块,而是可以通过几行代码构建建筑的基本结构。

你已经使用setBlock()创建了一个单一的方块,但setBlock()还有一个朋友叫做setBlocks(),它可以创建多个方块,形成一个长方体的形状。长方体是一个三维矩形。长方体的长度、宽度和高度可以是不同的值。

setBlocks() 函数允许你在大范围内创建多个方块。要使用 setBlocks(),只需传递两组坐标和方块类型。第一组坐标确定你想要的立方体一个角落的位置,第二组坐标指定你想要的对角线的另一角落。图 3-5 展示了立方体的角落,并标注了它们的坐标。

image

图 3-5:立方体及其用于设置尺寸的坐标

让我们创建图 3-5 中的立方体。正如你在清单 3-8 中看到的,我使用的是卵石,但你可以使用任何你喜欢的方块类型。嗯,除了岩浆、水或空气外,你可以使用任何东西——否则你将得到一个非常奇怪的房子!

building.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
➊ pos = mc.player.getPos()
   x = pos.x
   y = pos.y
   z = pos.z
   width = 10
   height = 5
   length = 6
➋ blockType = 4
➌ air = 0
   mc.setBlocks(x, y, z, x + width, y + height, z + length, blockType)

清单 3-8:构建一个立方体积木的代码

请注意,我使用了 getPos() ➊,而不是 getTilePos()getPos() 函数与 getTilePos() 相同,但它返回的是三个浮动坐标,而不是三个整数坐标。

这个立方体的宽度、高度和长度分别是 10、5 和 6,我使用了方块 ID 4 来生成卵石 ➋。你可以在图 3-6 中看到完成的建筑。

image

图 3-6:程序创建的建筑

然而,这个房子有一个小问题:它完全是实心的!在我运行程序后,我在建筑物的一侧打了一个洞,你可以看到它的中心是实心的。不过,这个立方体是一个很好的开始,现在你将负责把它挖空,这样玩家才能真正进入里面。

你的任务是修改程序,以在玩家位置创建一个有墙壁、天花板和地板的建筑。为此,你需要在刚才创建的实体立方体内创建一个由空气构成的立方体。这两个立方体结合起来应当形成一个空心盒子。你可以在图 3-7 中看到完成程序的结果。我在一侧打了个洞,这样你可以看到空心的中心部分。

image

图 3-7:当你的程序完成时,它应该会创建一个空心立方体。立方体非常适合快速创建建筑!

清单 3-8 中已经包括了一个名为 air ➌ 的变量,你可以使用它来将建筑内部的方块设置为空气。将清单 3-8 复制到 IDLE 中,保存为 building.py,并修改它来创建一个由空气构成的第二个立方体。你需要在最后一行添加一个额外的 setBlocks() 函数来创建这个空气立方体。空气立方体应该在四周的墙壁内有一个方块的空隙,这就是你需要使用加法和减法来解决的问题。保持耐心:如果第一次尝试失败了,尝试别的方法!

提示

要创建一个位于墙壁内一格的空气立方体,你可以使用加法和减法运算符。使用 setBlocks() 来创建空气立方体,并将第一个 x, y* 和* z 参数增加 1。然后从 x + width, y + height* 和* z + length 参数中减去 1。

额外目标:建造各种物品

你可以在任何时候重用你编写的代码来创建建筑。如果你想建造一个不同大小的建筑,该怎么做呢?你能想出如何修改建筑的宽度、高度和长度吗?

只需稍作修改,你的程序还可以有许多其他用途。你能算出如何用它来生成一个游泳池吗?提示:你需要将内立方体的方块类型改为水(方块 ID 8),并移除外立方体的顶部,以便玩家可以进入游泳池。

乘法

在 Python 代码中,乘法的表示方式与你通常见到的略有不同。你不用写 × 来表示两个数字相乘,而是使用星号 (*)。不过,除了符号,乘法的运算与平时一样。表达式 2 * 2 等于 4,就像 2 × 2 一样。

假设在你的 Minecraft 房子外面有四棵树,突然树木数量翻倍了。你可以用 Python 来表示这个计算,方法如下:

trees = 4
trees = trees * 2

在这个例子中,trees 的值是 8,即 4 乘以 2。

除法

在 Python 中,除法是用正斜杠 (/) 来表示的,而不是 ÷ 符号。

除法运算符将一个值除以另一个值。只需将你要除的数字放在正斜杠 (/) 的左侧,而将你要除以的数字放在右侧。

假设现在有 8 个骷髅在你的 Minecraft 堡垒外面,但其中一半走开了。要计算剩下的数量,你可以将 8 除以 2。以下是如何使用除法运算符在 Python 中表示这个计算:

skeletons = 8
skeletons = skeletons / 2

现在只有 4 个骷髅出现在你的堡垒外面。呼!让我们在 Minecraft 中试试这两个运算符。

任务 #9:壮观的尖塔

变量的一个优点是你可以改变单个变量的值,而它的值会在程序中所有出现该变量的地方发生变化。你只需通过改变一个变量的值,运用数学运算符(如乘法和除法),就能让程序做完全不同的事情。

在这个任务中,你将学习如何使用除法和乘法来建造一个非常高、非常薄的塔楼,也叫做尖塔

图 3-8 展示了当程序完成后,尖塔将呈现的样子。

image

图 3-8:由石块构建的尖塔

这个程序将使用一个单一的变量来设置尖塔的高度。通过乘法和除法,你将设置不同的尖塔高度。

在列表 3-9 中,我已经开始编写创建尖塔的程序,但还没有使用 height 变量或数学运算符来设置每个部分的高度。

spire.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z

   height = 2
   blockType = 1

   # Spire sides: should be same as height
   sideHeight = height
   mc.setBlocks(x + 1, y, z + 1, x + 3, y + sideHeight - 1, z + 3, blockType)

   # Spire point: should be two times the height
➊ pointHeight = 4
   mc.setBlocks(x + 2, y, z + 2, x + 2, y + pointHeight - 1, z + 2, blockType)

   # Spire base: should be half the height
➋ baseHeight = 1
   mc.setBlocks(x, y, z, x + 4, y + baseHeight - 1, z + 4, blockType)

列表 3-9:建造尖塔的程序

将清单 3-9 复制到 IDLE 中新文件,并将其保存为spire.pymath文件夹中。这个程序将创建一个尖塔,但是更改height变量并重新运行程序不会影响尖塔所有部分的高度。

为了修复这个程序,使得在更改height变量时,尖塔的所有部分的高度都会发生变化,你需要将pointHeight ➊ 和baseHeight ➋ 变量更改为包含使用height变量以及乘法或除法运算符的表达式。你希望pointHeightheight的两倍,baseHeightheight的一半。例如,如果我希望尖塔顶点的高度是尖塔侧面的三倍,我会将代码改为pointHeight = height * 3baseHeight = height / 2

在你做完这些更改后,当你更改height变量时,尖塔的所有部分都会改变大小。

你不需要修改程序的其他部分。

你可以通过更改原始的height变量并重新运行程序来测试它。如果你将height变量更改为3,你的尖塔将像图 3-9 一样显示。

image

图 3-9:你只需更改 height 变量就可以使尖塔变高。

因为你使用height变量来设置pointHeightbaseHeight的值,所以很容易改变尖塔的外观。通过更改原始的height变量为几个不同的数字来玩这个代码。每次重新运行程序看看会发生什么!

指数

你可以使用一个指数来表示一个数字应该自我相乘多少次。例如,3⁴(三的四次方)是3 * 3 * 3 * 3的简写方式。

在 Python 中,**是指数运算符。你想乘的数字(底数)放在运算符的左侧,你想将它自己相乘多少次的数字(指数)放在右侧。

假设你想开始一个 Minecraft 农场。你需要耕作四块土地。你希望每块土地是四块块四块的大小,这样你就能种植很多小麦。从数学上讲,你可以写成4 * 4 * 4,或者 4³。这里是计算你种植多少小麦的代码:

wheat = 4 ** 3

你的答案应该是 64 块独立的小麦,因为4 * 416,而16 * 464

括号和运算顺序

当你在一个表达式中使用多个数学运算符时,你需要小心如何排列它们。不同的运算符有不同的优先级。当你使用多个运算符时,除法和乘法会先从左到右进行计算,然后是加法和减法。让我们来看一下这个表达式是如何被计算的:

mooshroom = 5 * 2 – 1 + 4 / 2

因为乘法和除法总是先于加法和减法执行,Python 从左开始先将 5 乘以 2 得到 10,然后将 4 除以 2 得到 2。这样我们就得到了 10 – 1 + 2。接着,Python 从左开始先从 10 中减去 1,然后再加上 2,最终将mooshroom设置为 11。

但你可以通过使用括号来控制运算顺序。带有运算符的括号表达式会先计算括号中的运算,再进行其他操作。让我们看看括号如何改变运算顺序。首先,这是一个不使用括号的表达式:

zombiePigmen = 6 * 3 - 2

以这种方式写,zombiePigmen的值最终变为 16,因为 6 乘以 3 是 18,18 减去 2 是 16。然而,使用括号时,结果会发生变化:

zombiePigmen = 6 * (3 - 2)

zombiePigmen现在的值是6!Python 并没有按照通常的顺序操作,而是先从 3 中减去 2,结果是 1,然后将 6 乘以 1 得到 6。

当你希望计算按特定顺序执行时,可以使用括号告诉 Python 先做什么。这让你能更加控制 Python。

实用数学技巧

在接下来的章节中,我将教你两个数学技能,帮助你提升 Python 编程水平,然后我们将结合目前学到的内容进行一次任务。

简写操作符

很多时候,你可能想对一个变量使用运算符,然后将结果存储回同一个变量。例如,你可能想给现有的羊群增加五只羊:

sheep = 6
sheep = sheep + 5

不过,输入sheep = sheep + 5可能会让你觉得有点麻烦。别担心,Python 有一种更简短的写法!Python 有简写操作符,可以让你对一个变量使用数学运算符并将结果重新赋值给同一个变量。以下是四种简写操作符:

• 加法 (+=)

• 减法 (-=)

• 乘法 (*=)

• 除法 (/=)

例如,你可以使用加法简写操作符重写羊群的例子:

sheep = 6
sheep += 5

sheep的值仍然是11,和之前一样。

玩转随机数

使用随机数是一种为你的程序增添神秘感和乐趣的方式。你永远不知道最后会得到什么!许多现实世界的桌面游戏依赖于随机数:想想看你玩过多少游戏是需要掷骰子来决定你能移动多少步的。掷骰子是随机数在实际应用中的经典例子。

Python 可以轻松为你生成随机数,所以让我们模拟掷骰子。生成的数字应该在 1 到 6 之间:

➊ import random
➋ diceValue = random.randint(1, 6)

当你想要生成随机数时,确保在程序开始时包含import random ➊。randint() ➋函数生成一个整数值,程序可以像使用其他数字一样使用它。你放在括号中的数字作为参数告诉randint()生成介于第一个数字和第二个数字之间的值。在这个例子中,生成的数字可以是 1、2、3、4、5 或 6。

你可以使用 randint() 为变量添加一个随机数,甚至生成负数。来看看怎么做吧!

import random
score = 0
score += random.randint(0, 99)
points = random.randint(-99, 99)

Python 可以生成的最低数字是 0,最高数字是 99。而由于负值的参数,points 的值可能会设置为低至 -99 的数字!

任务 #10:超级跳跃,去一个新的地方!

在本章的最后任务中,你将让玩家在 x、y、z 三个轴上随机跳跃。你将存储玩家的当前位置,并为三个坐标值分别添加一个随机数。使用介于 -10 到 10 之间的随机数来改变 xz 的值,确保 y 的随机值在 0 到 10 之间。

要开始,请将清单 3-10 复制到 IDLE 中的新文件里,并将文件保存为 randomJump.py

randomJump.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
   import random

   pos = mc.player.getPos()
   x = pos.x
   y = pos.y
   z = pos.z

➊ x = x + random.randint(-10, 10)
   mc.player.setPos(x, y, z)

清单 3-10:不完整的随机跳跃程序

yz 变量的随机数生成代码缺失,接下来由你来补充它。一旦补充完成,玩家将能像我在图 3-10 中做的那样,随意跳跃。让随机性带你去探索新的、有趣的地方吧!

目前,代码中没有使用简写运算符来改变变量的值。尝试将 ➊ 处的加法表达式改成简写形式。

image

图 3-10:我朝一个随机方向跳跃,结果落在了这棵树上。你最后落到哪里了?

额外目标:随机方块传送

让我们让 randomJump.py 程序变得更加随机!在玩家跳跃到随机位置后,在他们下面放置一个随机的方块。你也可以改编任务 #1 中的传送程序(见 teleport.py,位于第 34 页),使玩家每次都能传送到一个随机位置。如果你不小心传送到某个地方并且卡住了,你可以随时重新运行 teleport.py,它会把你传送到一个安全的地方。

你学到了什么

在这一章中,你学会了如何在 Python 中做数学运算。你将在本书后续的 Python 程序中以及未来你自己创建的程序中频繁使用加法、减法、乘法和除法。你还学会了如何生成随机数,并且在《我的世界》中创建了一些非常有用的程序。做得好!

在第四章中,你将学习 Python 中的字符串数据类型,它用于存储字母、符号和数字。字符串在《我的世界》中非常有用,因为你可以用它们在游戏中发送消息到聊天窗口。你还将学习如何操作字符串来修改《我的世界》的聊天内容以及实现其他酷炫的功能。

第四章:4

使用字符串聊天

image

在第二章和第三章中,你已经学习了整数和浮点数,它们都是数字类型。在本章中,你将使用另一种数据类型,叫做字符串。你可以使用字符串来处理字母、符号以及数字。

字符串帮助你向使用你程序的人显示数据——这是编程中的一个重要部分。通过使用字符串,你可以告诉 Python 将数据输出到屏幕上,进而向用户显示和传达信息。

在 Minecraft 中,你可以在多个地方使用字符串,例如发布信息到聊天框,这是在多人游戏模式下与其他玩家交流的一种方式。虽然在其他版本的 Minecraft 中,发布信息是一个标准功能,但在 Raspberry Pi 版本中它是一个隐藏功能。不过你可以通过编程的力量访问这个功能。你将能够与朋友分享秘密信息,并炫耀你的宝藏!

在本章中,你还将学习关于函数的内容。如果你足够敏锐,你会注意到你已经见过一些函数了。setPos()setTilePos()setBlock()setBlocks()getPos()getTilePos()都是函数——可重用的代码块,使得你能够更轻松地完成任务。挺酷的,对吧?

在本章的任务中,你将基于目前学到的知识进行进一步的学习。你将使用字符串向 Minecraft 聊天框打印信息,并练习输入数据以在 Minecraft 世界中创建对象。

什么是字符串?

字符串数据类型包括任意数量的文本,从单个字母或符号——如"a""&"——到一大段文本。字符串中的每个字母、数字或符号都叫做字符。当你想在程序中包含字母、符号、单词、句子或它们的组合时,你会使用字符串。

使用字符串数据类型,你可以存储字母、数字和符号。所有字符串都被包含在引号中。例如,这是一个字符串:

"Look out! There's a zombie behind you!"

以下也是一个字符串:

'Welcome to my secret base!'

你注意到这些例子写法的微小差异了吗?在写字符串时,你可以使用单引号或双引号:'". 要小心不要混用引号!如果你使用单引号开始一个字符串,你必须用单引号结束它。如果你用双引号开始,就要用双引号结束。Python 编程语言提供这两种选项是有原因的;例如,如果你想在字符串中使用撇号,你可以安全地在双引号中包含它。

PRINT()函数

向用户显示文本和其他信息对于用户交互至关重要;否则,用户将无法知道你的程序在做什么。你显示给用户的信息被称为输出。要将数据输出到用户的屏幕,你可以使用print()函数。

要输出消息,将一个字符串作为参数传递给 print() 函数:

>>> print("String")

这告诉 Python 你想将单词 String 显示给用户。所以,要将 chocolate 打印到 Python shell 中,你写:

>>> print("chocolate")

该输出将会是:

chocolate

你还可以使用 print() 打印变量的值。例如,如果你有一个名为 name 的变量,它存储了一个名字字符串,并且你想将其显示到屏幕上,你可以这样做:

>>> name = "Steve the Miner"

在你将字符串 "Steve the Miner" 存储在 name 中后,你可以简单地写 print(name) 来显示以下输出:

>>> print(name)
Steve the Miner

既然你已经了解了字符串的基础知识,完成任务,向你的 Minecraft 世界问个好吧!

任务 #11:你好,Minecraft 世界

如果你想与 Minecraft Pi 中的其他玩家聊天,Minecraft Python API 允许你使用 postToChat() 函数将消息发送到聊天框中。postToChat() 函数接受一个字符串作为参数,并将该字符串发布到 Minecraft 聊天窗口中。例如,Listing 4-1 将 "Hello, Minecraft World" 发布到聊天框。

message.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()
mc.postToChat("Hello, Minecraft World")

Listing 4-1: 使用 Python 通过 Minecraft 聊天发送问候。

回想一下,参数是你在调用函数时传递给它的信息。函数需要这些信息才能完成它的任务。例如,在上一章中,我们需要将数字传递给函数,以定义我们希望它们执行的操作。在这个例子中,postToChat() 需要一个字符串,比如 "Hello, Minecraft World"

postToChat() 函数类似于 print() 函数。它们都可以在屏幕上显示字符串,并且都可以接受一个存储字符串的变量作为参数。不同之处在于,print() 函数将字符串输出到 Python shell,而 postToChat() 函数则将输出显示在 Minecraft 聊天框中。

从 Listing 4-1 复制代码,并将其保存在名为 strings 的新文件夹中,文件名为 message.py。当你运行程序时,你应该能看到消息被发布到聊天框中,如 Figure 4-1 所示。

image

Figure 4-1: 我的消息已发布到聊天框中。

尝试将不同的字符串传递给 postToChat(),使其显示不同的聊天消息。

额外目标:你在哪里?

你可以使用 mc.postToChat() 函数将各种信息发布到聊天框。试着显示玩家当前的 x 坐标或者他们站立的方块类型。回想一下,mc.player.getTilePos() 函数可以获取玩家当前的位置,而 mc.getBlock() 函数则可以告诉你某个坐标点的方块类型。

INPUT() 函数

到目前为止,所有的变量都在你的程序中设置,或者是 硬编码 的。要改变一个变量的值,你需要编辑程序。能够在程序运行时改变这些变量的值,或者接受 用户输入,会更方便。

为你的程序添加这种互动性的一种方法是使用input()函数。它会将一条字符串打印到控制台(告诉用户应该输入什么样的信息),然后等待用户输入响应。尝试将以下代码输入到 Python Shell 中,看看会发生什么:

>>> input("What is your name? ")

你会看到你传递给input()的字符串,并且你可以输入一个响应。

What is your name?

当你输入一个响应时,你应该看到类似这样的内容:

What is your name? Craig
'Craig'

很棒!但是,如果你想在程序的其他地方使用这个输入,你必须将其保存到一个变量中。与 Python Shell 不同,在文本编辑器中创建的程序不会自动输出语句的结果。例如:

>>> name = input("What is your name? ")
What is your name? Craig

请注意,这次在你输入名字并按下 ENTER 键后,程序不会自动显示你的输入。要查看已保存的输入,只需将变量name作为参数传递给print()函数:

>>> print(name)
Craig

太棒了!现在你已经把输入存储在一个变量中并打印了变量的值。这非常方便,因为它允许你从用户那里获取输入,并在程序的任何地方使用它。让我们使用这种技巧来将聊天消息写入 Minecraft 的聊天中!

任务 #12:编写你自己的聊天消息

让我们让聊天变得更具互动性!你可以像在任务 #11 中一样,使用 Python Shell 在 Minecraft 聊天中写一条消息。在这个任务中,我们将编写一个稍微不同的程序,将你想发布到聊天的字符串保存在一个名为message的变量中。

清单 4-2 会帮你入门。将它复制到 IDLE 中的新文件,并将文件保存为messageInput.py,保存在你的strings文件夹中。

messageInput.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
➊ message = "This is the default message."
➋ mc.postToChat(message)

清单 4-2:如何将字符串输出到 Minecraft 的聊天中

程序将你想输出到聊天的消息存储在变量message ➊中。在这种情况下,变量是一个字符串,内容是"This is the default message."。然后,程序将message传递给postToChat()函数 ➋,该函数将该字符串输出到 Minecraft 聊天。

在这个程序中,字符串是硬编码的,这意味着每次运行程序时它都是相同的。但通过一个简单的修改,你可以让它打印用户输入的任何内容!换句话说,你每次运行程序时都可以编写自己的自定义消息。你将创建属于你自己的聊天程序。

为了让程序接受输入,将字符串"This is the default message." ➊替换为input()函数。给input()函数一个参数,比如"Enter your message: "。记得把这个字符串放在input()函数的括号内!在你做完这些修改后,运行程序。你应该会看到 Python Shell 中出现一个提示符,显示"Enter your message: "。输入你的消息并按 ENTER 键。消息会显示在 shell 中,并且会出现在 Minecraft 的聊天中,如图 4-2 所示。

image

图 4-2:当我在 IDLE shell 中输入消息时,它会被发布到 Minecraft 的聊天中。

现在你的程序允许你写一条消息来显示在聊天框中,而不是你需要在程序中编写消息。看看使用输入功能聊天是不是更轻松了?

附加目标:更多消息

该程序会要求输入一条消息,但你能否想出如何让它先要求输入一条消息,等待几秒钟(使用 sleep() 函数),然后再要求输入第二条消息呢?

连接字符串

经常需要打印字符串的组合。这叫做字符串的拼接,或者 连接,Python 让它变得非常简单。

在 第三章中,我们使用加法运算符(+)来加法运算数字,但你也可以用它来连接字符串。例如:

firstName = "Charles"
lastName = "Christopher"
print(firstName + lastName)

print() 的输出将是 "CharlesChristopher"。如果你希望在值之间加一个空格,可以使用加法运算符来添加空格,像这样:

print(firstName + " " + lastName)

Python 经常提供多种方法来实现相同的结果。在这种情况下,你也可以使用逗号来创建空格:

print(firstName, lastName)

这两条语句都会输出 "Charles Christopher"。你还可以将硬编码的字符串与变量连接在一起,即使这些变量本身就是字符串。只需像写任何其他字符串一样写出值:

print("His name is " + firstName + " " + lastName)

这将输出 "His name is Charles Christopher"

将文本块拼接起来很有用,但有时候你可能需要将字符串与其他数据类型(如整数)连接在一起。Python 不允许你将字符串与整数拼接;在这种情况下,你需要告诉 Python 首先将整数转换为字符串。我们来试试看。

将数字转换为字符串

将一种变量类型转换为另一种变量类型是很有用的。例如,假设你将拥有的金苹果数量存储在名为 myGoldenApples 的变量中,它是一个整数。你想向朋友们炫耀你有多少个金苹果,因为它们很稀有,而你又喜欢炫耀。你可以打印一条类似 "My not-so-secret golden apple stash: " 的消息,后面跟上 myGoldenApples 中存储的值。但在将 myGoldenApples 的值包含到打印消息中之前,你必须告诉 Python 将 myGoldenApples 中的整数转换为字符串。

str() 函数将非字符串数据类型(如整数和浮点数)转换为字符串。要进行转换,只需将你想转换的值放入 str() 函数的括号内。

让我们回到你的金苹果储藏问题。假设你已经将 myGoldenApples 设置为 2,并且你希望 Python 将这个 2 作为字符串而不是整数处理。你可以这样打印你的消息:

print("My not-so-secret golden apple stash: " + str(myGoldenApples))

这条语句会输出字符串 "My not-so-secret golden apple stash: 2"

你还可以将浮点数转换为字符串。比如说你吃了一半的金苹果,现在 myGoldenApples 存储了 1.5 个苹果。str(myGoldenApples) 对 1.5 的处理方式与对 2 的处理方式相同。它将 1.5 转换为字符串,以便你可以将其包含在消息中。

在你将数字转换为字符串后,可以按你喜欢的方式进行连接。让我们来玩转数字与字符串的转换,并将它们连接起来吧!

连接整数和浮点数

如果你想连接两块数据,它们必须是字符串。但加号既用于加法也用于连接,因此,如果你在连接整数、浮点数和其他数字时,Python 会尝试将它们相加。你必须将数字值转换为字符串,才能通过连接将它们连接在一起。

要连接两个数字,而不是相加,只需使用str()方法:

print(str(19) + str(84))

因为你告诉 Python 将数字 19 和 84 视为字符串并连接它们,这条语句输出了1984,而不是 19 和 84 的和103

你可以在语句中多次使用连接。例如:

print("The year is " + str(19) + str(84))

这一行代码输出The year is 1984

现在你已经有了一些使用连接的练习,让我们在下一个任务中考验一下你的新技能!

任务 #13: 向聊天添加用户名

当你和两个人以上一起玩游戏时,可能会很难搞清楚谁在 Minecraft 聊天中写消息。显而易见的解决方法是,在他们的消息开头包含用户名。在这个任务中,你将修改任务#12 中的程序,以便为所有发送到聊天的消息添加用户名。

在 IDLE 中打开messageInput.py并将其另存为名为userChat.py的新文件,保存在strings文件夹中。然后,添加代码在接收消息之前获取用户的姓名作为输入。发送到聊天的消息应该按以下格式显示:"Anna: I need TNT." 你需要使用连接来完成这个任务。

在程序中,找到这一行代码:

message = input("Enter your message: ")

在上面那行代码中,你需要添加另一个变量,命名为username,并将其值设置为input("请输入用户名: ")。在添加了username变量后,找到这一行:

mc.postToChat(message)

使用连接,将usernamemessage字符串在postToChat()函数内连接起来。在这两个字符串之间添加": ",使输出在username变量和message变量之间有一个冒号和一个空格。图 4-3 展示了完成程序后应该显示的输出。

image

图 4-3:现在,当我通过我的程序发布聊天消息时,它会显示我的用户名。

保存你的更新程序并运行它。在 Python shell 中,你将被要求输入用户名。输入你的名字并按下 ENTER。然后,你将被提示写一条消息,完成后,用户名和消息应显示在 Minecraft 聊天中。

额外目标:没有名字的用户

如果你留空用户名并按下 ENTER,会发生什么?你认为这是为什么?

使用 INT()将字符串转换为整数

与将非字符串数据类型转换为字符串的str()函数类似,int()函数将非整数数据类型转换为整数。

int()函数在与input()函数一起使用时非常有用。input()函数将用户输入返回为字符串,但你通常希望在数学运算中使用这个输入。为此,你首先需要使用int()将输入转换为整数类型。

这是它的工作原理。假设我们已经为一个名为cansOfTunaPerCat的变量分配了一个整数值,我们需要一个程序来告诉我们根据用户拥有的猫的数量,吃掉了多少金枪鱼。下面是我们可以编写的程序示例:

cansOfTunaPerCat = 4
cats = input("How many cats do you have? ")
cats = int(cats)
dailyTunaEaten = cats * cansOfTunaPerCat

你可以通过将一个函数嵌套在另一个函数里,在一行代码中完成相同的操作:

cats = int(input("How many cats do you have? "))
dailyTunaEaten = cats * cansOfTunaPerCat

现在你知道如何将输入转换为整数,你可以使用它在 Minecraft 程序中输入方块类型。

任务#14:使用输入创建一个方块

在《Minecraft》中有大量的方块类型。虽然在创意模式下你可以选择很多方块,但许多方块是无法使用的。然而,Minecraft 的 Python API 允许你访问所有方块类型,并使用setBlocks()函数来设置它们。

你之前使用过setBlocks()函数,但你必须将方块类型硬编码到程序中。这意味着你无法在程序运行时更改它。现在,你可以使用input()函数。通过编写一个接受输入的程序,每次运行程序时,你都可以选择想要创建的方块类型。你可以在第一次运行程序时创建一个羊毛方块,第二次运行时创建铁矿石。

在这个任务中,你将编写一个程序,让用户决定他们想设置哪种方块。将清单 4-3 复制到一个新文件中,并将其保存为blockInput.py,放在你的strings文件夹中。

blockInput.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
➊ blockType = # Add input() function here
   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z
   mc.setBlock(x, y, z, blockType)

清单 4-3:在玩家位置设置方块的代码

这个程序会在玩家当前的位置设置一个方块。请将其修改为使用input()函数来设置blockType变量➊。我建议包括一个问题或其他文本提示,以便用户知道应该输入方块编号,而不是其他类型的输入。如果没有提示,IDLE 会一直等待空白行,直到用户输入内容,你需要让用户清楚程序需要一个数字输入。

回想一下,input()将你的输入作为字符串返回,为了将其作为整数输入,你需要使用int()函数。获取方块类型输入的表达式应该像这样:

blockType = int(input("Enter a block type: "))

保存修改后的程序,运行它,并输入你喜欢的任意方块编号。图 4-4 展示了程序的结果。

image

图 4-4:我现在可以创建我想要的任何方块了!

附加目标:更多互动游戏

你可以在程序中使用任意数量的输入。目前,程序会在玩家的当前位置创建一个方块。想一想如何通过输入设置xyz变量。如果你感觉非常冒险,可以尝试通过输入将玩家传送到特定坐标。

从错误中恢复

Python 使用异常处理确保你的程序在发生错误时能够恢复并继续运行。例如,异常处理是管理不正确用户输入的一种方式。

假设你的程序要求输入一个整数,但用户输入了一个字符串。通常,程序会显示一个错误信息,这也叫做抛出异常,然后停止运行。

通过异常处理,你可以自己管理这个错误:你可以保持程序平稳运行,向用户显示有用的错误信息——比如"Please enter a number instead"——并给他们一个修复问题的机会,而无需重新启动程序。

try-except语句是你可以用来处理错误的一种工具。它特别适用于在用户输入错误时向用户提供有用的反馈,并且能够防止程序在发生错误时停止运行。

这条语句由两部分组成:tryexcept。第一部分try是你希望在没有发生错误时运行的代码。这段代码可能会获取输入或打印字符串。如果try部分发生错误,except部分的代码才会执行。

假设有一段代码问你有多少副太阳镜(我有三副):

   try:
➊     noOfSunglasses = int(input("How many sunglasses do you own? "))
   except:
➋     print("Invalid input: please enter a number")

这个程序需要一个数字。如果你输入字母或符号,它会打印"Invalid input: please enter a number"。发生错误的原因是int()函数只能转换仅包含整数的字符串➊。如果你输入一个数字,代码会正常工作,但如果你输入不是数字的内容,比如many sunglasses,这种输入会导致int()函数出错。

顺便问一下,你有没有注意到这段代码有什么不同?这是我们第一次使用需要缩进的语句,缩进就是在输入任何文本之前输入若干个空格。当我在第六章讲解if语句和在第七章与第九章讲解for循环时,我会详细讨论缩进。现在,只要确保你按照本书中的样例输入代码。

通常,当发生错误时,Python 会显示一个难以理解的消息,并且不会清楚地告诉用户如何修复问题。但使用try-except语句,你可以阻止 Python 错误消息在用户输入错误类型的数据时显示,而是给用户提供简单、易懂的指引。 有时用户可能会直接按下 ENTER 键,而不是输入内容。通常这会导致错误,但通过try-except语句中的代码➊,程序会改为在聊天框中打印出"Invalid input: please enter a number" ➋。

你可以将几乎任何代码放入try-except语句中,甚至是其他的try-except语句。下一任务中试试看!

任务 #15:仅允许数字

记得你在任务 #14 中写的程序吗?当你输入一个整数值时,程序按预期工作并创建了一个区块。但如果你输入一个字符串,程序就会停止工作并显示错误,如图 4-5 所示。

image

图 4-5: cake 不是一个数字,因此程序没有创建区块。

这个错误消息对于习惯 Python 的人来说是有意义的。但如果一个从未使用过 Python 的人试图输入一个字符串而不是整数呢?他们会看到一个他们无法理解的错误消息。你的任务是使用错误处理编写一个容易理解的消息。

打开你在任务 #14 中创建的程序blockInput.py。将该程序保存为blockInputFix.py,并放入strings文件夹中。

你将修改程序,使它在要求输入区块编号时使用try-except语句。找到程序中的最后一行代码,它应该像这样:

mc.setBlock(x, y, z, blockType)

在这一行上方添加一个try语句,并在mc.setBlock()函数之前的行首加上四个空格。接下来,在setBlock()上方的那一行,添加以下代码来获取用户的输入:blockType = int(input("Enter a block type: "))

然后,在setBlock()函数后的那一行,写一个except语句。在except语句内部,添加一行代码,在 Minecraft 聊天框中发布一条信息,说明区块类型必须是一个数字;例如,"You didn't enter a number! Enter a number next time."。下面是修改后的代码应该是什么样子(注意行➊和➋开始时的四个空格或缩进):

   try:
➊     blockType = int(input("Enter a block type: "))
       mc.setBlock(x, y, z, blockType)
   except:
➋     mc.postToChat("You did not enter a number! Enter a number next time.")

int()函数期望将用户输入的内容转换为整数➊。由于我们已将try-except语句添加到程序中,如果用户输入的内容包含非数字(如字母或符号),将会发生错误。程序不会显示通常的 Python 错误消息,而是将一条请求用户只输入数字的消息输出到聊天框➋。你可能想把这个聊天消息改得更礼貌一些!

当你完成输入一个更友好的错误信息后,保存 blockInputFix.py 文件,并运行它以欣赏你的成果。结果应该类似于图 4-6。

image

图 4-6:聊天中显示的错误信息更加易于理解。

任务 #16:冲刺记录

本章的最终任务将你在变量(第二章)和数学运算符(第三章)方面学到的所有知识与向聊天发送消息结合起来。你的任务是创建一个记录保持器:程序将计算玩家在 10 秒内的行驶距离,并在聊天中显示结果。

记住,你可以使用以下代码让程序暂停,或者让程序“睡眠”一段时间(按秒计):

import time     # Place this somewhere near the top of your program
time.sleep(30)  # Makes the program wait 30 seconds

使用这个 sleep() 示例,输入以下代码来开始这个新程序:

sprint.py

   import time
   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

➊ pos1 = mc.player.getTilePos()
   x1 = pos1.x
   y1 = pos1.y
   z1 = pos1.z

   time.sleep(10)

➋ pos2 = mc.player.getTilePos()
   x2 = pos2.x
   y2 = pos2.y
   z2 = pos2.z

   # Compare the difference between the starting position and ending position
➌ xDistance = x2 – x1
   yDistance =
   zDistance =

   # Post the results to the chat
➍ mc.postToChat("")

让我们来分解这段代码。程序获取玩家的起始位置 ➊,等待 10 秒,然后获取玩家的结束位置 ➋。要完成程序,你需要计算起始位置和结束位置之间的差距。为此,设置 yDistancezDistance 变量的值,它们从 ➌ 开始。为了帮助你,我还提供了 xDistance 变量的值,应该是 x2 – x1yDistancezDistance 变量的值应该与此类似,不过请使用不同的变量而不是 x1x2

在最后一行,将结果输出到 Minecraft 聊天中 ➍。结果应该以以下格式显示:"玩家已移动 x: 10, y: 6, z: -3"。使用字符串、连接符以及 xDistanceyDistancezDistance 变量的值来实现这一点。

将这个程序保存在 strings 文件夹中,命名为 sprint.py 并运行它。图 4-7 显示了程序的运行结果。

image

图 4-7:程序完成后,我行驶的距离被显示出来。

如果你已经在运行程序,但发现自己在命令行和 Minecraft 之间切换不够快,可以尝试在步骤 2 前加入一个三秒钟的倒计时。将这个倒计时发送到聊天中。

额外目标:直线飞行

此时,程序分别显示了沿各个轴线的行进距离:x 轴、y 轴和 z 轴。你将如何创建并显示总的行驶距离,换句话说,就是“直线飞行”的距离呢?提示:你需要使用三角函数,特别是毕达哥拉斯定理。如果你现在还不确定怎么做,不用担心;你会在任务 #40(第 141 页)中看到一个类似的程序,其中有计算总行驶距离的代码。

你学到了什么

恭喜!你在本章学到了很多内容。你创建了字符串,使用打印语句显示字符串,并通过连接操作将它们合并。你编写了能够接收用户输入的程序,改变了值的数据类型,并处理了异常。在此过程中,你将你的 Python 知识应用到使 Minecraft 聊天更加生动。

在第五章,你将学习如何控制程序的流程,并告诉你的程序如何做出决策。

第五章:5

用布尔值搞清楚什么是真的,什么是假的

image

你时常会问是非问题:在下雨吗?我的头发太长了吗?一旦你知道答案是“是”还是“否”,你就能决定接下来做什么:带伞,还是不带;修剪头发,还是不修剪。在所有这些情况下,你的行动取决于问题的答案是否为“是”或“否”。根据问题的答案来决定该做什么,在编程中也很重要。在本章中,你将学习如何在 Python 中提问。

在编程中,你提出的问题通常是关于比较值的。一个值是否等于另一个值?一个值是否大于或小于另一个值?这种是非问题称为条件,其答案不是,而是TrueFalse。假设你问了这样一个问题:“我比我的朋友有更多的金块吗?”或者换句话说,“我的金矿比我朋友的金矿大吗?”为了让这个问题成为 Python 能理解的条件,我们必须将其表述为一个可以为真或假的声明(例如:“我的金矿比我朋友的金矿大”)。

在 Python 中,测试一个条件是否为真或假是非常有用的,因此有一个专门的数据类型用来存储TrueFalse这两个值。到目前为止,你已经看到过其他几种数据类型:整数、浮点数和字符串数据类型。用于存储TrueFalse值的数据类型就是布尔数据类型。布尔值只能是TrueFalse。当你在 Python 中提问时,结果要么是True,要么是False。当一个条件为真或假时,程序员会说它评估为True评估为False

在本章中,你将使用布尔值、比较运算符和逻辑运算符来测试涉及值的不同条件。然后你将准备好进入第六章,在那里你将使用问题的答案来决定接下来在程序中该做什么。

布尔基础

布尔值有点像电灯开关:它要么是True(开),要么是False(关)。在 Python 中,你可以像这样声明一个布尔变量来表示灯亮着:

light = True

在这里,你将True的值赋给变量light,若要关闭灯光,可以将False的值赋给light

light = False

始终大写TrueFalse的首字母。如果不这样做,Python 将无法识别该值为布尔值,并会抛出异常,而不是评估你的计算!

在下一个任务中,你将使用布尔值来阻止玩家在游戏世界中砸方块。

任务 #17:停止砸方块!

在 Minecraft 中,砸方块很容易,当你想挖掘资源时这很棒。但当你花了很长时间建造一个非常酷的结构,然后不小心砸坏它时,就很让人烦恼!在这个任务中,你将使你的 Minecraft 世界不可摧毁。

通过使用setting("world_immutable", True),你可以使方块不可变,这意味着它们无法被改变。setting()这行代码是一个函数,就像你之前看到的setTilePos()setPos()函数一样。列表 5-1 展示了如何使世界不可变。

immutableOn.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

mc.setting("world_immutable", True)

列表 5-1:防止方块被破坏的代码

setting()函数有一些选项,你可以将它们设置为True来启用它们。一个选项是"world_immutable"。要启用一个setting()选项,你只需在括号内写上True

在 IDLE 中输入列表 5-1 并将其保存为immutableOn.py,放入名为booleans的新文件夹中。当你运行它时,大多数方块不应该被破坏,如图 5-1 所示。但如果你确实想重新破坏方块呢?将你的程序复制到一个新文件中,并更改它,以允许玩家破坏方块。(提示:使用布尔值!)将新文件保存为immutableOff.py,放入booleans文件夹中。

image

图 5-1:无论我多么努力,方块都不会破碎!

连接布尔值

像整数和浮点数一样,布尔值必须在连接之前转换为字符串。例如,当你想使用print()函数输出布尔值时,你会将布尔值与字符串连接。要做到这一点,使用str()函数:

>>> agree = True
>>> print("I agree: " + str(agree))
I agree: True

agree变量存储一个布尔值。在第二行中,它被转换为字符串str(agree),与"I agree: "字符串连接,并被打印出来。

比较符

你非常擅长比较值。你知道 5 大于 2,8 和 8 是相同的数字,6 和 12 不是相同的数字。计算机也很擅长比较值;你只需要告诉它你想进行哪种比较,通过输入一个叫做比较符的符号。例如,你希望它检查一个值是否大于另一个,还是检查它是否更小?

比较符(或比较运算符)在 Python 中允许你比较数据。Python 使用六个比较符:

• 等于(==

• 不等于(!=

• 小于(<

• 小于或等于(<=

• 大于(>

• 大于或等于(>=

每个比较符返回一个布尔值(TrueFalse),表示条件是否成立。让我们来看看这些比较符,并探索如何使用它们!

等于

当你想找出两个值是否相同时,可以使用等于比较符(==)。当值相同,比较结果返回布尔值True。当值不同,比较结果返回False

例如,我们可以为两个变量赋值,然后使用等于运算符来比较它们:

>>> length = 2
>>> width = 2
>>> length == width
True

结果是True,因为lengthwidth变量的值相同。

但是如果它们不同,结果是False

>>> length = 4
>>> width = 1
>>> length == width
False

你可以对所有变量类型使用等于比较器:字符串、整数、浮动数值和布尔值。

请注意,我是如何使用==来比较lengthwidth的,而不是使用=,后者用于设置变量。Python 使用==运算符来区分比较(询问两个值是否相等)和设置变量(使一个变量等于某个值)。记住这个区别可以避免代码中的 bug。别担心,甚至我有时也会犯用=而不是==的错误!

任务 #18: 我在游泳吗?

现在,你将使用比较器制作一个程序,显示你是否站在水中。结果将会显示在 Minecraft 聊天框里。

要查找特定坐标的方块类型,你将使用getBlock()函数。这个函数以三个坐标作为参数,返回一个整数表示方块类型。例如:

blockType = mc.getBlock(10, 18, 13)

在这里,我将mc.getBlock(10, 18, 13)的结果存储在名为blockType的变量中。如果坐标(10, 18, 13)处的方块是甜瓜(方块值 103),那么blockType变量的值将是 103。

让我们来使用getBlock()函数。列表 5-2 检查玩家是否站在干燥的土地上。

swimming.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

pos = mc.player.getPos()
x = pos.x
y = pos.y
z = pos.z

blockType = mc.getBlock(x, y, z)
mc.postToChat(blockType == 0)

列表 5-2:这段代码检查玩家双腿所在的方块类型。

在这里,我获取了玩家位置的三个坐标,并将这些坐标作为参数传递给getBlock()。我将mc.getBlock(x, y, z)的结果存储在blockType中。表达式blockType == 0检查方块是否为空气;如果是空气,你知道自己只是在 Minecraft 世界中的某个地方站着,表达式为TrueTrue会显示在聊天框里。如果不是空气,False会显示在聊天框里,那你一定是在水下或者可能被沙子淹没!

复制列表 5-2 并将其保存为swimming.py文件在第五章目录下。然后修改代码,使其检查玩家是否站在水中(方块类型 9),并运行它。

尝试站在水中并运行程序。确保当玩家在水中时,聊天框显示True。当玩家不在水中时,聊天框应该显示False

程序的输出应该像图 5-2 一样。

注意

此时,你将无法持续运行这个程序。每次你想检查玩家下方的方块时,都必须重新运行程序。这同样适用于本章的其他任务。

图片

图 5-2:尽管我可以看到自己站在水中,Python 也很贴心地确认了这一点。

额外目标:我在飞行!

通过对代码进行一些修改,你可以检查你脚下的方块是否为空气。这可以告诉你你是在飞行还是跳跃。你会怎么做呢?

不等于

不等于比较器是等于比较器的相反操作。它不是检查两个值是否相同,而是检查它们是否不同。当两个值不同时,比较结果为True;当它们相同时,结果为False

假设你想确保一个物体是矩形但不是正方形。因为非正方形矩形的长度和宽度不同,你可以写一个比较来检查长度和宽度是否不相等:

>>> width = 3
>>> length = 2
>>> width != length
True

width != length表达式询问widthlength的值是否不同。

这个比较的结果为True,因为width变量和length变量的值不同。

但如果这些值相同,比较结果将返回False

>>> width = 3
>>> length = 3
>>> width != length
False

不等于比较器同样适用于字符串、整数、浮点数和布尔值,就像等于比较器一样。

任务 #19:我站的不是空气吗?

假设你想检查自己是否站在除了空气之外的某种物体上,比如水、岩浆、泥土、砾石或其他类型的方块。在任务 #18 中,你检查了当前位置的方块是否是空气,并且你也已经学会了如何检查自己是否站在水中。你可以将程序复制粘贴多次,每次稍作修改,来检查是否是岩浆、泥土、砾石等。但是这样做会很枯燥。相反,你可以使用不等于比较器来检查自己是否在地下、是否被困在沙子中、是否处于海底,甚至是否正在岩浆中溺水!

打开任务 #18 中的程序(swimming.py),并将其保存为notAir.pybooleans文件夹中。删除程序的最后一行,并用列表 5-3 替换。

notAir.py

➊ notAir = blockType == 0
   mc.postToChat("The player is not standing in air: " + str(notAir))

列表 5-3:游泳程序的更改

这段代码的最后一行将打印出你是否没有站在空气中的信息。比较的结果存储在notAir变量中 ➊。当比较结果为True时,notAir变量的值为True;当比较结果为False时,notAir变量的值为False

但是第一行的比较有点问题 ➊。它当前检查blockType是否等于空气,使用的是等于比较器(==)。实际上,应该使用不等于比较器(!=)来检查blockType变量是否不等于空气。将第一行改成使用不等于比较器,而不是等于比较器。这样可以检查玩家当前位置的方块是否不是空气。

运行程序时,确保它在你站在空气中、在水下、在岩浆中、在砾石中、在沙子中,或者被传送到地下时都能正常工作。当条件为True时,显示在聊天中的消息如图 5-3 所示。

image

图 5-3:在水里悠闲地游泳,而水不是空气。

大于和小于

当你需要判断一个值是否大于另一个值时,使用大于比较器。大于比较器会在左边的值大于右边的值时返回True。如果左边的值小于或等于右边的值,则比较会返回False

假设我们有一辆矿车,最多只能提升 99 块黑曜石。只要矿车的提升限制大于它试图提升的黑曜石块数,它就能提升这些块:

>>> limit = 100
>>> obsidian = 99
>>> limit > obsidian
True

太棒了!我们的矿车可以运载任何少于 100 块黑曜石的数量,而 99 小于 100,所以limit > obsidian评估为True。但如果有人往堆里加了一块黑曜石会怎样?

>>> limit = 100
>>> obsidian = 100
>>> canLift = limit > obsidian
False

哎呀,现在已经达到限制了!结果变成了False:100 不大于 100,它们相等。我们的矿车无法提升黑曜石。

小于比较器的工作方式相同。

一辆车驶过桥下时,需要知道它是否足够小,能够通过桥下:

>>> vanHeight = 8
>>> bridgeHeight = 12
>>> vanHeight < bridgeHeight
True

在这种情况下,车子能通过桥下,因为它的高度小于桥的高度:8 小于 12。后来在旅程中,同样的车可能会遇到一个过低的桥,无法通过:

>>> vanHeight = 8
>>> bridgeHeight = 7
>>> vanHeight < bridgeHeight
False

因为 8 不是小于 7,所以结果是False

大于或等于和小于或等于

与大于比较器类似,大于或等于比较器确定一个值是否大于另一个值。与大于比较器不同,如果两个值相等,它也会评估为True

假设我正在给所有来看我精彩的程序展示的人发贴纸。我需要检查是否有足够的贴纸分发给每个人:

>>> stickers = 30
>>> people = 30
>>> stickers >= people
True

我有足够的贴纸:30 等于 30,所以stickers >= people评估为True。但是假设我的朋友觉得这些贴纸很酷,想要一张。现在,31 个人想要贴纸:

>>> stickers = 30
>>> people = 31
>>> stickers >= people
False

我没有足够的贴纸:30 不大于或等于 31。看起来我的朋友不能得到贴纸。

到现在为止,你已经准备好解决几乎任何比较问题了。当你在 IDLE 时,试试“小于或等于”比较器(<=),看看它是如何工作的。

注意

大于、大于或等于、小于和小于或等于比较器不能用于字符串,尽管它们可以用于整数、浮点数和布尔值。

任务#20:我在地面上吗?

Minecraft 中玩家的 y 坐标显示他们在游戏中的高度。方块也使用坐标存储,这使得你可以使用getBlock()获取特定坐标处的方块类型,并使用setBlocks()在特定坐标处创建方块。

要获取 Minecraft 中的最高方块,你可以使用getHeight()函数。该函数接受 x 和 z 坐标并返回该位置的最高方块的 y 坐标,如清单 5-4 所示。

aboveGround.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()
pos = mc.player.getTilePos()
x = pos.x
y = pos.y
z = pos.z
highestBlockY = mc.getHeight(x, z)
mc.postToChat(highestBlockY)

清单 5-4: 查找玩家当前位置上方块的 y 坐标的代码

这个程序获取玩家的当前位置信息,获取玩家当前位置上方块的 y 坐标,然后将这个值发布到 Minecraft 聊天中。

通过将这个程序与大于或等于比较器结合,你可以检查玩家是否在地面上。现在我们来做这个。

复制清单 5-4 中的程序,并将其保存为aboveGround.py。修改程序以检查玩家的 y 坐标是否大于highestBlockY变量。然后,添加代码以将结果以“玩家是否在地面上:True/False”的格式发布到聊天中。

提示

记住,你可以将比较的结果存储在变量中。例如,如果我想检查 y 是否大于或等于 10,并将结果存储在一个名为 highEnough 的变量中,我会使用以下语句: highEnough = y >= 10

在进行这些更改后,运行程序。程序的False输出结果显示在图 5-4 中。

image

图 5-4: 现在我在一个洞穴里,所以 Python 的判断是正确的,我并不在地面上。

任务 #21: 我离家很近吗?

当你在 Minecraft 的世界里四处游荡时,可能会迷路,忘记自己家在哪。你可能会走几个小时,最终发现最初迷路时离家很近。

通过一行代码,你可以检查自己距离游戏中任意坐标的远近。例如,你可以使用你家的坐标和当前位置来计算自己距离家有多远。通过添加比较器,你还可以检查自己是否距离家在某个特定的范围内。如果你距离家只有 40 个方块,那么我们就认为你离家很近。

让我们写一个 Python 程序来为你检查!这个任务的代码应该检查你离家有多远,如清单 5-5 所示。

farFromHome.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
   import math
➊ homeX = 10
   homeZ = 10
   pos = mc.player.getTilePos()
   x = pos.x
   z = pos.z
➋ distance = math.sqrt((homeX - x) ** 2 + (homeZ - z) ** 2)
➌ mc.postToChat(distance)

清单 5-5: 输出你家距离的代码

这段代码假设你家的坐标为x = 10z = 10,这些值通过homeXhomeZ变量设置➊。在这种情况下,我们不需要关心 y 坐标。我使用getTilePos()函数获取玩家的位置并设置xz值。

为了计算distance变量,我们使用一种叫做勾股定理的公式。它计算直角三角形一边的长度,你可以在 Minecraft 中使用它来计算两点之间的距离。你可能在数学课上见过这个公式,形式是a² + b² = c²,其中ab是直角三角形的两条直角边,c是斜边,正如图 5-5 所示。在➋处,我们正在求解c,它由变量distance表示。

将清单 5-5 保存为farFromHome.py,并放在booleans文件夹中。

为了完成程序,使用小于或等于比较符检查distance变量的值是否小于或等于 40,并将结果以"Your house is nearby: True/False"的格式发布到聊天中 ➌。使用连接运算符将字符串与比较结果结合起来。更新postToChat() ➌函数的内容以输出该字符串。

image

图 5-5:直角三角形

测试程序。当你离家不超过 40 格时,应该收到True消息;当你距离超过 40 格时,应该看到False消息。图 5-6 展示了程序的运行效果。

image

图 5-6:我肯定在离家 40 格以内。事实上,那就是前门!

逻辑运算符

在程序中,组合两个或更多比较符是常见的需求。你可能想要判断两个条件是否都为True:例如,你可能想要一辆红色的车并且价格低于 10,000 美元。

要组合两个或更多的比较符,你可以使用逻辑运算符。与比较符一样,逻辑运算符可以在任何需要布尔值的地方使用。逻辑运算符也被称为布尔运算符。你将学习三种类型的逻辑运算符:andornot

AND

当你想要检查两个比较结果是否都为True时,使用and运算符。对于一个包含and运算符的表达式,要想结果为True,两个比较必须都为True。如果其中一个比较为False,整个语句的结果将为False

假设我想要判断一个人是否年满 18 岁且拥有汽车。我可能会写出以下程序:

>>> age = 21
>>> ownsCar = True
>>> age > 18➊ and ownsCar == True➋
True

在这里,我们在➊和➋处使用and组合了两个比较符。因为这个人的年龄大于 18 岁(age > 18的结果为True),并且他们拥有一辆车(ownsCar == True),所以整个表达式age > 18 and ownsCar == True的结果为True

如果其中一个比较结果是False,语句的结果将为False。假设这个人没有车但年满 18 岁:

>>> age = 25
>>> ownsCar = False
>>> age > 18 and ownsCar == True
False

在这里,age > 18的结果为True,而ownsCar == True的结果为False,因此整个表达式的结果为False

表 5-1 总结了使用and运算符时所有可能的布尔组合及其结果。

表 5-1: 使用and运算符的TrueFalse的不同组合

比较 A 比较 B A 和 B

任务 #22:我完全在水下吗?

在任务 #18(第 85 页),你检查了玩家是否在游泳。程序根据玩家当前位置的方块是否为水返回TrueFalse。这告诉你玩家的腿是否在水下,但无论玩家头是否在水下,结果都是一样的。那么,你如何检查玩家的腿和头是否都在水下呢?

通过一些简单的修改,添加and运算符,swimming.py程序就可以检查玩家的腿和头是否在水下。打开swimming.py并保存为underwater.py

进行以下更改,使程序检查玩家是否完全在水下:

  1. 添加第二个变量,检查玩家在y坐标位置+ 1的方块类型。该变量存储玩家头部上方的方块类型。将此变量命名为blockType2

  2. 检查blockType是否等于水,以及blockType2是否等于水。

  3. 将比较结果通过此消息发送到聊天:“玩家是否在水下:True/False”。

提示

要检查 blockType blockType2 是否都等于水,你可以使用 and 运算符。首先,你可以使用表达式 blockType == 9 检查 blockType 是否等于水。然后,使用表达式 blockType2 == 9 检查 blockType2 是否等于水。为了将这两者结合,你可以在中间加上and运算符,像这样: blockType == 9 and blockType2 == 9

运行程序时,请确保测试它在所有三种情况中的表现(当玩家在水面上方时,当只有玩家的腿在水下时,以及当玩家完全在水下时)。图 5-7 显示了程序工作的示例。

image

图 5-7:玩家在水下,沿着海底前进。

附加目标:我在隧道里吗?

检查玩家是否处于泥土隧道或鹅卵石隧道中。为此,你需要检查玩家上方和下方的方块。

or运算符的工作方式与and不同。当任一或两者比较结果为True时,or表达式将返回True。只要有一个比较为True,表达式就为True。但是,如果两个比较都不为True,表达式将评估为False

假设我想领养一只黑色或橙色的猫。我可以使用以下代码获取用户输入,然后检查字符串的值是否为"black""orange"

catColor = input("What color is the cat?")
myCatNow = catColor == "black" or catColor == "orange"
print("Adopt this cat: " + str(myCatNow))

只要 catColor"black""orange",我就会收养它。但如果它是其他颜色,比如 "gray",那么 myCatNow 就会是 False,我就不会收养这只猫。

表 5-2 包含了使用 or 运算符与布尔值结合时的所有可能组合及其结果。

表 5-2: 使用 or 运算符时 TrueFalse 的不同组合

比较 A 比较 B A 或 B
TRUE TRUE TRUE
TRUE FALSE TRUE
FALSE TRUE TRUE
FALSE FALSE FALSE

任务 #23:我在树中吗?

本章中你创建的程序会根据玩家是否站在某个特定的方块类型上,显示 TrueFalse。但如果你想检查玩家是否在树中呢?该怎么做呢?因为树是由木材和树叶组成的,所以你需要检查玩家是否站在木材树叶上。

让我们写一个程序。再次打开 swimming.py 文件,并将其另存为名为 inTree.py 的新程序。

修改程序,使其检查玩家下方一个方块的类型。你需要使用 or 运算符来检查玩家下方的方块是树叶(方块类型 18)还是木材(方块类型 11),然后将结果发布到聊天中。

请记住,你可以使用 y = y - 1 来检查玩家下方的方块。

注意

尽管树和树叶有不同的颜色,但所有树木共享相同的方块 ID,所有树叶也共享相同的方块 ID。(唯一的例外是金合欢和深色橡木木材与树叶,它们属于不同的方块类型。暂时我们忽略金合欢和深色橡木。)颜色是通过一个第二个值设置的,稍后的章节你将学习如何使用它。

当你运行程序时,应该会看到与 图 5-8 中相同的输出。

image

图 5-8:我在树中。

NOT

not 运算符与 andor 运算符有些不同。它用于单一的布尔值或比较,并简单地将其值反转。

换句话说,not 会将 True 改为 False,将 False 改为 True

>>> not True
False
>>> not False
True

当你开始将 not 运算符与其他逻辑运算符结合使用时,它非常有用。让我们在你 不饿 时给 timeForBed 赋值。

>>> hungry = False
>>> sleepy = True
>>> timeForBed = not hungry and sleepy
>>> print(timeForBed)
True

not 运算符只对它前面的布尔值起作用。这里,它反转了 hungry 变量的值,同时保持 sleepy 变量不变。因为我们之前把 hungry 设置为 False,所以写 not hungry 会将其值变为 Truesleepy 的值是 True。现在两个值都为 True,因此 timeForBedTrue

任务 #24:这个方块不是西瓜吗?

你饿了,想知道家里是否有食物。你最喜欢的食物是西瓜,你总是把它存放在家里的同一个地方。但你不记得家里是否还有西瓜了,所以你需要决定回家的路上是否要买些食物。

幸运的是,你在学习 Python!只需一点脑力,就能写出一个 Python 程序来检查你家里是否有瓜。

在这个任务中,你将创建一个程序来判断在回到你的 Minecraft 房子之前,是否需要找食物。程序会检查某些坐标处是否有瓜。你检查的坐标由你决定——它们可以在你的房子里,农场上,或你决定放置瓜的任何地方。在这些坐标上放置瓜也由你决定。

复制清单 5-6 并将其保存为notAMelon.py

notAMelon.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   x = 10
   y = 11
   z = 12
➊ melon = 103
➋ block = mc.getBlock(x, y, z)

➌ noMelon = # Check the block is not a melon

➍ mc.postToChat("You need to get some food: " + str(noMelon))

清单 5-6:检查特定位置是否有瓜的代码开头

这段代码是用来检查特定位置的块是否是瓜块。我包含了一个名为melon的变量,用来存储瓜的块 ID(103)➊,并调用了getBlock()方法,将结果存储在名为block的变量中➋。为了完成这个程序,你需要完成➌这一行,检查melon变量是否与block变量不相等。结果应该存储在noMelon变量中,以便在最后一行输出到 Minecraft 聊天窗口中 ➍。

你可以通过两种方式编写检查➌,看看melonblock变量是否不相等:你可以使用不等于比较符号或not逻辑运算符。尽管程序两种方式都能运行,但尝试使用not逻辑运算符来编写这个程序。

当你完成更改后,运行程序。结果应该类似于图 5-9。

image

图 5-9:我的农场上有一个瓜,所以我不需要再找其他食物。

奖励目标:储藏丰富的食品柜

更改程序检查的块类型。你可以检查你的农场上是否有玉米,或者有人是否偷走了你的前门。

逻辑运算符顺序

你可以在单个语句中组合任意多个逻辑运算符。例如,这里有一个非常复杂的组合,使用了andornot

>>> True and not False or False
True

这段代码的结果是True。你感到惊讶吗?在这个例子中,语句中的not False部分首先被计算为True。这相当于:

>>> True and True or False
True

然后评估andTrue and True的结果是True,这相当于:

>>> True or False
True

最后,评估or,所以True or False的结果是True

当 Python 评估逻辑运算符时,它有一定的顺序。如果你搞错了顺序,可能会得到你意料之外的结果!以下是 Python 的评估顺序:

  1. not

  2. and

  3. or

练习在 IDLE 中创建带有逻辑运算符的语句,看看你能否猜出每个语句的结果。

我的数字是否介于两个数之间?

通常,你会想检查一个值是否小于一个值且大于另一个值。假设你想确保你有 10 到 20 只狼,因为你喜欢狼并且希望有超过 10 只,但 20 只或更多可能会造成问题,因为你会吃光食物。你可以使用and运算符来测试这个条件:

wolves = input("Enter the number of wolves: ")
enoughWolves = wolves > 10 and wolves < 20
print("Enough wolves: " + str(enoughWolves))

但你也可以用另一种方法来做。你可以不使用and运算符,而是将变量写在两个比较运算符之间:

wolves = input("Enter the number of wolves: ")
enoughWolves = 10 < wolves < 20
print("Enough wolves: " + str(enoughWolves))

如果你运行这些程序中的任何一个,并输入一个介于 10 和 20 之间但不等于这两个值的数字,则enoughWolves将为True。你也可以使用大于等于运算符(>=)和小于等于运算符(<=)来实现相同的效果:

wolves = input("Enter the number of wolves: ")
enoughWolves = 10 <= wolves <= 20
print("Enough wolves: " + str(enoughWolves))

在这种情况下,输入 10 或 20 也会使enoughWolves的值为True

任务 #25:我在房子里吗?

使用 Python 代码,你可以在玩家走到地图上的某个区域时让一些酷炫的动作发生。你可以让一个秘密门在玩家走到某个特定方块时打开,或者当他们走过陷阱时将他们困在箱子里。在这个任务中,我将向你展示如何检测一个人是否在你的 Minecraft 房子里。

在任务 #8(第 55 页),你创建了一个自动构建建筑物的墙壁、天花板和地板的程序。你将该程序保存在math文件夹中的building.py中。现在打开这个程序。

阅读building.py程序中的代码,并记录widthheightlength变量的值(默认情况下,这些值分别为1056)。同时,写下你当前站立的坐标。运行建筑程序来建造一座房子。

现在你已经建好了一个建筑物,我们可以编写一个像清单 5-7 那样的程序,来检查玩家是否站在建筑物内部。

insideHouse.py

   from mcpi.minecraft import Minecraft
   mc = minecraft.create()

➊ buildX =
   buildY =
   buildZ =
➋ width = 10
   height = 5
   length = 6

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z

➌ inside = buildX < x < buildX + width and

清单 5-7:检查玩家是否在房子内的程序开头

清单 5-7 应该检查玩家的 x 坐标是否在building.py创建的建筑物内,但程序尚未完成!你的任务是确保程序还检查 y 和 z 坐标,确保它们在你使用building.py程序建造的房子的坐标范围内。

将清单 5-7 复制到一个新文件中,并将其保存为insideHouse.py。你将完成该程序,使其检查玩家是否在建筑物内部。

完成程序,执行以下操作:

  1. 添加建筑物的坐标(这些坐标是你运行building.py程序时站立的位置)➊。

  2. 如果widthheightlength变量与building.py程序中使用的值不同,请修正它们➋。

  3. 完成对inside变量的比较,以检查玩家的坐标是否在建筑物内。第一部分,用于检查 x 坐标是否在房子内,已经为你完成 ➌。你需要添加 y 和 z 坐标的比较。表达式与我为 x 坐标所写的类似(buildX < x < buildX + width)。

  4. inside变量的值发布到聊天中。

  5. 当你完成更改后,保存并运行程序。你应该看到类似图 5-10 的输出。

    image

    图 5-10:我在我的卧室,确实是在我的房子里。

你学到了什么

在本章中,你使用了布尔值、比较器和逻辑运算符来回答程序中的问题。在第六章,你将编写根据这些问题的答案做出决策的程序。你将检查一个条件是否为真,并告诉程序如果条件为真就运行某段代码,或者如果条件为假则运行另一段代码。在第七章,你的程序将在条件为真时持续运行某段代码,直到条件变为假时停止运行。这就是布尔值和比较器的真正威力。它们帮助你控制程序中哪段代码被执行,以及何时被执行。

第六章:6

使用 if 语句制作迷你游戏

image

在第五章中,你学会了如何在 Python 中提问。你使用了比较运算符(如==!=><等)和逻辑运算符(andornot)来判断条件或一组条件的真假。在本章中,你将利用这些问题的答案——你测试的条件的结果——来决定执行哪些代码。

你每天都根据条件做决策。现在是夜晚吗?如果是,你就穿上钻石盔甲,带上剑来击退怪物。如果不是,你可能会将所有装备留在你的秘密基地。你饿了吗?如果是,你就吃一些面包或苹果。如果不是,你可能会去进行一场大冒险,以便增加食欲。就像你在日常生活中做决策一样,你的程序也需要根据条件执行不同的任务。

我们将使用一些 Python 代码来帮助你的程序做出决策。if语句告诉你的程序是否运行某段特定的代码。if语句的意思是“如果这个条件为真,就运行这段代码。”例如,你可以检查玩家是否站在一个禁区内,如果是,则将地板变成熔岩。或者,你可以检查他们是否在特定位置放置了某个方块,如果放了,就打开一个隐藏的门。通过使用条件和if语句,你可以开始在 Minecraft 中制作你自己的迷你游戏。

使用 if 语句

能够控制程序的执行是一项非常强大的能力;事实上,它对编程至关重要!程序员有时称这一概念为流程控制。添加这种控制的最简单方法是使用简单的if语句,当条件为True时执行代码。

if语句有三部分:

if运算符

• 测试条件

• 如果条件为True,则执行一段代码

让我们看一个实际的if语句。以下代码只有在僵尸数量超过 20 时,才会打印出"That's a lot of zombies."。否则,它什么也不做。

zombies = int(input("Enter the number of zombies: "))
if zombies > 20:
    print("That's a lot of zombies.")

在这里,zombies > 20是我们要测试的条件,而print("That's a lot of zombies.")if语句的主体;它是当zombies > 20True时执行的代码。if语句行末的冒号(:)告诉 Python 接下来的一行将开始执行if语句的主体。缩进则告诉 Python 哪些代码行构成了这个主体。缩进是指在文本行的开头有额外的空格。在 Python 中,你通过四个空格来缩进代码行。如果我们想在if语句中添加更多的代码行,我们需要在它们前面加上相同数量的空格,就像print("That's a lot of zombies.")那样缩进。

尝试多次运行此代码,测试每个条件,看看会发生什么。例如,尝试输入一个小于 20 的数字,20,以及一个大于 20 的数字。以下是输入 22 时发生的情况:

Enter the number of zombies: 22
That's a lot of zombies.

好的,结果很有道理。让我们再运行一次,看看当条件不满足时会发生什么。

Enter the number of zombies: 5

请注意,如果条件是False,什么都不会发生。if语句的主体会被完全忽略。只有当条件为True时,if语句才会执行其主体中的代码。当if语句执行完毕后,程序会继续执行if语句后的代码。

让我们看另一个例子,以更好地理解它是如何工作的。以下代码使用if语句检查密码是否正确:

password = "cats"
attempt = input("Please enter the password: ")
if attempt == password:
    print("Password is correct")
print("Program finished")

if语句后的表达式是条件:attempt == password。在if attempt == password:之后缩进的那一行是if语句的主体:print("Password is correct")

只有当attempt变量中存储的值与password变量中的值相同,代码才会打印"Password is correct"。如果它们不相同,什么也不会打印。最后一行代码会在if语句的主体是否执行都执行,并打印"Program finished"

现在让我们尝试一些稍微有点爆炸性的东西。

任务 #26:爆炸坑洞

你已经学会了如何让玩家传送和跳得很高。现在,你将让玩家周围的方块消失。

当程序运行时,玩家上方、下方以及四周的方块将变为空气。这种力量非常具有破坏性,所以使用时要小心。为了安全起见,程序会询问用户是否确定要销毁这些方块,只有在用户回答“是”时才会执行销毁操作。

列表 6-1 通过删除玩家上方、下方以及四周的所有方块,创建一个坑洞。然后它在聊天中发布"Boom!"。将此程序保存为crater.py,并放在一个名为ifStatements的新文件夹中。

crater.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   answer = input("Create a crater? Y/N ")

➊ # Add an if statement here

   pos = mc.player.getPos()
➋ mc.setBlocks(pos.x + 1, pos.y + 1, pos.z + 1, pos.x - 1, pos.y - 1, pos.z - 1, 0)
   mc.postToChat("Boom!")

列表 6-1:无论用户输入什么,这段代码都会创建一个坑洞。

answer变量使用input()函数询问用户是否想要创建一个坑洞。然而,此时无论用户输入什么——YN、其他内容或什么也不输入——代码都会创建一个坑洞。

要完成这个程序,你需要添加一个if语句,检查用户是否输入了Y来回应问题。你可以将这个逻辑添加到游戏中的➊处。记住,用户的回答存储在answer变量中,因此你的if语句应该检查answer变量。在你添加完if语句后,程序只有在玩家输入Y时才会运行最后三行代码。为了做到这一点,你必须将这三行代码缩进四个空格。

请记住,setBlocks()函数的最后一个参数应该是你要设置的方块类型。这里,最后一个参数是0,表示空气方块类型。换句话说,陨石坑是通过setBlocks()将方块设置为空气➋来创建的,这样看起来玩家周围的所有方块都被摧毁了。通过对pos.xpos.ypos.z的值加减 1,代码在玩家的位置周围放置了一个 3x3 的空气方块立方体。这就是陨石坑。

在对程序进行修改后,保存并运行。Create a Crater? Y/N这个问题会出现在 Python 命令行中。输入YN。确保输入的是大写字母 Y,否则程序将无法正常运行。

当用户输入Y时,一个陨石坑将出现,正如图 6-1 所示。

image

图 6-1:砰!我周围出现了一个陨石坑。

附加目标:建造一个房子

你还能让这个程序做什么呢?试试修改程序,让它围绕玩家建造一个房子,而不是创建一个陨石坑。

ELSE 语句

现在我们将介绍一个更高级的语句,如果我们想在if条件为False时运行不同的代码,可以使用else语句。

else语句与if语句一起工作。首先,你写一个if语句,在条件为True时执行一些代码。接着,写一个else语句,在条件为False时执行其他代码。就像你在说:“如果条件为真,做这个。否则,做别的事情。”

以下程序将在房间里有超过 20 个僵尸时打印"啊啊啊!僵尸!";否则,将打印"你们这些僵尸还真不那么坏。"

zombies = int(input("Enter the number of zombies: "))
if zombies > 20:
    print("Ahhhh! Zombies!")
else:
    print("You know, you zombies aren't so bad.")

if语句一样,else语句使用冒号和缩进来表示哪些代码属于else语句的主体。但是else语句不能单独使用;它必须紧跟在if语句后面。else语句没有自己的条件;只有当if语句的条件(本例中是zombies > 20)为False时,else语句的主体才会执行。

回到之前的密码示例,我们可以添加一个else语句,当密码不正确时打印一条信息,代码如下:

password = "cats"
attempt = input("Please enter the password: ")
if attempt == password:
    print("Password is correct.")
else:
    print("Password is incorrect.")

attempt的值与password的值匹配时,条件为True。程序会执行打印"密码正确"的代码。

attemptpassword不匹配时,条件为False。程序会执行打印"密码不正确"的代码。

如果没有if语句,仅使用else语句会怎样?例如,如果程序只有这两行:

else:
    print("Nothing happened.")

否则,Python 将无法理解发生了什么,并且会报错。

任务 #27: 防止撞击,或者不防

在任务#17(第 82 页)中,你写了一个程序,通过使用mc.setting("world_immutable", True)让世界变为不可变,阻止玩家破坏区块。这个程序帮助你保护了珍贵的创作免受事故或破坏者的侵害。但尽管它很有用,程序却不够灵活。关闭它需要另写一个程序,这样挺不方便的!

使用if语句、else语句和控制台输入,你可以制作一个程序来开关不可变设置。程序会询问你是否希望区块不可变,然后根据你的回答将不可变设置为TrueFalse

打开 IDLE 并创建一个新文件。将文件保存为immutableChoice.py,并保存在ifStatements文件夹中。按照以下指示完成程序:

  1. 程序需要询问用户是否希望将区块设为不可变:

    "Do you want blocks to be immutable? Y/N"
    

    将这个字符串作为input()的参数,并将输入存储在名为answer的变量中。

  2. 程序检查answer变量中存储的值是否为"Y"。如果是,它将运行以下代码:

    mc.setting("world_immutable", True)
    mc.postToChat("World is immutable")
    

    将这段代码复制并放入if语句中,这样它只会在answer变量的值等于"Y"时运行。不要忘记缩进!

  3. 如果answer变量的值不是"Y",程序将运行以下代码。

    mc.setting("world_immutable", False)
    mc.postToChat("World is mutable")
    

    将这段代码复制并放入一个缩进的else语句中。

保存并运行程序。当程序询问是否要将区块设为不可变时,输入YN并按回车键。测试程序。当你选择将区块设为不可变时,它们不应被破坏;否则,它们应该是可以被破坏的。

图 6-2 显示了终端中的输出信息和问题。

如果你输入"N",你将得到与输入无意义的内容(比如"banana")时相同的结果。你认为为什么会发生这种情况?

image

图 6-2:我可以选择将世界设为不可变,现在我无法破坏任何区块。

附加目标:更好的界面

我们可以通过使用布尔运算符,让程序接受"Yes""No"的不同变体,比如小写的"yes"、大写的"YES",以及单个字符的回答"y",使得程序更友好。试试看吧!

ELIF 语句

使用if语句和else语句时,如果条件为True,程序可以运行一段代码;如果条件为False,则运行另一段代码。但如果你想运行多于两段代码怎么办?

为了实现这一点,你可以使用else-if语句,或在 Python 中使用elif。首先写一个if语句,然后写一个elif语句,最后写一个else语句。当你将这些语句一起使用时,你是在说:“如果某个条件为True,就运行这段代码。否则,如果第二个不同的条件为True,就运行另一段代码。最后,如果这两个条件都不为True,就运行另一段代码。”

让我们看看它。假设你正在决定在冰淇淋店买什么口味的冰淇淋。你可能会说:“如果还有巧克力冰淇淋,我就买那个。如果没有巧克力,但有草莓,我就买草莓。如果既没有巧克力也没有草莓,我就买香草。”

在一个程序中,这个决策过程看起来是这样的:

hasChocolate = False
hasStrawberry = True
if hasChocolate:
    print("Hooray! I'm getting chocolate.")
elif hasStrawberry:
    print("I'm getting the second best flavor, strawberry.")
else:
    print("Vanilla is OK too, I guess.")

前两行只是为场景设定基础:我们假设今天冰淇淋店没有剩下巧克力冰淇淋,但有草莓口味。所以我们将hasChocolate设置为FalsehasStrawberry设置为True

接下来是决策过程的逻辑:如果hasChocolateTrueif语句会打印出"Hooray! I'm getting chocolate."。但是在这个例子中,它是False,所以这个信息不会被打印。相反,程序会进入elif语句,测试hasStrawberry是否为True。因为它为True,所以elif语句中的代码会执行,并打印出"I'm getting the second best flavor, strawberry."

如你所见,这个elif语句有它自己的条件和代码块。只有在if语句的条件为False并且elif语句的条件为True时,elif语句才会执行。

最后,elif语句之后的else语句会在if语句的条件为Falseelif语句的条件也为False时执行。在这个例子中,如果hasChocolatehasStrawberry都为False,则else语句的代码会执行,打印出"Vanilla is OK too, I guess."

另一个例子,我们可以回到那个在房间里有超过 20 只僵尸时会打印出"Ahhhh! Zombies!"的程序。我们可以添加一个elif语句,在if语句的条件为False时测试另一个条件:

zombies = int(input("Enter the number of zombies: "))
if zombies > 20:
    print("Ahhhh! Zombies!")
elif zombies == 0:
    print("No zombies here! Phew!")
else:
    print("You know, you zombies aren't so bad.")

我们添加一个elif语句来比较zombies和 0。如果zombies == 0True,则elif语句中的代码会打印出"No zombies here! Phew!"。如果这个elif语句的条件为False,代码会进入else语句并打印出"You know, you zombies aren't so bad."

任务 #28:提供礼物

让我们创建一个程序,检查某个方块上是否放置了礼物,并根据礼物的不同输出不同的聊天回应。

这个程序允许你放置两种不同的礼物之一。一个礼物是钻石块,因为并不是每个人都有那么多钻石块,另一个是树苗。

列表 6-2 检查位置为 10, 11, 12 的方块是否是钻石块或树苗,或者没有礼物。但是,程序并不完整。

gift.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
   x = 10
   y = 11
   z = 12
   gift = mc.getBlock(x, y, z)

   # if gift is a diamond block
➊ if

   # else if gift is a sapling
➋ elif

   else:
       mc.postToChat("Bring a gift to " + str(x) + ", " + str(y) + ", " + str(z))

列表 6-2:检查是否已经送出礼物的代码开始部分

在 IDLE 中创建一个新文件,并将其保存在 ifStatements 文件夹中的 gift.py。将 清单 6-2 复制到文件中。获取方块类型的代码已经为你写好。方块类型保存在 gift 变量中。如果没有放置钻石块或树苗,else 语句将会运行,并向聊天发送一条消息,提示玩家将礼物带到这些坐标。你可以将 xyz 变量中的坐标更改为任何你喜欢的位置。

完成程序,请按照以下步骤操作:

  1. 完成 ➊ 位置的 if 语句,检查 gift 变量是否包含钻石块(57)的值。如果是,发送以下消息到聊天:“感谢你的钻石。”

  2. 在第二个注释下的 ➋ 位置添加一个 elif 语句,检查 gift 变量是否包含树苗(6)的值。如果包含,发送以下消息到聊天:“我猜树苗和钻石一样好...”

在做出更改后,保存并运行程序。试着在坐标处放置一个钻石块,看看会发生什么。也可以试试放一个树苗,或者什么都不放在这些坐标上。别忘了树苗需要种植在泥土块或草块上!每种情况你都能得到正确的响应吗?你每次都需要重新运行程序来检查它是否正常工作。图 6-3 显示了我的工作程序。

image

图 6-3:我把树苗作为礼物放置了。

奖励目标:赞美西瓜神

在这个任务中,你可以使用许多不同的方块。试着修改代码,检查你是否放置了一个金块或一个西瓜作为礼物。再试着写一个代码,放置礼物方块后将其销毁。

链式连接 elif 语句

elif 语句的数量没有限制,你可以在一个 if 语句中包含一个或 100 个 elif 语句。Python 会逐一评估它们。

这是使用“僵尸数量”程序的一个例子:

   zombies = int(input("Enter the number of zombies: "))
   if zombies > 20:
       print("Ahhhh! Zombies!")
➊ elif zombies > 10:
       print("There's just half a Minecraft zombie apocalypse.")
   elif zombies == 0:
       print("No zombies here! Phew!")
   else:
       print("You know, you zombies aren't so bad.")

在 ➊ 位置,我们在 if 语句后添加了一个新的 elif 语句,检查房间里是否有超过 10 个僵尸。如果有,打印出“这就像半个 Minecraft 僵尸末日”;否则,代码继续检查下一个 elif

ifelif 语句的顺序非常重要。如果它们的顺序错误,某些代码可能永远不会被执行,程序将无法按预期运行。

例如,如果我们将 if 语句的条件与第一个 elif 语句的条件交换,我们会遇到问题:

zombies = int(input("Enter the number of zombies: "))
if zombies > 10:
    print("There's just half a Minecraft zombie apocalypse.")
elif zombies > 20:
    print("Ahhhh! Zombies!")
elif zombies == 0:
    print("No zombies here! Phew!")
else:
    print("You know, you zombies aren't so bad.")

为什么这是错误的?让我们看看当 zombies 是 22 时发生了什么。因为 22 大于 10,第一个 if 语句的条件 zombies > 10True,所以 if 语句的代码会执行。一旦执行了这个,其他的 elifelse 语句就不会执行了。程序永远不会到达 elif zombies > 20,因为它已经执行了 if 语句的主体。这是一个错误。

如果你从 if 语句中得到意外的结果,始终检查你的 ifelif 语句是否顺序正确。

任务 #29:传送到正确的位置

ifelif 语句顺序错误时,预期执行的代码不会执行,而不该执行的代码却会执行。这可能会导致程序出现奇怪的错误。要修复程序,你需要将条件按正确的顺序排列。我们来试试看。

代码清单 6-3 不会工作。它应该根据用户输入的分数将玩家传送到不同的位置。分数与正确的位置相匹配,但条件似乎没有按正确的顺序排列。

玩家分数越高,位置越好。这里是代码。条件是通过 setPos() 为每个位置传送设置的。

teleportScore.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

points = int(input("Enter your points: "))
if points > 2:
    mc.player.setPos(112, 10, 112)
elif points <= 2:
    mc.player.setPos(0, 12, 20)
elif points > 4:
    mc.player.setPos(60, 20, 32)
elif points > 6:
    mc.player.setPos(32, 18, -38)
else:
    mc.postToChat("I don't know what to do with that information.")

代码清单 6-3:根据你的分数,你将传送到不同的地点。

这里有一个单独的条件,用于检查超过 6 分、超过 4 分、超过 2 分以及 2 分或以下的情况。

最后一行,在 else 语句内,除非用户输入一些完全异常的东西,比如输入文本字符串而不是他们的分数,或者根本不输入任何内容,否则是不会执行的。

在 IDLE 中创建一个新文件,并将其保存为 teleportScore.py,保存在 ifStatements 文件夹中。修改程序,使条件顺序正确,所有位置都能到达。使用不同的分数测试程序,确保每个传送目标的代码都能正常运行。图 6-4 显示了程序无法正常工作。

image

图 6-4:我没想到会来到这里!

由于程序当前无法正常工作,当我输入 5 时,它会将我传送到超过 2 分的位置,尽管我应该被传送到超过 4 分的位置。

附加目标:传送我,斯科特!

创建一个程序,允许你输入一个要传送到的地点作为字符串,比如 "castle"。使用 if 语句和 elif 语句选择你要传送到的位置。例如,"sea fortress" 会将你传送到一个地点,而 "tree house" 会将你传送到另一个地点。

嵌套的 IF 语句

假设你有一个 if 语句,如果它的条件为 True,你想要测试另一个条件(并在第二个条件为 True 时运行一些代码)。例如,如果你想让家园基地的入口更加秘密,你可能会编写一些代码,检查你是否站在一个开关上。如果这是真的,另一行代码会检查你是否拿着可以解锁门的秘密物品。你会怎么做呢?

你可以将一个 if 语句放在另一个 if 语句的主体内。这被称为 嵌套 if 语句。

清单 6-4 是一个嵌套的 if 语句示例。一个简单的现金机检查你是否有足够的钱,如果有,它会要求你确认取款。如果你确认,程序会进行取款。

   withdraw = int(input("How much do you want to withdraw? "))
   balance = 1000

➊ if balance >= withdraw:
       confirm = input("Are you sure? ")
➋     if confirm == "Yes":
           print("Here is your money.")
   else:
       print("Sorry, you don't have enough money.")

清单 6-4:一个用 Python 编写的假想现金机

注意,第二个 if 语句是缩进在第一个 if 语句内部的。如果外层 if 语句的条件 ➊ 为 True,说明你的账户里有足够的钱,接着 confirm = input("你确定吗?") 这一行就会运行。然后,如果内层 if 语句的条件 ➋ 为 True,代码会输出 "这是你的钱。"

任务 #30:打开一个秘密通道

在这个任务中,你将稍微扩展之前的示例。你将创建一个带有秘密通道的建筑,秘密通道只有在放置一个钻石块在基座上时才会打开。当基座上放置任何其他类型的方块时,地板将变成岩浆!

首先,建造一个建筑物。为了快速完成这项任务,找到 math 文件夹中的 building.py 程序(第 56 页)并运行它。不要在建筑物上添加门。外面,你想要编写建筑入口的地方,放置一个方块代表基座。当你把一个钻石块放到基座上时,代码会在建筑的侧面打开一个秘密入口。清单 6-5 提供了一些框架代码,你可以用来开始。

secretDoor.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

x = 10
y = 11
z = 12
gift = mc.getBlock(x, y, z)
if gift != 0:
    # Add your code here
else:
    mc.postToChat("Place an offering on the pedestal.")

清单 6-5:当你在基座上放置礼物时,打开秘密门的代码开始部分

在 IDLE 中创建一个新文件,并将其保存为 secretDoor.py,并将其放在 ifStatements 文件夹中。修改程序中的坐标,以匹配你在 Minecraft 世界中需要放置钻石块钥匙的位置。

复制 清单 6-5 并为以下任务添加代码:

• 如果基座上是钻石块(57),打开通往秘密房间的秘密通道。(提示:要在建筑物中创建一个开口,可以尝试将方块设置为空气。)

• 当基座上放置的方块不是钻石块时,让玩家脚下的地板变成岩浆(10)。

你需要使用嵌套的 if 语句来完成这个程序。

因为这是一个更复杂的程序,你应该分阶段进行构建和测试。当你添加了一个新特性时,运行程序并确保该部分功能正常后再继续。调试小块代码比修复冗长代码更容易。图 6-5 展示了秘密通道的开启。

image

图 6-5:通往神庙的秘密通道现在已开启。

附加目标:自动扶梯

通过修改secretDoor.py程序,你还可以做些什么呢?你能做一个自动门,它可以检测玩家是否站在门旁边,或者做一个自动扶梯,当玩家站在楼梯底部时自动将他们推上楼梯吗?

使用 IF 语句测试值的范围

正如你在第五章中学到的,你可以在 Python 中判断一个值是否位于另两个值之间。因为范围检查的结果为TrueFalse,你可以像使用简单的大小比较一样,将范围检查作为if语句的条件。任何计算结果为TrueFalse的表达式都可以作为if语句的条件。

假设你花了一整天时间收集材料来烤一些美味的 Minecraft 蛋糕。你找到了足够的材料来烤 30 个蛋糕,现在你想卖掉这些蛋糕。购买蛋糕的人必须购买 1 个蛋糕但少于 30 个,否则你不会卖蛋糕给他。他们不能霸占所有蛋糕!

这段代码表示了蛋糕的情况:

   cakes = int(input("Enter the number of cakes to buy: "))
➊ if 0 < cakes < 30:
       print("Here are your " + str(cakes) + " cakes.")
➋ elif cakes == 0:
       print("Don't you want some delicious cake?")
➌ else:
       print("That's too many cakes! Don't be selfish!")

如果cakes变量的值在 0 和 30 之间,比如 15,我们打印"Here are your 15 cakes." ➊。否则,我们打印一条不同的信息。如果cakes的值是 0,我们打印"Don't you want some delicious cake?" ➋;如果大于 30,我们打印"That's too many cakes! Don't be selfish!" ➌。

我们可以通过添加布尔运算符来测试一个更复杂的表达式。如果我真是个奇怪的人,不想让人们购买 20 到 30 个面包,我可以使用not运算符来实现:

bread = int(input("Enter the amount of bread: "))
if not 20 <= bread <= 30:
    print("Here are your " + bread + " breads.")
else:
    print("I don't sell that amount of bread for some reason.")

在这里,我使用了not运算符和大于或等于的比较来测试值的范围作为第一个条件。范围检查确定人们想买的面包数量是否在 20 到 30 之间。然后,notTrue变为False,将False变为True。所以,如果bread在范围内,整个表达式的值为False,我们执行else语句中的代码。如果bread不在 20 到 30 的范围内——比如它是 40——那么整个表达式为True,我们打印"Here are your 40 breads."

如果有人试图购买 23 个面包,我是不会允许的。但 17 个或 32 个就没问题。

任务 #31:限制传送位置

记得你在第二章创建的传送程序吗?它叫做teleport.py。在这个任务中,你将使用范围检查和if语句来限制玩家可以传送到的地方。如果你在 Raspberry Pi 上使用 Minecraft,游戏世界之外有些地方是不存在的,但你的程序仍然允许你传送到这些地方。如果你在桌面版 Minecraft 上玩,世界会更大,所以你不会像在 Pi 版游戏中那样受到同样的限制,但这个程序仍然很有用。例如,你可以在捉迷藏游戏中使用它来限制玩家可以藏身的区域。

列表 6-6 本应从用户的输入中获取 x、y 和 z 坐标,并将玩家传送到该位置。但程序还没有完成。

teleportLimit.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()
valid = True

x = int(input("Enter x: "))
y = int(input("Enter y: "))
z = int(input("Enter z: "))

if not -127 < x < 127:
    valid = False

# check if y is not between -63 and 63

# check if z is not between -127 and 127

if valid:
    mc.player.setPos(x, y, z)
else:
    mc.postToChat("Please enter a valid location")

列表 6-6:限制玩家可以传送到的地点的程序

为了限制玩家可以传送到的地点,我们创建了一个叫做valid的变量。这个变量将存储一个TrueFalse,表示目标地点的坐标是否有效。我们要求用户输入xyz的值。然后,我们使用if语句检查x变量是否不在–127 到 127 的范围内。如果不在这个范围内,说明该 x 坐标无效,valid变量被设置为False

当程序执行到最后一个if语句时,只有在validTrue时才会调用setPos()。而valid只有在三个条件都满足时才为True。否则,玩家无法传送,我们会发布一条聊天消息,告诉用户输入一个有效的地点。

在 IDLE 中创建一个新文件并将列表 6-6 复制到其中。将程序保存为teleportLimit.py,并放在ifStatements文件夹中。

完成程序,使其使用if语句和范围检查yz变量,如果值无效,则将valid设置为False

当你认为程序完成时,运行它。当你输入的xz变量在–127 到 127 的范围内,y变量在–63 到 63 的范围内时,程序应该传送你。当你输入一个不在这些范围内的值时,程序不应该传送你。图 6-6 展示了当用户输入无效数字时,游戏应该是什么样子。

image

图 6-6: z 变量太大了,所以我没有进行传送。

附加目标:保持在地面以上

传送程序的一个问题是,它可能会把你传送到地下,将你困在那里。你可以修改程序,防止玩家被传送到地下。比较用户输入的 y 坐标与getHeight()函数,检查玩家是否会被传送到地下,如果会,就阻止传送。

布尔运算符和 if 语句

在上一个任务中,你使用了not操作符在if语句中。你还可以使用andor。在这种情况下,if语句的行为和只有一个简单条件时一样:如果整体表达式为True,则执行语句体。这里有一个程序,询问某人是否有蛋糕,以及他们是否愿意给我们一些蛋糕:

hasCake = input("Do you have any cake? Y/N")
wouldShare = input("Would you give me some cake? Y/N")

if hasCake == "Y" and wouldShare == "Y":
    print("Yay!")
else:
    print("Boo!")

这段代码使用了and操作符,因此只有当这个人有蛋糕(hasCake == "Y"True)并且愿意分享(wouldShare == "Y"True)时,Python 才会打印"Yay!"。如果其中任何一个比较结果不为Trueelse语句中的代码会打印"Boo!"

你可以将and替换为or操作符,这样如果这个人有蛋糕或者愿意分享蛋糕,Python 就会打印"Yay!"

hasCake = input("Do you have any cake? Y/N")
wouldShare = input("Would you give me some cake? Y/N")

if hasCake == "Y" or wouldShare == "Y":
    print("Yay!")
else:
    print("Boo!")

如果hasCake == "Y"wouldShare == "Y"True,整个表达式的结果为True,然后我们会打印"Yay!"。只有当这两个条件都为False时,我们才会打印"Boo!":即这个人没有蛋糕,且即使有也不愿分享。

让我们试着在if语句中使用not操作符:

wearingShoes = input("Are you wearing shoes? Y/N")
if not wearingShoes == "Y":
    print("You're not wearing shoes.")

该程序要求用户输入Y表示他们穿着鞋子,输入N表示没有穿鞋子。程序将输入存储在wearingShoes变量中。接下来会将wearingShoes"Y"进行比较,判断它们是否相等。not操作符会反转比较结果——True变成FalseFalse变成True——因此,如果用户输入Y,比较结果会是True,而not会将其变为False,使得整个表达式为False。我们不会打印任何消息。如果用户没有输入Y,比较结果为Falsenot会将其变为True,整体表达式评估为True,然后我们会打印"你没有穿鞋子。"

任务 #32:洗个澡

最好的 Minecraft 房屋非常注重细节。许多人会在房屋里加入木质地板、壁炉和画作,以让房子更有家的感觉。你将更进一步,制作一个可以使用的淋浴。

要让淋浴工作,你需要使用范围检查和布尔操作符。你将创建一个淋浴区,当玩家走进淋浴区时,水流会开启。换句话说,当玩家走入特定坐标范围时,程序应该在玩家上方生成水块。

列表 6-7 提供了程序的基本结构,包含一些代码行来帮助你入门。剩下的部分需要你自己完成。

shower.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

➊ shwrX =
   shwrY =
   shwrZ =

➋ width = 5
   height = 5
   length = 5

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z

➌ if shwrX <= x < shwrX + width and
       mc.setBlocks(shwrX, shwrY + height, shwrZ,
                    shwrX + width, shwrY + height, shwrZ + length, 8)
   else:
       mc.setBlocks(shwrX, shwrY + height, shwrZ,
➍                  shwrX + width, shwrY + height, shwrZ + length, 0)

列表 6-7:淋浴程序的开始部分

复制列表 6-7 并将其保存为shower.py,放入ifStatements文件夹中。

要完成程序,首先将淋浴的坐标添加到shwrXshwrYshwrZ变量中 ➊。接下来,将淋浴的尺寸添加到widthheightlength变量中 ➋。我已经设置了默认值为 5,但你应该根据需要调整,使淋浴的尺寸符合你的要求。

完成if语句,以检查yz变量是否在淋浴区域内 ➌。我已经为x位置包含了检查帮助你完成(shwrX < x < shwrX + width)。yz位置的表达式与此类似。提示:你需要用and将所有这些检查结合起来。

淋浴的开关是通过setBlocks()函数来控制的 ➍。当将方块设置为水(方块 ID 8)时,淋浴开启;将方块设置为空气(方块 ID 0)时,淋浴关闭。

最后一个if/else语句中的setBlocks()函数被分成两行,因为它们的参数很长。Python 允许你这样做。它们本来可以写在一行上,我把它们分成两行只是为了让它们更易于阅读。

图 6-7 展示了我的淋浴工作情况。

image

图 6-7:这是我正在洗澡。

当你运行程序时,如果站在淋浴下方,程序会在你上方生成水流。水流不会停止,直到你离开淋浴并重新运行程序。玩得开心!

额外目标:节水

添加一个时间限制,在设定时间后自动关闭淋浴。

你学到了什么

你的程序现在可以做决策了。在这一章中,你学会了如何使用if语句、else语句和elif语句进行条件判断。在第七章,你将学习while 循环。像if语句一样,while循环帮助你的程序决定做什么以及何时做。但是与ifelse语句不同——你使用它们来运行一些代码,如果条件为真则运行,如果条件不为真则运行其他代码——while循环会在条件为真时运行代码,并不断重复运行,直到条件变为假。

第七章:7

使用while循环的舞会派对和花车游行

image

循环使得重复执行代码变得更加容易。你无需复制粘贴相同的代码,而是可以使用循环按需重复代码。在本章中,你将使用循环来使程序重复执行,而无需重新运行它们。我们将重点讨论一种名为while循环的 Python 循环。

一个简单的while循环

你使用while循环来重复代码块。类似于if语句,while循环只要条件为True,就会执行其中的代码。也就是说,必须满足某个条件,语句的主体才会执行。

while循环和if语句之间的区别在于,if语句中的代码最多只会执行一次,而while循环中的代码可以重复执行多次。程序员称代码的重复执行为迭代。当一个循环重复时,我们说它迭代

例如,这段代码使用while循环打印数字 1 到 5:

count = 1
while count <= 5:
    print(count)
    count += 1
print("Loop finished")

count变量记录循环已执行的次数。它从 1 开始。while循环中的条件检查count是否小于或等于 5。

注意

在第三章中,你学到了+=是一个简写运算符。你也可以使用标准的加法运算符 count = count + 1 来做同样的事情。

循环第一次运行时,count的值为1,小于 5。循环的条件为True,因此循环体执行。接下来,程序将count的值打印到 Python shell 中,然后将 1 加到count的值上。while循环现在重新开始,重新检查条件,逐步执行直到count变量大于 5。

循环外有最后一行,它会打印"循环结束"

保存这个程序并运行,你应该看到以下输出:

1
2
3
4
5
Loop finished

尝试对代码做一些实验。修改条件,使其列出超过 5 个数字,或者更改count变量增加的数量。以下是代码如何工作的回顾。while语句遵循以下步骤:

  1. 检查条件是否为True

  2. 如果条件为True

    a. 执行代码体。

    b. 重复步骤 1。

  3. 如果条件为False

    a. 忽略代码体。

  4. 继续到while循环块之后的行。

让我们尝试在 Minecraft 中使用while循环传送到许多新地点!

任务 #33:随机传送之旅

在任务 #3(第 40 页)中,你将玩家传送到了游戏中的不同位置。让我们使用while循环重写那个程序,这样你就可以一遍又一遍地重复传送。

通过循环一些会将玩家传送到随机位置的代码,你可以使程序更强大且更易于阅读。酷吧?

以下代码通过为xyz变量在游戏世界中选取随机值,将玩家传送到一个随机位置。然后,它将使用这些变量设置玩家的位置。

   import random
   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

➊ # Add the count variable here
➋ # Start the while loop here
➌ x = random.randint(-127, 127)  # Indent the code from this line
   y = random.randint(0, 64)
   z = random.randint(-127, 127)

   mc.player.setTilePos(x, y, z)
➍ # Add 1 to the value of the count variable here

然而,现在代码只会传送玩家一次。虽然这已经挺酷了,但你可以让它变得更棒。让我们写一个循环,让代码重复五次,这样就变成了一个快速的世界之旅。

要将代码改成使用循环,按照以下四个步骤操作:

  1. 创建一个count变量来控制循环 ➊。

  2. 添加一个基于count的条件的while循环 ➋。

  3. 缩进while语句的代码体 ➌。

  4. 每次循环时增大count的值 ➍。

count变量和count增量的目的是跟踪循环重复的次数。接下来我会详细讲解它们。现在你需要知道的是,count让我们控制代码重复的次数。

示例 7-1 显示了添加更改后的代码。

randomTeleport.py

import random
from mcpi.minecraft import Minecraft
mc = Minecraft.create()

count = 0
while count < 5:
    x = random.randint(-127, 127)
    y = random.randint(0, 64)
    z = random.randint(-127, 127)
    mc.player.setTilePos(x, y, z)
    count += 1

示例 7-1:让玩家在游戏世界中随机传送的代码

将示例 7-1 复制到一个新文件中,保存为randomTeleport.py,并放入名为whileLoops的文件夹内,然后运行代码。你应该能看到玩家在 Minecraft 世界中快速移动。但代码运行得太快了!整个过程不到一秒钟就结束了。让我们一起来解决这个问题。

你将使用time模块来减慢代码的执行速度。按照以下步骤操作:

  1. 在程序的第一行添加语句import time。这将导入 Python 的time模块,里面包含一组与时间相关的有用函数等。

  2. while循环的代码体末尾添加time.sleep(10)这一行,给程序增加 10 秒的延迟。确保你缩进这行新代码,使其处于while循环内部!

保存程序并运行。现在,玩家应该每 10 秒钟传送到一个新的随机位置。图 7-1 显示了我运行程序的情况。

image

图 7-1:每 10 秒钟,程序将我传送到一个新位置。

附加目标:好好休息

目前,程序会在每次循环结束时等待 10 秒钟。如果你把time.sleep(10)语句移到循环开始处会发生什么呢?

使用计数变量控制循环

计数变量是存储程序重复次数的常见方式。你已经在之前的几个例子中看到过这些变量的使用。我们再来看一个例子:

count = 0
while count < 5:
    print(count)
    count += 1

while循环的条件测试count变量的值是否小于 5。在循环体内,我改变了count变量的值,以记录循环重复的次数。增加count变量的值叫做增量

这段代码的最后一行将 count 变量的值增加 1。每次代码重复时,它会检查 count 变量的新值,看它是否小于 5。当它等于或大于 5 时,循环将停止。

如果你忘记增加 count 变量的值,你会陷入一个无限循环,这个循环将永远重复下去,正如下面的例子所示:

count = 0
while count < 5:
    print(count)

count 的值始终为 0,因为它从未被增加。因此,循环的条件始终为 True,循环将会永远重复。如果你不相信我,可以试着运行这段代码!

0
0
0
0
0
--snip--

要中断这个无限程序的执行,按下 CTRL-C。要修正代码,只需在循环体内添加一行 count += 1。现在你就不会被困在无限循环中了。呼!

计数并不总是必须每次增加 1。在某些情况下,你可能希望计数以不同的值增加。在下面的例子中,计数每次增加 2;结果是,代码打印出 0 到 100 之间的所有偶数:

count = 0
while count < 100:
    print(count)
    count += 2

你也可以通过使用负数来倒数,减少 计数的值。以下代码会从 100 倒数到 1:

count = 100
while count > 0:
    print(count)
    count -= 1

这个例子与之前的例子唯一的区别是条件。在这里我使用了大于比较符号(>)。只要 count 大于 0,循环就会继续;当 count 等于 0 时,循环停止。

注意

用来控制循环的变量不一定总是叫做 count你可以将它命名为 repeats 或任何你想要的名称。如果你查看其他人的代码,你会看到各种各样的不同命名。

任务 #34: 水之诅咒

让我们尝试一些有点恶作剧的操作,给玩家写一个持续时间非常短的诅咒。在视频游戏中,诅咒可能会以某种方式削弱角色,比如让他们变慢或者变弱,通常持续时间较短。

我们将创建一个诅咒程序,每秒在玩家的位置放置一个流动水块,持续 30 秒。这将使玩家在不被水流推开的情况下很难移动。

以下代码会在玩家的位置放置一个流动水块:

waterCurse.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

pos = mc.player.getPos()
mc.setBlock(pos.x, pos.y, pos.z, 8)

这段代码只会在玩家当前位置放置一个水块。你的任务是让它重复。最终的代码应该重复 30 次,每次循环持续 1 秒。

将这段代码保存为 waterCurse.py 文件,并放在 whileLoops 文件夹中,然后运行一次以确保它能正常工作。你应该会看到在程序停止之前,玩家的位置出现一个水块。

让我们讨论一下接下来需要添加什么,以让这个诅咒持续下去。使用你学到的 while 循环和 count 变量来完成以下任务:

  1. 在程序中添加一个 count 变量。

  2. 在程序中添加一个循环,重复执行最后两行代码。循环应该重复 30 次。

  3. 在循环结束时增加 count 变量。

  4. 导入time模块(在程序的第一行),然后在while循环的最后一行添加 1 秒的休眠。

保存程序并进行测试。当你在游戏世界中走动时,程序应该每秒创建一个水块,持续 30 秒。如果遇到困难,可以参考任务 #33(第 125 页)中的步骤获取帮助。

图 7-2 显示了这个诅咒的实际效果。

image

图 7-2:哦不!我被一小股洪水追着跑。

额外目标:更快的洪水

如何让循环以原来的两倍速度(每半秒一次)重复,同时仍然持续 30 秒?

无限循环

在大多数情况下,while循环中的布尔条件最终必须变为False;否则,循环将永远执行下去,可能会导致计算机崩溃。

但有时你可能想编写一个无限循环。例如,视频游戏通常使用无限循环来检查用户输入并管理玩家的移动。当然,这些视频游戏会包含一个退出按钮,这样你就可以在需要休息时暂停或停止无限循环!

创建无限循环的一个简单方法是,当你定义while循环时使用True条件,如下所示:

while True:
    print("Hello")

这段代码将会永远重复,反复打印字符串"Hello"。无论你是否打算创建一个无限循环,按 CTRL-C 可以在 Python shell 中常见地停止它。在 IDLE 中,你也可以选择ShellRestart Shell来停止循环。

注意,任何位于无限while循环之后的代码都永远不会执行。在以下示例中,由于前面的无限while循环,最后一行代码无法执行:

while True:
    print("Hello")
print("This line is never reached")

尽管无限循环有时可能会有点棘手,但你也可以利用它来做很多有趣的事情。接下来我们就来尝试一下!

任务 #35:花朵轨迹

你在这个任务中编写的程序类似于任务 #34 中的程序,但不是放置水块,而是让玩家身后留下花朵轨迹。花朵比洪水要漂亮多了!

打开waterCurse.py文件(位于whileLoops文件夹中),然后将其保存为flowerTrail.py

为了让玩家在游戏中走动时,花朵能不断出现并形成一条无限轨迹,请对程序进行如下修改:

  1. while循环的条件改为True

  2. 删除count变量和增量。

  3. setBlock()函数中的块类型参数从8改为38

  4. sleep()函数中的参数值改为0.2,让每秒出现五朵花。

  5. 保存程序并运行。图 7-3 显示了你应该看到的效果。

image

图 7-3:看,所有美丽的花朵!

额外目标:一条毁灭的轨迹

flowerTrail.py程序非常灵活。试着改变程序放置的方块类型。一个有趣的方块类型是爆炸性 TNT(setBlock(x, y, z, 46, 1))。注意在46之后的额外参数1,它是 TNT 方块类型。1将 TNT 的状态设置为只要碰到它就会引爆,而不需要打火石和火柴。当你指向 TNT 时,连续点击鼠标左键几次,就能让它爆炸!

复杂条件

由于while循环期望其条件是一个布尔值,你可以使用到目前为止学到的所有比较符和布尔运算符。例如,你已经看到大于和小于运算符就像在之前的章节中一样使用。

但你也可以通过其他方式使用比较符和布尔运算符来控制while循环。让我们来看看!

我们将从编写一个更互动的条件开始。以下代码在循环开始之前创建了continueAnswer变量,并检查其值是否等于"Y"。请注意,我们不能使用continue作为变量名,因为它是 Python 中的保留字。

continueAnswer = "Y"
coins = 0
while continueAnswer == "Y":
    coins = coins + 1
    continueAnswer = input("Continue? Y/N")
print("You have " + str(coins) + " coins")

while循环的最后一行,程序要求用户输入。如果用户输入除"Y"之外的任何内容,循环将结束。用户可以重复按下 Y、Y、Y,每次coins变量的值都会增加 1。

请注意,正在检查的变量continueAnswer是在循环开始之前创建的。如果没有创建,程序会显示错误。因此,我们用来测试条件的变量必须在使用之前就存在,并且在程序第一次进入while循环时,变量的值必须为True;否则,条件将不成立,while循环的主体语句将永远不会执行。

任务 #36: 潜水比赛

让我们用while循环和相等运算符(==)来玩得开心。在这个任务中,你将创建一个小游戏,让玩家尽可能长时间地潜水。程序会记录他们在水下停留的时间,并在程序结束时显示他们的得分。如果玩家在水下停留超过 6 秒,程序将为他们洒下鲜花,祝贺他们。

下面是一些代码,帮助你入门:

divingContest.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
   import time

   score = 0
   pos = mc.player.getPos()
➊ blockAbove = mc.getBlock(pos.x, pos.y + 2, pos.z)

➋ # Add a while loop here
   time.sleep(1)
   pos = mc.player.getPos()
➌ blockAbove = mc.getBlock(pos.x, pos.y + 2, pos.z)
➍ score = score + 1
   mc.postToChat("Current score: " + str(score))

   mc.postToChat("Final score: " + str(score))

➎ if score > 6:
       finalPos = mc.player.getTilePos()
       mc.setBlocks(finalPos.x - 5, finalPos.y + 10, finalPos.z - 5,
                    finalPos.x + 5, finalPos.y + 10, finalPos.z + 5, 38)

将程序保存为divingContest.py,并放在你的whileLoops文件夹中。score变量用于记录玩家在水下停留的时间(秒数)。

运行代码看看会发生什么。此时程序还未完成:它只会检查玩家是否在水下一次,然后结束。

在你修复此问题之前,让我们看看代码的其余部分。blockAbove 变量存储位于玩家头顶的方块类型 ➊。例如,如果玩家的头在水下,这个变量将存储一个值 8(表示该方块是水)。稍后在代码中,你将重新设置 blockAbove 存储玩家头顶上方方块的值 ➌,这样当你创建 while 循环时,它会将 blockAbove 更新为玩家头顶当前的方块类型。在 ➍,程序会为玩家每在水下待一秒钟,给总分加 1 分;而在 ➎,它会使用 if 语句,如果得分大于 6,就在玩家头顶上方生成花朵雨。

你需要在程序中添加一个循环,使用 blockAbove 变量作为条件,位置在 ➋。使 while 循环检查 blockAbove 是否等于水(方块类型 8)或等于流动水(方块类型 9)。你可以在 while 循环中使用以下条件来检查:while blockAbove == 8 or blockAbove == 9。这会检查玩家当前是否在水下,并在每次循环重复时继续检查玩家是否在水下。

要测试你的程序,找到至少三格深的水并跳进去。程序只会在你已经在水下时运行。当你运行程序时,它应该开始显示你在水下的秒数。过一会儿,游到水面。程序应该显示你的得分,如果你在水下呆了 6 秒或更长时间,还会洒上花朵。图 7-4 显示了玩家在水下且得分已显示。图 7-5 显示了获胜时出现的花朵。

image

图 7-4:我正在水下屏住呼吸,显示的是我在水下待的秒数。

image

图 7-5:我赢得了属于自己的花朵庆祝!

奖励目标:你是赢家

尝试通过在程序结尾的 if 语句中编写更多代码来添加额外的奖励。如果玩家得分很高,你可以给他们一个金块。试着为每个难度级别添加不同的奖励。

布尔运算符与 while 循环

当你希望循环使用多个条件时,可以在 while 循环中使用布尔运算符,如andornot。例如,以下循环将在用户未输入正确密码且尝试次数不超过三次时进行迭代:

   password = "cats"
   passwordInput = input("Please enter the password: ")
   attempts = 0

➊ while password != passwordInput and attempts < 3:
➋     attempts += 1
➌     passwordInput = input("Incorrect. Please enter the password: ")

➍ if password == passwordInput:
       print("Password accepted.")

while 循环条件 ➊ 执行了两项任务:它检查密码是否与用户输入的不同(password != passwordInput),并检查用户是否尝试输入密码不超过三次(attempts < 3)。and 操作符使 while 循环能够同时检查这两个条件。如果条件为 False,循环会增加 attempts 变量 ➋ 并要求用户重新输入密码 ➌。如果用户输入了正确的密码或 attempts 变量大于 3,循环将结束。循环结束后,程序将输出 Password accepted,前提是用户输入了正确的密码 ➍。

检查 while 循环中值的范围

你还可以使用 while 循环检查某个范围内的值。例如,以下代码检查用户输入的值是否在 0 和 10 之间。如果不是,循环将退出。

   position = 0
➊ while 0 <= position <= 10:
       position = int(input("Enter your position 0-10: "))
       print(position)

如果 position 变量大于 10,循环将不会重复 ➊。如果值小于 0 也会如此。这在 Minecraft 中非常有用,当你检查玩家是否处于游戏中的某个特定区域时,正如你在下一个任务中将会看到的。

任务 #37:制作一个舞池

该跳舞时间到了!但在你能展现舞步之前,你需要一个舞池。本任务中的程序会生成一个舞池,并且只要玩家站在舞池上,舞池就会每半秒改变一次颜色。

以下是代码的开头。它在玩家当前的位置创建一个舞池,并使用 if 语句改变颜色。但代码尚未完成。

danceFloor.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
   import time

   pos = mc.player.getTilePos()
   floorX = pos.x – 2
   floorY = pos.y - 1
   floorZ = pos.z – 2
   width = 5
   length = 5
   block = 41
➊ mc.setBlocks(floorX, floorY, floorZ,
                floorX + width, floorY, floorZ + length, block)

➋ while floorX <= pos.x <= floorX + width and # Check z is within the floor
➌     if block == 41:
           block = 57
       else:
           block = 41
       mc.setBlocks(floorX, floorY, floorZ,
                    floorX + width, floorY, floorZ + length, block)
       pos = mc.player.getTilePos()
       time.sleep(0.5)

打开 IDLE,创建一个新文件,并将程序保存为 danceFloor.pywhileLoops 文件夹中。代码根据玩家的当前位置➊构建舞池,并将舞池的位置和大小存储在 floorXfloorYfloorZwidthlength 变量中。在 while 循环内部,代码使用 if 语句交替改变构成舞池的方块 ➌,使得舞池看起来像是在闪烁。

为了让程序正常工作,你需要修改 while 循环的条件,检查玩家的 z 坐标是否在舞池上➋。换句话说,检查 pos.z 是否大于或等于 floorZ 并且小于或等于 floorZlength。为了指导你,看看我是如何通过使用(floorX <= pos.x <= floorX + width)来检查 pos.x 是否在舞池上的。图 7-6 展示了舞池的实际效果!

image

图 7-6:我在舞池上展示我的舞步。

完成程序后,保存并运行它。玩家下方应该会出现一个舞池,并且每半秒钟改变一次。跳一会儿舞——玩得开心!完成后,离开舞池,确保它停止闪烁。除非重新运行程序来创建新的舞池,否则它不会再次开启。

附加目标:派对结束

当玩家在舞池中跳舞完成后,让舞池消失。为此,当循环结束时,将舞池更改为空气。

嵌套 IF 语句与 WHILE 循环

你可以通过在 while 循环中使用 if 语句和嵌套的 if 语句来编写更强大的程序。你可能已经注意到,在 任务 #37 (第 135 页) 的代码中使用了一个嵌套的 if 语句。

在下面的示例中,嵌套的 if 语句检查最后打印的单词,并决定是否打印单词 "mine""craft"。循环重复 50 次。

word = "mine"
count = 0
while count < 50:
    print(word)
    if word == "mine":
        word = "craft"
   else:
        word = "mine"

word 变量存储将要打印的第一个单词。循环中的 if 语句检查当前单词是否为 "mine",如果是,它将把单词更改为 "craft",并在下一个循环迭代中打印该单词。如果单词不是 "mine",它将被更改为 "mine"。这是一个无限循环,所以请确保使用 CTRL-C 来退出!

你还可以在 while 循环中嵌套 elif 语句和其他 while 循环。

以下程序会询问用户是否希望打印出从 1 到 100 万之间的所有数字:

   userAnswer = input("Print the numbers between 1 and 1000000? (yes/no): ")

➊ if userAnswer = "yes":
       count = 1
➋     while count <= 1000000:
           print(count)
           count += 1

if 语句检查用户的输入是否为 yes ➊。如果是,程序将运行嵌套在 if 语句中的循环 ➋。如果输入的是其他内容,程序将不会运行该循环,并将结束。

任务 #38:米达斯的触摸

米达斯是一个传奇中的国王。他所触及的所有东西都会变成黄金。你的任务是编写一个程序,将玩家下面的每一块地板都变成黄金——当然,空气和水除外,否则你会陷入麻烦!请记住,黄金块的值是 41,静水的值是 9,空气的值是 0。

midas.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   air = 0
   water = 9

➊ # Add an infinite while loop here
       pos = mc.player.getTilePos()
       blockBelow = mc.getBlock(pos.x, pos.y - 1, pos.z)

➋     # Add if statement here
           mc.setBlock(pos.x, pos.y - 1, pos.z, 41)

打开 IDLE 并创建一个新文件。将文件保存为 midas.py,并放入 whileLoops 文件夹中。你需要在程序中添加更多内容,以便它能完成你需要的操作。首先,你将添加一个无限的 while 循环 ➊。记住,无限 while 循环的条件总是 True。你还需要添加一个 if 语句,用来检查玩家下面的方块既不等于空气也不等于静水 ➋。玩家下面的方块的值存储在 blockBelow 变量中,空气和水的值分别存储在 airwater 变量中。

当你完成程序后,保存并运行它。玩家应在身后留下黄金的足迹。当你跳入水中或飞上空中时,你下面的方块不应该发生变化。图 7-7 显示了程序运行的效果。

image

图 7-7:我走过的每一块地板都变成了黄金。

要退出无限循环,进入 ShellRestart Shell,在 IDLE shell 中或点击 shell 并按 CTRL-C。

附加目标:我是一个犁耕者

你可以修改midas.py以实现各种功能。你会如何修改它,使其自动将泥土方块转换为耕地?那如果将泥土方块转换为草方块呢?

使用 BREAK 结束 WHILE 循环

使用while循环时,你可以完全控制循环的开始和结束。到目前为止,你只使用条件来结束循环,但你也可以使用break语句。break语句让你的代码立即退出while循环。我们来看看这个概念!

使用break语句的一种方法是将其放入嵌套在循环中的if语句中。这样,当if语句的条件为True时,循环会立即停止。以下代码会不断请求用户输入,直到他们输入"exit"

➊ while True:
➋    userInput = input("Enter a command: ")
➌    if userInput == "exit":
➍        break
      print(userInput)
➎ print("Loop exited")

这是一个无限循环,因为它使用了while True ➊。每次循环重复时,它都会请求用户输入命令 ➋。程序通过if语句检查输入是否为"exit" ➌。如果输入符合条件,break语句会停止循环的重复 ➍,程序会继续执行循环体之后的代码,并在 Python Shell 中打印"Loop exited" ➎。

任务 #39:创建一个带循环的持久聊天

在任务 #13(第 72 页)中,你创建了一个使用字符串、输入和输出将用户消息发布到聊天的程序。尽管这个程序很有用,但因为每次发布新消息时都需要重新运行程序,所以它的功能非常有限。

在这个任务中,你将通过使用while循环改进你的聊天程序,让用户可以在不重新启动程序的情况下发送任意数量的消息。

打开userChat.py文件(位于strings文件夹中),然后将其另存为chatLoop.py,保存在whileLoops文件夹中。

要在不重新运行程序的情况下每次发布新消息,向你的代码添加以下内容:

  1. 将一个无限的while循环添加到程序中。

  2. 向循环中添加一个if语句,检查用户的输入是否为"exit"。如果输入是"exit",循环应当中断。

  3. 确保在循环开始之前定义好userName变量。

当你添加完更改后,保存你的程序并运行它。Python Shell 会提示你输入用户名。输入后按 ENTER 键。然后程序会要求你输入一条消息。输入一条消息后按 ENTER 键。程序会继续要求你输入消息,直到你输入exit为止。图 7-8 展示了我的聊天程序运行情况。

image

图 7-8:我在和自己聊天。

奖励目标:方块聊天

扩展聊天功能,让用户能够创建方块。例如,如果用户输入"wool",程序将创建一个羊毛方块。你可以通过向if语句中添加elif语句来检查用户输入。

WHILE-ELSE 语句

if语句一样,while循环也可以有由else语句触发的次要条件。

else语句在while语句的条件为False时执行。与while语句的主体不同,else语句只会执行一次,如下所示:

message = input("Please enter a message.")

while message != "exit":
    print(message)
    message = input("Please enter a message.")
else:
    print("User has left the chat.")

这个循环会在输入的message不等于"exit"时一直重复。如果message"exit",循环将停止重复,else语句的内容将打印"User has left the chat."

如果你在while语句中使用了break语句,else语句将不会执行。以下代码与前面的示例类似,但包含一个嵌套的if语句和一个break语句。当用户输入abort而不是exit时,聊天循环将退出,而不会打印"User has left the chat."的消息。

message = input("Please enter a message.")

while message != "exit":
    print(message)
    message = input("Please enter a message.")
    if message == "abort":
        break
else:
    print("User has left the chat.")

if 语句检查输入的消息是否为"abort"。如果为True,则执行break语句,循环将退出。因为使用了break语句,所以else语句的内容不会执行,"User has left the chat."也不会被打印。

任务 #40:热与冷

在这个任务中,我们将在《Minecraft》中创建一个“热与冷”游戏。如果你从未玩过,游戏的规则是你的朋友隐藏一个物体,而你需要找到它。你的朋友会根据你离物体的远近给你提示。如果你很近,他们会说“热”,如果很远,他们会说“冷”。当你站在物体旁边时,他们会说“你着火了!”,如果你很远,他们会说“冰冻!”

游戏的目标是找到并站在随机放置在游戏世界中的钻石方块上。在这个版本的游戏中,你将单独玩,Python 程序会告诉你距离隐藏方块的远近。当你站在钻石方块上时,游戏结束。

列表 7-2 将一个方块放置在随机位置。

blockHunter.py

   from mcpi.minecraft import Minecraft
   import math
   import time
   import random
   mc = Minecraft.create()

   destX = random.randint(-127, 127)
   destZ = random.randint(-127, 127)
➊ destY = mc.getHeight(destX, destZ)

   block = 57
➋ mc.setBlock(destX, destY, destZ, block)
   mc.postToChat("Block set")

   while True:
       pos = mc.player.getPos()
➌     distance = math.sqrt((pos.x - destX) ** 2 + (pos.z - destZ) ** 2)

➍     if distance > 100:
           mc.postToChat("Freezing")
       elif distance > 50:
           mc.postToChat("Cold")
       elif distance > 25:
           mc.postToChat("Warm")
       elif distance > 12:
           mc.postToChat("Boiling")
       elif distance > 6:
           mc.postToChat("On fire!")
       elif distance == 0:
➎          mc.postToChat("Found it")

列表 7-2:热与冷程序的开始部分

在随机放置方块之前,程序确保方块不会被放置在地下。为此,它使用getHeight()函数 ➊,该函数找到游戏中任何位置的最高 y 坐标的方块(即地面上的方块)。然后,它在随机位置 ➋ 放置一个钻石方块。

➌ 处的代码计算到钻石方块的距离。它使用sqrt()函数,该函数位于math模块中——这就是为什么程序开始时需要import math的原因。sqrt()函数计算一个数的平方根。

注意

列表 7-2 使用了一个叫做毕达哥拉斯定理的公式。该公式使用三角形的两边来计算第三边的长度。在这种情况下,我使用玩家到隐藏方块在 x 轴和 z 轴的距离来计算到隐藏方块的直线距离。*

程序显示的信息取决于你距离方块的远近,你可以通过if语句和distance变量 ➍ 来确定。当你远离方块时,程序会显示 "Freezing",而当你非常接近时,程序会显示 "On fire!"

将清单 7-2 复制到 IDLE 中新文件中,并将程序保存为blockHunter.py,存放在whileLoops文件夹内。

目前程序是可以运行的,但当你找到方块时并不会结束。要完成代码,你需要在玩家与方块的距离为 0 时添加一个break语句➎。

完成程序后,保存并运行它。程序会生成一个随机方块,你需要找到它。当你找到并站在方块上时,程序应该停止。图 7-9 显示了我刚刚找到了方块。

image

图 7-9:我已经找到了方块,现在只需要站在上面。

额外目标:时间就是时间

blockHunter.py程序会给你足够的时间来找到方块。你能想到一种方法来显示玩家找到方块所花的时间,或者甚至限制他们玩游戏的时间吗?

你学到了什么

做得好!你已经学到了很多关于while循环的知识。你可以创建while循环和无限while循环,并且能够使用带条件和布尔运算符的循环。通过使用循环,你现在可以编写重复代码的程序,这样可以节省大量时间,让你可以专注于掌握 Minecraft。在第八章中,你将学习另一种使用函数使代码可重用的方法。

第八章:8

函数赋予你超能力

image

函数是可重用的代码块,执行特定的任务。假设你想写一段代码在 Minecraft 中构建一棵树。你可以在每次需要使用时重新编写构建树的代码(或复制粘贴);然而,这样做效率低下,尤其是在你需要修改代码时。

与其复制粘贴代码,你可以将构建树的代码写成一个函数。回想一下,我们在前面的章节中使用了一些函数:str()input()int()。它们都是 Python 内置的函数。你甚至已经在使用 Minecraft 函数,比如 getBlocks()setPos() 函数,它们是随 Minecraft Python API 提供的。在本章中,你将创建你自己的函数。

你创建并使用函数的原因如下:

可重用性 函数节省时间。因为你不需要一次又一次地重写相同的代码,编写程序变得更快也更容易。

调试 通过将任务分组到代码块中,能够更容易地识别问题的来源并进行修改来修复问题。

模块化 你可以在同一个程序中独立开发不同的功能。这样可以更方便地与他人共享代码,并在其他程序中重用功能。

可扩展性 使用函数可以更容易地增加程序的规模和它处理的数据量。

定义你自己的函数

让我们来看一下如何在代码中使用函数。在下面的例子中,我创建了一个名为 greeting() 的函数,它只是简单地打印两行:

def greeting():
    print("Hello")
    print("Nice to meet you")

def 关键字是 define 的缩写,告诉 Python 你正在编写一个函数。每当你想编写一个函数时,必须先写 def,然后是函数的名称。在这个例子中,greeting 是函数的名称。不要忘记在第一行末尾加上括号和冒号。冒号后面的代码行是函数的主体,也就是在调用函数时执行的代码。

注意

保持代码缩进的一致性。始终使用四个空格缩进函数体。

一个函数可以包含任意多的语句。它还可以包括 if 语句、循环、变量、条件、数学运算符等等。当你到达函数代码的结尾时,停止缩进代码行,这样 Python 就知道哪些语句属于函数,哪些语句属于代码的其他部分。

你可以在一个程序中创建任意多个函数,只要它们的名称不同。

调用函数

要使用或调用一个函数,你需要在括号中写出函数的名称及其可能需要的任何参数。如果你的函数不需要任何参数,只需写出函数名称和一对空括号。

要调用之前定义的 greeting() 函数,你可以使用以下代码:

greeting()

你可以根据需要调用这个函数多次。让我们调用greeting()函数三次:

greeting()
greeting()
greeting()

当你运行程序时,它应该会产生函数的输出三次,如下所示:

Hello
Nice to meet you
Hello
Nice to meet you
Hello
Nice to meet you

你必须在代码的主体中调用这个函数,否则函数不会做任何事情。这是一个常见的错误。如果你运行一个定义了函数的程序,但代码没有任何反应,那可能是因为你忘记调用你创建的函数。

你也可以在另一个你创建的函数中调用函数。这些函数包括内置的 Python 函数,以及你自己创建的函数。你稍后会看到这个功能的实际应用。

函数接受参数

函数中的括号包含它的参数,这些是函数在运行时使用的值。这些值会在函数内部用于特定的变量。并不是每个函数都需要参数。例如,greeting()函数不接受参数。

假设我想要用某人的名字展示一个问候。我将把它写成一个函数,这样我就可以重用这段代码来向不同的人问好:

def fancyGreeting(personName):
    print("Hello, " + personName)

fancyGreeting("Mario")
fancyGreeting("Steve")

在这个示例中,函数被使用了两次,传入不同的参数,分别是"Mario""Steve"。当你运行程序时,输出如下所示:

Hello, Mario
Hello, Steve

如果你在调用一个需要参数的函数时忘记提供参数,你会得到一个错误。同样,如果一个函数需要多个参数,而你忘记提供其中任何一个,你也会得到错误。例如,让我们尝试调用fancyGreeting()函数时没有提供任何参数,像这样:

fancyGreeting()

会显示以下错误信息:

   Traceback (most recent call last):
     File "<pyshell#2>", line 1, in <module>
       fancyGreeting()
➊ TypeError: fancyGreeting() takes exactly 1 argument (0 given)

这是一个有用的错误信息,因为最后一行解释了代码出错的原因 ➊。fancyGreeting()函数接受一个参数,但因为没有提供参数,导致了这个错误。

你可以创建一个接受多个参数的函数。例如,下面的程序包含一个函数,能够向某人打招呼,等待一段时间,然后说再见。该函数使用一个参数来表示人的名字和程序将等待的秒数:

   import time

➊ def helloAndGoodbye(personName, secsToWait):
       print("Hello, " + personName)
       time.sleep(secsToWait)
       print("Goodbye, " + personName)

➋ helloAndGoodbye("Mario", 10)
   helloAndGoodbye("Steve", 23)

每个参数在定义函数时由逗号分隔 ➊。然后,当函数被调用时,参数会按定义时的顺序传入 ➋。

注意

你可能会遇到参数 参数值 这两个术语几乎可以互换使用。函数的参数定义了它接受或需要的参数类型,而参数值是你在调用函数时传递给它的值。为了简便起见,本书中我们将只使用参数 这个术语。

任务 #41:建立一个森林

你的任务是创建一个 Minecraft 中的森林。因为森林其实就是一堆树,所以我们将通过创建一个构建单棵树的函数,然后多次调用这个函数来创建森林。

清单 8-1 是你将使用的基础代码。

forest.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

➊ def growTree(x, y, z):
       # Creates a tree at the coordinates given
       # Write your code to make a tree here

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z

➋ growTree(x + 1, y, z)

清单 8-1:一个使用函数创建森林的程序结构

这个代码中创建的growTree()函数 ➊ 需要接收树木要建造的坐标作为参数。你的任务是在函数体内编写代码,在给定的坐标处创建一棵树。你将使用setBlock()setBlocks()函数来完成这项工作。

将清单 8-1 复制到 IDLE 中的新文件,并将其保存为forest.py,存放在名为functions的新文件夹中。

当你创建了一个类似树形的结构并将其显示在屏幕上时,尝试使用不同的参数编写更多的函数调用,以便树木出现在不同的位置。第一个已经为你做了 ➋。每次运行程序时,尝试在玩家面前创建至少九棵树。图 8-1 展示了我程序创建的树木。

image

图 8-1:我刚刚种下了一排美丽的树木。

附加目标:随机森林

使用random模块中的randint()函数随机化森林中树木之间的距离。

重构程序

很多时候,你会写一个多次使用相同代码块的程序。当你需要在不同地方修改相同代码时,这将变得非常繁琐。你可能在过去写的程序中也做过这种操作,但其实有更好的方法。

你可以重构你的程序以使用函数。为此,将多次重复的代码移入一个单一的函数中,然后在其余的代码中随时调用。因为你只需要在一个地方进行修改而不是多个地方,这样你不仅节省空间,程序也更容易维护。将代码重构为这种方式的过程称为重构

例如,以下代码会询问三个人的名字,然后向每个人打印问候语:

name1 = input("Hello, what is your name?")
print("Pleased to meet you, " + name1)
name2 = input("Hello, what is your name?")
print("Pleased to meet you, " + name2)
name3 = input("Hello, what is your name?")
print("Pleased to meet you, " + name3)

这里的代码重复了相同的两行代码三次。如果你想要更改问题或问候语,会有什么问题吗?对于 3 个人,修改代码不成问题,但如果你要为 100 个人编写代码呢?

另一种方法是将代码写成一个函数,并调用三次。以下是重构后的代码:

def helloFriend():
    name = input("Hello, what is your name?")
    print("Pleased to meet you, " + name)

helloFriend()
helloFriend()
helloFriend()

现在,当程序运行时,它将要求输入并输出一个字符串,并且它将执行这两个任务三次。以下是输入和输出:

Hello, what is your name? Craig
Pleased to meet you, Craig
Hello, what is your name? Still Craig
Pleased to meet you, Still Craig
Hello, what is your name? Craig again
Pleased to meet you, Craig again

代码的第二个版本与第一个版本的结果相同,但正如你所看到的,它更易于阅读,也更容易修改。

任务 #42:重构代码

有时你会写一个程序,事后才意识到其实应该使用函数(我经常这样做)。将代码重构为使用函数是一个非常重要的技能。

在这个任务中,你将练习将程序重构为使用函数,而不是多次重复相同的语句。

列表 8-2 每 10 秒在玩家下方放置一个甜瓜方块。我们将重写代码,使用函数。目前,程序通过同一行代码重复三次来放置三个方块。图 8-2 展示了程序的结果。

melonFunction.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

import time

pos = mc.player.getPos()
x = pos.x
y = pos.y
z = pos.z
mc.setBlock(x, y - 1, z, 103)
time.sleep(10)

pos = mc.player.getPos()
x = pos.x
y = pos.y - 1
z = pos.z
mc.setBlock(x, y, z, 103)
time.sleep(10)

pos = mc.player.getPos()
x = pos.x
y = pos.y - 1
z = pos.z
mc.setBlock(x, y, z, 103)
time.sleep(10)

列表 8-2:需要重构的代码

这段代码不太好看,是吧?有几行是重复的,这总是表明代码需要通过函数定义来重构。

提示

识别出代码中重复的部分,以便了解你的函数应该做什么。

image

图 8-2:地下的三颗美味的甜瓜

修改代码,使其总共放置六个方块,通过调用你的函数六次来实现。创建一个新文件并将其保存为melonFunction.py,放入functions文件夹中。将列表 8-2 复制到文件中,并重构代码以使用函数。将新函数命名为makeMelon()

附加目标:脚下的方块

makeMelon()函数添加参数,以控制方块的类型、休眠时间或玩家下方的距离。

使用文档字符串进行注释

在 Python 代码中使用注释是解释代码功能的一种方式。当 Python 运行程序时,它会忽略注释中的所有内容,因此注释不会影响代码的运行。注释的主要目的是向其他可能查看或使用你代码的人解释你的代码应该做什么。注释也是你自己未来的有用提醒。

由于函数应该是可重用的,因此解释它们的目的非常重要。为了编写我们的函数解释,我们将使用长注释,这就是所谓的文档字符串。文档字符串是放置在函数开始处的多行注释,用于解释函数的用途。

以下示例中的duplicateWord()函数有一个文档字符串,解释了它的任务:

   def duplicateString(stringToDbl):
➊     """ Prints a string twice on the same line.
       stringToDbl argument should be a string """
       print(stringToDbl * 2)

函数的文档字符串应该位于函数的第一行 ➊。文档字符串以三重引号(""")开始和结束,可以跨多行书写,视需要而定。

参数中的换行符

为了让程序员更容易阅读长参数列表,Python 允许你将参数拆分成多行。例如,程序中的函数调用将参数拆分成多行,以提高可读性:

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

pos = mc.player.getPos()
width = 10
height = 12
length = 13
block = 103
mc.setBlocks(pos.x, pos.y, pos.z,
             pos.x + width, pos.y + height, pos.z + length, block)

在参数中使用换行符特别有用,当你想在参数上使用数学运算符时,或者当你使用长变量名作为参数,或者当你有多个参数要传递给一个函数时。

函数返回值

函数有两种类型:一种是返回值的,另一种是没有返回值的。到目前为止,你创建的函数都是没有返回值的。现在让我们来看一下返回值的函数。

从函数返回一个值非常有用,因为它允许函数处理数据并将值返回给程序的主体。例如,假设你在卖饼干。为了计算每个饼干的售价以赚取足够的利润,你需要在你支付的饼干成本基础上加上两枚金币,然后将总和乘以 10。通过使用一个返回值的函数,你可以编写这个计算并在 Python 中重用它。

在编写你自己的函数时,你可以使用return关键字从函数返回一个值。例如,这里是计算饼干售价的代码:

def calculateCookiePrice(cost):
    price = cost + 2
    price = price * 10
    return price

要返回一个值,你只需写return后跟你想要的值,在这个例子中是price。要使用返回值的函数,你需要在期望值的地方调用它。例如,要设置priceOfCookie变量,调用calculateCookiePrice()函数并输入一个费用,比如6

priceOfCookie = calculateCookiePrice(6)  # Value will be 80

你可以使用返回值的函数来设置变量的值,并且可以在任何需要值的地方使用它们,甚至作为另一个函数的参数。

不返回值的函数不能用来设置变量的值。让我们快速看一下它们之间的区别。

因为下面的函数会返回一个值,它可以在任何可以使用值的地方使用,比如设置变量,甚至作为另一个函数调用的参数:

def numberOfChickens():
    return 5

coop = numberOfChickens()
print(numberOfChickens())

运行这段代码来查看它的输出。你可以像对待一个值一样处理函数的返回结果,甚至对其进行数学运算。在这里,我将返回的值加上 4,并将其存储在一个名为extraChickens的变量中:

extraChickens = 4 + numberOfChickens()  # Value of 9

然而,下面的函数没有return语句,这意味着你不能将它用作值。你只能调用该函数:

def chickenNoise():
    print("Cluck")

chickenNoise()

在文本编辑器中编写这段代码并运行它会打印"Cluck",尽管它不能在其他语句中使用,因为它没有返回值给程序。例如,我可以尝试将函数与字符串连接,像这样:

multipleNoises = chickenNoise() + ", Bork"

如果我运行这个程序,我将得到以下错误信息:

Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    multipleNoises = chickenNoise + ", Bork"
TypeError: unsupported operand type(s) for +: 'function' and 'str'

这个错误意味着你不能将此函数与字符串组合使用,因为该函数不返回值。

然而,如果我将代码更改为返回一个值,而不是仅仅打印它:

def chickenNoise():
    return "Cluck"

multipleNoises = chickenNoise() + ", Bork"
print(multipleNoises)

文件会运行并显示以下输出:

Cluck, Bork

记住这个区别。当你需要时,记得包含return语句,当你的函数不需要返回值时,省略它。随着你在函数方面经验的积累,决定是否让你的函数返回一个值会变得更加容易。

任务 #43:块 ID 提醒

因为 Minecraft 有这么多块,所以很难记住所有的块 ID。我总是记得西瓜(103)和空气(0)的值,但忘记了其他的,所以我总是不得不用西瓜来建造房子!

为了更容易记住,我希望你为我创建一个程序,返回不同方块的值。你的程序应该有多个函数,帮助我记住方块 ID。每个函数的名称应该和它返回的方块的名称相同。例如,列表 8-3 中有一个名为melon()的函数,它返回西瓜方块的值(103)。

blockIds.py

def melon():
    """ Returns the value of the melon block """
    return 103

列表 8-3:帮助我记住方块 ID 的程序起始部分

在 IDLE 中创建一个新文件,并将其保存为blockIds.py,放在functions文件夹中。将列表 8-3 的代码复制到该文件中,并为它添加返回以下方块值的函数(参见方块 ID 备忘单,在第 283 页):

• 水

• 羊毛

• 熔岩

• TNT

• 花

• 钻石方块

在添加了你的函数后,通过调用这些函数来创建方块进行测试。由于你的新函数返回方块的值,你可以使用它们来设置变量的值,并将其传递给setBlock()函数。以下代码将帮助你开始:

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

# Functions go here

block = melon()
pos = mc.player.getTilePos()
mc.setBlock(pos.x, pos.y, pos.z, block)

图 8-3 展示了完整程序的结果,其中包括对melon()函数的测试。注意,任何方块的位置都被硬编码到这个程序中;它总是将方块放置在你当前的位置。

image

图 8-3:现在我不需要记住方块类型,这全要归功于这个方便的函数。

提示

要放置一个钻石方块、TNT 或任何其他类型的方块,你首先需要定义一个返回所需方块值的函数。然后你需要在代码中调用该函数,就像我在这个例子中调用melon()函数一样。

附加目标:更多方块

为你想要的任何其他方块类型添加额外的函数。

在函数中使用 IF 语句和 WHILE 循环

在第六章和第七章中,你学到了如何将if语句嵌套在其他if语句中,以及将while循环嵌套在其他while循环中。你还学到了如何将if语句放入while循环中,反之亦然!在本节中,你将学习如何将if语句和循环放入函数中。这样可以使你的函数非常灵活,因为你可以用它们做决策并重复代码。

IF 语句

当你在函数中编写if语句时,语法与常规if语句相同。你只需要记住,在每行的开始缩进四个额外的空格,以便 Python 知道它是函数的一部分。

以下代码将一个以字符串形式书写的数字转换为整数。例如,参数"four"将返回值4

def wordToNumber(numToConvert):
    """ Converts a number written as a word to an integer """
    if numToConvert == "one":
        numAsInt = 1
    elif numToConvert == "two":
        numAsInt = 2
    elif numToConvert == "three":
        numAsInt = 3
    elif numToConvert == "four":
        numAsInt = 4
    elif numToConver == "five":
        numAsInt = 5

    return numAsInt

让我们看另一个例子。以下函数检查你是否曾经遇到过某个人,并根据结果使用适当的问候语:

➊ def chooseGreeting(metBefore):
       """ Chooses a greeting depending on whether you've met someone before.
       metBefore argument should be a Boolean value """
       if metBefore:
➋         print("Nice to see you again")
       else:
➌         print("Nice to meet you")

   chooseGreeting(True)
   chooseGreeting(False)

chooseGreeting()函数接受一个布尔值参数,名为metBefore➊。函数中的if语句根据该参数的值打印输出。如果值为True,输出为"很高兴再次见到你"➋;如果值为False➌,输出为"很高兴认识你"

任务 #44:羊毛颜色助手

你已经使用过setBlock()setBlocks()方法,带有参数来设置方块坐标和方块类型,但这些方法还可以接受一个可选的额外参数,用来设置方块的状态

Minecraft 中的每个方块都有 16 种状态,从 0 到 15。例如,羊毛每个状态都有不同的颜色。TNT(方块 ID 46)在默认状态(状态 0)下不会爆炸,但在方块状态 1 下被击碎时会爆炸。尽管每个方块都有 16 个状态,但并不是所有的状态都有不同的行为。

要设置方块的状态,你需要为setblock()setblocks()函数提供一个额外的参数。以下代码创建了一个粉色羊毛方块:

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

block = 35
state = 6
# Creates a single block of pink wool
mc.setBlock(10, 3, -4, block, state)

# Creates a cuboid of pink wool
mc.setBlocks(11, 3, -4, 20, 6, -8, block, state)

羊毛(方块 ID 35)在 Minecraft 中有许多用途,因其不同的颜色而变得非常有用,但记住不同方块状态的颜色却很困难。幸运的是,你不需要记住不同的方块状态,因为你可以使用程序来提醒你。

让我们制作一个包含羊毛方块状态的程序。这个程序将包含一个带有参数的函数,参数是你想要的颜色,作为字符串传入。然后,函数返回该羊毛颜色对应的方块状态整数。该函数将包含程序的大部分代码。但是,你需要再添加几行代码来获取用户输入并在游戏中放置方块,同时使用你刚创建的函数来设置颜色。

首先,你需要找出不同颜色羊毛的方块状态。你可以在《方块 ID 备忘单》的第 283 页找到它们。下面是一些可以帮助你入门的代码(粉色的方块状态是 6):

woolColors.py

   def getWoolState(color):
       """ Takes a color as a string and returns the wool block state for
       that color """
➊     if color == "pink":
           blockState = 6
       elif # Add elif statements for the other colors
       # Return the blockState here

➋ colorString = input("Enter a block color: ")
   state = getWoolState(colorString)

➌ pos = mc.player.getTilePos()
   mc.setBlock(pos.x, pos,y, pos.z, 35, state)

目前,程序只包含了getWoolState()函数的初步代码。它仅包含一个针对粉色的if语句➊。程序的结尾还包括了用于接收用户输入方块颜色的代码➋,以及用于在玩家位置放置羊毛方块的代码➌。

使用elif语句为getWoolState()函数添加其他羊毛颜色及其对应的方块状态。程序应接受一个表示方块颜色的参数,并返回该方块状态的整数值。例如,提供参数"pink"将返回值 6。你还需要在程序中添加一个return语句。使用注释来指导你。

将文件保存为*woolColors.py*,并放入*functions*文件夹中。

如果你想让程序更加友好,你可以在颜色参数无效时向聊天发送消息。图 8-4 展示了 Python Shell 中的输入及羊毛方块在游戏中被放置的情况。

image

图 8-4:现在我可以通过输入我想要的颜色名称来创建任何颜色的羊毛方块。

while 循环

就像if语句一样,循环也可以写在函数内部。函数内部的循环语法与常规循环相同。你只需要记住,循环的每一行都应缩进四个空格,以表明它属于函数。

在以下示例中,函数中的while循环将打印toPrint参数。循环重复的次数由repeats参数决定。

def printMultiple(toPrint, repeats):
    """ Prints a string a number of times determined by the repeats variable """
    count = 0
    while count < repeats:
        print(toPrint)
        count += 1

你还可以在同一个函数中使用return语句和while循环。在大多数情况下,你会希望return语句位于循环外部。(如果你在循环内部使用return语句,它会终止循环并结束函数。)让我们来看一个例子:

   def doubleUntilHundred(numberToDbl):
       """ Doubles a number until it is greater than 100\. Returns the number of
       times the number was doubled """
       count = 0
       while numToDbl < 100:
           numberToDbl = numberToDbl * 2
           count += 1
➊     return count

   print(doubleUntilHundred(2))

该程序将一个数字翻倍,直到它大于 100。然后它返回循环重复的次数➊。

你也可以像在前几章中一样将函数调用放入循环中。

任务 #45:方块,无处不在

通过在函数内部使用循环,你可以通过一个参数来确定循环的重复次数。通过使用setBlock()函数,你还可以在循环内确定要放置的方块类型。

警告

本任务中的程序可能会具有破坏性,因此你可能想在新世界中尝试它,以保留你宝贵的创作。

在本任务中,你将创建一个函数,随机地在地图上放置方块。它放置的方块数量和类型由函数参数决定。

示例 8-4 会在地图上随机位置生成一个西瓜。

blocksEverywhere.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()
   import random

   def randomBlockLocations(blockType, repeats):
➊     count = 0
➋     # Add the loop here
       x = random.randint(-127, 127)
       z = random.randint(-127, 127)
➌     y = mc.getHeight(x, z)
       mc.setBlock(x, y, z, blockType)
       count += 1

示例 8-4:当调用时,此函数将在游戏中随机放置一个方块。

将示例 8-4 复制到 IDLE 中的新文件,并将其保存在functions文件夹中的blocksEverywhere.py文件中。在➋处,在函数内部添加一个while循环,以便代码可以重复执行。count变量➊可以帮助你知道循环已重复多少次。将repeats参数与循环条件中的count变量进行比较,设置循环重复的次数。将➋之后函数内部的所有行缩进,使它们也在循环内部。getHeight()函数确保方块位于地面之上➌。

最后,添加三个函数调用来创建方块。第一个函数应该创建 10 个方块,第二个创建 37 个方块,第三个创建 102 个方块。你可以选择任何你喜欢的方块类型。

保存程序并运行它。程序应该会在地图上随机生成方块。图 8-5 展示了一个示例。

image

图 8-5:你可以看到程序随机放置的一些方块。我创建了一个新世界来演示这个程序,以免破坏我的建筑物。

全局变量和局部变量

当你定义函数时,你将面临一个新的挑战,那就是变量的作用域。变量的作用域描述了你的程序如何访问它的数据。学习作用域的最佳方法是通过实际操作来理解它,所以让我们来看一些代码。假设你正在使用以下代码,它会增加你为派对准备的鸡蛋数量:

➊ eggs = 12

   def increaseEggs():
➋     eggs += 1
       print(eggs)

   increaseEggs()

有两个变量名为eggs,一个在函数外➊,另一个在函数内➋。看起来没什么太大问题,但 Python 会抛出一个错误。以下是错误信息的部分内容:

UnboundLocalError: local variable 'eggs' referenced before assignment

问题是,eggs变量是在函数外定义的,但当你试图在函数内对它进行操作时,Python 无法识别该变量。对于 Python 而言,函数内的变量和外部的变量是完全不同的,即使它们的名字相同。Python 这样做是为了防止不同函数中的变量意外地共享相同的名称,从而导致意外的错误。

在 Python 代码中,你有两种方式来处理文件中的变量:你可以将变量设置为全局的,这意味着它会影响整个程序或文件,或者将变量设置为局部的,这意味着它只在特定的函数或循环中可见。换句话说,你可以在函数内外使用相同的变量,或者可以创建两个不同的变量,它们影响代码的不同部分。

一个全局变量会在函数内外被视为相同的变量。对函数内变量的任何修改都会影响函数外定义的变量,反之亦然。要创建一个全局变量,使用global关键字➊:

   eggs = 12

   def increaseEggs():
➊     global eggs
       eggs += 1
       print(eggs)

   increaseEggs()

在这个例子中,当打印出eggs时,它的值将是 13。

你可以将变量当作局部变量来处理,从而产生不同的效果。在这种情况下,函数内外的变量会被视为不同的变量。对函数内变量的修改不会影响函数外的变量,反之亦然。所以你可以将代码修改为局部变量➊,如下所示:

   eggs = 12

   def increaseEggs():
➊     eggs = 0
       eggs += 1
➋     print(eggs)

   increaseEggs()
➌ print(eggs)

当函数内打印出eggs的值时➋,它将是 1,因为函数外的eggs变量不会影响函数内的局部变量。increaseEggs()函数内的eggs值是 1,而全局的eggs变量依然保持值 12➌。

任务 #46:一个会移动的方块

不久前,我认为让一个方块在 Minecraft 世界中自动移动会很酷。每秒钟它会向前移动。如果它碰到墙壁、树木或其他高物体,它会转向并朝另一个方向前进。然而,如果它掉进了一个洞里,它就会被卡住,无法逃脱。

代码清单 8-5 是创建一个能够自动移动的神奇方块程序的起始部分。

movingBlock.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time

   def calculateMove():
       """ Changes the x and z variables for a block. If the block
       in front of the block is less than 2 blocks higher, it will move
       forward; otherwise it will try to move left, then backward,
       then finally right. """
➊     # Create global variables here

       currentHeight = mc.getHeight(x, z) - 1

       forwardHeight = mc.getHeight(x + 1, z)
       rightHeight = mc.getHeight(x, z + 1)
       backwardHeight = mc.getHeight(x - 1, z)
       leftHeight = mc.getHeight(x, z - 1)

       if forwardHeight - currentHeight < 3:
           x += 1
       elif rightHeight - currentHeight < 3:
           z += 1
       elif leftHeight - currentHeight < 3:
           z -= 1
       elif backwardHeight - currentHeight < 3:
           x -= 1

       y = mc.getHeight(x, z)

   pos = mc.player.getTilePos()
   x = pos.x
   z = pos.z
   y = mc.getHeight(x, z)

   while True:
       # Calculate block movement
       calculateMove()

       # Place block
       mc.setBlock(x, y, z, 103)

       # Wait
       time.sleep(1)

       # Remove the block
       mc.setBlock(x, y, z, 0)

代码清单 8-5:不幸的是,这段代码在添加全局变量之前是无法正常工作的。

但这段代码仍然无法运行,因为 calculateMove() 函数中的变量不是全局的。

你的任务是完成 清单 8-5 中的代码。将其复制到 IDLE 中,并保存在 functions 文件夹下,命名为 movingBlock.py。在函数的开头添加代码,使 xyz 变量成为全局变量。全局定义应该放在 ➊ 位置。

在声明了一些全局变量后,运行程序。你的块应该会四处移动。图 8-6 展示了块向上移动到墙壁并开始绕过它。

附加目标:更智能的哈密瓜块

当你运行 movingBlock.py 程序时,你可能会注意到块沿 x 轴移动得最多,有时会导致它在两个块之间陷入循环。原因是代码没有考虑块已经移动过的方向,并且总是会尝试先沿 x 轴移动。你能找到方法来存储块最后一次移动的方向并修改 if 语句,让它先沿那个方向移动吗?

image

图 8-6:看到哈密瓜向前移动并试图绕过墙壁,真是太有趣了。

你学到了什么

太棒了!在本章中,你学会了如何创建和调用函数。通过 return 语句,你可以让函数返回值,并且可以在函数内部编写循环和 if 语句。在 第九章中,你将学习关于列表的知识,它们可以让你在一个变量中存储多个数据项。

第九章:9

使用列表和字典处理事物

image

我们使用列表,例如购物清单或指令清单,来记住一组项目或按特定顺序执行步骤。在 Python 中,列表非常相似:它们用于在序列中存储数据集合。一个列表可以存储多种类型的数据,包括字符串、数字、布尔值,甚至其他列表。

通常,变量只能保存一个值。列表非常有用,因为它们允许你在一个变量中存储多个值,例如从 1 到 100 的数字,或者你朋友的名字。在其他编程语言中,列表有时被称为数组

你可以使用块 ID、坐标或其他多种内容的列表,以便更好地控制你的 Minecraft 世界。因为列表可以在一个变量中存储多种类型的值,它们为你提供了常规变量无法提供的灵活性。

在本章中,你将学习如何使用 Minecraft Python API 和列表来创建一个迷你游戏,用于记录高度,制作进度条,并编写一个程序,让玩家在游戏中随机滑动。

使用列表

使用 Python 创建列表非常简单。定义一个列表时,可以将任意数量的值放在方括号内——或者根本不放任何值,这就是所谓的列表。列表中的每个项都需要用逗号分隔。

例如,一份面条汤的配料列表可能看起来像这样:

>>> noodleSoup = ["water", "soy sauce", "spring onions", "noodles", "beef"]

noodleSoup 列表包含多个项,它们都是字符串。

你可以像这样创建一个空列表:

>>> emptyList = []

当你希望稍后在程序中添加值时,可以使用空列表。

你可以在列表中存储任何数据类型,甚至可以混合不同的数据类型。例如,你可以有一个包含整数和字符串的列表:

>>> wackyList = ["cardigan", 33, "goofballs"]

有时你的列表可能非常长,使得人类难以阅读。但是你可以在 Python 中将长列表跨多行格式化,这样程序员可以更容易地阅读它们。将项分成多行不会影响 Python 代码的运行。例如,以下的汤配料格式与之前的 noodleSoup 列表一样:

>>> noodleSoup = ["water",
        "soy sauce",
        "spring onions"
        "noodles",
        "beef"]

接下来,我们将看看如何访问和修改列表中的项。

访问列表项

要访问列表中的值,需要引用项在列表中的位置,这个位置被称为索引。以面条汤为例,你可以像这样访问列表中的第一个项:

>>> print(noodleSoup[0])
water

需要注意的是,列表中的第一个索引是 0。第二项是索引 1,第三项是索引 2,依此类推。之所以如此,是因为计算机在使用列表时是从零开始计数的。

从零开始计数可能看起来很傻,但其实有充分的理由。早期的计算机非常慢,内存也非常有限。从零开始计数更快,也更高效。即使现在的计算机已经非常快,它们仍然从零开始计数。

还需要注意的是,如果你尝试访问一个列表索引,它大于列表中的项数,你会得到一个错误信息。以下这行代码尝试打印索引位置 5 中的项:

>>> print(noodleSoup[5])

这是部分错误信息:

IndexError: list index out of range

IndexError 提示我,我想访问的索引位置没有数据。列表中的索引位置 5 没有数据,因为它超出了列表的长度。Python 无法返回一个不存在的值!

更改列表项

就像你可以更改变量的值一样,你也可以更改列表中的单个项。这是因为列表是可变的,意味着它们是可以修改的。要更改列表中的一项,你可以使用该项的索引位置,并像设置变量的值一样设置它的值(通过等号)。

让我们把面条汤中的牛肉项改成鸡肉。牛肉是列表中的第五个项,所以它的索引是 4(记住,列表是从零开始计数的)。我们可以轻松地将索引 4 的项改为鸡肉,像这样:

>>> noodleSoup[4] = "chicken"

现在让我们在 Minecraft 中做一些有趣的事情,利用列表。

任务 #47:高与低

当我在探索 Minecraft 世界时,回顾我的旅程是件很有趣的事。从最高的山峰到最深的洞穴,探索是我在游戏中最喜欢的活动之一。有时候和朋友们一起玩时,我们会比赛,看看谁能最快到达游戏中的最高或最低点。为了防止作弊,我写了一个程序,记录玩家在 60 秒内到达的最低和最高 y 坐标。

当我运行程序时,它会告诉我我在游戏中旅行的一分钟内到达的最高和最低位置。清单 9-1 包含了我为你开始编写的代码。将其复制到一个新文件中,并将其保存为 highAndLow.py,放在一个名为 lists 的新文件夹中。

highAndLow.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time

➊ heights = [100, 0]
   count = 0

   while count < 60:
       pos = mc.player.getTilePos()

       if pos.y < heights[0]:
➋        # Set the lowest height to the y variable
       elif pos.y > heights[1]:
➌        # Set the highest height to the y variable

       count += 1
       time.sleep(1)

➍ mc.postToChat("Lowest: ")   # Output lowest height
➎ mc.postToChat("Highest: ")  # Output highest height

清单 9-1:获取玩家访问的最低和最高位置的代码开始部分

程序会将你所到达的最低和最高 y 坐标保存在一个名为 heights 的列表中 ➊。列表中的第一个项(索引位置 0)存储最低坐标,第二个项(索引位置 1)存储最高坐标。我们需要从一个较高的“最低”值和较低的“最高”值开始,这样第一次运行程序时,玩家的位置将成为新的最低或最高值,并会显示在聊天窗口中。这里我使用了默认的最低值 100 和默认的最高值 0。

while 循环每秒运行一次,持续 60 秒,以不断更新 heights 中的值。if 语句检查玩家当前的高度是否低于列表中存储的最低值 ➋。然后,elif 语句检查当前高度是否大于列表中存储的最高位置 ➌。

要完成代码,你需要将最低高度height[0]的值设置为➋处pos.y的值。记住,你可以像设置变量一样设置列表中的值,因此代码行应该如下所示:height[0] = pos.y。你还需要将最高高度height[1]的值设置为➌处pos.y的值。

最后,你需要在程序的最后两行输出最低➍和最高➎高度的值。为此,你需要从heights列表中访问最低和最高高度的索引位置(再次说明,索引 0 是最低高度,索引 1 是最高高度)。

运行程序并开始在游戏中跑动。看看你能跑得多高或多低。60 秒后,循环将停止,程序会显示你的最高和最低高度。多次运行程序,看看你能否打破自己的记录!

图 9-1 展示了我尝试的一种方法。

image

图 9-1:我访问过的最低 y 坐标是 15,最高是 102。

额外目标:一个意外的 bug

highAndLow.py中,最低和最高位置的默认值分别设置为 100 和 0。这没问题,只要你走得比 100 低、比 0 高。但是,如果你没有走得比 100 低、比 0 高,值就不会改变,这会导致程序不准确。你能解决这个问题吗?

操作列表

列表有一组内置函数,可以让你操作它们。这些函数包括常见操作,如向列表添加项目、插入项目或删除项目。

添加项目

你可以使用append()函数向列表末尾添加项目:只需将你想添加的项目的值作为参数传递进去。

如果我们往面条汤里加点蔬菜,味道会更好。为此,使用append()函数:

>>> noodleSoup.append("vegetables")

现在,noodleSoup列表的最后一项是一个"vegetables"字符串。

向列表中添加项目在你从一个空列表开始时非常有用。通过使用append()函数,你可以将第一个项目添加到空列表中:

>>> food = []
>>> food.append("cake")

插入项目

你也可以将项目插入到列表的中间。insert()函数将项目放在两个现有项目之间,并改变所有插入项后面项目的索引位置。

这个函数有两个参数,一个是你想插入项目的索引位置,另一个是你想插入的值。

例如,这是我们当前的noodleSoup列表:

>>> noodleSoup = ["water", "soy sauce", "spring onions", "noodles", "beef",
"vegetables"]

让我们把"pepper"添加到列表的第三个索引位置:

>>> noodleSoup.insert(3, "pepper")

插入后,更新的列表包含以下值:

["water", "soy sauce", "spring onions", "pepper", "noodles", "beef", "vegetables"]

如果你尝试在一个比列表长度更大的索引位置插入项目,项目将被添加到最后一个项目之后。例如,如果你的列表有七个项目,但你尝试插入第 10 个项目,项目将直接添加到列表的末尾。

>>> noodleSoup.insert(10, "salt")

运行这段代码后,列表中的最后一个项目将是"salt"

["water", "soy sauce", "spring onions", "pepper", "noodles", "beef",
"vegetables", "salt"]

请注意,盐并不在索引位置 10,而是在索引位置 7。

删除一个项目

有时你可能需要从列表中移除一个项目。你可以使用del关键字来做到这一点。该关键字放在列表名称之前,后面跟上你想删除的项目的索引位置,位置用方括号表示。

例如,要删除noodleSoup列表中现在位于索引位置 5 的"beef"项,可以这样做:

>>> del noodleSoup[5]

如果你想查找一个值的索引位置并将其删除,你也可以将del关键字与index()函数结合使用:

>>> beefPosition = noodleSoup.index("beef")
>>> del noodleSoup[beefPosition]

删除一个项目后,列表中的索引位置将会发生变化。这是我们删除位于索引位置 5 的"beef"后列表的样子:

["water", "soy sauce", "spring onions", "pepper", "noodles", "vegetables", "salt"]

"vegetables"的索引位置从 6 变为 5,而"salt"的索引位置从 7 变为 6。注意,只有在删除项之后的索引会受到影响;删除项之前的索引不会改变。删除列表中的项时,请记住这一点。

任务 #48:进度条

让我们使用一些列表函数来在 Minecraft 中创建一个进度条。它看起来像你在下载文件时看到的进度条,或者在角色扮演游戏中追踪你的下一级进度条。

程序将使用进度条计时到 10 秒。当程序开始时,进度条将由玻璃块组成。每经过一秒,进度条将用青金石块替换一个玻璃块。图 9-2 显示了进度条的前五个步骤。

image

图 9-2:进度条显示 50%的进度(10 个块中有 5 个是青金石)。

打开 IDLE 并创建一个新文件。将其保存为progressBar.py,并放在lists文件夹中。清单 9-2 中的程序是未完成的。将其复制到你的文本编辑器中。

progressBar.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time

   pos = mc.player.getTilePos()
   x = pos.x + 1
   y = pos.y
   z = pos.z

   # Add 10 glass blocks (ID 20) to this empty list
➊ blocks = [ ]
   barBlock = 22  # Lapis lazuli

   count = 0
   while count <= len(blocks):

       mc.setBlock(x, y, z, blocks[0])
       mc.setBlock(x, y + 1, z, blocks[1])
       mc.setBlock(x, y + 2, z, blocks[2])
➋     # Add setBlock() for the remaining blocks in the list

       count += 1

➌     # Delete the last block in the list

➍     # Insert a lapis lazuli block at the first position in the list

       time.sleep(2)

清单 9-2:制作进度条的未完成代码

要完成清单 9-2 中的程序,你需要执行以下操作:

  1. 在➊位置将 10 个玻璃块(ID 20)添加到空的blocks列表中。

  2. 使用setBlock()函数将列表中的所有 10 个块➋设置到游戏世界中。前 3 个块已经为你设置好了。

  3. 编写一个语句删除列表中的最后一个块(索引位置 9)➌。记住,你需要使用del关键字从列表中删除一个项目。

  4. 在列表的开始位置插入一个新的青金石块➍。使用insert()函数和barBlock变量,将一个新的青金石块插入到索引位置 0。

代码中包含注释,以帮助你找到需要执行这些任务的位置。

附加目标:再次上下

此时,progressBar.py中的进度条只会向上计数,并在满格时停止。你能弄明白如何让进度条向相反方向倒计时吗?

将字符串当作列表处理

字符串可以像列表一样处理,因为字符串也是一个 数据序列。你可以通过索引访问字符串中的单个字符;然而,你不能使用 appendinsert 函数更改每个索引位置的字符,因为字符串是 不可变的。这意味着它们无法更改。

以下代码将打印字符串 "Grape" 中的第二个字母:

>>> flavor = "Grape"
>>> print(flavor[1])
r

这表明你可以像访问列表中的元素一样访问字符串的部分。例如,你可以访问某人的名字和姓氏的首字母来打印他们的首字母:

>>> firstName = "Lyra"
>>> lastName = "Jones"
>>> initials = firstName[0] + " " + lastName[0]
>>> print(initials)
L J

通过使用索引位置访问字符串的部分,你得到的新字符串 "L J" 被称为 子字符串。注意,字符串的索引也是从零开始的!

元组

元组 是一种不可变的列表类型。但和其他列表一样,它们是由任何变量类型的项组成的序列。元组使用圆括号而不是方括号,并且使用逗号分隔项。

例如,假设一个国家唯一的奥林匹克运动员来自一个资金匮乏的训练项目,记录了他们在长跳中的多个跳跃距离(单位:米):

>>> distance = (5.17, 5.20, 4.56, 53.64, 9.58, 6.41, 2.20)

如果运动员只跳了一次,你也可以创建一个包含单一值的元组。要写一个包含单一值的元组,你仍然需要加上逗号:

>>> distance = (5.17,)

在定义元组时,圆括号是可选的,因此你可以通过在值之间放置逗号来定义元组,就像这样:

>>> distance = 5.17, 5.20, 4.56, 53.64, 9.58, 6.41, 2.20

要访问元组的值,可以使用与普通列表相同的方括号表示法。让我们将 distance 元组中索引为 1 的值赋给变量 jump

>>> jump = distance[1]
>>> print(jump)
5.20

列表和元组之间的主要区别在于元组是不可变的:你不能改变它们的内容。你不能向元组的末尾添加项目、插入项目、删除项目或更新任何值。当你的程序不需要改变元组中项目的值时,你会使用元组而不是列表。

使用元组设置变量

元组的一个有用特点是你可以同时为多个变量赋值。这节省了空间,并且可以将相关的变量聚集在一起。

通常,你会像引用列表一样引用元组,使用一个变量名:

measurements = 6, 30

然而,假设我们想要将值存储在两个变量中而不是一个。实现这一点的语法并不复杂。你只需要用逗号分隔变量名,然后使用等号,接着在等号的另一边写上元组。每个元组值将被分配给对应位置的变量。让我们看一下。

在这个例子中,两个变量 widthheight 分别被设置为值 630

width, height = 6, 30

现在我们有了两个变量。一个叫做 width,其值为 6,另一个叫做 height,其值为 30。而且我们仅通过一行代码就完成了这件事!

任务 #49: 滑动

使用元组设置变量是节省程序空间的一种快速简便的方法。它对于将相关变量集中设置在程序中的一个地方也非常有用。例如,在本书中,你已经使用过类似这样的代码来设置xyz变量的值:

x = 10
y = 11
z = 12

相反,你可以使用元组在一行代码中设置所有这些值:

x, y, z = 10, 11, 12

接下来,你将运用你新的代码编写能力!你的任务是创建一个程序,使玩家在游戏世界中随机移动,通过小步伐看起来像是在冰上滑行。我已经为你开始了程序代码,见清单 9-3;一些部分缺失,你需要完成它们。

sliding.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import random
   import time

➊ # Get the player's position

➋ # Set the x, y, and z variables on the same line using a tuple

   while True:
➌     x += random.uniform(-0.2, 0.2)
       # Change the z variable by a random float
➍     z +=
       y = mc.getHeight(x, z)

       mc.player.setPos(x, y, z)
       time.sleep(0.1)

清单 9-3:让玩家在地图上滑行的代码开头

将清单 9-3 复制到一个新文件中,并将其保存在你的lists文件夹中,命名为sliding.py。要完成程序,你需要获取玩家的起始位置 ➊,并设置xyz变量的值 ➋。使用元组设置这些值。该程序还使用了uniform()函数 ➌,它类似于randint()函数(见“玩转随机数”在第 62 页),但返回一个随机的浮动值,而不是整数值。在循环中,使用uniform()函数来更改z变量的值 ➍。x变量已经通过该函数进行了更改 ➌。

图 9-3 展示了我的玩家在游戏中缓慢滑行。

image

图 9-3:我在花园里慢慢滑行

附加目标:滑动方块

sliding.py程序使玩家在游戏中随机滑动。你能想出如何修改程序使方块滑动吗?

返回元组

一些 Python 的内置函数返回一个元组。当你定义自己的函数时,它们也可以返回一个元组作为结果。为此,你只需要在return关键字后面放一个元组。例如,我们可以创建一个将日期转换为元组的函数。我们将日期作为字符串参数传递,函数会返回一个包含年份、月份和日期的元组。以下是代码:

def getDateTuple(dateString):
    year = int(dateString[0:4])
    month = int(dateString[5:7])
    day = int(dateString[8:10])
    return year, month, day

当我们调用函数并传入一个日期字符串时,它会返回一个元组,元组中的顺序为年份、月份和日期:

>>> getDateTuple("1997-09-27")
(1997, 9, 27)

当我们调用函数时,可以根据需要存储返回的元组。以下代码将每个值存储到一个单独的变量中:

year, month, day = getDateTuple("1997-09-27")

现在我们可以快速将日期字符串转换为单独的变量。在我作为软件开发人员的工作中,我经常使用与此非常相似的代码。

列表的其他有用特性

你可以使用列表完成许多其他任务。本节解释了如何查找列表的长度、如何从列表中随机选择一个项目,以及如何使用if语句检查一个值是否在列表中。

列表长度

len() 函数是一个快速查找 Python 中任何列表长度的方式。当列表作为参数时,函数返回列表中项目的数量。我们来看看它是如何工作的:

>>> noodleSoup = ["water", "soy sauce", "spring onions", "noodles", "beef",
"vegetables"]
>>> print(len(noodleSoup))
6

尽管 Python 从零开始计算索引,但它以常规的计数方式计算列表中有多少个项目。这个列表中的最高索引是 5,但 Python 知道总共有 6 个项目!

任务 #50:块击打

Minecraft Python API 提供了一个方便的函数,返回你用剑击中的方块位置列表。你可以使用列表中的项目来获取你击中的方块的坐标。你将在本章后面的程序以及本书后续章节中看到它的有用之处。

你还可以制作一个短小有趣的游戏,计算你在一分钟内能击打多少个方块。在这个任务中,你将实现这一目标。这是一个非常有趣的游戏:与朋友一起玩,试图打破彼此的记录!你还可以扩展它,例如通过记录最高分来增加趣味。

图 9-4 展示了程序的运行效果。

image

图 9-4:在 60 秒内我击打了 197 个方块。

制作这个游戏所需的代码并不多。以下是代码结构的概述:

  1. 连接到 Minecraft 游戏。

  2. 等待 60 秒。

  3. 获取块击打列表。

  4. 显示块击打列表的长度到聊天窗口。

以下代码展示了你到目前为止尚未看到的部分,即从游戏中获取块击打列表的代码:

blockHits = mc.events.pollBlockHits()

这段代码使用 pollBlockHits() 函数返回一个块击打列表,并将该列表存储在名为 blockHits 的变量中。blockHits 变量将像其他任何类型的列表一样工作,因此你可以通过索引位置访问数据,并获取列表的长度。

当你玩这个游戏时,你需要右键点击方块来计算它们的数量。原因是 pollBlockHits() 函数会记录所有你用剑右键点击的方块。在 Minecraft 的 PC 版本中,使用剑右键看起来更像是你在防御而不是击打某物,但它仍然会记录你点击了哪些方块。图 9-5 展示了这是什么样子。确保你只用剑右键点击:用剑左键点击不会被记录,手里拿着别的东西右键点击也不会被记录!但你可以使用任何类型的剑,包括铁剑、金剑和钻石剑。

image

图 9-5:当我右键点击时,玩家像这样举着剑。

当你打印列表的输出时,它应该看起来类似于这个,尽管每次值会根据你击打的位置不同而变化:

[BlockEvent(BlockEvent.HIT, 76, -2, 144, 1, 452),
BlockEvent(BlockEvent.HIT, 79, -2, 145, 1, 452),
BlockEvent(BlockEvent.HIT, 80, -3, 147, 1, 452),
BlockEvent(BlockEvent.HIT, 76, -3, 149, 1, 452)]

这个列表输出存储了四个块击打的详细信息。每个项目包含击打的坐标。你将在 任务 #55(第 196 页)中学会如何访问这些坐标。

为了帮助你开始这个程序,我已经在 清单 9-4 中写了基本的结构。

swordHits.py

   # Connect to the Minecraft game
   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time

   # Wait 60 seconds
   time.sleep(60)

   # Get the list of block hits
➊ blockHits =

   # Display the length of the block hits list to chat
➋ blockHitsLength =
   mc.postToChat("Your score is " + str(blockHitsLength))

清单 9-4:剑击游戏的开端

为了完成这个程序,打开 IDLE,创建一个新文件,并将清单 9-4 的内容复制到文件中。将文件保存为swordHits.py,并放入lists文件夹中。使用pollBlockHits()函数➊设置blockHits变量,并通过获取blockHits变量的长度来设置blockHitsLength变量➋。

随机选择一个项

到这里,你可能已经意识到,我真的很喜欢在我的程序中使用随机生成的元素。随机性使得程序在每次运行时表现得有些不可预测。

当你使用列表时,你会时不时需要从列表中访问随机项。例如,你可能需要从一堆块中随机选择一个。

random模块中的choice()函数是选择列表项的首选函数。这个函数接受一个参数,即你想要使用的列表,并从中返回一个随机项。

在清单 9-5 中,colors列表包含了几种颜色的名称。它使用choice()函数随机选择一个,并将其打印出来:

import random
colors = ["red", "green", "blue", "yellow", "orange", "purple"]
print(random.choice(colors))

清单 9-5:从颜色列表中打印一个随机颜色

当你运行代码时,程序会随机输出列表中的一项。

任务 #51:随机块

在 Minecraft 中,从一系列数字中随机选择一个块 ID 可能会导致程序出错,因为有些块 ID 没有对应的块。一个解决方法是使用一个有效块的列表,从中随机选择。列表允许你创建有限数量的项,然后使用choice()函数从中随机选择一个。

你的任务是创建一个块 ID 的列表,从中随机选择一个块,然后将该块设置到玩家的位置。你可以使用清单 9-5 作为起点。

首先,创建一个块 ID 的列表。其次,使用random.choice()函数从列表中选择一个块。最后,使用setBlock()函数将随机块放置到 Minecraft 游戏中。

将程序保存为randomBlock.py,并放入lists文件夹中。

可以在列表中包含任意数量的块。对于我的列表,我选择了五种块,包括西瓜、钻石和金块。你可以在图 9-6 中看到运行程序的结果。

image

图 9-6:程序随机选择了一个金块。

复制列表

在大多数编程语言中,复制列表是相当棘手的。列表变量实际上并不包含值;相反,它们包含一个指向你计算机内存中某个地址的引用,这个地址进一步引用了列表中包含的值。虽然你的计算机会在幕后处理这一功能,但了解它的工作原理是值得的,因为这会让你成为一个更聪明的程序员!你可以使用id()函数查看列表的内存地址:

>>> cake = ["Eggs",
            "Butter",
            "Sugar",
            "Milk",
            "Flour"]
>>> print(id(cake))

例如,在我的电脑上运行这段代码时,输出为 3067456428。值 3067456428cake 存储的内存位置。当你在你的电脑上运行这段代码时,可能会得到一个不同的数字,因为它存储在电脑内存中的不同位置。

你不需要完全理解这个行为,但你需要知道,当你想把一个列表复制到另一个变量时,它会产生影响。与预期的不同,列表中的值并没有被复制,而是列表的内存位置被复制到了新变量中。这意味着,当你在任一列表中更改某个值时,它会影响另一个列表。

例如,下面的程序创建了一个名为 cake 的列表,然后将 chocolateCake 的值设置为与 cake 相同。接着,向 chocolateCake 列表中添加了一个项,"Chocolate"

>>> cake = ["Eggs",
            "Butter",
            "Sugar",
            "Milk",
            "Flour"]

>>> # Store the list in a second variable
>>> chocolateCake = cake
>>> chocolateCake.append("Chocolate")

不幸的是,尽管你不希望这样,"Chocolate" 也被添加到了 cake 列表中。你可以通过打印列表看到这个错误:

>>> print(cake)
['Eggs', 'Butter', 'Sugar', 'Milk', 'Flour', 'Chocolate']
>>> print(chocolateCake)
['Eggs', 'Butter', 'Sugar', 'Milk', 'Flour', 'Chocolate']

这个问题发生是因为变量存储的是列表的内存位置,而不是列表中的项。

克服这个问题的一种简单方法是使用 列表切片。当你用刀切割食物时,你是在把它分成不同的部分。在 Python 中,列表切片也类似。当你切割一个列表时,你实际上是从列表中取出一部分。你可以使用列表切片来获取列表中的某些项,但在这个例子中,你将使用列表切片来复制列表中的每一项。要将 cake 列表复制到 chocolateCake 变量中,可以使用以下代码:

>>> chocolateCake = cake[:]

现在,chocolateCake 变量将包含 cake 列表中的值,但内存地址不同。

可以通过列表切片来修正蛋糕配料的代码:

   >>> cake = ["Eggs",
               "Butter",
               "Sugar",
               "Milk",
               "Flour"]

   >>> # Store the list in a second variable
➊ >>> chocolateCake = cake[:]
   >>> chocolateCake.append("Chocolate")

你可以看到,cake 中的项已经通过 [:] 被复制到了 chocolateCake 中,见 ➊。

以下是输出结果:

>>> print(cake)
['Eggs', 'Butter', 'Sugar', 'Milk', 'Flour']
>>> print(chocolateCake)
['Eggs', 'Butter', 'Sugar', 'Milk', 'Flour', 'Chocolate']

请注意,现在两个列表中的值已经不同——只有 chocolateCake 包含了 "Chocolate" 值。

项目和 if 语句

要检查某个值是否在列表中,可以使用 in 运算符。in 运算符位于一个值和你想检查的列表之间。如果该值在列表中,表达式的结果为 True;如果该值不在列表中,表达式的结果为 False

以下示例检查值 "Eggs" 是否在 cake 列表中:

>>> cake = ["Eggs", "Butter", "Sugar", "Milk", "Flour"]
>>> print("Eggs" in cake)

输出值为 True,因为 "Eggs" 在列表中。

你当然可以将 in 运算符用作 if 语句条件的一部分。以下代码扩展并改编了这个例子,使用 if 语句而不是直接打印布尔值。它检查 "Ham" 是否在 cake 列表中,并根据是否在列表中打印不同的信息:

>>> cake = ["Eggs", "Butter", "Sugar", "Milk", "Flour"]
>>> if "Ham" in cake:
>>>     print("That cake sounds disgusting.")
>>> else:
>>>     print("Good. Ham in a cake is a terrible mistake.")

你可以将not操作符与in操作符结合使用,产生相反的效果。代码将返回False而不是当一个项目在列表中时返回True,反之亦然。下面是示例(注意ifelse语句的主体也已交换):

>>> cake = ["Eggs", "Butter", "Sugar", "Milk", "Flour"]
>>> if "Ham" not in cake:
>>>     print("Good. Ham in a cake is a terrible mistake.")
>>> else:
>>>     print("That cake sounds disgusting")

你可以在程序中使用任一技术。只需选择你认为最合适的一种!

任务 #52:夜视剑

你在探索 Minecraft 中的洞穴时是否会忘记带足够的火把?我经常这样做。有时我甚至忘记带火把,而且我已经走得太远,无法回头。所以我在黑暗中摸索,不确定自己是否发现了什么有用的东西。但是,凭借你的 Python 知识,你可以制作一个程序,帮助你用剑找到钻石。

让我们编写一个基本程序,使用pollBlockHits()函数检查你击中的方块是否是钻石矿石。这对于在没有光的洞穴中探险,或者玩“在黑暗中找钻石矿石”的游戏很有用。代码在 Listing 9-6 中。将其复制到新文件中,并保存为nightVisionSword.py,保存在lists文件夹中。

nightVisionSword.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time

   blocks = []

   while True:
       hits = mc.events.pollBlockHits()
       if len(hits) > 0:
           hit = hits[0]
➊         hitX, hitY, hitZ = hit.pos.x, hit.pos.y, hit.pos.z
           block = mc.getBlock(hitX, hitY, hitZ)
           blocks.append(block)

➋     # Add the if statement here

       time.sleep(0.2)

Listing 9-6:这个程序将帮助你在黑暗中找到钻石矿石。

注意如何使用hit.pos.xhit.pos.yhit.pos.z ➊。每次击中都会存储点击方块的坐标,这些坐标是通过元组存储的。你可以使用点符号访问这些坐标。在这个例子中,hit这个变量名被用来命名包含每个击中方块的列表,因此我通过hit.pos.xhit.pos.yhit.pos.z来访问这些坐标。

代码几乎完成了。唯一剩下的任务是检查你是否找到了钻石。添加一个if语句 ➋,检查blocks列表中是否有钻石矿石(方块 ID 56),如果有,则向聊天窗口发送消息"你找到了钻石矿石!"。在if语句中添加一个break语句,使得当你找到矿石时,循环停止。

图 9-7 展示了程序的运行效果。

image

图 9-7:很黑,但我找到了钻石矿石。耶!

如果你不像我那么健忘,记得带火把进入洞穴,你仍然可以将这段代码当作一个游戏来使用。制作一个没有光的地下室,并在墙上某个地方放一个钻石矿石。运行程序,看看你需要多少时间才能在黑暗中找到钻石矿石。记得用剑右键点击!这是pollBlockHits()函数记录你击中的方块的唯一方式。

额外目标:钻石挑战

nightVisionSword.py程序改造成一个完整的小型游戏会很酷。你能自动生成一个房间,在其中随机放置一个钻石方块,把玩家放在这个房间里,然后计时玩家在黑暗中找到该方块所花费的时间吗?

字典

字典是一种使用不同方法的列表。字典不使用索引来标识项,而是使用程序员定义的一组键来标识项。

例如,这个raceTimes字典存储了参加比赛的人的名字和他们的比赛时间:

raceTimes = {'Katy': 26,
             'Alex': 30,
             'Richard': 19}

键唯一地标识字典中的每个值。在这个示例中,键是人的名字。'Katy'键对应的值是26

和列表一样,字典是可变的;它们的内容可以更改。

定义字典

要定义一个字典,可以使用一对花括号将一组键值对括起来。例如,你可以使用字典来描述一个人。你可以使用像'name''favoriteAnimal'这样的键来存储关于此人的信息,像这样:

person = {'name': 'David',
        'age': 42,
        'favoriteAnimal': 'Snake',
        'favoritePlace': 'Inside a cardboard box'}

在这个示例中,每个键都是一个字符串。每个键与一个值配对,使用冒号。例如,'age'是一个键,42是其对应的值。字典中的项然后用逗号分隔。

你可能已经注意到,使用字典可以让程序员轻松理解列表中的每一项表示什么;例如,很容易理解'name'键存储的是名字,而不是数字或其他随机信息。

你还可以使用整数和浮动数作为字典的键。在字典中使用浮动数或整数非常有用,当你想要匹配的键与值之间不遵循严格的顺序时。

以下示例创建了一个火车时间的字典。火车时间(浮动值)作为键存储,火车的目的地作为值存储:

trainTimes = {1.00: 'Castle Town',
             2.30: 'Sheep Farm',
             3.15: 'Lake City',
             3.45: 'Castle Town',
             3.55: 'Storage Land'
             }

因为字典可以存储成对的数据,它们非常适合像这样的情况。如果我使用一个火车目的地的列表而不是字典,我就无法将时间与目的地匹配起来。我只能使用列表的索引位置,如 0、1、2、3、4,等等,而不是时间。

访问字典中的项

要访问字典中项的值,可以使用方括号和键,而不是使用索引。键通常是字符串或整数。当你创建一个使用字符串作为键的字典时,确保将它们放在引号中。

例如,要访问之前创建的person字典中'name'键的值,你可以使用以下语法:

person = {'name': 'David',
        'age': 42,
        'favoriteAnimal': 'Snake',
        'favoritePlace': 'Inside a cardboard box'}

agentName = person['name']

agentName变量将包含值'David',因为它访问了'name'键的值。同样,如果你想访问代理的年龄,可以使用'age'键:

agentAge = person['age']

这会将值42存储在agentAge变量中。

trainTimes示例中,你可以使用字典中的键值(火车时间)来访问字典中的值(目的地),这些键值是浮动的:

trainTimes = {1.00: 'Castle Town',
             2.30: 'Sheep Farm',
             3.15: 'Lake City',
             3.45: 'Castle Town',
             3.55: 'Storage Land'
             }

myTrain = trainTimes[3.15]

访问trainTimes字典中的3.15键会将myTrain变量设置为'Lake City'

任务 #53: 旅游指南

在使用字典时,你可以将任何数据类型作为值存储,包括列表和元组。例如,你可以存储一个包含xyz值的元组。以下是一个实现该功能的代码示例:

places = {'Living room': (76, 1, -61), 'Bedroom': (61, 9, -61)}

places字典存储了两个项。字典的键是我在 Minecraft 游戏中某个地点的名称(例如我的客厅或卧室),而值是该地点坐标的元组。如果我想访问我客厅的坐标,我将使用以下代码:

location = places['Living room']
x, y, z = location[0], location[1], location[2]

你的任务是创建一个程序,使用字典存储 Minecraft 游戏中不同地点的位置,以便你可以通过名称传送到这些地方。可以在字典中包含任意数量的位置。要传送到这些地点,你需要访问字典中存储的坐标元组,然后将xyz设置为元组中存储的值。代码中的注释标明了你需要进行这些操作的位置。

将示例 9-7 复制到 IDLE 文本编辑器中,并将其保存到lists文件夹下,命名为sightseeingGuide.py

sightseeingGuide.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   # Add locations to the dictionary
   places = {}

   choice = ""
   while choice != "exit":
➊     choice = input("Enter a location ('exit' to close): ")
➋     if choice in places:
          # Store the dictionary item's value using its key (choice)
          location =
          # Store the values stored in the tuple in the x, y, and z variables
          x, y, z =
          mc.player.setTilePos(x, y, z)

示例 9-7:一些用于传送到不同位置的有趣代码

我已经包含了一条语句,提示你输入你想要前往的地点名称。这个输入被存储在choice变量中 ➊。程序随后使用if语句检查choice的值是否在字典中 ➋。最后一行使用xyz变量将玩家传送到字典中存储的位置。

当程序运行时,输入你想要前往的地点名称。图 9-8 展示了我在程序中传送到不同地点的版本。

image

图 9-8:我传送到了我的客厅(上)和卧室(下)。

更改或添加字典中的项

更改字典中项的值并不需要太多工作。你只需使用方括号和键来访问该项,并像设置普通变量一样(使用等号)为其赋值。你也可以使用这种方法添加新项。

让我们将person字典中age项的值从 42 更改为 43:

person['age'] = 43

让我们也添加一个名为location的新项,值为'USS Discovery'

person['location'] = 'USS Discovery'

运行此代码后,字典将包含一个名为location的新键,其值为'USS Discovery'

删除字典中的项

有时候,你可能想删除字典中的某个项。和列表一样,你可以使用del关键字来删除它。例如,要删除person字典中的favoriteAnimal项,你可以这样做:

del person['favoriteAnimal']

如你所见,这和从列表中删除项一样有效。

任务 #54:击打方块得分

在任务 #50(第 180 页)中,你编写了一个程序,计算玩家在 60 秒内用剑击打方块的次数。虽然这个程序很有趣,但如果你能记录所有玩家的得分,那会更酷。

要为游戏添加成绩榜,你将使用字典。字典将存储玩家的名字和分数,这些可以与其他玩家的分数一起显示出来。

要开始,打开swordHits.py并将其保存为swordHitsScore.py,放在lists文件夹中。更新代码以匹配清单 9-8,在这里我对程序做了一些修改,使其能够重复运行,要求玩家输入他们的名字,然后打印所有的成绩。(我还包括了swordHits.py中缺失代码的解决方案。)旧部分已被灰色标记。(记得缩进循环内部的所有内容。)

swordHitsScore.py

   # Connect to the Minecraft game
   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time

   name = ""
   scoreboard = {}

   while True:
       # Get the player's name
       name = input("What is your name? ")
       # Break loop if name is exit
       if name == "exit":
           break
       mc.postToChat("Go!")

       # Wait 60 seconds
       time.sleep(60)

       # Get the list of block hits
       blockHits = mc.events.pollBlockHits()

       # Display the length of the block hits list to chat
       blockHitsLength = len(blockHits)
       mc.postToChat("Your score is " + str(blockHitsLength))

➊     # Add the player to the scoreboard

       # Display the scoreboard
       print(scoreboard)

清单 9-8:当代码完成时,它将为方块击打游戏添加一个成绩榜。

要完成程序,你需要存储每个玩游戏的玩家的名字和分数。通过使用代码中➊部分的数据,向字典添加一个新的条目来实现这一点。字典叫做scoreboard,玩家的名字存储在name变量中。

图 9-9 显示了我的成绩榜的输出。

image

图 9-9:我和我的朋友们玩了一个游戏,Jim 以 274 次方块击打获胜。

注意

你可能已经注意到,当打印scoreboard字典时,它不太容易阅读。在任务 #59 中,你将学会如何解决这个问题(第 205 页)。

附加目标:最佳成绩

目前,如果某人玩了swordHitsScore.py游戏两次或更多次(并且输入了相同的用户名),程序只会记录他们最近的分数。你能想出如何使用if语句来检查玩家是否已经输入过分数,并且仅在新分数大于之前的分数时存储新分数吗?以下是帮助你开始的代码。它检查某人的名字是否已经在scoreboard字典中:

if name in scoreboard:

你学到了什么

做得好!在本章中,你学到了列表、元组和字典。你看到它们可以在一个变量中存储多个数据值。它们是结构化和存储程序数据的非常有用的方式。

在这些任务中,你创建了几个有趣的程序,使用了列表、字典和元组。通过列表,你创建了一个使用青金石和玻璃的进度条。通过元组,你学到了设置xyz变量的更快捷方式。字典让你存储你建造的物体的坐标,然后通过输入它们的名字传送到这些物体。

在第十章中,你将通过学习for循环,进一步发展你对列表的理解。你将创建一些非常酷的程序,包括一个可以用来复制你所建造的物品的程序。

第十章:10

使用 for 循环在 Minecraft 中施展魔法

图片

现在是时候学习 for 循环了。for 循环非常有用,因为它们可以遍历列表中的项目,就像你在 第九章 中看到的列表一样。这意味着当你在程序中需要使用循环遍历列表时,for 循环是完美的选择。

在跟随本章任务的过程中,你将使用 for 循环生成楼梯、柱子、金字塔和风化的墙壁。通过嵌套的 for 循环和列表,你将能够在几秒钟内创建像素艺术并生成新结构。for 循环是 Minecraft 中构建的强大工具!

一个简单的 for 循环

for 循环会对列表中的每个项目重复执行一段代码,直到列表结束,而不是像 while 循环或 if 语句那样使用条件。

你在 for 语句中使用的列表可以包含任意数量的任何数据类型的项。for 循环将按顺序遍历每个项,即按其索引。例如,要打印面条汤列表中的每一项,我们可以使用以下代码:

noodleSoup = ["water", "soy sauce", "spring onions", "pepper", "noodles",
"beef", "vegetables"]

for ingredient in noodleSoup:
    print(ingredient)

我们使用 for 操作符告诉 Python 我们正在使用一个循环。

for 操作符后面是一个变量 ingredient,它代表循环当前使用的项目。每次循环迭代时,值都会改变,直到循环遍历完列表中的每一项。第一次执行时,值将是索引位置 0 的项(在这个例子中是 "water"),第二次执行时,值将是索引位置 1 的项("soy sauce"),第三次执行时,值将是索引位置 2 的项("spring onions"),依此类推。

in 操作符和语句末尾的列表名称告诉 Python 你正在使用哪个列表。这个例子中,列表的名称是 noodleSoup

循环会对列表中的每一项执行一次,直到达到列表的末尾才结束。以下是这个程序的输出:

water
soy sauce
spring onions
pepper
noodles
beef
vegetables

列表中的每个项都会被打印出来!现在,让我们在 Minecraft 中用 for 循环来玩得开心吧。

任务 #55:魔法棒

Minecraft 中的每个工具都有其独特的功能。铲子挖掘泥土,镐子打破石块,斧头砍伐木材,而剑则击打敌人。通常,你不能改变工具的行为;你只能接受剑只击打敌人。但使用 Python,你可以改变工具的工作方式。在这个程序中,我们将把剑变成魔法棒。

在 第九章 中,你了解了 pollBlockHits() 函数。这个函数返回一个包含剑击中的方块坐标的列表。通过 for 循环,你可以访问该列表中的每一组坐标。我们将把过去 60 秒内我们打到的所有方块变成西瓜。你可以在 图 10-1 中看到它是如何实现的。

图片

图 10-1:变魔术!我打到的所有方块现在变成了西瓜。

列表 10-1 包含了程序的开头。将其保存为 magicWand.py,并放入一个名为 forLoops 的新文件夹中。

magicWand.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time

   time.sleep(60)

➊ hits = mc.events.pollBlockHits()
   block = 103

➋ for
➌ x, y, z = hit.pos.x, hit.pos.y, hit.pos.z
➍ # Set melon blocks at the coordinates

示例 10-1:魔法棒程序的开始部分

要获取方块击中的列表,我们调用pollBlockHits()函数并将结果存储在hits变量中➊。

代码中包括了一行将获取你击中的方块位置并将其坐标存储在xyz变量中的代码➌。它使用一个元组(在第九章的页面 175 中介绍)来在一行中为三个变量赋值。

目前,这行代码无法工作,因为hit变量并不存在。在➋处创建一个for循环,并将for循环的变量命名为hitfor循环应遍历hits列表。for循环的第一部分代码应如下所示:

for hit in hits:

确保将获取xyz值的代码行缩进到➌的for循环内。在for循环的最后一行,添加setBlock()函数以在 x、y 和 z 坐标➍处设置一个西瓜方块。

当用户运行完成的程序时,他们将有 60 秒钟的时间在四处奔跑,并用剑右键点击尽可能多的方块。60 秒后,所有被剑击中的方块将变成西瓜。

额外目标:你是一个巫师

修改magicWand.py程序,使其传送玩家:第一次击中设置位置,第二次击中将玩家传送到该位置。

range()函数

range()函数创建一个整数列表。它是为for循环快速生成数字列表的好方法。让我们来看一下并传递两个参数,0 和 5,给range()函数:

aRange = range(0, 5)

这是比单独写出每个列表项更快速的创建列表的方法,单独写出的列表项会像这样:

aRange = [0, 1, 2, 3, 4]

注意,range()函数的第二个参数是 5,但列表中的最后一个项是 4。这是因为该函数只生成小于但不等于第二个参数的值。

要创建一个使用range()函数打印从 1 到 15 的数字的循环,你可以使用以下代码:

for item in range(1, 16):
    print(item)

你可以像这样打印出列表中每个项的两倍值:

for item in range(1, 16):
    print(item * 2)

你可以使用while循环做同样的事情,在第七章中你已经学习过while循环。以下代码使用while循环代替for循环,打印从 1 到 15 的数字:

count = 1
while count < 16:
    print(count)
    count += 1

注意到for循环更简单且更易于阅读。在大型复杂的程序中,for循环通常比带有countwhile循环更合适。

任务#56:魔法楼梯

使用 Minecraft 和 Python 的一个最佳特点是,你只需几行代码就能快速构建东西。你不必花费大量时间建造墙壁,只需运行一些代码就完成了。你还可以随时重复使用这些代码,节省时间和精力。

建造楼梯是一个通常需要花费大量时间的任务。幸运的是,通过几行 Python 代码,你可以在 Minecraft 中快速创建楼梯。在这个任务中,你将使用 for 循环让楼梯在游戏世界中出现。

清单 10-2 使用 while 循环在 Minecraft 中创建楼梯。将其保存为 stairs.py 文件,放入 forLoops 文件夹中。

stairs.py

from mcpi.minecraft import Minecraft
mc = Minecraft.create()

pos = mc.player.getTilePos()
x, y, z = pos.x, pos.y, pos.z

stairBlock = 53

step = 0
while step < 10:
    mc.setblock(x + step, y + step, z, stairBlock)
    step += 1

清单 10-2: 使用 while 循环创建楼梯的程序

虽然你可以像这里展示的那样使用 while 循环来实现这个程序,但实际上 for 循环更为合适。与 while 循环不同,for 循环不需要 countstep 变量。相反,你可以使用 range() 函数来确定循环重复的次数。

为了完成程序,将代码修改为使用 for 循环,而不是 while 循环。

你可以在 图 10-2 中看到程序的结果。

额外目标:向下走?

目前,stairs.py 程序只在一个方向上建造楼梯。尝试找出如何在其他方向上建造楼梯。提示:你将使用 setBlock() 函数中的可选块状态参数,并对 xz 变量进行加法或减法操作。

image

图 10-2: 你的魔法楼梯将通向何方?

玩转 RANGE()

你已经了解了 range() 函数以及当你传递两个参数时会发生什么。如果你只传递一个参数会怎样呢?在 IDLE shell 中输入这段代码看看会发生什么:

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

当你只向 range() 函数传递一个参数时,它将从 0 开始,并存储每个值,直到值比你传递的参数小 1。换句话说,就好像你为第一个参数传递了 0,为第二个参数传递了 5。在这个示例中,list() 函数显示了 range() 函数创建的列表值(否则你是看不到它们的!)。如你所见,list(aRange) 的值是一个包含五个数字的列表,从 0 开始:[0, 1, 2, 3, 4]。这是一个快速创建范围的方式,如果你想让第一个值从 0 开始的话。

如你所见,当你向 range() 函数传递两个参数时,列表从第一个参数开始,到第二个参数之前结束:

>>> aRange = range(2, 5)
>>> list(aRange)
[2, 3, 4]

这个示例创建了一个与列表 [2, 3, 4] 等效的范围。

当你给 range() 函数提供三个参数时,第三个参数定义了项与项之间的 步长。通常,range() 函数创建的列表中的每个值比前一个值大 1。通过更改步长,你可以改变项之间的差异。例如,步长为 2 时,列表中的下一个值将比前一个值大 2。步长为 3 时,下一个值比前一个值大 3,依此类推。

例如,这个列表通过将前一个值加 2 来得到下一个值:

>>> aRange = range(3, 10, 2)
>>> list(aRange)
[3, 5, 7, 9]

注意,每个项比前一个项大 2(5 是 3 + 2,7 是 5 + 2,9 是 7 + 2)。

你甚至可以给 range() 函数一个负步长值,像这样:

>>> newRange = range(100, 0, -2)
>>> list(newRange)
[100, 98, 96, 94, 92, 90, 88, 86, 84, 82, 80, 78, 76, 74, 72, 70, 68, 66, 64,
62, 60, 58, 56, 54, 52, 50, 48, 46, 44, 42, 40, 38, 36, 34, 32, 30, 28, 26,
24, 22, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2]

请注意,列表中的值每次减少 2,因为使用了负步长值。

其他列表函数

由于我们正在处理列表,让我们探索一些其他专为与列表交互而设计的函数。

reversed()函数接受一个参数,即你想要使用的列表,并返回反转后的列表。最后一个元素将成为第一个元素,倒数第二个元素将成为第二个元素,依此类推。让我们反转一个先前的列表:

>>> backwardsList = reversed(aRange)
>>> list(backwardsList)
[9, 7, 5, 3]

列表中的项目已被反转,正如我们所期望的那样。这种列表操作在你编写for循环时非常有用。

以下示例使用range()函数生成从 1 到 100 的数字列表。然后,它反转该列表并使用for循环打印出来,从而有效地创建一个从 100 到 1 的倒计时:

countDown = range(1, 101)
countDown = reversed(countDown)
for item in countDown:
    print(item)

运行它以查看输出结果!

100
99
98
97
96
--snip--
3
2
1

你还可以在声明for循环时反转列表,而无需使用变量来存储列表:

for item in reversed(range(0, 101)):
    print(item)

这个程序用更少的代码行实现相同的效果。使用这个技巧节省时间,以便你可以专注于构建!

任务 #57:柱子

在 Minecraft 中建造一个宫殿是不是很酷?因为宫殿应该是宏伟的,所以我们需要一排排高大、威严的柱子。显然,我们不想手动建造它们,所以使用循环来建造它们是最好的解决方案。

我们将创建一个函数来构建柱子,然后在需要时调用该函数。列表 10-3 包含了构建柱子的函数。将其复制到一个名为pillars.py的新文件中,并将其保存在forLoops文件夹中。

pillars.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   def setPillar(x, y, z, height):
       """ Creates a pillar. Args set position and height of pillar """
       stairBlock = 156
       block = 155

       # Pillar top
       mc.setBlocks(x - 1, y + height, z - 1, x + 1, y + height, z + 1, block, 1)
       mc.setBlock(x - 1, y + height - 1, z, stairBlock, 12)
       mc.setBlock(x + 1, y + height - 1, z, stairBlock, 13)
       mc.setBlock(x, y + height - 1, z + 1, stairBlock, 15)
       mc.setBlock(x, y + height - 1, z - 1, stairBlock, 14)

       # Pillar base
       mc.setBlocks(x - 1, y, z - 1, x + 1, y, z + 1, block, 1)
       mc.setBlock(x - 1, y + 1, z, stairBlock, 0)
       mc.setBlock(x + 1, y + 1, z, stairBlock, 1)
       mc.setBlock(x, y + 1, z + 1, stairBlock, 3)
       mc.setBlock(x, y + 1, z - 1, stairBlock, 2)

       # Pillar column
       mc.setBlocks(x, y, z, x, y + height, z, block, 2)

   pos = mc.player.getTilePos()
   x, y, z = pos.x + 2, pos.y, pos.z

➊ # Add the for loop here
➋ # Call the function here

列表 10-3:创建柱子的函数

setPillar()函数用来创建一个柱子。它需要四个参数:x、y 和 z 坐标以及柱子的高度。

要完成程序,添加一个for循环 ➊,它调用setPillar()函数 ➋。我们想要创建一排 20 根柱子,每根柱子相距 5 个方块。为此,使用一个带有三个参数的range()函数来确定将创建多少根柱子,以及它们之间的间隔。通过将for循环中变量的值添加到setPillar()函数调用中的xz变量,你可以确保每根柱子之间的距离相等。

图 10-3 展示了其中一些柱子。

image

图 10-3:一排精美的柱子

任务 #58:金字塔

继续使用for循环构建精彩物品的主题,让我们来建造一个金字塔。金字塔由许多层组成。最底层最宽,顶部最窄。每一层都是由方块组成的。我们将构建一个每层宽度比上一层少两个方块的金字塔。例如,如果底层宽度是七个方块,那么下一层将是五个方块宽,接着是三个方块宽,最后顶层为一个方块宽。

列表 10-4 创建了一个金字塔。将其复制到一个名为pyramid.py的新文件中,并将其保存在forLoops文件夹中。

pyramid.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   block = 24  # sandstone
➊ height = 10
➋ levels = range(height)

   pos = mc.player.getTilePos()
➌ x, y, z = pos.x + height, pos.y, pos.z

➍ for level in levels:
➎     mc.setBlocks(x - level, y, z - level, x + level, y, z + level, block)
       y += 1

示例 10-4:倒金字塔程序

尽管示例 10-4 创建了一个金字塔,但它包含了一个你需要修复的小 bug!我们将金字塔的高度存储在height变量中➊。你可以将height变量的值更改为任何你想要的值。levels变量使用range()函数创建一个列表,每个金字塔的每一层都包含一个元素➋。当我们设置xyz变量时,height变量被加到玩家的 x 坐标上➌。如果我们不这么做,当金字塔建造时,玩家就会被困在金字塔的中心。

for循环会遍历levels列表中的每个level ➍。创建每个金字塔层的代码行使用level变量来计算每个方块的宽度 ➎。每一层金字塔的宽度和长度将始终是level变量的两倍大小。

还记得我之前提到的那个 bug 吗?运行程序看看问题出在哪里。金字塔是倒过来的!

为了解决这个问题并让金字塔恢复正常,你需要对levels变量使用reversed()函数,这样可以生成一个随着时间变小的列表。或者你也可以偷偷使用带负值的range()函数。

图 10-4 展示了完成的金字塔。

image

图 10-4:宏伟的金字塔

遍历字典

你也可以使用for循环遍历字典。当你使用for循环遍历字典时,语法与遍历列表的for循环相同;但是,循环将仅遍历字典的键。

例如,以下代码在每次循环迭代时打印for循环的变量。在这种情况下,它会打印字典中每个项的键:

inventory = {'gems': 5, 'potions': 2, 'boxes': 1}

for key in inventory:
    print(key)

这段代码输出如下:

gems
potions
boxes

要打印字典中每个项的值,你需要使用dictionary[key]语法。以下是如何修改代码,以便它打印每个项的值和键:

inventory = {'gems': 5, 'potions': 2, 'boxes': 1}

for key in inventory:
    print(key + " " + str(inventory[key]))

这个示例现在输出如下:

gems 5
potions 2
boxes 1

注意,这个输出比字典本身更容易阅读。通过使用循环输出字典的值,你可以更好地控制信息的显示方式。

任务 #59:记分板

回想一下 swordHitsScore.py 游戏,来自任务 #54(第 192 页)。这个游戏记录了玩家在一分钟内击中的方块数量。分数和玩家名字被存储在一个字典中。虽然程序本身运行得很好,但程序结尾的记分板没有以非常易读的格式输出分数和名字。它只是简单地打印了一个没有任何格式化的字典。

为了改进程序,在这个任务中,你将修改swordHitsScore.py,使其以易读的格式输出scoreboard字典。为此,你将使用for循环。

打开你的 swordHitsScore.py 程序(它应该在 lists 文件夹中),并将其保存为 scoreBoard.py,放在 forLoops 文件夹中。在程序中,找到并删除这一行:

print(scoreboard)

将这一行替换为一个 for 循环,用来打印每个玩家的名字和分数。这些值存储在 scoreboard 字典中:每个玩家的名字是字典中的键,分数是键的值。

图 10-5 显示了更新后的输出。

image

图 10-5:程序的输出现在更容易阅读了。

FOR-ELSE 循环

你也可以在 for 循环中使用 else 语句。当你在 for 循环中使用 else 时,它会在 for 循环遍历完列表后执行。如果 for 循环没有遍历到列表的末尾,else 语句将不会执行。

例如,这里有一段代码,它打印三明治的成分,然后使用了一个 else 语句:

sandwich = ["Bread", "Butter", "Tuna", "Lettuce", "Mayonnaise", "Bread"]

for ingredient in sandwich:
    print(ingredient)
else:
    print("This is the end of the sandwich.")

当你运行这段代码时,它会输出以下内容:

Bread
Butter
Tuna
Lettuce
Mayonnaise
Bread
This is the end of the sandwich.

你可能会认为这段代码与以下写法是一样的:

for ingredient in sandwich:
    print(ingredient)
print("This is the end of the sandwich.")

是的,确实如此。两段代码的效果是一样的。那么,使用 elsefor 循环有什么意义呢?其实,当与 break 语句一起使用时,else 语句的行为会有所不同。接下来我们来看看这个情况。

中断 FOR-ELSE 循环

使用 break 语句退出 for 循环是一种防止 else 语句执行的方法。

以下示例在 if 语句中加入了 break 语句。如果当前项是 "Mayonnaise",循环将会中断:

sandwich = ["Bread", "Butter", "Tuna", "Lettuce", "Mayonnaise", "Bread"]

for ingredient in sandwich:
    if ingredient == "Mayonnaise":
        print("I don't like mayonnaise on my sandwich.")
        break
    else:
        print(ingredient)
else:
    print("This is the end of the sandwich.")

你能预测输出结果吗?在运行这段代码之前想一想,然后运行代码看看会发生什么。

任务 #60:钻石勘探者

有时在和朋友一起玩《Minecraft》时,他们不让我用 Python 程序来生成钻石方块。但我仍然需要钻石来制作盔甲、工具以及建造钻石城堡。直接向下挖掘钻石很容易,但并不总是能找到。

为了节省时间,我编写了一个程序,检查我正下方是否有钻石矿石。程序获取我的当前位置,然后使用一个 for 循环逐一检查我下方的方块,看看它们是否是钻石矿石。如果发现钻石矿石,程序会告诉我矿石的深度;如果没有发现钻石矿石,程序会发布一条消息,告诉我下方没有钻石矿石。

创建一个新程序,并将其保存为 diamondSurvey.py,放在 forLoops 文件夹中。

使用 for 循环,每次循环迭代时,将 y 变量的值减少 1。循环应该总共执行 50 次,以检查 50 个方块的深度。对于每次迭代,使用 if 语句检查该位置的方块是否是钻石矿石(方块 ID 56)。如果是钻石矿石方块,向聊天中发送消息,告诉玩家该方块距离自己有多远,并退出循环。如果没有找到钻石矿石方块,使用 else 语句在 for 循环中发送消息,告知玩家下面没有钻石矿石方块。

图 10-6 显示了正在工作的程序。

image

图 10-6:看起来像是有一个钻石矿石块在我下面四个方块的位置。是时候开始挖掘了!

附加目标:那些山里的黄金

修改 diamondSurvey.py 程序,使其也能寻找其他矿石块,比如铁矿石或金矿石。

嵌套的 FOR 循环和多维列表

在你的程序中,你可以将多个列表结合使用,达到多种目的。你可以将列表包含在其他列表中,这些被称为多维列表。在本节中,我们将使用二维(2D)和三维(3D)列表来在 Minecraft 中构建结构。

在二维中思考

你已经学会了如何编写列表,特别是一维列表。它们被称为一维列表,因为列表中的每个位置只包含一个项目。

例如,看看下面这个列表,叫做 oneDimensionalRainbowList。这个列表的格式稍有不同,旨在强调每个位置只包含一个项目;否则,它与其他你工作过的列表相同:

oneDimensionalRainbowList = [0,
                             1,
                             2,
                             3,
                             4,
                             5]

这个列表中有六个项目:数字 0 到 5。列表中的每个项目只有一个值,因此这个列表是一个一维列表。

清单 10-5 在 Minecraft 中显示了这个列表,作为羊毛方块。程序文件 rainbowStack1.py 可在书本的资源中找到。你可以从 www.nostarch.com/pythonwithminecraft/ 下载代码文件,或者自己输入并一起玩!

rainbowStack1.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

➊ oneDimensionalRainbowList = [0, 1, 2, 3, 4, 5]

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z

➋ for color in oneDimensionalRainbowList:
       mc.setBlock(x, y, z, 35, color)
       y += 1

清单 10-5:构建一个彩虹堆叠的方块

程序首先创建一个方块颜色的列表➊,然后使用for循环基于列表中的颜色来创建一个颜色堆叠的羊毛方块➋。

当你运行程序时,你会得到一个单一的羊毛方块堆叠,如图 10-7 所示。注意,这个堆叠有六个方块高和一个方块宽。你在本书中一直使用的xyz变量,每一个都可以被称为维度。这个程序在 y 维度上创建了一个六个方块高的堆叠。通过在代码的最后一行更改x变量,而不是y变量,你可以在 x 维度上创建一个方块堆叠,正如你在图 10-8 中看到的那样。

image

图 10-7:由 rainbowStack1.py 创建的彩虹方块堆叠

image

图 10-8:在程序的最后一行将 y 变量替换为 x 变量,会在水平方向上构建方块。

由于列表是一维的,你一次只能更改一个维度上的一个变量。换句话说,你可以更改y变量、x变量或z变量,但不能一次性更改所有这些变量。

现在是时候开始考虑二维了!一维列表允许你在每个位置只存储一个值,而二维列表允许你在每个位置存储多个值。你可以通过在原始列表的每个位置放置一个列表来实现,如下所示。

➊ twoDimensionalRainbowList = [[0, 0, 0],
➋                              [1, 1, 1],
➌                              [2, 2, 2],
                               [3, 3, 3],
                               [4, 4, 4],
➍                              [5, 5, 5]]

仔细观察,你会看到第一行有一个开括号,后面跟着一个充满零的列表,然后是一个逗号 ➊。那是一个列表里面嵌套了另一个列表!我们可以称外部的列表为外层列表,并说它包含嵌套列表

在索引位置 1 是一个包含三个 1 的列表 ➋。在索引位置 2 是另一个列表,这个列表包含三个 2 ➌。这个模式在每一行中都会重复。最后一行是一个包含三个 5 的列表,后面跟着一个方括号,关闭了外层列表 ➍。这段代码展示了一个包含六个项的列表,每一项本身也是一个列表。这就是一个二维列表!

当你在 Minecraft 中使用二维列表时,你会更好地理解它们。我们来看一个例子。通过修改rainbowStack1.py,我们可以让它与二维列表一起工作。这个新程序被命名为rainbowRows.py

rainbowRows.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   twoDimensionalRainbowList = [[0, 0, 0],
                                [1, 1, 1],
                                [2, 2, 2],
                                [3, 3, 3],
                                [4, 4, 4],
                                [5, 5, 5]]

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z

➊ startingX = x

➋ for row in twoDimensionalRainbowList:
➌     for color in row:
➍         mc.setBlock(x, y, z, 35, color)
➎         x += 1
➏     y += 1
➐     x = startingX

在我解释代码之前,请查看图 10-9,以查看rainbowRows.py的输出,这是一组在 y 维度上有六个方块高,在 x 维度上有三个方块宽的方块。

image

图 10-9:使用二维列表制作彩虹墙

因为我们正在处理二维数据,所以需要两个for循环来输出twoDimensionalRainbowList列表中的值。第一个循环遍历外层列表中的每个项 ➋。第二个循环 ➌,因为它在另一个循环内,所以被称为嵌套循环,然后遍历每个嵌套列表中的项。

例如,外层循环第一次运行时,它会获取twoDimensionalRainbowList列表中索引位置为 0 的项,并将其存储在一个名为row的变量中 ➋。row的值是[0, 0, 0],因为这是列表中的第一个项。

第二个循环遍历row列表中的每个项目,并将其存储在color变量中 ➌。在这个例子中,每个项目的值将是 0。程序然后使用color变量来设置每个羊毛块的颜色 ➍。嵌套循环在放置完该行的所有三个块后结束,然后外部循环再次运行。接下来,外部循环移动到索引位置 1,并将该位置的值存储在row变量中,此时row的值是[1, 1, 1]。然后再次执行嵌套循环来设置块,并继续迭代,直到到达twoDimensionalRainbowList列表的末尾。

在处理二维数据时,你可以同时改变两个坐标变量。在这个例子中,我们在外部for循环的倒数第二行 ➏ 增加y变量,使得每一行的块都会放置在上一行的上方。我们还在嵌套的for循环中增加x变量 ➎,以确保块按行排列。然后,每次外部for循环迭代时,我们需要将x变量重置为它的原始值(该值存储在startingX变量 ➊ 中)。重置x变量使得每一行的第一个块直接放置在上一行第一个块的正上方,依此类推,因此行与行之间能够正确对齐。

访问二维列表中的值

在获取或设置一维列表中的值时,你只需要使用方括号和索引位置。例如,这段代码创建了一个名为scores的列表来记录玩家的得分,然后它将索引位置 2 处的项从 6 改为 7:

scores = [1, 5, 6, 1]
scores[2] = 7

使用或更改二维列表中的值并没有太大不同。你仍然使用方括号和索引位置,但因为你同时访问两个列表,所以需要使用两组索引和方括号。我们来看一下!

这是你之前看到的列表:

twoDimensionalRainbowList = [[0, 0, 0],
                             [1, 1, 1},
                             [2, 2, 2],
                             [3, 3, 3],
                             [4, 4, 4],
                             [5, 5, 5]]

如果我们想将第一个列表(索引位置 0)中第二个项目(索引位置 1)的值改为 7,我们可以使用以下代码:

twoDimensionalRainbowList[0][1] = 7

因为我们使用了两个列表,并且一个列表嵌套在另一个列表中,所以我们需要使用两组方括号。第一组方括号选择twoDimensionalRainbowList列表的索引位置 0,这是它的第一个嵌套列表。在第二组方括号中,我们放入想要访问的嵌套列表中的索引位置 1。然后,我们使用等号将该位置的值设置为 7。

我将这段代码添加到rainbowRows.py程序中(第 211 页),并重新运行。 图 10-10 显示了结果。注意到第一行的第二个块已经改变,因为我们将嵌套列表中的值改为 7。

如果你想获取二维列表中某个项目的值,也需要使用两组方括号。例如,如果你想打印最后一行(索引 5)中第一个位置(索引 0)的值,你可以使用以下代码:

print(twoDimensionalRainbowList[5][0])

这段代码输出值 5。

image

图 10-10:更改嵌套列表中的一个值以获得不同的结果

任务 #61:像素艺术

像素是构成你计算机上图像的单色方块。通过将许多像素组合成网格,你的计算机可以显示文本、图像、视频以及显示在屏幕上的其他所有内容。你计算机上的所有照片和绘画都是通过像素显示的。

像素艺术在《Minecraft》中非常流行。玩家通过使用不同颜色的方块在《Minecraft》中构建图片。在二维视频游戏中的人物图片是最受欢迎的之一。你可以手工创建像素艺术,当然,也可以使用 Python 程序生成像素艺术。

在这个程序中,你将使用二维列表和嵌套循环在《Minecraft》中创建像素艺术。列表 10-6 包含程序的开始部分。将其复制到一个名为 pixelArt.py 的新文件中,并将其保存在 forLoops 文件夹中。

pixelArt.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   pos = mc.player.getTilePos()
   x, y, z = pos.x, pos.y, pos.z

➊ blocks = [[35, 35, 35, 35, 35, 35, 35, 35],
             [35, 35, 35, 35, 35, 35, 35, 35],
             [35, 35, 35, 35, 35, 35, 35, 35],
             [35, 35, 35, 35, 35, 35, 35, 35]]

➋ for row in reversed(blocks):
       for block in row:
           mc.setBlock(x, y, z, block)
           x += 1
       y += 1
       x = pos.x

列表 10-6:绘制笑脸的二维列表

该程序创建了一个名为 blocks 的二维列表,其中包含方块 ID ➊,然后使用两个循环将方块放置到《Minecraft》世界中 ➋。为了确保当列表被放置到《Minecraft》时,列表的第一行位于顶部,最后一行位于底部,第一 for 循环中包含了 reversed() 函数 ➋。如果没有这个函数,图像的顺序将与 blocks 列表的顺序相反,显示成倒置的样子。

目前,所有的方块都是白色羊毛方块,并且没有显示图片。为了完成程序,你需要重写二维方块列表,使其绘制一个笑脸,如图 10-11 所示。

image

图 10-11:用方块绘制的笑脸

更改列表中的一些值,使输出与图 10-11 匹配。你需要将列表中的一些羊毛方块(方块 ID 35)更改为青金石方块(方块 ID 22)。例如,将第一行更改为以下内容:

blocks = [[35, 35, 22, 22, 22, 22, 35, 35],

你还需要向 blocks 列表中添加更多的行,以便图像的高度与图片中的高度匹配。

额外目标:自己绘制

尝试更改 pixelArt.py 中二维列表的值,以显示不同的图片。你还可以改变列表的长度。先在方格纸上画出设计图。然后将其转换为二维列表,以便你可以在《Minecraft》中创建它们!

使用循环生成二维列表

使用随机数的程序很有趣,因为每次运行时它们的行为都会不同。在过去,我创建了很多使用随机数的程序,它们利用二维列表来创建图片。每个随机数可能显示一个颜色,或者在《Minecraft》中,显示一个不同的方块。

这是一个生成随机数并将其存储在二维列表中的程序的开头:

   import random
➊ randomNumbers = []
   for outer in range(10):
➋     randomNumbers.append([])
       for inner in range(10):
➌         number = random.randint(1, 4)
           randomNumbers[outer].append(number)
   print(randomNumbers)

程序从一个空的列表 randomNumbers 开始 ➊。每当外层的 for 循环重复时,它会将一个新的空列表添加到 randomNumbers 列表中 ➋。在内层循环中,程序会生成一个介于 1 和 5 之间的随机数,并将其存储在内层列表中 ➌。内层循环重复 10 次,为每个内层列表生成 10 个项目。

为了提高可读性,我们添加了换行符,程序的输出如下所示(请注意,10 个内层列表中有 10 个项目):

[[3, 1, 4, 1, 4, 1, 2, 3, 2, 2],
 [1, 3, 4, 2, 4, 3, 4, 1, 3, 2],
 [4, 2, 4, 1, 4, 3, 2, 3, 4, 4],
 [1, 4, 3, 4, 3, 4, 3, 3, 4, 4],
 [3, 1, 4, 2, 3, 3, 3, 1, 4, 2],
 [4, 1, 4, 2, 3, 2, 4, 3, 3, 1],
 [2, 4, 2, 1, 2, 1, 4, 2, 4, 3],
 [3, 1, 3, 4, 1, 4, 2, 2, 4, 1],
 [4, 3, 1, 2, 4, 2, 2, 3, 1, 2],
 [3, 1, 3, 3, 1, 3, 1, 4, 1, 2]]

通过将随机数融入到你的二维 Minecraft 创作中,你可以创建一些非常酷的效果,而这些效果是手动创建难以实现的!

任务 #62:风化的墙壁

当我在 Minecraft 中建造墙壁时,我不会使用单一的方块类型。通过将一些圆石方块换成苔石圆石方块,我可以把一堵普通的墙变成看起来受损、风化、有机且酷的墙壁。虽然手动建墙很有趣,但我永远无法让我随机添加的方块看起来足够随机。你可能已经猜到,让破损墙壁看起来更随机的解决方案就是使用 Python 程序。

要使用 Python 生成风化墙壁,你需要将程序分解为两个主要步骤:

  1. 创建一个二维列表,并将方块值存储在该列表中。

  2. 将二维列表输出到 Minecraft 世界中。

为了帮助你入门,清单 10-7 包括了选择随机方块值、设置列表和获取玩家位置的代码。将清单复制到一个名为 brokenWall.py 的新文件中,并保存在 forLoops 文件夹中。

brokenWall.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import random

➊ def brokenBlock():
       brokenBlocks = [48, 67, 4, 4, 4, 4]
       block = random.choice(brokenBlocks)
       return block

   pos = mc.player.getTilePos()
   x, y, z = pos.x, pos.y, pos.z

   brokenWall = []
   height, width = 5, 10

   # Create the list of broken blocks

   # Set the blocks

清单 10-7:创建破损墙壁的程序开始部分

brokenBlock() 函数返回一个随机方块值,用于构建墙体 ➊。widthheight 变量设置墙体的宽度和高度。

要完成程序,你需要生成一个二维的方块值列表,然后使用这些值在 Minecraft 中构建设计。

从空列表 brokenWall 开始。使用嵌套在另一个 for 循环中的 for 循环,利用 brokenBlock() 函数生成随机方块值。将方块值存储在列表中,并将这些列表存储在 brokenWall 列表中。然后使用另一组嵌套循环将方块放置到 Minecraft 中。

当程序完成后,前往 Minecraft 世界中你想要建造风化墙壁的地方,运行代码。你可以使用该程序装饰城堡,或在森林中创建看起来阴森的废墟。尝试不同的位置,看看你最喜欢哪一个!图 10-12 显示了运行程序后墙壁的样子。

image

图 10-12:一个随机生成破损方块的墙壁。看起来像是被幽灵附身了!

额外目标:创建一个五彩斑斓的墙壁

brokenWall.py程序中,修改brokenBlock()函数中brokenBlocks列表中的方块值,可以创建各种墙壁。试试把方块值改成不同颜色的羊毛,看看会发生什么!

三维思维

当然,Minecraft 是一个使用三维空间的游戏。在本书中,你也使用了三维空间。你在大多数程序中使用的xyz变量代表了一个维度。

你已经学会了如何将一个列表嵌套到另一个列表中,从而得到二维列表,并创建炫酷的像素艺术和风化墙壁。将第三组列表嵌套到二维列表中,就能创建三维列表,这可以让你将建筑技能提升到全新的维度!

三维列表在 Minecraft 中非常有用,因为你可以用它们来复制三维结构,比如建筑、雕塑以及许多其他东西。

清单 10-8 中的三维列表有四个嵌套在其中的列表。值得注意的是,在这些嵌套列表的每个索引中,都有一个列表!基本上,这个列表中的每个元素都是一个二维列表。我已添加空白注释,以便使列表更易于阅读。

cube = [[[57, 57, 57, 57],
         [57, 0, 0, 57],
         [57, 0, 0, 57],
         [57, 57, 57, 57]],
         #
        [[57, 0, 0, 57],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [57, 0, 0, 57]],
        #
        [[57, 0, 0, 57],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [57, 0, 0, 57]],
        #
        [[57, 57, 57, 57],
         [57, 0, 0, 57],
         [57, 0, 0, 57],
         [57, 57, 57, 57]]]

清单 10-8:一个包含嵌套列表的三维列表

这样的代码可以用来创建一个酷炫的立方体结构!接下来,我们将深入研究一个正好做到这一点的程序。

输出三维列表

三维列表非常适合存储有关三维物体的数据,比如你在 Minecraft 中的那些炫酷建筑。存储三维物体很重要,而将它们正确输出到 Minecraft 中同样重要。因为三维列表是一个嵌套在列表中的列表,所以你可以使用一个for循环嵌套在另一个for循环中,再嵌套在第三个for循环中,以访问所有数据。换句话说,你可以使用三个嵌套的for循环。

在清单 10-9 中,我复制了清单 10-8 中的三维列表,并创建了一个名为cube.py的程序。这个程序使用了三个嵌套的for循环,逐个输出三维列表中的所有值,以在 Minecraft 世界中构建一个立方体结构。

cube.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z
   cube = [[[57, 57, 57, 57], [57, 0, 0, 57], [57, 0, 0, 57], [57, 57, 57, 57]],
           [[57, 0, 0, 57], [0, 0, 0, 0], [0, 0, 0, 0], [57, 0, 0, 57]],
           [[57, 0, 0, 57], [0, 0, 0, 0], [0, 0, 0, 0], [57, 0, 0, 57]],
           [[57, 57, 57, 57], [57, 0, 0, 57], [57, 0, 0, 57], [57, 57, 57, 57]]]

   startingX = x
➊ startingY = y
➋ for depth in cube:
       for height in reversed(depth):
           for block in height:
               mc.setBlock(x, y, z, block)
               x += 1
           y += 1
           x = startingX
➌     z += 1
➍     y = startingY

清单 10-9:用来创建由钻石构成的三维立方体的代码

图 10-13 展示了这个程序的结果。

image

图 10-13:cube.py程序创建的立方体

cube.py中的代码与用于构建彩虹墙的二维rainbowRows.py程序非常相似(见第 211 页)。主要区别在于,cube.py使用了三个for循环,而不是两个,因为它处理的是三维列表。额外的for循环为结构增加了一个额外的维度,即深度➋。所以,现在结构有了宽度、高度和深度。

每次外层循环for depth in cube运行时,它会通过两个嵌套循环for height in reversed(depth)for block in height创建一个二维列表。这两个嵌套循环中的代码与rainbowRows.py程序中的代码类似,这意味着这些循环在 Minecraft 中建造了一堵墙。

让我们看看每次外层循环重复时的结果,这样我们就能一步步看到立方体的构建过程。第一次外层循环运行时,它会输出cube列表中索引位置 0 的方块。该列表如下所示:

[[57, 57, 57, 57],
 [57, 0, 0, 57],
 [57, 0, 0, 57],
 [57, 57, 57, 57]]

图 10-14 展示了输出结果:我们的第一堵块墙。

image

图 10-14:第一个二维循环的结果, cube 索引 0

每次游戏中构建完一个二维列表后,cube.py ➌中的z变量会增加,沿 z 轴移动一个方块。这赋予了立方体深度,这样我们就不仅仅是在建造一堵墙。此外,我们还需要将y变量的值在➍时重置为其原始值 ➊,这样每次外层循环重复时,立方体底部的方块就能对齐。如果y变量没有被重置,每组方块的 y 坐标将不断增高,最终会形成一些奇怪的阶梯!图 10-15 展示了这种情况。

image

图 10-15:我们重置了 y 变量,所以这种情况不会发生!

第二次外层循环运行时,它会输出cube列表中索引位置 1 的块,结果如下:

[[57, 0, 0, 57],
 [0, 0, 0, 0],
 [0, 0, 0, 0],
 [57, 0, 0, 57]]

这添加了立方体的下一部分,如图 10-16 所示。在这部分立方体构建完成后,z变量增加了 1 ➌,y变量再次被重置为原始值 ➍。

image

图 10-16:第二个二维循环的结果, cube 索引 1

下一次循环重复时,它会输出cube列表中索引位置 2 的二维列表:

[[57, 0, 0, 57],
 [0, 0, 0, 0],
 [0, 0, 0, 0],
 [57, 0, 0, 57]]

图 10-17 展示了结果。同样,z值增加了 1,y值被重置。

然后循环再次运行第四次并最后一次,输出cube的索引位置 3:

[[57, 57, 57, 57],
 [57, 0, 0, 57],
 [57, 0, 0, 57],
 [57, 57, 57, 57]]

图 10-18 展示了完成的立方体结构。

image

图 10-17:第三个二维循环的结果, cube 索引 2

image

图 10-18:最后一个二维循环的结果,位于最终的 cube 索引位置

尝试一下这个程序——使用不同的方块类型,尝试构建更大的立方体,或者尝试任何你能想象到的变化!在下一节中,我将向你展示如何访问三维列表中的值,这样你就可以做出这些改变。

访问三维列表中的值

三维列表中的值可以像一维和二维列表一样,通过使用方括号和索引位置进行修改。

让我们从三维钻石立方体列表开始:

cube = [[[57, 57, 57, 57],
         [57, 0, 0, 57],
         [57, 0, 0, 57],
         [57, 57, 57, 57]],
         #
        [[57, 0, 0, 57],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [57, 0, 0, 57]],
         #
        [[57, 0, 0, 57],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [57, 0, 0, 57]],
         #
        [[57, 57, 57, 57],
         [57, 0, 0, 57],
         [57, 0, 0, 57],
         [57, 57, 57, 57]]]

我想把我立方体前面的底部左侧方块改为金色。

首先,我需要访问包含立方体前面的立方体列表的索引,那个索引是 0。所以表达式的第一部分将像这样:

cube[0]

如果我打印这个表达式的值,我将得到如下输出(我已将其格式化以便更易阅读):

[[57, 57, 57, 57],
 [57, 0, 0, 57],
 [57, 0, 0, 57],
 [57, 57, 57, 57]]

这个二维列表表示立方体的前面。接下来,我想访问最底部的行,也就是索引 3。于是我在表达式中加上 [3]

cube[0][3]

如果我打印存储在这个位置的列表,我将得到如下结果:

[57, 57, 57, 57]

最后,我想访问这一行中最左边的方块,它的索引是 3。所以最终的表达式将是这样,用来将底部左侧的方块改为金色方块:

cube[0][3][3] = 41

当我运行带有这个代码行的 cube.py 程序时,我得到一个由钻石制成的立方体,中间有一个单一的金色方块,如 图 10-19 所示。

image

图 10-19:修改后的立方体,角落处有一个金色方块

任务 #63: 复制一个建筑

尽管使用 Python 程序在 Minecraft 中建造物品能节省大量时间,但如果你像我一样,可能仍然会花费相当多的精力在建筑中添加细节,比如图片和家具。有时你可能需要制作一个特定物品的完全复制品,而手动复制一个物品可能需要花费很多时间。一个一个地放置方块也很繁琐,而且你可能会把方块放错位置。显然的解决方案是制作一个程序,它能在 Minecraft 中复制一个建筑,并在游戏中为你建造出一个副本!

完成的程序需要做两件事:首先,它会复制游戏中的一个区域,并将其存储在一个三维列表中;然后,它将使用这个三维列表来建造复制的结构。

我已经将程序的开头部分包括在 清单 10-10 中,以帮助你。将清单复制到一个新文件中,并将其保存为 duplicateArea.pyforLoops 文件夹中。

duplicateArea.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

➊ def sortPair(val1, val2):
       if val1 > val2:
           return val2, val1
       else:
           return val1, val2

➋ def copyStructure(x1, y1, z1, x2, y2, z2):
       # Sort the highest and lowest x, y, and z values
       x1, x2 = sortPair(x1, x2)
       y1, y2 = sortPair(y1, y2)
       z1, z2 = sortPair(z1, z2)

       width = x2 - x1
       height = y2 - y1
       length = z2 - z1

       structure = []

       print("Please wait...")

➌     # Copy the structure

       return structure

➍ def buildStructure(x, y, z, structure):
       xStart = x
       yStart = y

➎     # Build the structure

   # Get the position of the first corner
➏ input("Move to the first corner and press enter in this window")
   pos = mc.player.getTilePos()
   x1, y1, z1 = pos.x, pos.y, pos.z

   # Get the position of the second corner
➐ input("Move to the opposite corner and press enter in this window")
   pos = mc.player.getTilePos()
   x2, y2, z2 = pos.x, pos.y, pos.z

   # Copy the building
➑ structure = copyStructure(x1, y1, z1, x2, y2, z2)

   # Set the position for the copy
➒ input("Move to the position you want to create the structure and press ENTER 
          in this window")
   pos = mc.player.getTilePos()
   x, y, z = pos.x, pos.y, pos.z
   buildStructure(x, y, z, structure)

清单 10-10:当程序完成时,它将复制建筑。

这个程序分为几个部分。首先,sortPair() 函数 ➊ 将一对值排序,较小的值放在第一个索引位置,较大的值放在第二个索引位置。例如,如果我给 sortPair() 传入 9 和 3,它会返回一个元组 (3, 9),因为 3 小于 9。我使用这个函数来对 xyz 的值进行排序,这样在计算时,widthlengthdepth 变量始终为正值。

接下来,copyStructure() 函数 ➋ 从游戏世界中复制结构,但它尚未完成 ➌。buildStructure() 函数 ➍ 用来构建结构,但它也未完成 ➎。你将在本任务中完成这两个函数。

我添加了一个巧妙的技巧,来获取你想要复制的建筑的坐标和你希望复制建筑物的位置:使用input()函数,程序首先要求你将角色移动到建筑物的一个角落并按 ENTER 键 ➏。input()函数会使程序等待,直到你将玩家移动到你希望他们停留的位置。一旦按下 ENTER 键,它会使用getTilePos()函数获取玩家的位置。我们在建筑物的对角线另一端再次执行相同的操作 ➐。然后,copyStructure()函数使用这两组坐标来复制建筑物 ➑。(当复制较大的结构时,这部分程序可能需要一些时间才能运行。)最后,你移动到你希望建筑物建造的地方并按 ENTER 键 ➒,将玩家的最后位置传递给buildStructure()函数。

为了完成程序,你需要完成copyStructure()buildStructure()函数。在copyStructure()函数中添加三个嵌套循环,以将参数中给出的坐标之间的所有方块复制到一个三维列表中 ➌。为了完成buildStructure()函数,添加三个嵌套的for循环,从三维列表中输出方块值 ➎。该函数应该使用给定坐标作为参数。

确保程序能够处理结构内部的xyz坐标。使用for循环更改xyz的位置。

虽然duplicateArea.py是一个较长的程序,但它非常有用,值得付出努力。完成这个任务后,你将能够在 Minecraft 世界中建造整个城市!我使用duplicateArea.py复制了一个我在探索时发现的有趣悬崖。图 10-20 展示了我想要复制的悬崖。

image

图 10-20:我喜欢这个悬崖的外观,所以我做了一个复制。

使用duplicateArea.py程序进行复制时,首先站在物体的外面(如果你的结构是建筑物的话),靠近你想要复制的物体的底部角落。然后在 IDLE 中按 ENTER 键。图 10-21 展示了我站在第一个角落的样子。

image

图 10-21:首先我移动到结构的一个角落并在 IDLE 中按下 ENTER 键。

接下来,飞到你想要复制的物体的对角线另一端,并按第二次 ENTER 键。图 10-22 展示了我已经飞到空中,并围绕悬崖周围移动。

image

图 10-22:然后我移动到结构的对角线另一端并按下 ENTER 键。

程序会发送一条消息,提示你稍等片刻,因为结构正在被复制。移动到你想要构建复制品的位置,然后等待一条消息,询问你希望在何处建造新的结构(图 10-23)。

image

图 10-23:我等了一会儿让结构复制完成。然后我移动到想要构建复制品的位置,按下 ENTER 键来构建它。

当你到达合适的位置时,按下 ENTER 键,复制品就会直接在你面前建成!图 10-24 展示了我复制的悬崖。

image

图 10-24:原始悬崖的复制品!

你学到了什么

本章内容涵盖了很多内容。你学会了如何使用for循环与列表,并且了解了如何使用range()函数。你还学习了更多关于for循环和列表的内容,例如反转列表、循环遍历字典以及如何中断for循环。你通过嵌套循环创建了二维和三维列表,这对构建一个精彩的 Minecraft 世界非常有用。

从生成楼梯和金字塔到复制结构和创建艺术品,你现在对 Minecraft 的控制能力比以往任何时候都更强了。本章中的程序是我在书中最喜欢的一些程序,它们将帮助你创建自己的高级项目!

本章和第九章重点讲解了列表和for循环,它们是密切相关的。在第十一章中,你将进入文件和模块的内容,这些内容与函数密切相关,而你在整本书中都在使用函数。作为任务的一部分,你将学会如何从文件中保存和加载结构。

第十一章:11

使用文件和模块保存与加载建筑物

image

文件是计算机中的一个重要部分。它们允许你保存数据进行长期存储,并加载数据用于程序中。到目前为止,你所使用的程序仅将数据存储在变量中。你可能在程序中使用了硬编码的数据,或者从用户输入中获取数据。虽然你可以使用这些数据做出惊人的事情,但你的操作仅限于单一会话和你自己的 Minecraft 世界。一旦你学会了如何存储和检索数据,你就可以将你的 Minecraft 创作保存到任何 Minecraft 世界中,甚至是你朋友的游戏里!

在本章中,你将学习如何从文件中获取输入以及如何将数据输出到文件。你将使用一些 Python 的内置函数,并学习如何使用两个 Python 模块,pickleshelve模块,来存储整个 Minecraft 创作。

模块可以扩展 Python 的功能。通过使用模块,你可以在屏幕上绘制图像或运行网站。模块还提供了执行常见任务的函数,因此你不需要自己编写解决方案。

你还将学习如何使用 pip,这是一个非常有用的程序,用于安装新的模块。你将通过使用Flask模块来创建一个简单的网站,该网站可以连接到 Minecraft 并显示玩家的位置。

使用文件

在使用计算机时,你总是与文件打交道。每次你写文本文件或 Python 代码并保存时,你都在与文件打交道。文本、图片、视频和音乐都是文件!甚至这本书在我写的时候也存储为一个文本文件。Python 的文件处理功能很容易学习,它能让你创建文件、保存文件并从文件中读取信息,从而在 Minecraft 中做出酷炫的事情。让我们从基础开始,学习如何在 Python 中读取和写入文本文件。

打开文件

打开文件是你在与文件打交道时的第一步。要在 Python 中打开文件,你使用open()函数,该函数接受两个参数:文件的位置和权限。文件的位置是文件在你计算机上存储的路径。你将以字符串形式将文件位置传递给open()函数。文件的权限控制 Python 是否被允许读取或修改该文件。

要在 Python 中打开(或创建)一个名为secretFile.txt的文本文件,你需要使用参数"secretFile.txt"

secretFile = open("secretFile.txt", "w")

第二个参数,"w",是权限参数,指定程序对文件可以执行的操作。在这个例子中,w表示程序可以向文件secretFile.txt写入数据。

当程序调用open()函数并提供一个文件名时,Python 首先检查是否已经存在该文件。如果文件存在,Python 将在程序中使用该文件的内容。如果文件不存在,Python 将创建一个新文件。

如果你没有在文件名中指定目录(文件夹和目录是同一回事),Python 将会在程序所在的目录中查找文件。如果文件存储在其他目录中,你必须在参数中指定该目录。例如,如果secretFile.txt 位于secrets 目录中,第一个参数应该是"/secrets/secretFile.txt"

secretFile = open("/secrets/secretFile.txt", "w")

如果你在参数中提供了一个目录,但该目录不存在,或者文件不存在,你将收到一条错误消息。

权限参数有四种选择:

w 这意味着仅写。仅写权限允许程序将新数据写入文件并覆盖文件中已有的内容,但程序无法读取文件的内容。如果提供的文件名没有对应的文件,程序将创建一个新文件。

r 这意味着仅读。仅读权限允许程序读取文件的内容,但程序不能修改文件的内容。此权限无法用于创建新文件。

r+ 这意味着读写。读写权限允许程序读取和更改文件的内容。程序还可以覆盖文件中已存在的任何内容。但是,如果文件不存在,将不会创建一个新文件;相反,你会收到一个错误。

a 这代表追加。追加权限允许程序仅将新数据写入文件的末尾,而保留文件中的其他内容不变。程序也无法读取文件的内容。这个权限可以用来创建新文件。

在不同的情况下,你会使用不同的权限类型。假设你写下了通往一个极棒的钻石矿的路线,你想将这些路线加载到 Minecraft 中,而不小心更改它们。在这种情况下,你会希望使用仅读权限,确保文件中的内容不会发生变化。或者,如果你希望某人能够向文件添加数据,但不希望他们看到文件中的其他数据,你可以使用追加权限。例如,如果你想让朋友们在共享旅行日志中添加笔记,而不让他们看到你所有关于秘密宝藏的内容,你可以使用追加权限!

接下来,你将学习如何向打开的文件写入数据,并学习如何关闭该文件以便稍后使用这些数据。

写入并保存文件

write() 函数将数据写入程序已打开的文件。这是操作文件时的核心功能,因为它可以让你保存各种数据。你将希望写入文件的数据作为参数传递给 write() 函数。

例如,让我们打开一个文件并向其中写入一个简单的字符串:

   secretFile = open("secretFile.txt", "w")
   secretFile.write("This is a secret file. Shhh! Don't tell anyone.")
➊ secretFile.close()

首先,你必须使用 open() 函数打开文件。接着,使用点表示法调用 write() 函数,将字符串写入 secretFile.txt 文件。然后,你需要调用 close() 函数 ➊,这会保存并关闭文件。记得一定要包含 close() 函数,否则数据将不会存储到文件中。

运行程序,然后在文本编辑器中打开 secretFile.txt,查看你的秘密信息是否已保存。尝试更改字符串,写入不同的信息,然后再次运行程序。发生了什么?旧的信息应该被新信息替换了!再试试更改信息,这次不要传递 "w",而是传递 "a"。现在发生了什么?很酷吧?

读取文件

read() 函数读取程序打开的文件的全部内容。你可能希望在程序中使用这些数据,修改数据,然后再将其写回文件,或者将数据输出,方便查看。不管是什么原因,你都将使用 read() 函数来读取文件。

要读取文件,你必须首先打开文件,然后记得在操作完成后关闭它。当你在程序中处理文件时,养成这个习惯非常重要,以避免出现错误!

让我们读取一个文件,并输出其内容,这样我们就可以看到文件的内容。这个程序 showSecretFile.py 使用 read()print() 函数输出文件内容:

showSecretFile.py

   secretFile = open("secretFile.txt", "r")

➊ print(secretFile.read())
   secretFile.close()

首先,我们打开文件,并传递 "r" 作为权限参数,这样程序就可以从文件中读取数据。你也可以传递 "r+",但在这个例子中,我们不需要写入文件,所以使用 "r" 最为合适。为了打印出 secretFile.txt 的内容,我们将 secretFile.read() 传递给 print 语句。最后,尽管我们没有向文件写入任何数据,但仍然建议使用 close() 函数关闭文件。

运行程序看看会发生什么。secretFile.txt 的内容应该会被打印到屏幕上。现在你可以在不需要像往常一样在文本编辑器中打开文件的情况下读取文件内容了!

读取文件的一行

假设你有一个很长的文本文件,想只查看其中的一部分。这时,readline() 函数就非常有用。与 read() 函数(该函数获取整个文件内容)不同,readline() 函数一次只获取文件中的一行。

要尝试 readline() 函数,首先向 secretFile.txt 文件中添加一些文本。你可以通过使用文本编辑器或者利用你新学的 Python 技巧来向文件写入一堆信息!如果你使用 Python 写入文件,每当你想换行时,可以在字符串中加入 \n。例如,如果你将 "Cool\nDance\nParty" 写入文件,Python 会将 "Cool" 放在第一行,"Dance" 放在第二行,"Party" 放在最后一行,如下所示:

Cool
Dance
Party

在你向secretFile.txt添加文本后,将以下代码写入一个 Python 文件,并将文件保存为showSecretLines.py,存放在名为files的新文件夹中:

showSecretLines.py

secretFile = open("secretFile.txt", "r")

print(secretFile.readline())
print(secretFile.readline())
print(secretFile.readline())

secretFile.close()

你必须再次打开secretFile.txt,才能使用readline()函数读取它的内容。因为你希望showSecretLines.py程序从文件中读取数据,所以你必须再次传入r(或r+)。接下来,包含三个print语句,用于打印secretFile.txt的前三行。最后,使用close()再次关闭文件。

readline()函数从文件的第一行开始。每次使用readline()函数时,它会自动读取下一行。这个函数对于打印文本文件开头的几行非常方便。

注意事项

readline() 函数将文件转换为字符串列表,其中列表中的每一项表示文件的一行。如果你想打印文档中间的某一行,可以编写一个循环来查找并打印列表中的特定字符串!

任务 #64:待办事项列表

有时候你可能没有太多的空闲时间来玩《我的世界》。你可能会在几天内分短时间段建造复杂的结构。随着你为开门或传送玩家到某个地方添加程序,你的建筑会变得更加复杂,可能需要更长的时间来完成。跨越几天的项目可能会导致你忘记自己在做什么以及接下来需要做什么。这种情况经常发生在我身上。幸运的是,你可以写一个程序来帮助你记住!

本任务中的程序创建了一个待办事项列表,并将其显示在《我的世界》聊天中。你可以使用这个程序来跟踪你的《我的世界》目标,这样当你需要停止游戏时,可以轻松地从中断的地方继续。

为了制作待办事项列表,你将编写两个独立的程序:一个用于写入列表,另一个用于显示列表。让我们从编写写入列表的程序开始。

第一部分:编写待办事项列表

首先,你需要一个程序来创建待办事项列表中的项目。清单 11-1 使用while循环和input()函数来添加待办事项。将它复制到 IDLE 中的文件,并将文件保存为inputToDoList.py,存放在files文件夹中。

inputToDoList.py

➊ toDoFile =

➋ toDoList = ""

➌ toDoItem = input("Enter a to-do list item: ")

➍ while toDoItem != "exit":
➎     toDoList = toDoList + toDoItem + "\n"
       toDoItem = input("Enter a to-do list item: ")

➏ # Write the to-do list to the file
➐ # Close the file

清单 11-1:开始编写待办事项列表程序的代码

该程序创建一个空字符串toDoList ➋,当你输入待办事项时,它将存储所有的待办事项。程序使用input()函数询问你输入一个待办事项 ➌。while循环检查输入是否不等于"exit" ➍;如果不等,程序将你的项目添加到待办事项列表中,并在末尾加上换行符"\n" ➎。然而,如果你输入"exit",循环将停止,你将无法再添加任何项目到待办事项列表中。

你的任务是完成这个程序。为此,你需要编写代码来打开文件、将 toDoList 写入文件,然后关闭文件。使用 open() 函数在程序开始时打开文件 ➊。你应该以写权限打开它。将该函数打开的文件命名为 toDoList.txt。如果文件在目录中不存在,程序会创建该文件。

在程序的最后,将待办事项列表的内容写入文件,以便稍后访问。使用 write() 函数将 toDoList 变量写入 toDoFile ➏。在文件写入之后,确保在最后一行使用 close() 函数关闭文件 ➐。

图 11-1 展示了我用程序写待办事项列表。当我完成后,我输入 exit

image

图 11-1:输入待办事项,例如建造蛋糕森林和玩捉迷藏

第二部分:显示待办事项列表

现在你有了一个可以将待办事项写入文件的程序,你需要将待办事项一行一行地显示在 Minecraft 聊天中。清单 11-2 为你提供了程序的起始部分。将该清单复制到一个新文件中,并将其保存为 outputToDoList.py 文件,放在 files 文件夹中。

outputToDoList.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

➊ toDoList =

   for line in toDoList:
➋     # Output "line" to the chat

清单 11-2:将待办事项列表输出到 Minecraft 聊天的程序

清单 11-2 使用 for 循环将 toDoList.txt 文件中的每一行输出到 Minecraft 聊天窗口中,一次一行。目前程序还不完整。为了完成程序,请添加 open() 函数来打开你用 inputToDoList.py 创建的 toDoList.txt 文件 ➊。确保文件具有读取权限。打开文件后,在 for 循环中添加代码,将存储在 line 变量中的字符串输出到 Minecraft 聊天中 ➋。你需要使用 readline()postToChat() 函数来实现这一点。

图 11-2 展示了我的待办事项列表在 Minecraft 聊天中的样子。

image

图 11-2:现在当我回到建造时,我可以看到我需要做的事情。

使用模块

模块 是一组函数,你可以将其导入到 Python 中,这样就无需在程序中自己编写这些函数。一个模块通常有一个特定的用途,例如执行科学计算或制作游戏,Python 有很多种类的模块可供使用。你可能会惊讶地发现,实际上你已经在本书中使用过模块!Minecraft Python API 就是一个模块:每次你写 from mcpi.minecraft import Minecraft 时,你就在使用一个模块。Minecraft Python API 模块让你能够将 Python 程序与 Minecraft 连接起来。由于这是别人预先编写的,你可以直接使用模块的函数,而无需自己编写代码。

Python 自带了许多可以在程序中使用的模块。这些模块和你在本书中所学的所有 Python 知识一起,被称为Python 标准库。你还可以安装那些不属于标准库的模块;我们将在“使用 pip 安装新模块”中介绍,第 252 页有详细内容。

在本节中,你将学习如何设置程序以使用模块。作为示例,我们将使用pickle模块,它提供了比简单的文件读写更先进的保存和加载数据的方法。现在我们来看看pickle模块。

PICKLE 模块

当你需要将复杂数据写入文件时,pickle模块非常有用。例如,字典和多维列表使用我们在本章前面介绍的标准函数存储和检索时非常困难。这个时候,pickle模块就非常方便了。

pickle模块可以帮你节省大量编写和调试自己存储复杂数据解决方案的时间。你还可以使用pickle模块处理简单数据:例如,你可以使用它来存储数字,而不必将其转换为字符串并在转换回去,这在标准的文件输入输出中是必须的。

换句话说,你可以使用pickle模块将一个变量的值保存在文件中,然后直接在另一个程序中读取这个变量的值,而无需额外的处理。即使数据类型是字符串、整数、浮点数或布尔值,数据类型也会保持与存储时相同。

接下来,你将学习如何以pickle为例导入模块。然后你将使用pickle保存一些复杂的数据——一个完整的 Minecraft 建筑!

导入 PICKLE

要使用任何模块的函数,你需要通过import关键字导入它们。实际上,你已经使用过import关键字导入模块,比如time模块,以及来自 Python Minecraft API 的函数。

导入模块到程序后,你可以通过点号符号来使用模块的函数。包括模块名、一个点和你要使用的函数。让我们导入pickle模块并使用它的一些函数:

➊ import pickle

   locations = {'John': 'Forest', 'Phillipa': 'Mountains', 'Pete': 'City'}

➋ secretFile= open("secretFile.txt", "wb")
➌ pickle.dump(locations, secretFile)

我们在➊处导入了pickle模块。接下来,我们用特殊的文件权限"wb"打开secretFile.txt文件➋。当你使用pickle打开文件时,必须在文件权限中添加b。在这个例子中,"wb"会使用pickle模块要求的特殊格式将数据写入文件。

dump()函数会在➌处写入文件。pickle模块的dump()函数将一个变量存储到文件中。它接受两个参数:要写入文件的数据和要写入的打开文件。这个示例将秘密特工的位置存储在一个名为locations的字典中,然后将该字典转储到一个名为secretFile的文件中。由于dump()属于pickle模块,因此你必须使用点符号来指定模块和函数,写作pickle.dump()。与 Python 标准的文件函数不同,dump()函数会自动将数据保存到文件中—你不需要使用close()函数来关闭文件。

pickle模块还允许你读取存储的数据。你可以使用pickleload()函数来读取文件的内容。它接受一个参数,即你想要加载的文件,并返回文件的内容。以下示例加载了我们之前存储的locations字典。将这段代码添加到程序中:

   import pickle

➊ secretFile= open("secretFile.txt", "rb")
   locations = pickle.load(secretFile)

首先,我们以权限"rb"打开文件 ➊,这允许你的程序读取pickle使用的特殊数据格式。然后我们加载字典。

现在字典已经加载,你可以像操作其他字典一样操作它。例如,你可以访问其中一个键的值。只需在pickle.load()函数之后添加这段代码:

print(locations['Phillipa'])

这将打印出'Mountains',即'Phillipa'键的值。这是因为当字典被pickle加载到程序中时,文件中的字典没有变化—它仍然是一个字典,所以我们可以访问它的键和值,并像使用其他任何 Python 字典一样使用它。你也可以对列表或变量执行相同的操作。

使用 FROM 语句导入一个函数

导入模块意味着你可以访问该模块中的所有函数,但有时你只需要模块中的一个函数。如果你只想导入一个函数,可以在导入模块时使用from语句。这个语句让你在调用函数时无需每次都包含模块名和点符号。你只需要写function(),而不是module.function()

有时,当你使用pickle模块时,可能只想使用dump()函数,而不是它的其他函数。为此,你需要修改你之前写的代码,使其看起来像这样:

➊ from pickle import dump

   locations = {'John': 'Forest', 'Phillipa': 'Mountains', 'Pete': 'City'}

   secretFile= open("secretFile", "wb")
➋ dump(locations, secretFile)

第一行使用from语句只导入了pickle模块中的dump()函数 ➊。最后一行调用了dump()函数 ➋。注意它没有使用点符号。你只需要直接调用函数名,而不需要引用模块名。

你也可以使用from从模块中导入多个函数。你只需要用逗号分隔函数名。例如,如果你想在同一个文件中使用pickledump()load()函数,你可以同时导入它们:

➊ from pickle import dump, load
   locations = {'John': 'Forest', 'Phillipa': 'Mountains', 'Pete': 'City'}

   secretFile= open("secretFile", "wb")
➋ dump(locations, secretFile)

➌ locations = load(secretFile)
   print(locations['Phillipa'])

第一行使用from子句和逗号导入了dump()load()函数➊。这意味着在程序的后续部分,你可以使用这些函数,而无需包含函数名和点号表示法,你可以在➋和➌看到这一点。

导入所有函数使用*

你还可以导入一个模块中的所有函数,这样每次使用时就不需要再加上模块名和点号表示法。你可以通过在import语句的末尾输入星号(*)来实现,像这样:

➊ from pickle import *
   locations = {'John': 'Forest', 'Phillipa': 'Mountains', 'Pete': 'City'}

   secretFile= open("secretFile", "wb")
➋ dump(locations, secretFile)

➌ locations = load(secretFile)
   print(locations['Phillipa'])

由于这段代码使用星号➊导入了模块中的所有函数,所以我们在调用dump()➋和load()➌函数时不需要使用点号表示法。

*选项非常方便,但它也有风险!如果你同时使用多个模块,两个模块可能会有相同的函数名。发生这种情况时,Python 可能会感到困惑,并出现错误。因此,当你使用多个模块时,最好避免使用*选项,而是只导入你需要使用的函数。

为模块起个昵称

有时你可能会想重命名一个模块,因为它的名字太长,你希望在程序中使用更短的名字。或者,你希望修改模块名使其更容易记住。又或者,模块的名字可能与另一个模块相同,你想避免冲突。

你可以使用as子句和import语句为模块指定一个别名——一个昵称。例如,这段代码导入了pickle模块,并将其重命名为p

import pickle as p

现在,每次你想使用pickle模块时,你可以在程序中写p,而不是pickle。下面是一个实际应用的例子:

p.dump(locations, secretFile)

注意,使用了p.dump()而不是pickle.dump()。这样节省了时间,因为你不需要反复输入pickle了!

任务 #65:保存一个建筑物

在 Minecraft 中,建造东西是我最喜欢的部分。我已经花了几个小时建造房屋、城堡、村庄以及许多其他东西。但是当我移动到地图的另一部分或进入不同的世界时,我必须把我的创作留下。我敢肯定你也有过不得不放弃一些精彩创作的经历。

如果你能保存你的建筑物并在进入不同的世界时带着它们,那不是很酷吗?好了,借助pickle和 Python API,你完全可以做到!

在这个任务中,你将开发两个程序来保存和加载 Minecraft 中的建筑物。一个程序将存储建筑物,另一个将加载建筑物。这两个程序基于第十章中的duplicateArea.py(第 225 页)。

第一部分:保存建筑物

第一个程序将把一个建筑物保存到文件中。示例 11-3 包括了复制建筑物的代码。将此代码复制到 IDLE 中的一个文件,并将其保存为saveStructure.py,并放在files文件夹中。

saveStructure.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import pickle

   def sortPair(val1, val2):
       if val1 > val2:
           return val2, val1
       else:
           return val1, val2

➊ def copyStructure(x1, y1, z1, x2, y2, z2):
       x1, x2 = sortPair(x1, x2)
       y1, y2 = sortPair(y1, y2)
       z1, z2 = sortPair(z1, z2)

       width = x2 - x1
       height = y2 - y1
       length = z2 - z1

       structure = []

       print("Please wait..." )

       # Copy the structure
       for row in range(height):
           structure.append([])
           for column in range(width):
               structure[row].append([])
               for depth in range(length):
➋                 block = mc.getBlock(x1 + column, y1 + row, z1 + depth)
                   structure[row][column].append(block)

       return structure

➌ # Get the position of the first corner
   input("Move to the first position and press ENTER in this window")
   pos1 = mc.player.getTilePos()

   x1 = pos1.x
   y1 = pos1.y
   z1 = pos1.z

➍ # Get the position of the second corner
   input("Move to the opposite corner and press ENTER in this window")
   pos2 = mc.player.getTilePos()

   x2 = pos2.x
   y2 = pos2.y
   z2 = pos2.z

➎ structure = copyStructure( x1, y1, z1, x2, y2, z2)

➏ # Store the structure in a file

示例 11-3:保存建筑物到文件的未完成代码

copyStructure()函数将游戏中的一个区域复制到一组三维列表 ➊。它接受两个坐标集作为参数。我对copyStructure()函数做了一些小的修改,与duplicateArea.py相比,我使用了getBlockWithData()函数而不是getBlock()函数 ➋。与仅获取某些坐标处方块 ID 不同,getBlockWithData()函数还会获取该方块的状态。这对于像楼梯这样的方块非常有用,因为楼梯的方向存储在方块的状态中。当建筑被复制时,楼梯和其他具有特定朝向的方块将按正确的方向构建。

我已经包含了一些简洁的代码,以便你可以使用玩家的位置来设置你想要复制的建筑的坐标。当你运行程序时,它会要求你移动到建筑的第一个角落,然后在 Python shell 中按 ENTER 键 ➌。程序使用玩家的位置获取建筑的第一组坐标。接下来,它会要求你移动到建筑的对角线另一端,并执行相同的操作 ➍。这样,你只需要站在你想开始复制建筑的位置,而不必手动输入坐标或将其硬编码到程序中。

这些坐标变量的值被传递给copyStructure()函数 ➎。返回值存储在一个名为structure的变量中。

为了完成代码,你需要用pickle打开一个新文件。将新文件命名为"pickleFile"。然后编写代码,将建筑存储到该文件中。使用pickle模块将structure变量的值写入文件 ➏。

图 11-3 展示了我在我的 Minecraft 世界中建造的一座塔楼。

图片

图 11-3:我要复制的塔楼

为了使用saveStructure.py复制塔楼,我移动到一个角落,并在 IDLE 中按 ENTER 键(图 11-4)。

图片

图 11-4:站在塔楼的一个角落旁边

然后我飞到塔楼的对角线另一端,再次在 IDLE 中按 ENTER 键(图 11-5)。

图片

图 11-5:飞到塔楼的对角线另一端

按照相同的步骤使用saveStructure.py保存你自己的建筑。接下来,我们将完成流程的另一半,把我们保存的建筑加载到游戏中。

第二部分:加载建筑

第二个程序需要从saveStructure.py创建的文件(名为pickleFile)中将建筑加载到游戏中。清单 11-4 包括来自duplicateArea.py程序的代码(第 225 页),该代码将存储在列表中的建筑放置到游戏中。将清单复制到 IDLE 中的一个文件并保存为loadStructure.py,保存在files文件夹中。

loadStructure.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import pickle

➊ def buildStructure(x, y, z, structure):
       xStart = x
       zStart = z
       for row in structure:
           for column in row:
               for block in column:
                   mc.setBlock(x, y, z, block.id, block.data)
                   z += 1
               x += 1
               z = zStart
           y += 1
           x = xStart

   # Open and load the structure file
➋ structure =

➌ pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z
➍ buildStructure(x, y, z, structure)

清单 11-4:完成时,该程序将从文件中构建一座建筑。

buildStructure() 函数 ➊ 在这个程序中完成了大部分的工作。它使用四个参数:x、y 和 z 坐标,以及存储在三维列表中的结构,来构建游戏中的结构。

导入 pickle 模块,以便将结构加载到程序中,然后将其存储在 structure 变量中 ➋。使用 open() 函数,打开你保存结构的 pickleFile 文件。然后使用 pickleload() 函数将其加载到 structure 变量中。加载结构后,使用 pickleclose() 函数关闭 pickleFile 文件。

Listing 11-4 中还包括了一些代码,用于获取玩家的位置,将其用作结构的起始位置 ➌。

在结构加载完毕并且坐标设置完成后,将结构和位置 ➍ 一并传递给 buildStructure() 函数,该函数会在新的位置构建保存的结构。

图 11-6 显示了程序的运行情况。我之前保存的建筑物已经加载到游戏中,并且在新位置重新构建。试试看—现在你有能力随身携带你的创作了!

image

图 11-6:看,它是我的塔的复制品!

但是,如果你创建了一个完整的村庄,并想要把它带走呢?你可以使用 pickle 将每个建筑物保存到独立的文件中,但这并不方便。pickle 模块对于保存单个建筑物非常有效,但对于保存一堆建筑物就不太适用了。这时候,shelve 模块就派上用场了。接下来,我们来看看它。

使用 SHELVE 模块存储大量数据

pickle 模块一次只能存储一条数据。在某些程序中,你可能想要存储多个变量;如果使用 pickle 模块,你需要创建多个文件,这样管理起来会比较麻烦。

Python 的 shelve 模块解决了这个问题。它可以将多个数据项存储在一个文件中。它像一个字典,每个数据值都有一个键,你可以使用这个键来存储和检索数据。可以把 shelve 想象成一个架子:架子上的每个格子存储不同的数据值。

使用 SHELVE 打开文件

导入 shelve 模块后,你将使用它的 open() 函数来打开一个文件。如果文件不存在,系统会创建一个新文件。

以下代码打开 locationsFile.db 文件,并将其存储在 shelveFile 变量中:

import shelve
shelveFile = shelve.open("locationsFile.db")

open() 函数只接受一个参数,即文件名。使用 shelve 模块时,你无需指定文件权限,因为它会自动授予读写权限。

在使用 shelve 模块命名文件时,必须在文件名末尾包含 .db 扩展名。你可以看到我的 locationsFile.db 文件末尾有 .db 扩展名。

使用 SHELVE 添加、修改和访问数据项

shelve模块的工作方式类似于字典。为了将数据添加到文件中,你使用方括号和键名来存储一个值。例如,假设一个名叫贝阿特丽斯的秘密特工在潜水艇上,我们想把贝阿特丽斯的位置存储在shelveFile字典中:

import shelve
shelveFile = shelve.open("locationsFile.db")
shelveFile['Beatrice'] = 'Submarine'
shelveFile.close()

首先,我们打开文件。接下来,我们给shelveFile字典添加键'贝阿特丽斯'和值'潜水艇'。这行代码在shelveFile字典中创建了一个新项,键为'贝阿特丽斯',值为'潜水艇'

然后我们使用shelveclose()函数将新数据添加到文件中并安全地关闭文件。

如果一个键已经存在于shelve文件中,这段代码会将旧值更新为新值。假设贝阿特丽斯完成了任务并返回总部。你可以这样更新贝阿特丽斯的位置:

import shelve
shelveFile = shelve.open('locationsFile.db')
shelveFile['Beatrice'] = 'Headquarters'
shelveFile.close()

现在,贝阿特丽斯键对应的值是'总部'

shelve访问值的方式也和字典一样。你使用键来访问特定的值。例如,要打印贝阿特丽斯的位置,我们会使用以下代码:

import shelve
shelveFile = shelve.open('locationsFile.db')
print(shelveFile['Beatrice'])

这将输出贝阿特丽斯的位置,总部

和标准字典一样,shelve模块可以存储任何数据类型,包括浮动数、字符串、布尔值、多维列表、其他字典等等。事实上,在下一个任务中,你将存储和访问多维列表来保存和加载多个结构!

任务 #66:保存结构集合

本任务中的程序将使用一个文件存储和加载你所有保存的结构。这个任务再次分为两个程序:一个用于保存,另一个用于加载。

你需要将任务 #65 中的程序改为使用shelve模块,而不是pickle模块。你还需要添加代码来获取用户输入,以便用户为他们的建筑物命名。打开saveStructure.pyloadStructure.py文件,并将其保存为saveCollection.pyloadCollection.py

就像我们在上一个任务中做的那样,让我们把这些程序的修改分为两个部分来完成。

第一部分:将结构保存到集合中

这里包含了原始saveStructure.py文件的一部分并进行了注释,帮助你识别在哪些地方进行修改。以下是saveCollection.py的第一行和最后几行:

saveCollection.py

➊ import pickle

   --snip--

   # Name the structure
➋ structureName = input("What do you want to call the structure?")
   # Store the structure in a file
➌ pickleFile = open("pickleFile", "wb")
➍ pickleFile.dump(structure)

文件中添加了一行,询问你在使用pickle保存结构时想要给结构命名是什么 ➋。例如,我版本的程序会问“你想给结构起什么名字?”然后我可以回答诸如“房子”或“蛋糕森林”之类的名字。确保每个新结构的名字都不同;如果新结构和另一个结构的名字相同,旧的结构将被新的结构覆盖。

要将这个程序修改为使用shelve模块而不是pickle,你需要做两个更改。首先,将模块导入从pickle换成shelve ➊。然后,更改代码的最后几行,使用shelve而不是pickle。打开名为structuresFile.db的文件,并使用shelve.open()函数将其存储在名为shelveFile的变量中 ➌。接着,使用structureName变量作为字典键,将structure变量存储在shelve字典中 ➍。它应该看起来像这样:shelveFile[structureName] = structure。最后,在最后一行使用close()关闭shelveFile

第二部分:从集合中加载结构

现在,你需要更改loadCollection.py文件。我已将文件的中间部分删除,以节省空间,并使你需要更改的部分更易于查看:

loadCollection.py

➊ import pickle

   --snip--

➋ structure = pickle.load("pickleFile")
➌ structureName = input("Enter the structure's name")

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z

➍ buildStructure(x, y, z, structureDictionary[structureName])

我在代码中添加了一行额外的代码,用于询问你想要构建的结构的名称 ➌。此外,我在最后一行添加了一点代码,从shelve字典中获取结构并将其传递给buildStructure()函数。

你需要对这个程序做几个更改。首先,与saveCollection.py一样,将import改为shelve而不是pickle ➊。其次,通过使用shelve.open()加载你在saveCollection.py中创建的shelveFile ➋。将shelve.open()函数返回的数据存储在变量structureDictionary中 ➍。代码应该像下面这样:structureDictionary = shelve.load("shelveFile")

所有结构的数据,包括它们的名称和方块,都存储在structuresFile.db文件中,这意味着在运行loadCollection.py之前,你无需对其进行任何更改。你需要做的就是在运行程序时输入你想要使用的结构名称。

让我们看看程序的运行效果,使用我 Minecraft 世界中的一个结构。首先,我通过飞到结构的一个角落并按下 ENTER 来使用saveCollection.py复制该结构(图 11-7)。

image

图 11-7:我移到我想保存的结构的一个角落。

接下来,我飞到结构的对角线另一端,再次按下 ENTER(图 11-8)。

image

图 11-8:我移动到对角的另一角落。

然后,程序提示我输入结构的名称。图 11-9 显示了我给结构起的名字是"蛋糕树"

image

图 11-9:我输入了我想要保存结构的名称。

最后,我运行loadCollection.py,飞到我想构建结构副本的位置,输入我要构建的结构名称(图 11-10)。程序在我面前开始构建,就像魔法一样!

image

图 11-10:现在,当我想创建一个副本时,我只需输入结构的名称,它就会自动构建。

你可以重复这个过程,复制任意数量的建筑或结构;例如,我在图 11-11 中复制了一个小屋。复制完一个结构后,你可以随时通过运行 loadCollection.py 并输入结构的名称来加载它!

image

图 11-11:你可以使用这个程序来保存多个结构。我在这里复制了一个小屋。

使用 PIP 安装新模块

除了 pickleshelve 外,你还可以导入成千上万的其他模块来使用在你的 Python 程序中。由于有这么多模块可用,正确安装它们非常重要。为了简化模块的安装,Python 提供了一个名为 pip 的包管理器。包管理器是一个包含其他软件数据库的软件,你可以在计算机上安装这些软件。它还包括一些功能,使得安装、升级和卸载其他软件变得简单直接。

pip 包管理器可以安装、升级和删除 Python 中的包。它还拥有大量可以在 Python 中使用的模块。本节将向你展示如何使用 pip 安装一个包,并展示 Flask 模块,你可以用它来构建网站!

如果你使用的是最新版本的 Python 3,pip 已经预安装。如果你使用的是较早版本的 Python,pip 可能没有安装。获取 pip 的最简单方法是安装最新版本的 Python。(请参阅 Windows 上的 “安装 Python”(第 3 页)以及 Mac 上的 第 13 页)

让我们看看如何使用 pip。根据你使用的操作系统,你可以通过几种方式使用 pip。确保按照与你的计算机匹配的指示操作!

在 Windows 上使用 PIP

在 Windows 上使用 pip 时,你需要打开 Windows 命令提示符。命令提示符类似于 Python shell,它允许你在一行中输入一个命令,当你按下 ENTER 键时,该命令就会运行。

要打开命令提示符,按下 Windows 键或打开开始菜单并搜索 cmd。当你打开程序时,你会看到一个黑色的窗口(图 11-12)。

image

图 11-12:Windows 命令提示符

要在命令提示符中使用 pip,输入pip,后面跟上你希望执行的操作。例如,让我们安装 Python 模块 Flask,你可以用它来创建 Python 网站。在命令提示符中输入以下命令:

> pip install Flask

在 Python 软件包索引网站 pypi.python.org/ 上,你可以找到许多其他可以安装的 Python 模块。

在 Mac 或 Raspberry Pi 上使用 PIP

如果你在 Mac 或 Raspberry Pi 上使用 pip,你需要在命令前加上sudo才能使其正常工作。例如,输入以下命令:

$ sudo pip install Flask

如果你遇到错误,请翻到第一章,重新检查 Mac 或 Raspberry Pi 的安装说明。

在 Python 包索引网站 pypi.python.org/ 上,你可以找到许多其他可以安装的 Python 模块。

使用 PIP 模块:FLASK

Flask是一个 Python 模块,可以用来开发网站。在这一节中,你将学习如何设置一个基本的Flask网站,并将其与 Minecraft 集成,以便在网站上显示你玩家的位置!

使用Flask,你只需要几行代码就可以创建并管理一个网站。你只需要像平常一样编写 Python 代码,并添加一些与Flask相关的额外信息。接下来,运行你的代码,它将创建一个你的计算机可以访问的网站。然后,你可以在网页浏览器中查看这个网站。

清单 11-5 创建了一个基本的Flask网站,其中包含了关于我最重要的信息:我的名字。

namePage.py

   from flask import Flask
➊ app = Flask(__name__)

➋ @app.route("/")
   def showName():
➌     return "Craig Richardson"

➍ app.run()

清单 11-5:一个使用Flask创建网站的 Python 程序

要使用Flask,你首先需要通过Flask()函数创建Flask ➊。__name__参数告诉Flask,你的Flask项目包含在这个文件中,它不需要去其他地方寻找程序的其他部分。请注意,__name__参数前后有两个下划线,而不是一个。

@app.route()标签使用了装饰器。装饰器为 Python 提供关于你函数的额外信息。例如,在这个程序中,@app.route()装饰器告诉Flask函数将在网站的哪个部分使用。在这种情况下,"/"告诉FlaskshowName()函数将在主页上使用 ➋。函数中的return语句告诉Flask将在页面上显示什么内容。在这个例子中,它返回了我的名字,所以我的名字会显示在页面上 ➌。程序的最后一行告诉 Python 在运行此文件时启动Flask ➍。

将此文件保存为 namePage.py,并放入 files 文件夹中。添加你自己的文本,创建属于你自己的网站。

要运行网站,在 IDLE 中点击运行运行模块。程序运行并生成一个网站文件,你可以在网页浏览器中打开它。要找到网站的位置,你需要查看程序开始运行时输出的那行代码。当我运行程序时,输出看起来是这样的:

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

从这一行中,我可以看出,输入 http://127.0.0.1:5000/ 到网页浏览器中,将会带我到刚刚启动的Flask网站。现在,当我打开浏览器并访问这个网站时,我可以看到我的名字显示在页面上(图 11-13)。

注意

你在此程序中创建的网站目前只在你的计算机上可用。只有你可以访问这个网站——互联网上其他人无法查看它。

image

图 11-13:看看我的网站!你可以将这里显示的内容更改为你想要的任何内容。你会在你的网站上写些什么?

要停止程序,请进入 IDLE 并按 CTRL-C 或点击ShellRestart Shell

注意

本章仅对 Flask 进行了非常基础的介绍。 Flask 非常有用,因为它允许你用 Python 快速构建交互式网站。如果你想了解更多关于 Flask 的信息,可以查看这个 Flask 网站上的教程:flask.pocoo.org/docs/0.10/tutorial/

任务 #67: 位置网站

Python 的一个最佳特点是它能够轻松地将不同模块的功能集成到一个程序中。你在整本书中一直使用 Minecraft Python API 模块,并且刚刚了解了Flask模块。只需几个步骤,你就可以将两者集成在一起。

在这个任务中,你将结合 Minecraft Python API 和Flask,在网页上显示玩家的位置。

在 IDLE 中创建一个新文件,并将其保存为positionPage.py,放在files文件夹中。你需要从 Minecraft 获取玩家的位置,并使用带有Flask @app.route("/")标签的函数将其显示在网页上。你可以参考 Listing 11-5 中的示例代码。使代码以"x 10, y 110, z 12"的格式显示位置。

运行程序并查看你的网站页面。挺酷的,对吧?使用Flask,你可以创建包含各种信息的网页。你甚至可以将这些页面上传到互联网上与朋友们分享!

你学到的内容

在这一章中,你学习了如何在 Python 中使用文件。你学会了如何使用 Python 的标准库读写文件,这让你在创建自己的程序时可以控制文件的操作。你还学会了如何使用模块,这些模块扩展了 Python 的功能,让你可以做更多的事情。

你探索了pickle模块、shelve模块和 pip 包管理器。pickleshelve模块有不同的用途。pickle模块用于保存单个变量的值,特别是当它包含多维列表或字典时,这些内容使用标准库存储和打开会很困难。shelve模块与pickle模块有相同的优点,但它提供了更多的灵活性,可以将多个值一次性存储在类似字典的结构中。使用 pip,你学习了如何安装新的模块。你还了解了Flask模块,这是一个用 Python 快速灵活构建网站的方式。

拥有这些知识后,你已经完成了四个任务。第一个任务让你创建待办事项列表,你可以在《Minecraft》游戏中显示这些列表,提醒你正在做的事情。第二个任务使你能够保存建筑物,并将它们加载到当前世界和其他世界中。第三个任务修改了第二个任务,使你能够将所有建筑物存储在一个文件中,而不是为每个建筑物创建一个文件。最后一个任务教你如何使用Flask模块创建一个网页,显示玩家当前的位置。

你做得很棒!下一章是最后一章,你将学习类和面向对象编程,这是一种流行的编程风格,可以让你重复使用代码。

第十二章:12

与面向对象编程的高级应用

image

可重用性是编程中非常重要的一个方面。它节省了时间和精力。你已经在循环和函数中看到了这一点,现在你将学习面向对象编程

面向对象编程是一种将函数和变量组合在一起以创建的编程方法。每个类都可以用来创建对象,这些对象共享类的变量和函数。你可以从同一个类创建许多对象,从而使类的变量和函数可重用。

当一个函数是类的一部分时,它被称为方法,而类中的变量被称为属性

在这一章中,你将学习面向对象编程,并使用类来重用代码。掌握面向对象编程和类使得构建程序变得轻松,你甚至可以使用面向对象编程来制作游戏。在本章的任务中,你将使用类来制作一些基本程序。你将从创建一个简单的建筑开始,但很快你就会建造整个城镇。

面向对象编程基础

面向对象编程非常流行,你可以用它来创建各种酷的软件,但它也可能是一个难以理解的概念。让我们把它与更熟悉的东西联系起来:你。

你是一个人。你有许多方法:你可以吃、呼吸、睡觉、数到 10,还能做很多其他事情。你也有属性:名字、年龄、身高、鞋码等等。

你的朋友玛丽和你有相同的方法;她也能吃、呼吸、睡觉、数到 10,并做许多其他事情。她也有相同的属性(名字、年龄等等),尽管它们包含不同的值。

事实上,每个人都有这些方法和属性。你可以把人描述为一个类。你和玛丽都是人,因此你可以说你们俩都是Person类中的对象。

在面向对象编程中,对象被称为类的实例。所有对象都共享类的方法和属性,但属性的值对于每个对象可能不同。

让我们跳进 Python,创建一个类。

创建一个类

你将从创建一个类开始,然后从这个类创建所有的对象。要创建一个类,你使用class关键字、你想要的类名,以及括号中的object类(我将在 “继承类” 的第 274 页解释object类):

class ClassName(object):
    def __init__(self):
        # Body of init

将类的名称大写是一个很好的实践。这使得区分类和函数更加容易,函数应该以小写字母开头。

当你创建一个新类时,你需要包含__init__()方法并传入self作为参数。self参数是类中每个方法都必须有的,它指代该方法所属的类。__init__()方法告诉 Python 当你第一次在程序中使用这个类时,它应该做些什么。这叫做初始化类,__init__()正是初始化的缩写。

例如,我们来创建一个名为Cat的类,然后制作一些猫对象。Cat类将为每只猫存储两个属性,它们的name(名字)和weight(体重,单位:千克)。每个猫对象将拥有自己独立的nameweight值。打开 IDLE 的文本编辑器,创建一个新文件并将其保存为catClass.py,并放在一个名为classes的新文件夹中。输入以下代码来创建一个名为Cat的类:

catClass.py

class Cat(object):
➊     def __init__(self, name, weight):
➋         self.name = name
➌         self.weight = weight

在这个例子中,__init__()方法接受三个参数 ➊。第一个是self,它是每个类方法中必需的参数。第二个参数name和最后一个参数weight是额外的参数,用来为所有猫创建属性。

最后两行代码创建了属性name ➋和weight ➌,并将它们设置为nameweight参数的值。当你在类中创建属性时,需要使用带有self的点表示法。属性总是通过self来标识,它告诉 Python 某个属性属于该类。

接下来,你将学习如何使用这个类来创建对象的实例。

创建对象

使用新创建的类,让我们创建一些猫对象,或者说Cat类的实例。

初始化对象类似于创建变量。要初始化一个对象,你只需输入对象的名称,等号(=)和类名。你将参数传递给类,就像调用函数一样,放在圆括号中。

例如,假设我们领养了一只猫并给它取名 Fluff。使用Cat类,我们可以通过在catClass.py的最后一行添加以下代码来创建一个名为fluff的猫对象(请注意,它没有缩进):

catClass.py

class Cat(object):
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

fluff = Cat("Fluff", 4.5)

当你创建一个对象时,提供的参数数量取决于__init__()函数中的参数。在这里,我们包括了两个参数,一个是name"Fluff"),另一个是weight4.5)。创建对象时不需要包含self参数,因为self参数是 Python 自动添加的。

创建一个对象也叫做调用构造函数__init__()方法通常被称为构造函数,因为它在被调用时构造了一个类。__init__()方法是一种特殊类型的方法,因为你不会通过名称引用它。相反,它会在你使用类名创建对象时自动运行。例如,这段代码fluff = Cat("Fluff", 4.5)调用了__init__()方法,从而构造了一个名为fluffCat对象。

接下来,你将学习如何访问fluff对象的属性。

访问属性

你可以访问对象的属性,以获取有关该对象的更多信息。例如,在fluff对象之后将以下代码添加到catClass.py中,以打印fluff对象的weight属性:

catClass.py

print(fluff.weight)

当你运行程序时打印出来的值应该是 4.5,因为那是你在创建对象时为weight属性设置的值。

注意,我们在对象名fluffweight属性之间使用了点号表示法。点号表示你要使用属于特定对象的属性。在这种情况下,weight属性的值属于fluff对象。每当你获取或设置对象的属性值时,都使用点号表示法。

你可以像更改任何其他变量一样更改属性的值——通过使用等号(=)。例如,让我们将 Fluff 的体重改为 5,因为他在寒假期间增加了体重。我们通过将fluff对象的weight属性更改为 5 来实现这一点:

catClass.py

fluff.weight = 5

现在,每当你访问fluff对象的weight属性时,它的值将是 5。

运用你现在拥有的关于创建类和实例化它的知识,让我们在 Minecraft 中做一些酷炫的东西。

任务 #68:位置对象

在整本书中,你已经在 Minecraft 世界中存储了许多位置,比如你的房子、城堡或宫殿。你已经使用变量、列表、元组和字典以各种方式完成了这些操作。

你也可以使用面向对象编程创建并存储相关信息,比如位置。例如,你可以使用对象来存储一堆不同位置的坐标。

每个位置都有一个 x、y 和 z 坐标,但每个位置的坐标值不同。通过创建位置类,你可以存储并访问不同位置的坐标。这将帮助你跟踪在 Minecraft 中建造的所有精彩事物。你将能够轻松访问所有 Minecraft 创作的坐标,以便你可以瞬间将玩家传送到它们的位置!

示例 12-1 包含了Location类的起始部分。当代码完成后,它可以用来在单个对象中存储位置的坐标。将代码复制到classes文件夹中的一个新文件,命名为locationClass.py

locationClass.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

➊ class Location(object):
       def __init__(self, x, y, z):
➋         self.x = x
➌         # Add the y and z attributes here

➍ bedroom = Location(64, 52, -8)
➎ mc.player.setTilePos(bedroom.x, bedroom.y, bedroom.z)

示例 12-1: Location 类的起始部分

为了开始创建类,我使用了class关键字并命名为Location ➊。在 ➍ 处是初始化一个名为bedroom的对象的代码,它将存储我的 Minecraft 家中的卧室位置。setTilePos()方法将玩家的位置设置为卧室位置——bedroom对象的xyz属性 ➎。然而,程序是不完整的。你需要完成类的__init__()方法,并将yz属性设置为传递给__init__()方法的参数的值。我设置了x属性的值 ➋,但是你需要对yz属性做同样的操作 ➌。别忘了使用你自己卧室的位置信息在 ➍!

图 12-1 显示了完整的程序运行情况,它将玩家传送到我的卧室。

image

图 12-1:程序已将玩家传送到我的卧室。

额外目标:温馨的家

你还想传送到你家里的哪些房间?使用Location类创建更多对象,以便在家里轻松移动!

理解方法

类可以包含方法,方法是与类相关的函数。编写类的方法可以让你创建所有该类实例都可以使用的函数。这是一种节省时间和重用代码的好方法,因为你只需要编写一个方法。

要创建一个方法,你在类的主体中使用def关键字写一个函数。你在前面的章节中已经用过def关键字来创建函数。方法也是使用def关键字创建的,但它们在所属类的下面缩进。例如,让我们更新catClass.py中的Cat类。我们希望猫咪能吃东西,所以让我们在Cat类中添加一个名为eat()的方法。按照代码的顺序输入并修改catClass.py

catClass.py

class Cat(object):
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

    def eat(self, food):
        self.weight = self.weight + 0.05
        print(self.name + " is eating " + food)

注意,方法的定义和方法体比正常的代码多缩进了四个空格,这样 Python 就能知道它们属于这个类。

像函数一样,方法也可以接受参数。在这里,eat()方法接受一个名为food的参数,表示猫咪正在吃的食物。eat()方法将猫咪的weight属性增加0.05,然后打印出猫咪正在吃食物的消息。

在创建对象后,你可以调用它所属类的任何方法。例如,你可以使用fluff对象调用eat()方法。将以下代码添加到catClass.py的末尾:

catClass.py

fluff = Cat("Fluff", 4.5)
fluff.eat("tuna")

这里我们看到了之前的代码,我们创建了一个名为fluff的对象,它是Cat类的一部分。然后我们调用了eat()方法,并传递了参数"tuna"。当你运行程序时,输出会是这样的:

Fluff is eating tuna

现在,Fluff 正开心地吃着金枪鱼。记住,eat()方法还会增加体重属性。调用eat()方法后,添加代码打印fluff的体重。

你还可以通过在另一个方法内部调用方法来从类内部调用方法。让我们在Cat类内部创建另一个名为eatAndSleep()的方法。eatAndSleep()方法调用eat()方法,然后打印出猫正在睡觉的信息。将此代码添加到catClass.py文件中,紧接在eat()方法后面(确保像示例中一样缩进新方法,以便 Python 知道它是类的一部分):

catClass.py

def eatAndSleep(self, food):
    self.eat(food)
    print(self.name + " is now sleeping...")

要从类内部调用方法,你需要在方法名的前面加上self.。这里eat()方法通过self.eat()被调用。请注意,这与在类外部调用方法是不同的。当你这样做时,只需输入对象名和你要调用的方法。例如,以下代码在fluff对象上调用了新的eatAndSleep()方法。将其添加到你的catClass.py文件中,这应该是程序的最后一行代码:

catClass.py

fluff.eatAndSleep("tuna")

下面是你运行程序时应该得到的输出:

Fluff is eating tuna
Fluff is now sleeping...

这是完整的程序,你可以查看所有部分的位置:

class Cat(object):
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

    def eat(self, food):
        self.weight = self.weight + 0.05
        print(self.name + " is eating " + food)

    def eatAndSleep(self, food):
        self.eat(food)
        print(self.name + " is now sleeping...")

fluff = Cat("Fluff", 4.5)
print(fluff.weight)
fluff.eat("tuna")
fluff.eatAndSleep("tuna")

让我们将你学到的新技能带入 Minecraft 的世界吧!

任务 #69:鬼屋

使用 Python 和 Minecraft 编程的最佳之处在于,你可以从一个愚蠢的想法开始,然后把它实现。你的想法可能开始得很小,但只需几行代码,你就能很快构建出一个有趣的程序。

建造一个在游戏中出现,30 秒后又消失的鬼屋,岂不是很有趣吗?然后,鬼屋可以在别的地方重新出现,再次消失,前提是你希望它如此。

这是鬼屋程序的第一个版本。将清单 12-2 保存为ghostHouse.py文件,并放在classes文件夹中。

ghostHouse.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time
➊ class Building(object):
➋     def __init__(self, x, y, z, width, height, depth):
           self.x = x
           self.y = y
           self.z = z

           self.width = width
           self.height = height
           self.depth = depth

➌     def build(self):
           mc.setBlocks(self.x, self.y, self.z,
                        self.x + self.width, self.y + self.height,
                        self.z + self.depth, 4)

           mc.setBlocks(self.x + 1, self.y + 1, self.z + 1,
                        self.x + self.width - 1, self.y + self.height - 1,
                        self.z + self.depth - 1, 0)
➍         # Call the buildDoor() and buildWindows() methods here

➎     def clear(self):
           mc.setBlocks(self.x, self.y, self.z,
                        self.x + self.width, self.y + self.height,
                        self.z + self.depth, 0)
➏         # Remove the doors and windows here

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z

➐ ghostHouse = Building(x, y, z, 10, 6, 8)
   ghostHouse.build()

   time.sleep(30)

   ghostHouse.clear()
➑ ghostHouse.x = 8

清单 12-2: Building 类创建一个建筑。

清单 12-2 使用了一个名为Building ➊的类,它通过__init__()方法设置房子的坐标和大小 ➋。它创建了一个名为ghostHouse ➐的Building对象。该建筑物出现后会神秘消失,30 秒后通过build() ➌和clear() ➎方法。唯一的问题是它看起来不像房子。现在它看起来像一个由鹅卵石做成的大空壳。

你需要让鬼屋看起来更像一座房子,而不是像一个空壳,因为鬼壳不像鬼屋那么可怕。为了让建筑物看起来更像房子,你的任务是添加一个方法,在房子的前面建造一扇门,并添加第二个方法来安装窗户。从build()方法中调用这两个方法,以便它们同时构建 ➍。

在添加构建门和窗户的方法后,你需要更新clear()方法来删除它们➏;否则,当房子消失时,它们将被遗留在原地。

当你添加了额外的方法后,通过更改 ghostHouse 对象的 xyz 属性,并添加更多对 build()clear() 方法的调用来将建筑物移动到新位置。我已经为你做了一些修改,改变了房子的 x 位置 ➑。

当你运行程序时,鬼屋应该会突然出现,然后在 30 秒后消失,再次出现在其他地方。好吓人!

图 12-2 显示了我的鬼屋。

image

图 12-2:鬼屋出现后又消失。

额外目标:家庭改造

目前,鬼屋非常基础。利用你在本书中学到的 Python 技巧,向 build() 函数中添加任何你想要的内容,以定制你的房子。

通过方法返回值

像函数一样,方法也可以使用 return 关键字返回值或对象的属性。例如,假设我们想将猫 Fluff 的体重从千克转换为克。1 千克等于 1000 克,因此为了进行转换,你需要将 weight 属性乘以 1000 并返回它。将以下 getWeightInGrams() 方法添加到 catClass.py 中的 Cat 类:

catClass.py

class Cat(object):
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

    def getWeightInGrams(self):
        return self.weight * 1000

要输出方法返回的值,你需要创建一个对象并调用该方法。在以下代码中,使用了 fluff 对象,并在 print() 函数中调用该方法,以获取猫的体重(以克为单位):

catClass.py

fluff = Cat("Fluff", 4.5)
print(fluff.getWeightInGrams())

现在,当你运行该文件时,它将输出以下内容:

4500

在下一个任务中,我们将扩展鬼屋程序,加入一个返回建筑物信息的方法。

任务 #70:鬼屋城堡

我有各种各样的名字,分别对应我在 Minecraft 世界中建造的不同地方:海滩房、植物农场、动物农场、储藏室、宫殿、水下宫殿、地下宫殿,等等。问题是这些名字仅存在于我的脑海中!

使用类,你可以为你建造的事物创建诸如位置和大小等属性,正如你在 任务 #69(第 263 页)中看到的那样。你还可以包括名称!

让我们给鬼屋起个名字,并让 Python 为我们记住它。我们将更新 任务 #69 中的 Building 类,添加一个额外的方法来返回建筑物的名称。将 清单 12-3 复制到一个名为 ghostCastle.py 的新文件中,放在 classes 文件夹中。

ghostCastle.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   import time

➊ class NamedBuilding(object):
➋     def __init__(self, x, y, z, width, height, depth, name):
           self.x = x
           self.y = y
           self.z = z

           self.width = width
           self.height = height
           self.depth = depth

➌         self.name = name

      def build(self):
          mc.setBlocks(self.x, self.y, self.z,
                       self.x + self.width, self.y + self.height,
                       self.z + self.depth, 4)

          mc.setBlocks(self.x + 1, self.y + 1, self.z + 1,
                       self.x + self.width - 1, self.y + self.height - 1,
                       self.z + self.depth - 1, 0)

      def clear(self):
          mc.setBlocks(self.x, self.y, self.z,
                       self.x + self.width, self.y + self.height,
                       self.z + self.depth, 0)

➍     def getInfo():
           # Add the body of the getInfo() method here

   pos = mc.player.getTilePos()
   x = pos.x
   y = pos.y
   z = pos.z
   ghostCastle = NamedBuilding(x, y, z, 10, 16, 16, "Ghost Castle")
   ghostCastle.build()
➎ mc.postToChat(ghostCastle.getInfo())

   time.sleep(30)

   ghostCastle.clear()

清单 12-3: NamedBuilding Building 类非常相似,不同之处在于它有一个额外的属性 name 和一个返回建筑物描述的额外方法。

首先,我将类的名称改为NamedBuilding,以免与之前任务中的Building类混淆 ➊。我在构造函数中添加了一个额外的参数和属性,叫做name ➋。该参数允许你为建筑物命名,构造函数将该名称赋给name属性 ➌。

你的任务是向新的NamedBuilding类添加一个名为getInfo()的方法,该方法返回建筑物的名字和位置。我已经为你在➍处添加了getInfo()方法的开头。你只需补充方法体即可。在➎处调用了getInfo()方法,它会将方法返回的字符串输出到 Minecraft 聊天框。例如,如果鬼屋位于x = -310y = 64z = 1081getInfo()方法应返回字符串"Ghost Castle's location is at -310, 64, 1081"

图 12-3 显示了我的工作程序。尽管鬼屋更高,但它看起来像任务 #69 中的房子。这是因为build()方法在两者之间是相同的,但你可以随意修改你的代码版本,使你的建筑更像一座城堡。

image

图 12-3:鬼屋的描述被显示出来。

额外目标:热烈欢迎

如果你走进任何建筑物时,建筑物的名字自动出现在聊天框里,不是很酷吗?其实是可能的,但有点挑战。如果你想试试,可以使用任务 #32 中的shower.py程序(第 120 页)作为起点。该文件应该在你的ifStatements文件夹里。你可以使用该程序检测玩家的坐标,并且如果玩家在建筑物内,就调用building对象的getInfo()方法。

创建多个对象

你可以通过使用相同的类构造函数创建具有不同名称的对象,从而从同一个类中创建多个对象(记住,构造函数__init__()方法的另一种说法)。例如,假设我们找到了第二只猫,名叫 Stella,现在她和 Fluff 成为了朋友。打开catClass.py并输入以下代码来添加 Stella:

catClass.py

class Cat(object):
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

fluff = Cat("Fluff", 4.5)
stella = Cat("Stella", 3.9)

现在我们有了两个猫对象,fluffstella。它们有相同的属性,nameweight,但值不同。

将以下代码添加到catClass.py以打印猫的名字:

catClass.py

print(fluff.name)
print(stella.name)

当你运行文件时,你将得到以下输出:

Fluff
Stella

这两个猫对象也可以访问相同的方法。它们都可以调用eat()函数。将以下代码添加到catClass.py

catClass.py

fluff.eat("tuna")
stella.eat("cake")

输出将如下所示:

Fluff is eating tuna
Stella is eating cake

编写类使得创建多个对象变得非常容易。让我们尝试在 Minecraft 中创建多个对象!

任务 #71:鬼镇

什么比一座幽灵房子更可怕?没错,两个幽灵房子。但三座幽灵房子会更可怕。而超过三座幽灵房子?我得停止这样想,否则今晚我就睡不着了!

在任务#69(第 263 页)中,你创建了一个可以建造消失房子的类。现在你可以使用相同的类创建多个对象,Python 会记住每个对象的属性和方法。你可以创建任意数量的房子,并轻松地让它们出现和消失。

你的任务是创建四个或更多幽灵房子对象,并将它们布置成一个村庄。在一定时间后,让它们全部消失,并在地图的其他地方重新出现,就像一个真正的鬼镇一样。

在 IDLE 中打开ghostHouse.py——我们将以此为基础。当你在ghostHouse.py程序中创建房子时,你的代码应该像这样:

ghostHouse.py

ghostHouse = Building(17, 22, -54, 10, 6, 8)
ghostHouse.build()

time.sleep(30)

ghostHouse.clear()

ghostHouse.py保存为一个名为ghostVillage.py的新文件,然后在文件中使用Building类创建三个或更多对象来构建村庄。为了帮助你入门,我在 Listing 12-4 中创建了一个名为shop的第二个对象。我还设置了xyz变量,用于存储玩家当前位置,这个位置通过player.getTilePos()来获取。这样可以更方便地在你周围构建村庄。

ghostVillage.py

pos = mc.player.getTilePos()
x = pos.x
y = pos.y
z = pos.z
ghostHouse = Building(x, y, z, 10, 6, 8)
shop = Building(x + 12, y, z, 8, 12, 10)
# Create more ghost building objects here

ghostHouse.build()
shop.build()
# Build more ghost building objects here

time.sleep(30)

ghostHouse.clear()
shop.clear()

Listing 12-4:创建多个幽灵建筑对象

Figure 12-4 展示了我的幽灵村庄。30 秒后,幽灵建筑突然消失。

image

Figure 12-4:看看幽灵村庄里的所有幽灵建筑!

类属性

有时你可能想为类中的每个对象实例设置具有相同值的属性。每次创建对象时传递相同的参数是冗余的。相反,你可以在类中创建一个预设的属性,类中所有对象实例将共享这些属性。

当多个对象共享相同的属性时,这被称为类属性。例如,我们创建的所有猫对象都由 Craig(我)拥有。我可以在catClass.py文件中重新访问Cat类,创建一个名为owner的类属性,并将其设置为"Craig"

catClass.py

class Cat(object):
    owner = "Craig"

    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

如你所见,类属性在名称前不使用self。在这个例子中,owner是一个类属性,而self.name是一个实例属性。注意,类属性是在__init__()函数之外定义的。

类属性与对象中的任何其他属性工作方式相同。例如,你可以像访问普通属性一样访问类属性的值。在这个例子中,为了找出 Fluff 的所有者,我们可以打印fluff对象的owner类属性:

catClass.py

fluff = Cat("Fluff", 4.5)
print(fluff.owner)

打印的值应该是"Craig"。如果我们打印 Stella 的所有者,值也会是一样的,因为类属性对该类中的每个对象都是相同的:

catClass.py

stella = Cat("Stella", 3.9)
print(stella.owner)

这里打印的值也是"Craig"

你可以改变单个对象的类属性值。这会改变该对象的属性值,但不会影响类中其他对象。例如,Stella 已被我的朋友 Matthew 收养,所以我们需要将 Stella 的所有者改为"Matthew"

catClass.py

stella.owner = "Matthew"
print(stella.owner)
print(fluff.owner)

当打印stellaowner属性时,它显示的是"Matthew",但是fluff的所有者仍然是"Craig"

在我们对catClass.py做了所有更改后,最终的程序如下所示。它也可以在本书的资源中找到,地址是www.nostarch.com/pythonwithminecraft/

catClass.py

class Cat(object):
    owner = "Craig"

    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

    def eat(self, food):
        self.weight = self.weight + 0.05
        print(self.name + " is eating " + food)

    def eatAndSleep(self, food):
        self.eat(food)
        print(self.name + " is now sleeping...")

    def getWeightInGrams(self):
        return self.weight * 1000

fluff = Cat("Fluff", 4.5)
print(fluff.owner)
stella = Cat("Stella", 3.9)
print(stella.owner)

print(fluff.weight)
fluff.eat("tuna")
fluff.eatAndSleep("tuna")

print(fluff.getWeightInGrams())
print(fluff.name)
print(stella.name)

fluff.eat("tuna")
stella.eat("cake")

stella.owner = "Matthew"
print(stella.owner)
print(fluff.owner)

现在你已经看到如何使用对象了,让我们来看一下如何通过继承让它们更强大。

理解继承

继承发生在类共享与其他类相同的方法和属性时。例如,鸭子是一种鸟类。它们与其他鸟类共享相同的方法(飞行、进食等),并且具有与其他鸟类相同的属性(体重、翼展等)。因此,你可以说鸭子鸟类继承了它们的属性和方法。图 12-5 展示了这种关系的示意图。

image

图 12-5:企鹅和鸭子都是鸟类的一种。

其他类继承的类称为父类;从父类继承的类称为子类

继承很有用,因为它允许你在相似的对象之间创建细微的差异。例如,企鹅也是一种鸟类,但它们可以在水下游泳,而不像大多数鸟类。为了表示企鹅,你需要创建一个从鸟类继承的子类,但该子类有适应性,使得企鹅能够在水下游泳。这些适应性是你创建子类的原因:你可以保留父类的主要特征,以避免重复编写代码,并且仅在子类中添加所需的方法和属性。

继承类

当子类从父类继承时,子类可以使用父类的所有方法和属性。子类还可以添加额外的类和属性,而无需修改原始父类。

我们将用鸟类的例子来说明这一点。首先,我们将编写Bird父类的代码。打开 IDLE 中新建一个文件,命名为birdClass.py,然后添加以下代码来创建类:

birdClass.py

➊ class Bird(object):
➋     def __init__(self, name, wingspan):
           self.name = name
           self.wingspan = wingspan

➌     def birdcall(self):
           print("chirp")

➍     def fly(self):
           print("flap")

我们创建了一个名为Bird的类➊,但请注意,Bird类继承自objectobject类是一个基类,所有其他类都将基于它来构建。所有类都继承自object类,当没有其他父类可供继承时,你就会使用它。即使存在多个继承层次,许多类彼此继承,object类始终是继承体系中最高层次的父类。

Bird类的__init__()方法有两个参数,用于设置两个属性:鸟的name和它的wingspan ➋。它有两个方法:birdcall() ➌和fly() ➍。目前,birdcall()方法只是打印"chirp",而fly()方法只是打印"flap"

在同一文件中,使用Bird类创建一个名为gardenBird的对象:

birdClass.py

gardenBird = Bird("Geoffrey", 12)
gardenBird.birdcall()
gardenBird.fly()

这段代码将输出:

chirp
flap

现在你已经创建了一个超类,你可以创建一个继承超类的子类,并且该子类有自己的方法。在下一部分中你将实现这一点。

向子类添加新方法

让我们为企鹅在birdClass.py中添加一个类,命名为Penguin。因为企鹅可以在水下游泳,所以你可以向Penguin类添加一个名为swim()的额外方法:

birdClass.py

class Penguin(Bird):
    def swim(self):
        print("swimming")

当你定义一个子类并希望它继承另一个超类而不是object时,你需要在括号中放入超类的名称。请注意,我没有为Penguin类创建__init__()方法。原因是它继承了Bird类,因此使用的是Bird类的__init__()方法。让我们使用该__init__()方法,并通过创建企鹅来测试swim()函数:

birdClass.py

sarahThePenguin = Penguin("Sarah", 10)
sarahThePenguin.swim()

这段代码将输出以下内容:

swimming

Penguin类也可以使用fly()birdcall()方法,因为它继承了这些方法来自Bird

birdClass.py

sarahThePenguin.fly()
sarahThePenguin.birdcall()

在这种情况下,输出将是这样的:

flap
chirp

但是flapchirp对于企鹅来说没有意义,因为企鹅不能飞,而且它们的鸟鸣更像是嘎嘎声!我们将在《重写方法和属性》中学习如何重写继承的方法并修复这个问题,参见第 278 页。

但是首先,让我们回到 Minecraft,并利用继承创建一些新的鬼建筑。

任务 #72: 鬼酒店

房屋和酒店都是建筑物类型:它们有门、窗、房间、楼梯和墙壁。酒店只是豪华的房子,增加了阳台、更多的房间和一个漂亮的入口。

如何利用你已经为鬼屋创建的代码编写一些鬼酒店?建筑物的基本结构是相同的。所以可以说唯一的区别是鬼酒店在房间内有创建地毯和在建筑物边缘添加花卉的额外方法。这意味着鬼酒店类可以继承鬼屋类的所有方法。然后,鬼酒店类只需要两个额外的方法来处理地毯和花卉。

在 IDLE 中,创建一个新文件并将其保存为ghostHotel.py,保存在classes文件夹中。将ghostHouse.py程序中Building类的代码复制并粘贴到该文件中。

创建一个名为FancyBuilding的新类,它继承自Building类。FancyBuilding类应该有一个新的方法upgrade(),该方法在建筑物内部添加地毯,并在墙壁周围种植花卉。代码清单 12-5 展示了我的upgrade()方法的代码,但你可以根据需要自定义你的酒店。

ghostHotel.py

# Create a FancyBuilding class here

    def upgrade(self):
        # Carpet
        mc.setBlocks(self.x + 1, self.y, self.z + 1,
                     self.x + self.width - 1, self.y, self.z + self.depth - 1,
                     35, 6)

        # Flowers
        mc.setBlocks(self.x - 1, self.y, self.z -1,
                     self.x - 1, self.y, self.z + self.depth + 1,
                     37)
        mc.setBlocks(self.x - 1, self.y, self.z - 1,
                     self.x + self.width + 1, self.y, self.z – 1,
                     37)
        mc.setBlocks(self.x + self.width + 1, self.y, self.z - 1,
                     self.x + self.width + 1, self.y, self.z + self.depth + 1,
                     37)
        mc.setBlocks(self.x - 1, self.y, self.z + self.depth + 1,
                     self.x + self.width + 1, self.y, self.z + self.depth = 1,
                     37)

# Create an instance of the FancyBuilding class
# Call the build() and upgrade() methods

代码清单 12-5:为FancyBuilding类添加地毯和花卉的一个方法

在你创建了类并添加了新方法后,创建一个FancyBuilding类的实例,并命名为ghostHotel。使用build()方法构建幽灵酒店,然后使用upgrade()方法添加额外的元素。

图 12-6 展示了我的华丽幽灵酒店。

image

图 12-6:看看那些花和地毯!

附加目标:华丽的村庄

在任务 #71 中,你创建了一个幽灵村庄,其中所有建筑物看起来差不多。在现实中的城市中,很少会看到完全相同的建筑。通过创建多个从Building类继承的类,来修改这个幽灵村庄程序。你可以创建一个Shop类,一个Hospital类,一个Restaurant类,例如。然后在创建对象时,你可以通过使用不同的类来选择创建哪种类型的建筑。

重写方法和属性

子类可以重新定义其父类中的方法和属性。这在你希望使用相同名称的函数,但希望在子类中表现得不同的时候非常有用。

在《理解继承》一节中,第 273 页,我们创建了一个Bird类和一个Penguin类。Penguin类继承自Bird,因此它共享所有父类的方法。但是,企鹅不能飞,它们的鸟叫声更像是呱呱声,而不是鸟鸣。因此,我们应该修改fly()birdcall()方法以反映这一点。打开birdClass.py并添加以下代码:

birdClass.py

   class Penguin(Bird):
       def swim(self):
           print("swimming")

➊     def birdcall(self):
           print("sort of a quack")

➋     def fly(self):
           print("Penguins cannot fly :(")

我对Penguin类做了两个修改。我添加了一个birdcall() ➊方法和一个fly() ➋方法。由于这两个方法的拼写与Bird父类中的方法相同,因此它们会重写父类中的方法。

通过将以下代码添加到birdClass.py中来调用这些方法:

birdClass.py

sarahThePenguin.fly()
sarahThePenguin.birdcall()

现在,当你运行程序时,你会看到以下输出:

Penguins cannot fly :(
sort of a quack

重写父类中的方法会改变该方法对子类的作用,但不会影响父类。因此,企鹅不能飞,但其他继承自Bird的鸟类仍然可以飞。

你还可以在子类中重写__init__()方法。这意味着当子类对象被创建时,它可以有不同于父类的属性或行为。

例如,假设我们在同一个文件中创建一个Parrot类,作为Bird类的子类。鹦鹉可以有不同的颜色,所以我们在__init__()方法中添加一个color属性作为额外的参数:

birdClass.py

   class Parrot(Bird):
➊     def __init__(self, name, wingspan, color):
           self.name = name
           self.wingspan = wingspan
           self.color = color

我已为Parrot类添加了一个新的__init__()方法,与原始的Bird类相比,它有一个额外的参数color➊。

现在,当我们创建一个新的Parrot对象时,我们可以访问color属性。我们还可以访问birdcall()fly()方法,因为它们是从Bird超类继承来的:

birdClass.py

freddieTheParrot = Parrot("Freddie", 12, "blue")
print(freddieTheParrot.color)
freddieTheParrot.fly()
freddieTheParrot.birdcall()

这段代码将输出以下内容:

blue
flap
chirp

记住,你可以重写任何子类继承自超类的方法;甚至可以重写__init__()方法。这使你可以对对象及其许多属性和方法有很大的控制权。

在我们对birdClass.py做出所有更改后,最终的程序如下所示。它也可以在本书的资源中找到,网址是www.nostarch.com/pythonwithminecraft/

birdClass.py

class Bird(object):
    def __init__(self, name, wingspan):
        self.name = name
        self.wingspan = wingspan

    def birdcall(self):
        print("chirp")

    def fly(self):
        print("flap")

class Penguin(Bird):
    def swim(self):
        print("swimming")

    def birdcall(self):
        print("sort of a quack")

    def fly(self):
        print("Penguins cannot fly :(")

class Parrot(Bird):
    def __init__(self, name, wingspan, color):
        self.name = name
        self.wingspan = wingspan
        self.color = color

gardenBird = Bird("Geoffrey", 12)
gardenBird.birdcall()
gardenBird.fly()

sarahThePenguin = Penguin("Sarah", 10)
sarahThePenguin.swim()
sarahThePenguin.fly()
sarahThePenguin.birdcall()

freddieTheParrot = Parrot("Freddie", 12, "blue")
print(freddieTheParrot.color)
freddieTheParrot.fly()
freddieTheParrot.birdcall()

在下一个任务中,你将尝试重写方法和属性。

任务 #73:幽灵树

你已经创建了几种形式的幽灵建筑。让我们将其提升到一个新的层次,创建一棵幽灵树。这是个了不起的想法,但我们该怎么做呢?Building类是为建筑物设计的,建筑物有墙壁和天花板——而树没有墙壁或天花板。别担心!你可以通过修改你的幽灵Building类来解决这个问题。

像幽灵建筑一样,幽灵树将通过build()clear()方法出现和消失。但由于树与房屋不同,方法需要进行不同的处理。所以,你需要创建一个从Building类继承的类,然后重写build()clear()方法。

为了让你开始,我从forest.py文件中提取了创建树的函数(第 149 页),并将其放入清单 12-6。将其复制到classes文件夹中的新文件ghostTree.py中。

ghostTree.py

   from mcpi.minecraft import Minecraft
   mc = Minecraft.create()

   # Paste the ghostHouse.py program here
   # Create a Tree class here

➊ def growTree(x, y, z):
       """ Creates a tree at the coordinates given """
       wood = 17
       leaves = 18

       # Trunk
       mc.setBlocks(x, y, z, x, y + 5, z, wood)

       # Leaves
       mc.setBlocks(x - 2, y + 6, z - 2, x + 2, y + 6, z + 2, leaves)
       mc.setBlocks(x - 1, y + 7, z - 1, x + 1, y + 7, z + 1, leaves)

   # Create build() and clear() methods for the Tree class here

清单 12-6:创建树的函数

为了完成程序,将ghostHouse.py中的Building类代码复制并粘贴到新文件中。然后,创建一个名为Tree的新类,继承自Building类。在Tree类内部,添加一个build()方法和一个clear()方法,以重写Building类中的方法,从而建立一棵树而不是房子。确保在最终的growTree()方法中,属性前面加上self参数➊。

创建完程序后,创建一个名为ghostTreeTree对象。调用build()方法让树出现,稍等片刻,然后使用clear()让它消失。

图 12-7 显示了我的程序结果。

image

图 12-7:那是一棵诡异的树!

附加目标:幽灵森林

修改ghostTree.py中的代码,创建一个幽灵森林。你认为在幽灵森林中可以找到什么样的宝藏?

你学到的内容

你刚刚学习了当今编程中最重要的概念之一:面向对象编程!你学会了如何编写类和创建对象,也学会了如何使用继承来定制类和对象的行为。你将能够将这一非常有用的技能应用到不仅是 Minecraft 中,还可以在你选择的任何编程冒险中!

第十三章:后记

image

这是一个具有里程碑意义的时刻。你刚刚完成了这本书。对我来说,对你来说,都是一段漫长的旅程!自从我开始写这本书,我已经长了几次胡须,住过三个不同的城市,还发现香蕉并不产籽。说实话,写这本书的过程我非常开心。有时它很艰难、很累,但我坚持下来了,因为我希望你能读到它。

自从你开始阅读这本书以来,你已经覆盖了很多内容。你学习了 Python 编程的基础,并且编写了一些非常酷的程序,利用这些概念在 Minecraft 中做出令人惊叹的事情。你学习了变量、数学运算、字符串、输入、布尔值、if语句、whilefor循环、函数、列表和字典、模块、文件和类。你可能在开始这本书时还是一个完全的初学者,但现在有了这些知识,你将能够在 Python 中做一些非常高级的事情。

无论你将来选择做什么编程相关的事情,我真诚地祝你好运。对我来说,编程一直是一个非常棒的爱好,幸运的是,它变成了我的全职工作。

如果我们有机会见面,一定要互相击个掌!

第十四章:方块 ID 小抄

当列出两个数字时,第二个数字表示方块状态。标有星号()的方块在 Minecraft:Pi Edition 中可用。*

image 196 金合欢门方块
image 161 金合欢树叶
image 6, 4 金合欢树苗
image 162 金合欢木
image 5, 4 金合欢木板材
image 126, 4 金合欢木板
image 125, 4 金合欢木板(双)
image 157 激活轨道
image 38, 2 蓝宝石百合
image 1, 5* 安山岩
image 38, 3 湛蓝小花
image 138 信标
image 26*
image 7* 基岩
image 194 白桦木门方块
image 18, 2* 白桦树叶
image 6, 2* 白桦树苗
image 17, 2* 白桦木
image 5, 2* 白桦木板材
image 126, 2 白桦木板
image 125, 2 白桦木板(双)
image 38, 1 蓝色兰花
image 47* 书架
image 117 酿造架
image 44, 4* 砖石板
image 43, 4* 砖石板(双)
image 108* 砖石楼梯
image 45* 砖块
image 39* 褐色蘑菇
image 99 褐色蘑菇方块
image 62* 燃烧的熔炉
image 81* 仙人掌
image 92 蛋糕方块
image 171, 15 地毯,黑色
image 171, 11 地毯,蓝色
image 171, 12 地毯,棕色
image 171, 9 地毯,青色
image 171, 7 地毯,灰色
image 171, 13 地毯,绿色
image 171, 3 地毯,浅蓝色
image 171, 8 地毯,浅灰色
image 171, 5 地毯,石灰色
image 171, 2 地毯,品红色
image 171, 1 地毯,橙色
image 171, 6 地毯,粉色
image 171, 10 地毯,紫色
image 171, 14 地毯,红色
image 171 白色地毯
image 171, 4 黄色地毯
image 141 胡萝卜
image 118
image 54* 箱子
image 155, 1* 雕刻石英块
image 179, 1 雕刻红沙岩
image 24, 1 雕刻沙岩
image 98, 3 雕刻石砖
image 82* 黏土
image 173 煤炭块
image 16* 煤矿石
image 3, 1* 粗糙泥土
image 4* 圆石
image 44, 3* 圆石台阶
image 43, 3* 圆石台阶(双)
image 67* 圆石阶梯
image 30* 蜘蛛网
image 127 可可
image 98, 2* 裂石砖
image 58* 工作台
image 37* 蒲公英
image 197 深色橡木门块
image 161, 1 深色橡木叶子
image 6, 5 深色橡木树苗
image 162, 1 深色橡木木材
image 5, 5 深色橡木木板
image 126, 5 深色橡木台阶
image 125, 5 深色橡木台阶(双)
image 168, 2 深色海晶石
image 151 日光传感器
image 32 枯萎灌木
image 31* 枯萎灌木丛
image 28 探测轨道
image 57* 钻石块
image 56* 钻石矿石
image 1, 3* 花岗岩
image 3* 泥土
image 23 发射器
image 122 龙蛋
image 158 投掷器
image 133 翡翠块
image 129 翡翠矿石
image 116 附魔台
image 119 末地传送门
image 120 末地传送门框架
image 121 末地石
image 60* 耕地
image 31, 2* 蕨类植物
image 51
image 140 花盆
image 10* 流动的岩浆
image 8* 流动水
image 61* 炉子
image 20* 玻璃
image 102* 玻璃板
image 95, 15* 玻璃,黑色染色
image 95, 11* 玻璃,蓝色染色
image 95, 12* 玻璃,棕色染色
image 95, 9* 玻璃,青色染色
image 95, 7* 玻璃,灰色染色
image 95, 13* 玻璃,绿色染色
image 95, 3* 玻璃,浅蓝色染色
image 95, 8* 玻璃,浅灰色染色
image 95, 5* 玻璃,石灰色染色
image 95, 2* 玻璃,洋红色染色
image 95, 1* 玻璃,橙色染色
image 95, 6* 玻璃,粉红色染色
image 95, 10* 玻璃,紫色染色
image 95, 14* 玻璃,红色染色
image 95* 玻璃,白色染色
image 95, 4* 玻璃,黄色染色
image 74* 发光的红石矿石
image 89* 发光石
image 41* 金块
image 14* 金矿石
image 1, 1* 花岗岩
image 2*
image 31, 1*
image 13* 砾石
image 172 烧结黏土
image 170 干草捆
image 154 漏斗
image 79* 冰块
image 178 倒置的日光感应器
image 101 铁栅栏
image 42* 铁块
image 71* 铁门块
image 15* 铁矿石
image 167 铁制陷门
image 91 南瓜灯
image 84 唱机
image 195 热带丛林门块
image 18, 3 热带丛林叶子
image 6, 3 热带丛林树苗
image 17, 3 热带丛林木材
image 5, 3 热带丛林木板
image 126, 3 热带丛林木板(双层)
image 125, 3 热带丛林木板(双层)
image 65* 梯子
image 22* 青金石块
image 21* 青金石矿石
image 175, 3 大蕨
image 69 杠杆
image 175, 1 丁香
image 111 睡莲
image 103* 西瓜块
image 105* 西瓜茎
image 52 怪物生成器
image 48* 苔石
image 98, 1* 苔石砖
image 110 草木灰
image 112* 下界砖
image 44, 6* 下界砖板
image 43, 6* 下界砖板(双倍)
image 114* 下界砖楼梯
image 90 下界传送门
image 153 下界石英矿石
image 115 下界疣
image 87 下界岩
image 25 音符块
image 64* 橡木门块
image 85* 橡木栅栏
image 107* 橡木栅栏门
image 18* 橡树叶
image 6* 橡木树苗
image 17* 橡木
image 5* 橡木木板
image 126 橡木板
image 125 橡木板(双倍)
image 53* 橡木楼梯
image 49* 黑曜石
image 38, 5 橙色郁金香
image 38, 8 牛眼雏菊
image 174 压缩冰
image 160, 15 黑色染色窗格
image 160, 11 蓝色染色窗格
image 160, 12 棕色染色窗格
image 160, 9 青色染色窗格
image 160, 7 灰色染色窗格
image 160, 13 绿色染色窗格
image 160, 3 浅蓝色染色窗格
image 160, 8 浅灰色染色窗格
image 160, 5 青柠染色窗格
image 160, 2 品红色染色窗格
image 160, 1 橙色染色窗格
image 160, 6 粉色染色窗格
image 160, 10 紫色染色窗格
image 160, 14 红色染色窗格
image 160 面板,白色染色
image 160, 4 面板,黄色染色
image 175, 5 牡丹
image 155, 2* 柱状石英块
image 38, 7 粉色郁金香
image 33 活塞
image 34 活塞头
image 3, 2* 腐植土
image 1, 6* 抛光安山岩
image 1, 4* 抛光闪长岩
image 1, 2* 抛光花岗岩
image 38 罂粟
image 142 土豆
image 27 激活铁轨
image 148 压力板(重)
image 147 压力板(轻)
image 168 海晶
image 168, 1 海晶砖
image 86 南瓜
image 104 南瓜秧
image 155* 石英块
image 44, 7* 石英台阶
image 43, 7* 石英台阶(双层)
image 156* 石英楼梯
image 66 铁轨
image 40* 红蘑菇
image 100 红蘑菇块
image 12, 1 红砂
image 179 红砂岩
image 182 红砂岩台阶
image 181 红砂岩台阶(双层)
image 38, 4 红郁金香
image 152 红石块
image 124 红石灯(亮)
image 123 红石灯(不亮)
image 73* 红石矿
image 93 红石中继器(熄灭)
image 94 红石中继器(开启)
image 75 红石火把(熄灭)
image 76 红石火把(点亮)
image 175, 4 玫瑰丛
image 12* 沙子
image 24* 沙岩
image 44, 1* 沙岩台阶
image 43, 1* 沙岩台阶(双层)
image 128* 沙岩楼梯
image 169 海洋灯笼
image 165 蜘蛛块
image 179, 2 光滑红砂岩
image 24, 2 光滑沙岩
image 78*
image 80* 雪块
image 88 灵魂沙
image 19 海绵
image 193 云杉木门块
image 18, 1* 云杉树叶
image 6, 1* 云杉树苗
image 17, 1* 云杉木
image 5, 1* 云杉木板
image 126, 1 云杉木台阶
image 125, 1 云杉木台阶(双层)
image 63* 立式标志块
image 29 粘性活塞
image 11* 静态熔岩
image 9* 静态水
image 1* 石头
image 44, 5* 石砖台阶
image 43, 5* 石砖台阶(双层)
image 109* 石砖楼梯
image 98* 石砖
image 70 石质压力板
image 44* 石板
image 43* 石板(双层)
image 83* 甘蔗
image 175 向日葵
image 46* TNT
image 46, 1* TNT, 手动引爆
image 175, 2 高草(双层)
image 50* 火把
image 132 绳网
image 131 绳线钩
image 106 藤蔓
image 68* 壁挂标志块
image 19, 1 湿海绵
image 59* 小麦作物
image 38, 6 白色郁金香
image 72 木质压力板
image 44, 2* 木制台阶
image 43, 2* 木制台阶(双层)
image 96 木制活板门
image 35, 15* 羊毛, 黑色
image 35, 11* 羊毛, 蓝色
image 35, 12* 羊毛, 棕色
image 35, 9* 羊毛, 青色
image 35, 7* 羊毛, 灰色
image 35, 13* 羊毛, 绿色
image 35, 3* 羊毛, 淡蓝色
image 35, 8* 羊毛, 浅灰色
image 35, 5* 羊毛, 绿黄色
image 35, 2* 羊毛, 品红色
image 35, 1* 羊毛,橙色
image 35, 6* 羊毛,粉色
image 35, 10* 羊毛,紫色
image 35, 14* 羊毛,红色
image 35* 羊毛,白色
image 35, 4* 羊毛,黄色
posted @ 2025-11-26 09:19  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报