创造你自己的-Python-文本冒险-全-
创造你自己的 Python 文本冒险(全)
一、入门指南
介绍
你可能听过互联网上的口号,“学会编码!学会编码!”,并且您已经了解到 Python 是一个很好的起点…但是现在该怎么办呢?很多想编程的人不知道从何下手。你可以用代码创造“任何东西”的想法是令人麻痹的。这本书提供了一个明确的目标:通过创建文本冒险来学习 Python。
这本书将教你编程的基础,包括如何组织代码和一些编码的最佳实践。在书的结尾,你将会有一个工作的游戏,你可以玩或者向朋友炫耀。你还可以通过编写不同的故事线来改变游戏,使之成为你自己的游戏,包括添加新物品,创造新角色等。
学习编程是一项令人兴奋的努力,但一开始会感到畏惧。然而,如果你坚持下去,你可以成为一名职业程序员或周末爱好者,或者两者兼而有之!我的故事与许多程序员的故事相似:我编程的第一件事是 QBASIC 中的一个数字猜谜游戏,现在编程是我的工作。我希望你也能加入我们,我感谢你选择这本书作为起点。
这本书是给谁的
这本书是为以前从未编程的人或初学 Python 的程序员而写的。如果你属于第二组,你可能会浏览一些早期的材料。
虽然这是面向初学者的,但我确实假设您了解计算机基础知识,如打开命令提示符、安装软件等。如果你遇到什么困难,在网上搜索“如何在[操作系统]上做[事情]”通常会帮你解决。对程序员特别有用的网站是 stack overflow(http://stackoverflow.com
)1和超级用户( http://superuser.com
), 2 所以如果你在搜索结果中看到它们,先给它们一个机会。
如何使用这本书
在这本书的每一章中,你将在创造你的文本冒险的总体目标上取得进展。前几章可能看起来很慢,因为它们侧重于学习 Python 的基础知识。事情会在本书的后半部分有所好转,那时焦点会转向游戏世界的构建。
我建议在你的电脑上或旁边阅读这本书,这样你就可以轻松地在阅读和编写代码之间来回切换。这本书前半部分的每一章都将以家庭作业部分结束。这些问题主游戏不会要求,但你至少应该试一试。在适用的情况下,本书末尾提供了解决方案。
本书中的大部分 Python 代码如下所示:
1 greeting = "Hello, World!"
2 print(greeting)
打算输入到交互式 Python 会话中的代码(参见第三章)将如下所示:
>>> greeting = "Hello, World!"
>>> print(greeting)
内联出现的代码或命令的引用将appear like this
。你应该学习的专业术语是这样出现的。
如果你遇到困难,你可以在这里下载本书每一章的代码。 3 忍住复制粘贴一切的冲动!如果你把密码打出来,你会记住更多的信息。然而,我们都会犯错误,所以如果您不能找出问题所在,可以将您的代码与我的代码进行比较。如果你真的确定一切都是一样的,用 DiffChecker 4 或 Mergely 这样的在线比较工具再检查一遍。 5 你还可以查看附录 B 中一些你可能会遇到的常见错误。
最后,这个游戏是你的游戏。它是完全可定制的,如果你觉得增加更多的房间和敌人,改变故事,增加难度等都很舒服。,请这样做。我会指出这样的定制机会:
Customization Point
关于定制的一些注意事项。
请记住,每一章都是建立在最后一章的基础上的,所以如果您偏离材料太远,您可能希望将您的定制代码保存在另一个目录中,以便您可以继续从源材料中学习。
设置您的工作空间
不要跳过这一节!在开始编写本书中的代码之前,您需要确保一切都设置妥当。如果配置不当,很多问题会等着你。
Python 版本
Python 的创造者决定 Python 3 不会向后兼容 Python 2。虽然 Python 3 在 2008 年发布,但一些人仍然坚持使用 Python 2。初学者没有理由从 Python 2 开始,所以这本书是用 Python 3 写的。不幸的是,一些操作系统与 Python 2 捆绑在一起,这使得安装和使用 Python 3 有点棘手。如果你遇到麻烦,网上有大量针对你的具体操作系统的详细说明。
安装 Python
根据您的操作系统和您使用的包管理器(如果有的话),有许多安装 Python 的方法。
Windows 操作系统
在 Windows 上安装 Python 的一个优点是,您不需要担心已经存在的旧版本。Windows 没有标准的包管理器,所以你需要从 Python 下载安装程序。
- 在浏览器中打开
http://python.org/download/
,下载最新的 Windows 3 . x . y 安装程序。 - 运行安装程序。
- 在安装程序的第一个屏幕上,您会看到一个包含 Python 3 的选项。路径上的 x。一定要选中那个框。
- 继续安装;默认设置就可以了。如果您在安装程序中看到另一个将 Python 添加到环境变量的选项,请确保也选中了该框。
Mac OS X
根据我的经验,在 Mac OS X 上安装开发者工具最简单的方法就是使用家酿 6 包管理器( http://brew.sh
)。但是,我很欣赏你可能不想装东西去装别的东西!我将首先提供自制步骤,然后是更传统的途径。
使用自制软件:
- 打开一个终端。
- 在终端运行
http://brew.sh
命令安装家酿。 - 用下面的命令安装 Python 3:
brew install python3
。
现在,您可以在任何想要使用 Python 的时候使用命令python3
。命令python
指向 Python 的默认 Mac OS X 安装,即版本 2.7.5。
使用安装程序:
- 在浏览器中打开
http://python.org/download/
,下载 Mac OS X 最新的 3.x.y 安装程序 - 打开下载包,然后运行
Python.mpkg
。 - 按照安装向导进行操作。默认设置就可以了。
现在,您可以在任何想要使用 Python 的时候使用命令python3
。命令python
指向 Python 的默认 Mac OS X 安装,即版本 2.7.5。
Linux 操作系统
如果您使用的是 Linux,那么您可能已经习惯使用发行版的包管理器了,所以我就不赘述了。通常,像sudo apt-get install python3
或sudo yum install python3
这样的东西会得到你想要的。也有可能您的发行版已经包含了 Python 3。如果其他方法都失败了,你可以从官方网站( https://www.python.org/downloads/source/
)下载源代码并编译 Python。 7
验证您的安装
要验证您的安装,请打开命令提示符/终端(我将交替使用控制台、命令提示符和终端),并尝试这两个命令:
python version
python3 version
有四种可能性:
- 两者都显示版本号:太好了,您的计算机上同时安装了 Python 2 和 3。只要确保你总是用
python3
运行本书中的代码。 - 只有 Python 显示版本号:如果版本中的第一个数字是 3,就像在“Python 3.5.1”中一样,那就没问题。如果它是 Python 2 版本,如“Python 2.7.10”,那么 Python 3 没有正确安装。尝试重复安装,如果还是不行,你可能需要调整你的
PATH
来指向 Python 3 而不是 Python 2。 - 只有 Python3 显示一个版本号:太好了,你安装了 Python 3。只要确保你总是用
python3
运行本书中的代码。 - 都不显示版本号:Python 没有正确安装。尝试重复安装。如果这仍然不起作用,您可能需要调整您的
PATH
来包含 Python 安装的位置。
Footnotes 1
2
3
https://www.dropbox.com/sh/udvdkxtjhtlqdh1/AAD9HOD6VTb5RGFZ7kBv-ghua?dl=0
4
5
6
7
https://www.python.org/downloads/source/
二、你的第一个程序
当你在电脑上打开一个应用程序时,比如一个互联网浏览器,在最低层,CPU 正在执行指令来移动字节的信息。早期的程序都是辛辛苦苦写在打孔卡上的,如图 2-1 所示。
图 2-1。
An early punch card Credit: Wikipedia user Harke
谢天谢地,我们已经对计算机编程进行了几十年的改进,使得编写这些指令变得容易多了!现在,编程语言处于“低级”到“高级”的范围内,像 C 和 C++这样的语言是低级的,像 Python 和 Ruby 这样的语言是高级的。按照设计,高级语言允许程序员忽略计算机程序的许多幕后细节。这也是 Python 经常被推荐为首选编程语言的原因之一。
首先,在你的电脑上创建一个文件夹,在那里你可以完成游戏的所有工作。从现在开始,这个目录将被称为项目的根目录。
创建模块
Python 代码被组织成称为模块的文件。每个模块通常包含大量逻辑上相关的代码。例如,我们的项目将包含一个运行游戏的模块,另一个包含管理敌人的代码的模块,另一个用于世界,等等。要创建您的第一个模块,导航到您的根目录并创建一个名为game.py
的空文件。
编写代码
编写代码时,严格按照本书中出现的内容编写代码是非常重要的。但是,我不建议简单的复制粘贴。尤其是刚开始的时候,肌肉记忆会帮助你学得更快。如果你遇到错误,逐行检查你的代码,检查错别字、大小写错误、符号错位等。如果你真的不能解决问题,那么也只有这样,复制代码才是正确的。但是一定要仔细阅读粘贴的代码,找出错误。
这里我需要提到 Python 语法中一个更有争议的部分:有意义的空白。许多语言会忽略空格和制表符,但 Python 不会。这意味着您可能会遇到由您(很容易)看不到的字符引起的问题!因此,您需要决定是否使用制表符或空格来缩进代码。大多数 Python 程序员都选择使用空格,所以我将坚持使用四个空格来缩进本书的代码。如果您选择使用制表符并复制代码,您必须将缩进切换到制表符!一些文本编辑器可以通过工具栏命令来完成这项工作。如果您的没有,您应该能够用“\t”(表示“tab”)替换四个空格。
记住这一点,让我们编写第一行代码。打开game.py
并添加以下行:
print("Escape from Cave Terror!")
Customization Point
您可以通过替换引号内的文本来更改游戏的名称。想想你的游戏发生的场景。是中世纪的森林,外星飞船,还是犯罪猖獗的城市?
运行 Python 程序
现在,我们将执行刚刚编写的代码。首先打开命令提示符或终端,然后使用cd
命令导航到您的项目根目录。比如cd ~/Documents/code/learn-python-game
或者cd C:\Code\my_python_adventure
。最后,运行以下命令:
python game.py
(注意:根据您安装 Python 的方式,您可能需要运行python3 game.py
。)
如果一切顺利,您应该看到"Escape from Cave Terror!"
被打印到控制台。恭喜你!你刚刚写了你的第一个 Python 程序。
家庭作业
尝试以下作业练习:
- 制作一个名为
calculator.py
的新模块,并编写将"Which numbers do you want to add?"
输出到控制台的代码。 - 运行计算器程序,确保它工作正常。
- 尝试从代码中删除引号。会发生什么?
三、倾听你的用户
所有的计算机程序都有一定程度的用户输入。有些可能只是要求用户启动应用程序,而另一些只是耐心等待,直到用户告诉它做一些事情。由于这个应用程序是一个文本冒险,它更接近于“耐心等待”的范围。在本章中,您将学习如何阅读和处理用户在命令提示符下键入的指令。
你的朋友:标准输出和标准输入
根据定义,文本冒险需要用户输入程序的文本指令。作为响应,程序将向用户显示文本。这是命令行应用程序中的常见模式。
为了对此有所了解,让我们演示一个已经安装的命令行应用程序——Python。没错,python
命令不仅仅可以运行程序。打开命令提示符并运行python
。您应该会看到类似这样的内容:
$ python
Python 3.4.1 (default, May 8 2015, 22:07:39)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.49)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
现在,在光标处,键入以下内容并按 Enter 键:
help(print)
您应该看到这个:
>>> help(print)
Help on built-in function print in module builtins:
[...]
要退出该视图,只需按下q
。
我们只是通过输入两个命令与这个命令行应用程序进行交互:help(print)
和q
。这两个命令都由 Python 读入、解释和响应。
当应用程序向控制台写出文本时,这被称为写入标准输出或简称为 stdout。类似地,当用户(甚至另一个应用程序)将文本写入控制台时,这被称为写入标准输入或标准输入。
事实上,如果您回到控制台并再次键入help(print)
,您将会看到文档引用了sys.stdout
。默认情况下,print
函数将文本写入标准输出。您已经在运行游戏时看到了这一点——应用程序向控制台显示了介绍文本。
现在尝试输入help(input)
。您可以看到input
函数将“从标准输入中读取一个字符串”这正是我们所寻求的,让我们的应用程序能够倾听用户。
要退出,按下q
,然后键入quit()
。这应该会把你带回一个常规的命令提示符。
从标准输入读取
打开一个新文件,另存为echo.py
。输入以下行:
input("Type some text: ")
保存文件并使用python echo.py
运行。请记住,要运行应用程序,您必须位于包含该文件的同一目录下。
希望您看到提示"Type some text"
。继续按它说的做,然后按回车键。看起来好像程序忽略了你,把你扔回到命令提示符下。刚刚发生了什么?input
命令将提示文本("Type some text"
)打印到标准输出,并读入您对标准输入的响应。因为没有更多的指令,应用程序简单地退出。
正如这个文件的名字所暗示的那样,我们将创建一个向用户反馈信息的应用程序,但是首先我们需要学习如何存储临时信息。
保存信息
在上一个练习中,我们能够从用户那里读入信息,但是我们不能对它做任何事情。我们需要暂时保存这些输入,以便可以打印出来。可以使用变量将临时信息存储在计算机的内存中,也可以从内存中访问临时信息。
Python 中变量名的一些例子有n
、my_number
和address
。要在变量中存储信息,我们只需使用=
操作符。这被称为赋值操作符。例如:
1 n = 5
2 my_number = 3
3 address = '123 Maple St.'
然后,每当我们需要调用这些信息时,我们可以参考变量名称,如print(n)
或print(address)
。让我们练习一下。
返回您的终端并运行python
。当我们之前这样做时,我们使用了help
命令来获取关于函数的信息。也许更有用的特性是能够输入 Python 代码并立即执行。这被称为“Python 解释器”、“Python shell”或“Python REPL”(Read Evaluate Print Loop 的缩写)。
继续输入以下命令:
>>> address = '123 Maple St.'
>>> print(address)
你应该看到"123 Maple St."
被打印出来。这是因为我们使用赋值操作符将值“123 Maple St .”赋给了变量address
。然后当print
函数运行时,它在内存中查找address
的真实值,这样它就知道要打印出什么。
有了这些信息,我们现在可以用我们的 echo 程序做一些更有趣的事情。返回echo.py
,按如下方式更改代码:
1 user_input = input("Type some text: ")
2 print(user_input)
再次运行这个程序,验证它是否能回显您输入的任何文本。让我们做一些类似于我们游戏的事情。打开game.py
并添加以下几行:
2 action_input = input('Action: ')
3 print(action_input)
数据类型
在我们结束这一章之前,我们需要简要回顾一些数据类型。到目前为止,我们主要看到了像"Type some text"
和"123 Maple St."
这样的文本数据。这些被称为字符串 1 ,Python 知道它们是字符串,因为它们在代码中被单引号或双引号包围。input
返回的数据也是一个字符串。以下是一些字符串示例:
1 name = 'Phillip'
2 forecast = "It's going to rain!"
3 url = 'http://letstalkdata.com'
下一个最常见的数据类型是整数。如果你还记得数学课的话,整数就是像 1,15,-99 和 0 这样的数字。在 Python 中,整数作为数字输入,没有任何额外的符号。
1 a = 3
2 b = 4
3 hypotenuse = 5
有小数点的数称为浮点数,或简称为 float。浮点数的输入类似于整数,除了它们包含小数点。
1 a = 3.0
2 b = 4.0
3 hypotenuse = 5.0
您可以像预期的那样对数字执行基本的数学运算。在解释器中尝试其中的一些:
>>> 5 + 6
>>> 0 - 99
>>> 5.0 / 2.0
>>> 5 / 2
>>> 4 * (7 - 2)
Python 内置了很多很多的数据类型,但是现在需要注意的重要一点是,Python 是根据您的输入方式来推断类型的。my_variable = 5
和my_variable = '5'
差别很大!
家庭作业
尝试以下家庭作业练习:
-
my_variable = 5
和my_variable = '5'
有什么区别? -
print(n)
和print('n')
有什么区别?如果您不确定,请尝试在 Python 解释器中输入以下命令:n = 5 print(n) print('n')
-
尝试不使用变量重写
echo.py
。
Footnotes 1
在内部,Python 以各种不同的格式存储文本数据。你会遇到的最常见的是str
和bytes
。因此,单词“字符串”并不总是与 Python str
类型完全相关。
四、决策
你明天要做一个重大决定——坐公共汽车还是走路。好吧,好吧,也许这不是一个重大的决定,但还是一个决定。你的决定可能基于许多因素,但让我们保持简单。如果下雨,那你就坐公交;否则,你会走路。
请注意决策的结构:
- 第一,有些事不是真的就是假的。在这种情况下,是真是假的事情就是有没有下雨。这叫做条件。
- 接下来,如果条件为真,则采取行动。如果下雨,你就乘公共汽车。
- 最后,如果条件为假,则采取行动。如果不下雨,那你就步行。
计算机需要以同样的方式做决定的能力。在计算机代码中,我们可以给计算机一个条件来评估,如果这个条件为真或为假,我们就采取行动。这个概念被称为分支,因为当我们需要时,代码可以“分支”到两个或更多的方向。
布尔运算
形式上,一个或真或假的陈述被称为布尔表达式。以下是布尔表达式的一些示例:
- 我的年龄是 30 岁
- 我有两个兄弟姐妹
- 1 > 100
- 1 < 100
如果你通读这些陈述,你应该能够说出每一个对你来说是对还是错。对于前两个条件,我们的答案各不相同,但希望我们都同意后两个条件!
在 Python 中,我们可以按如下方式编写这些表达式:
1 age == 30
2 siblings == 2
3 1 > 100
4 1 < 100
注意,我们可以像在数学中一样使用<
和>
操作符。但是双等号是怎么回事?那是打印错误吗?不,这个符号是相等运算符。记住一个等号(=
)已经有了一个目的——给变量赋值。在示例中,我们不是赋值,而是检查值,因此我们必须使用不同的运算符。
如上所述,这些表达式中的每一个都可以计算为真或假。“真”和“假”是如此重要的概念,以至于它们实际上是 Python 中的关键字。不出所料,这种新的数据类型被称为布尔数据类型。“布尔表达式”和“布尔数据类型”通常都简称为“布尔”,上下文暗示了所指的内容。
当 Python 代码被求值时,布尔表达式被转换为它们的布尔类型。这意味着以下表达式都是等价的:
1 1 == 1
2 'abc' == 'abc'
3 True
类似地,这些表达式也是等价的:
1 1 == 0
2 'abc' == 'xyz'
3 False
为了向自己证明这一点,打开 Python shell 并输入一些内容。Python 会对每个表达式进行求值,并用True
或False
进行响应。
还有一个比较运算符需要学习,那就是“不等于”运算符。在 Python 中,这被写成!=
。在 Python 解释器中尝试这些表达式
>>> 1 != 0
>>> True != True
>>> 'abc' != 'xyz'
总结一下,这里是我们目前所知的算子,加上>=
和<=
:
If 语句
现在我们知道了布尔表达式和数据类型,我们可以开始用 if 语句向代码添加条件。一个if
-语句必须有一个条件,一个条件为真时要采取的动作,以及一个条件不为真时要采取的可选动作。例如:
1 n = 50
2 if n < 100:
3 print("The condition is true!")
4 else: # <-- This part is optional
5 print ("The condition is false!")
有时,将自己和他人的注释直接放入代码中会很有帮助。这些被称为代码注释,Python 会在程序运行时忽略它们。在 Python 中,代码注释以#
开头。
我们还可以使用关键字elif
堆叠if
语句:
1 n = 150
2 if n < 100:
3 print("n is less than 100.")
4 elif n < 1000:
5 print("n is less than 1000.")
6 else:
7 print("n is a big number!")
在 Python 中,elif
是“else if”的写法。它被简称为elif
,因为它被广泛使用。
现在继续打开game.py
,按如下方式更改代码:
1 print("Escape from Cave Terror!")
2 action_input = input('Action: ')
3 if action_input == 'n':
4 print("Go North!")
5 elif action_input == 's':
6 print("Go South!")
7 elif action_input == 'e':
8 print("Go East!")
9 elif action_input == 'w':
10 print("Go West!")
11 else:
12 print("Invalid action!")
该代码将读入用户输入,并将输入的值与预定义的字符(“n”、“s”、“e”或“w”)进行比较。如果其中一个条件为真,程序将分支到代码的那个部分,并将操作打印到控制台。否则,它会通知用户该操作无效。
布尔运算
有时,将多个条件组合成一个条件会很有帮助,我们使用关键字and
和or
来实现这一点。这些工作就像你期望的那样。
1 if a == 3 and b == 4:
2 print("The hypotenuse is 5.")
3 if a == 3 or b == 4:
4 print("The hypotenuse might be 5.")
您可以根据需要使用任意数量的运算符,但是当您开始组合运算符时,有时需要使用括号来指定条件的求值顺序。尝试在解释器中键入以下内容:
>>> 1 == 100 and 1 == 2 or 1 == 1
>>> (1 == 100 and 1 == 2) or 1 == 1
>>> 1 == 100 and (1 == 2 or 1 == 1)
第一个例子在语法上是正确的,但是读起来令人困惑。为了澄清这一点,在第二个例子中,我们用圆括号将前两个条件括起来。在第三个例子中,我们实际上改变了表达式的求值顺序,从而改变了响应。
这两种分组的区别如下:
1 (1 == 100 and 1 == 2) or 1 == 1
2 (False) or 1 == 1
3 False or True
4 True
和...相对
1 1 == 100 and (1 == 2 or 1 == 1)
2 1 == 100 and (True)
3 False and True
4 False
考虑到这一点,我们可以这样做:
1 if (a == 3 and b == 4) or (a == 4 and b == 3):
2 print("The hypotenuse is 5.")
请注意,我们不能这样做:
1 # Warning: Bad Code!
2 favorite_color = 'blue'
3 if (favorite_color = 'red' or 'orange'):
4 print("You like warm colors.")
虽然在头脑中阅读代码可能有意义,但这是无效的语法。or
或and
两边的语句必须是完整的布尔表达式。
为了使我们的游戏更加用户友好,让我们让每个条件忽略行动的情况:
1 print("Escape from Cave Terror!")
2 action_input = input('Action: ')
3 if action_input == 'n' or action_input == 'N':
4 print("Go North!")
5 elif action_input == 's' or action_input == 'S':
6 print("Go South!")
7 elif action_input == 'e' or action_input == 'E':
8 print("Go East!")
9 elif action_input == 'w' or action_input == 'W':
10 print("Go West!")
11 else:
12 print("Invalid action!")
如果你现在测试这个游戏,你可以验证动作是否被接受。
家庭作业
尝试以下家庭作业练习:
=
和==
有什么区别?- 创建
ages.py
来询问用户的年龄,然后打印出一些与他们年龄相关的信息。例如,如果那个人是成年人,如果他们可以买酒,他们可以投票,等等。注意:int()
函数可以将字符串转换成整数。
五、函数
在计算机编程中,函数是一个命名的代码块。有时,值被传递到函数中。我们已经看到了一个函数的例子:
print("Hello, World!")
单词print
指的是 Python 内核中的一段代码,我们向它传递一个值,以显示在控制台上。如果您做了上一章的功课,您可能还使用了int()
,这是 Python 核心中的另一个函数,它接受一个值并将该值转换为一个整数。从视觉上看,因为有了括号,你就知道某个东西是函数。你能想出我们用过的另一个函数吗?
与函数非常相似的是方法。事实上,函数和方法是如此的相似,以至于你会经常看到这两个术语可以互换使用。区别在于,方法是与对象相关联的函数。稍后我们将更多地讨论对象,但是现在把对象想象成应用程序中的一个具体的“东西”——一个人的名字、一个日历日期或者一种喜欢的颜色。一个if
语句不是一个对象,一个>=
操作符也不是一个对象,等等。一个方法的例子是处理字符串的title()
函数。在 Python shell 中尝试一下:
>>> place = "white house"
>>> important_place = place.title()
>>> print(important_place)
你应该看到当你把它打印出来的时候,“white house”变成了大写的“White House”。我们可以看到,title()
是一个方法,因为我们需要一个对象(在本例中是字符串“white house”)存在才能使用它。使用.
字符引用一个方法。在某些方面,你可以像英语中的所有格“‘s’:place.title()
变成“处所的称谓函数”或“属于处所宾语的称谓函数”。
数据输入,数据输出
大多数函数都返回值。例如,int()
函数返回我们传入的整数结果,而title()
方法则给出一个大写的字符串。其他函数只是“做一些事情”,比如print()
函数。它接受一个值并显示文本,但实际上并不返回任何数据。在实践中,我们通常将函数返回的结果存储在一个变量中,而对于像print()
这样的函数,我们不会做同样的事情。
1 my_number = '15'
2 # The int() function gives something back, so we save it.
3 my_integer = int(my_number)
4
5 # But this doesn't make sense because print() doesn't give anything back.
6 useless_variable = print(my_integer)
当然,我们并不总是使用 Python 的内置函数;我们经常自己写。请记住,函数是一个已命名的代码块,我们用关键字def
来命名函数。下面是一个打印问候语的函数:
1 def say_hello():
2 print("Hello, World!")
为了使用这个函数,我们需要在我们希望函数运行的任何地方写下它的名字来调用它。如下创建hello.py
:
hello.py
1 def say_hello():
2 print("Hello, World!")
3
4 say_hello()
5
6 answer = input("Would you like another greeting?")
7 if answer == 'y':
8 say_hello()
每当程序看到say_hello()
,它就跳到那个代码块,并在里面做所有的事情。试用这个程序,并验证"Hello, World!"
总是至少打印一次,并且根据您的回答可以选择打印第二次。
say_hello()
函数不接受数据。我们说这个函数没有任何参数。接受数据的函数必须有一个或多个参数。 1 我们来试试say_hello
的改装版:
1 def say_hello(name):
2 print("Hello, " + name)
该函数有一个名为name
的参数。当函数运行时,name
实际上变成了一个变量,它的值就是传入的值。然后,函数(只有函数)可以在任何需要的地方使用该变量。在此示例中,变量用于在控制台中显示变量的值。
这个函数还使用了+
操作符将字符串组合或连接成一个字符串。我们现在已经看到+
操作符可以用于数学等式中的数字或字符串。
创建hello_name.py
来练习编写参数化函数。
hello_name.py
1 def say_hello(name):
2 print("Hello, " + name)
3
4 user_name = input("What is your name? ")
5
6 say_hello(user_name)
现在我们知道了函数,我们可以组织我们的游戏代码了。切换回game.py
,创建一个返回玩家动作的函数。
1 def get_player_command():
2 return input('Action: ')
然后在控制玩家移动的代码中调用这个新函数。
1 print("Escape from Cave Terror!")
2 action_input = get_player_command()
接下来,缩进控制玩家移动的代码,并将其包装在一个函数中。为了节省空间,我不包括整个函数。
1 def play():
2 print("Escape from Cave Terror!")
3 action_input = get_player_command()
4 # Remaining code omitted for brevity
为了让游戏仍然可以玩,在文件底部,简单调用play()
函数。您的game.py
文件现在应该是这样的:
game.py
1 def play():
2 print("Escape from Cave Terror!")
3 action_input = get_player_command()
4 if action_input == 'n' or action_input == 'N':
5 print("Go North!")
6 elif action_input == 's' or action_input == 'S':
7 print("Go South!")
8 elif action_input == 'e' or action_input == 'E':
9 print("Go East!")
10 elif action_input == 'w' or action_input == 'W':
11 print("Go West!")
12 else:
13 print("Invalid action!")
14
15
16 def get_player_command():
17 return input('Action: ')
18
19
20 play()
从用户的角度来看,游戏和上一章没什么变化。但是从编码的角度来看,我们添加了一些结构来使代码更易于维护。代码组织成函数的方式是决定代码好坏的众多因素之一。随着您阅读和编写更多的代码,您将对自己的代码应该如何组织有更好的感觉。
关于函数还有很多可以说的,事实上函数式编程的整个范例都深入到了函数中。一定要理解本章介绍的概念,因为这本书的其余部分非常依赖它们。
家庭作业
尝试以下作业:
- 用什么关键字创建函数?
- 无参数函数和参数化函数有什么区别?
- 当阅读一个函数的代码时,你如何知道它只是“做一些事情”还是“给出一些回报”?
- 创建
doubler.py
来包含一个名为double
的函数,该函数接受单个参数。该函数应该返回乘以 2 的输入值。打印出 12345 和 1.57 的双精度值。 - 创建
calculator.py
来包含一个名为add
的函数,它接受两个参数。该函数应该返回两个数的和。打印出 45 和 55 的总和。 - 创建
user_calculator.py
并重用之前练习中的add
函数。这一次,要求用户输入两个数字,并打印这两个数字的总和。提示:如果这只适用于整数,那也没关系。
Footnotes 1
函数最多可以有 255 个参数。请不要写 255 个参数的函数!
六、列表
到目前为止,我们一直使用只包含一个值的变量,比如age = 30
和name = 'Joe'
。但是在现实世界中(延伸到计算机程序),将值分组在一起通常是有用的。考虑这样一个程序,它需要显示一个班级中所有学生的名字。这对编码来说真的很烦人:
1 student1 = 'John'
2 student2 = 'Jack'
3 student3 = 'Ashton'
4 student4 = 'Loretta'
5 print(student1)
6 print(student2)
7 print(student3)
8 print(student4)
想象一下,一个班级有 30 或 300 名学生!在本章中,我们将学习如何将这些值组合在一起,并允许它们在代码中作为一个组存在。
什么是列表?
当值被组合成一个变量时,它被称为集合,列表是最常用的集合类型。在 Python 中,列表是用括号和逗号创建的,如下例所示:
students = ['John', 'Jack', 'Ashton', 'Loretta']
这非常方便。我们现在可以编写一次对所有学生通用的代码。这个短程序的一个简化版本(虽然不是完全相同的)就是:
1 students = ['John', 'Jack', 'Ashton', 'Loretta']
2 print(students)
列表有两个定义特征:
- 这是命令。事物被添加到列表中的顺序被保留。
- 它可能包含重复项。
这意味着这两个列表不完全相同:
1 list1 = ['John', 'Jack', 'Ashton', 'Loretta']
2 list2 = ['Ashton', 'Jack', 'John', 'Loretta']
这个列表完全没问题:
list1 = ['Buffalo', 'Buffalo', 'Buffalo', 'Buffalo', 'Buffalo']
这些特征可能看起来很明显,但是我们将在后面了解其他无序的和/或不包含重复的集合类型。
除了能够编写整体作用于列表的代码之外,Python 还提供了许多处理列表的便捷方法。
常见列表操作
增加
要向列表添加项目,使用append
功能。
>>> my_list = ['A','B','C']
>>> my_list.append('D')
>>> my_list
['A', 'B', 'C', 'D']
长度
为了找出一个列表的长度或大小,我们使用内置的len()
函数。
>>> my_list = ['A','B','C']
>>> len(my_list)
3
>>> my_list.append('D')
>>> len(my_list)
4
你可能会奇怪,为什么我们写my_list.append()
而不写my_list.len()
?原因是len()
实际上可以用于列表之外的东西,所以它位于List
类之外。在解释器中尝试这些:
>>> len('Hello, World!')
>>> len({})
第一个是字符串,第二个是(空的)字典,这是我们稍后将了解的另一个集合。
得到
要从列表中获取一个特定的条目,您需要知道该条目在列表中的位置。列表中项目的位置也称为索引。如果我们查看列表['A', 'B', 'C', 'D']
,这些是项目如何被索引的。
index 0 1 2 3
item A B C D
请注意,索引从 0 开始。大多数计算机编程语言都是 0 索引的,这意味着计数从 0 开始。
为了获得列表中的第一项,我们使用索引 0:
>>> my_list = ['A', 'B', 'C', 'D']
>>> my_list[0]
A
请确保在指定列表索引时使用方括号,而不是圆括号。
要获得列表中的最后一项,我们可以使用len()
函数来帮助:
>>> my_list = ['A', 'B', 'C', 'D']
>>> last_position = len(my_list) - 1
>>> my_list[last_position]
D
搜索
有两种简单的方法来搜索项目列表。第一个将告诉我们一个条目是否在列表中,第二个将告诉我们一个条目在列表中的什么位置。
如果你回想一下关于if
语句的那一章,你会学到布尔运算符,比如==
和<
。有一个特殊的布尔运算符可以用于列表,它就是单词in
。下面是如何使用in
操作符:
>>> 2 in [1, 2, 3]
True
>>> 5 in [1, 2, 3]
False
>>> 'A' in ['A', 'B', 'C']
True
有时,知道项目在列表中的位置是很有用的。为此,我们使用了index()
函数。
>>> my_list = ['John', 'Jack', 'Ashton', 'Loretta']
>>> my_list.index('Ashton')
2
如果一个项目出现多次,则返回第一个索引。
>>> my_list = ['Buffalo', 'Buffalo', 'Buffalo']
>>> my_list.index('Buffalo')
0
如果列表中没有某个项目,就会抛出一个错误。
>>> my_list = ['John', 'Jack', 'Ashton', 'Loretta']
>>> my_list.index('Buffalo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: 'Buffalo' is not in list
你可以在 Python 文档 2 中读到其他有用的列表操作。
向游戏中添加列表
现在我们知道了列表,我们可以为玩家提供一个物品清单。在play
函数的顶部,添加以下列表:
1 def play():
2 inventory = ['Dagger','Gold(5)','Crusty Bread']
Customization Point
您可以通过添加、更改或删除物品来更改玩家的物品清单。
我们还应该允许玩家查看库存,所以让我们用i
键打印库存。将此添加到“向西走”行动的正下方:
13 elif action_input == 'i' or action_input == 'I':
14 print("Inventory:")
15 print(inventory)
运行游戏,并验证您可以打印库存。
我们现在也有机会通过将等价的动作(例如‘W’和‘W’)放入一个列表来使我们的代码更简洁。更新play
函数中的if
语句,如下所示:
5 if action_input in ['n', 'N']:
6 print("Go North!")
7 elif action_input in ['s', 'S']:
8 print("Go South!")
9 elif action_input in ['e', 'E']:
10 print("Go East!")
11 elif action_input in ['w', 'W']:
12 print("Go West!")
13 elif action_input in ['i', 'I']:
14 print("Inventory:")
15 print(inventory)
这是个人喜好,但我觉得比以前的版本更容易阅读。使用列表还允许我们以更简单的方式添加字符。例如,如果我们愿意,我们可以通过简单地将>
添加到列表:action_input in ['e', 'E', '>']
中,使>
成为“Go East”的别名。
家庭作业
尝试以下作业:
-
哪两个特征使集合成为列表?
-
编写允许用户输入他们最喜欢的三种食物的代码。把这些食物列成清单。
-
使用索引:
['Mercury', 'Venus', 'Earth']
打印出该列表的中间项。你能改变你的代码来处理任意大小的列表吗(假设有奇数个条目)?提示:回想一下将某物转换成整数的int()
函数。 -
运行这段代码会发生什么?你知道为什么吗?
>>> my_list = ['A','B','C'] >>> my_list[len(my_list)]
Footnotes 1
在下一章学习了循环之后,我们可以编写一个行为一致的程序。
2
https://docs.python.org/3.5/tutorial/datastructures.html#more-on-lists
七、循环
All work and no play makes Jack a dull boy
All work and no play makes Jack a dull boy
All work and no play makes Jack a dull boy
All work and no play makes Jack a dull boy
计算机的真正力量在于它们毫无怨言地执行重复任务的能力。一个 CPU 会非常高兴地处理翻转的位,直到它烧坏。只要你给计算器输入数字,它就会一直计算下去。当然——继续在您最喜欢的、目前服务器有问题的网站上使用 F5——您的路由器不会在意。
当我们希望一个计算机程序多次运行同一段代码时,我们将这段代码放在一个循环中。
While 循环
不同的编程语言有不同类型的循环,但大多数情况下有两个主要类别:“做某事直到我说停止”循环和“做某事 N 次”循环。通常,它们分别被称为 while 循环和 for 循环。Python 每种都有一个:一个while
循环和一个for-each
循环。
事实证明,您真正需要的唯一一个循环是一个while
循环。然而,许多编程语言都提供了其他循环关键字来简化循环的编写。
一个while
循环总是与一个布尔表达式配对。记住,布尔表达式的值可以是真或假。当条件为真时,循环将继续运行,因此得名。Python 中的while
循环是使用while
关键字编写的。这里有一个例子:
1 while True:
2 print("All work and no play makes Jack a dull boy")
尝试使用该代码创建一个脚本并运行它。准备好按 Ctrl+C 就行了!当"All work and no play makes Jack a dull boy"
被打印到控制台时,您应该会看到文本飞快地闪过。如果不去管它,这段代码会一直运行到计算机关闭,因为布尔表达式True
显然总是真的。
我们来看一个更现实的方案。基于上一章作业中的程序(如果你还没有做,现在就应该做!),如果我们希望用户一直输入喜欢的东西,直到完成为止,会怎么样呢?我们不知道他们是否想要输入一个、两个或 20 个项目,所以我们使用一个while
循环来继续接受项目。将favorites.py
中的代码更改如下:
1 favorites = []
2 more_items = True
3 while more_items:
4 user_input = input("Enter something you like: ")
5 if user_input == '':
6 more_items = False
7 else:
8 favorites.append(user_input)
9
10 print("Here are all the things you like!")
11 print(favorites)
第一行创建一个空列表。每次循环运行时,都会向列表中添加另一个项目。循环中的布尔条件仅仅是more_items
,这意味着为了让循环退出,more_items
需要为假。我们也可以写while more_items == True
,但这是不必要的冗长。要停止添加项目,用户应该输入一个空字符串,只需按 enter 键即可。继续运行这个脚本,看看输出是什么样子。这是我最后得到的结果:
Here are all the things you like!
['family', 'pizza', 'python!']
嗯,看起来不错,但是 Python 打印列表的默认行为不太好。如果我们对如何打印列表有更多的控制权,那就好了…
For-Each 循环
首先,让我们试着打印一份所有收藏项目的列表。为此,我们将使用一个for-each
循环。for-each
循环之所以得名,是因为它为集合中的每一件事都做了一些事情。这是完美的,因为我们想打印出favorites
列表中的每一件事。让我们在文件的顶部添加一个函数,它将打印给定集合的有序列表。
1 def pretty_print_unordered(to_print):
2 for item in to_print:
3 print("* " + str(item))
for-each
循环的 Python 语法可读性很强:for
variable
in
collection
。变量的名字由我们决定。每次循环运行时,变量都指向集合中的下一项。当循环到达集合末尾时,它将停止运行。
在循环内部,我们可以通过循环语法中定义的变量来访问当前项。为了确保我们可以打印当前项目,变量item
被包装在str()
函数中,以将项目强制转换为字符串。这个功能就像你以前用过的int()
功能一样。如果我们不使用它,Python 会在遇到非字符串项时抛出一个错误。
要使用 pretty print 函数,请更改脚本的结尾并重新运行它。
14 print("Here are all the things you like!")
15 pretty_print_unordered(favorites)
您现在应该会看到类似这样的内容:
Here are all the things you like!
* family
* pizza
* python!
好吧,但是如果我们想要一个有序的数字列表呢?实际上有几种不同的方法可以做到这一点,我们将讨论其中的三种。
循环计数器
如果我们想为每个项目打印一个数字,我们需要有某种方法来跟踪循环中增加的数字和实际的项目。第一种方法是使用计数器。
1 def pretty_print_ordered(to_print):
2 i = 1
3 for item in to_print:
4 print(i + ". " + str(item))
5 i = i + 1
在这个循环中,我们设置i
等于1
,并且每次循环运行时,我们将i
加 1。这种风格的缺点是它需要两行额外的代码,而且我们必须跟踪和更新我们的计数器。另一种选择是使用 Python 的range()
函数。
范围
打开 Python shell 并尝试这些:
>>> list(range(5))
>>> list(range(3,7))
>>> list(range(7,3))
>>> list(range(-2,2))
list()
函数将范围强制转换成一个我们易于阅读的列表。你有没有注意到int()
、str()
和list()
的模式?
使用range()
给我们一组数字。我们知道一个for-each
循环可以操作一个集合。有了这些信息,我们可以按如下方式更改我们的排序列表:
1 def pretty_print_ordered(to_print):
2 for i in range(len(to_print)):
3 print(str(i + 1) + ". " + str(to_print[i]))
这里,我们再次使用len()
函数来获取列表的大小,这个数字被传递给range()
以给出一个数字列表,这些数字对应于to_print
集合中的条目的索引。这似乎有点令人困惑,所以让我们看一个例子:
>>> to_print = ['abc', 'def', 'ghi']
>>> len(to_print)
3
>>> list(range(len(to_print)))
[0, 1, 2]
在这个例子中,列表中有三样东西。因此,我们得到的范围有三个数字:0、1 和 2。
为什么我们必须在 shell 中使用list()
而不是在脚本中使用?答案是range()
实际上返回的是一个 Python range
对象,而不是一个list
对象。当脚本运行时,Python 知道如何使用range
对象。然而,当我们想要查看REPL
中的对象时,我们需要强制它成为一个列表,这样我们就可以一次看到所有的值。使用print(range(3))
将打印出无用的字符串"range(3)"
。
当循环运行时,我们使用来自range
函数的数字来定位列表中当前索引处的项目。例如,str(to_print[2])
将返回to_print
集合中索引 2 处的项目。最后,为了使打印输出对用户友好,我们给str(i + 1)
中的每个索引添加一个。如果我们不这样做,我们将得到如下列表:
0\. abc
1\. def
2\. xyz
它是正确的,但不是非常用户友好的。使用range()
可能看起来比使用计数器更令人困惑,但是代码更短,也省去了我们维护计数器的麻烦。我们将了解的最后一个选项是我们已经看到的两个选项之间的一个很好的中间地带。
使用枚举
当我们想在一个变量中存储许多相似的东西时,比如教室里的学生,列表就非常有用。但有时我们只有两三个 2 相互紧密关联的变量。在这种情况下,一个列表可能是多余的,所以我们使用一个元组。像列表一样,元组中的内容是有序的,可以重复,但与列表不同,元组的长度是固定的。我们不能从元组中添加或删除项目。以下是元组可能有意义的几种情况:
1 first_name, last_name = ('Barack', 'Obama')
2 month, day, year = (10, 22, 2015)
3 dna_aminos = ('A','T','C','G')
元组语法允许您在左边定义变量名,在右边定义变量值。如果变量和值的数量匹配,则每个变量被赋予元组中出现的下一个值。所以在前面的例子中,month
等于10
。如果只使用了一个变量名,那么整个元组都被赋给该变量。dna_aminos
的值是('A','T','C','G')
的总和。
函数也可以返回元组。尝试以下三种不同的脚本:
1 def get_date():
2 return (10, 22, 2015)
3
4 month, day, year = get_date()
5 print(month)
1 def get_date():
2 return (10, 22, 2015)
3
4 date = get_date()
5 print(date)
1 def get_date():
2 return (10, 22, 2015)
3
4 month, day = get_date()
5 print(month)
第一个脚本像预期的那样工作:我们将函数中返回的元组解包成month
、day
和year
。第二个脚本没有解包元组,而是将所有部分存储在date
变量中。最后一个脚本抛出了一个错误,因为返回的元组有三个值,但是我们只使用了两个变量。那么,为什么转向元组呢?好了,下一个我们将要学习的内置函数返回一个元组!
如果您将一个集合传递给enumerate()
函数,您将获得一个特殊的 Python 对象 3 ,其行为类似于元组列表。每个元组包含两个值:当前索引和原始列表中的值。尝试运行以下代码:
>>> letters = ['a', 'b', 'c']
>>> list(enumerate(letters))
[(0, 'a'), (1, 'b'), (2, 'c')]
>>> list(enumerate(letters, 1))
[(1, 'a'), (2, 'b'), (3, 'c')]
那么我们如何利用这一点呢?在打印内容的循环中,我们可以枚举列表。
1 def pretty_print_ordered(to_print):
2 for i, value in enumerate(to_print, 1):
3 print(str(i) + ". " + str(value))
在for
循环中,我们将元组解包到索引i
中,并将列表中最喜欢的东西解包到变量value
中。现在我们已经有了列表值,我们不必像使用range()
函数那样从列表中提取它。我们也不需要知道列表使用了多长时间的len()
——枚举器会帮我们处理这个问题。
那么哪个最好呢?正如编程中的许多事情一样,没有一个正确的答案。在不同的情况下,你可以认为这些都是最好的解决方案。在我看来,我更喜欢这个例子中的enumerator
。它只需要几行代码,比range
选项更容易阅读。你同意吗?如果不是,你更喜欢你的选择是什么?
嵌套
关于循环要介绍的最后一个概念是嵌套。嵌套循环只是另一个循环中的一个循环。这是一个非常简单的嵌套循环,它创建了一个小的乘法问题列表。
1 for i in range(3):
2 for j in range(3):
3 product = i * j
4 print(str(i) + " * " + str(j) + " = " + str(product))
注意,在第二个循环中,我们可以访问第一个循环的索引。 4 事实上第二个循环可以访问第一个循环和第二个循环之间声明的任何变量。
假设我们想找出一系列数字的因子。这需要我们为每个数字准备一个不同的因素列表。利用我们刚刚学到的知识,我们可以编写这个脚本来查找从 1 到 10 的每个数字的因子。
1 for i in range(1,11):
2 factors = []
3 for j in range(1, i + 1):
4 if i % j == 0:
5 factors.append(j)
6 print("The factors of " + str(i) + " are: " + str(factors))
%
运算符被称为模或模数运算符,它返回两个数相除后的余数。如果a % b
返回 0,那么a
可以被b
整除,就像在4 % 2
中一样。这有一些方便的用途,比如每运行 n 次循环就做一些事情。我们不会经常使用它,但它是一个很好的工具,可以放在您的计算机编程技巧包中。
这段代码可以从第二个循环中访问factors
列表,即使变量是在第一个循环中声明的。
游戏循环
在游戏中,大部分代码运行在所谓的游戏循环中。每次循环运行时,游戏世界都会发生变化,用户输入会被传递回程序。在一个 3D 游戏中,这可能每秒发生 60 次!在我们的游戏中,循环不需要运行那么快,因为没有图形需要重画。然而,每次循环运行时,世界将被更新,用户输入将被接受。
将这个循环添加到游戏的play()
函数中,并确保缩进函数的其余部分:
1 def play():
2 inventory = ['Dagger','Gold(5)','Crusty Bread']
3 print("Escape from Cave Terror!")
4 while True:
5 action_input = get_player_command()
6 if action_input in ['n', 'N']:
为什么使用了while
循环而不是for-each
循环?我们不知道这个循环会运行多少次。现在,它无限运行,但即使在真实的游戏中,我们也需要循环运行,直到玩家赢或输。因为我们不知道玩家会走多少圈,所以我们使用了一个while
循环。
现在我们知道了如何打印一个列表,让我们修改打印清单的代码。
14 elif action_input in ['i', 'I']:
15 print("Inventory:")
16 for item in inventory:
17 print('*' + str(item))
如果你现在运行游戏,你会看到while
循环允许你继续输入命令,而for
循环以更好的格式打印清单。要退出游戏,使用 Ctrl+D 或 Ctrl+C。
家庭作业
尝试以下作业:
-
对于以下各项,您会使用哪种循环:
- 每五秒钟检查一次温度的程序
- 在杂货店打印收据的程序
- 保龄球比赛中记录分数的程序
- 一个随机播放音乐库中歌曲的程序
-
打开关于函数的第五章中的
user_calculator.py
,并添加一个while
循环,允许用户持续添加两个数。 -
写一个脚本,显示 1 * 1 到 10 * 10 的乘法表。左上角应该是这样的:
1 2 3 4 2 4 6 8 3 6 9 12 4 8 12 16
-
使用
enumerate
和%
操作符打印列表中的每三个单词:['alpha','beta','gamma','delta','epsilon','zeta','eta']
Footnotes 1
在有计数器的循环中使用变量i
是一种惯例;这是少数不使用描述性变量名的例外情况之一!
2
实际上,你可以在一个元组中存储大量的条目,但是如果你需要更多的变量,你应该重新考虑使用元组的选择。
3
如果你想做更多的研究,这个对象叫做迭代器。range()
函数也返回一个迭代器。
4
就像使用i
是约定一样,j
是嵌套循环中的约定。如果你真的需要他们,k
和l
是下一个。
八、对象
在计算机程序中,对象是存储在计算机内存中的容器,它保存一个或多个值。更简单地说,对象是程序中可用的“东西”。我们已经看到了一些对象:像"Hello, World!"
这样的字符串是对象,列表[1, 2, 3]
是对象,甚至函数print()
也是对象!事实上,在 Python 中,一切都是幕后的对象。但大多数时候,我们实际上对自己创造的东西最感兴趣。本章结束时,你将能够在游戏中添加代表武器的物体。
对象成员
在代码中,对象通常被用作捆绑相关数据的手段。例如,Person
对象可能包含一个人的姓名字符串,一个代表他们年龄的数字,以及他们最喜欢的食物列表。对象也可以有自己的函数,称为方法。这些方法通常处理存储在对象内部的数据。我们的Person
对象可以有一个根据年龄计算人出生年份的方法。对象的数据和方法统称为对象的成员或属性。
用类定义对象
在创建对象之前,我们需要创建一个类来定义对象。你可以把一个类想象成一张蓝图——它告诉我们如何建造一所房子,但它不是一所房子。我们也可以重复使用相同的蓝图来建造许多相似的房子,即使这些房子在位置、颜色等方面有所不同。为了定义一个类,我们使用class
关键字,后跟类的名称,这个名称是按照约定命名的。
创建census.py
并添加这个类 1 :
1 class Person:
2 age = 15
3 name = "Rolf"
4 favorite_foods = ['beets', 'turnips', 'weisswurst']
5
6 def birth_year():
7 return 2015 - age
现在,让我们通过创建一些人并找到平均年龄来为我们的人口普查添加一些功能。要创建一个新对象,只需在类名后添加括号。这段代码将创建三个人:
1 people = [Person(), Person(), Person()]
2
3 sum = 0
4 for person in people:
5 sum = sum + person.age
6
7 print("The average age is: " + str(sum / len(people)))
注意,为了访问对象中的数据,我们使用了.
操作符,就像访问函数一样。代码list.append()
与person.age
非常相似,因为append()
是List
类的成员,而age
是Person
类的成员。运行这个程序,不出所料,会告诉我们平均年龄是 15 岁。我们真正需要的是Person
类的每个对象或实例能够有不同的年龄、名字和喜欢的食物的值。为此,我们将学习一个可以添加到任何 Python 对象的特殊函数,称为__init__()
。
使用 init() 初始化对象
在前面的例子中,我们创建了三个相同的人。但是Person
类只有在我们可以用它来创建三个名字和年龄不同的人时才有用。一种选择是这样的:
1 people = [Person(), Person(), Person()]
2
3 people[0].name = "Ed"
4 people[0].age = "11"
5 people[0].favorite_foods = ["hotdogs", "jawbreakers"]
6 people[1].name = "Edd"
7 people[1].age = "11"
8 people[1].favorite_foods = ["broccoli"]
9 people[2].name = "Eddy"
10 people[2].age = "12"
11 people[2].favorite_foods = ["chunky puffs", "jawbreakers"]
但是那是相当冗长和乏味的。为了简化对象创建,Python 围绕一个名为__init
__()
的方法定义了一些特殊行为。首先,该方法在对象创建时立即运行。其次,我们可以向方法添加参数,这些参数在创建对象时成为必需的参数。继续修改你的Person
类,使其具有下面的初始化式:
1 class Person:
2 def __init__(self, name, age, favorite_foods):
3 self.name = name
4 self.age = age
5 self.favorite_foods = favorite_foods
初始化器接受传入的参数,并将它们分配给刚刚创建的对象。密切关注的读者可能已经注意到了初始化器中弹出的关键字self
。那个关键字 2 是用来指特定的对象。这意味着Person
类中的age
成员不是某种通用的“年龄”,而是初始化器所作用的这个特定人的年龄。如果有帮助的话,每当你看到self
,就认为这个物体指的是它自己。
已经定义了初始化器,我们可以像这样创建人:
1 people = [Person("Ed", 11, ["hotdogs", "jawbreakers"])
2 , Person("Edd", 11, ["broccoli"])
3 , Person("Eddy", 12, ["chunky puffs", "jawbreakers"])]
这比显式设置每个类成员要方便得多!
事实证明,我们还需要在类中访问或操作对象成员的任何地方使用self
。这意味着改变birth_year()
功能。下面是改装过的Person
级:
1 class Person:
2 def __init__(self, name, age, favorite_foods):
3 self.name = name
4 self.age = age
5 self.favorite_foods = favorite_foods
6
7 def birth_year(self):
8 return 2015 - self.age
让我们更新我们的人口普查,也输出平均出生年份。为此,我们只需在每个 person 对象上调用birth_year()
函数。当该函数运行时,它是为该特定对象运行的。我们知道这一点是因为函数通过关键字self
引用它自己。
18 age_sum = 0
19 year_sum = 0
20 for person in people:
21 age_sum = age_sum + person.age
22 year_sum = year_sum + person.birth_year()
23
24 print("The average age is: " + str(age_sum / len(people)))
25 print("The average birth year is: " + str(int(year_sum / len(people))))
关于self
关键字更神奇的是 Python 知道它指的是对象,所以你不必手动传递它。类似于person.birth_year(person)
的东西是不必要的,实际上是不正确的。
我还选择将平均年份包含在一个int()
函数中,因为年份“2003”比“2003”更有意义。46360 . 68668686666
使用 str()打印对象
假设我们想要显示人口普查中的原始信息。一种简单的方法可能是这样的:
18 print("The people polled in this census were:")
19 print(people)
但是现在当您运行该脚本时,您将会看到类似这样的内容:
The average age is: 11.333333333333334
The average birth year is: 2003
The people polled in this census were:
[<__main__.Person object at 0x10135eb00>, <__main__.Person object at 0x10135eb38>, <__main__.Person object at 0x10135eb70>]
那一点帮助都没有!你看到的是 Python 打印对象的默认实现。它告诉你这个东西是什么类型的对象,在这个例子中是Person
,以及它在内存中的位置(内存位置会因计算机而异)。如果我们可以将一个Person
传递到print()
中,并查看这个人的信息,那就太好了。
幸运的是,Python 提供了一种简单的方法来做到这一点。像 Python 寻找的__init__()
函数一样,在打印对象或使用str()
函数将对象转换成字符串时,Python 也寻找一个__str__()
函数。此方法必须返回一个字符串。继续将这个方法添加到Person
类中:
10 def __str__(self):
11 return "Name: " + self.name \
12 + " Age: " + str(self.age) \
13 + " Favorite food: " + str(self.favorite_foods[0])
为了可读性,我们可以使用反斜杠将一个字符串换行到多行。所有这些字符串将被合并成一个字符串,以便函数返回。现在当你运行这个脚本时,同样的事情发生了。啊。
Python 在打印容器(比如列表)时的一个怪癖是没有为容器中的每个对象调用__str__()
方法。 3 所以我们需要用循环自己做。
18 for person in people:
19 print(person)
现在,如果你运行这个程序,你应该看到这个:
The average age is: 11.333333333333334
The average birth year is: 2003
The people polled in this census were:
Name: Ed Age: 11 Favorite food: hotdogs
Name: Edd Age: 11 Favorite food: broccoli
Name: Eddy Age: 12 Favorite food: chunky puffs
让使用__str__()
更容易的最后一件事是对字符串使用format()
方法。使用format()
让我们不用连接字符串。下面是编写该方法的另一种方式:
10 def __str__(self):
11 return "Name: {} Age: {} Favorite food: {}".format(
12 self.name, self.age, self.favorite_foods[0])
大括号{}
用作占位符,传递给format()
的每个参数都按顺序注入占位符中。当您需要连接一串字符串时,这通常更容易读写。你还可以用format()
做很多其他的事情,比如填充字符串、截断长十进制数、打印货币等。如果您感兴趣,可以阅读 Python 文档中关于字符串格式化“迷你语言”的所有内容。 4
给游戏添加武器
现在我们知道了如何创建职业,让我们在游戏中添加一些来代表武器。在game.py
的顶部添加以下内容:
1 class Rock:
2 def __init__(self):
3 self.name = "Rock"
4 self.description = "A fist-sized rock, suitable for bludgeoning."
5 self.damage = 5
6
7 def __str__(self):
8 return self.name
9
10 class Dagger:
11 def __init__(self):
12 self.name = "Dagger"
13 self.description = "A small dagger with some rust. " \
14 "Somewhat more dangerous than a rock."
15 self.damage = 10
16
17 def __str__(self):
18 return self.name
19
20 class RustySword:
21 def __init__(self):
22 self.name = "Rusty sword"
23 self.description = "This sword is showing its age, " \
24 "but still has some fight in it."
25 self.damage = 20
26
27 def __str__(self):
28 return self.name
定义了一个实际的Dagger
类后,我们现在可以更新起始库存,以包含一个Dagger
对象,而不仅仅是一个表示"Dagger"
的字符串。
1 def play():
2 inventory = [Dagger(),'Gold(5)','Crusty Bread']
3 print("Escape from Cave Terror!")
Customization Point
尝试定义一些你自己的武器类型,比如Crossbow
或者Axe
。或者如果你的游戏设定在科幻世界,也许你想拥有RayGun
和ShockStick
。请确保相应地更新玩家的库存。
当玩家选择展示他们的库存时,你认为会发生什么?试试看,看看你是否正确!
一点面向对象编程
在计算机编程中,我们应该努力遵循的一个原则是“不要重复自己!”还是“干”。如果您发现自己不止一次地键入相同的代码,可能有更好的方法来组织您的代码。你可能已经注意到,对于每种武器来说,__str__()
的方法是完全一样的。现在假设我们想改变这个方法。我们必须在三个地方做出改变。谢天谢地,有更好的方法。
面向对象编程(或 OOP)是一种范式,涉及围绕对象的思想构建代码。如前所述,Python 中的一切都是对象,但我们在本章中只是明确地开始创建对象。在此之前,我们能够进行大量的编程,因为 Python 支持 OOP,但并不要求 OOP。在构建我们的游戏时,我们会在有帮助的地方使用一些 OOP,但我们不会为了坚持范例而不必要地强迫自己进入一个盒子。结构化武器代码是 OOP 可以帮助的一个地方。
OOP 中的两个重要概念是组合和继承。复合是指一个对象包含另一个对象。我们在人口普查中看到了这一点,因为每个Person
包含一个List
最喜欢的食物。继承是指一个类从另一个类继承行为。父类和子类的比喻在这里适用,我们有时将一个类称为“父类”,将从父类继承的类称为“子类”。或者,也使用术语“超类”和“子类”。
为了对武器应用继承,让我们从创建一个父类Weapon
开始,并将复制的__str__()
方法移动到该类中。
1 class Weapon:
2 def __str__(self):
3 return self.name
为了让一个类从Weapon
继承,我们使用语法ClassName(Weapon):
。任何从Weapon
继承的类都将自动免费获得Weapon
类的相同行为。这意味着如果我们让Rock
、Dagger
和RustySword
从Weapon
继承,我们可以移除重复的__str__()
方法。
6 class Rock(Weapon):
7 def __init__(self):
8 self.name = "Rock"
9 self.description = "A fist-sized rock, suitable for bludgeoning."
10 self.damage = 5
11
12
13 class Dagger(Weapon):
14 def __init__(self):
15 self.name = "Dagger"
16 self.description = "A small dagger with some rust. " \
17 "Somewhat more dangerous than a rock."
18 self.damage = 10
19
20
21 class
RustySword
(Weapon):
22 def __init__(self):
23 self.name = "Rusty sword"
24 self.description = "This sword is showing its age, " \
25 "but still has some fight in it."
26 self.damage = 20
家庭作业
尝试以下作业:
- 类和对象的区别是什么?
- 一个类中的
__init__()
方法的目的是什么? __str__()
和str()
有什么区别?- 创建一个名为
food.py
的文件,其中包含一个类Food
。这个类应该有四个成员:name
、carbs
、protein
和fat
。这些成员应该在类的初始化器中设置。 - 向名为
calories()
的Food
类添加一个方法,计算食物中的卡路里数。每克碳水化合物含 4 卡路里,每克蛋白质含 4 卡路里,每克脂肪含 9 卡路里。 - 创建另一个名为
Recipe
的类,它的初始化器接受一个name
和一个名为ingredients
的食物列表。向该类添加一个名为calories()
的方法,该方法返回食谱中的总热量。 - 向
Recipe
类添加一个__str__()
方法,该方法只返回菜谱的名称。 - 创建两个(简单!)食谱,并打印出每个食谱的名称和总热量。如果你愿意,你可以编造碳水化合物、蛋白质和脂肪的数字。为了加分,试着用两个或 200 个食谱的方式来做。
- 这个脚本中的类是继承或组合的例子。哪一个,为什么?
Footnotes 1
在一个实际的应用程序中,我们当然会使用 Python 的日期和时间库来计算出生年份,但这将用于我们的演示。
2
其实自我并不是一个保留的关键词,而是一个大家都遵循的约定。
3
被搜索的方法称为__repr__()
。我们在这里坚持使用__str__()
,因为__str__()
的目的是使对象可读。__repr__()
的目的是在应用出现问题时帮助排除故障。在实际应用中,你可能也想实现__repr__()
,但这超出了我们游戏的范围。
4
https://docs.python.org/3.5/library/string.html#formatspec
九、异常
在一个完美的世界里,程序员从不犯错,用户也总是表现良好。在现实世界中,程序员总是会犯错,用户也从来不会循规蹈矩!当出错时,Python 会抛出异常,这是程序执行时遇到的错误。幸运的是,处理大多数异常并从中恢复是可能的。在这一章中,我们将学习一些我们应该预料到的常见异常,以及如何处理它们。
验证用户输入
假设您想收集一些关于用户的基本数据,然后将一些信息返回给他们。这似乎很简单:
1 name = input("Please enter your name: ")
2 age = input("Please enter your age: ")
3 print("You were born in {}.".format(2015 - int(age)))
你测试了程序,它运行得很好。然后你把它给你的朋友看,她输入“25 年”作为年龄。好吧,这个无用的信息转储到屏幕上,现在看起来你不知道如何编程。
Traceback (most recent call last):
File "validate.py", line 3, in <module>
print("You were born in {}.".format(2015 - int(age)))
ValueError: invalid literal for int() with base 10: '25 years'
这是一个异常(特别是一个ValueError
),引发它是因为用户输入了一个不能用int()
转换成数字的值。作为勤奋的程序员,我们可以预见和计划这种情况。
关键字try
允许我们将代码块标记为可能引发异常的东西。接下来是except
关键字,它标记了遇到异常时要运行的代码块。
1 name = input("Please enter your name: ")
2 age = input("Please enter your age: ")
3 try:
4 print("You were born in {}.".format(2015 - int(age)))
5 except ValueError:
6 print('Unable to calculate the year you were born, ' \
7 + '"{}" is not a number.'.format(age))
特别注意这里的语法,记住 Python 中的空格是有意义的。缩进告诉我们什么代码是try
块的一部分,什么代码是except
块的一部分。如果try
块中的任何代码遇到ValueError
,程序将立即跳转到except
块并运行该块中的代码。
检查对象成员
在我们的游戏中,玩家有一些不同的物品清单。我们再补充一个:
inventory = [Rock(), Dagger(), 'Gold(5)', 'Crusty Bread']
有些是武器,有些不是。如果我们想在清单中找到最强大的武器,我们需要检查每个项目,看看它的损害是什么。
59 def most_powerful_weapon(inventory):
60 max_damage = 0
61 best_weapon = None
62 for item in inventory:
63 if item.damage > max_damage:
64 best_weapon = item
65 max_damage = item.damage
66
67 return best_weapon
这应该很简单。该函数循环遍历库存中的所有商品,并检查损失是否大于已经发现的损失。这里新增了一个关键词:None
。这是一种价值的缺失。我们最初设置best_weapon
等于None
,因为如果玩家没有任何武器,这个函数不能返回武器!
如果运行这段代码,很不幸会引发一个异常:
Traceback (most recent call last):
File "game.py", line 80, in <module>
play()
File "game.py", line 46, in play
best_weapon = most_powerful_weapon(inventory)
File "game.py", line 59, in most_powerful_weapon
if item.damage > max_damage:
AttributeError: 'str' object has no attribute 'damage'
这是有道理的,因为“硬皮面包”和“黄金”不会造成损害。因为我们知道库存经常会有非武器,所以我们可以将代码包装在一个try
中并处理AttributeError
。
59 def most_powerful_weapon(inventory):
60 max_damage = 0
61 best_weapon = None
62 for item in inventory:
63 try:
64 if item.damage > max_damage:
65 best_weapon = item
66 max_damage = item.damage
67 except AttributeError:
68 pass
69
70 return best_weapon
71
72 play()
如果遇到了一个AttributeError
,我们实际上不需要做任何事情,因为我们并不在乎 Crusty Bread 没有一个damage
属性。关键字pass
可以在我们想跳过或忽略代码块的任何时候使用。请记住,对于大多数异常,您需要做一些事情,比如警告用户或遵循不同的代码路径。对于这种特定的情况,我们可以安全地忽略这个异常。
故意引发异常
乍一看,这似乎有悖常理,但在某些情况下,我们确实希望引发一个异常。当我们想对自己做错的事情大喊大叫时,我们通常会这样做!对坏代码进行检查将有助于我们在测试过程中发现错误。
当前代码中的一个漏洞与Weapon
类有关。此代码将导致异常:
1 axe = Weapon()
2 print(axe)
为什么呢?因为Weapon
类的__str__()
方法在打印对象时会寻找一个name
,但是该类没有那个属性。我们可以通过给Weapon
指定一个名字来解决这个问题,但是这实际上没有意义,因为这个类太普通了,无法描述。真的,我们永远不应该创造一个Weapon
对象;我们应该始终创建一个类似Dagger
的特定子类。如果我们需要一个 axe 对象,我们应该创建一个继承自超类Weapon
的Axe
类。
为了防止我们自己意外地创建了Weapon
对象,我们可以在初始化器中引发一个异常。
1 class Weapon:
2 def __init__(self):
3 raise NotImplementedError("Do not create raw Weapon objects.")
4
5 def __str__(self):
6 return self.name
Python 内置了NotImplementedError
异常,它是一个很好的标记,提醒我们正在做错事。我们可以为异常包含一条消息,以帮助提醒我们问题是什么。如果你想测试这个新代码,试着将Weapon()
添加到玩家的物品清单中并运行游戏。您应该会看到以下错误:
Traceback (most recent call last):
File "game.py", line 72, in <module>
play()
File "game.py", line 33, in play
inventory = [Weapon(), Rock(), Dagger(), 'Gold(5)', 'Crusty Bread']
File "game.py", line 3, in __init__
raise NotImplementedError("Do not create raw Weapon objects.")
NotImplementedError: Do not create raw Weapon objects.
当你完成测试时,记得从清单中删除Weapon()
。
家庭作业
尝试以下作业:
- 用
try
和except
更新user_calculator.py
来处理没有输入数字的用户。 None
是什么意思,什么时候用?pass
是什么意思,什么时候用?- 创建一个
Vehicle
类,一个Motorcycle
类是Vehicle
的子类,其wheels
属性设置为 2,一个Car
类是Vehicle
的子类,其wheels
属性设置为 4。添加代码,如果程序员试图创建一个Vehicle
,将引发一个异常。
十、休息一下
信不信由你,到现在为止,你实际上已经知道了本书将涉及的大部分 Python 材料。将会有一些新的东西需要学习,但是这本书的其余部分将会集中在游戏的制作上。在这个过程中,我们将获得一些构建应用程序的最佳实践和指南。首先,我们将把代码重新组织成几个文件。
将代码组织到多个文件中
首先,我们将创建items.py
来存储玩家将与之交互的物品的所有类。现在,我们只有武器,但以后我们会增加更多。
items.py
1 class Weapon:
2 def __init__(self):
3 raise NotImplementedError("Do not create raw Weapon objects.")
4
5 def __str__(self):
6 return self.name
7
8
9 class Rock(Weapon):
10 def __init__(self):
11 self.name = "Rock"
12 self.description = "A fist-sized rock, suitable for bludgeoning."
13 self.damage = 5
14
15
16 class Dagger(Weapon):
17 def __init__(self):
18 self.name = "Dagger"
19 self.description = "A small dagger with some rust. " \
20 "Somewhat more dangerous than a rock."
21 self.damage = 10
22
23
24 class RustySword(Weapon):
25 def __init__(self):
26 self.name = "Rusty sword"
27 self.description = "This sword is showing its age, " \
28 "but still has some fight in it."
29 self.damage = 20
接下来,我们将用一个Player
类创建player.
py
。既然库存是真正和玩家关联的,我们就把它作为对象的一个属性。这也意味着与打印库存相关的方法需要转移到Player
类中。我们完成重组后会报道import
。
player.py
1 import items
2
3
4 class Player:
5 def __init__(self):
6 self.inventory = [items.Rock(),
7 items.Dagger(),
8 'Gold(5)',
9 'Crusty Bread']
10
11 def print_inventory(self):
12 print("Inventory:")
13 for item in self.inventory:
14 print('* ' + str(item))
15 best_weapon = self.most_powerful_weapon()
16 print("Your best weapon is your {}".format(best_weapon))
17
18 def most_powerful_weapon(self):
19 max_damage = 0
20 best_weapon = None
21 for item in self.inventory:
22 try:
23 if item.damage > max_damage:
24 best_weapon = item
25 max_damage = item.damage
26 except AttributeError:
27 pass
28
29 return best_weapon
请注意,这些方法与前面的相似,但不完全相同。现在我们在一个对象内部,我们需要在适当的时候使用self
。
最后,我们需要清理我们的游戏功能来解释这些变化。
game.py
1 from player import Player
2
3
4 def play():
5 print("Escape from Cave Terror!")
6 player = Player()
7 while True:
8 action_input = get_player_command()
9 if action_input in ['n', 'N']:
10 print("Go North!")
11 elif action_input in ['s', 'S']:
12 print("Go South!")
13 elif action_input in ['e', 'E']:
14 print("Go East!")
15 elif action_input in ['w', 'W']:
16 print("Go West!")
17 elif action_input in ['i', 'I']:
18 player.print_inventory()
19 else:
20 print("Invalid action!")
21
22
23 def get_player_command():
24 return input('Action: ')
25
26
27 play()
从其他文件导入
因为我们将代码移动到了其他文件(或模块)中,所以我们需要一种方法让代码能够引用那些模块。关键字import
可以用来从其他模块中获取对象。它出现在 Python 文件的顶部。
有两种主要风格:
import module
和
from module import ClassName
第一种风格让我们可以访问被引用模块中的所有类。但是,我们必须在该模块中的任何类前面加上模块名。比如在玩家的库存里,我们要写items.Rock()
,意思是items
模块里的Rock
类。如果我们只保留Rock()
,Python 会搜索player
模块,自然不会找到这个类。
当您只需要一个模块中的一两个类时,通常使用第二种风格。在我们的游戏中,player
模块只有一个类,所以我们可以使用任何一种风格。为了可读性,我更喜欢player = Player()
而不是player = player.Player()
,所以我选择了第二种import
样式。
现在运行游戏,并验证游戏是否像以前一样运行。这些变化是重构的一个例子。重构是我们在不影响代码行为的情况下提高代码质量的工作。定期后退一步重构代码总是一个好主意,否则你会发现自己有很多杂乱的代码。在企业界,我们通常称之为“遗留”代码。没人想碰遗留代码。
虽然这里的导入看起来很神奇,但那只是因为所有的模块都在我们运行代码的目录中。Python 在几个不同的位置搜索模块。如果你想了解更多,你可以阅读 PYTHONPATH。否则,请记住,你不能将模块放到文件系统中的任意位置,然后期望它们被 Python 拾取。
家庭作业
这一次,作业是复习:
- 回顾一下这些章节,回顾一下你曾经纠结过的事情。否则,休息一下,准备一头扎进世界大厦!
Footnotes 1
https://docs.python.org/3/using/cmdline.html
十一、构建你的世界
在早期,我们给了我们的玩家在游戏世界中移动的能力,但是到目前为止,那个世界只是我们想象中的虚构。在这一章中,我们将最终为玩家创造一个移动的世界。
X-Y 网格
由于这是一个文本冒险,我们只需要担心玩家在两个方向上的移动:向前/向后和向左/向右。这让我们可以像从上往下看玩家一样构建世界,类似于吃豆人或象棋。为了跟踪所有东西的位置,我们使用一个坐标平面,类似于你在数学课上学到的坐标平面。X 轴代表游戏对象的水平位置,Y 轴代表游戏对象的垂直位置。然而,在游戏编程中,我们对网格的定位略有不同。
数学和科学中一个典型的坐标平面是这样的:
(0,2)──(1,2)──(2,2)
│ │ │
(0,1)──(1,1)──(2,1)
│ │ │
(0,0)──(1,0)──(2,0)
但是在游戏编程中,我们翻转 Y 轴,使数字向下而不是向上增加。
(0,0)──(1,0)──(2,0)
│ │ │
(0,1)──(1,1)──(2,1)
│ │ │
(0,2)──(1,2)──(2,2)
如果我们标记空间而不是交叉点,我们最终会得到一个单元格网格。
╔═════╦═════╦═════╗
║ (0,0) ║ (1,0) ║ (2,0) ║
╠═════╬═════╬═════╣
║ (0,1) ║ (1,1) ║ (2,1) ║
╠═════╬═════╬═════╣
║ (0,2) ║ (1,2) ║ (2,2) ║
╚═════╩═════╩═════╝
我们可以把每个网格单元想象成洞穴的不同部分(或者宇宙飞船中的房间,或者城市街区)。玩家在任何时候都会在一个单元中,在那个单元中他们可能会遇到敌人、战利品或一些可爱的风景。他们可以通过使用已经定义的动作北、南、东和西从一个单元移动到另一个单元。这些动作分别对应于向上(y - 1)、向下(y + 1)、向左(x - 1)和向右(x + 1)。
在我们继续深入之前,让我们将其中的一些内容写成代码。首先用下面的 tile 类创建一个名为world.py
的新模块。
1 class
MapTile:
2 def __init__(self, x, y):
3 self.x = x
4 self.y = y
5
6 def intro_text(self):
7 raise NotImplementedError("Create a subclass instead!")
8
9
10 class StartTile(MapTile):
11 def intro_text(self):
12 return """
13 You find yourself in a cave with a flickering torch on the wall.
14 You can make out four paths, each equally as dark and foreboding.
15 """
16
17
18 class BoringTile(MapTile):
19 def intro_text(self):
20 return """
21 This is a very boring part of the cave.
22 """
23
24
25 class VictoryTile(MapTile):
26 def intro_text(self):
27 return """
28 You see a bright light in the distance...
29 ... it grows as you get closer! It's sunlight!
30
31
32 Victory is yours!
Customization Point
更改磁贴的介绍文本以适应您的游戏世界。
MapTile
类是定义初始化器的超类。以下子类是游戏中特定类型的瓷砖。(放心吧,我们会搞定BoringTile
!)在下一节中将使用intro_text()
方法,但是您应该能够猜出它的目的。注意,如果一个淘气的程序员试图直接使用MapTile
,我们会抛出一个异常。
您可能已经注意到介绍文本周围的三重引号("""
)。Python 允许我们通过用三重引号括起文本来编写多行字符串。这可以使编写长字符串变得更容易。
定义了类之后,我们需要将它们放到一个网格中。
35 world_map = [
36 [None,VictoryTile(1,0),None],
37 [None,BoringTile(1,1),None],
38 [BoringTile(0,2),StartTile(1,2),BoringTile(2,2)],
39 [None,BoringTile(1,3),None]
40 ]
这个列表是表示网格模式的一种方式。“外部”列表代表 Y 轴。因此,“外部”列表中的第一项是整个第一行,而“外部”列表中的第二项是整个第二行。每个“内部”列表代表一行。第一行的第一个项目是网格左上角的单幅图块。最后一行的最后一项是网格右下角的单幅图块。None
值用于我们不希望地图图块存在的网格空间。
为了方便起见,我们还添加了一个在坐标上定位图块的函数。
42 def tile_at(x, y):
43 if x < 0 or y < 0:
44 return None
45 try:
46 return world_map[y][x]
47 except IndexError:
48 return None
world_map[y][x]
语法可能看起来令人困惑,但那是因为我们正在处理一个列表的列表。world_map[y]
部分选择地图的行,添加[x]
选择该行中的特定单元格。捕捉IndexError
将处理我们传入的坐标大于地图边界而if x < 0 or y < 0
处理的坐标小于地图边界的情况。如果没有这个函数,每当我们想要查看一个图块是否存在时,我们就必须不断地检查世界的边界。
在世界上移动
我们添加到游戏中的第一个功能是让用户在游戏世界中移动。然而,到目前为止,这些只是安慰剂作用。为了让玩家移动,我们需要向Player
类添加 X-Y 坐标来表示玩家的位置,并且我们需要添加修改这些坐标的方法。从在初始化器中添加self.x
和self.y
开始。
4 class Player:
5 def __init__(self):
6 self.inventory = [items.Rock(),
7 items.Dagger(),
8 'Gold(5)',
9 'Crusty Bread']
10
11 self.x = 1
12 self.y = 2
接下来,在类中添加这些方法:
34 def move(self, dx, dy):
35 self.x += dx
36 self.y += dy
37
38 def move_north(self):
39 self.move(dx=0, dy=-1)
40
41 def move_south(self):
42 self.move(dx=0, dy=1)
43
44 def move_east(self):
45 self.move(dx=1, dy=0)
46
47 def move_west(self):
48 self.move(dx=-1, dy=0)
如果你没有通读作业答案,语法move(dx=0, dy=-1)
可能对你来说是新的。这段代码使用命名参数调用move
方法。命名参数从来都不是必需的,但是它们可以使代码更容易阅读,特别是当方法中有相同类型的参数时。名字dx
和dy
来自数学,分别表示“x 方向的变化”和“y 方向的变化”。因此move()
方法接受 x 和/或 y 方向上的一般变化,特定的方法定义变化的数量。
最后,我们的主游戏循环需要使用这些方法,而不仅仅是打印出占位符文本。跳到game.py
并按如下方式更改play()
功能。
12 if action_input in ['n', 'N']:
13 player.move_north()
14 elif action_input in ['s', 'S']:
15 player.move_south()
16 elif action_input in ['e', 'E']:
17 player.move_east()
18 elif action_input in ['w', 'W']:
19 player.move_west()
20 elif action_input in ['i', 'I']:
21 player.print_inventory()
现在玩家可以在地图上四处移动,但是我们也应该显示每个区块的介绍文字,让玩家知道他们在哪里。不要忘记导入world
模块。
1 from player import Player
2 import world
3
4
5 def play():
6 print("Escape from Cave Terror!")
7 player = Player()
8 while True:
9 room = world.tile_at(player.x, player.y)
10 print(room.intro_text())
11 action_input = get_player_command()
Help! What’s an
AttributeError
?
在游戏的这一点上,一个非常常见的问题是你得到一个错误,显示AttributeError: 'NoneType' object has no attribute 'intro_text'
。
What does it mean?
这意味着 Python 代码说在一个对象上运行intro_text()
方法,但是那个对象实际上是None
类型。
Why does it happen?
当玩家进入一个不存在的房间时,就会出现错误。更具体地说,当玩家移动到地图上标记为None
的部分时。
How do I fix it?
如果错误立即出现,这可能意味着你的播放器的起始位置是错误的。检查Player
级的__init()__
,确保self.x
和self.y
坐标正确。记得从零开始计数!
如果在你四处走动的时候错误出现了,你是在走进一个不存在的房间。如果你想让这个房间存在,改变你的地图。如果您不小心移动到那里,您会发现一个错误,我们将很快修复它!
你现在应该可以测试游戏,验证你可以在世界各地移动。现在有一些错误。值得注意的是,当你到达VictoryTile
时,游戏并没有结束,玩家还可以环绕地图。我们会修复这些错误,但是现在,享受这开始感觉更像一个游戏的事实吧!
十二、让世界变得更有趣
由于玩家没有风险,我们的游戏现在很无聊。我们将通过增加敌人并使玩家变得脆弱来解决这个问题。但是我们也会给玩家反击和治疗的能力,这样他们就有机会活下来。
敌人
到目前为止,创建包含多个子类的基类的模式应该看起来很熟悉了。我们会用这个模式来创造敌人。每个敌人都会有一个name
、hp
(生命值)、和damage
。在一个名为enemies.py
的新模块中创建这些敌人职业。
enemies.py
1 class Enemy:
2 def __init__(self):
3 raise NotImplementedError("Do not create raw Enemy objects.")
4
5 def __str__(self):
6 return self.name
7
8 def is_alive(self):
9 return self.hp > 0
10
11
12 class GiantSpider(Enemy):
13 def __init__(self):
14 self.name = "Giant Spider"
15 self.hp = 10
16 self.damage = 2
17
18
19 class Ogre(Enemy):
20 def __init__(self):
21 self.name = "Ogre"
22 self.hp = 30
23 self.damage = 10
24
25
26 class BatColony(Enemy):
27 def __init__(self):
28 self.name = "Colony of bats"
29 self.hp = 100
30 self.damage = 4
31
32
33 class RockMonster(Enemy):
34 def __init__(self):
35 self.name = "Rock Monster"
36 self.hp = 80
37 self.damage = 15
Customization Point
你可以创造你自己的敌人类型,只要确保他们都有一个name
、hp
和damage
。
要将敌人放入洞穴,我们需要一种新型的Tile
。这张磁贴需要生成一个敌人,介绍文字应该适当地说明敌人是生是死。首先切换到world.py
并将import random
添加到文件的顶部。random
模块内置于 Python 中,它提供了随机生成数字的方法。
由于我们的敌人并不都一样容易被击败,我们希望玩家以不同的频率遭遇他们。例如,我们可以让他们有 50%的机会遇到一只巨大的蜘蛛,而只有 5%的机会遇到一只岩石怪物。random
模块中的random()
方法返回一个从 0.0 到 1.0 的十进制数,这意味着大约 50%的时候,随机返回的数会小于 0.5。
25 class EnemyTile(MapTile):
26 def __init__(self, x, y):
27 r = random.random()
28 if r < 0.50:
29 self.enemy = enemies.GiantSpider()
30 elif r < 0.80:
31 self.enemy = enemies.Ogre()
32 elif r < 0.95:
33 self.enemy = enemies.BatColony()
34 else:
35 self.enemy = enemies.RockMonster()
36
37 super().__init__(x, y)
Customization Point
调整数字使游戏变得更容易或更难。例如,较难的游戏可能使用 0.40、0.70 和 0.90。如果你有超过四种敌人类型,确保你定义了每种类型的百分比。
每创造一个新的EnemyTile
,也会创造一个新的敌人。因为我们对enemy
变量使用了self
关键字,所以敌人将被链接到图块。初始化器底部的代码行将获取这个图块的 X-Y 坐标,并将它们传递给超类MapTile
的__init__()
方法。我们不必在StartTile
中显式地这样做,因为我们没有为那个类定义一个__init()__
方法。如果初始化器没有在子类中定义,超类初始化器将被自动调用。
为了提醒玩家注意敌人,我们可以为EnemyTile
类创建intro_text()
方法。这个方法调用我们在Enemy
类中定义的is_alive()
方法。
39 def intro_text(self):
40 if self.enemy.is_alive():
41 return "A {} awaits!".format(self.enemy.name)
42 else:
43 return "You've defeated the {}.".format(self.enemy.name)
现在我们有了一个更有趣的图块,让我们删除BoringTile
类并用EnemyTile
替换地图中对该类的任何引用。
56 world_map = [
57 [None,VictoryTile(1,0),None],
58 [None,EnemyTile(1,1),None],
59 [EnemyTile(0,2),StartTile(1,2),EnemyTile(2,2)],
60 [None,EnemyTile(1,3),None]
61 ]
你现在可以玩这个游戏,但是你会意识到你不能对敌人做任何事情,而敌人也不能对你做任何事情。修复第一个问题非常简单:我们只需要向Player
类添加一个attack
方法,然后让玩家启动那个动作。
这个在Player
职业上的新方法将利用我们已经写好的most_powerful_weapon()
方法,然后用那个武器对付敌人。确保你import world
也在班上名列前茅!
71 def attack(self):
72 best_weapon = self.most_powerful_weapon()
73 room = world.tile_at(self.x, self.y)
74 enemy = room.enemy
75 print("You use {} against {}!".format(best_weapon.name, enemy.name))
76 enemy.hp -= best_weapon.damage
77 if not enemy.is_alive():
78 print("You killed {}!".format(enemy.name))
79 else:
80 print("{} HP is {}.".format(enemy.name, enemy.hp))
为了允许玩家使用这种方法,在game.py
的分支中再添加一个elif
:
13 if action_input in ['n', 'N']:
14 player.move_north()
15 elif action_input in ['s', 'S']:
16 player.move_south()
17 elif action_input in ['e', 'E']:
18 player.move_east()
19 elif action_input in ['w', 'W']:
20 player.move_west()
21 elif action_input in ['i', 'I']:
22 player.print_inventory()
23 elif action_input in ['a', 'A']:
24 player.attack()
由于这种方法自动选择最佳武器,我从print_inventory()
中删除了向用户显示最佳武器的两行。这是可选的,对游戏没有影响,所以如果你愿意,你可以把它们留在里面,但是你在示例代码中不会再看到那些行。
在敌人能够攻击玩家之前,Player
职业需要有自己的hp
成员。我们可以很容易地在初始化器中添加这一点:
4 class Player:
5 def __init__(self):
6 self.inventory = [items.Rock(),
7 items.Dagger(),
8 'Gold(5)',
9 items.CrustyBread()]
10 self.x = 1
11 self.y = 2
12 self.hp = 100
为了让敌人反击,我们需要在EnemyTile
类内部提供一些逻辑。EnemyTile
类是游戏中了解当前敌人实力的部分。因为我们可能希望其他磁贴也能够响应玩家,所以让我们将该方法一般命名为modify_player()
,这样我们就可以在其他磁贴中重用该名称。
56 def modify_player(self, player):
57 if self.enemy.is_alive():
58 player.hp = player.hp - self.enemy.damage
59 print("Enemy does {} damage. You have {} HP remaining.".
60 format(self.enemy.damage, player.hp))
我们现在应该从游戏循环中调用这个方法,这样玩家一进入磁贴敌人就会做出反应。将这一行添加到play()
方法中:
8 while True:
9 room = world.tile_at(player.x, player.y)
10 print(room.intro_text())
11 room.modify_player(player) # New line
12 action_input = get_player_command()
请注意,无论图块类型如何,每次都会调用该方法。但是因为我们只在EnemyTile
中添加了方法。该游戏在其当前状态下会引发一个异常。解决这个问题的一个方法是在每个 tile 类中添加modify_player()
,但是这违反了前面讨论的 DRY 原则。更好的选择是在MapTile
类中添加一个基本实现。记住,MapTile
的任何子类都将继承MapTile
中的行为,除非它被覆盖。我们并不真的希望 base 方法做什么,所以我们可以使用pass
关键字。
1 class MapTile:
2 def __init__(self, x, y):
3 self.x = x
4 self.y = y
5
6 def intro_text(self):
7 raise NotImplementedError("Create a subclass instead!")
8
9 def modify_player(self, player):
10 pass
现在游戏玩起来应该感觉更“真实”了。有一些危险感,因为你可以承受伤害,但你也感觉在控制中,因为你可以在必要时移动和攻击。事实上,仍然有错误(我们将修复!),但是游戏的核心元素现在都已经到位了。
我选择添加最后一点,这是为了使每个区块的介绍文本更具描述性,基于敌人在区块中的状态。这是增强后的完整的EnemyTile
。
1 class EnemyTile(MapTile):
2 def __init__(self, x, y):
3 r = random.random()
4 if r < 0.50:
5 self.enemy = enemies.GiantSpider()
6 self.alive_text = "A giant spider jumps down from " \
7 "its web in front of you!"
8 self.dead_text = "The corpse of a dead spider " \
9 "rots on the ground."
10 elif r < 0.80:
11 self.enemy = enemies.Ogre()
12 self.alive_text = "An ogre is blocking your path!"
13 self.dead_text = "A dead ogre reminds you of your triumph."
14 elif r < 0.95:
15 self.enemy = enemies.BatColony()
16 self.alive_text = "You hear a squeaking noise growing louder" \
17 "...suddenly you are lost in s swarm of bats!"
18 self.dead_text = "Dozens of dead bats are scattered on the ground."
19 else:
20 self.enemy = enemies.RockMonster()
21 self.alive_text = "You've disturbed a rock monster " \
22 "from his slumber!"
23 self.dead_text = "Defeated, the monster has reverted " \
24 "into an ordinary rock."
25
26 super().__init__(x, y)
27
28 def intro_text(self):
29 text = self.alive_text if self.enemy.is_alive() else self.dead_text
30 return text
31
32 def modify_player(self, player):
33 if self.enemy.is_alive():
34 player.hp = player.hp - self.enemy.damage
35 print("Enemy does {} damage. You have {} HP remaining.".
36 format(self.enemy.damage, player.hp))
Customization Point
重写每个区块的介绍文本,以适应您的游戏心情。
你有药水…或食物吗?
还记得我们给了玩家一些面包吗?现在我们要让它变得有用。我们将把它做成玩家可以用来治疗的东西,而不仅仅是一根绳子。首先,在items.py
中创建这两个类。
32 class Consumable:
33 def __init__(self):
34 raise NotImplementedError("Do not create raw Consumable objects.")
35
36 def __str__(self):
37 return "{} (+{} HP)".format(self.name, self.healing_value)
38
39
40 class CrustyBread(Consumable):
41 def __init__(self):
42 self.name = "Crusty Bread"
43 self.healing_value = 10
Customization Point
为角色在你的游戏世界中可能遇到的食物添加另一种Consumable
类型。
基础类允许我们在未来制造一种新的可消耗物品,比如治疗药剂。目前,我们只有一个子类,CrustyBread
。我们现在应该改变玩家在player.py
中的库存,让它有一个真正的CrustyBread
对象,而不是字符串。
1 class Player:
2 def __init__(self):
3 self.inventory = [items.Rock(),
4 items.Dagger(),
5 'Gold(5)',
6 items.CrustyBread()]
接下来我们需要为玩家创建一个heal()
函数。这一职能应该:
- 确定玩家有什么物品可以用来治疗
- 向玩家显示这些物品
- 接受玩家的输入来决定要使用的物品
- 消耗该物品并将其从库存中移除
听起来很多,但这实际上不会占用太多代码行。首先,我们想在清单中找到Consumable
s。Python 的内置函数isinstance()
接受一个对象和一个类型,并告诉我们该对象是该类型还是该类型的子类。在 REPL,isinstance(1, int)
是True
,isinstance(1, str)
是False
,因为数字一是一个int
,而不是一个str
(弦)。同样,isinstance(CrustyBread(), Consumable)
是True
因为CrustyBread
是Consumable
的子类,但是isinstance(CrustyBread(), Enemy)
是False
。
下面是使用该功能的一种方法:
1 consumables = []
2 for item in self.inventory:
3 if isinstance(item, Consumable):
4 consumables.append(item)
这是完全合理和正确的,但我们可以使用列表理解使它更简洁一点。列表理解是 Python 中的一个特殊特性,它允许我们“动态地”创建一个列表。语法是[what_we_want for thing in iterable if condition]
:
- 新列表中的内容。这通常只是 iterable 中的东西,但是如果我们愿意,我们可以修改它。
thing
:iterable 中的对象。iterable
:可以传递给for-each
循环的东西,比如列表、范围或者元组。condition
:(可选。)限制添加到列表中的内容的条件。
为了使这一点具体化,请尝试 REPL 的这些理解:
[a for a in range(5)]
[a*2 for a in range(5)]
[a for a in range(5) if a > 3]
[a*2 for a in range(5) if a > 3]
以下是我们用来过滤玩家物品的理解:
19 def heal(self):
20 consumables = [item for item in self.inventory
21 if isinstance(item, items.Consumable)]
有时候,玩家没有东西吃,所以我们需要检查这种情况。如果consumables
是一个空列表,我们应该警告玩家并退出heal()
方法。
19 def heal(self):
20 consumables = [item for item in self.inventory
21 if isinstance(item, items.Consumable)]
22 if not consumables:
23 print("You don't have any items to heal you!")
24 return
if not consumables
行是一个快捷方式,表示“如果列表中没有任何内容”或if consumables == []
。如果是这种情况,我们需要退出该功能,我们用return
来完成。你以前见过return
,但我们现在回来了……什么都没有?没错。如果你需要立即退出一个函数,关键字return
本身就可以做到。
接下来,我们需要找出玩家想吃什么。
19 def heal(self):
20 consumables = [item for item in self.inventory
21 if isinstance(item, items.Consumable)]
22 if not consumables:
23 print("You don't have any items to heal you!")
24 return
25
26 for i, item in enumerate(consumables, 1):
27 print("Choose an item to use to heal: ")
28 print("{}. {}".format(i, item))
29
30 valid = False
31 while not valid:
32 choice = input("")
33 try:
34 to_eat = consumables[int(choice) - 1]
35 self.hp = min(100, self.hp + to_eat.healing_value)
36 self.inventory.remove(to_eat)
37 print("Current HP: {}".format(self.hp))
38 valid = True
39 except (ValueError, IndexError):
40 print("Invalid choice, try again.")
这里唯一的新东西是内置函数min()
,它返回两个值中较小的一个。这使得玩家的生命值上限为 100。除此之外,这个函数很好地回顾了我们之前学过的一些概念。你应该一行一行地检查,以确保你理解每一行的目的。
最后,我们需要给玩家使用这个新功能的能力。打开game.py
并添加线条让用户治疗。
25 elif action_input in ['h', 'H']:
26 player.heal()
现在试试这个游戏,确保只要你的库存里有一些硬皮面包,你就能痊愈。当被要求做出选择时,您还应该尝试输入一个像5
这样的错误值,并验证代码是否恰当地处理了这种情况。
我们在这一章给游戏增加了很多新功能。在下一章中,我们将花一些时间清理我们的代码并修复一些错误。
十三、构建世界第二部分
在这一点上,我们已经建立了一个相当不错的游戏世界,玩家可以在其中活动和体验。然而,在这个过程中,我们引入了一些需要解决的非故意的错误。为了帮助修复这些错误,我们将引入一种新的数据结构,称为字典,以帮助使我们的代码更干净。
字典
在现实生活中,一个人使用字典来搜索一个单词并检索其定义。Python 字典基于相同的原理工作,除了不仅仅是单词,任何类型的对象 1 都可以被搜索,并且“定义”也可以是任何类型的对象。通常,我们称之为键-值对,其中键是我们搜索的对象,值是链接到该键的对象。一个具体的例子是一个字典,其中的键是城市的名称,值是人口。我们将用这个例子来介绍使用字典的语法。
创建字典
使用大括号{}
创建字典:
>>> cities = {"Amsterdam": 780000, "Brasilia": 2480000, "Canberra": 360000}
得到
为了从字典中获取一个值,我们使用两种语法之一传入所需的键:
>>> cities = {"Amsterdam": 780000, "Brasilia": 2480000, "Canberra": 360000}
>>> cities['Brasilia']
2480000
>>> cities.get('Brasilia')
2480000
如果密钥存在,这些语法的行为是相同的。但是如果键不存在,就有不同的行为。
>>> cities['Dresden']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'Dresden'
>>> cities.get('Dresden')
>>> cities.get('Dresden', 0)
0
如果没有找到,get()
方法将返回None
,或者我们指定为第二个参数的默认值。[]
语法将抛出一个异常。如果您 100%知道这个键存在于字典中,那么括号语法通常更清晰,可读性更好。但是,如果有可能这个键不存在,那么使用get()
方法会更安全。
添加/更新
向字典添加键值的语法与更新现有键值的语法相同。如果我们传递的键存在,则值被更新。如果键不存在,则添加键-值对。我们是这样添加德累斯顿的:
>>> cities = {"Amsterdam": 780000, "Brasilia": 2480000, "Canberra": 360000}
>>> cities
{'Amsterdam': 780000, 'Canberra': 360000, 'Brasilia': 2480000}
>>> cities['Dresden'] = 525000
>>> cities
{'Dresden': 525000, 'Amsterdam': 780000, 'Canberra': 360000, 'Brasilia': 2480000}
请注意,德累斯顿没有被添加到词典的末尾。这是因为字典是无序的。在大多数情况下,这很好,因为我们只需将一个键传递到字典中,并让计算机计算出如何找到该值。如果您需要一个有序字典,Python 确实在集合模块中提供了一个OrderedDict
类型。 2
如果我们想更新阿姆斯特丹的人口,我们使用相同的语法。
>>> cities = {"Amsterdam": 780000, "Brasilia": 2480000, "Canberra": 360000}
>>> cities
{'Amsterdam': 780000, 'Canberra': 360000, 'Brasilia': 2480000}
>>> cities['Amsterdam'] = 800000
>>> cities
{'Amsterdam': 800000, 'Canberra': 360000, 'Brasilia': 2480000}
这可能是显而易见的,但其含义是不能在字典中存储重复的键。
删除
要从字典中删除一个对,请使用del
关键字。
>>> cities = {"Amsterdam": 780000, "Brasilia": 2480000, "Canberra": 360000}
>>> cities
{'Amsterdam': 780000, 'Canberra': 360000, 'Brasilia': 2480000}
>>> del cities['Amsterdam']
>>> cities
{'Canberra': 360000, 'Brasilia': 2480000}
环
有时在一个for-each
循环中迭代一个字典是有用的。类似于enumerate()
函数,我们使用items()
来迭代一个字典并得到一个元组。具体来说,我们将字典中的每个键值对作为一个元组来获取。
>>> cities = {"Amsterdam": 780000, "Brasilia": 2480000, "Canberra": 360000}
>>> for k, v in cities.items():
... print("City: {}, Pop: {}".format(k, v))
...
City: Amsterdam, Pop: 780000
City: Canberra, Pop: 360000
City: Brasilia, Pop: 2480000
您可能以前没有在 REPL 中见过for
循环,但是您可以像输入任何其他 Python 代码一样输入它们。您甚至可以在 REPL 中定义方法和类。当您按回车键时,您将自动看到…
,这意味着 REPL 正在等待您完成该语句。只要记住,你需要手动输入缩进。完成后,按两次 Return 键来完成循环、函数、类等。
记住前面例子中的k
和v
可以有任何名称,比如city
和pop
,但是k
和v
是常用的,因为它们代表“关键”和“价值”。
限制行动
目前,玩家可以在任何时候采取任何行动,即使它没有意义。例如,玩家可以在开始的时候攻击,或者在完全恢复健康的时候治疗。
为了开始解决这个问题,让我们向game.py
模块添加一个新函数,它将所有法律行为存储在一个字典中。我们将使用一个OrderedDict
来确保玩家每次看到的动作顺序相同。要创建一个有序字典,需要在模块顶部添加from collections import
OrderedDict
。
我们希望为每个动作做这样的事情:
1 actions = OrderedDict()
2 if player.inventory:
3 actions['i'] = player.print_inventory
4 actions['I'] = player.print_inventory
5 print("i: View inventory")
首先我们检查一个条件。在这种情况下,我们检查玩家是否有库存(记住,if my_list
与if my_list != []
相同)。其次,我们将大写和小写热键映射到该动作。最后,我们将动作打印给用户。这里有一个很重要的很容易被忽略的语法差异:我们不写player.print_inventory()
,我们写player.print_inventory
。正如我们之前看到的,my_function()
是执行函数的语法。如果我们只想引用函数,我们使用不带()
的函数名。这很重要,因为我们不想现在就做动作,我们只想把可能的动作存储在字典中。 3
由于我们需要为一系列动作做这件事,我们还将创建一个助手函数,名为action_adder()
。
29 def get_available_actions(room, player):
30 actions = OrderedDict()
31 print("Choose an action: ")
32 if player.inventory:
33 action_adder(actions, 'i', player.print_inventory, "Print inventory")
34 if isinstance(room, world.EnemyTile) and room.enemy.is_alive():
35 action_adder(actions, 'a', player.attack, "Attack")
36 else:
37 if world.tile_at(room.x, room.y - 1):
38 action_adder(actions, 'n', player.move_north, "Go north")
39 if world.tile_at(room.x, room.y + 1):
40 action_adder(actions, 's', player.move_south, "Go south")
41 if world.tile_at(room.x + 1, room.y):
42 action_adder(actions, 'e', player.move_east, "Go east")
43 if world.tile_at(room.x - 1, room.y):
44 action_adder(actions, 'w', player.move_west, "Go west")
45 if player.hp < 100:
46 action_adder(actions, 'h', player.heal, "Heal")
47
48 return actions
49
50 def action_adder(action_dict, hotkey, action, name):
51 action_dict[hotkey.lower()] = action
52 action_dict[hotkey.upper()] = action
53 print("{}: {}".format(hotkey, name))
现在,我们可以随时调用get_available_actions()
来创建热键-动作对的字典。要使用字典,创建另一个新函数。
17 def choose_action(room, player):
18 action = None
19 while not action:
20 available_actions = get_available_actions(room, player)
21 action_input = input("Action: ")
22 action = available_actions.get(action_input)
23 if action:
24 action()
25 else:
26 print("Invalid action!")
我们以前见过这种模式:我们一直循环,直到从用户那里得到有效的输入。然而,这三行需要一些解释:
22 action = available_actions.get(action_input)
23 if action:
24 action()
我们使用get()
而不是[]
语法,因为用户可能输入了无效的热键。if action
线是if action != None
或if action is not None
的简称。如果找到了一个函数,我们通过添加括号来执行这个函数:action()
。这里重要的区别是action
只是对函数的引用,而action()
运行函数。
添加了这个新功能后,我们可以删除get_player_command()
并按如下方式清理play()
:
6 def play():
7 print("Escape from Cave Terror!")
8 player = Player()
9 while True:
10 room = world.tile_at(player.x, player.y)
11 print(room.intro_text())
12 room.modify_player(player)
13 choose_action(room, player)
如果你现在玩这个游戏,你会看到玩家的行动是基于上下文限制的。好了,我们可以从列表中划掉一些 bug 了!我们应该借此机会做一些重构。
扩展世界
目前,我们的世界很小。足够小以至于我们的world_map
仍然具有相当好的可读性和可维护性。但是如果它变得更大,做出改变将会非常令人沮丧。我们还需要手动指定每个图块的 X-Y 坐标。
有时,当程序要求代码的特定部分比语言提供的更灵活、更易维护时,程序员使用领域特定语言(DSL)。DSL 是以一种特定于手边问题的方式编写的;因此,它是一种特定于领域的语言。
我们将使用 DSL 来定义我们世界的地图,然后使用 Python 代码来解释 DSL 并将其转换成world_map
变量。因为地图是一个网格,如果 DSL 能反映出来就好了。通常,DSL 具有完整编程语言的一些特性,但是我们的领域非常简单,一个字符串就可以满足我们的目的。让我们开始勾勒出 DSL 可能的样子,然后我们将编写代码来解释它。
第一次尝试可能是这样的:
1 world_dsl = """
2 ||VictoryTile||
3 ||EnemyTile||
4 |EnemyTile|StartTile|EnemyTile|
5 ||EnemyTile||
6 """
字符串的每一个新行都是地图中的一行,行内的每个图块都由一个|
(竖线)字符分隔。如果没有瓷砖,我们就把两根管子挨着放。我喜欢这里的想法,它确实消除了 X-Y 坐标,但它在视觉上看起来仍然有点不稳定。如果我们试着让它看起来更像网格呢?
1 world_dsl = """
2 | |VictoryTile| |
3 | |EnemyTile| |
4 |EnemyTile|StartTile|EnemyTile|
5 | |EnemyTile| |
6 """
嗯,好多了,但还是不太符合。此外,它相当宽,这意味着一个大的地图可能仍然很难阅读。如果我们缩短这些名字呢?
1 world_dsl = """
2 | |VT| |
3 | |EN| |
4 |EN|ST|EN|
5 | |EN| |
6 """
对我来说,这是一个进步,因为你可以清楚地看到地图的布局,它看起来像一个网格。权衡是我们必须使用瓷砖类型的缩写。我认为这应该没问题,因为即使我们添加了更多的瓷砖类型,我们也只有 5-10 种类型需要跟踪。如果我们有几十种瓷砖类型,缩写可能会变得很难跟踪,我们可能会选择不同的格式。现在,继续将这个world_dsl
变量添加到模块world.py
中的world_map
变量的正上方。
当我们运行 Python 代码时,Python 解释器会进行各种检查,以防止我们出错。其中,它验证语法,如果有语法错误,就阻止程序运行。因为 DSL 是为特定的程序而发明的,所以它们没有任何错误检查。你能想象试图追踪一个 Python 程序中的错误,却发现它是一个语法错误吗?为了我们自己的理智,我们应该为 DSL 添加一些简单的错误检查。
让我们从检查这三个基础开始:
- 应该正好有一个起始牌
- 应该至少有一张胜利牌
- 每行应该有相同数量的单元格
为了帮助我们做到这一点,我们将使用两个字符串方法:count()
和splitlines()
。count()
方法的工作方式与您预期的一样:它计算某个子字符串在字符串中出现的次数。splitlines()
方法在有新行的地方分解多行字符串,并返回一个行列表。记住这一点,下面是验证函数。
81 def is_dsl_valid(dsl):
82 if dsl.count("|ST|") != 1:
83 return False
84 if dsl.count("|VT|") == 0:
85 return False
86 lines = dsl.splitlines()
87 lines = [l for l in lines if l]
88 pipe_counts = [line.count("|") for line in lines]
89 for count in pipe_counts:
90 if count != pipe_counts[0]:
91 return False
92
93 return True
由于dsl
是一个字符串,我们可以立即计算开始牌和胜利牌的数量,以确保满足这些要求。为了检查每一行中瓷砖的数量,我们首先需要将字符串分成若干行。一旦分成行,我们使用列表理解来过滤掉任何空行(因为我们使用了三重引号字符串语法,所以在开头和结尾应该有一个空行)。记住if l
是if l != ''
的简写。过滤后,我们使用第二个列表理解来计算每一行中管道的数量,然后确保每一行都有与第一行相同的管道数量。如果这些条件中的任何一个失败,函数立即返回False
。
接下来,我们需要添加使用 DSL 构建world_map
变量的函数。首先,我们需要定义一个字典,将 DSL 缩写映射到 tile 类型。
95 tile_type_dict = {"VT": VictoryTile,
96 "EN": EnemyTile,
97 "ST": StartTile,
98 " ": None}
请注意,我们将缩写映射到图块类型,而不是图块对象。EnemyTile
和EnemyTile(1,5)
的区别在于,前者是类型,后者是类型的新实例。这类似于go_north
是对函数的引用,而go_north()
调用函数。
因为我们现在要以编程方式构建world_map
,所以用world_map = []
替换现有的映射。在此之下,我们将添加解析 DSL 的函数。通常,该函数将验证 DSL,逐行逐单元地查找缩写的映射,并基于找到的图块类型创建新的图块对象。
104 def parse_world_dsl():
105 if not is_dsl_valid(world_dsl):
106 raise SyntaxError("DSL is invalid!")
107
108 dsl_lines = world_dsl.splitlines()
109 dsl_lines = [x for x in dsl_lines if x]
110
111 for y, dsl_row in enumerate(dsl_lines):
112 row = []
113 dsl_cells = dsl_row.split("|")
114 dsl_cells = [c for c in dsl_cells if c]
115 for x, dsl_cell in enumerate(dsl_cells):
116 tile_type = tile_type_dict[dsl_cell]
117 row.append(tile_type(x, y) if tile_type else None)
118
119 world_map.append(row)
您还应该在game.py
中调用这个新函数。
6 def play():
7 print("Escape from Cave Terror!")
8 world.parse_world_dsl()
9 player = Player()
10 while True:
11 room = world.tile_at(player.x, player.y)
12 print(room.intro_text())
13 room.modify_player(player)
14 choose_action(room, player)
让我们详细回顾一下这个函数是做什么的。首先,DSL 被验证,如果它无效,我们抛出一个SyntaxError
。这是另一个异常的例子,我们会故意引发它来提醒程序员他们做错了什么。接下来,就像前面一样,我们将 DSL 分成几行,并删除由三重引号语法创建的空行。函数的最后一部分实际上创建了世界。它有点密集,所以我将解释每一行:
1 # Iterate over each line in the DSL.
2 # Instead of i, the variable y is used because
3 # we're working with an X-Y grid.
4 for y, dsl_row in enumerate(dsl_lines):
5 # Create an object to store the tiles
6 row = []
7 # Split the line into abbreviations using
8 # the "split" method
9 dsl_cells = dsl_row.split("|")
10 # The split method includes the beginning
11 # and end of the line so we need to remove
12 # those nonexistent cells
13 dsl_cells = [c for c in dsl_cells if c]
14 # Iterate over each cell in the DSL line
15 # Instead of j, the variable x is used because
16 # we're working with an X-Y grid.
17 for x, dsl_cell in enumerate(dsl_cells):
18 # Look up the abbreviation in the dictionary
19 tile_type = tile_type_dict[dsl_cell]
20 # If the dictionary returned a valid type, create
21 # a new tile object, pass it the X-Y coordinates
22 # as required by the tile __init__(), and add
23 # it to the row object. If None was found in the
24 # dictionary, we just add None.
25 row.append(tile_type(x, y) if tile_type else None)
26 # Add the whole row to the world_map
27 world_map.append(row)
语法value_if_true
if
condition
else
value_if_
false
是一种稍微不同的编写if
语句的方式,当您只需要切换基于布尔表达式的值时。如示例row.append(tile_type(x, y) if tile_type else None)
所示,它可以将原本多行的代码块压缩成一行。这种语法有时被称为三元语法。
虽然这需要大量的工作,但从玩家的角度来看,游戏相对来说没有什么变化。这就是重构开发者的命运!但别担心,这不是徒劳的。这项工作是为了让我们的生活更轻松。现在,扩展地图是微不足道的,即使是 20x20 的世界也很容易查看和编辑。
对应用程序的具体细节做了很多修改,所以你可能会在这里或那里有一些错误。如果遇到困难,请务必仔细检查您的代码,并将其与随书附带的代码进行比较。
Footnotes 1
实际上,只有“不可变的”对象可以用于字典中的键。不可变对象是不能改变的对象,例如字符串或整数。
2
https://docs.python.org/3.5/library/collections.html
3
Python 的这个特性非常方便,但是很多语言不支持它。在 Python 中,函数是“一级对象”,这意味着它们可以像字符串、整数或MapTiles
一样被传递和修改。
十四、经济学 101
就像我们把硬皮面包变成了绳子一样,这一章将着重于让黄金成为游戏中真正的商品。毕竟,没有买卖战利品的能力,什么样的冒险才是完整的呢?
分享财富
虽然有可能追踪黄金作为一个实际的项目,但如果玩家有很多黄金,这可能会失去控制。取而代之的是,更容易把黄金和物品分开处理,只需要有一个和玩家相关的统计数据就可以了。更新Player
类的__init__()
函数,将黄金移出库存列表。
4 class Player:
5 def __init__(self):
6 self.inventory = [items.Rock(),
7 items.Dagger(),
8 items.CrustyBread()]
9 self.x = world.start_tile_location[0]
10 self.y = world.start_tile_location[1]
11 self.hp = 100
12 self.gold = 5
我们也应该更新print_inventory()
方法,让玩家知道自己有多少黄金。
14 def print_inventory(self):
15 print("Inventory:")
16 for item in self.inventory:
17 print('* ' + str(item))
18 print("Gold: {}".format(self.gold))
现在玩家有钱花了,我们应该给游戏中的物品添加一个value
属性,让它变得有意义。这里是带有value
的RustySword
类。
26 class RustySword(Weapon):
27 def __init__(self):
28 self.name = "Rusty sword"
29 self.description = "This sword is showing its age, " \
30 "but still has some fight in it."
31 self.damage = 20
32 self.value = 100
您还需要为其他项目添加一个值。以下是我选择的价值观。
| 班级 | 价值 | | :-- | :-- | | `Rock` | one | | `Dagger` | Twenty | | `RustySword` | One hundred | | `CrustyBread` | Twelve |Customization Point
更改项目的值,使它们更符合需要或不太符合需要。
说到这里,让我们添加另一个项目:一种比硬皮面包更强、更有价值的东西。
49 class HealingPotion(Consumable):
50 def __init__(self):
51 self.name = "Healing Potion"
52 self.healing_value = 50
53 self.value = 60
当然,为了让游戏有经济效益,玩家需要有人来交易。为了将其他角色引入游戏,我们将创建一个新的npc.py
模块。我们将使用大家都熟悉的模式——通用基类和特定子类——来定义Trader
类。
npc.py
1 import items
2
3
4 class NonPlayableCharacter():
5 def __init__(self):
6 raise NotImplementedError("Do not create raw NPC objects.")
7
8 def __str__(self):
9 return self.name
10
11
12 class Trader(NonPlayableCharacter):
13 def __init__(self):
14 self.name = "Trader"
15 self.gold = 100
16 self.inventory = [items.CrustyBread(),
17 items.CrustyBread(),
18 items.CrustyBread(),
19 items.HealingPotion(),
20 items.HealingPotion()]
Customization Point
更改Trader
库存中的物品。一个更难的游戏可能有更少的Consumable
,而一个更容易的游戏可能有更多的Consumable
和Weapon
给交易者一个家
就像拥有一个Enemy
对象的EnemyTile
一样,我们将创建一个拥有一个Trader
对象的TraderTile
。(别忘了import npc
!)
98 class TraderTile(MapTile):
99 def __init__(self, x, y):
100 self.trader = npc.Trader()
101 super().__init__(x, y)
为了处理买卖业务,我们将向该类添加一个trade()
方法。该方法将显示所有可用于交易的物品(即卖家的库存),要求玩家选择一个物品,如果玩家做出选择,则最终完成交易。
刚编班的时候用了一个buy()
和sell()
的方法。然而,很明显这两种方法非常相似。为了避免重复代码,我修改了使用两种方法的原始计划,改为使用一种通用的“交易”方法,其中一个人是买方,一个人是卖方。如果玩家在买,交易者在卖,如果玩家在卖,交易者在买。这个过程被称为抽象,将代码抽象成更通用的模式通常是一个好主意,因为这使得代码更加可重用。学习抽象需要实践,有时,就像在这个例子中,需要在抽象展现自己之前写出一些代码。
118 def trade(self, buyer, seller):
119 for i, item in enumerate(seller.inventory, 1):
120 print("{}. {} - {} Gold".format(i, item.name, item.value))
121 while True:
122 user_input = input("Choose an item or press Q to exit: ")
123 if user_input in ['Q', 'q']:
124 return
125 else:
126 try:
127 choice = int(user_input)
128 to_swap = seller.inventory[choice - 1]
129 self.swap(seller, buyer, to_swap)
130 except ValueError:
131 print("Invalid choice!")
这个方法使用了一个看起来像无限循环的东西(while True
),但是你会注意到,如果玩家选择不进行交易就退出,那么return
关键字被用来退出这个方法。这个方法还调用了swap()
方法,这个方法还没有被编写,但是我们现在将添加它。
133 def swap(self, seller, buyer, item):
134 if item.value > buyer.gold:
135 print("That's too expensive")
136 return
137 seller.inventory.remove(item)
138 buyer.inventory.append(item)
139 seller.gold = seller.gold + item.value
140 buyer.gold = buyer.gold - item.value
141 print("Trade complete!")
这种方法只是将物品从卖家手中拿走,交给买家,然后用交易物品的黄金价值进行反向操作。因为这个函数“双向”工作,所以我们需要一种方法让玩家在想买或卖物品时启动。方法check_if_trade()
将接受用户输入来控制谁是买家和卖家。
103 def check_if_trade(self, player):
104 while True:
105 print("Would you like to (B)uy, (S)ell, or (Q)uit?")
106 user_input = input()
107 if user_input in ['Q', 'q']:
108 return
109 elif user_input in ['B', 'b']:
110 print("Here's whats available to buy: ")
111 self.trade(buyer=player, seller=self.trader)
112 elif user_input in ['S', 's']:
113 print("Here's whats available to sell: ")
114 self.trade(buyer=self.trader, seller=player)
115 else:
116 print("Invalid choice!")
这个方法也使用了一个看似无限的循环,但是当玩家完成交易时,使用return
退出。根据玩家的选择,player
对象被传递给trade()
作为买方或卖方。命名参数用于明确区分谁是谁。
最后,我们需要给这个房间一段介绍文字:
144 def intro_text(self):
145 return """
146 A frail not-quite-human, not-quite-creature squats in the corner
147 clinking his gold coins together. He looks willing to trade.
148 """
为了让玩家发起交易,我们需要在Player
类中创建一个动作,然后将其添加到可用动作列表中。将该方法添加到player.py
中的Player
类的底部。
83 def trade(self):
84 room = world.tile_at(self.x, self.y)
85 room.check_if_trade(self)
现在切换到game.py
,加上这个检查,看看玩家是不是在一个TraderTile
。
32 if player.inventory:
33 action_adder(actions, 'i', player.print_inventory, "Print inventory")
34 if isinstance(room, world.TraderTile):
35 action_adder(actions, 't', player.trade, "Trade")
36 if isinstance(room, world.EnemyTile) and room.enemy.is_alive():
37 action_adder(actions, 'a', player.attack, "Attack")
扩展世界
为了让商店的概念对玩家有用,我们还需要给玩家增加财富的机会。我们将在world.py
: FindGoldTile
中再创建一个图块类型。这个方块将有一个随机数量的黄金寻找和一个布尔值,如果黄金已被拾起跟踪。这个布尔变量确保玩家不能只是反复进出房间来无限增加财富!
75 class FindGoldTile(MapTile):
76 def __init__(self, x, y):
77 self.gold = random.randint(1, 50)
78 self.gold_claimed = False
79 super().__init__(x, y)
80
81 def modify_player(self, player):
82 if not self.gold_claimed:
83 self.gold_claimed = True
84 player.gold = player.gold + self.gold
85 print("+{} gold added.".format(self.gold))
86
87 def intro_text(self):
88 if self.gold_claimed:
89 return """
90 Another unremarkable part of the cave. You must forge onwards.
91 """
92 else:
93 return """
94 Someone dropped some gold. You pick it up.
95 """
这里的新功能是random.randint()
。与返回小数的random.random()
不同,random.randint()
返回给定范围内的整数。
有了两种新的瓷砖类型,我们可以扩大游戏世界,为游戏增添更多的趣味。这是我选择的布局:
150 world_dsl = """
151 |EN|EN|VT|EN|EN|
152 |EN| | | |EN|
153 |EN|FG|EN| |TT|
154 |TT| |ST|FG|EN|
155 |FG| |EN| |FG|
156 """
Customization Point
以你喜欢的任何方式改变游戏世界的布局。只要确保它符合 DSL 的要求,否则你会得到一个SyntaxError
。
为了确保我们的 DSL 仍然工作,我们需要将新的 tile 缩写添加到字典中。
173 tile_type_dict = {"VT": VictoryTile,
174 "EN": EnemyTile,
175 "ST": StartTile,
176 "FG": FindGoldTile,
177 "TT": TraderTile,
178 " ": None}
如果你现在运行游戏,你会遇到一些问题,因为开始的瓷砖移动。理想情况下,我们希望能够调整 DSL,而无需手动调整Player
类中的起始位置。由于只有一个StartTile
(我们在is_dsl_valid()
中强制执行了这一点),在解析期间记录它的位置并在Player
类中使用该值会很容易。为了记录位置,我们在world.py
模块中需要一个名为start_tile_location
的新变量。该变量将在parse_world_dsl()
功能中设置。
183 start_tile_location = None
184
185 def parse_world_dsl():
186 if not is_dsl_valid(world_dsl):
187 raise SyntaxError("DSL is invalid!")
188
189 dsl_lines = world_dsl.splitlines()
190 dsl_lines = [x for x in dsl_lines if x]
191
192 for y, dsl_row in enumerate(dsl_lines):
193 row = []
194 dsl_cells = dsl_row.split("|")
195 dsl_cells = [c for c in dsl_cells if c]
196 for x, dsl_cell in enumerate(dsl_cells):
197 tile_type = tile_type_dict[dsl_cell]
198 if tile_type == StartTile:
199 global start_tile_location
200 start_tile_location = x, y
201 row.append(tile_type(x, y) if tile_type else None)
202
203 world_map.append(row)
你应该已经注意到,在设置变量之前,我们必须包含一个global start_tile_location
行。关键字global
允许我们从函数内部访问模块级的变量。在模块级声明的变量被认为是“全局的”,因为使用该模块的应用程序的任何部分都可以访问该变量。一般来说,修改全局变量可能会产生不良后果,尤其是当其他模块依赖于该变量时。所以global
关键字是一种迫使程序员清楚他们修改全局变量意图的方式。如果我们想避免使用全局变量,我们可以让start_tile_
location
成为一个解析 DSL 并返回起始位置的函数。然而,我认为这会在代码中引入不必要的复杂性。这个全局变量的使用是非常有限的,我们知道它只会被设置一次和访问一次。全局变量并不邪恶;他们只是需要一些额外的照顾。
当我们设置start_tile_location
变量时,我们使用元组语法将x
和y
存储在变量中。知道坐标是这样存储的,我们可以从player.py
的Player
类中引用它们。
4 class Player:
5 def __init__(self):
6 self.inventory = [items.Rock(),
7 items.Dagger(),
8 items.CrustyBread()]
9 self.x = world.start_tile_location[0]
10 self.y = world.start_tile_location[1]
11 self.hp = 100
12 self.gold = 5
元组值可以像列表一样通过索引来访问。因为我们知道变量存储为(X,Y),所以索引 0 处的值是 X 坐标,索引 1 处的值是 Y 坐标。 1 该代码依赖于世界被首先创建,否则start_tile_location
仍然是None
。谢天谢地,在game.py
中,我们在创建玩家对象之前解析了 DSL。
这最后一个变化使得 DSL 与游戏的其他部分完全分离,因为游戏不需要“知道”DSL 的任何细节。通常,应用程序的各个部分越不耦合越好。解耦允许您更改应用程序的一部分,而不更改另一部分。在这个应用程序中,这意味着您可以在任何时候修改世界地图,而不必更改代码的另一部分。
Customization Point
添加一些新的瓷砖类型。也许你可以有一个FindItemTile
、一个InstantDeathTile
或者一个BossTile
,有一个特别难对付的敌人。
Footnotes 1
如果你觉得通过索引访问元组值有点笨拙,你不会错。Python 有一个名为元组的替代方法(参见 https://docs.python.org/3.5/library/collections.html#collections.namedtuple
),如果你愿意,它也可以在这种情况下工作。
十五、终场
我们成功了!就像你可以宣告学习 Python 的胜利一样,我们的游戏玩家很快也能宣告胜利。我们只需要再添加一个功能,这样当玩家死亡或到达胜利牌时游戏就结束了。
收尾工作
我们可以从修改player.py
中的Player
类开始,添加一个victory
属性和一个is_alive()
方法。
5 class Player:
6 def __init__(self):
7 self.inventory = [items.Rock(),
8 items.Dagger(),
9 items.CrustyBread()]
10 self.x = world.start_tile_location[0]
11 self.y = world.start_tile_location[1]
12 self.hp = 100
13 self.gold = 5
14 self.victory = False
15
16 def is_alive(self):
17 return self.hp > 0
我们应该将world.py
中的VictoryTile
的victory
属性设置为 true。
64 class VictoryTile(MapTile):
65 def modify_player(self, player):
66 player.victory = True
67
68 def intro_text(self):
69 return """
70 You see a bright light in the distance...
71 ... it grows as you get closer! It's sunlight!
72
73
74 Victory is yours!
75 """
接下来,我们需要调整game.py
中游戏循环的条件,以便检查玩家是否活着或者是否已经取得胜利。在play
方法中,将while True
改为while player.is_alive() and not player.victory
。另一种表述这种情况的方式是“继续下去,直到玩家死亡或获胜。”
我们还需要在modify_player()
运行后添加一个检查,因为该功能可能会导致玩家输赢。最后,我们应该让玩家知道他们是否死了。下面是完整的play()
方法。
6 def play():
7 print("Escape from Cave Terror!")
8 world.parse_world_dsl()
9 player = Player()
10 while player.is_alive() and not player.victory:
11 room = world.tile_at(player.x, player.y)
12 print(room.intro_text())
13 room.modify_player(player)
14 if player.is_alive() and not player.victory:
15 choose_action(room, player)
16 elif not player.is_alive():
17 print("Your journey has come to an early end!")
你现在可以玩游戏了,如果你死了或者当你到达胜利牌的时候游戏就会结束。
下一步是什么?
首先,花点时间祝贺自己。你从对 Python 一无所知到拥有一个完整的工作游戏。但我猜你想做的不仅仅是构建我组装的游戏。本节包含一些关于下一步该做什么的建议。
为游戏增加更多功能
你的想象力是你在文本冒险中所能做的极限。以下是一些想法:
- 再加一个能给任务的 NPC。然后,在玩家完成任务的地方添加另一种牌类型。
- 杀死敌人后,让敌人拥有可以取回的战利品。
- 给予玩家消耗法力的魔法攻击。每当玩家进入房间和/或使用药剂时,允许法力补充一点。
- 允许玩家穿上一定比例减少敌人攻击的盔甲。
使用 Python 脚本简化您的工作
Python 是一种很好的语言,可以用来编写自动化枯燥任务的小脚本。修改电子表格,从网站获取数据等。要获得更多指导,请看一下 Al Sweigart 的《用 Python 自动化枯燥的东西》【1】。
编写一个 Web 应用程序
这比你想象的要简单,尤其是用 Python。既然我假设你是编程新手,我推荐 Udacity 课程如何建立一个博客 2 ,作者是 Steve Huffman(因 Reddit 出名)。本课程教授使用 Python 的 web 开发基础知识。
关于 Python 还有很多很多要学习,我鼓励你继续学习。无论你的兴趣在哪里,都有很多适合初学者的资源。编码快乐!
Footnotes 1
https://automatetheboringstuff.com
2
https://www.udacity.com/course/web-development--cs253
十六、作业解决方案
本附录中的解决方案只有在给作业问题一个公平的机会后才能参考。如果你卡住了,将你的代码与解决方案代码进行比较,确保你能遵循解决方案中的逻辑。
您也可以将您的代码与解决方案进行比较,看看您是否以正确的方式解决了问题。虽然我鼓励这样做,但每个解决方案都只代表解决问题的一种可能方式。一般来说,代码应该是正确的、可读的和高效的——按照这个顺序。您的代码可能有所不同,但仍然符合这些目标。如果你的代码是不同的,试着看看你是否能从我的解决方案中学到一些东西。你甚至会发现你的解决方案比我的好。解决问题总是有多种方法,只要我们不断相互学习,我们就在做正确的事情。
第二章:你的第一个程序
-
制作一个名为
calculator.py
的新模块,并编写将"Which numbers do you want to add?"
输出到控制台的代码。calculator.py1 print("Which numbers do you want to add?")
-
运行计算器程序,确保它工作正常。
1 $ python calculator.py 2 Which numbers do you want to add?
-
尝试从代码中删除引号。会发生什么?
1 $ python calculator.py 2 File "calculator.py", line 1 3 print(What numbers do you want to add?) 4 ^ 5 SyntaxError: invalid syntax
第三章:倾听你的用户
-
my_variable = 5
和my_variable = '5'
有什么区别?第一个是实际的数字 5,而第二个只是文本字符“5”。 -
print(n)
和print('n')
有什么区别?第一个将试图打印出变量n
的值,而第二个将只打印出字符“n”。 -
尝试不使用变量重写
echo.py
。echo.py1 print(input("Type some text: "))
第章第四章:决策
-
=
和==
有什么区别?=
操作符给变量赋值,而==
操作符比较两个值看它们是否相等。 -
创建
ages.py
来询问用户的年龄,然后打印出与他们年龄相关的信息。例如,如果那个人是成年人,他们是否可以买酒,他们是否可以投票,等等。注意:int()
函数可以将字符串转换成整数。这是一个例子;你的会不同:年龄。py1 age = int(input("What is your age? ")) 2 if age < 18: 3 print("You are a child.") 4 elif 18 < age < 21: 5 print("You are an adult, but you cannot purchase alcohol.") 6 else: 7 print("You are an adult.") 8 if age >= 16: 9 print("You are allowed to drive.") 10 else: 11 print("You are not allowed to drive")
第五章:功能
-
用什么关键字创建函数?
def
关键字。 -
无参数函数和参数化函数有什么区别?这些函数在代码中的调用方式不同。对
do_domething()
的调用是无参数的,对do_something(a, b)
的调用是参数化的。参数化函数需要输入来完成工作,而无参数函数已经可以访问完成工作所需的一切。 -
当阅读一个函数的代码时,你如何知道它只是“做一些事情”还是“给出一些回报”?如果函数包含关键字
return
后跟一个值,那么它会返回一些东西。 -
创建
doubler.py
来包含一个名为double
的函数,该函数接受单个参数。该函数应该返回乘以 2 的输入值。打印出 12345 和 1.57 的双精度值。doubler.py1 def double(a): 2 return a * 2 3 4 print(double(12345)) 5 print(double(1.57))
-
创建
calculator.py
来包含一个名为add
的函数,它接受两个参数。该函数应该返回两个数的和。打印出 45 和 55 的总和。calculator.py1 def add(a, b): 2 return a + b 3 4 print(add(45, 55))
-
创建
user_calculator.py
并重用之前练习中的add
函数。这一次,要求用户输入两个数字,并打印这两个数字的总和。提示:如果这只适用于整数,那也没关系。user_calculator.py1 def add(a, b): 2 return a + b 3 4 num1 = int(input("Please enter your 1st number: ")) 5 num2 = int(input("Please enter your 2nd number: ")) 6 7 print(add(num1, num2))
第六章:列表
-
哪两个特征使集合成为列表?列表是有序的,它们可能包含重复项。
-
编写一个名为
favorites.py
的脚本,允许用户输入他们最喜欢的三种食物。把这些食物列成清单。favorites.py1 favorites = [] 2 favorites.append(input("What is your favorite food? ")) 3 favorites.append(input("What is your 2nd favorite food? ")) 4 favorites.append(input("What is your 3rd favorite food? "))
-
使用索引:
['Mercury', 'Venus', 'Earth']
打印出该列表的中间项。你能修改你的代码来处理任意大小的列表吗(假设有奇数个条目)?提示:回想一下将某物转换成整数的int
函数。1 planets = ['Mercury', 'Venus', 'Earth'] 2 print(planets[1])
或
1 planets = ['Mercury', 'Venus', 'Earth'] 2 middle_index = int(len(planets) / 2) 3 print(planets[middle_index])
-
运行这段代码会发生什么?你知道为什么吗?抛出一个
IndexError: list index out of range
。这是因为列表索引是从零开始的。第一项位于索引 0,最后一项位于索引 2,但我们要求索引 3,因为列表包含三项。
第七章:循环
-
对于以下各项,您会使用哪种循环:
- 一个每五秒钟检查一次温度的程序是一个
while
循环,因为程序需要保持运行,没有确定的终点。 - 一个在杂货店打印收据的程序产生了一个
for-each
循环,因为我们想要打印顾客购买的每一件商品。(从技术上讲,也可以使用while
循环,但是for-each
循环更惯用。) - 在保龄球游戏中记录分数的程序是一个
for-each
循环,因为我们想要遍历游戏中的十个回合中的每一个回合来找到最终的分数。(从技术上讲,也可以使用while
循环,但是for-each
循环更惯用。) - 一个随机播放音乐库中歌曲的程序会产生一个
while
循环,因为我们不知道用户会运行这个程序多长时间。您可能会尝试使用一个for-each
循环来遍历库中的每首歌曲,但是如果用户在遍历完所有歌曲后还想继续播放音乐,该怎么办呢?
- 一个每五秒钟检查一次温度的程序是一个
-
打开关于函数的第五章中的
user_calculator.py
,并添加一个while
循环,允许用户不断添加两个数。user_calculator.py1 def add(a, b): 2 return a + b 3 4 while True: 5 num1 = int(input("Please enter your 1st number: ")) 6 num2 = int(input("Please enter your 2nd number: ")) 7 8 print(add(num1, num2))
-
写一个脚本,显示 1 * 1 到 10 * 10 的乘法表。乘法. py
1 for i in range(1, 11): 2 line = "" 3 for j in range(1, 11): 4 line = line + str(i * j) + " " 5 print(line)
-
使用
enumerate
和%
操作符打印列表中的每三个单词。greek.py1 letters = ['alpha','beta','gamma','delta','epsilon','zeta','eta'] 2 for i, letter in enumerate(letters): 3 if i % 3 == 0: 4 print(letter)
第章第八章:对象
-
类和对象的区别是什么?类是代码中的模板,它定义了类所代表的“事物”的数据元素。对象是程序运行时驻留在内存中的类的特定实例。
-
一个类中的
__init__()
方法的目的是什么?它在对象创建后立即运行,通常用于设置类中成员的值。 -
__str__()
和str()
有什么区别?__str()__
是一个可以在类中定义的方法,它告诉 Python 如何打印由该类构成的对象,以及如何将这些对象表示为字符串。str()
是一个内置函数,试图将一个对象转换成一个字符串。 -
创建一个名为
food.py
的文件,其中包含一个名为Food
的类。这个类应该有四个成员:name
、carbs
、protein
和fat
。这些成员应该在类的初始化器中设置。food.py1 class Food: 2 def __init__(self, name, carbs, protein, fat): 3 self.name = name 4 self.carbs = carbs 5 self.protein = protein 6 self.fat = fat
-
向名为
calories()
的Food
类添加一个方法,计算食物中的卡路里数。每克碳水化合物含 4 卡路里,每克蛋白质含 4 卡路里,每克脂肪含 9 卡路里。food.py1 def calories(self): 2 return self.carbs * 4 + self.protein * 4 + self.fat * 9
-
创建另一个名为
Recipe
的类,它的初始化器接受一个name
和一个名为ingredients
的食物列表。向该类添加一个名为calories()
的方法,该方法返回食谱中的总热量。食物。py1 class Recipe: 2 def __init__(self, name, ingredients): 3 self.name = name 4 self.ingredients = ingredients 5 6 def calories(self): 7 total = 0 8 for ingredient in self.ingredients: 9 total = total + ingredient.calories() 10 11 return total
-
向
Recipe
类添加一个__str__()
方法,该方法只返回菜谱的名称。food.py1 def __str__(self): 2 return self.name
-
创建两个(简单!)食谱,并打印出每个食谱的名称和总热量。如果你愿意,你可以编造碳水化合物、蛋白质和脂肪的数字。为了加分,试着用两个或 200 个食谱的方式来做。在下面的回答中,我使用了一个名为 named arguments 的功能来阐明哪个数字是脂肪、蛋白质等。这不是必需的,但是我想给你看一个选项,当你有很多论点的时候,让论点更清晰。我的解决方案“适用于两个或 200 个食谱”,因为它将每个食谱存储在一个列表中,然后使用一个循环来打印列表中的所有内容。food.py
1 pbj = Recipe("Peanut Butter & Jelly", [ 2 Food(name="Peanut Butter", carbs=6, protein=8, fat=16), 3 Food(name="Jelly", carbs=13, protein=0, fat=0), 4 Food(name="Bread", carbs=24, protein=7, fat=2)] 5 ) 6 7 omelette = Recipe("Omelette du Fromage", [ 8 Food(name="Eggs", carbs=3, protein=18, fat=15), 9 Food(name="Cheese", carbs=5, protein=24, fat=24) 10 ]) 11 12 recipes = [pbj, omelette] 13 14 for recipe in recipes: 15 print("{}: {} calories".format(recipe.name, recipe.calories()))
-
这个脚本中的类是继承还是组合的例子,为什么?作文。一个
Recipe
不与Food
对象共享任何行为,但是一个Recipe
包含Food
对象。
第九章例外
-
用
try
和except
更新user_calculator.py
来处理没有输入数字的用户。user_calculator.py1 def add(a, b): 2 return a + b 3 4 while True: 5 try: 6 num1 = int(input("Please enter your 1st number: ")) 7 num2 = int(input("Please enter your 2nd number: ")) 8 9 print(add(num1, num2)) 10 except ValueError: 11 print("You must enter a number.")
-
None
是什么意思,什么时候用?关键字None
代表没有值,当我们想要创建一个没有值的变量时使用。 -
pass
是什么意思,什么时候用?关键字pass
的意思是“忽略这个代码块”。它可以用在任何没有主体的代码块中,如空类或方法,也可以用在被忽略的异常中。 -
创建一个
Vehicle
类,一个Motorcycle
类是Vehicle
的子类,其wheels
属性设置为 2,一个Car
类是Vehicle
的子类,其wheels
属性设置为 4。添加代码,如果程序员试图创建一个Vehicle
,将引发一个异常。车辆。py1 class Vehicle: 2 def __init__(self): 3 raise NotImplementedError("You must use a subclass.") 4 5 6 class Motorcycle(Vehicle): 7 def __init__(self): 8 self.wheels = 2 9 10 11 class Car(Vehicle): 12 def __init__(self): 13 self.wheels = 4
十七、常见错误
我们都想成为完美的程序员,但那当然是不可能的!这里列出了其他人遇到的错误,以及您可以如何修复它们。
属性错误
AttributeError: 'NoneType' object has no attribute 'intro_text'
检查你的世界地图和玩家位置。这个错误意味着玩家已经进入了地图上的None
点。那不应该发生,所以要么你的玩家在你不期望的地方,要么你的地图没有正确配置。
名称错误
NameError: name 'enemies' is not defined (or player, world, etc.)
检查你的进口。这个错误意味着 Python 看到了它不理解的东西的名称。为了让 Python 理解enemies
(或任何其他模块),它必须包含在文件顶部的import
语句中。
类型错误
TypeError: super() takes at least 1 argument (0 given)
使用 Python 3.X。如果使用 Python 2,可能会出现此错误。如果您不确定您使用的是哪个版本,请查看第一章中的“设置您的工作区”。