日拱一卒,伯克利CS61A,手把手开发植物大战僵尸小游戏

2022-09-21 12:40:37 浏览数 (1)

作者 | 梁唐

出品 | 公众号: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

在第一个阶段,实现基本的蚂蚁(HavesterAntThrowerAnt)。

阶段结束时,可以运行最基础的游戏版本

Problem 0

阅读代码回答以下几个问题:

  1. insect类中的armor属性的作用是?在游戏当中它会改变吗?如果会改变,触发条件是?
  2. Ant类所有的属性有哪些?
  3. Ant类的armor属性是类属性还是实例属性?为什么
  4. Ant部分子类的damage属性是类属性还是实例属性,为什么
  5. AntBee是从哪一个类中继承来的?
  6. AntBee类的实例有什么共同点?
  7. 在任意给定时间,一个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增加即可

代码语言:javascript复制
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创建之前就需要这两个属性,会导致循环依赖。为了解决这个问题,我们采用如下方法:

  • 新创建的PlaceentranceNone
  • 如果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即可。

代码语言:javascript复制
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,如果它上面有蜜蜂, 返回任意一只。如果没有,检查它前一个Placeentrance
  • 如果没有蜜蜂可以攻击,返回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上有蜜蜂,随机返回一只即可。

代码语言:javascript复制
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方法。因为它们选择攻击目标的逻辑是一样的,除了ShortThrowerLongThrower拥有最大和最小距离的限制。

所以你需要修改ThrowerAnt中的nearest_bee方法来使用min_rangemax_range属性,只返回在射程中的蜜蜂

原始ThrowerAnt没有射程的限制,所以你需要为它添加对应的min_rangemax_range属性,使得它不会影响ThrowerAnt,接着给子类LongThrowerShortThrower设置合适的范围和食物开销。

提示

float('inf')返回代表无穷大的浮点数,和其他任何数字相比都要更大

别忘了将LongThrowerShortThowerimplemented属性改成True

在你开始开发之前,先完成测试:

代码语言:javascript复制
python3 ok -q 04 -u

完成之后,进行测试:

代码语言:javascript复制
python3 ok -q 04
答案

意思是说我们修改父类ThrowerAnt而不改子类,让父类拥有无限的射程保证父类的功能不变,又可以支持子类射程的要求。

给子类设置合适的射程,使得不需要重复实现nearest_bee方法,只需要修改配置就可以实现功能

代码语言:javascript复制
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发生变化。所以我们需要先拷贝序列,再对拷贝中的蜜蜂进行攻击。

代码语言:javascript复制
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的伤害很高,灵活使用获胜并不难

代码语言:javascript复制
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

代码语言:javascript复制
NinjaAnt`不会阻碍蜜蜂的前进,要实现这一点,需要在`Ant`类中加上一个新的类属性`blocks_path`,表示是否会阻碍蜜蜂前进,在`Ant`中设置成`True`。这样的话继承自`Ant`的子类默认都是`True`,在`NinjaAnt`中设置成`False

其次修改Bee中的方法blocked,在没有蚂蚁拦路或者拦路的蚂蚁blocks_pathFalse时返回False。这样的话,蜜蜂将会从NinjaAnts身边飞过。

最后,我们希望NinjaAnt会攻击所有飞过的蜜蜂。在action函数当中实现这个功能,让它能够攻击所有在同一格的蜜蜂。和FireAnt一样,我们需要遍历可能发生改变的序列,所以需要先进行拷贝

开发前答题:

代码语言:javascript复制
python3 ok -q 07 -u

开发后测试:

代码语言:javascript复制
python3 ok -q 07

尝试只用HarvestAntNinjaAnt获胜。

答案
代码语言: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属性设置成Wallimplemented设置成True,这样图像才能正常显示。

开发前答题:

代码语言:javascript复制
python3 ok -q 08 -u

开发后测试:

代码语言:javascript复制
python3 ok -q 08
答案

实现WallAnt继承自Ant,设置对应的参数,并且调用父类的构造函数设置护甲(可以参考其他类)。

代码语言:javascript复制
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属性。另外,实现BodyguardAntaction方法,来执行被保护的antaction

另外,你需要做如下的改动,保证容器和被它容纳的蚂蚁一起占据同一个格子。每个格子最多两只蚂蚁,并且有一只一定是容器。

  1. 添加Ant.container类属性,表明某一个Ant的子类是否是容器。对于Ant的所有子类来说,除了BodyguardAnt以外都必须是False,除了BodyguardAntTrue
  2. 实现Ant.can_contain方法,接收一个参数other,当满足一下条件时返回True
    1. 当前的ant是容器
    2. 当前的ant没有容纳蚂蚁
    3. other蚂蚁不是容器
  3. 修改Place.add_insect函数,使得它兼容容器的case,该函数接收一个参数insect,表示要放置在Place上的昆虫,考虑以下情况:
    1. 当前格子上有一个ant,并且可以容纳传入add_insect函数的insect,那么将insect容纳进ant当中
    2. 如果insect是容器,可以容纳当前格子上的ant,并且将Place.ant设置成insect
    3. 如果不满足上述条件,抛出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

让我们为蚂蚁王国当中添加水池,现在我们只有两种格子:HivePlace. 为了让游戏更有趣,让我们开发新的类型Water

只有watersafe的蚂蚁可以在水池中存活,为了判断昆虫是否有能力在水中生存,需要在Insect类中添加一个类属性watersafe,默认设置成False。因为蜜蜂可以飞,所以蜜蜂的watersafeTrue

Water类开发add_insect方法。首先调用Place.add_insect方法来添加昆虫,这一步不需要考虑昆虫的生存能力。如果昆虫不是防水的,那么调用reduce_armor方法,将昆虫的护甲减到0。不要复制粘贴代码,使用那些已经开发好的代码。

开发前答题:

代码语言:javascript复制
python3 ok -q 11 -u

开发后测试:

代码语言:javascript复制
python3 ok -q 11

当你开发完成之后,可以使用--water参数来开启水池。

代码语言:javascript复制
python3 gui.py --water
答案

Water类当中,我们可以调用Place中的函数来完成功能,这样可以尽可能复用代码。

别忘了给对应的子类设置watersafe的属性

代码语言:javascript复制
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设置成Scubaimplemented设置成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

然而能力越大责任越大,蚁后需要被三种特殊的规则制约:

  1. 如果蚁后护甲减为0,蜜蜂直接获胜,你可以调用bees_win函数来结束游戏
  2. 只能有一个真的蚁后,之后生产的蚁后都是替身,替身会在它们第一个回合消亡(护甲减为0)。替身死去不会结束游戏
  3. 真的蚁后不能被移出游戏,尝试移出蚁后的操作不会生效。你需要修改Placeremove_insect方法

一些提示:

  • 所有的实例共享同样的类属性,怎么实现真假蚁后的区分?
  • 你可以使用place.exit来遍历蚁后身后的格子,直到遇到None为止
  • 为了确保不会将同样的蚂蚁攻击力翻倍两次,你可以维护一个list存储已经翻倍过的蚂蚁
  • 你可以使用isinstance函数来判断一个实例的类,比如:
代码语言:javascript复制
>>> a = Foo()
>>> isinstance(a, Foo)
True

开发前答题:

代码语言:javascript复制
python3 ok -q 13 -u

开发完成后测试:

代码语言:javascript复制
python3 ok -q 13
答案

属性true_queen是类属性,一旦发生修改,对于所有实例生效。所以当第一只queen诞生的时候,将它设置为0,这样就可以做出区别。

action函数当中,我们根据是否是替身采取不同的行动即可。

reduce_armor函数中加入真女王死亡触发游戏结束的逻辑。

代码语言:javascript复制
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类型必须是QueenAntis_true_queen属性为True,如果成立,则直接return

代码语言:javascript复制
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

想要实现上面两种蚂蚁,需要先设置它们的类属性并且实现以下三个函数:

  1. make_slow输入是一个action函数,返回一个新的action函数,并且当colony.time是偶数时执行原本的action,否则轮空
  2. make_stun同样接收一个action返回新的action,它的效果是轮空
  3. apply_effect接收三个参数,第一个参数是debuff(make_slowmake_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进行修改,从而判断什么时候需要恢复。

代码语言:javascript复制
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就聊到这里,感谢大家的阅读。

喜欢本文的话不要忘记三连~

0 人点赞