作者 | 梁唐
出品 | 公众号:Coder梁(ID:Coder_LT)
大家好,日拱一卒,我是梁唐。
今天我们继续来肝伯克利的CS61A,这一次我们做的是这门课的第二个大作业。
这个项目非常有意思,让我们做一个类似植物大战僵尸的小游戏。只不过这里改成了蚂蚁大战蜜蜂,蜜蜂一波一波来袭,我们要建造各种功能的蚂蚁抵御蜜蜂的进攻。
完全做好之后的运行效果是这样的:
img
项目说明地址:https://inst.eecs.berkeley.edu/~cs61a/sp18/proj/ants/#download-starter-files
由于原文档为英文,对于一些小伙伴来说可能比较吃力。所以我把它做了一个翻译,有些地方加上了一些说明。想要查看完整的代码 实现的同学可以点击【阅读原文】访问我的GitHub仓库。
这一次的项目难度不算高,主要讲解的是关于Python当中面向对象的部分,教我们学会设计类,以及使用类的继承。
核心概念
Colony:游戏发生的场景,由多个通路(tunnel)组合而成,每一个通道是由一个一个网格(place)组成的
img
Places:网格,每一个网格连接另外的网格组成通路,玩家可以在网格上放置一只蚂蚁,然而每个网格可以有多只蜜蜂
The hive:蜂巢,蜜蜂的老家,蜜蜂从hive中出来,来到colony中
Ants:蚂蚁,玩家在游戏当中用来操控对付蜜蜂的士兵。每一种蚂蚁在每一个回合可以采取一个特定的行动(action),生产蚂蚁需要消耗一定的食物(food)。最基础的两种蚂蚁是**HarvesterAnt**
(收割蚂蚁),它每个回合可以采集1点食物,和**ThrowerAnt**
(投掷蚂蚁),每个回合每个回合可以朝蜜蜂丢一片树叶
Bees:蜜蜂,游戏当中我们需要对付的敌人。每一个回合当中,如果没有蚂蚁拦路的话,蜜蜂会前进一步。否则会攻击拦路的蚂蚁,当有一只蜜蜂到达tunnel
末端时游戏结束
Queen Ant:蚁后,每局游戏只有一只蚁后。蚁后也有攻击能力,除此之外它还能鼓舞士气,提升其他蚂蚁的攻击力。如果蜜蜂击杀了蚁后,同样游戏结束
运行游戏
有两种运行游戏的方式,一种是基于文本命令行的形式,用于开发测试:
代码语言:javascript复制python3 ants.py
我们也可以通过游戏界面运行游戏,用于最终演示:
代码语言:javascript复制python3 gui.py -d easy --food 10
-d 控制难度 test/easy/medium/hard/insane
-w 地图是否有水
--food 开局食物数量
Phase 1 gameplay
在第一个阶段,实现基本的蚂蚁(HavesterAnt
和ThrowerAnt
)。
阶段结束时,可以运行最基础的游戏版本
Problem 0
阅读代码回答以下几个问题:
insect
类中的armor
属性的作用是?在游戏当中它会改变吗?如果会改变,触发条件是?Ant
类所有的属性有哪些?Ant
类的armor
属性是类属性还是实例属性?为什么Ant
部分子类的damage
属性是类属性还是实例属性,为什么Ant
和Bee
是从哪一个类中继承来的?Ant
和Bee
类的实例有什么共同点?- 在任意给定时间,一个
Place
可以出现多少insect
?
我们可以以命令行交互的形式回答这些问题(原问题是英文的),下载项目代码之后,输入以下命令即可:
代码语言:javascript复制python3 ok -q 00 -u
题目和选项都会以命令行文本的形式展示,通过输入选项的形式来答题。
img
老师为每道题都设置了测试样例,但是需要我们先通过问题才能使用这些样例。解锁测试样例和进行测试不会进行身份验证,即使我们不是伯克利的学生也能享用,非常非常人性化。
Problem 1
首先添加食物的开销,以及开发HarvesterAnt
。
目前生产蚂蚁不需要任何消耗,因此游戏完全没有难度。你会注意到Ant
这个类中的属性food_cost
设置成了0。在下列的子类当中覆盖这个属性,将它设置成正确的值。
Class | Food Cost | Armor |
---|---|---|
HarvesterAnt | 2 | 1 |
ThrowerAnt | 3 | 1 |
现在生产蚂蚁需要花费了,因此我们需要HarvesterAnt
来收集食物。开发HarvesterAnt
类,使得它每回合在行动时可以添加colony.food
。
在开始开发之前,先回答问题进行测试,测试通过可以解锁测试样例:
代码语言:javascript复制python3 ok -q 01 -u
当完成实现之后,进行测试:
代码语言:javascript复制python3 ok -q 01
答案
action
函数会在每个回合被调用,被调用时将colony.food
增加即可
class HarvesterAnt(Ant):
"""HarvesterAnt produces 1 additional food per turn for the colony."""
name = 'Harvester'
implemented = True
food_cost = 2
def action(self, colony):
"""Produce 1 additional food for the COLONY.
colony -- The AntColony, used to access game state information.
"""
# BEGIN Problem 1
"*** YOUR CODE HERE ***"
colony.food = 1
# END Problem 1
Problem 2
完善Place
类的构造函数,使得它能够追踪入口(entrance
)。
目前,一个place
仅仅会记录出口(exit
),我们希望place
也能记录它的入口。每一个place
只会有一个入口,有了入口之后,Ant
就可以知道面前有哪些蜜蜂了。
然而,只是简单地将一个entrance
传进构造函数是有问题的。因为我们在当前place
创建之前就需要这两个属性,会导致循环依赖。为了解决这个问题,我们采用如下方法:
- 新创建的
Place
的entrance
为None
- 如果
Place
拥有一个exit
,那么将exit
的入口设置成Place
提示:
在__init__
函数中,self
会绑定当前对象
如果觉得困惑,可以先画出两个Place
。在GUI当中,一个Place
的入口是它的右边的Place
,出口是它的左边
在开始编码之前,先回答问题,确保已经理解了
代码语言:javascript复制python3 ok -q 02 -u
完成之后,使用下列命令来测试:
代码语言:javascript复制python3 ok -q 02
答案
exit
是外界传入的,当exit
不为空时,将exit.entrance
设置成self
即当前Place
即可。
class Place(object):
"""A Place holds insects and has an exit to another Place."""
def __init__(self, name, exit=None):
"""Create a Place with the given NAME and EXIT.
name -- A string; the name of this Place.
exit -- The Place reached by exiting this Place (may be None).
"""
self.name = name
self.exit = exit
self.bees = [] # A list of Bees
self.ant = None # An Ant
self.entrance = None # A Place
# Phase 1: Add an entrance to the exit
# BEGIN Problem 2
"*** YOUR CODE HERE ***"
if self.exit is not None:
self.exit.entrance = self
# END Problem 2
Problem 3
下面我们实现ThrowerAnt
类,用来攻击蜜蜂。
首先,它必须知道它需要攻击哪一只蜜蜂。最初版本的代码nearest_bee
方法,只会攻击和它在同一个格子里的蜜蜂。你的任务是修改它,让ThrowerAnt
会对距离它最近的蜜蜂(蜂巢中的除外)使用throw_at
方法。
nearest_bee
方法随机返回距离它最近的格子当中的一只蜜蜂:
- 从当前
ThrowerAnt
在的Place
开始遍历 - 对于每一个
Place
,如果它上面有蜜蜂, 返回任意一只。如果没有,检查它前一个Place
(entrance
) - 如果没有蜜蜂可以攻击,返回
None
提示:
代码语言:javascript复制random_or_none`函数可以随机返回序列中一个元素,如果序列为空时返回`None
开始开发之前先回答问题,确保已经充分理解
代码语言:javascript复制python3 ok -q 03 -u
开发完成之后,进行测试:
代码语言:javascript复制python3 ok -q 03
测试通过之后,可以模拟以下效果:
代码语言:javascript复制python3 gui.py --food 10
效果如下:
img
答案
通过while
循环来遍历place
,直到place
上有蜜蜂,随机返回一只即可。
class ThrowerAnt(Ant):
"""ThrowerAnt throws a leaf each turn at the nearest Bee in its range."""
name = 'Thrower'
implemented = True
damage = 1
food_cost = 3
def nearest_bee(self, hive):
"""Return the nearest Bee in a Place that is not the HIVE, connected to
the ThrowerAnt's Place by following entrances.
This method returns None if there is no such Bee (or none in range).
"""
# BEGIN Problem 3 and 4
place = self.place
while place != hive:
bee = random_or_none(place.bees)
if bee is not None:
return bee
place = place.entrance
return None
# END Problem 3 and 4
Phase 2 Ants Attack
现在你已经开发好了两个最基本的蚂蚁类型,可以进行最基础的游戏了。
在这个阶段当中,你将会开发更多的拥有不同技能的蚂蚁类型。当你完成了Ant
子类的开发之后,你需要将其中implemented
属性改成True
才能在GUI中使用它。你可以在每完成一种新的蚂蚁之后进行游戏测试。
这个阶段开发完成之后,你可以尝试使用命令:python3 gui.py -d easy
当前的蚂蚁类型来对决一大波蜜蜂。你也可以使用-d normal, -d hard
或者-d insane
来尝试更高的难度。如果你觉得很难获胜,也许你需要开发更多类型的蚂蚁。
Problem 4
ThrowerAnt
是一个很好的进攻单位,但如果价格便宜一点就更好了。开发ThrowerAnt
的两个子类,它们拥有更低的造价,但在投掷距离上会有限制:
LongThrower
只能throw_at
距离大于等于5的蜜蜂。对于距离小于等于4的蜜蜂无能为力,当多只蜜蜂出现,也只会攻击距离满足条件的蜜蜂ShortThrower
只能throw_at
距离小于等于3的蜜蜂。
以上两种蜜蜂都不能攻击距离刚好是4的蜜蜂,只放一只攻击蜜蜂是无法获胜的
Class | Food Cost | Armor |
---|---|---|
ShortThrower | 2 | 1 |
LongThrower | 2 | 1 |
一个比较好的实现方式是,让它们继承ThrowerAnt
类的nearest_bee
方法。因为它们选择攻击目标的逻辑是一样的,除了ShortThrower
和LongThrower
拥有最大和最小距离的限制。
所以你需要修改ThrowerAnt
中的nearest_bee
方法来使用min_range
和max_range
属性,只返回在射程中的蜜蜂
原始ThrowerAnt
没有射程的限制,所以你需要为它添加对应的min_range
和max_range
属性,使得它不会影响ThrowerAnt
,接着给子类LongThrower
和ShortThrower
设置合适的范围和食物开销。
提示
float('inf')
返回代表无穷大的浮点数,和其他任何数字相比都要更大
别忘了将LongThrower
和ShortThower
的implemented
属性改成True
在你开始开发之前,先完成测试:
代码语言:javascript复制python3 ok -q 04 -u
完成之后,进行测试:
代码语言:javascript复制python3 ok -q 04
答案
意思是说我们修改父类ThrowerAnt
而不改子类,让父类拥有无限的射程保证父类的功能不变,又可以支持子类射程的要求。
给子类设置合适的射程,使得不需要重复实现nearest_bee
方法,只需要修改配置就可以实现功能
class ThrowerAnt(Ant):
"""ThrowerAnt throws a leaf each turn at the nearest Bee in its range."""
name = 'Thrower'
implemented = True
damage = 1
food_cost = 3
min_range = 0
max_range = float('inf')
def nearest_bee(self, hive):
"""Return the nearest Bee in a Place that is not the HIVE, connected to
the ThrowerAnt's Place by following entrances.
This method returns None if there is no such Bee (or none in range).
"""
# BEGIN Problem 3 and 4
place = self.place
dis = 0
while place != hive:
bee = random_or_none(place.bees)
if bee is not None and self.min_range <= dis <= self.max_range:
return bee
place = place.entrance
dis = 1
return None
# END Problem 3 and 4
class LongThrower(ThrowerAnt):
"""A ThrowerAnt that only throws leaves at Bees at least 5 places away."""
name = 'Long'
# BEGIN Problem 4
implemented = True # Change to True to view in the GUI
min_range = 5
max_range = float('inf')
food_cost = 2
# END Problem 4
class ShortThrower(ThrowerAnt):
"""A ThrowerAnt that only throws leaves at Bees at most 3 places away."""
name = 'Short'
# BEGIN Problem 4
implemented = True # Change to True to view in the GUI
min_range = 0
max_range = 3
food_cost = 2
# END Problem 4
Problem 5
实现FireAnt
类,FireAnt
类拥有一个特别的方法reduce_armor
:当FireAnt
的护甲减为0或更低时:它会攻击相同格子里的所有蜜蜂,将它们的护甲减去它的damage
(默认是3)。
Class | Food Cost | Armor |
---|---|---|
FireAnt | 5 | 1 |
提示:
攻击蜜蜂会导致蜜蜂死去被移出场地,如果一遍遍历一个list,一遍移除list中的内容,可能会导致出错。
所以Python 官方教程
中建议,如果你需要一遍遍历一遍修改一个序列(比如复制/删除选中的元素等),最好先将序列拷贝。你可以调用list
构造函数或者使用切片诸如a[:]
来完成拷贝
当你开发完成之后,将implemnted
设置成True
测试之前,先答题,确保理解正确
代码语言:javascript复制python3 ok -q 05 -u
开发之后,进行测试:
代码语言:javascript复制python3 ok -q 05
答案
护甲减到0以下就攻击同一格中所有的蜜蜂,由于攻击蜜蜂可能导致蜜蜂死亡,place.bees
发生变化。所以我们需要先拷贝序列,再对拷贝中的蜜蜂进行攻击。
class FireAnt(Ant):
"""FireAnt cooks any Bee in its Place when it expires."""
name = 'Fire'
damage = 3
# BEGIN Problem 5
food_cost = 5
implemented = True # Change to True to view in the GUI
# END Problem 5
def reduce_armor(self, amount):
"""Reduce armor by AMOUNT, and remove the FireAnt from its place if it
has no armor remaining. If the FireAnt dies, damage each of the bees in
the current place.
"""
# BEGIN Problem 5
"*** YOUR CODE HERE ***"
self.armor -= amount
if self.armor <= 0:
bees = self.place.bees[:]
for bee in bees:
bee.reduce_armor(self.damage)
self.place.remove_insect(self)
# END Problem 5
可以用已有的蚂蚁玩一两局游戏了,FireAnt
的伤害很高,灵活使用获胜并不难
python3 gui.py --food 10
Problem 6
实现HungryAnt
,它可以选择和它同一格的蜜蜂进行吞食。吞食完一只蜜蜂之后,需要经过3轮消化才能再次吞食。
Class | Food Cost | Armor |
---|---|---|
HungryAnt | 4 | 1 |
给HungryAnt
类添加一个time_to_digest
的类属性,它表示HungryAnt
在吞吃之后消化需要的回合数。同样,给HungryAnt
添加一个实例属性digesting
标记还需要消化的回合数(默认是0,因为初始时还没有吞吃)。
实现action
函数,检查是否在消化,如果是,减少digesting
值,否则随机选择同一格的蜜蜂进行吞吃(将蜜蜂的护甲置为0,重置digesting
)
编码之前先答题,确保理解正确
代码语言:javascript复制python3 ok -q 06 -u
实现之后,进行测试:
代码语言:javascript复制python3 ok -q 06
答案
注意类属性和实例属性的区别即可
代码语言:javascript复制class HungryAnt(Ant):
"""HungryAnt will take three turns to digest a Bee in its place.
While digesting, the HungryAnt can't eat another Bee.
"""
name = 'Hungry'
# BEGIN Problem 6
implemented = True # Change to True to view in the GUI
time_to_digest = 3
food_cost = 4
armor = 1
# END Problem 6
def __init__(self):
# BEGIN Problem 6
"*** YOUR CODE HERE ***"
self.digesting = 0
# END Problem 6
def eat_bee(self, bee):
# BEGIN Problem 6
"*** YOUR CODE HERE ***"
bee.armor = 0
bee.place.remove_insect(bee)
self.digesting = self.time_to_digest
# END Problem 6
def action(self, colony):
# BEGIN Problem 6
"*** YOUR CODE HERE ***"
if self.digesting > 0:
self.digesting -= 1
return
if len(self.place.bees) > 0:
bee = random.choice(self.place.bees)
self.eat_bee(bee)
# END Problem 6
Problem 7
开发忍者蚂蚁(NinjaAnt
),它会攻击所有经过的蜜蜂,并且不会被叮。
Class | Food Cost | Armor |
---|---|---|
NinjaAnt | 5 | 1 |
NinjaAnt`不会阻碍蜜蜂的前进,要实现这一点,需要在`Ant`类中加上一个新的类属性`blocks_path`,表示是否会阻碍蜜蜂前进,在`Ant`中设置成`True`。这样的话继承自`Ant`的子类默认都是`True`,在`NinjaAnt`中设置成`False
其次修改Bee
中的方法blocked
,在没有蚂蚁拦路或者拦路的蚂蚁blocks_path
为False
时返回False
。这样的话,蜜蜂将会从NinjaAnts
身边飞过。
最后,我们希望NinjaAnt
会攻击所有飞过的蜜蜂。在action
函数当中实现这个功能,让它能够攻击所有在同一格的蜜蜂。和FireAnt
一样,我们需要遍历可能发生改变的序列,所以需要先进行拷贝
开发前答题:
代码语言:javascript复制python3 ok -q 07 -u
开发后测试:
代码语言:javascript复制python3 ok -q 07
尝试只用HarvestAnt
和NinjaAnt
获胜。
答案
代码语言:javascript复制class Bee(Insect):
...
def blocked(self):
"""Return True if this Bee cannot advance to the next Place."""
# Phase 4: Special handling for NinjaAnt
# BEGIN Problem 7
return self.place.ant is not None and self.place.ant.blocks_path
# END Problem 7
class NinjaAnt(Ant):
"""NinjaAnt does not block the path and damages all bees in its place."""
name = 'Ninja'
damage = 1
# BEGIN Problem 7
implemented = True # Change to True to view in the GUI
blocks_path = False
food_cost = 5
armor = 1
# END Problem 7
def action(self, colony):
# BEGIN Problem 7
"*** YOUR CODE HERE ***"
bees = self.place.bees[:]
for bee in bees:
bee.reduce_armor(self.damage)
# END Problem 7
Phase 3: But They Also Protect
我们现在已经有了很多进攻手段,也需要开发一些防御机制。
在这个阶段,我们将会开发一些拥有防御功能的蚂蚁,比如增加护甲,或者是保护同伴。
Problem 8
我们将要开发WallAnt
为蚂蚁王国提供保护,它每个回合不会做任何事情只会阻挡蜜蜂,拥有很高的护甲,类似于植物大战僵尸里的坚果。
Class | Food Cost | Armor |
---|---|---|
WallAnt | 4 | 4 |
和之前的蚂蚁不同,我们没有提供类的初始代码。
你需要从零开始实现WallAnt
,将类中的name
属性设置成Wall
,implemented
设置成True
,这样图像才能正常显示。
开发前答题:
代码语言:javascript复制python3 ok -q 08 -u
开发后测试:
代码语言:javascript复制python3 ok -q 08
答案
实现WallAnt
继承自Ant
,设置对应的参数,并且调用父类的构造函数设置护甲(可以参考其他类)。
class WallAnt(Ant):
name = 'Wall'
implemented = True
food_cost = 4
armor = 4
def __init__(self):
super().__init__(self.armor)
Problem 9
我们的蚂蚁除了WallAnt
之外都太脆弱,所以我们希望开发BodyguardAnt
为蚂蚁们提供保护。
Class | Food Cost | Armor |
---|---|---|
BodyguardAnt | 4 | 2 |
BodyguardAnt
和普通的蚂蚁不同,因为它是一个容器。它可以容纳一只另外的蚂蚁,并且保护它,它们在同一个Place
里。当有蜜蜂攻击时,只有容器会受到攻击。
容器内的蚂蚁每回合同样可以执行action
,当容器被摧毁,被保护的蚂蚁依然留存。
每个BodyguardAnt
拥有一个实例属性ant
,它用来存储被保护的蚂蚁。初始时因为还没有保护蚂蚁,所以是None
。实现contain_ant
函数,它会传入一个ant
表示要被保护的ant
,需要将它赋给ant
属性。另外,实现BodyguardAnt
的action
方法,来执行被保护的ant
的action
。
另外,你需要做如下的改动,保证容器和被它容纳的蚂蚁一起占据同一个格子。每个格子最多两只蚂蚁,并且有一只一定是容器。
- 添加
Ant.container
类属性,表明某一个Ant
的子类是否是容器。对于Ant
的所有子类来说,除了BodyguardAnt
以外都必须是False
,除了BodyguardAnt
是True
。 - 实现
Ant.can_contain
方法,接收一个参数other
,当满足一下条件时返回True
: -
- 当前的
ant
是容器 - 当前的
ant
没有容纳蚂蚁 other
蚂蚁不是容器
- 当前的
- 修改
Place.add_insect
函数,使得它兼容容器的case,该函数接收一个参数insect
,表示要放置在Place
上的昆虫,考虑以下情况: -
- 当前格子上有一个
ant
,并且可以容纳传入add_insect
函数的insect
,那么将insect
容纳进ant
当中 - 如果
insect
是容器,可以容纳当前格子上的ant
,并且将Place.ant
设置成insect
- 如果不满足上述条件,抛出
AssertionError
异常
- 当前格子上有一个
编码前答题:
代码语言:javascript复制python3 ok -q 09 -u
开发后测试:
代码语言:javascript复制python3 ok -q 09
答案
代码语言:javascript复制class BodyguardAnt(Ant):
"""BodyguardAnt provides protection to other Ants."""
name = 'Bodyguard'
# BEGIN Problem 9
implemented = True # Change to True to view in the GUI
container = True
food_cost = 4
# END Problem 9
def __init__(self):
Ant.__init__(self, 2)
self.ant = None # The Ant hidden in this bodyguard
def contain_ant(self, ant):
# BEGIN Problem 9
"*** YOUR CODE HERE ***"
self.ant = ant
# END Problem 9
def action(self, colony):
# BEGIN Problem 9
"*** YOUR CODE HERE ***"
if self.ant is not None:
self.ant.action(colony)
# END Problem 9
class Place(object):
...
def add_insect(self, insect):
"""Add an INSECT to this Place.
There can be at most one Ant in a Place, unless exactly one of them is
a BodyguardAnt (Phase 6), in which case there can be two. If add_insect
tries to add more Ants than is allowed, an assertion error is raised.
There can be any number of Bees in a Place.
"""
if insect.is_ant:
if self.ant is None:
self.ant = insect
else:
# BEGIN Problem 9
# assert self.ant is None, 'Two ants in {0}'.format(self)
if self.ant.can_contain(insect):
self.ant.contain_ant(insect)
elif insect.can_contain(self.ant):
insect.contain_ant(self.ant)
self.ant = insect
else:
assert self.ant is None, 'Two ants in {0}'.format(self)
# END Problem 9
else:
self.bees.append(insect)
insect.place = self
Problem 10
BodyguardAnt
提供了很好的防御,但常言道:最好的防御是进攻。
TankAnt
是一个可以进攻的容器,除了提供保护之外,它每回合还能对和它相同格子的蜜蜂产生1点伤害
Class | Food Cost | Armor |
---|---|---|
TankAnt | 6 | 2 |
你只能修改TankAnt
类中的代码,如果你发现你还需要修改其他地方的代码,那么说明你之前的实现可能有点问题。想办法在不修改之前代码的前提下,完成TankAnt
的功能。
答题:
代码语言:javascript复制python3 ok -q 10 -u
开发后测试:
代码语言:javascript复制python3 ok -q 10
答案
代码语言:javascript复制class TankAnt(BodyguardAnt):
"""TankAnt provides both offensive and defensive capabilities."""
name = 'Tank'
damage = 1
# BEGIN Problem 10
implemented = True # Change to True to view in the GUI
container = True
food_cost = 6
# END Problem 10
def action(self, colony):
# BEGIN Problem 10
"*** YOUR CODE HERE ***"
for bee in self.place.bees[:]:
bee.reduce_armor(self.damage)
if self.ant is not None:
self.ant.action(colony)
# END Problem 10
Phase 4: Water and Might
在这个最终阶段,你需要开发一个新的格子类型和能够使用它的蚂蚁。
这些蚂蚁当中有最重要的一种:蚁后
Problem 11
让我们为蚂蚁王国当中添加水池,现在我们只有两种格子:Hive
和Place
. 为了让游戏更有趣,让我们开发新的类型Water
。
只有watersafe
的蚂蚁可以在水池中存活,为了判断昆虫是否有能力在水中生存,需要在Insect
类中添加一个类属性watersafe
,默认设置成False
。因为蜜蜂可以飞,所以蜜蜂的watersafe
是True
。
为Water
类开发add_insect
方法。首先调用Place.add_insect
方法来添加昆虫,这一步不需要考虑昆虫的生存能力。如果昆虫不是防水的,那么调用reduce_armor
方法,将昆虫的护甲减到0。不要复制粘贴代码,使用那些已经开发好的代码。
开发前答题:
代码语言:javascript复制python3 ok -q 11 -u
开发后测试:
代码语言:javascript复制python3 ok -q 11
当你开发完成之后,可以使用--water
参数来开启水池。
python3 gui.py --water
答案
在Water
类当中,我们可以调用Place
中的函数来完成功能,这样可以尽可能复用代码。
别忘了给对应的子类设置watersafe
的属性
class Water(Place):
"""Water is a place that can only hold 'watersafe' insects."""
def add_insect(self, insect):
"""Add INSECT if it is watersafe, otherwise reduce its armor to 0."""
# BEGIN Problem 11
"*** YOUR CODE HERE ***"
Place.add_insect(self, insect)
if not insect.watersafe:
insect.reduce_armor(insect.armor)
# END Problem 11
class Insect(object):
"""An Insect, the base class of Ant and Bee, has armor and a Place."""
is_ant = False
damage = 0
watersafe = False
Problem 12
现在没有蚂蚁可以在水中生存,所以我们要开发ScubaThrower
蚂蚁,它是ThrowerAnt
的子类,开销更大,但能够在水中生存,但和它的基类不同的是,ScubaThrower
蚂蚁在水中不会损失护甲(蜜蜂无法下水攻击)。
Class | Food Cost | Armor |
---|---|---|
ScubaThrower | 6 | 1 |
我们没有为你提供该类的初始代码,需要从头完成开发。将它类属性name
设置成Scuba
,implemented
设置成True
让GUI能够工作。
开发前答题:
代码语言:javascript复制python3 ok -q 12 -u
开发后测试:
代码语言:javascript复制python3 ok -q 12
答案
代码语言:javascript复制class ScubaThrower(ThrowerAnt):
name = 'Scuba'
armor = 1
food_cost = 6
watersafe = True
Problem 13
最后, 实现蚁后QueenAnt
类。蚁后是ScubaThrower
的子类,并且能够鼓舞士气。
蚁后每回合能够将它身后的蚂蚁的攻击力翻倍,注意:蚂蚁的攻击力只能被翻倍一次
Class | Food Cost | Armor |
---|---|---|
QueenAnt | 7 | 1 |
然而能力越大责任越大,蚁后需要被三种特殊的规则制约:
- 如果蚁后护甲减为0,蜜蜂直接获胜,你可以调用
bees_win
函数来结束游戏 - 只能有一个真的蚁后,之后生产的蚁后都是替身,替身会在它们第一个回合消亡(护甲减为0)。替身死去不会结束游戏
- 真的蚁后不能被移出游戏,尝试移出蚁后的操作不会生效。你需要修改
Place
中remove_insect
方法
一些提示:
- 所有的实例共享同样的类属性,怎么实现真假蚁后的区分?
- 你可以使用
place.exit
来遍历蚁后身后的格子,直到遇到None
为止 - 为了确保不会将同样的蚂蚁攻击力翻倍两次,你可以维护一个
list
存储已经翻倍过的蚂蚁 - 你可以使用
isinstance
函数来判断一个实例的类,比如:
>>> a = Foo()
>>> isinstance(a, Foo)
True
开发前答题:
代码语言:javascript复制python3 ok -q 13 -u
开发完成后测试:
代码语言:javascript复制python3 ok -q 13
答案
属性true_queen
是类属性,一旦发生修改,对于所有实例生效。所以当第一只queen
诞生的时候,将它设置为0,这样就可以做出区别。
在action
函数当中,我们根据是否是替身采取不同的行动即可。
在reduce_armor
函数中加入真女王死亡触发游戏结束的逻辑。
class QueenAnt(ScubaThrower): # You should change this line
# END Problem 13
"""The Queen of the colony. The game is over if a bee enters her place."""
name = 'Queen'
# BEGIN Problem 13
implemented = True # Change to True to view in the GUI
food_cost = 7
armor = 1
true_queen = 1
# END Problem 13
def __init__(self):
# BEGIN Problem 13
"*** YOUR CODE HERE ***"
self.doubled = set()
self.is_true_queen = QueenAnt.true_queen > 0
QueenAnt.true_queen = max(0, QueenAnt.true_queen - 1)
# END Problem 13
def action(self, colony):
"""A queen ant throws a leaf, but also doubles the damage of ants
in her tunnel.
Impostor queens do only one thing: reduce their own armor to 0.
"""
# BEGIN Problem 13
"*** YOUR CODE HERE ***"
if not self.is_true_queen:
self.reduce_armor(self.armor)
return
def double(ant):
if ant is None:
return
if ant not in self.doubled:
ant.damage *= 2
self.doubled.add(ant)
if ant.container:
double(ant.ant)
place = self.place.exit
while place is not None:
ant = place.ant
double(ant)
place = place.exit
ScubaThrower.action(self, colony)
# END Problem 13
def reduce_armor(self, amount):
"""Reduce armor by AMOUNT, and if the True QueenAnt has no armor
remaining, signal the end of the game.
"""
# BEGIN Problem 13
"*** YOUR CODE HERE ***"
self.armor -= amount
if self.armor <= 0:
self.place.remove_insect(self)
if self.is_true_queen:
bees_win()
# END Problem 13
在Place
中加入不能移出女王的逻辑,即判断要移出的insect
类型必须是QueenAnt
且is_true_queen
属性为True
,如果成立,则直接return
。
def remove_insect(self, insect):
"""Remove an INSECT from this Place.
A target Ant may either be directly in the Place, or be contained by a
container Ant at this place. The true QueenAnt may not be removed. If
remove_insect tries to remove an Ant that is not anywhere in this
Place, an AssertionError is raised.
A Bee is just removed from the list of Bees.
"""
# print(insect)
if insect.is_ant:
# Special handling for QueenAnt
# BEGIN Problem 13
"*** YOUR CODE HERE ***"
# END Problem 13
# Special handling for BodyguardAnt
if isinstance(insect, QueenAnt) and insect.is_true_queen:
return
if self.ant is insect:
if hasattr(self.ant, 'container') and self.ant.container:
self.ant = self.ant.ant
else:
self.ant = None
else:
if hasattr(self.ant, 'container') and self.ant.container and self.ant.ant is insect:
self.ant.ant = None
else:
assert False, '{0} is not in {1}'.format(insect, self)
else:
self.bees.remove(insect)
insect.place = None
Extra Credit
附加题,实现最后两个Thrower
蚂蚁,它们不会产生伤害,但是会有特殊的效果,会给被投中的蜜蜂的action
加上debuff。
并且这两种蚂蚁产生的debuff持续的时间不同,当持续时间结束之后,蜜蜂的action
恢复。持续时间按照.action(colony)
调用的次数计算。
我们将要实现下面两种蚂蚁,它们都是ThrowerAnt
的子类:
- SlowThrower 产生3回合的缓慢效果
- StunThrower 产生1回合的停止效果
Class | Food Cost | Armor |
---|---|---|
SlowThrower | 4 | 1 |
StunThrower | 6 | 1 |
想要实现上面两种蚂蚁,需要先设置它们的类属性并且实现以下三个函数:
make_slow
输入是一个action
函数,返回一个新的action
函数,并且当colony.time
是偶数时执行原本的action
,否则轮空make_stun
同样接收一个action
返回新的action
,它的效果是轮空apply_effect
接收三个参数,第一个参数是debuff(make_slow
或make_stun
),一个Bee
,和一个duation
即debuff持续时间。它会将debuff作用在bee.action
上,持续duation
个回合。之后恢复正常
开发前答题和测试:
代码语言:javascript复制python3 ok -q EC -u
python3 ok -q EC
答案
这题有一定难度,主要难点在于apply_effect
函数只会执行一次,bee
一直调用的是action
函数。所以我们得在apply_effect
当中对bee.action
重新赋值。比较容易想到,我们可以使用高阶函数,这样可以在内部对duration
进行修改,从而判断什么时候需要恢复。
def make_slow(action):
"""Return a new action method that calls ACTION every other turn.
action -- An action method of some Bee
"""
# BEGIN Problem EC
"*** YOUR CODE HERE ***"
def new_action(colony):
if colony.time %2 == 0:
action(colony)
return new_action
# END Problem EC
def make_stun(action):
"""Return a new action method that does nothing.
action -- An action method of some Bee
"""
# BEGIN Problem EC
"*** YOUR CODE HERE ***"
def new_action(colony):
pass
return new_action
# END Problem EC
def apply_effect(effect, bee, duration):
"""Apply a status effect to a BEE that lasts for DURATION turns."""
# BEGIN Problem EC
"*** YOUR CODE HERE ***"
origin_action = bee.action
def func(colony):
nonlocal duration
if duration > 0:
duration -= 1
return effect(origin_action)(colony)
else:
return origin_action(colony)
bee.action = func
# END Problem EC
当所有功能开发完成之后,进行游戏,效果是这样的:
这次的project,内容非常丰富,难度适中,对于新人来说非常友好,很适合入门练习。
当看到自己开发的功能能够在页面当中运行,还可以亲自玩耍一下的时候,相信是非常有成就感的。非常推荐大家亲自尝试一下。
好了,关于这个project就聊到这里,感谢大家的阅读。
喜欢本文的话不要忘记三连~