CS61A SP24 Project3: Ants Vs. SomeBees

游戏介绍

蚂蚁大战蜜蜂(?)有一系列回合构成,每个回合都可能有新的蜜蜂进入蚂蚁领地,然后新的蚂蚁会被放置以保卫领地,最后所有昆虫各自采取行动。蜜蜂会尝试向通道终点移动或者攻击挡路的蚂蚁,蚂蚁根据种类的不同采取不同的行动(比如收集更多食物或者向蜜蜂扔叶子)。当有蜜蜂到达了通道终点(蜜蜂获胜),蜜蜂摧毁了一个QueenAnt(如果存在)(蜜蜂获胜)或者所有蜜蜂部队都被击败(蚂蚁获胜)时游戏结束。

核心概念

领地(The Colony):领地是游戏发生的地点,由一些链在一起的Place形成的供蜜蜂通过的通道组成。领地也有一些食物,用以消耗来在通道中部署蚂蚁。

地点(Places):一个地点和其他地点连在一起形成通道,玩家可以在每个地点放置一个蚂蚁,然而一个地点可以存在多个蜜蜂。

蜂巢(The Hive):产生蜜蜂的场所,蜜蜂离开蜂巢进入蚂蚁领地。

蚂蚁(Ants):玩家从屏幕顶端的可用蚂蚁类型中选择一个蚂蚁部署到领地,每种蚂蚁有不同的行为并且需要消耗不同数量的领地食物来放置。两种最基本的蚂蚁类型是:HarvesterAnt(每回合增加一食物)和ThrowerAnt(每回合向一个蜜蜂投掷一个叶子)。

蜜蜂(Bees):每回合,如果蜜蜂前面没有挡路的蚂蚁就前进到通道的下一place,或者攻击挡路的蚂蚁,至少有一个蜜蜂到达隧道终点时蜜蜂获胜。除了橘色蜜蜂,还有攻击伤害翻倍的黄色胡峰和很难击败的绿色boss蜜蜂。

重要的类

上述概念每一个都对应了封装了这个概念的逻辑的类,本游戏中设计的主要的类有:

游戏状态(GameState):表示领地和游戏中的一些状态信息,包括有多少可用的食物,游戏进行了多长时间,AntHomeBase在哪里以及游戏中所有的Places。

场所(Place):表示存在昆虫的一块place。一块场所最多存在一只蚂蚁,但可以存在很多蜜蜂。Place对象有一个exit和entrance分别通向左右两边的place,蜜蜂通过向place的exit移动来通过通道。

蜂巢(Hive):表示蜜蜂出发点(位于通道的右端)。

蚂蚁基地(AntHomeBase):表示蚂蚁保卫的地方(位于通道左端)。如果蜜蜂到达这里,蜜蜂获胜。

昆虫(Insect):Ant类和Bee类的基类,每个昆虫都有health属性表示其当前血量和place属性表示当前其所在位置。每回合,游戏中每个活跃的昆虫都会行动。

蚂蚁(Ant):表示蚂蚁。每个Ant子类都有能够区别其他蚂蚁类型的特别的属性或者一个特殊行为,比如一个HarvesterAnt从领地获取食物,一个ThrowerAnt攻击蜜蜂。每种蚂蚁类型也有food_cost属性,表示部署一个单位的该类型蚂蚁消耗的食物量。

蜜蜂(Bee):表示蜜蜂。每回合,一只蜜蜂或者移动到其当前所在place的exit(如果不被蚂蚁阻挡),或者攻击占据同一place的蚂蚁。

游戏布局

(图片来自CS61A)

 Phase1: Basic gameplay

第一阶段实现基础游戏和两种基础蚂蚁(HarvesterAnt和ThrowerAnt)

Problem1

基类Ant的food_cost属性默认为0,将Ant类的子类HarvesterAnt和ThrowerAnt的food_cost属性分别设为2和3,并为HarvesterAnt类添加action方法用于每回合产生1食物。

Problem2

每个Place类的实例都有exit属性和entrance属性,在__init__()方法中实现以下逻辑:新创建的Place实例的entrance为None,如果该实例exit属性不为None,那么其exit的entrance属性是新创建的这个Place实例。通过这种方式来实现同一个tunnel中的Place的相邻关系(蜜蜂会从entrance进入place,从exit离开place)。

Problem3

实现ThrowerAnt类的nearest_bee方法,该方法会按照以下逻辑寻找最近的蜜蜂作为攻击目标:从TrowerAnt所在的place开始,对于当前place,如果有蜜蜂,就返回其中的随机一个蜜蜂;如果没有蜜蜂且有相邻的下一个place,那么检查下一个place;如果找不到可以攻击的蜜蜂,那么返回None。

注意在蜂巢的蜜蜂不可被选为攻击目标(根据place实例的is_hive属性判断),因此在判定条件中要考虑这一条件。

Phase2: More Ants

实现几个不同的蚂蚁攻击策略。

Problem4

实现两种ThrowerAnt的子类LongThrower和ShortThrower,它们有更少的食物消耗和不同的投掷距离限制:LongThrower的投掷距离至少为5,ShortThrower的投掷距离最多为3。

为ThrowerAnt类添加lower_bound和upper_bound属性,利用nearest_bee方法实现在[lower_bound, upper_bound]闭区间选择投掷目标。在LongThrower和ShortThrower子类中设置不同的lower_bound和upper_bound,它们都继承ThrowerAnt类的nearest_bee方法,实现了子类不同限制下的选择投掷目标方法。

这种方法实现了对于与基类的操作逻辑一致但限制条件不同的子类,只需要改变限制条件的参数,不需要改变基类的方法。

Problem5

实现FireAnt类的reduce_health方法,受到攻击后会对同一place内的所有蜜蜂造成同等伤害,如果死亡,那么会对同一place内的所有蜜蜂造成damage属性的伤害。

父类Ant的reduce_health方法会对Ant实例减少参数amount的血量,如果血量小于等于0,那么会从place上删除该Ant实例,蜜蜂Bee类的reduce_health方法同理。因此调用FireAnt的reduce_health方法时,首先要根据收到的伤害进行反伤(即传入的参数amount),对于当前place的所有蜜蜂的list(bees属性),依次调用reduce_health(amount),如果当前蜜蜂bee的血量小于等于0,reduce_health方法会将其从place中删除。需要注意的是,如果在使用for语句遍历一个可变序列(mutable sequence)时对其进行了修改,那么可能会导致跳过元素等错误,因此需要使用for语句时表达式处使用序列的副本(如列表可以用list的slice、copy()方法或list函数,都会创建一个不指向原列表的副本)。然后,如果FireAnt的血量小于等于伤害amount,表示受到伤害后该FireAnt实例会死亡并触发亡语伤害damage,于是要对当前place的bees分别造成self.damage伤害,实现方法同上一步。最后,对当前FireAnt实例调用super().reduce_health(amount),即调用了父类Ant类中的reduce_health方法,该方法会对FireAnt实例造成amount的伤害并在其生命值小于等于0的情况下从所在place上删除该FireAnt。

Problem6

实现WallAnt类,血量较厚。

Problem7

实现HungryAnt,类似PvZ中的食人花,会选择位于同一place的一个蜜蜂吃掉(血量直接降为0),并进去回合数为chewing_turns的冷却期。

用类属性chewing_turns来记录该HungryAnt的统一冷却时间,用实例属性turns_to_chew来记录某一具体HungryAnt实例的当前冷却时间(用__init__方法初始化为0,每当吃掉一个蜜蜂后赋值为chewing_turns,并每回合减少1)。

HungryAnt的类方法action用于实现该类蚂蚁每回合的行为:首先判断某一HungryAnt的冷却时间turns_to_chew是否为0,如果不为0,那么不能进行其他行为,而是减1;如果为0,那么如果所在place有蜜蜂存在,使用random_bee方法选择一个蜜蜂吃掉(使用reduce_health方法,参数为该目标蜜蜂的血量),并将turns_to_chew设置为冷却时间chewing_turns。

Problem8

实现ContainerAnt和BodyguardAnt,其中ContainerAnt是Ant类的子类,BodyguardAnt是ContainerAnt的子类。ContainerAnt类似于PvZ中的南瓜头,可以实现保护place中的非Container蚂蚁,当ContainerAnt存在时,优先受到蜜蜂的攻击,当ContainerAnt死亡消失后,被它contain的蚂蚁才会受到伤害。

为了实现ContainerAnt和其他蚂蚁存在于同一place中,增加了Ant类中add_to方法的功能,当想添加蚂蚁的place中有蚂蚁存在时,如果已存在的蚂蚁是ContainerAnt且要添加的蚂蚁非ContainerAnt或者已存在的蚂蚁非ContainerAnt而要添加的蚂蚁是ContainerAnt,那么可以添加,将place.ant属性设为ContainerAnt,将ContainerAnt的ant_contained属性设为非ContainerAnt(实现过程会使用can_contain方法)。

ContainerAnt类中继承了can_contain方法用于判断self是否能contain其他的蚂蚁,如果self非ContainerAnt类,直接返回False;如果self是ContainerAnt类的实例(is_container属性为True),参数other为非ContainerAnt类的实例(避免无限套娃),那么返回True,否则返回False。

ContainerAnt类的action方法在其contain了某一蚂蚁时调用该被contain的蚂蚁的action。

BodyguardAnt类的action方法与其父类ContainerAnt相同,只有__init__方法用于设定血量。

Problem9

实现TankAnt类,它的基类是ContainerAnt。

TankAnt的action方法首先执行ContainerAnt的action方法,即其contain的蚂蚁执行自己的action方法,然后会对所有与该TankAnt实例位于同一place的蜜蜂造成同样的damage。需要注意的是,这里对self.place.bees列表的访问与FireAnt中类似,需要使用bees列表的副本(此处使用了[:])以避免遍历跳过某元素等错误。

Phase3: Water and Might

增加新的场地Water以及新的昆虫

Problem10

实现Place类的子类Water,增加昆虫的属性is_waterproof,只有is_waterproof属性为True的昆虫可以在Water场地上存活,否则会直接将血量减为0。

Problem11

实现ScubaThrower类,其基类为ThrowerAnt,有更高的食物消耗,并且is_waterproof属性为True。

Problem12

实现QueenAnt类,其基类为ThrowerAnt。除了执行ThrowerAnt类的action外,每回合QueenAnt实例会激励它后面的所有蚂蚁,使其damage属性翻倍(仅限加倍一次)。因此,QueenAnt类的action方法中会使用exit依次遍历其后面的场地,如果有蚂蚁存在,就对其调用double方法(注意如果有ContainerAnt存在,对其contain的蚂蚁也要调用double方法)。由于QueenAnt实例死亡会导致蚂蚁失败,因此在reduce_health方法中调用基类的reduce_health后还要在health小于等于0的情况下调用ants_lose函数。

Optional Problem1

实现NinjaAnt类,它不会被蚂蚁攻击到,并且对和它在同一place的蚂蚁造成damage属性的伤害。

为了实现这个功能,在Ant类中添加了blocks_path属性,default为True(NinjaAnt类的该属性为False,因此不会阻挡蜜蜂),表示蚂蚁会阻挡在蜜蜂的前进路线上,如果蜜蜂被阻挡了,就会攻击当前place中的蚂蚁。Bee类中的blocked方法用于判断当前所在位置是否被阻挡,只有当前place有蚂蚁存在且其blocks_path属性为True时该方法返回True,此时蜜蜂会调用sting方法进行攻击;否则blocked方法返回False。

注意在实现NinjaAnt类的action方法时,会对NinjaAnt实例所在的place的bees属性列表中所有蜜蜂造成self.damage的伤害,要使用副本进行遍历。

Optional Problem2

实现LaserAnt类,它的基类为ThrowerAnt,并具有贯穿伤害(会对同一tunnel的不包括自己在内的后续昆虫造成伤害),其伤害会随着距离和造成伤害的次数递减。

实例属性insects_shot记录造成伤害的次数,初始为0;insects_in_front方法用于寻找LaserAnt实例的伤害目标,返回值是一个字典,其中的键值对的key为后续的昆虫(包括蚂蚁和蜜蜂),value为key指向的昆虫与LaserAnt实例的距离distance(若在同一place距离为0);calculate_damage方法用于计算对当前目标造成的伤害,传入参数为distance,damage初始为2,每向后1个place减少0.25,每造成一次伤害减少0.0625,最小为0。

调用action方法时,首先调用insects_in_front方法获得伤害目标的字典insects_and_distances,然后对该字典中的每个insect依次造成伤害damage(damage由调用calculate_damage方法获得),并在每次造成伤害后将insects_shot增1。

最终代码

"""CS 61A presents Ants Vs. SomeBees."""

import random
from ucb import main, interact, trace
from collections import OrderedDict

################
# Core Classes #
################


class Place:
    """A Place holds insects and has an exit to another Place."""
    is_hive = False

    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 exit:
            exit.entrance = self
        # END Problem 2

    def add_insect(self, insect):
        """Asks the insect to add itself to this place. This method exists so
        that it can be overridden in subclasses.
        """
        insect.add_to(self)

    def remove_insect(self, insect):
        """Asks the insect to remove itself from this place. This method exists so
        that it can be overridden in subclasses.
        """
        insect.remove_from(self)

    def __str__(self):
        return self.name


class Insect:
    """An Insect, the base class of Ant and Bee, has health and a Place."""

    next_id = 0  # Every insect gets a unique id number
    damage = 0
    # ADD CLASS ATTRIBUTES HERE
    is_waterproof = False

    def __init__(self, health, place=None):
        """Create an Insect with a health amount and a starting PLACE."""
        self.health = health
        self.place = place

        # assign a unique ID to every insect
        self.id = Insect.next_id
        Insect.next_id += 1

    def reduce_health(self, amount):
        """Reduce health by AMOUNT, and remove the insect from its place if it
        has no health remaining.

        >>> test_insect = Insect(5)
        >>> test_insect.reduce_health(2)
        >>> test_insect.health
        3
        """
        self.health -= amount
        if self.health <= 0:
            self.zero_health_callback()
            self.place.remove_insect(self)

    def action(self, gamestate):
        """The action performed each turn."""

    def zero_health_callback(self):
        """Called when health reaches 0 or below."""

    def add_to(self, place):
        self.place = place

    def remove_from(self, place):
        self.place = None

    def __repr__(self):
        cname = type(self).__name__
        return '{0}({1}, {2})'.format(cname, self.health, self.place)


class Ant(Insect):
    """An Ant occupies a place and does work for the colony."""

    implemented = False  # Only implemented Ant classes should be instantiated
    food_cost = 0
    is_container = False
    # ADD CLASS ATTRIBUTES HERE
    is_doubled = False
    blocks_path = True

    def __init__(self, health=1):
        super().__init__(health)

    def can_contain(self, other):
        return False

    def store_ant(self, other):
        assert False, "{0} cannot contain an ant".format(self)

    def remove_ant(self, other):
        assert False, "{0} cannot contain an ant".format(self)

    def add_to(self, place):
        if place.ant is None:
            place.ant = self
        else:
            # BEGIN Problem 8b
            if place.ant.can_contain(self):
                place.ant.store_ant(self)
            elif self.can_contain(place.ant):
                self.store_ant(place.ant)
                place.ant = self
            else:
                assert place.ant is None, 'Too many ants in {0}'.format(place)
            # END Problem 8b
        Insect.add_to(self, place)

    def remove_from(self, place):
        if place.ant is self:
            place.ant = None
        elif place.ant is None:
            assert False, '{0} is not in {1}'.format(self, place)
        else:
            place.ant.remove_ant(self)
        Insect.remove_from(self, place)

    def double(self):
        """Double this ants's damage, if it has not already been doubled."""
        # BEGIN Problem 12
        "*** YOUR CODE HERE ***"
        if not self.is_doubled:
            self.damage *= 2
            self.is_doubled = True
        # END Problem 12


class HarvesterAnt(Ant):
    """HarvesterAnt produces 1 additional food per turn for the colony."""

    name = 'Harvester'
    implemented = True
    # OVERRIDE CLASS ATTRIBUTES HERE
    food_cost = 2

    def action(self, gamestate):
        """Produce 1 additional food for the colony.

        gamestate -- The GameState, used to access game state information.
        """
        # BEGIN Problem 1
        "*** YOUR CODE HERE ***"
        gamestate.food += 1
        # END Problem 1


class ThrowerAnt(Ant):
    """ThrowerAnt throws a leaf each turn at the nearest Bee in its range."""

    name = 'Thrower'
    implemented = True
    damage = 1
    # ADD/OVERRIDE CLASS ATTRIBUTES HERE
    lower_bound = 0
    upper_bound = float('inf')
    food_cost = 3

    def nearest_bee(self):
        """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
        count = 0
        nearest = self.place    #Start from the current Place of the ThrowerAnt
        while count < self.lower_bound:
            if nearest.entrance:
                nearest = nearest.entrance
                count += 1
            else:
                return None
        while not nearest.bees:
            if nearest.entrance:
                nearest = nearest.entrance
                count += 1
            else:
                return None
        if count > self.upper_bound:
            return None
        if not nearest.is_hive:
            return random_bee(nearest.bees)
        else:
            return None
        # END Problem 3 and 4

    def throw_at(self, target):
        """Throw a leaf at the target Bee, reducing its health."""
        if target is not None:
            target.reduce_health(self.damage)

    def action(self, gamestate):
        """Throw a leaf at the nearest Bee in range."""
        self.throw_at(self.nearest_bee())


def random_bee(bees):
    """Return a random bee from a list of bees, or return None if bees is empty."""
    assert isinstance(bees, list), \
        "random_bee's argument should be a list but was a %s" % type(bees).__name__
    if bees:
        return random.choice(bees)

##############
# Extensions #
##############


class ShortThrower(ThrowerAnt):
    """A ThrowerAnt that only throws leaves at Bees at most 3 places away."""

    name = 'Short'
    food_cost = 2
    # OVERRIDE CLASS ATTRIBUTES HERE
    lower_bound = 0
    upper_bound = 3
    # BEGIN Problem 4
    implemented = True   # Change to True to view in the GUI
    # END Problem 4


class LongThrower(ThrowerAnt):
    """A ThrowerAnt that only throws leaves at Bees at least 5 places away."""

    name = 'Long'
    food_cost = 2
    # OVERRIDE CLASS ATTRIBUTES HERE
    lower_bound = 5
    upper_bound = float('inf')
    # BEGIN Problem 4
    implemented = True   # Change to True to view in the GUI
    # END Problem 4


class FireAnt(Ant):
    """FireAnt cooks any Bee in its Place when it expires."""

    name = 'Fire'
    damage = 3
    food_cost = 5
    # OVERRIDE CLASS ATTRIBUTES HERE
    # BEGIN Problem 5
    implemented = True # Change to True to view in the GUI
    # END Problem 5

    def __init__(self, health=3):
        """Create an Ant with a HEALTH quantity."""
        super().__init__(health)

    def reduce_health(self, amount):
        """Reduce health by AMOUNT, and remove the FireAnt from its place if it
        has no health remaining.

        Make sure to reduce the health of each bee in the current place, and apply
        the additional damage if the fire ant dies.
        """
        # BEGIN Problem 5
        "*** YOUR CODE HERE ***"
        if self.place.bees:
            for bee in list(self.place.bees):
                bee.reduce_health(amount)
        if self.health <= amount:
            if self.place.bees:
                for bee in self.place.bees.copy():
                    bee.reduce_health(self.damage)
        super().reduce_health(amount)
        # END Problem 5

# BEGIN Problem 6
# The WallAnt class
class WallAnt(Ant):
    '''WallAnt does nothing each turn, and it is useful because it has a large health value.'''

    name = 'Wall'
    implemented = True
    food_cost = 4

    def __init__(self, health = 4):
        super().__init__(health)
# END Problem 6

# BEGIN Problem 7
# The HungryAnt Class
class HungryAnt(Ant):
    '''HungryAnt will select a random Bee from its place and deal damage to the Bee equal to 
    the Bee's health, eating it whole. Then a HungryAnt must spend some turns chewing before
    able to eat again.'''

    name = 'Hungry'
    food_cost = 4
    chewing_turns = 3
    implemented = True

    def __init__(self, health = 1):
        super().__init__(health)
        self.turns_to_chew = 0

    def action(self, gamestate):
        if self.turns_to_chew > 0:
            self.turns_to_chew -= 1
        else:
            if self.place.bees:
                target = random_bee(self.place.bees)
                target.reduce_health(target.health)
                self.turns_to_chew = self.chewing_turns
# END Problem 7


class ContainerAnt(Ant):
    """
    ContainerAnt can share a space with other ants by containing them.
    """
    is_container = True

    def __init__(self, health):
        super().__init__(health)
        self.ant_contained = None

    def can_contain(self, other):
        # BEGIN Problem 8a
        "*** YOUR CODE HERE ***"
        if self.ant_contained:
            return False
        if other.is_container:
            return False
        return True
        # END Problem 8a

    def store_ant(self, ant):
        # BEGIN Problem 8a
        "*** YOUR CODE HERE ***"
        self.ant_contained = ant
        # END Problem 8a

    def remove_ant(self, ant):
        if self.ant_contained is not ant:
            assert False, "{} does not contain {}".format(self, ant)
        self.ant_contained = None

    def remove_from(self, place):
        # Special handling for container ants
        if place.ant is self:
            # Container was removed. Contained ant should remain in the game
            place.ant = place.ant.ant_contained
            Insect.remove_from(self, place)
        else:
            # default to normal behavior
            Ant.remove_from(self, place)

    def action(self, gamestate):
        # BEGIN Problem 8a
        "*** YOUR CODE HERE ***"
        if self.ant_contained:
            self.ant_contained.action(gamestate)
        # END Problem 8a


class BodyguardAnt(ContainerAnt):
    """BodyguardAnt provides protection to other Ants."""

    name = 'Bodyguard'
    food_cost = 4
    # OVERRIDE CLASS ATTRIBUTES HERE
    # BEGIN Problem 8c
    implemented = True   # Change to True to view in the GUI
    
    def __init__(self, health = 2):
        super().__init__(health)
    # END Problem 8c

# BEGIN Problem 9
# The TankAnt class
class TankAnt(ContainerAnt):
    '''TankAnt protects an ant in its place and also deals 1 damage to all bees
    in its place each turn'''

    name = 'Tank'
    food_cost = 6
    damage = 1
    implemented = True

    def __init__(self, health = 2):
        super().__init__(health)

    def action(self, gamestate):
        super().action(gamestate)
        for bee in self.place.bees[:]:
            bee.reduce_health(self.damage)
# END Problem 9

class Water(Place):
    """Water is a place that can only hold waterproof insects."""

    def add_insect(self, insect):
        """Add an Insect to this place. If the insect is not waterproof, reduce
        its health to 0."""
        # BEGIN Problem 10
        "*** YOUR CODE HERE ***"
        super().add_insect(insect)
        if not insect.is_waterproof:
            insect.reduce_health(insect.health)
        # END Problem 10

# BEGIN Problem 11
# The ScubaThrower class
class ScubaThrower(ThrowerAnt):
    '''A ScubaThrower should not lose its health when placed in Water.'''

    name = 'Scuba'
    is_waterproof = True
    food_cost = 6
    implemented = True

    def __init__(self, health = 1):
        super().__init__(health)
# END Problem 11


class QueenAnt(ThrowerAnt):
    """QueenAnt boosts the damage of all ants behind her."""

    name = 'Queen'
    food_cost = 7
    # OVERRIDE CLASS ATTRIBUTES HERE
    # BEGIN Problem 12
    implemented = True   # Change to True to view in the GUI
    # END Problem 12
    
    def __init__(self, health = 1):
        super().__init__(health)

    def action(self, gamestate):
        """A queen ant throws a leaf, but also doubles the damage of ants
        in her tunnel.
        """
        # BEGIN Problem 12
        "*** YOUR CODE HERE ***"
        super().action(gamestate)
        current = self.place.exit
        while current:
            if isinstance(current.ant, ContainerAnt):
                current.ant.double()
                if current.ant.ant_contained:
                    current.ant.ant_contained.double()
            elif current.ant:
                current.ant.double()
            current = current.exit
        # END Problem 12

    def reduce_health(self, amount):
        """Reduce health by AMOUNT, and if the QueenAnt has no health
        remaining, signal the end of the game.
        """
        # BEGIN Problem 12
        "*** YOUR CODE HERE ***"
        super().reduce_health(amount)
        if self.health <= 0:
            ants_lose()
        # END Problem 12




############
# Optional #
############

class NinjaAnt(Ant):
    """NinjaAnt does not block the path and damages all bees in its place.
    This class is optional.
    """

    name = 'Ninja'
    damage = 1
    food_cost = 5
    # OVERRIDE CLASS ATTRIBUTES HERE
    blocks_path = False
    # BEGIN Problem Optional 1
    implemented = True   # Change to True to view in the GUI
    # END Problem Optional 1

    def action(self, gamestate):
        # BEGIN Problem Optional 1
        "*** YOUR CODE HERE ***"
        for bee in self.place.bees[:]:
            bee.reduce_health(self.damage)
        # END Problem Optional 1

############
# Statuses #
############


class LaserAnt(ThrowerAnt):
    # This class is optional. Only one test is provided for this class.

    name = 'Laser'
    food_cost = 10
    # OVERRIDE CLASS ATTRIBUTES HERE
    # BEGIN Problem Optional 2
    implemented = True   # Change to True to view in the GUI
    # END Problem Optional 2

    def __init__(self, health=1):
        super().__init__(health)
        self.insects_shot = 0

    def insects_in_front(self):
        # BEGIN Problem Optional 2
        insects = {}
        distance = 0
        current = self.place
        if isinstance(current.ant, ContainerAnt):
            insects[current.ant] = distance
        for bee in current.bees:
            insects[bee] = distance
        while current.entrance:
            current = current.entrance
            distance += 1
            if not current.is_hive:
                if isinstance(current.ant, ContainerAnt):
                    insects[current.ant] = distance
                    if current.ant.ant_contained:
                        insects[current.ant.ant_contained] = distance
                elif current.ant:
                    insects[current.ant] = distance
                for bee in current.bees:
                    insects[bee] = distance
        return insects 
        # END Problem Optional 2

    def calculate_damage(self, distance):
        # BEGIN Problem Optional 2
        damage = 2 - 0.25 * distance
        damage -= 0.0625 * self.insects_shot
        if damage < 0:
            return 0
        return damage
        # END Problem Optional 2

    def action(self, gamestate):
        insects_and_distances = self.insects_in_front()
        for insect, distance in insects_and_distances.items():
            damage = self.calculate_damage(distance)
            insect.reduce_health(damage)
            if damage:
                self.insects_shot += 1
        self.insects_shot = 0


########
# Bees #
########

class Bee(Insect):
    """A Bee moves from place to place, following exits and stinging ants."""

    name = 'Bee'
    damage = 1
    is_waterproof = True

    def sting(self, ant):
        """Attack an ANT, reducing its health by 1."""
        ant.reduce_health(self.damage)

    def move_to(self, place):
        """Move from the Bee's current Place to a new PLACE."""
        self.place.remove_insect(self)
        place.add_insect(self)

    def blocked(self):
        """Return True if this Bee cannot advance to the next Place."""
        # Special handling for NinjaAnt
        # BEGIN Problem Optional 1
        if self.place.ant:
            if self.place.ant.blocks_path:
                return True
        return False
        # END Problem Optional 1

    def action(self, gamestate):
        """A Bee's action stings the Ant that blocks its exit if it is blocked,
        or moves to the exit of its current place otherwise.

        gamestate -- The GameState, used to access game state information.
        """
        destination = self.place.exit

        if self.blocked():
            self.sting(self.place.ant)
        elif self.health > 0 and destination is not None:
            self.move_to(destination)

    def add_to(self, place):
        place.bees.append(self)
        super().add_to( place)

    def remove_from(self, place):
        place.bees.remove(self)
        super().remove_from(place)


class Wasp(Bee):
    """Class of Bee that has higher damage."""
    name = 'Wasp'
    damage = 2


class Boss(Wasp):
    """The leader of the bees. Damage to the boss by any attack is capped.
    """
    name = 'Boss'
    damage_cap = 8

    def reduce_health(self, amount):
        super().reduce_health(min(amount, self.damage_cap))


class Hive(Place):
    """The Place from which the Bees launch their assault.

    assault_plan -- An AssaultPlan; when & where bees enter the colony.
    """
    is_hive = True

    def __init__(self, assault_plan):
        self.name = 'Hive'
        self.assault_plan = assault_plan
        self.bees = []
        for bee in assault_plan.all_bees():
            self.add_insect(bee)
        # The following attributes are always None for a Hive
        self.entrance = None
        self.ant = None
        self.exit = None

    def strategy(self, gamestate):
        exits = [p for p in gamestate.places.values() if p.entrance is self]
        for bee in self.assault_plan.get(gamestate.time, []):
            bee.move_to(random.choice(exits))
            gamestate.active_bees.append(bee)

###################
# Game Components #
###################

class GameState:
    """An ant collective that manages global game state and simulates time.

    Attributes:
    time -- elapsed time
    food -- the colony's available food total
    places -- A list of all places in the colony (including a Hive)
    bee_entrances -- A list of places that bees can enter
    """

    def __init__(self, beehive, ant_types, create_places, dimensions, food=2):
        """Create an GameState for simulating a game.

        Arguments:
        beehive -- a Hive full of bees
        ant_types -- a list of ant classes
        create_places -- a function that creates the set of places
        dimensions -- a pair containing the dimensions of the game layout
        """
        self.time = 0
        self.food = food
        self.beehive = beehive
        self.ant_types = OrderedDict((a.name, a) for a in ant_types)
        self.dimensions = dimensions
        self.active_bees = []
        self.configure(beehive, create_places)

    def configure(self, beehive, create_places):
        """Configure the places in the colony."""
        self.base = AntHomeBase('Ant Home Base')
        self.places = OrderedDict()
        self.bee_entrances = []

        def register_place(place, is_bee_entrance):
            self.places[place.name] = place
            if is_bee_entrance:
                place.entrance = beehive
                self.bee_entrances.append(place)
        register_place(self.beehive, False)
        create_places(self.base, register_place,
                      self.dimensions[0], self.dimensions[1])

    def ants_take_actions(self): # Ask ants to take actions
        for ant in self.ants:
            if ant.health > 0:
                ant.action(self)

    def bees_take_actions(self, num_bees): # Ask bees to take actions
        for bee in self.active_bees[:]:
            if bee.health > 0:
                bee.action(self)
            if bee.health <= 0:
                num_bees -= 1
                self.active_bees.remove(bee)
        if num_bees == 0: # Check if player won
            raise AntsWinException()
        return num_bees

    def simulate(self):
        """Simulate an attack on the ant colony. This is called by the GUI to play the game."""
        num_bees = len(self.bees)
        try:
            while True:
                self.beehive.strategy(self) # Bees invade from hive
                yield None # After yielding, players have time to place ants
                self.ants_take_actions()
                self.time += 1
                yield None # After yielding, wait for throw leaf animation to play, then ask bees to take action
                num_bees = self.bees_take_actions(num_bees)
        except AntsWinException:
            print('All bees are vanquished. You win!')
            yield True
        except AntsLoseException:
            print('The bees reached homebase or the queen ant queen has perished. Please try again :(')
            yield False

    def deploy_ant(self, place_name, ant_type_name):
        """Place an ant if enough food is available.

        This method is called by the current strategy to deploy ants.
        """
        ant_type = self.ant_types[ant_type_name]
        if ant_type.food_cost > self.food:
            print('Not enough food remains to place ' + ant_type.__name__)
        else:
            ant = ant_type()
            self.places[place_name].add_insect(ant)
            self.food -= ant.food_cost
            return ant

    def remove_ant(self, place_name):
        """Remove an Ant from the game."""
        place = self.places[place_name]
        if place.ant is not None:
            place.remove_insect(place.ant)

    @property
    def ants(self):
        return [p.ant for p in self.places.values() if p.ant is not None]

    @property
    def bees(self):
        return [b for p in self.places.values() for b in p.bees]

    @property
    def insects(self):
        return self.ants + self.bees

    def __str__(self):
        status = ' (Food: {0}, Time: {1})'.format(self.food, self.time)
        return str([str(i) for i in self.ants + self.bees]) + status


class AntHomeBase(Place):
    """AntHomeBase at the end of the tunnel, where the queen normally resides."""

    def add_insect(self, insect):
        """Add an Insect to this Place.

        Can't actually add Ants to a AntHomeBase. However, if a Bee attempts to
        enter the AntHomeBase, a AntsLoseException is raised, signaling the end
        of a game.
        """
        assert isinstance(insect, Bee), 'Cannot add {0} to AntHomeBase'
        raise AntsLoseException()


def ants_win():
    """Signal that Ants win."""
    raise AntsWinException()


def ants_lose():
    """Signal that Ants lose."""
    raise AntsLoseException()


def ant_types():
    """Return a list of all implemented Ant classes."""
    all_ant_types = []
    new_types = [Ant]
    while new_types:
        new_types = [t for c in new_types for t in c.__subclasses__()]
        all_ant_types.extend(new_types)
    return [t for t in all_ant_types if t.implemented]


def bee_types():
    """Return a list of all implemented Bee classes."""
    all_bee_types = []
    new_types = [Bee]
    while new_types:
        new_types = [t for c in new_types for t in c.__subclasses__()]
        all_bee_types.extend(new_types)
    return all_bee_types


class GameOverException(Exception):
    """Base game over Exception."""
    pass


class AntsWinException(GameOverException):
    """Exception to signal that the ants win."""
    pass


class AntsLoseException(GameOverException):
    """Exception to signal that the ants lose."""
    pass


###########
# Layouts #
###########


def wet_layout(queen, register_place, tunnels=3, length=9, moat_frequency=3):
    """Register a mix of wet and and dry places."""
    for tunnel in range(tunnels):
        exit = queen
        for step in range(length):
            if moat_frequency != 0 and (step + 1) % moat_frequency == 0:
                exit = Water('water_{0}_{1}'.format(tunnel, step), exit)
            else:
                exit = Place('tunnel_{0}_{1}'.format(tunnel, step), exit)
            register_place(exit, step == length - 1)


def dry_layout(queen, register_place, tunnels=3, length=9):
    """Register dry tunnels."""
    wet_layout(queen, register_place, tunnels, length, 0)


#################
# Assault Plans #
#################

class AssaultPlan(dict):
    """The Bees' plan of attack for the colony.  Attacks come in timed waves.

    An AssaultPlan is a dictionary from times (int) to waves (list of Bees).

    >>> AssaultPlan().add_wave(4, 2)
    {4: [Bee(3, None), Bee(3, None)]}
    """

    def add_wave(self, bee_type, bee_health, time, count):
        """Add a wave at time with count Bees that have the specified health."""
        bees = [bee_type(bee_health) for _ in range(count)]
        self.setdefault(time, []).extend(bees)
        return self

    def all_bees(self):
        """Place all Bees in the beehive and return the list of Bees."""
        return [bee for wave in self.values() for bee in wave]

作者:席尘丶

物联沃分享整理
物联沃-IOTWORD物联网 » CS61A SP24 Project3: Ants Vs. SomeBees

发表回复