创造你自己的 Python 文本冒险(二)
十二、让世界变得更有趣
由于玩家没有风险,我们的游戏现在很无聊。我们将通过增加敌人并使玩家变得脆弱来解决这个问题。但是我们也会给玩家反击和治疗的能力,这样他们就有机会活下来。
敌人
到目前为止,创建包含多个子类的基类的模式应该看起来很熟悉了。我们会用这个模式来创造敌人。每个敌人都会有一个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,可能会出现此错误。如果您不确定您使用的是哪个版本,请查看第一章中的“设置您的工作区”。